Merge pull request #71 from AIOSAI/feat/flow-unified-plan-pipeline
feat(flow): unified plan pipeline — plugin architecture for FPLAN/DPLAN parity
This commit is contained in:
@@ -1,14 +1,63 @@
|
||||
# FLOW Branch-Local Context
|
||||
<!-- Source: src/aipass/flow/.aipass/aipass_local_prompt.md -->
|
||||
# Flow -- Plan Lifecycle Management
|
||||
|
||||
> Auto-created by aipass init. Customize for your branch.
|
||||
Flow is AIPass's unified plan lifecycle system. It creates, tracks, closes, and archives numbered work plans across multiple plan types (FPLAN, DPLAN) via a data-driven plugin architecture.
|
||||
|
||||
## Status: NEEDS CONFIGURATION
|
||||
## Commands
|
||||
|
||||
This file is injected into every AI conversation when working from this branch directory. Configure it with:
|
||||
```bash
|
||||
drone @flow create . "Subject" # FPLAN (default)
|
||||
drone @flow create . "Subject" master # FPLAN master template
|
||||
drone @flow create . "Design topic" dplan # DPLAN
|
||||
drone @flow close FPLAN-0042 # Close specific plan
|
||||
drone @flow close --all # Close all open plans
|
||||
drone @flow list # List open plans (all types)
|
||||
drone @flow list all # List all plans
|
||||
drone @flow restore FPLAN-0042 # Reopen a closed plan
|
||||
```
|
||||
|
||||
- Who this branch is (role, purpose)
|
||||
- Key commands and workflows
|
||||
- Architecture overview
|
||||
- Critical files and operational rules
|
||||
- Integration points with other branches
|
||||
## Architecture
|
||||
|
||||
- `apps/flow.py` -- Entry point. Auto-discovers modules in `apps/modules/` via `handle_command()` convention.
|
||||
- `apps/modules/` -- Thin orchestrators. No business logic. Route to handlers and display results.
|
||||
- `apps/handlers/` -- Implementation. Grouped by domain: `plan/`, `registry/`, `template/`, `dashboard/`, `mbank/`, `summary/`.
|
||||
- `plan_types/` -- Data-only plugins. Each subdirectory has `plan_type.json` config + `templates/` with Markdown templates.
|
||||
- `flow_json/` -- Per-type JSON registries (`fplan_registry.json`, `dplan_registry.json`).
|
||||
|
||||
## Plan Type Plugins
|
||||
|
||||
Plan types are DATA, not code. Each plugin directory under `plan_types/` contains:
|
||||
- `plan_type.json` -- prefix, digits, registry_file, available_templates, default_template
|
||||
- `templates/` -- Markdown plan templates
|
||||
|
||||
Discovered at runtime by `apps/handlers/template/plan_type_loader.py`. Add a new type by creating a new directory with these files.
|
||||
|
||||
| Type | Prefix | Registry File | Templates |
|
||||
|------|--------|---------------|-----------|
|
||||
| flow_plans | FPLAN | fplan_registry.json | default, master |
|
||||
| dev_plans | DPLAN | dplan_registry.json | default |
|
||||
|
||||
## Critical Files
|
||||
|
||||
- `apps/flow.py` -- CLI entry point, module discovery, command routing
|
||||
- `apps/modules/create_plan.py` -- Plan creation orchestrator
|
||||
- `apps/modules/close_plan.py` -- Plan closure orchestrator (async post-processing, archival)
|
||||
- `apps/modules/list_plans.py` -- Multi-registry plan listing
|
||||
- `apps/handlers/plan/list_ops.py` -- Merges plans from all registries for display
|
||||
- `apps/handlers/plan/display.py` -- All formatting functions (prefix-aware)
|
||||
- `apps/handlers/plan/close_ops.py` -- Close implementation (file ops, registry update, vector intake)
|
||||
- `apps/handlers/template/plan_type_loader.py` -- Plugin discovery and config resolution
|
||||
- `apps/handlers/registry/load_registry.py` -- Registry loader (supports per-type registry files)
|
||||
|
||||
## Integration Points
|
||||
|
||||
- **aipass.cli** -- Rich console output (`console`, `header`, `success`, `error`, `warning`)
|
||||
- **aipass.prax** -- System logger
|
||||
- **aipass.memory** -- Vector intake pipeline on plan close
|
||||
- **aipass.trigger** -- Startup events and branch dashboard updates
|
||||
|
||||
## Conventions
|
||||
|
||||
- Modules return `True` from `handle_command()` when the command was recognized (even on failure), `False` only for "not my command".
|
||||
- Plan IDs follow `{PREFIX}-{NNNN}_topic_slug_YYYY-MM-DD.md`.
|
||||
- All file I/O uses `pathlib.Path` and `encoding='utf-8'`.
|
||||
- Handlers are stateless functions; modules inject dependencies.
|
||||
|
||||
+40
-29
@@ -1,31 +1,34 @@
|
||||
# Flow
|
||||
|
||||
**Purpose:** Plan lifecycle management for AIPass. Creates, tracks, closes, and archives numbered work plans (FPLANs) with registry-backed state, async post-processing, and cross-branch aggregation.
|
||||
**Purpose:** Unified plan lifecycle management for AIPass. Creates, tracks, closes, and archives numbered work plans across multiple plan types (FPLAN, DPLAN, etc.) via a plugin architecture. Registry-backed state, async post-processing, vector intake on close, and cross-branch aggregation.
|
||||
**Module:** `aipass.flow`
|
||||
**Created:** 2025-11-15
|
||||
**Last Updated:** 2026-03-08
|
||||
**Last Updated:** 2026-03-17
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
### What I Do
|
||||
- Create numbered FPLANs from templates (default, master, proposal)
|
||||
- Close plans with async post-processing and archival
|
||||
- List and filter plans across branches
|
||||
- Create numbered plans from type-specific templates via `plan_types/` plugins
|
||||
- Unified create/close/list commands for all plan types (FPLAN, DPLAN, ...)
|
||||
- Close plans with async post-processing and archival to `backup/processed_plans/`
|
||||
- Vector processing on close (via `aipass.memory` intake pipeline)
|
||||
- List and filter plans across branches and plan types
|
||||
- Restore plans from backups
|
||||
- Monitor registry health with orphan detection and auto-healing
|
||||
- Aggregate plans across branches
|
||||
- Background post-close processing via `dplan_post_close_runner`
|
||||
- Delegated plan management via `dplan_flow` orchestrator
|
||||
|
||||
## Commands / Usage
|
||||
|
||||
```bash
|
||||
drone @flow create . "Subject" # Create FPLAN in current dir
|
||||
drone @flow create . "Subject" master # Create master plan
|
||||
drone @flow close FPLAN-XXXX # Close a plan
|
||||
drone @flow list # List active plans
|
||||
drone @flow create . "Subject" # Create FPLAN (default)
|
||||
drone @flow create . "Subject" master # Create FPLAN master template
|
||||
drone @flow create . "Design topic" dplan # Create DPLAN
|
||||
drone @flow close FPLAN-0042 # Close an FPLAN
|
||||
drone @flow close DPLAN-0005 # Close a DPLAN
|
||||
drone @flow close --all # Close all open plans
|
||||
drone @flow list # List all plan types
|
||||
drone @flow --help # Full help
|
||||
```
|
||||
|
||||
@@ -37,51 +40,59 @@ drone @flow --help # Full help
|
||||
flow/
|
||||
├── apps/
|
||||
│ ├── flow.py # Entry point (auto-discovers modules)
|
||||
│ ├── modules/ # Business logic
|
||||
│ ├── modules/ # Business logic (thin orchestrators)
|
||||
│ │ ├── create_plan.py # Plan creation with template support
|
||||
│ │ ├── close_plan.py # Closure with async archival
|
||||
│ │ ├── list_plans.py # Plan listing and filtering
|
||||
│ │ ├── restore_plan.py # Plan recovery from backups
|
||||
│ │ ├── registry_monitor.py # Orphan detection, auto-healing
|
||||
│ │ ├── aggregate_central.py # Cross-branch plan aggregation
|
||||
│ │ ├── post_close_runner.py # Background post-processing
|
||||
│ │ ├── dplan_flow.py # Delegated plan management orchestrator
|
||||
│ │ └── dplan_post_close_runner.py # DPLAN-specific post-close runner
|
||||
│ │ └── post_close_runner.py # Background post-processing
|
||||
│ └── handlers/ # Implementation details
|
||||
│ ├── plan/ # Lifecycle, file ops, validation
|
||||
│ ├── plan/ # Lifecycle, file ops, validation, close_ops
|
||||
│ ├── registry/ # Load, save, auto-heal
|
||||
│ ├── template/ # Plan templates (default, master, proposal)
|
||||
│ ├── template/ # Plan type loader + template resolution
|
||||
│ ├── dashboard/ # Status aggregation
|
||||
│ ├── dplan/ # DPLAN handlers (list, create, close, display, etc.)
|
||||
│ ├── mbank/ # Memory bank archival
|
||||
│ └── summary/ # AI-generated plan summaries
|
||||
├── templates/ # Plan template files
|
||||
├── plan_types/ # Plan type plugins (DATA, not code)
|
||||
│ ├── flow_plans/ # FPLAN config + templates (default, master)
|
||||
│ └── dev_plans/ # DPLAN config + templates (default)
|
||||
├── flow_json/ # Per-type registries (fplan_registry.json, dplan_registry.json)
|
||||
├── docs/ # Documentation
|
||||
├── flow_json/ # Configuration and registry data
|
||||
├── .archive/ # Archived legacy code (DPLAN handlers, old templates, old registry)
|
||||
└── tests/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plan Naming
|
||||
## Plan Types (Plugins)
|
||||
|
||||
Plans follow the convention `FPLAN-XXXX_topic_slug_YYYY-MM-DD.md` where XXXX is an auto-incrementing number.
|
||||
Plan types live in `plan_types/` as data-only plugins. Each contains a `plan_type.json` config and a `templates/` directory. No per-type Python code is needed.
|
||||
|
||||
| Type | Prefix | Registry | Description |
|
||||
|------|--------|----------|-------------|
|
||||
| flow_plans | FPLAN | fplan_registry.json | Build/execution plans (default, master templates) |
|
||||
| dev_plans | DPLAN | dplan_registry.json | Design/thinking plans |
|
||||
|
||||
Plans follow the convention `{PREFIX}-{NNNN}_topic_slug_YYYY-MM-DD.md` where NNNN is auto-incrementing per type.
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Depends On
|
||||
- `aipass.cli` — Terminal formatting (console, header, success, error)
|
||||
- `aipass.prax` — Structured logging via `system_logger`
|
||||
- `aipass.trigger` — Error reporting (optional)
|
||||
- `aipass.cli` -- Terminal formatting (console, header, success, error)
|
||||
- `aipass.prax` -- Structured logging via `system_logger`
|
||||
- `aipass.trigger` -- Error reporting (optional)
|
||||
- `aipass.memory` -- Vector intake on plan close (optional, best-effort)
|
||||
- Python stdlib (`pathlib`, `json`, `importlib`, `sys`, `signal`)
|
||||
|
||||
### Provides To
|
||||
- All modules — Plan creation, tracking, closure, and archival
|
||||
- `aipass.devpulse` — Plan status aggregation for system dashboards
|
||||
- Registry: Reads/writes plan registry in `flow_json/`
|
||||
- All modules -- Plan creation, tracking, closure, and archival
|
||||
- `aipass.devpulse` -- Plan status aggregation for system dashboards
|
||||
- Registry: Per-type registries in `flow_json/`
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2026-03-08*
|
||||
*Last Updated: 2026-03-17*
|
||||
|
||||
@@ -232,39 +232,20 @@ def print_help(modules: List[Any]):
|
||||
console.print("─" * 70)
|
||||
console.print()
|
||||
|
||||
console.print("[bold cyan]FPLAN EXAMPLES:[/bold cyan]")
|
||||
console.print("[bold cyan]EXAMPLES:[/bold cyan]")
|
||||
console.print()
|
||||
console.print(" [yellow]Create new FPLAN:[/yellow]")
|
||||
console.print(" [dim]drone @flow create . \"Implementation task\"[/dim]")
|
||||
console.print(" [dim]drone @flow create . \"subject\" master[/dim]")
|
||||
console.print(" [yellow]Create plans:[/yellow]")
|
||||
console.print(" [dim]drone @flow create . \"Implementation task\"[/dim] [dim]# FPLAN (default)[/dim]")
|
||||
console.print(" [dim]drone @flow create . \"subject\" master[/dim] [dim]# FPLAN master template[/dim]")
|
||||
console.print(" [dim]drone @flow create . \"Design topic\" dplan[/dim] [dim]# DPLAN[/dim]")
|
||||
console.print()
|
||||
console.print(" [yellow]Close FPLAN:[/yellow]")
|
||||
console.print(" [yellow]Close plans:[/yellow]")
|
||||
console.print(" [dim]drone @flow close FPLAN-0042[/dim]")
|
||||
console.print(" [dim]drone @flow close DPLAN-0005[/dim]")
|
||||
console.print(" [dim]drone @flow close --all[/dim]")
|
||||
console.print()
|
||||
console.print(" [yellow]List FPLANs:[/yellow]")
|
||||
console.print(" [dim]drone @flow list[/dim]")
|
||||
console.print()
|
||||
console.print("─" * 70)
|
||||
console.print()
|
||||
|
||||
console.print("[bold cyan]DPLAN EXAMPLES:[/bold cyan]")
|
||||
console.print()
|
||||
console.print(" [yellow]Create DPLAN:[/yellow]")
|
||||
console.print(" [dim]drone @flow plan create \"Topic\"[/dim]")
|
||||
console.print()
|
||||
console.print(" [yellow]List DPLANs:[/yellow]")
|
||||
console.print(" [dim]drone @flow plan list[/dim]")
|
||||
console.print(" [dim]drone @flow plan list --tag idea[/dim]")
|
||||
console.print()
|
||||
console.print(" [yellow]Close DPLAN:[/yellow]")
|
||||
console.print(" [dim]drone @flow plan close 42[/dim]")
|
||||
console.print(" [dim]drone @flow plan close --all[/dim]")
|
||||
console.print()
|
||||
console.print(" [yellow]DPLAN status:[/yellow]")
|
||||
console.print(" [dim]drone @flow plan status[/dim]")
|
||||
console.print()
|
||||
console.print(" [yellow]Sync registry:[/yellow]")
|
||||
console.print(" [dim]drone @flow plan sync[/dim]")
|
||||
console.print(" [yellow]List plans:[/yellow]")
|
||||
console.print(" [dim]drone @flow list[/dim] [dim]# All plan types[/dim]")
|
||||
console.print()
|
||||
console.print("─" * 70)
|
||||
console.print()
|
||||
|
||||
@@ -17,7 +17,7 @@ targets the branch where a plan LIVES. Each branch sees its own active plans
|
||||
on its own dashboard.
|
||||
|
||||
Data Flow:
|
||||
1. Read flow_registry.json (source of truth)
|
||||
1. Read fplan_registry.json (source of truth)
|
||||
2. Filter active plans for target branch (by location path)
|
||||
3. Get recently closed plans (last 5, within last 7 days)
|
||||
4. Get total plan count for this branch
|
||||
@@ -51,7 +51,7 @@ _PKG_ROOT = Path(__file__).resolve().parents[4]
|
||||
FLOW_ROOT = _PKG_ROOT / "flow"
|
||||
|
||||
# Registry location
|
||||
REGISTRY_FILE = FLOW_ROOT / "flow_json" / "flow_registry.json"
|
||||
REGISTRY_FILE = FLOW_ROOT / "flow_json" / "fplan_registry.json"
|
||||
|
||||
# Dashboard template path (package-relative)
|
||||
DASHBOARD_TEMPLATE_FILE = _PKG_ROOT / "devpulse" / "templates" / "DASHBOARD.template.json"
|
||||
@@ -192,7 +192,7 @@ def _calculate_quick_status(sections: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
def _load_registry() -> Dict[str, Any]:
|
||||
"""
|
||||
Load flow_registry.json.
|
||||
Load fplan_registry.json.
|
||||
|
||||
Returns:
|
||||
Registry dict or empty structure if unavailable
|
||||
@@ -214,7 +214,7 @@ def _filter_branch_plans(
|
||||
Filter plans for a specific branch from the registry.
|
||||
|
||||
Args:
|
||||
registry: Full flow_registry.json data
|
||||
registry: Full fplan_registry.json data
|
||||
branch_path: Absolute path to the branch directory
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -13,7 +13,7 @@ Pushes Flow's plan data to the central PLANS.central.json file at AI_CENTRAL.
|
||||
This handler follows the 3-tier logging standard (no Prax imports, no logging).
|
||||
|
||||
Features:
|
||||
- Reads flow_registry.json to get Flow's plans
|
||||
- Reads fplan_registry.json to get Flow's plans
|
||||
- Extracts only plans where location='flow' (Flow's own plans)
|
||||
- Updates branches.flow section in PLANS.central.json
|
||||
- Preserves all other branch sections
|
||||
@@ -44,7 +44,7 @@ from aipass.flow.apps.modules.aggregate_central import aggregate_central
|
||||
|
||||
MODULE_NAME = "push_central"
|
||||
FLOW_JSON_DIR = FLOW_ROOT / "flow_json"
|
||||
REGISTRY_FILE = FLOW_JSON_DIR / "flow_registry.json"
|
||||
REGISTRY_FILE = FLOW_JSON_DIR / "fplan_registry.json"
|
||||
def _find_repo_root() -> Path:
|
||||
"""Walk up from this file to find the repo root (contains AIPASS_REGISTRY.json)."""
|
||||
current = Path(__file__).resolve().parent
|
||||
@@ -63,7 +63,7 @@ CENTRAL_FILE = AI_CENTRAL_DIR / "PLANS.central.json"
|
||||
# =============================================
|
||||
|
||||
def _load_registry() -> Dict[str, Any]:
|
||||
"""Load flow_registry.json
|
||||
"""Load fplan_registry.json
|
||||
|
||||
Returns:
|
||||
Registry dict or empty structure if file doesn't exist
|
||||
@@ -82,7 +82,7 @@ def _extract_flow_plans(registry: Dict[str, Any]) -> tuple[List[Dict], List[Dict
|
||||
"""Extract Flow's own plans from registry
|
||||
|
||||
Args:
|
||||
registry: The flow_registry.json data
|
||||
registry: The fplan_registry.json data
|
||||
|
||||
Returns:
|
||||
Tuple of (active_plans, recently_closed_plans)
|
||||
@@ -190,7 +190,7 @@ def push_to_plans_central() -> bool:
|
||||
"""Push Flow's plan data to AI_CENTRAL/PLANS.central.json
|
||||
|
||||
Algorithm:
|
||||
1. Read flow_registry.json
|
||||
1. Read fplan_registry.json
|
||||
2. Extract only plans where location='flow' (Flow's own plans)
|
||||
3. Format for central structure with branch metadata
|
||||
4. Read existing PLANS.central.json if exists
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"""
|
||||
Update Dashboard Local Handler
|
||||
|
||||
Updates Flow's DASHBOARD.local.json file with plan summaries from flow_registry.json.
|
||||
Updates Flow's DASHBOARD.local.json file with plan summaries from fplan_registry.json.
|
||||
|
||||
This handler follows the 3-tier logging standard:
|
||||
- NO Prax imports
|
||||
@@ -24,7 +24,7 @@ Flow's Dual Role:
|
||||
- Key principle: Each branch touches ONLY its own section, respects others
|
||||
|
||||
Data Flow:
|
||||
1. Read flow_registry.json (source of truth)
|
||||
1. Read fplan_registry.json (source of truth)
|
||||
2. Extract Flow's plans only (location='flow')
|
||||
3. Partition into active (status='open') and recently_closed (status='closed', last 5)
|
||||
4. Calculate statistics (active_count, total_closed, next_number)
|
||||
@@ -81,7 +81,7 @@ FLOW_ROOT = _PKG_ROOT / "flow"
|
||||
# CONFIGURATION
|
||||
# =============================================
|
||||
|
||||
REGISTRY_FILE = FLOW_ROOT / "flow_json" / "flow_registry.json"
|
||||
REGISTRY_FILE = FLOW_ROOT / "flow_json" / "fplan_registry.json"
|
||||
DASHBOARD_FILE = FLOW_ROOT / "DASHBOARD.local.json"
|
||||
|
||||
# =============================================
|
||||
@@ -90,7 +90,7 @@ DASHBOARD_FILE = FLOW_ROOT / "DASHBOARD.local.json"
|
||||
|
||||
def _read_registry() -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Read flow_registry.json.
|
||||
Read fplan_registry.json.
|
||||
|
||||
Returns:
|
||||
Registry dict or None if error
|
||||
@@ -253,7 +253,7 @@ def update_dashboard_local() -> bool:
|
||||
Update DASHBOARD.local.json with Flow's plan summaries from registry.
|
||||
|
||||
This is the main handler function that:
|
||||
1. Reads flow_registry.json
|
||||
1. Reads fplan_registry.json
|
||||
2. Extracts Flow's plans (location='flow')
|
||||
3. Partitions into active and closed
|
||||
4. Updates ONLY the 'flow_plans' section of DASHBOARD.local.json
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
# DPLAN Files - Extracted from Dev-Pass
|
||||
|
||||
Extracted from Dev-Pass devpulse on 2026-03-08.
|
||||
|
||||
These files need adaptation for AIPass before use.
|
||||
|
||||
Original imports use `aipass_os.dev_central.devpulse` -- must be converted to `aipass.flow`.
|
||||
|
||||
## Source Location
|
||||
|
||||
```
|
||||
/home/patrick/Projects/Dev-Pass/aipass_os/dev_central/devpulse/
|
||||
```
|
||||
|
||||
## What Was Extracted
|
||||
|
||||
### Handler Files (apps/handlers/dplan/)
|
||||
These were the `apps/handlers/plan/` handlers from Dev-Pass devpulse.
|
||||
In Dev-Pass, the same `plan/` directory handled both DPLANs and FPLANs.
|
||||
Here they are placed under `dplan/` to sit alongside Flow's existing `plan/` (FPLAN) handlers.
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `close.py` | DPLAN close operations (mark complete, archive) |
|
||||
| `counter.py` | Plan numbering (sequential, multi-type DPLAN/BPLAN) |
|
||||
| `create.py` | Plan file creation with template rendering |
|
||||
| `dashboard.py` | DPLAN dashboard integration (counts, central push) |
|
||||
| `display.py` | Help text and introspection |
|
||||
| `list.py` | Plan listing with type/tag/status filters |
|
||||
| `registry.py` | DPLAN registry and summaries (JSON persistence) |
|
||||
| `status.py` | Status extraction from plan files (checkboxes) |
|
||||
| `template.py` | Template loading and rendering (DPLAN + BPLAN) |
|
||||
|
||||
### Module Files (apps/modules/)
|
||||
| File | Original Name | Purpose |
|
||||
|------|---------------|---------|
|
||||
| `dplan_flow.py` | `dev_flow.py` | Main DPLAN orchestrator module (thin orchestrator pattern) |
|
||||
| `dplan_post_close_runner.py` | `post_close_runner.py` | Background post-close processing (Memory Bank archival) |
|
||||
|
||||
### Templates (templates/)
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `dplan_default.md` | Default DPLAN template with sections: Vision, Current State, What Needs Building, Design Decisions, etc. |
|
||||
| `bplan_default.md` | Default BPLAN (business plan) template with sections: Executive Summary, Market Analysis, Revenue Model, etc. |
|
||||
|
||||
### JSON Data (flow_json/)
|
||||
These are reference data files from the Dev-Pass environment. They contain Dev-Pass-specific plan data
|
||||
and should be treated as structural examples, not live data.
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `dplan_registry.json` | Registry of all DPLANs with metadata (47 plans from Dev-Pass) |
|
||||
| `dplan_summaries.json` | Cached AI-generated summaries for closed plans |
|
||||
|
||||
## Key Differences from FPLANs
|
||||
|
||||
- **DPLANs** are design/planning documents (what to build, why, design decisions)
|
||||
- **FPLANs** are build/execution plans (how to build it, steps, acceptance criteria)
|
||||
- **BPLANs** are business plans (market analysis, revenue model, go-to-market)
|
||||
- DPLANs typically transition to FPLANs when "Ready for Execution"
|
||||
|
||||
## Import Conversions Needed
|
||||
|
||||
All files currently use Dev-Pass import patterns that must be changed:
|
||||
|
||||
```python
|
||||
# OLD (Dev-Pass)
|
||||
from aipass_os.dev_central.devpulse.apps.handlers.plan.create import create_plan
|
||||
from prax.apps.modules.logger import system_logger as logger
|
||||
from cli.apps.modules import console, header, success, error
|
||||
|
||||
# NEW (AIPass) -- needs to be determined by Flow
|
||||
from aipass.flow.apps.handlers.dplan.create import create_plan
|
||||
# Logger and CLI imports TBD
|
||||
```
|
||||
|
||||
## Hardcoded Paths to Fix
|
||||
|
||||
Several files reference Dev-Pass paths:
|
||||
- `Path.home() / "aipass_os" / "dev_central" / "dev_planning"` -- plan storage root
|
||||
- `Path.home() / "aipass_core" / "backup_system" / "processed_plans"` -- archive dir
|
||||
- `Path.home() / "aipass_os" / "AI_CENTRAL"` -- central dashboard
|
||||
- `Path.home() / "BRANCH_REGISTRY.json"` -- branch resolution
|
||||
- Shebang lines: `#!/home/aipass/.venv/bin/python3`
|
||||
@@ -1,8 +0,0 @@
|
||||
"""
|
||||
DPLAN Handlers - Extracted from Dev-Pass devpulse on 2026-03-08
|
||||
|
||||
Migration complete (2026-03-10):
|
||||
- All imports converted from aipass_os.dev_central.devpulse to aipass.flow
|
||||
- All data paths converted from ~/aipass_os/ to Path(__file__).parents[N] relative paths
|
||||
- sys.path hacks removed
|
||||
"""
|
||||
@@ -1,83 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: background_spawn.py
|
||||
# Description: Background process spawning handler
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Background Spawn Handler
|
||||
|
||||
Spawns a background process (e.g., post-close runner) in a new session.
|
||||
Extracted from dplan_flow.py to comply with 3-tier architecture
|
||||
(modules must not make direct subprocess calls).
|
||||
|
||||
Usage:
|
||||
from aipass.flow.apps.handlers.dplan.background_spawn import spawn_post_close
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
import io
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Default runner script lives alongside the module that invokes it
|
||||
DEFAULT_RUNNER = Path(__file__).parents[2] / "modules" / "post_close_runner.py"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# OPERATIONS
|
||||
# =============================================================================
|
||||
|
||||
def spawn_post_close(
|
||||
runner_path: Optional[Path] = None,
|
||||
log_file_handle: Optional[io.TextIOWrapper] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Spawn the post-close background runner in a detached session.
|
||||
|
||||
Args:
|
||||
runner_path: Path to the runner script. Defaults to
|
||||
apps/modules/post_close_runner.py.
|
||||
log_file_handle: Open file handle for stdout/stderr.
|
||||
If None, output is discarded (DEVNULL).
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
success (bool): Whether the process was spawned
|
||||
pid (Optional[int]): PID of the spawned process, or None on failure
|
||||
runner (str): Path to the runner script that was invoked
|
||||
error (str): Error description if failed, empty string on success
|
||||
"""
|
||||
script = runner_path or DEFAULT_RUNNER
|
||||
|
||||
stdout_target = log_file_handle if log_file_handle else subprocess.DEVNULL
|
||||
stderr_target = log_file_handle if log_file_handle else subprocess.DEVNULL
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, str(script)],
|
||||
stdout=stdout_target,
|
||||
stderr=stderr_target,
|
||||
start_new_session=True,
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"pid": proc.pid,
|
||||
"runner": str(script),
|
||||
"error": "",
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"pid": None,
|
||||
"runner": str(script),
|
||||
"error": f"Failed to spawn background process: {e}",
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: branch_resolve.py
|
||||
# Description: @ branch reference resolution
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Branch Resolution Handler
|
||||
|
||||
Resolves @branch references to filesystem paths via BRANCH_REGISTRY.json.
|
||||
Extracted from dplan_flow.py to comply with 3-tier architecture.
|
||||
|
||||
Usage:
|
||||
from aipass.flow.apps.handlers.dplan.branch_resolve import resolve_branch_target
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
BRANCH_REGISTRY_PATH = Path.home() / "BRANCH_REGISTRY.json"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# OPERATIONS
|
||||
# =============================================================================
|
||||
|
||||
def resolve_branch_target(branch_ref: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Resolve @branch reference to a filesystem path via BRANCH_REGISTRY.json.
|
||||
|
||||
Args:
|
||||
branch_ref: Branch reference like "@vera" or "@team_1"
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
success (bool): Whether resolution succeeded
|
||||
path (Optional[Path]): Resolved path, or None on failure
|
||||
error (str): Error description if failed, empty string on success
|
||||
"""
|
||||
name = branch_ref.lstrip("@").upper()
|
||||
|
||||
if not BRANCH_REGISTRY_PATH.exists():
|
||||
return {
|
||||
"success": False,
|
||||
"path": None,
|
||||
"error": f"BRANCH_REGISTRY.json not found at {BRANCH_REGISTRY_PATH}",
|
||||
}
|
||||
|
||||
try:
|
||||
data = json.loads(BRANCH_REGISTRY_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
for branch in data.get("branches", []):
|
||||
if branch.get("name", "").upper() == name:
|
||||
branch_path = Path(branch["path"])
|
||||
if branch_path.exists():
|
||||
return {
|
||||
"success": True,
|
||||
"path": branch_path,
|
||||
"error": "",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"path": None,
|
||||
"error": f"Branch path does not exist: {branch_path}",
|
||||
}
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"path": None,
|
||||
"error": f"Branch '{name}' not found in registry",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"path": None,
|
||||
"error": f"Failed to read branch registry: {e}",
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: close.py
|
||||
# Description: D-PLAN close handler
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-02-18
|
||||
# Modified: 2026-02-18
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Close Handler - D-PLAN Close Operations
|
||||
|
||||
Validates, marks as closed, and archives DPLAN files.
|
||||
Adapted from Flow's close system for single-user DPLANs.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Tuple, Optional, List
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
from .status import extract_status
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# close.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning"
|
||||
PROCESSED_PLANS_DIR = FLOW_ROOT / "processed_plans"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PLAN RESOLUTION
|
||||
# =============================================================================
|
||||
|
||||
def normalize_plan_number(plan_input: str) -> Tuple[int, str]:
|
||||
"""
|
||||
Normalize plan number from various input formats.
|
||||
|
||||
Accepts: "1", "001", "42", "DPLAN-001", "DPLAN-42"
|
||||
|
||||
Args:
|
||||
plan_input: User-provided plan identifier
|
||||
|
||||
Returns:
|
||||
Tuple of (plan_number_int, error_message)
|
||||
Error is empty string on success.
|
||||
"""
|
||||
cleaned = plan_input.strip()
|
||||
|
||||
# Strip DPLAN- prefix if present
|
||||
if cleaned.upper().startswith("DPLAN-"):
|
||||
cleaned = cleaned[6:]
|
||||
|
||||
# Extract numeric portion
|
||||
try:
|
||||
num = int(cleaned)
|
||||
return (num, "")
|
||||
except ValueError:
|
||||
return (0, f"Invalid plan number: '{plan_input}'. Expected a number or DPLAN-XXX format.")
|
||||
|
||||
|
||||
def find_plan_file(plan_num: int) -> Optional[Path]:
|
||||
"""
|
||||
Find a DPLAN file by its number.
|
||||
|
||||
Scans dev_planning/ root for matching DPLAN-XXX files.
|
||||
|
||||
Args:
|
||||
plan_num: Plan number to find
|
||||
|
||||
Returns:
|
||||
Path to plan file or None if not found
|
||||
"""
|
||||
if not DEV_PLANNING_ROOT.exists():
|
||||
return None
|
||||
|
||||
# Match DPLAN-XXX where XXX matches plan_num (any zero-padding)
|
||||
for plan_file in DEV_PLANNING_ROOT.glob("DPLAN-*.md"):
|
||||
match = re.match(r"DPLAN-(\d+)", plan_file.name)
|
||||
if match and int(match.group(1)) == plan_num:
|
||||
return plan_file
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_open_plans() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all plans that are not complete or abandoned.
|
||||
|
||||
Returns:
|
||||
List of dicts with keys: number, file, topic, status
|
||||
"""
|
||||
plans = []
|
||||
|
||||
if not DEV_PLANNING_ROOT.exists():
|
||||
return plans
|
||||
|
||||
for plan_file in DEV_PLANNING_ROOT.glob("DPLAN-*.md"):
|
||||
match = re.match(r"DPLAN-(\d+)_(.+)_(\d{4}-\d{2}-\d{2})\.md", plan_file.name)
|
||||
if match:
|
||||
num = int(match.group(1))
|
||||
topic = match.group(2).replace('_', ' ')
|
||||
status = extract_status(plan_file)
|
||||
|
||||
if status not in ("complete", "abandoned"):
|
||||
plans.append({
|
||||
"number": num,
|
||||
"file": plan_file,
|
||||
"topic": topic,
|
||||
"status": status
|
||||
})
|
||||
|
||||
plans.sort(key=lambda x: x["number"])
|
||||
return plans
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CLOSE OPERATIONS
|
||||
# =============================================================================
|
||||
|
||||
def mark_as_closed(plan_file: Path) -> Tuple[bool, str]:
|
||||
"""
|
||||
Update the status checkbox in the plan file to Complete.
|
||||
|
||||
Changes:
|
||||
- [x] Planning/In Progress/Ready → unchecks
|
||||
- [ ] Complete → [x] Complete
|
||||
|
||||
Args:
|
||||
plan_file: Path to the plan file
|
||||
|
||||
Returns:
|
||||
Tuple of (success, error_message)
|
||||
"""
|
||||
try:
|
||||
content = plan_file.read_text(encoding='utf-8')
|
||||
|
||||
# Uncheck all currently checked statuses
|
||||
content = re.sub(r'- \[x\] (Planning)', r'- [ ] \1', content, flags=re.IGNORECASE)
|
||||
content = re.sub(r'- \[x\] (In Progress)', r'- [ ] \1', content, flags=re.IGNORECASE)
|
||||
content = re.sub(r'- \[x\] (Ready for Execution)', r'- [ ] \1', content, flags=re.IGNORECASE)
|
||||
|
||||
# Check Complete
|
||||
content = re.sub(r'- \[ \] (Complete)', r'- [x] \1', content, flags=re.IGNORECASE)
|
||||
|
||||
plan_file.write_text(content, encoding='utf-8')
|
||||
return (True, "")
|
||||
|
||||
except Exception as e:
|
||||
return (False, f"Failed to update status checkbox: {e}")
|
||||
|
||||
|
||||
def archive_plan(plan_file: Path) -> Tuple[bool, str]:
|
||||
"""
|
||||
Move closed plan file to processed_plans/ directory.
|
||||
|
||||
Verification: Returns True ONLY if file successfully moved AND verified.
|
||||
|
||||
Args:
|
||||
plan_file: Path to the plan file
|
||||
|
||||
Returns:
|
||||
Tuple of (success, error_message)
|
||||
"""
|
||||
try:
|
||||
PROCESSED_PLANS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
destination = PROCESSED_PLANS_DIR / plan_file.name
|
||||
|
||||
# Handle duplicate names by appending timestamp
|
||||
if destination.exists():
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime("%H%M%S")
|
||||
stem = destination.stem
|
||||
suffix = destination.suffix
|
||||
destination = PROCESSED_PLANS_DIR / f"{stem}_{timestamp}{suffix}"
|
||||
|
||||
source_path = Path(plan_file)
|
||||
plan_file.rename(destination)
|
||||
|
||||
# Verification
|
||||
if not destination.exists():
|
||||
return (False, "Move verification failed: destination not found")
|
||||
if source_path.exists():
|
||||
return (False, "Move verification failed: source still exists")
|
||||
|
||||
return (True, "")
|
||||
|
||||
except Exception as e:
|
||||
return (False, f"Failed to archive plan: {e}")
|
||||
|
||||
|
||||
def close_plan(plan_num: int) -> Tuple[bool, Dict[str, Any], str]:
|
||||
"""
|
||||
Close a single DPLAN: validate, mark status, return info for archival.
|
||||
|
||||
Does NOT archive or process Memory Bank — that's done by post_close_runner.
|
||||
This function marks the plan as closed so the background runner can pick it up.
|
||||
|
||||
Args:
|
||||
plan_num: Plan number to close
|
||||
|
||||
Returns:
|
||||
Tuple of (success, result_data, error_message)
|
||||
result_data has keys: plan_file, plan_num, topic, old_status
|
||||
"""
|
||||
# Find plan file
|
||||
plan_file = find_plan_file(plan_num)
|
||||
if plan_file is None:
|
||||
return (False, {}, f"DPLAN-{plan_num:03d} not found in {DEV_PLANNING_ROOT}")
|
||||
|
||||
# Check current status
|
||||
current_status = extract_status(plan_file)
|
||||
if current_status == "complete":
|
||||
return (False, {}, f"DPLAN-{plan_num:03d} is already marked as complete")
|
||||
if current_status == "abandoned":
|
||||
return (False, {}, f"DPLAN-{plan_num:03d} is already abandoned")
|
||||
|
||||
# Extract topic from filename
|
||||
match = re.match(r"DPLAN-\d+_(.+)_\d{4}-\d{2}-\d{2}\.md", plan_file.name)
|
||||
topic = match.group(1).replace('_', ' ') if match else plan_file.stem
|
||||
|
||||
# Mark as closed (update checkbox)
|
||||
ok, err = mark_as_closed(plan_file)
|
||||
if not ok:
|
||||
return (False, {}, err)
|
||||
|
||||
return (True, {
|
||||
"plan_file": plan_file,
|
||||
"plan_num": plan_num,
|
||||
"topic": topic,
|
||||
"old_status": current_status
|
||||
}, "")
|
||||
@@ -1,100 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: closed_plans_registry.py
|
||||
# Description: CLOSED_PLANS.local.json file operations
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Closed Plans Registry Handler
|
||||
|
||||
Manages appending closed DPLAN entries to CLOSED_PLANS.local.json.
|
||||
Extracted from dplan_flow.py to comply with 3-tier architecture:
|
||||
modules orchestrate, handlers implement file I/O.
|
||||
|
||||
Usage:
|
||||
from aipass.flow.apps.handlers.dplan.closed_plans_registry import append_closed_dplan
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# closed_plans_registry.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
CLOSED_PLANS_PATH = FLOW_ROOT / "CLOSED_PLANS.local.json"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# OPERATIONS
|
||||
# =============================================================================
|
||||
|
||||
def append_closed_dplan(plan_num: int, topic: str, location: str = "dev_central") -> Dict[str, Any]:
|
||||
"""
|
||||
Append a closed DPLAN entry to CLOSED_PLANS.local.json.
|
||||
|
||||
Duplicate-safe: skips if plan_id already exists.
|
||||
|
||||
Args:
|
||||
plan_num: Plan number (integer)
|
||||
topic: Plan topic/subject string
|
||||
location: Location identifier (default: "dev_central")
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
success (bool): Whether the operation succeeded
|
||||
action (str): "appended", "duplicate_skipped", or "error"
|
||||
plan_id (str): The DPLAN-XXX identifier
|
||||
error (str): Error message if failed, empty string on success
|
||||
"""
|
||||
plan_id = f"DPLAN-{plan_num:03d}"
|
||||
entry = {
|
||||
"plan_id": plan_id,
|
||||
"type": "DPLAN",
|
||||
"subject": topic,
|
||||
"date_closed": datetime.now().strftime("%Y-%m-%d"),
|
||||
"location": location,
|
||||
}
|
||||
|
||||
try:
|
||||
if CLOSED_PLANS_PATH.exists():
|
||||
data = json.loads(CLOSED_PLANS_PATH.read_text(encoding="utf-8"))
|
||||
else:
|
||||
data = {"closed_plans": []}
|
||||
|
||||
# Duplicate check
|
||||
if any(p.get("plan_id") == plan_id for p in data.get("closed_plans", [])):
|
||||
return {
|
||||
"success": True,
|
||||
"action": "duplicate_skipped",
|
||||
"plan_id": plan_id,
|
||||
"error": "",
|
||||
}
|
||||
|
||||
data["closed_plans"].insert(0, entry)
|
||||
CLOSED_PLANS_PATH.write_text(
|
||||
json.dumps(data, indent=2, ensure_ascii=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"action": "appended",
|
||||
"plan_id": plan_id,
|
||||
"error": "",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"action": "error",
|
||||
"plan_id": plan_id,
|
||||
"error": str(e),
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: counter.py
|
||||
# Description: Plan counter management
|
||||
# Version: 2.0.0
|
||||
# Created: 2025-12-02
|
||||
# Modified: 2025-12-02
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Counter Handler - Plan Numbering
|
||||
|
||||
Manages sequential plan numbers by scanning existing files.
|
||||
Supports multiple plan types (DPLAN, BPLAN) with separate sequences.
|
||||
Counter file is a cache, not source of truth.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# counter.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning"
|
||||
COUNTER_FILE = DEV_PLANNING_ROOT / "counter.json"
|
||||
|
||||
VALID_PLAN_TYPES = {"dplan": "DPLAN", "bplan": "BPLAN"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HANDLER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
def get_next_plan_number(
|
||||
plan_type: str = "DPLAN",
|
||||
planning_root: Path | None = None
|
||||
) -> Tuple[int, str]:
|
||||
"""
|
||||
Get next plan number for a given plan type.
|
||||
|
||||
Strategy: Scan files for highest number with matching prefix, increment by 1.
|
||||
Counter file is cache, not source of truth.
|
||||
|
||||
Args:
|
||||
plan_type: Plan prefix (DPLAN, BPLAN). Case-insensitive, normalized to upper.
|
||||
planning_root: Override directory to scan. Defaults to DEV_PLANNING_ROOT.
|
||||
|
||||
Returns:
|
||||
Tuple of (next_number, error_message)
|
||||
Error message is empty on success
|
||||
"""
|
||||
plan_type = plan_type.upper()
|
||||
root = planning_root or DEV_PLANNING_ROOT
|
||||
|
||||
# Scan existing plans to find highest number for this type
|
||||
highest = 0
|
||||
|
||||
if root.exists():
|
||||
for plan_file in root.glob(f"{plan_type}-*.md"):
|
||||
match = re.match(rf"{plan_type}-(\d+)", plan_file.name)
|
||||
if match:
|
||||
num = int(match.group(1))
|
||||
if num > highest:
|
||||
highest = num
|
||||
|
||||
next_num = highest + 1
|
||||
|
||||
# Update counter cache (best effort, return error for logging by module)
|
||||
cache_error = ""
|
||||
try:
|
||||
counter_file = root / "counter.json"
|
||||
counter_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Load existing counter data
|
||||
counter_data = {}
|
||||
if counter_file.exists():
|
||||
try:
|
||||
with open(counter_file, 'r', encoding='utf-8') as f:
|
||||
counter_data = json.load(f)
|
||||
except Exception:
|
||||
counter_data = {}
|
||||
|
||||
# Update per-type counter
|
||||
counter_data[plan_type] = {"next_number": next_num + 1}
|
||||
|
||||
# Backwards compat: also set top-level next_number for DPLAN
|
||||
if plan_type == "DPLAN":
|
||||
counter_data["next_number"] = next_num + 1
|
||||
|
||||
with open(counter_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(counter_data, f, indent=2)
|
||||
except Exception as e:
|
||||
cache_error = f"Cache update failed: {e}"
|
||||
|
||||
# Return number even if cache failed (cache is not critical)
|
||||
return next_num, cache_error
|
||||
@@ -1,136 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: create.py
|
||||
# Description: Plan creation handler
|
||||
# Version: 2.0.0
|
||||
# Created: 2025-12-02
|
||||
# Modified: 2025-12-02
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Create Handler - Plan File Creation
|
||||
|
||||
Creates new plan files (DPLAN, BPLAN) with proper naming and content.
|
||||
Supports @ branch resolution via target_path parameter.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Tuple, Dict, Any
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
from .counter import get_next_plan_number, VALID_PLAN_TYPES
|
||||
from .template import render_template
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# create.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HANDLER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
def create_plan(
|
||||
topic: str,
|
||||
tag: str = "idea",
|
||||
plan_type: str = "dplan",
|
||||
target_path: Path | None = None,
|
||||
subdir: str | None = None
|
||||
) -> Tuple[bool, Dict[str, Any], str]:
|
||||
"""
|
||||
Create a new plan file.
|
||||
|
||||
Args:
|
||||
topic: Topic name for the plan
|
||||
tag: Plan tag classification (default: idea)
|
||||
plan_type: Plan type - dplan or bplan (default: dplan)
|
||||
target_path: Branch path for @ resolution (creates dev_planning/ there).
|
||||
None defaults to dev_central/dev_planning/.
|
||||
subdir: Optional subdirectory within dev_planning/
|
||||
|
||||
Returns:
|
||||
Tuple of (success, result_data, error_message)
|
||||
result_data contains: plan_number, filename, path, topic, tag, plan_type, date, subdir
|
||||
"""
|
||||
if not topic or not topic.strip():
|
||||
return False, {}, "Topic is required"
|
||||
|
||||
topic = topic.strip()
|
||||
|
||||
# Validate plan type
|
||||
plan_type_lower = plan_type.lower()
|
||||
if plan_type_lower not in VALID_PLAN_TYPES:
|
||||
valid = ", ".join(VALID_PLAN_TYPES.keys())
|
||||
return False, {}, f"Invalid plan type '{plan_type}'. Valid types: {valid}"
|
||||
|
||||
prefix = VALID_PLAN_TYPES[plan_type_lower]
|
||||
|
||||
# Determine planning root
|
||||
if target_path:
|
||||
planning_root = target_path / "dev_planning"
|
||||
else:
|
||||
planning_root = DEV_PLANNING_ROOT
|
||||
|
||||
# Sanitize topic for filename (snake_case)
|
||||
topic_slug = re.sub(r'[^\w\s-]', '', topic.lower())
|
||||
topic_slug = re.sub(r'[\s-]+', '_', topic_slug)
|
||||
topic_slug = topic_slug[:40] # Limit length
|
||||
|
||||
# Determine target directory
|
||||
if subdir:
|
||||
# Sanitize subdir name (alphanumeric and underscore only)
|
||||
subdir = re.sub(r'[^\w-]', '', subdir.strip())
|
||||
if not subdir:
|
||||
return False, {}, "Invalid subdirectory name"
|
||||
target_dir = planning_root / subdir
|
||||
else:
|
||||
target_dir = planning_root
|
||||
|
||||
# Get next number for this plan type in this directory
|
||||
plan_number, cache_err = get_next_plan_number(
|
||||
plan_type=prefix,
|
||||
planning_root=planning_root
|
||||
)
|
||||
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
# Build filename: PREFIX-XXX_topic_name_YYYY-MM-DD.md
|
||||
filename = f"{prefix}-{plan_number:03d}_{topic_slug}_{date_str}.md"
|
||||
plan_path = target_dir / filename
|
||||
|
||||
# Render template
|
||||
content, template_err = render_template(
|
||||
plan_number, topic, date_str, tag=tag, plan_type=plan_type_lower
|
||||
)
|
||||
if template_err:
|
||||
return False, {}, f"Failed to render template: {template_err}"
|
||||
|
||||
# Create file
|
||||
try:
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
plan_path.write_text(content, encoding='utf-8')
|
||||
|
||||
result = {
|
||||
"plan_number": plan_number,
|
||||
"filename": filename,
|
||||
"path": str(plan_path),
|
||||
"topic": topic,
|
||||
"tag": tag,
|
||||
"plan_type": plan_type_lower,
|
||||
"prefix": prefix,
|
||||
"date": date_str,
|
||||
"subdir": subdir,
|
||||
"target_branch": str(target_path) if target_path else None,
|
||||
"cache_warning": cache_err
|
||||
}
|
||||
|
||||
return True, result, ""
|
||||
|
||||
except Exception as e:
|
||||
return False, {}, f"Failed to write file: {e}"
|
||||
@@ -1,196 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: dashboard.py
|
||||
# Description: DPLAN Dashboard Push Handler
|
||||
# Version: 2.0.0
|
||||
# Created: 2026-02-25
|
||||
# Modified: 2026-02-25
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Dashboard Handler - DPLAN Dashboard Integration
|
||||
|
||||
Computes enriched DPLAN summary data. The module layer injects
|
||||
the write_section function to push to DASHBOARD.local.json (handler
|
||||
independence pattern). Central push is handled directly here.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, Callable
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
from .registry import load_registry
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# dashboard.py → dplan/ → handlers/ → apps/ → flow/ → aipass/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
AIPASS_ROOT = Path(__file__).resolve().parents[4]
|
||||
DEVPULSE_ROOT = AIPASS_ROOT / "devpulse"
|
||||
CENTRAL_FILE = DEVPULSE_ROOT / "DEVPULSE.central.json"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HANDLER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
def compute_dplan_summary(activity: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Compute enriched DPLAN summary from registry.
|
||||
|
||||
Args:
|
||||
activity: Optional recent activity string
|
||||
(e.g. "DPLAN-036 created (dashboard_overhaul)")
|
||||
|
||||
Returns:
|
||||
Dashboard section dict with managed_by, dplan_counts, recent_activity
|
||||
"""
|
||||
registry = load_registry()
|
||||
plans = registry.get("plans", {})
|
||||
|
||||
by_status: Dict[str, int] = {}
|
||||
|
||||
for plan in plans.values():
|
||||
status = plan.get("status", "unknown")
|
||||
by_status[status] = by_status.get(status, 0) + 1
|
||||
|
||||
# Derive recent_activity from registry if not provided
|
||||
if not activity:
|
||||
activity = _derive_recent_activity(plans)
|
||||
|
||||
return {
|
||||
"managed_by": "devpulse",
|
||||
"dplan_counts": {
|
||||
"total": len(plans),
|
||||
"by_status": by_status
|
||||
},
|
||||
"recent_activity": activity
|
||||
}
|
||||
|
||||
|
||||
def _derive_recent_activity(plans: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Derive a recent_activity string from the most recently updated plan.
|
||||
|
||||
Args:
|
||||
plans: Registry plans dict
|
||||
|
||||
Returns:
|
||||
Activity string like "DPLAN-036 updated (dashboard_overhaul)"
|
||||
"""
|
||||
if not plans:
|
||||
return ""
|
||||
|
||||
# Find plan with most recent last_updated timestamp
|
||||
most_recent = None
|
||||
most_recent_ts = ""
|
||||
|
||||
for plan in plans.values():
|
||||
ts = plan.get("last_updated", "")
|
||||
if ts > most_recent_ts:
|
||||
most_recent_ts = ts
|
||||
most_recent = plan
|
||||
|
||||
if most_recent:
|
||||
num = most_recent.get("number", 0)
|
||||
topic = most_recent.get("topic", "unknown")
|
||||
short_topic = topic[:30].replace(" ", "_").lower()
|
||||
status = most_recent.get("status", "unknown")
|
||||
return f"DPLAN-{num:03d} {status} ({short_topic})"
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def push_dplan_to_dashboard(
|
||||
summary: Dict[str, Any],
|
||||
write_fn: Optional[Callable] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Update devpulse's own DASHBOARD.local.json.
|
||||
|
||||
Uses injected write_fn (write_section from module layer) for handler
|
||||
independence. Falls back to direct JSON write if no write_fn provided.
|
||||
|
||||
Args:
|
||||
summary: DPLAN section data from compute_dplan_summary()
|
||||
write_fn: Callable(branch_path, section_name, section_data) -> bool.
|
||||
Injected by module layer (write_section from dashboard operations).
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
if write_fn:
|
||||
return write_fn(DEVPULSE_ROOT, "devpulse", summary)
|
||||
|
||||
# Fallback: direct write (backward compatibility)
|
||||
dashboard_file = DEVPULSE_ROOT / "DASHBOARD.local.json"
|
||||
if not dashboard_file.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(dashboard_file, 'r', encoding='utf-8') as f:
|
||||
dashboard = json.load(f)
|
||||
|
||||
dashboard.setdefault("sections", {})
|
||||
summary["last_updated"] = datetime.now().isoformat()
|
||||
dashboard["sections"]["devpulse"] = summary
|
||||
dashboard["last_updated"] = datetime.now().isoformat()
|
||||
|
||||
with open(dashboard_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(dashboard, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def push_dplan_to_central(summary: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Add DPLAN counts to DEVPULSE.central.json alongside branch summaries.
|
||||
|
||||
Args:
|
||||
summary: DPLAN section data from compute_dplan_summary()
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
if not CENTRAL_FILE.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(CENTRAL_FILE, 'r', encoding='utf-8') as f:
|
||||
central = json.load(f)
|
||||
|
||||
central["dplan_summary"] = summary
|
||||
central["last_updated"] = datetime.now().isoformat()
|
||||
|
||||
with open(CENTRAL_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(central, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def push_all(
|
||||
activity: Optional[str] = None,
|
||||
write_fn: Optional[Callable] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Compute DPLAN summary and push to both dashboard and central.
|
||||
|
||||
Args:
|
||||
activity: Optional recent activity string for dashboard display
|
||||
write_fn: Optional write_section callable injected by module layer
|
||||
|
||||
Returns:
|
||||
The computed summary dict
|
||||
"""
|
||||
summary = compute_dplan_summary(activity=activity)
|
||||
push_dplan_to_dashboard(summary, write_fn=write_fn)
|
||||
push_dplan_to_central(summary)
|
||||
return summary
|
||||
@@ -1,144 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: display.py
|
||||
# Description: Plan display handler
|
||||
# Version: 2.0.0
|
||||
# Created: 2025-12-02
|
||||
# Modified: 2025-12-02
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Display Handler - D-PLAN Help and Introspection
|
||||
|
||||
Provides help text and introspection information.
|
||||
"""
|
||||
|
||||
# INFRASTRUCTURE IMPORT PATTERN
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# display.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
TEMPLATE_FILE = FLOW_ROOT / "templates" / "dplan_default.md"
|
||||
|
||||
HELP_TEXT = """
|
||||
[bold]USAGE:[/bold]
|
||||
drone @flow plan <subcommand> [options]
|
||||
|
||||
[bold]SUBCOMMANDS:[/bold]
|
||||
create "topic" [options] - Create new plan document
|
||||
list [--type type] [--tag tag] [--status status] - List plans (with filters)
|
||||
status [--type type] - Quick overview of plan counts
|
||||
close <number> - Close plan and archive
|
||||
close --all - Close all open plans
|
||||
sync - Refresh registry from filesystem
|
||||
|
||||
[bold]PLAN TYPES:[/bold]
|
||||
dplan - Development plans (default)
|
||||
bplan - Business plans
|
||||
|
||||
[bold]EXAMPLES:[/bold]
|
||||
drone @flow plan create "new feature design"
|
||||
drone @flow plan create "API upgrade" --tag upgrade
|
||||
drone @flow plan create "revenue model" --type bplan
|
||||
drone @flow plan create "vera improvements" --type dplan @vera
|
||||
drone @flow plan list
|
||||
drone @flow plan list --type bplan
|
||||
drone @flow plan list --tag idea
|
||||
drone @flow plan list --status planning
|
||||
drone @flow plan status
|
||||
drone @flow plan status --type dplan
|
||||
drone @flow plan close 3
|
||||
drone @flow plan close --all
|
||||
|
||||
[bold]@ RESOLUTION:[/bold]
|
||||
Append @branch to create plans in another branch's dev_planning/:
|
||||
plan create "topic" @vera → creates in vera/dev_planning/
|
||||
plan create "topic" @team_1 → creates in team_1/dev_planning/
|
||||
|
||||
[bold]TAGS:[/bold]
|
||||
idea, upgrade, proposal, bug, research, seed, infrastructure
|
||||
|
||||
[bold]STATUS VALUES:[/bold]
|
||||
📋 Planning - Initial state
|
||||
🔄 In Progress - Actively working on design
|
||||
✅ Ready - Ready for execution (send to Flow)
|
||||
✓ Complete - Design work done
|
||||
❌ Abandoned - No longer pursuing
|
||||
|
||||
[bold]OPTIONS:[/bold]
|
||||
--help - Show this help message
|
||||
--type <type> - Plan type: dplan (default), bplan
|
||||
--tag <tag> - Filter by tag (list) or set tag (create)
|
||||
--status <s> - Filter by status (list only)
|
||||
--dir <name> - Create in dev_planning/<name>/ subdirectory
|
||||
@<branch> - Target branch for plan creation
|
||||
"""
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HANDLER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
def get_help_text() -> str:
|
||||
"""
|
||||
Get help information text
|
||||
|
||||
Returns:
|
||||
Formatted help text string (Rich markup)
|
||||
"""
|
||||
return HELP_TEXT
|
||||
|
||||
|
||||
def show_help() -> str:
|
||||
"""
|
||||
Get formatted help content for display
|
||||
|
||||
Returns:
|
||||
Help text string (caller should use CLI header + print)
|
||||
"""
|
||||
return get_help_text()
|
||||
|
||||
|
||||
def get_introspection_data() -> dict:
|
||||
"""
|
||||
Get module introspection data
|
||||
|
||||
Returns:
|
||||
Dictionary with configuration info
|
||||
"""
|
||||
return {
|
||||
"name": "D-PLAN Management Module",
|
||||
"description": "Manages numbered planning documents in dev_planning/",
|
||||
"template_file": str(TEMPLATE_FILE)
|
||||
}
|
||||
|
||||
|
||||
def print_introspection() -> str:
|
||||
"""
|
||||
Get introspection display text
|
||||
|
||||
Returns:
|
||||
Formatted introspection text (caller handles output)
|
||||
"""
|
||||
data = get_introspection_data()
|
||||
|
||||
lines = [
|
||||
"",
|
||||
"[bold cyan]D-PLAN Management Module[/bold cyan]",
|
||||
"",
|
||||
f"[dim]{data['description']}[/dim]",
|
||||
"",
|
||||
"[yellow]Configuration:[/yellow]",
|
||||
f" [dim]Template:[/dim] {data['template_file']}",
|
||||
"",
|
||||
"[dim]Run 'drone @flow plan --help' for usage[/dim]",
|
||||
""
|
||||
]
|
||||
|
||||
return "\n".join(lines)
|
||||
@@ -1,91 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: list.py
|
||||
# Description: Plan listing handler
|
||||
# Version: 2.0.0
|
||||
# Created: 2025-12-02
|
||||
# Modified: 2025-12-02
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
List Handler - Plan Listing
|
||||
|
||||
Collects and returns plan data for display. Supports multiple plan types.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Tuple
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
from .status import extract_status, extract_tag, extract_description
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# list.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning"
|
||||
|
||||
# Regex matches any plan type: DPLAN-001_topic_2026-02-19.md, BPLAN-001_topic_2026-02-19.md
|
||||
PLAN_FILENAME_PATTERN = re.compile(r"([A-Z]+PLAN)-(\d+)_(.+)_(\d{4}-\d{2}-\d{2})\.md")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HANDLER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
def list_plans(filter_type: str | None = None) -> Tuple[List[Dict[str, Any]], str]:
|
||||
"""
|
||||
List all plans with their metadata.
|
||||
|
||||
Args:
|
||||
filter_type: Optional plan type filter (e.g. "dplan", "bplan").
|
||||
None returns all types.
|
||||
|
||||
Returns:
|
||||
Tuple of (plans_list, error_message)
|
||||
Each plan has: number, topic, date, status, tag, description, plan_type, prefix, file
|
||||
"""
|
||||
plans = []
|
||||
|
||||
if not DEV_PLANNING_ROOT.exists():
|
||||
return [], ""
|
||||
|
||||
for plan_file in DEV_PLANNING_ROOT.glob("*PLAN-*.md"):
|
||||
match = PLAN_FILENAME_PATTERN.match(plan_file.name)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
prefix = match.group(1)
|
||||
num = int(match.group(2))
|
||||
topic = match.group(3).replace('_', ' ')
|
||||
date = match.group(4)
|
||||
plan_type = prefix.lower()
|
||||
|
||||
# Apply type filter if specified
|
||||
if filter_type and plan_type != filter_type.lower():
|
||||
continue
|
||||
|
||||
# Extract metadata from file content
|
||||
status = extract_status(plan_file)
|
||||
tag = extract_tag(plan_file)
|
||||
description = extract_description(plan_file)
|
||||
|
||||
plans.append({
|
||||
"number": num,
|
||||
"topic": topic,
|
||||
"date": date,
|
||||
"status": status,
|
||||
"tag": tag,
|
||||
"description": description,
|
||||
"plan_type": plan_type,
|
||||
"prefix": prefix,
|
||||
"file": plan_file.name
|
||||
})
|
||||
|
||||
# Sort by type then number
|
||||
plans.sort(key=lambda x: (x["plan_type"], x["number"]))
|
||||
|
||||
return plans, ""
|
||||
@@ -1,72 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: log_setup.py
|
||||
# Description: Log file directory and handle preparation
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Log Setup Handler
|
||||
|
||||
Prepares log file directory and returns an open file handle for subprocess output.
|
||||
Extracted from dplan_flow.py to comply with 3-tier architecture
|
||||
(modules must not do direct file operations).
|
||||
|
||||
Usage:
|
||||
from aipass.flow.apps.handlers.dplan.log_setup import prepare_log_file
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
import io
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# log_setup.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
DEFAULT_LOG_DIR = FLOW_ROOT / "logs"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# OPERATIONS
|
||||
# =============================================================================
|
||||
|
||||
def prepare_log_file(filename: str = "post_close_runner.log",
|
||||
log_dir: Optional[Path] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Ensure log directory exists and return an open file handle for appending.
|
||||
|
||||
Args:
|
||||
filename: Log file name (default: post_close_runner.log)
|
||||
log_dir: Override log directory (default: flow/logs/)
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
success (bool): Whether preparation succeeded
|
||||
file_handle (Optional[io.TextIOWrapper]): Open file handle, or None on failure
|
||||
log_path (Optional[Path]): Full path to log file
|
||||
error (str): Error description if failed, empty string on success
|
||||
"""
|
||||
target_dir = log_dir or DEFAULT_LOG_DIR
|
||||
log_path = target_dir / filename
|
||||
|
||||
try:
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fh = open(log_path, "a", encoding="utf-8")
|
||||
return {
|
||||
"success": True,
|
||||
"file_handle": fh,
|
||||
"log_path": log_path,
|
||||
"error": "",
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"file_handle": None,
|
||||
"log_path": log_path,
|
||||
"error": f"Failed to prepare log file {log_path}: {e}",
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: registry.py
|
||||
# Description: DPLAN Registry Handler
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-02-18
|
||||
# Modified: 2026-02-18
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Registry Handler - DPLAN Registry and Summaries
|
||||
|
||||
Manages dplan_registry.json and dplan_summaries.json for tracking
|
||||
plan metadata, status, tags, and AI-generated summaries.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
from .status import extract_status, extract_tag, extract_description
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# registry.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning"
|
||||
REGISTRY_FILE = FLOW_ROOT / "flow_json" / "dplan_registry.json"
|
||||
SUMMARIES_FILE = FLOW_ROOT / "flow_json" / "dplan_summaries.json"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# REGISTRY OPERATIONS
|
||||
# =============================================================================
|
||||
|
||||
def load_registry() -> Dict[str, Any]:
|
||||
"""Load registry from disk, return empty structure if missing"""
|
||||
if not REGISTRY_FILE.exists():
|
||||
return {"plans": {}}
|
||||
try:
|
||||
with open(REGISTRY_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return {"plans": {}}
|
||||
|
||||
|
||||
def save_registry(data: Dict[str, Any]) -> None:
|
||||
"""Save registry to disk"""
|
||||
REGISTRY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(REGISTRY_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def register_plan(
|
||||
plan_number: int,
|
||||
topic: str,
|
||||
status: str,
|
||||
tag: str,
|
||||
file_path: str,
|
||||
date: str,
|
||||
description: str = ""
|
||||
) -> None:
|
||||
"""Register a new plan or update existing entry"""
|
||||
registry = load_registry()
|
||||
key = f"{plan_number:03d}"
|
||||
registry["plans"][key] = {
|
||||
"number": plan_number,
|
||||
"topic": topic,
|
||||
"status": status,
|
||||
"tag": tag,
|
||||
"file_path": file_path,
|
||||
"created": date,
|
||||
"description": description,
|
||||
"last_updated": datetime.now().isoformat()
|
||||
}
|
||||
save_registry(registry)
|
||||
|
||||
|
||||
def update_plan_status(plan_number: int, new_status: str) -> None:
|
||||
"""Update a plan's status in the registry"""
|
||||
registry = load_registry()
|
||||
key = f"{plan_number:03d}"
|
||||
if key in registry["plans"]:
|
||||
registry["plans"][key]["status"] = new_status
|
||||
registry["plans"][key]["last_updated"] = datetime.now().isoformat()
|
||||
if new_status == "complete":
|
||||
registry["plans"][key]["closed"] = datetime.now().isoformat()
|
||||
save_registry(registry)
|
||||
|
||||
|
||||
def get_plan(plan_number: int) -> Optional[Dict[str, Any]]:
|
||||
"""Get a single plan's registry entry"""
|
||||
registry = load_registry()
|
||||
key = f"{plan_number:03d}"
|
||||
return registry["plans"].get(key)
|
||||
|
||||
|
||||
def populate_from_filesystem() -> Dict[str, Any]:
|
||||
"""
|
||||
Scan dev_planning/ and build/update registry from all DPLAN files.
|
||||
|
||||
Returns:
|
||||
Updated registry data
|
||||
"""
|
||||
registry = load_registry()
|
||||
plans = registry.setdefault("plans", {})
|
||||
|
||||
if not DEV_PLANNING_ROOT.exists():
|
||||
return registry
|
||||
|
||||
for plan_file in DEV_PLANNING_ROOT.glob("DPLAN-*.md"):
|
||||
match = re.match(r"DPLAN-(\d+)_(.+)_(\d{4}-\d{2}-\d{2})\.md", plan_file.name)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
num = int(match.group(1))
|
||||
key = f"{num:03d}"
|
||||
topic = match.group(2).replace('_', ' ')
|
||||
date = match.group(3)
|
||||
status = extract_status(plan_file)
|
||||
tag = extract_tag(plan_file)
|
||||
description = extract_description(plan_file)
|
||||
|
||||
# Preserve existing fields (like closed date), update the rest
|
||||
existing = plans.get(key, {})
|
||||
existing.update({
|
||||
"number": num,
|
||||
"topic": topic,
|
||||
"status": status,
|
||||
"tag": tag,
|
||||
"file_path": str(plan_file),
|
||||
"created": date,
|
||||
"description": description,
|
||||
"last_updated": datetime.now().isoformat()
|
||||
})
|
||||
plans[key] = existing
|
||||
|
||||
save_registry(registry)
|
||||
return registry
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SUMMARY OPERATIONS
|
||||
# =============================================================================
|
||||
|
||||
def load_summaries() -> Dict[str, Any]:
|
||||
"""Load summaries cache from disk"""
|
||||
if not SUMMARIES_FILE.exists():
|
||||
return {}
|
||||
try:
|
||||
with open(SUMMARIES_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def save_summaries(data: Dict[str, Any]) -> None:
|
||||
"""Save summaries cache to disk"""
|
||||
SUMMARIES_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(SUMMARIES_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def get_summary(plan_number: int) -> str:
|
||||
"""Get cached summary for a plan, returns empty string if not cached"""
|
||||
summaries = load_summaries()
|
||||
key = f"{plan_number:03d}"
|
||||
entry = summaries.get(key, {})
|
||||
return entry.get("summary", "")
|
||||
|
||||
|
||||
def save_plan_summary(
|
||||
plan_number: int,
|
||||
summary: str,
|
||||
status: str = "",
|
||||
topic: str = "",
|
||||
file_path: str = ""
|
||||
) -> None:
|
||||
"""Save a summary to the cache"""
|
||||
summaries = load_summaries()
|
||||
key = f"{plan_number:03d}"
|
||||
summaries[key] = {
|
||||
"summary": summary,
|
||||
"status": status,
|
||||
"topic": topic,
|
||||
"file_path": file_path,
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"is_empty": not bool(summary)
|
||||
}
|
||||
save_summaries(summaries)
|
||||
|
||||
|
||||
def generate_description_summary(plan_file: Path) -> str:
|
||||
"""
|
||||
Extract a usable summary from a plan file.
|
||||
Uses the blockquote description line as summary.
|
||||
Falls back to empty string if no meaningful description found.
|
||||
|
||||
Args:
|
||||
plan_file: Path to the plan file
|
||||
|
||||
Returns:
|
||||
Summary string
|
||||
"""
|
||||
description = extract_description(plan_file)
|
||||
if description:
|
||||
return description
|
||||
|
||||
# Fallback: try to get the first line of the Vision section
|
||||
try:
|
||||
content = plan_file.read_text(encoding='utf-8')
|
||||
lines = content.split('\n')
|
||||
in_vision = False
|
||||
for line in lines:
|
||||
if line.strip().startswith('## Vision'):
|
||||
in_vision = True
|
||||
continue
|
||||
if in_vision and line.strip() and not line.strip().startswith('#'):
|
||||
text = line.strip()
|
||||
if text != "What we're trying to achieve":
|
||||
return text[:100]
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ""
|
||||
@@ -1,177 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: status.py
|
||||
# Description: D-PLAN status handler
|
||||
# Version: 1.1.0
|
||||
# Created: 2025-12-02
|
||||
# Modified: 2025-12-02
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Status Handler - D-PLAN Status Operations
|
||||
|
||||
Extracts status from plan files and provides status summary.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, Tuple
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# status.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
DEV_PLANNING_ROOT = FLOW_ROOT / "dev_planning"
|
||||
|
||||
VALID_TAGS = ["idea", "upgrade", "proposal", "bug", "research", "seed", "infrastructure"]
|
||||
|
||||
STATUS_ICONS = {
|
||||
"planning": "📋",
|
||||
"in_progress": "🔄",
|
||||
"ready": "✅",
|
||||
"complete": "✓",
|
||||
"abandoned": "❌",
|
||||
"unknown": "?"
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HANDLER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
def extract_status(plan_file: Path) -> str:
|
||||
"""
|
||||
Extract status from plan file by checking checkboxes
|
||||
|
||||
Args:
|
||||
plan_file: Path to the plan file
|
||||
|
||||
Returns:
|
||||
Status string: planning, in_progress, ready, complete, abandoned, unknown
|
||||
"""
|
||||
try:
|
||||
content = plan_file.read_text(encoding='utf-8')
|
||||
|
||||
# Look for checked status items (order matters - check most final states first)
|
||||
if re.search(r'- \[x\] Complete', content, re.IGNORECASE):
|
||||
return "complete"
|
||||
if re.search(r'- \[x\] Abandoned', content, re.IGNORECASE):
|
||||
return "abandoned"
|
||||
if re.search(r'- \[x\] Ready for Execution', content, re.IGNORECASE):
|
||||
return "ready"
|
||||
if re.search(r'- \[x\] In Progress', content, re.IGNORECASE):
|
||||
return "in_progress"
|
||||
if re.search(r'- \[x\] Planning', content, re.IGNORECASE):
|
||||
return "planning"
|
||||
|
||||
return "planning" # Default
|
||||
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def get_status_icon(status: str) -> str:
|
||||
"""
|
||||
Get emoji icon for status
|
||||
|
||||
Args:
|
||||
status: Status string
|
||||
|
||||
Returns:
|
||||
Emoji icon string
|
||||
"""
|
||||
return STATUS_ICONS.get(status, "?")
|
||||
|
||||
|
||||
def get_status_summary(filter_type: str | None = None) -> Tuple[Dict[str, int], int, str]:
|
||||
"""
|
||||
Get summary of all plans by status, optionally filtered by type.
|
||||
|
||||
Args:
|
||||
filter_type: Optional plan type filter (e.g. "dplan", "bplan").
|
||||
None counts all types.
|
||||
|
||||
Returns:
|
||||
Tuple of (status_counts, total, error_message)
|
||||
status_counts has keys: planning, in_progress, ready, complete, abandoned, unknown
|
||||
"""
|
||||
status_counts = {
|
||||
"planning": 0,
|
||||
"in_progress": 0,
|
||||
"ready": 0,
|
||||
"complete": 0,
|
||||
"abandoned": 0,
|
||||
"unknown": 0
|
||||
}
|
||||
|
||||
total = 0
|
||||
|
||||
if not DEV_PLANNING_ROOT.exists():
|
||||
return status_counts, 0, ""
|
||||
|
||||
for plan_file in DEV_PLANNING_ROOT.glob("*PLAN-*.md"):
|
||||
match = re.match(r"([A-Z]+PLAN)-\d+", plan_file.name)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
plan_type = match.group(1).lower()
|
||||
|
||||
if filter_type and plan_type != filter_type.lower():
|
||||
continue
|
||||
|
||||
total += 1
|
||||
status = extract_status(plan_file)
|
||||
|
||||
if status in status_counts:
|
||||
status_counts[status] += 1
|
||||
else:
|
||||
status_counts["unknown"] += 1
|
||||
|
||||
return status_counts, total, ""
|
||||
|
||||
|
||||
def extract_tag(plan_file: Path) -> str:
|
||||
"""
|
||||
Extract tag from plan file Tag: metadata line
|
||||
|
||||
Args:
|
||||
plan_file: Path to the plan file
|
||||
|
||||
Returns:
|
||||
Tag string (lowercase) or empty string if not found/invalid
|
||||
"""
|
||||
try:
|
||||
content = plan_file.read_text(encoding='utf-8')
|
||||
match = re.search(r'^Tag:\s*(\S+)', content, re.MULTILINE)
|
||||
if match:
|
||||
tag = match.group(1).lower().strip()
|
||||
if tag in VALID_TAGS:
|
||||
return tag
|
||||
return ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def extract_description(plan_file: Path) -> str:
|
||||
"""
|
||||
Extract one-line description from plan file blockquote
|
||||
|
||||
Args:
|
||||
plan_file: Path to the plan file
|
||||
|
||||
Returns:
|
||||
Description string or empty if not found/placeholder
|
||||
"""
|
||||
try:
|
||||
content = plan_file.read_text(encoding='utf-8')
|
||||
match = re.search(r'^>\s*(.+)$', content, re.MULTILINE)
|
||||
if match:
|
||||
desc = match.group(1).strip()
|
||||
if desc != "One-line description":
|
||||
return desc
|
||||
return ""
|
||||
except Exception:
|
||||
return ""
|
||||
@@ -1,195 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: template.py
|
||||
# Description: Plan template management
|
||||
# Version: 2.0.0
|
||||
# Created: 2025-12-02
|
||||
# Modified: 2025-12-02
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Template Handler - Plan Templates
|
||||
|
||||
Manages template loading and rendering for plan documents.
|
||||
Supports multiple plan types with type-specific templates.
|
||||
"""
|
||||
|
||||
# INFRASTRUCTURE IMPORT PATTERN
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
# NOTE: Handlers do NOT import Prax logger (per 3-tier standard)
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# template.py → dplan/ → handlers/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[3]
|
||||
TEMPLATE_DIR = FLOW_ROOT / "templates"
|
||||
|
||||
DPLAN_DEFAULT_TEMPLATE = """# DPLAN-{{NUMBER}}: {{TOPIC}}
|
||||
|
||||
Tag: {{TAG}}
|
||||
|
||||
> One-line description
|
||||
|
||||
## Vision
|
||||
What we're trying to achieve
|
||||
|
||||
## Current State
|
||||
What exists now
|
||||
|
||||
## What Needs Building
|
||||
Concrete items to build
|
||||
|
||||
## Design Decisions
|
||||
Key choices and why
|
||||
|
||||
## Status
|
||||
- [x] Planning
|
||||
- [ ] In Progress
|
||||
- [ ] Ready for Execution
|
||||
- [ ] Complete
|
||||
- [ ] Abandoned
|
||||
|
||||
## Notes
|
||||
Session notes, discoveries, changes
|
||||
|
||||
---
|
||||
*Created: {{DATE}}*
|
||||
*Updated: {{DATE}}*
|
||||
"""
|
||||
|
||||
BPLAN_DEFAULT_TEMPLATE = """# BPLAN-{{NUMBER}}: {{TOPIC}}
|
||||
|
||||
Tag: {{TAG}}
|
||||
|
||||
> One-line description
|
||||
|
||||
## Executive Summary
|
||||
What this business initiative achieves and why it matters.
|
||||
|
||||
## Market Analysis
|
||||
Target market, size, trends, and opportunity.
|
||||
|
||||
## Revenue Model
|
||||
How this generates or saves revenue. Pricing, margins, unit economics.
|
||||
|
||||
## Competitive Landscape
|
||||
Who else is doing this? What's our edge?
|
||||
|
||||
## KPIs
|
||||
| Metric | Target | Timeline |
|
||||
|--------|--------|----------|
|
||||
| Example | TBD | Q1 2026 |
|
||||
|
||||
## Go-to-Market
|
||||
Launch strategy, channels, partnerships.
|
||||
|
||||
## Risk Assessment
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|------------|
|
||||
| Example | High | Plan B |
|
||||
|
||||
## Timeline
|
||||
- [ ] Phase 1: Research & Validation
|
||||
- [ ] Phase 2: MVP / Pilot
|
||||
- [ ] Phase 3: Scale
|
||||
|
||||
## Budget Considerations
|
||||
Estimated costs, resource requirements, ROI timeline.
|
||||
|
||||
## Relationships
|
||||
- **Related BPLANs:** None yet
|
||||
- **Related DPLANs:** None yet
|
||||
- **Owner branches:** Who owns this
|
||||
|
||||
## Status
|
||||
- [x] Planning
|
||||
- [ ] In Progress
|
||||
- [ ] Ready for Execution
|
||||
- [ ] Complete
|
||||
- [ ] Abandoned
|
||||
|
||||
## Notes
|
||||
Session notes, discoveries, changes
|
||||
|
||||
---
|
||||
*Created: {{DATE}}*
|
||||
*Updated: {{DATE}}*
|
||||
"""
|
||||
|
||||
DEFAULT_TEMPLATES = {
|
||||
"dplan": DPLAN_DEFAULT_TEMPLATE,
|
||||
"bplan": BPLAN_DEFAULT_TEMPLATE,
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HANDLER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
def get_default_template(plan_type: str = "dplan") -> str:
|
||||
"""
|
||||
Return built-in default template for the given plan type.
|
||||
|
||||
Args:
|
||||
plan_type: Plan type (dplan, bplan). Case-insensitive.
|
||||
|
||||
Returns:
|
||||
Template string with {{NUMBER}}, {{TOPIC}}, {{DATE}}, {{TAG}} placeholders
|
||||
"""
|
||||
return DEFAULT_TEMPLATES.get(plan_type.lower(), DPLAN_DEFAULT_TEMPLATE)
|
||||
|
||||
|
||||
def render_template(
|
||||
plan_number: int,
|
||||
topic: str,
|
||||
date_str: str,
|
||||
tag: str = "idea",
|
||||
plan_type: str = "dplan"
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
Render plan template with variables.
|
||||
|
||||
Loads custom template if available, falls back to built-in default.
|
||||
Replaces {{NUMBER}}, {{TOPIC}}, {{DATE}}, {{TAG}} placeholders.
|
||||
|
||||
Args:
|
||||
plan_number: The plan number (e.g., 42)
|
||||
topic: Topic name
|
||||
date_str: Date string (YYYY-MM-DD)
|
||||
tag: Plan tag classification (default: idea)
|
||||
plan_type: Plan type (dplan, bplan). Default: dplan.
|
||||
|
||||
Returns:
|
||||
Tuple of (rendered_content, error_message)
|
||||
Error message is empty on success
|
||||
"""
|
||||
plan_type_lower = plan_type.lower()
|
||||
|
||||
# Try to load custom template for this type
|
||||
template_content = None
|
||||
template_file = TEMPLATE_DIR / f"{plan_type_lower}_default.md"
|
||||
if template_file.exists():
|
||||
try:
|
||||
template_content = template_file.read_text(encoding='utf-8')
|
||||
except Exception:
|
||||
template_content = None
|
||||
|
||||
if template_content is None:
|
||||
template_content = get_default_template(plan_type_lower)
|
||||
|
||||
# Replace placeholders
|
||||
prefix = plan_type.upper()
|
||||
content = template_content.replace("{{NUMBER}}", f"{plan_number:03d}")
|
||||
content = content.replace("{{TOPIC}}", topic)
|
||||
content = content.replace("{{DATE}}", date_str)
|
||||
content = content.replace("{{TAG}}", tag)
|
||||
|
||||
# Handle templates that use hardcoded DPLAN prefix — replace with correct type
|
||||
if prefix != "DPLAN" and content.startswith("# DPLAN-"):
|
||||
content = content.replace("# DPLAN-", f"# {prefix}-", 1)
|
||||
|
||||
return content, ""
|
||||
@@ -48,9 +48,9 @@ def _find_repo_root() -> Path:
|
||||
|
||||
_REPO_ROOT = _find_repo_root()
|
||||
MEMORY_BANK_PATH = _REPO_ROOT / "MEMORY_BANK" / "plans"
|
||||
PROCESSED_PLANS_DIR = FLOW_ROOT / "processed_plans"
|
||||
PROCESSED_PLANS_DIR = _PKG_ROOT / "backup" / "processed_plans"
|
||||
PRIVATE_BRANCH_REGISTRY = _REPO_ROOT / "PRIVATE_BRANCH_REGISTRY.json"
|
||||
REGISTRY_FILE = FLOW_JSON_DIR / "flow_registry.json"
|
||||
REGISTRY_FILE = FLOW_JSON_DIR / "fplan_registry.json"
|
||||
CONFIG_FILE = FLOW_JSON_DIR / "flow_mbank_config.json"
|
||||
TRL_REGISTRY_FILE = FLOW_JSON_DIR / "flow_mbank_registry.json"
|
||||
API_CONFIG_FILE = FLOW_ROOT / "apps" / "handlers" / "json_templates" / "custom" / "api_config.json"
|
||||
@@ -217,54 +217,75 @@ def get_ai_model() -> Optional[str]:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# =============================================
|
||||
# PLAN TYPE HELPERS
|
||||
# =============================================
|
||||
|
||||
|
||||
def _get_all_registry_files() -> List[str]:
|
||||
"""Return per-type registry filenames via plan-type discovery."""
|
||||
try:
|
||||
from aipass.flow.apps.handlers.template.plan_type_loader import discover_plan_types # type: ignore[import-not-found]
|
||||
files: List[str] = []
|
||||
for _key, config in discover_plan_types().items():
|
||||
rf = config.get("registry_file")
|
||||
if rf and rf not in files:
|
||||
files.append(rf)
|
||||
if files:
|
||||
return files
|
||||
except Exception:
|
||||
pass
|
||||
return [REGISTRY_FILE.name]
|
||||
|
||||
|
||||
# =============================================
|
||||
# REGISTRY OPERATIONS
|
||||
# =============================================
|
||||
|
||||
def load_flow_registry() -> Dict[str, Any]:
|
||||
"""Load the flow registry"""
|
||||
if not REGISTRY_FILE.exists():
|
||||
raise Exception(f"Flow registry not found at {REGISTRY_FILE}")
|
||||
|
||||
def load_flow_registry(registry_file: str | None = None) -> Dict[str, Any]:
|
||||
"""Load a plan registry."""
|
||||
target = FLOW_JSON_DIR / registry_file if registry_file else REGISTRY_FILE
|
||||
if not target.exists():
|
||||
raise Exception(f"Flow registry not found at {target}")
|
||||
try:
|
||||
with open(REGISTRY_FILE, 'r', encoding='utf-8') as f:
|
||||
with open(target, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to load flow registry: {e}")
|
||||
|
||||
def save_flow_registry(registry: Dict[str, Any]):
|
||||
"""Save the flow registry"""
|
||||
def save_flow_registry(registry: Dict[str, Any], registry_file: str | None = None) -> None:
|
||||
"""Save a plan registry."""
|
||||
target = FLOW_JSON_DIR / registry_file if registry_file else REGISTRY_FILE
|
||||
try:
|
||||
registry["last_updated"] = datetime.now(timezone.utc).isoformat()
|
||||
with open(REGISTRY_FILE, 'w', encoding='utf-8') as f:
|
||||
with open(target, 'w', encoding='utf-8') as f:
|
||||
json.dump(registry, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to save flow registry: {e}")
|
||||
|
||||
def get_closed_plans() -> List[Dict[str, Any]]:
|
||||
"""Get closed PLANs from flow registry
|
||||
|
||||
AUTO-HEAL: Before getting closed plans, verify and heal any orphaned plans
|
||||
"""Get closed PLANs from ALL per-type registries.
|
||||
|
||||
Returns:
|
||||
List of dicts with keys: number, path, info
|
||||
List of dicts with keys: number, path, info, registry_file
|
||||
"""
|
||||
# AUTO-HEAL LAYER: Fix orphaned plans before processing new ones
|
||||
heal_result = verify_and_heal_orphaned_plans()
|
||||
|
||||
registry = load_flow_registry()
|
||||
closed_plans = []
|
||||
|
||||
for plan_num, plan_info in registry.get("plans", {}).items():
|
||||
if plan_info.get("status") == "closed" and plan_info.get("processed") != True:
|
||||
file_path = Path(plan_info.get("file_path", ""))
|
||||
if file_path.exists():
|
||||
closed_plans.append({
|
||||
"number": plan_num,
|
||||
"path": file_path,
|
||||
"info": plan_info
|
||||
})
|
||||
|
||||
verify_and_heal_orphaned_plans()
|
||||
closed_plans: List[Dict[str, Any]] = []
|
||||
for reg_file in _get_all_registry_files():
|
||||
try:
|
||||
registry = load_flow_registry(registry_file=reg_file)
|
||||
except Exception:
|
||||
continue
|
||||
for plan_num, plan_info in registry.get("plans", {}).items():
|
||||
if plan_info.get("status") == "closed" and plan_info.get("processed") is not True:
|
||||
file_path = Path(plan_info.get("file_path", ""))
|
||||
if file_path.exists():
|
||||
closed_plans.append({
|
||||
"number": plan_num,
|
||||
"path": file_path,
|
||||
"info": plan_info,
|
||||
"registry_file": reg_file,
|
||||
})
|
||||
return closed_plans
|
||||
|
||||
# =============================================
|
||||
@@ -654,188 +675,105 @@ def cleanup_temp_files() -> Dict[str, Any]:
|
||||
# =============================================
|
||||
|
||||
def verify_and_heal_orphaned_plans() -> Dict[str, Any]:
|
||||
"""Cross-check registry vs filesystem and auto-heal orphaned plans
|
||||
|
||||
Detects plans where registry says processed=true but file still at original location.
|
||||
Attempts to move orphaned files to processed_plans/ directory.
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- orphans_found: int
|
||||
- successfully_healed: int
|
||||
- failed_to_heal: int
|
||||
- orphans: list of plan details
|
||||
"""
|
||||
registry = load_flow_registry()
|
||||
|
||||
"""Cross-check ALL registries vs filesystem and auto-heal orphaned plans."""
|
||||
orphans_found = 0
|
||||
successfully_healed = 0
|
||||
failed_to_heal = 0
|
||||
orphan_details = []
|
||||
orphan_details: List[Dict[str, Any]] = []
|
||||
|
||||
for plan_num, plan_info in registry.get("plans", {}).items():
|
||||
# Only check plans marked as processed
|
||||
if plan_info.get("processed") == True and plan_info.get("cleanup_completed") == True:
|
||||
original_path = Path(plan_info.get("file_path", ""))
|
||||
|
||||
# VERIFICATION: Does file still exist at original location?
|
||||
if original_path.exists():
|
||||
# ORPHAN DETECTED
|
||||
orphans_found += 1
|
||||
|
||||
# Attempt auto-heal by moving file now
|
||||
try:
|
||||
PROCESSED_PLANS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
destination = PROCESSED_PLANS_DIR / original_path.name
|
||||
|
||||
# Handle duplicates
|
||||
if destination.exists():
|
||||
timestamp = datetime.now().strftime("%H%M%S")
|
||||
stem = destination.stem
|
||||
suffix = destination.suffix
|
||||
destination = PROCESSED_PLANS_DIR / f"{stem}_{timestamp}{suffix}"
|
||||
|
||||
# Attempt move
|
||||
original_path.rename(destination)
|
||||
|
||||
# Verify move
|
||||
if destination.exists() and not original_path.exists():
|
||||
successfully_healed += 1
|
||||
orphan_details.append({
|
||||
"plan": f"FPLAN-{plan_num}",
|
||||
"status": "healed",
|
||||
"original_path": str(original_path),
|
||||
"destination": str(destination)
|
||||
})
|
||||
else:
|
||||
for reg_file in _get_all_registry_files():
|
||||
try:
|
||||
registry = load_flow_registry(registry_file=reg_file)
|
||||
except Exception:
|
||||
continue
|
||||
for plan_num, plan_info in registry.get("plans", {}).items():
|
||||
if plan_info.get("processed") is True and plan_info.get("cleanup_completed") is True:
|
||||
original_path = Path(plan_info.get("file_path", ""))
|
||||
if original_path.exists():
|
||||
orphans_found += 1
|
||||
plan_label = original_path.stem
|
||||
try:
|
||||
PROCESSED_PLANS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
destination = PROCESSED_PLANS_DIR / original_path.name
|
||||
if destination.exists():
|
||||
timestamp = datetime.now().strftime("%H%M%S")
|
||||
destination = PROCESSED_PLANS_DIR / f"{destination.stem}_{timestamp}{destination.suffix}"
|
||||
original_path.rename(destination)
|
||||
if destination.exists() and not original_path.exists():
|
||||
successfully_healed += 1
|
||||
orphan_details.append({"plan": plan_label, "status": "healed",
|
||||
"original_path": str(original_path), "destination": str(destination)})
|
||||
else:
|
||||
failed_to_heal += 1
|
||||
orphan_details.append({"plan": plan_label, "status": "heal_failed",
|
||||
"error": "Verification failed", "path": str(original_path)})
|
||||
except Exception as e:
|
||||
failed_to_heal += 1
|
||||
orphan_details.append({
|
||||
"plan": f"FPLAN-{plan_num}",
|
||||
"status": "heal_failed",
|
||||
"error": "Verification failed after rename",
|
||||
"path": str(original_path)
|
||||
})
|
||||
orphan_details.append({"plan": plan_label, "status": "heal_failed",
|
||||
"error": str(e), "path": str(original_path)})
|
||||
|
||||
except Exception as e:
|
||||
failed_to_heal += 1
|
||||
orphan_details.append({
|
||||
"plan": f"FPLAN-{plan_num}",
|
||||
"status": "heal_failed",
|
||||
"error": str(e),
|
||||
"path": str(original_path)
|
||||
})
|
||||
|
||||
return {
|
||||
"orphans_found": orphans_found,
|
||||
"successfully_healed": successfully_healed,
|
||||
"failed_to_heal": failed_to_heal,
|
||||
"orphans": orphan_details
|
||||
}
|
||||
return {"orphans_found": orphans_found, "successfully_healed": successfully_healed,
|
||||
"failed_to_heal": failed_to_heal, "orphans": orphan_details}
|
||||
|
||||
# =============================================
|
||||
# MAIN PROCESSING
|
||||
# =============================================
|
||||
|
||||
def process_closed_plans() -> Dict[str, Any]:
|
||||
"""Main function to process all closed plans
|
||||
"""Process all closed plans across all plan types.
|
||||
|
||||
# AI summarization removed — plans vectorized directly from flow/processed_plans/
|
||||
# Processing is now: archive_plan() → update registry flags → done
|
||||
|
||||
AUTO-HEAL: Cleans up old -TEMP files from MEMORY_BANK after processing
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- processed: int (successfully processed)
|
||||
- errors: int (failed)
|
||||
- results: list of per-plan results
|
||||
- error: str (if success=False)
|
||||
- cleanup: dict (TEMP file cleanup results)
|
||||
Processing: archive_plan() -> update registry flags -> vector processing (best effort)
|
||||
"""
|
||||
try:
|
||||
# Get closed plans from registry (includes auto-heal)
|
||||
closed_plans = get_closed_plans()
|
||||
|
||||
if not closed_plans:
|
||||
# No closed plans to process, but still run cleanup
|
||||
cleanup_result = cleanup_temp_files()
|
||||
return {
|
||||
"success": True,
|
||||
"processed": 0,
|
||||
"errors": 0,
|
||||
"results": [],
|
||||
"cleanup": cleanup_result
|
||||
}
|
||||
return {"success": True, "processed": 0, "errors": 0, "results": [], "cleanup": cleanup_result}
|
||||
|
||||
processed_count = 0
|
||||
error_count = 0
|
||||
results = []
|
||||
results: List[Dict[str, Any]] = []
|
||||
|
||||
for plan in closed_plans:
|
||||
try:
|
||||
plan_path = plan["path"]
|
||||
plan_num = plan["number"]
|
||||
plan_path: Path = plan["path"]
|
||||
plan_num: str = plan["number"]
|
||||
reg_file: str = plan.get("registry_file", REGISTRY_FILE.name)
|
||||
plan_label = plan_path.stem
|
||||
correlation_id = f"{plan_label}-{datetime.now().strftime('%H%M%S')}"
|
||||
|
||||
# Generate correlation ID for tracking
|
||||
correlation_id = f"FPLAN-{plan_num}-{datetime.now().strftime('%H%M%S')}"
|
||||
|
||||
# Archive plan to flow/processed_plans/
|
||||
archive_success = archive_plan(plan_path)
|
||||
|
||||
# Update registry flags
|
||||
registry = load_flow_registry()
|
||||
registry = load_flow_registry(registry_file=reg_file)
|
||||
if plan_num in registry.get("plans", {}):
|
||||
registry["plans"][plan_num]["cleanup_completed"] = archive_success
|
||||
registry["plans"][plan_num]["cleanup_date"] = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
if archive_success:
|
||||
registry["plans"][plan_num]["processed"] = True
|
||||
registry["plans"][plan_num]["processed_date"] = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
save_flow_registry(registry)
|
||||
save_flow_registry(registry, registry_file=reg_file)
|
||||
|
||||
if archive_success:
|
||||
processed_count += 1
|
||||
results.append({
|
||||
"plan": f"FPLAN-{plan_num}",
|
||||
"status": "archived",
|
||||
"correlation_id": correlation_id
|
||||
})
|
||||
# Best-effort vector processing
|
||||
try:
|
||||
from aipass.memory.apps.handlers.intake.plans_processor import process_plans # type: ignore[import-not-found]
|
||||
process_plans()
|
||||
except Exception:
|
||||
pass
|
||||
results.append({"plan": plan_label, "status": "archived", "correlation_id": correlation_id})
|
||||
else:
|
||||
error_count += 1
|
||||
results.append({
|
||||
"plan": f"FPLAN-{plan_num}",
|
||||
"status": "archive_failed",
|
||||
"error": "Failed to move plan to flow/processed_plans/",
|
||||
"correlation_id": correlation_id
|
||||
})
|
||||
|
||||
results.append({"plan": plan_label, "status": "archive_failed",
|
||||
"error": "Failed to move plan to backup/processed_plans/",
|
||||
"correlation_id": correlation_id})
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
plan_num = plan.get('number', 'unknown')
|
||||
results.append({
|
||||
"plan": f"FPLAN-{plan_num}",
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
})
|
||||
results.append({"plan": str(plan.get("path", "unknown")), "status": "error", "error": str(e)})
|
||||
|
||||
# AUTO-HEAL: Clean up old -TEMP files from MEMORY_BANK
|
||||
cleanup_result = cleanup_temp_files()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"processed": processed_count,
|
||||
"errors": error_count,
|
||||
"results": results,
|
||||
"cleanup": cleanup_result
|
||||
}
|
||||
return {"success": True, "processed": processed_count, "errors": error_count,
|
||||
"results": results, "cleanup": cleanup_result}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"processed": 0,
|
||||
"errors": 0,
|
||||
"results": [],
|
||||
"error": str(e)
|
||||
}
|
||||
return {"success": False, "processed": 0, "errors": 0, "results": [], "error": str(e)}
|
||||
|
||||
@@ -53,7 +53,7 @@ def find_branch_registry(branch_path: Path, branch_name: str) -> Optional[Path]:
|
||||
if not branch_path.exists():
|
||||
return None
|
||||
|
||||
# Pattern 1: flow_json/flow_registry.json
|
||||
# Pattern 1: flow_json/{branch}_registry.json
|
||||
candidate = branch_path / "flow_json" / f"{branch_name}_registry.json"
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
@@ -284,8 +284,8 @@ def save_central(central_file: Path, central_dir: Path, central_data: Dict[str,
|
||||
# =============================================
|
||||
|
||||
def aggregate_central_impl(heal: bool = True,
|
||||
central_file: Path = None,
|
||||
central_dir: Path = None) -> bool:
|
||||
central_file: Path | None = None,
|
||||
central_dir: Path | None = None) -> bool:
|
||||
"""Aggregate and validate central plans
|
||||
|
||||
Algorithm:
|
||||
@@ -310,6 +310,14 @@ def aggregate_central_impl(heal: bool = True,
|
||||
try:
|
||||
logger.info(f"[{MODULE_NAME}] Starting central aggregation")
|
||||
|
||||
# Apply defaults when caller passes None
|
||||
if central_file is None or central_dir is None:
|
||||
logger.error(f"[{MODULE_NAME}] central_file and central_dir are required")
|
||||
return False
|
||||
|
||||
assert central_file is not None # narrowing for type checker
|
||||
assert central_dir is not None
|
||||
|
||||
# Load central file
|
||||
central_data = load_central(central_file)
|
||||
branches = central_data.get("branches", {})
|
||||
|
||||
@@ -36,6 +36,56 @@ FLOW_ROOT = _PKG_ROOT / "flow"
|
||||
|
||||
MODULE_NAME = "close_plan"
|
||||
|
||||
# =============================================
|
||||
# PLAN TYPE ROUTING
|
||||
# =============================================
|
||||
|
||||
|
||||
def _extract_prefix(plan_num_raw: str) -> str | None:
|
||||
"""Extract plan-type prefix (e.g. ``"DPLAN"``) from raw input."""
|
||||
import re
|
||||
m = re.match(r'^([A-Z]+PLAN)-', plan_num_raw.strip(), re.IGNORECASE)
|
||||
return m.group(1).upper() if m else None
|
||||
|
||||
|
||||
def _resolve_registry_file(plan_num_raw: str) -> str | None:
|
||||
"""Resolve registry_file from a raw plan number with prefix.
|
||||
|
||||
Returns registry filename or None if no prefix detected.
|
||||
"""
|
||||
prefix = _extract_prefix(plan_num_raw)
|
||||
if prefix is None:
|
||||
return None
|
||||
try:
|
||||
from aipass.flow.apps.handlers.template.plan_type_loader import get_plan_type # type: ignore[import-not-found]
|
||||
config = get_plan_type(prefix)
|
||||
return config.get("registry_file")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _find_plan_across_registries(plan_key: str, load_registry_fn: Any) -> str | None:
|
||||
"""Search all registries for a plan number when no prefix given.
|
||||
|
||||
Returns registry filename where the plan was found, or None.
|
||||
"""
|
||||
try:
|
||||
from aipass.flow.apps.handlers.template.plan_type_loader import discover_plan_types # type: ignore[import-not-found]
|
||||
for _type_key, config in discover_plan_types().items():
|
||||
reg_file = config.get("registry_file")
|
||||
if not reg_file:
|
||||
continue
|
||||
try:
|
||||
registry = load_registry_fn(registry_file=reg_file)
|
||||
if plan_key in registry.get("plans", {}):
|
||||
return reg_file
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# =============================================
|
||||
# HELPER
|
||||
# =============================================
|
||||
@@ -55,18 +105,19 @@ def _spawn_background_runner():
|
||||
# CLOSE PLAN IMPLEMENTATION
|
||||
# =============================================
|
||||
|
||||
def close_plan_impl(plan_num=None, confirm=False, all_plans=False, spawn_background=True,
|
||||
def close_plan_impl(plan_num: Any = None, confirm: bool = False,
|
||||
all_plans: bool = False, spawn_background: bool = True,
|
||||
# Dependencies injected from module
|
||||
normalize_plan_number=None,
|
||||
load_registry=None,
|
||||
save_registry=None,
|
||||
validate_plan_exists=None,
|
||||
confirm_plan_deletion=None,
|
||||
is_template_content=None,
|
||||
update_dashboard_local=None,
|
||||
push_to_plans_central=None,
|
||||
push_flow_to_branch_dashboard=None,
|
||||
close_all_plans_fn=None) -> Dict[str, Any]:
|
||||
normalize_plan_number: Any = None,
|
||||
load_registry: Any = None,
|
||||
save_registry: Any = None,
|
||||
validate_plan_exists: Any = None,
|
||||
confirm_plan_deletion: Any = None,
|
||||
is_template_content: Any = None,
|
||||
update_dashboard_local: Any = None,
|
||||
push_to_plans_central: Any = None,
|
||||
push_flow_to_branch_dashboard: Any = None,
|
||||
close_all_plans_fn: Any = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Implement plan closure workflow
|
||||
|
||||
@@ -85,7 +136,7 @@ def close_plan_impl(plan_num=None, confirm=False, all_plans=False, spawn_backgro
|
||||
Dict with keys: success (bool), messages (list of dicts with type/text),
|
||||
plan_key (str), cancelled (bool)
|
||||
"""
|
||||
messages: List[Dict[str, str]] = []
|
||||
messages: List[Dict[str, Any]] = []
|
||||
|
||||
# Handle --all flag
|
||||
if all_plans:
|
||||
@@ -107,8 +158,20 @@ def close_plan_impl(plan_num=None, confirm=False, all_plans=False, spawn_backgro
|
||||
# 1. VALIDATE: Normalize plan number (handler)
|
||||
plan_key = normalize_plan_number(plan_num)
|
||||
|
||||
# 2. LOAD DATA: Get registry (service)
|
||||
registry = load_registry()
|
||||
# 2. LOAD DATA: Detect correct registry from prefix, then load
|
||||
reg_file = _resolve_registry_file(plan_num)
|
||||
if reg_file:
|
||||
registry = load_registry(registry_file=reg_file)
|
||||
else:
|
||||
# No prefix -- try default registry first
|
||||
registry = load_registry()
|
||||
exists_default, _ = validate_plan_exists(plan_key, registry)
|
||||
if not exists_default:
|
||||
# Search other registries
|
||||
found_reg = _find_plan_across_registries(plan_key, load_registry)
|
||||
if found_reg:
|
||||
reg_file = found_reg
|
||||
registry = load_registry(registry_file=reg_file)
|
||||
|
||||
# 3. VALIDATE: Check plan exists (handler)
|
||||
exists, error_msg = validate_plan_exists(plan_key, registry)
|
||||
@@ -124,24 +187,30 @@ def close_plan_impl(plan_num=None, confirm=False, all_plans=False, spawn_backgro
|
||||
plan_info = registry["plans"][plan_key]
|
||||
plan_file = Path(plan_info.get("file_path", ""))
|
||||
|
||||
# Derive display label from filename (e.g. "FPLAN-0079" or "DPLAN-0004")
|
||||
plan_label = plan_file.stem if plan_file.name else f"PLAN-{plan_key}"
|
||||
|
||||
# Extract prefix for display functions (e.g. "FPLAN", "DPLAN")
|
||||
plan_prefix = _extract_prefix(plan_label) or "FPLAN"
|
||||
|
||||
# 4. IDEMPOTENCY CHECK: Prevent double-closing (with orphan cleanup)
|
||||
if plan_info['status'] == 'closed':
|
||||
closed_date = plan_info.get('closed', 'unknown')
|
||||
|
||||
# Check if .md file is orphaned on disk (registry-closed but file never moved)
|
||||
if plan_file.exists():
|
||||
messages.append({"type": "warning", "text": f"FPLAN-{plan_key} already closed on {closed_date} — orphaned .md file detected"})
|
||||
messages.append({"type": "warning", "text": f"{plan_label} already closed on {closed_date} — orphaned .md file detected"})
|
||||
messages.append({"type": "dim", "text": f" Cleaning up: moving {plan_file.name} to processed_plans/"})
|
||||
try:
|
||||
from aipass.flow.apps.handlers.mbank.process import archive_plan
|
||||
if archive_plan(plan_file):
|
||||
logger.info(f"[{MODULE_NAME}] Cleaned up orphaned file for FPLAN-{plan_key}: {plan_file}")
|
||||
logger.info(f"[{MODULE_NAME}] Cleaned up orphaned file for {plan_label}: {plan_file}")
|
||||
messages.append({"type": "success", "text": " Orphaned file archived successfully"})
|
||||
else:
|
||||
logger.warning(f"[{MODULE_NAME}] Failed to archive orphaned file for FPLAN-{plan_key}: {plan_file}")
|
||||
logger.warning(f"[{MODULE_NAME}] Failed to archive orphaned file for {plan_label}: {plan_file}")
|
||||
messages.append({"type": "error_text", "text": " Failed to move orphaned file — manual cleanup required"})
|
||||
except Exception as e:
|
||||
logger.warning(f"[{MODULE_NAME}] Error cleaning orphaned file for FPLAN-{plan_key}: {e}")
|
||||
logger.warning(f"[{MODULE_NAME}] Error cleaning orphaned file for {plan_label}: {e}")
|
||||
messages.append({"type": "error_text", "text": f" Error during cleanup: {e}"})
|
||||
return {
|
||||
"success": True,
|
||||
@@ -150,7 +219,7 @@ def close_plan_impl(plan_num=None, confirm=False, all_plans=False, spawn_backgro
|
||||
"cancelled": False,
|
||||
}
|
||||
|
||||
messages.append({"type": "warning", "text": f"FPLAN-{plan_key} already closed on {closed_date}"})
|
||||
messages.append({"type": "warning", "text": f"{plan_label} already closed on {closed_date}"})
|
||||
messages.append({"type": "dim", "text": "Nothing to do - plan is already archived"})
|
||||
return {
|
||||
"success": False,
|
||||
@@ -166,18 +235,21 @@ def close_plan_impl(plan_num=None, confirm=False, all_plans=False, spawn_backgro
|
||||
content = f.read()
|
||||
|
||||
if is_template_content(content):
|
||||
messages.append({"type": "warning", "text": f" FPLAN-{plan_key} is empty template - fast-deleting (not archiving)"})
|
||||
messages.append({"type": "warning", "text": f" {plan_label} is empty template - fast-deleting (not archiving)"})
|
||||
|
||||
# Delete the file
|
||||
plan_file.unlink()
|
||||
logger.info(f"[{MODULE_NAME}] Deleted empty template file: {plan_file}")
|
||||
|
||||
# Remove from registry
|
||||
# Remove from registry and save to correct per-type registry
|
||||
del registry["plans"][plan_key]
|
||||
save_registry(registry)
|
||||
logger.info(f"[{MODULE_NAME}] Removed FPLAN-{plan_key} from registry")
|
||||
if reg_file:
|
||||
save_registry(registry, registry_file=reg_file)
|
||||
else:
|
||||
save_registry(registry)
|
||||
logger.info(f"[{MODULE_NAME}] Removed {plan_label} from registry")
|
||||
|
||||
messages.append({"type": "success", "text": f" Empty template deleted - FPLAN-{plan_key} removed from system"})
|
||||
messages.append({"type": "success", "text": f" Empty template deleted - {plan_label} removed from system"})
|
||||
return {
|
||||
"success": True,
|
||||
"messages": messages,
|
||||
@@ -193,7 +265,7 @@ def close_plan_impl(plan_num=None, confirm=False, all_plans=False, spawn_backgro
|
||||
messages.append({"type": "warning", "text": " Could not check template status, continuing with normal close"})
|
||||
|
||||
# DISPLAY: plan info header
|
||||
messages.append({"type": "header", "plan_key": plan_key, "plan_info": plan_info})
|
||||
messages.append({"type": "header", "plan_key": plan_key, "plan_info": plan_info, "prefix": plan_prefix})
|
||||
|
||||
# CONFIRM: Ask user only if explicitly requested (--confirm/--interactive)
|
||||
if confirm:
|
||||
@@ -212,8 +284,11 @@ def close_plan_impl(plan_num=None, confirm=False, all_plans=False, spawn_backgro
|
||||
# CRITICAL: Close ALWAYS succeeds from this point. Archive is non-blocking.
|
||||
plan_info['status'] = 'closed'
|
||||
plan_info['closed'] = datetime.now(timezone.utc).isoformat()
|
||||
save_registry(registry)
|
||||
logger.info(f"[{MODULE_NAME}] Marked FPLAN-{plan_key} as closed")
|
||||
if reg_file:
|
||||
save_registry(registry, registry_file=reg_file)
|
||||
else:
|
||||
save_registry(registry)
|
||||
logger.info(f"[{MODULE_NAME}] Marked {plan_label} as closed")
|
||||
except Exception as e:
|
||||
logger.error(f"[{MODULE_NAME}] Failed to mark plan as closed: {e}")
|
||||
messages.append({"type": "error_text", "text": f" Failed to update registry: {e}"})
|
||||
@@ -229,7 +304,7 @@ def close_plan_impl(plan_num=None, confirm=False, all_plans=False, spawn_backgro
|
||||
messages.append({"type": "step", "text": "[3/5] Starting background processing..."})
|
||||
try:
|
||||
_spawn_background_runner()
|
||||
logger.info(f"[{MODULE_NAME}] Spawned background post-processing for FPLAN-{plan_key}")
|
||||
logger.info(f"[{MODULE_NAME}] Spawned background post-processing for {plan_label}")
|
||||
messages.append({"type": "dim", "text": " Summary generation and archival running in background"})
|
||||
except FileNotFoundError as e:
|
||||
logger.warning(f"[{MODULE_NAME}] Background runner not found: {e}")
|
||||
@@ -264,7 +339,7 @@ def close_plan_impl(plan_num=None, confirm=False, all_plans=False, spawn_backgro
|
||||
|
||||
# --- Step 5/5: Done ---
|
||||
messages.append({"type": "step", "text": "[5/5] Finalizing..."})
|
||||
messages.append({"type": "close_success", "plan_key": plan_key})
|
||||
messages.append({"type": "close_success", "plan_key": plan_key, "prefix": plan_prefix})
|
||||
|
||||
# Append to branch's CLOSED_PLANS.local.json
|
||||
try:
|
||||
@@ -308,10 +383,10 @@ def close_plan_impl(plan_num=None, confirm=False, all_plans=False, spawn_backgro
|
||||
}
|
||||
|
||||
|
||||
def close_all_plans_impl(confirm=False,
|
||||
def close_all_plans_impl(confirm: bool = False,
|
||||
# Dependencies injected from module
|
||||
get_open_plans=None,
|
||||
close_plan_fn=None) -> Dict[str, Any]:
|
||||
get_open_plans: Any = None,
|
||||
close_plan_fn: Any = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Close all open plans in one operation
|
||||
|
||||
@@ -323,7 +398,7 @@ def close_all_plans_impl(confirm=False,
|
||||
Returns:
|
||||
Dict with keys: success (bool), messages (list), success_count, failure_count, total
|
||||
"""
|
||||
messages: List[Dict[str, str]] = []
|
||||
messages: List[Dict[str, Any]] = []
|
||||
|
||||
try:
|
||||
# Get all open plans (handler)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: display.py
|
||||
# Description: Plan Display Handler
|
||||
# Version: 0.1.0
|
||||
# Version: 0.2.0
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# Modified: 2026-03-17
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -83,13 +83,14 @@ def display_plan_result(
|
||||
|
||||
# DELETE PLAN DISPLAY FUNCTIONS
|
||||
|
||||
def format_plan_deletion_header(plan_key: str, plan_info: Dict[str, Any]) -> str:
|
||||
def format_plan_deletion_header(plan_key: str, plan_info: Dict[str, Any], prefix: str = "FPLAN") -> str:
|
||||
"""
|
||||
Format plan information header for deletion confirmation
|
||||
|
||||
Args:
|
||||
plan_key: Normalized plan number (e.g., "0001")
|
||||
plan_info: Plan metadata dictionary from registry
|
||||
prefix: Plan prefix (e.g. "FPLAN", "DPLAN")
|
||||
|
||||
Returns:
|
||||
Formatted header string with Rich markup for plan details
|
||||
@@ -98,7 +99,7 @@ def format_plan_deletion_header(plan_key: str, plan_info: Dict[str, Any]) -> str
|
||||
|
||||
lines = [
|
||||
"",
|
||||
"[bold cyan]╭─ Close FPLAN-" + plan_key + " ─╮[/bold cyan]",
|
||||
f"[bold cyan]╭─ Close {prefix}-" + plan_key + " ─╮[/bold cyan]",
|
||||
"",
|
||||
f" [dim]Location:[/dim] {plan_info.get('relative_path', 'unknown')}",
|
||||
f" [dim]Subject:[/dim] {plan_info.get('subject', 'N/A')}",
|
||||
@@ -114,7 +115,8 @@ def format_plan_deletion_header(plan_key: str, plan_info: Dict[str, Any]) -> str
|
||||
def format_plan_error(
|
||||
error_type: str,
|
||||
plan_num: str | None = None,
|
||||
details: str | None = None
|
||||
details: str | None = None,
|
||||
prefix: str = "FPLAN",
|
||||
) -> str:
|
||||
"""
|
||||
Format error messages for plan operations
|
||||
@@ -123,12 +125,13 @@ def format_plan_error(
|
||||
error_type: Type of error ("not_found", "invalid_number", "general")
|
||||
plan_num: Plan number if relevant
|
||||
details: Additional error details
|
||||
prefix: Plan prefix (e.g. "FPLAN", "DPLAN")
|
||||
|
||||
Returns:
|
||||
Formatted error message
|
||||
"""
|
||||
if error_type == "not_found":
|
||||
return f"[ERROR] FPLAN-{plan_num} not found in registry"
|
||||
return f"[ERROR] {prefix}-{plan_num} not found in registry"
|
||||
elif error_type == "invalid_number":
|
||||
return f"[ERROR] Invalid plan number: {plan_num}"
|
||||
elif error_type == "general":
|
||||
@@ -137,30 +140,32 @@ def format_plan_error(
|
||||
return "[ERROR] Unknown error"
|
||||
|
||||
|
||||
def format_plan_deletion_success(plan_key: str) -> str:
|
||||
def format_plan_deletion_success(plan_key: str, prefix: str = "FPLAN") -> str:
|
||||
"""
|
||||
Format success message for completed plan deletion
|
||||
|
||||
Args:
|
||||
plan_key: Normalized plan number (e.g., "0001")
|
||||
prefix: Plan prefix (e.g. "FPLAN", "DPLAN")
|
||||
|
||||
Returns:
|
||||
Formatted success message
|
||||
"""
|
||||
return f"\n[SUCCESS] FPLAN-{plan_key} deleted successfully\n"
|
||||
return f"\n[SUCCESS] {prefix}-{plan_key} closed successfully\n"
|
||||
|
||||
|
||||
def format_registry_removal_status(plan_key: str) -> str:
|
||||
def format_registry_removal_status(plan_key: str, prefix: str = "FPLAN") -> str:
|
||||
"""
|
||||
Format status message for registry removal
|
||||
|
||||
Args:
|
||||
plan_key: Normalized plan number (e.g., "0001")
|
||||
prefix: Plan prefix (e.g. "FPLAN", "DPLAN")
|
||||
|
||||
Returns:
|
||||
Formatted status message
|
||||
"""
|
||||
return f"[OK] Removed FPLAN-{plan_key} from registry"
|
||||
return f"[OK] Removed {prefix}-{plan_key} from registry"
|
||||
|
||||
|
||||
def format_deletion_cancelled() -> str:
|
||||
@@ -192,13 +197,14 @@ def format_delete_usage_error() -> str:
|
||||
|
||||
# RESTORE PLAN DISPLAY FUNCTIONS
|
||||
|
||||
def format_restore_header(plan_key: str, plan_info: Dict[str, Any]) -> str:
|
||||
def format_restore_header(plan_key: str, plan_info: Dict[str, Any], prefix: str = "FPLAN") -> str:
|
||||
"""
|
||||
Format plan information header for restore confirmation
|
||||
|
||||
Args:
|
||||
plan_key: Normalized plan number (e.g., "0001")
|
||||
plan_info: Plan metadata dictionary from registry
|
||||
prefix: Plan prefix (e.g. "FPLAN", "DPLAN")
|
||||
|
||||
Returns:
|
||||
Formatted header string with Rich markup for plan details
|
||||
@@ -209,7 +215,7 @@ def format_restore_header(plan_key: str, plan_info: Dict[str, Any]) -> str:
|
||||
|
||||
lines = [
|
||||
"",
|
||||
"[bold cyan]╭─ Restore FPLAN-" + plan_key + " ─╮[/bold cyan]",
|
||||
f"[bold cyan]╭─ Restore {prefix}-" + plan_key + " ─╮[/bold cyan]",
|
||||
"",
|
||||
f" [dim]Location:[/dim] {plan_info.get('relative_path', 'unknown')}",
|
||||
f" [dim]Subject:[/dim] {plan_info.get('subject', 'N/A')}",
|
||||
@@ -224,23 +230,24 @@ def format_restore_header(plan_key: str, plan_info: Dict[str, Any]) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def format_restore_success(plan_key: str, restored_location: str | None = None) -> str:
|
||||
def format_restore_success(plan_key: str, restored_location: str | None = None, prefix: str = "FPLAN") -> str:
|
||||
"""
|
||||
Format success message for completed plan restore
|
||||
|
||||
Args:
|
||||
plan_key: Normalized plan number (e.g., "0001")
|
||||
restored_location: Where the plan was restored to
|
||||
prefix: Plan prefix (e.g. "FPLAN", "DPLAN")
|
||||
|
||||
Returns:
|
||||
Formatted success message
|
||||
"""
|
||||
if restored_location:
|
||||
return f"\n[SUCCESS] FPLAN-{plan_key} restored to open status at: {restored_location}\n"
|
||||
return f"\n[SUCCESS] FPLAN-{plan_key} restored to open status\n"
|
||||
return f"\n[SUCCESS] {prefix}-{plan_key} restored to open status at: {restored_location}\n"
|
||||
return f"\n[SUCCESS] {prefix}-{plan_key} restored to open status\n"
|
||||
|
||||
|
||||
def format_restore_error(error_type: str, plan_key: str | None = None, details: str | None = None) -> str:
|
||||
def format_restore_error(error_type: str, plan_key: str | None = None, details: str | None = None, prefix: str = "FPLAN") -> str:
|
||||
"""
|
||||
Format error messages for restore operations
|
||||
|
||||
@@ -248,16 +255,17 @@ def format_restore_error(error_type: str, plan_key: str | None = None, details:
|
||||
error_type: Type of error ("not_found", "already_open", "file_missing", "invalid_number", "general")
|
||||
plan_key: Plan number if relevant
|
||||
details: Additional error details
|
||||
prefix: Plan prefix (e.g. "FPLAN", "DPLAN")
|
||||
|
||||
Returns:
|
||||
Formatted error message
|
||||
"""
|
||||
if error_type == "not_found":
|
||||
return f"[ERROR] FPLAN-{plan_key} not found in registry"
|
||||
return f"[ERROR] {prefix}-{plan_key} not found in registry"
|
||||
elif error_type == "already_open":
|
||||
return f"[ERROR] FPLAN-{plan_key} is already open - cannot restore what isn't closed"
|
||||
return f"[ERROR] {prefix}-{plan_key} is already open - cannot restore what isn't closed"
|
||||
elif error_type == "file_missing":
|
||||
return f"[ERROR] FPLAN-{plan_key} file not found at registered location - move file back first"
|
||||
return f"[ERROR] {prefix}-{plan_key} file not found at registered location - move file back first"
|
||||
elif error_type == "invalid_number":
|
||||
return f"[ERROR] Invalid plan number: {plan_key}"
|
||||
elif error_type == "general":
|
||||
@@ -285,19 +293,25 @@ def format_restore_usage_error() -> str:
|
||||
|
||||
# LIST PLAN DISPLAY FUNCTIONS
|
||||
|
||||
def format_plan_info(plan_key: str, plan_info: Dict[str, Any]) -> str:
|
||||
def format_plan_info(plan_key: str, plan_info: Dict[str, Any], prefix: str = "FPLAN") -> str:
|
||||
"""
|
||||
Format a single plan's information for display
|
||||
|
||||
Args:
|
||||
plan_key: Plan number (e.g., "0001")
|
||||
plan_info: Plan metadata dictionary
|
||||
prefix: Plan prefix (e.g. "FPLAN", "DPLAN").
|
||||
Overridden by ``_source_prefix`` in *plan_info* when present
|
||||
(set by list_ops when merging multi-registry plans).
|
||||
|
||||
Returns:
|
||||
Formatted string with plan details
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
# Use source prefix if tagged by list_ops, otherwise fall back to arg
|
||||
display_prefix = plan_info.get("_source_prefix", prefix)
|
||||
|
||||
subject = plan_info.get("subject", "No subject")
|
||||
location = plan_info.get("relative_path", "unknown")
|
||||
status = plan_info.get("status", "unknown")
|
||||
@@ -311,7 +325,7 @@ def format_plan_info(plan_key: str, plan_info: Dict[str, Any]) -> str:
|
||||
except (ValueError, AttributeError):
|
||||
pass # Keep original value if parsing fails
|
||||
|
||||
return f" FPLAN-{plan_key} [{status:>6}] {location:<30} {subject:<40} {created}"
|
||||
return f" {display_prefix}-{plan_key} [{status:>6}] {location:<30} {subject:<40} {created}"
|
||||
|
||||
|
||||
def format_plans_list(
|
||||
|
||||
@@ -29,27 +29,51 @@ from aipass.flow.apps.handlers.registry.load_registry import load_registry
|
||||
# HANDLER FUNCTION
|
||||
# =============================================
|
||||
|
||||
def _get_all_registry_files() -> List[str]:
|
||||
"""Return per-type registry filenames via plan-type discovery."""
|
||||
try:
|
||||
from aipass.flow.apps.handlers.template.plan_type_loader import discover_plan_types # type: ignore[import-not-found]
|
||||
files: List[str] = []
|
||||
for _key, config in discover_plan_types().items():
|
||||
rf = config.get("registry_file")
|
||||
if rf and rf not in files:
|
||||
files.append(rf)
|
||||
if files:
|
||||
return files
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
def get_open_plans() -> List[Tuple[str, Dict[str, Any]]]:
|
||||
"""
|
||||
Get all open plans from registry
|
||||
Get all open plans from ALL per-type registries.
|
||||
|
||||
Returns:
|
||||
List of tuples: [(plan_num, plan_info), ...]
|
||||
Empty list if no open plans found
|
||||
|
||||
Example:
|
||||
>>> plans = get_open_plans()
|
||||
>>> for plan_num, plan_info in plans:
|
||||
... print(f"PLAN{plan_num}: {plan_info['subject']}")
|
||||
"""
|
||||
# Load registry
|
||||
registry = load_registry()
|
||||
open_plans: List[Tuple[str, Dict[str, Any]]] = []
|
||||
reg_files = _get_all_registry_files()
|
||||
|
||||
# Filter for open plans
|
||||
open_plans = [
|
||||
(plan_num, plan_info)
|
||||
for plan_num, plan_info in registry.get("plans", {}).items()
|
||||
if plan_info.get("status") == "open"
|
||||
]
|
||||
if reg_files:
|
||||
for reg_file in reg_files:
|
||||
try:
|
||||
registry = load_registry(registry_file=reg_file)
|
||||
open_plans.extend(
|
||||
(plan_num, plan_info)
|
||||
for plan_num, plan_info in registry.get("plans", {}).items()
|
||||
if plan_info.get("status") == "open"
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
else:
|
||||
# Fallback: load default registry
|
||||
registry = load_registry()
|
||||
open_plans = [
|
||||
(plan_num, plan_info)
|
||||
for plan_num, plan_info in registry.get("plans", {}).items()
|
||||
if plan_info.get("status") == "open"
|
||||
]
|
||||
|
||||
return open_plans
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: list_ops.py
|
||||
# Description: Plan Listing Implementation Handler
|
||||
# Version: 1.0.0
|
||||
# Version: 2.0.0
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# Modified: 2026-03-17
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Plan Listing Operations Handler
|
||||
|
||||
Implements plan listing business logic, extracted from list_plans module.
|
||||
Loads registry, filters plans, gets statistics, and returns data for display.
|
||||
Plan-type-agnostic: loads ALL per-type registries and merges plans for display.
|
||||
|
||||
Usage:
|
||||
from aipass.flow.apps.handlers.plan.list_ops import list_plans_impl
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, Any, Tuple
|
||||
|
||||
from aipass.prax import logger
|
||||
# logger imported from aipass.prax
|
||||
|
||||
# =============================================
|
||||
# CONFIGURATION
|
||||
@@ -28,6 +27,31 @@ from aipass.prax import logger
|
||||
MODULE_NAME = "list_plans"
|
||||
|
||||
|
||||
def _get_all_registry_info() -> Tuple[list[str], Dict[str, str]]:
|
||||
"""Return per-type registry filenames and a registry_file -> prefix map.
|
||||
|
||||
Returns:
|
||||
(registry_files, prefix_map) where prefix_map maps e.g.
|
||||
"dplan_registry.json" -> "DPLAN". Empty list means caller
|
||||
should fall back to the default registry.
|
||||
"""
|
||||
try:
|
||||
from aipass.flow.apps.handlers.template.plan_type_loader import discover_plan_types # type: ignore[import-not-found]
|
||||
files: list[str] = []
|
||||
prefix_map: Dict[str, str] = {}
|
||||
for _key, config in discover_plan_types().items():
|
||||
rf = config.get("registry_file")
|
||||
if rf:
|
||||
prefix_map[rf] = config.get("prefix", "FPLAN")
|
||||
if rf not in files:
|
||||
files.append(rf)
|
||||
if files:
|
||||
return files, prefix_map
|
||||
except Exception:
|
||||
pass
|
||||
return [], {} # empty means caller should fall back to default
|
||||
|
||||
|
||||
# =============================================
|
||||
# LIST PLANS IMPLEMENTATION
|
||||
# =============================================
|
||||
@@ -35,13 +59,13 @@ MODULE_NAME = "list_plans"
|
||||
def list_plans_impl(
|
||||
filter_type: str = "open",
|
||||
# Dependencies injected from module
|
||||
load_registry=None,
|
||||
get_registry_statistics=None,
|
||||
format_plans_list=None,
|
||||
format_statistics_summary=None,
|
||||
load_registry: Any = None,
|
||||
get_registry_statistics: Any = None,
|
||||
format_plans_list: Any = None,
|
||||
format_statistics_summary: Any = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Implement plan listing workflow
|
||||
Implement plan listing workflow across all plan-type registries.
|
||||
|
||||
Args:
|
||||
filter_type: Filter plans by status ("open", "closed", "all")
|
||||
@@ -55,14 +79,31 @@ def list_plans_impl(
|
||||
empty (bool), filter_type (str)
|
||||
"""
|
||||
try:
|
||||
# STEP 1: Load registry (handler)
|
||||
registry = load_registry()
|
||||
# STEP 1: Load ALL per-type registries and merge plans
|
||||
merged_plans: Dict[str, Any] = {}
|
||||
reg_files, reg_prefix_map = _get_all_registry_info()
|
||||
|
||||
# STEP 2: Get plans
|
||||
plans = registry.get("plans", {})
|
||||
if reg_files:
|
||||
for reg_file in reg_files:
|
||||
try:
|
||||
registry = load_registry(registry_file=reg_file)
|
||||
source_prefix = reg_prefix_map.get(reg_file, "FPLAN")
|
||||
for plan_num, plan_info in registry.get("plans", {}).items():
|
||||
# Tag each plan with its source prefix for display
|
||||
plan_info["_source_prefix"] = source_prefix
|
||||
merged_plans[plan_num] = plan_info
|
||||
except Exception:
|
||||
continue
|
||||
else:
|
||||
# Fallback: load default registry
|
||||
registry = load_registry()
|
||||
merged_plans = registry.get("plans", {})
|
||||
|
||||
if not plans:
|
||||
logger.info(f"[{MODULE_NAME}] No plans in registry")
|
||||
# Build a synthetic merged registry for statistics
|
||||
merged_registry: Dict[str, Any] = {"plans": merged_plans}
|
||||
|
||||
if not merged_plans:
|
||||
logger.info(f"[{MODULE_NAME}] No plans in any registry")
|
||||
return {
|
||||
"success": True,
|
||||
"formatted_list": "",
|
||||
@@ -71,20 +112,20 @@ def list_plans_impl(
|
||||
"filter_type": filter_type,
|
||||
}
|
||||
|
||||
# STEP 3: Determine filter
|
||||
# STEP 2: Determine filter
|
||||
if filter_type == "all":
|
||||
filter_status = None
|
||||
else:
|
||||
filter_status = filter_type # "open" or "closed"
|
||||
|
||||
# STEP 4: Format plans list
|
||||
formatted_list = format_plans_list(plans, filter_status)
|
||||
# STEP 3: Format plans list
|
||||
formatted_list = format_plans_list(merged_plans, filter_status)
|
||||
|
||||
# STEP 5: Get and format statistics
|
||||
stats = get_registry_statistics(registry)
|
||||
# STEP 4: Get and format statistics
|
||||
stats = get_registry_statistics(merged_registry)
|
||||
formatted_stats = format_statistics_summary(stats)
|
||||
|
||||
# STEP 6: Log success
|
||||
# STEP 5: Log success
|
||||
logger.info(f"[{MODULE_NAME}] Listed plans (filter: {filter_type})")
|
||||
|
||||
return {
|
||||
@@ -96,7 +137,6 @@ def list_plans_impl(
|
||||
}
|
||||
|
||||
except BrokenPipeError:
|
||||
# Pipe closed by reader (e.g. automated subprocesses, head)
|
||||
logger.info(f"[{MODULE_NAME}] Broken pipe (stdout closed early)")
|
||||
return {
|
||||
"success": True,
|
||||
|
||||
@@ -1,69 +1,70 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: resolve_location.py
|
||||
# Description: Plan Location Resolution Handler
|
||||
# Version: 1.0.0
|
||||
# Version: 2.0.0
|
||||
# Created: 2025-11-29
|
||||
# Modified: 2025-11-29
|
||||
# Modified: 2026-03-17
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Plan Location Resolution Handler
|
||||
|
||||
Resolves plan locations for explicit paths.
|
||||
Resolves plan locations using the CALLER's working directory, not flow's.
|
||||
Drone sets AIPASS_CALLER_CWD env var before invoking flow, so relative
|
||||
paths (like ".") resolve to where the user actually is.
|
||||
|
||||
@ symbols are pre-resolved by Drone before Flow receives args.
|
||||
Handles absolute paths, relative paths, and defaults to cwd.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
def _get_caller_cwd() -> Path:
|
||||
"""Return the caller's working directory.
|
||||
|
||||
Drone passes this via AIPASS_CALLER_CWD env var. Falls back to
|
||||
Path.cwd() for direct invocation (testing, standalone).
|
||||
"""
|
||||
caller_cwd = os.environ.get("AIPASS_CALLER_CWD")
|
||||
if caller_cwd:
|
||||
return Path(caller_cwd)
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def resolve_plan_location(
|
||||
location: str | None,
|
||||
ecosystem_root: Path
|
||||
) -> Tuple[bool, Path, str]:
|
||||
"""
|
||||
Resolve plan location for explicit paths
|
||||
Resolve plan location relative to the CALLER's directory.
|
||||
|
||||
Handles two location types:
|
||||
1. Explicit path: Resolved to absolute path
|
||||
2. None: Defaults to current working directory
|
||||
|
||||
Note: @ symbols are pre-resolved by Drone before Flow receives args.
|
||||
Flow only handles absolute/relative paths.
|
||||
|
||||
Validates that resolved directory exists.
|
||||
Plans are created where the caller is, not where flow lives.
|
||||
Drone passes AIPASS_CALLER_CWD so "." resolves to the caller's CWD.
|
||||
|
||||
Args:
|
||||
location: Target location (absolute/relative path, or None for cwd)
|
||||
ecosystem_root: Root directory (kept for API compatibility, not used)
|
||||
location: Target location (absolute/relative path, ".", or None)
|
||||
ecosystem_root: Root directory (kept for API compatibility)
|
||||
|
||||
Returns:
|
||||
Tuple of (success, resolved_path, error_message)
|
||||
- success: False if directory doesn't exist
|
||||
- resolved_path: Absolute path to directory (or Path.cwd() on error)
|
||||
- error_message: Empty string on success, error details on failure
|
||||
|
||||
Examples:
|
||||
>>> resolve_plan_location("/repo/src/aipass/flow", Path("/repo/src/aipass"))
|
||||
(True, Path("/repo/src/aipass/flow"), "")
|
||||
|
||||
>>> resolve_plan_location(None, Path("/repo/src/aipass"))
|
||||
(True, Path.cwd(), "")
|
||||
|
||||
>>> resolve_plan_location("/nonexistent", Path("/repo/src/aipass"))
|
||||
(False, Path.cwd(), "Directory /nonexistent does not exist")
|
||||
"""
|
||||
# Determine target directory
|
||||
caller_cwd = _get_caller_cwd()
|
||||
|
||||
if location:
|
||||
# Handle explicit path
|
||||
target_dir = Path(location).resolve()
|
||||
loc_path = Path(location)
|
||||
if loc_path.is_absolute():
|
||||
target_dir = loc_path.resolve()
|
||||
else:
|
||||
# Resolve relative paths against CALLER's CWD, not flow's
|
||||
target_dir = (caller_cwd / loc_path).resolve()
|
||||
else:
|
||||
# Default to current working directory
|
||||
target_dir = Path.cwd()
|
||||
# No location provided — use caller's CWD
|
||||
target_dir = caller_cwd
|
||||
|
||||
# Validate directory exists
|
||||
if not target_dir.exists():
|
||||
return False, Path.cwd(), f"Directory {target_dir} does not exist"
|
||||
return False, caller_cwd, f"Directory {target_dir} does not exist"
|
||||
|
||||
return True, target_dir, ""
|
||||
|
||||
@@ -32,6 +32,7 @@ from aipass.prax import logger
|
||||
|
||||
_PKG_ROOT = Path(__file__).resolve().parents[4] # handlers/plan/ -> handlers/ -> apps/ -> flow/ -> aipass/
|
||||
FLOW_ROOT = _PKG_ROOT / "flow"
|
||||
PROCESSED_PLANS_DIR = _PKG_ROOT / "backup" / "processed_plans"
|
||||
|
||||
MODULE_NAME = "restore_plan"
|
||||
|
||||
@@ -40,9 +41,12 @@ MODULE_NAME = "restore_plan"
|
||||
# RECOVERY IMPLEMENTATION
|
||||
# =============================================
|
||||
|
||||
def recover_plan_from_backup(plan_key: str, load_registry=None, save_registry=None) -> tuple[bool, str]:
|
||||
def recover_plan_from_backup(plan_key: str, load_registry: Any = None, save_registry: Any = None) -> tuple[bool, str]:
|
||||
"""
|
||||
Attempt to recover a plan from processed_plans backup
|
||||
Attempt to recover a plan from processed_plans backup.
|
||||
|
||||
Plan-type-agnostic: searches for any prefix matching the plan key
|
||||
(e.g. FPLAN-0165, DPLAN-0165).
|
||||
|
||||
Args:
|
||||
plan_key: Normalized plan number (e.g., "0165")
|
||||
@@ -52,19 +56,18 @@ def recover_plan_from_backup(plan_key: str, load_registry=None, save_registry=No
|
||||
Returns:
|
||||
(success, message)
|
||||
"""
|
||||
# Check processed_plans directory
|
||||
processed_plans = FLOW_ROOT / "processed_plans"
|
||||
plan_file = processed_plans / f"FPLAN-{plan_key}.md"
|
||||
# Check backup processed_plans directory
|
||||
processed_plans = PROCESSED_PLANS_DIR
|
||||
|
||||
# CRITICAL: If base file doesn't exist, or if timestamp variants exist, use the NEWEST backup
|
||||
# This handles cases where plan was closed multiple times from different locations
|
||||
variants = list(processed_plans.glob(f"FPLAN-{plan_key}*.md"))
|
||||
# Search for any prefix matching the plan key (FPLAN-, DPLAN-, etc.)
|
||||
variants = list(processed_plans.glob(f"*-{plan_key}*.md")) if processed_plans.exists() else []
|
||||
plan_file = processed_plans / f"FPLAN-{plan_key}.md" # fallback default
|
||||
if variants:
|
||||
# Sort by modification time, newest first
|
||||
variants.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
plan_file = variants[0] # Use most recent backup
|
||||
elif not plan_file.exists():
|
||||
return False, f"FPLAN-{plan_key} not found in backups"
|
||||
return False, f"Plan {plan_key} not found in backups"
|
||||
|
||||
# Read plan file to extract original location from header
|
||||
try:
|
||||
@@ -114,10 +117,13 @@ def recover_plan_from_backup(plan_key: str, load_registry=None, save_registry=No
|
||||
original_location = str(FLOW_ROOT)
|
||||
relative_path = "flow"
|
||||
|
||||
# Copy file to ORIGINAL location (preserve backup)
|
||||
target = Path(original_location) / f"FPLAN-{plan_key}.md"
|
||||
# Copy file to ORIGINAL location (preserve backup) using the original filename
|
||||
target = Path(original_location) / plan_file.name
|
||||
copy2(plan_file, target)
|
||||
|
||||
# Derive display label from the backup filename
|
||||
plan_label = plan_file.stem # e.g. "FPLAN-0165" or "DPLAN-0004"
|
||||
|
||||
# Create minimal registry entry
|
||||
registry = load_registry()
|
||||
registry["plans"][plan_key] = {
|
||||
@@ -133,7 +139,7 @@ def recover_plan_from_backup(plan_key: str, load_registry=None, save_registry=No
|
||||
}
|
||||
save_registry(registry)
|
||||
|
||||
return True, f"Recovered FPLAN-{plan_key} from {plan_file.name} to {original_location}"
|
||||
return True, f"Recovered {plan_label} from {plan_file.name} to {original_location}"
|
||||
|
||||
|
||||
# =============================================
|
||||
@@ -143,14 +149,14 @@ def recover_plan_from_backup(plan_key: str, load_registry=None, save_registry=No
|
||||
def restore_plan_impl(
|
||||
plan_num: str | None = None,
|
||||
# Dependencies injected from module
|
||||
normalize_plan_number=None,
|
||||
load_registry=None,
|
||||
save_registry=None,
|
||||
validate_plan_exists=None,
|
||||
recover_plan_from_backup_fn=None,
|
||||
scan_plan_files=None,
|
||||
update_dashboard_local=None,
|
||||
push_to_plans_central=None,
|
||||
normalize_plan_number: Any = None,
|
||||
load_registry: Any = None,
|
||||
save_registry: Any = None,
|
||||
validate_plan_exists: Any = None,
|
||||
recover_plan_from_backup_fn: Any = None,
|
||||
scan_plan_files: Any = None,
|
||||
update_dashboard_local: Any = None,
|
||||
push_to_plans_central: Any = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Implement plan restore workflow
|
||||
|
||||
@@ -10,17 +10,36 @@
|
||||
Plan Validation Handler
|
||||
|
||||
Validates and normalizes plan numbers and registry entries.
|
||||
Plan-type-agnostic: handles FPLAN-, DPLAN-, and future prefixes.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Dict, Any, Tuple
|
||||
|
||||
# Matches any PREFIX- at the start (e.g. FPLAN-, DPLAN-, XPLAN-)
|
||||
_PREFIX_RE = re.compile(r'^([A-Z]+PLAN)-', re.IGNORECASE)
|
||||
|
||||
|
||||
def extract_prefix(plan_num: str) -> str | None:
|
||||
"""Extract the plan-type prefix from a plan identifier.
|
||||
|
||||
Args:
|
||||
plan_num: Raw input like ``"DPLAN-0004"`` or ``"42"``.
|
||||
|
||||
Returns:
|
||||
Uppercase prefix (e.g. ``"DPLAN"``) or ``None`` if no prefix found.
|
||||
"""
|
||||
if not isinstance(plan_num, str):
|
||||
return None
|
||||
m = _PREFIX_RE.match(plan_num.strip())
|
||||
return m.group(1).upper() if m else None
|
||||
|
||||
|
||||
def normalize_plan_number(plan_num: str) -> str:
|
||||
"""
|
||||
Normalize plan number to 4-digit format
|
||||
|
||||
Accepts various formats ("1", "42", "0001") and normalizes
|
||||
to standard 4-digit format ("0001", "0042", "0001").
|
||||
Strips any known prefix (FPLAN-, DPLAN-, PLAN-, etc.).
|
||||
|
||||
Args:
|
||||
plan_num: Plan number in any format
|
||||
@@ -34,25 +53,18 @@ def normalize_plan_number(plan_num: str) -> str:
|
||||
Examples:
|
||||
>>> normalize_plan_number("1")
|
||||
"0001"
|
||||
>>> normalize_plan_number("42")
|
||||
"0042"
|
||||
>>> normalize_plan_number("0001")
|
||||
"0001"
|
||||
>>> normalize_plan_number("FPLAN-0042")
|
||||
"0042"
|
||||
>>> normalize_plan_number("PLAN4444")
|
||||
"4444"
|
||||
>>> normalize_plan_number("DPLAN-0004")
|
||||
"0004"
|
||||
"""
|
||||
# Normalize prefix variations for robustness
|
||||
if isinstance(plan_num, str):
|
||||
upper = plan_num.upper()
|
||||
# Strip FPLAN- prefix (standard format)
|
||||
if upper.startswith("FPLAN-"):
|
||||
plan_num = plan_num[6:]
|
||||
# Strip PLAN- prefix (alternate format)
|
||||
upper = plan_num.upper().strip()
|
||||
m = _PREFIX_RE.match(upper)
|
||||
if m:
|
||||
plan_num = plan_num[m.end():]
|
||||
elif upper.startswith("PLAN-"):
|
||||
plan_num = plan_num[5:]
|
||||
# Strip PLAN prefix without dash (e.g., PLAN4444)
|
||||
elif upper.startswith("PLAN"):
|
||||
plan_num = plan_num[4:]
|
||||
return f"{int(plan_num):04d}"
|
||||
@@ -70,12 +82,7 @@ def validate_plan_exists(plan_key: str, registry: Dict[str, Any]) -> Tuple[bool,
|
||||
Tuple of (exists, error_message)
|
||||
- exists: True if plan found in registry
|
||||
- error_message: None if exists, error string if not found
|
||||
|
||||
Examples:
|
||||
>>> exists, error = validate_plan_exists("0001", registry)
|
||||
>>> if not exists:
|
||||
... print(error)
|
||||
"""
|
||||
if plan_key not in registry.get("plans", {}):
|
||||
return False, f"FPLAN-{plan_key} not found in registry"
|
||||
return False, f"Plan {plan_key} not found in registry"
|
||||
return True, None
|
||||
|
||||
@@ -12,7 +12,7 @@ Load Registry Handler
|
||||
Loads the Flow PLAN registry from JSON file with error handling.
|
||||
|
||||
Features:
|
||||
- Loads flow_registry.json
|
||||
- Loads fplan_registry.json
|
||||
- Returns default structure if file missing
|
||||
- Graceful error handling
|
||||
- Reusable across Flow modules
|
||||
@@ -36,7 +36,7 @@ FLOW_ROOT = _PKG_ROOT / "flow"
|
||||
|
||||
MODULE_NAME = "load_registry"
|
||||
FLOW_JSON_DIR = FLOW_ROOT / "flow_json"
|
||||
REGISTRY_FILE = FLOW_JSON_DIR / "flow_registry.json"
|
||||
REGISTRY_FILE = FLOW_JSON_DIR / "fplan_registry.json"
|
||||
|
||||
# =============================================
|
||||
# HANDLER FUNCTION
|
||||
@@ -49,7 +49,7 @@ def load_registry(registry_file: str | None = None) -> Dict[str, Any]:
|
||||
registry_file: Optional filename (e.g. "fplan_registry.json",
|
||||
"dplan_registry.json"). When provided, loads from
|
||||
``FLOW_JSON_DIR / registry_file`` instead of the default
|
||||
``flow_registry.json``.
|
||||
``fplan_registry.json``.
|
||||
|
||||
Returns:
|
||||
Dict containing:
|
||||
|
||||
@@ -12,7 +12,7 @@ Save Registry Handler
|
||||
Saves the Flow PLAN registry to JSON file with automatic timestamp updates.
|
||||
|
||||
Features:
|
||||
- Saves flow_registry.json
|
||||
- Saves fplan_registry.json
|
||||
- Auto-updates last_updated timestamp
|
||||
- Creates directory if missing
|
||||
- Graceful error handling
|
||||
@@ -39,7 +39,7 @@ FLOW_ROOT = _PKG_ROOT / "flow"
|
||||
|
||||
MODULE_NAME = "save_registry"
|
||||
FLOW_JSON_DIR = FLOW_ROOT / "flow_json"
|
||||
REGISTRY_FILE = FLOW_JSON_DIR / "flow_registry.json"
|
||||
REGISTRY_FILE = FLOW_JSON_DIR / "fplan_registry.json"
|
||||
|
||||
# =============================================
|
||||
# HANDLER FUNCTION
|
||||
@@ -53,7 +53,7 @@ def save_registry(registry: Dict[str, Any], registry_file: str | None = None) ->
|
||||
registry_file: Optional filename (e.g. "fplan_registry.json",
|
||||
"dplan_registry.json"). When provided, saves to
|
||||
``FLOW_JSON_DIR / registry_file`` instead of the default
|
||||
``flow_registry.json``.
|
||||
``fplan_registry.json``.
|
||||
|
||||
Returns:
|
||||
True if save successful, False on error
|
||||
|
||||
@@ -105,18 +105,18 @@ def _display_messages(messages: List[Dict[str, Any]]):
|
||||
error(msg['text'])
|
||||
|
||||
elif msg_type == "header":
|
||||
console.print(format_plan_deletion_header(msg["plan_key"], msg["plan_info"]))
|
||||
console.print(format_plan_deletion_header(msg["plan_key"], msg["plan_info"], prefix=msg.get("prefix", "FPLAN")))
|
||||
|
||||
elif msg_type == "cancelled":
|
||||
console.print(format_deletion_cancelled())
|
||||
|
||||
elif msg_type == "close_success":
|
||||
console.print(format_plan_deletion_success(msg["plan_key"]))
|
||||
console.print(format_plan_deletion_success(msg["plan_key"], prefix=msg.get("prefix", "FPLAN")))
|
||||
|
||||
elif msg_type == "plan_list":
|
||||
warning(f"Found {msg['count']} open plan(s) to close:")
|
||||
for plan in msg.get("plans", []):
|
||||
console.print(f" * FPLAN-{plan['plan_num']}: {plan['subject']}")
|
||||
console.print(f" * {plan.get('prefix', 'FPLAN')}-{plan['plan_num']}: {plan['subject']}")
|
||||
|
||||
elif msg_type == "confirm_warning":
|
||||
error(f"WARNING: This will close all {msg['count']} plans!")
|
||||
@@ -126,7 +126,7 @@ def _display_messages(messages: List[Dict[str, Any]]):
|
||||
console.print("-" * 60)
|
||||
|
||||
elif msg_type == "closing_single":
|
||||
console.print(f"\n[dim]Closing FPLAN-{msg['plan_num']}...[/dim]")
|
||||
console.print(f"\n[dim]Closing {msg.get('prefix', 'FPLAN')}-{msg['plan_num']}...[/dim]")
|
||||
|
||||
elif msg_type == "close_all_summary":
|
||||
console.print("\n" + "=" * 60)
|
||||
@@ -290,13 +290,15 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
# 2. VALIDATE: Check for parsing errors
|
||||
if error:
|
||||
console.print(format_delete_usage_error())
|
||||
return False
|
||||
return True # Command was handled (error already displayed)
|
||||
|
||||
# 3. EXECUTE: Run workflow orchestrator
|
||||
success = close_plan(plan_num=plan_num, confirm=confirm, all_plans=all_plans)
|
||||
close_plan(plan_num=plan_num, confirm=confirm, all_plans=all_plans)
|
||||
|
||||
# 4. RETURN: Result (close_plan already handles all output)
|
||||
return success
|
||||
# 4. RETURN: True = command was handled (even if the operation failed,
|
||||
# the error has already been displayed -- returning False would cause
|
||||
# flow.py to print a spurious "Unknown command" message)
|
||||
return True
|
||||
|
||||
|
||||
# =============================================
|
||||
|
||||
@@ -275,11 +275,10 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
)
|
||||
console.print(result_msg)
|
||||
|
||||
# Return boolean result
|
||||
if success:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
# Command was handled (even if the operation failed, the error has
|
||||
# already been displayed -- returning False would cause flow.py to
|
||||
# print a spurious "Unknown command" message)
|
||||
return True
|
||||
|
||||
|
||||
# =============================================
|
||||
|
||||
@@ -1,591 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: dplan_flow.py
|
||||
# Description: Plan management module (thin orchestrator)
|
||||
# Version: 5.0.0
|
||||
# Created: 2025-12-02
|
||||
# Modified: 2025-12-02
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Plan Management Module - Thin Orchestrator
|
||||
|
||||
Routes commands to handlers in handlers/dplan/.
|
||||
Manages numbered, dated planning documents (DPLAN, BPLAN) in dev_planning/.
|
||||
Supports @ branch resolution for creating plans in other branches.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
# Infrastructure imports (module does the logging)
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
from aipass.cli.apps.modules import console, header, success, error, warning
|
||||
|
||||
# Handler imports (local handlers in handlers/dplan/)
|
||||
from aipass.flow.apps.handlers.dplan.create import create_plan
|
||||
from aipass.flow.apps.handlers.dplan.list import list_plans
|
||||
from aipass.flow.apps.handlers.dplan.status import get_status_summary, get_status_icon, VALID_TAGS
|
||||
from aipass.flow.apps.handlers.dplan.display import show_help, print_introspection
|
||||
from aipass.flow.apps.handlers.dplan.close import (
|
||||
normalize_plan_number, close_plan, get_open_plans
|
||||
)
|
||||
from aipass.flow.apps.handlers.dplan.counter import VALID_PLAN_TYPES
|
||||
from aipass.flow.apps.handlers.dplan.registry import (
|
||||
register_plan, update_plan_status, populate_from_filesystem,
|
||||
get_summary, save_plan_summary, generate_description_summary
|
||||
)
|
||||
from aipass.flow.apps.handlers.dplan.dashboard import push_all as _push_dashboard_raw
|
||||
from aipass.prax.apps.handlers.dashboard.operations import write_section
|
||||
|
||||
# Local handlers (file I/O extracted from this module)
|
||||
from aipass.flow.apps.handlers.dplan.branch_resolve import resolve_branch_target as _resolve_branch
|
||||
from aipass.flow.apps.handlers.dplan.closed_plans_registry import append_closed_dplan
|
||||
from aipass.flow.apps.handlers.dplan.log_setup import prepare_log_file
|
||||
from aipass.flow.apps.handlers.dplan.background_spawn import spawn_post_close
|
||||
|
||||
|
||||
def push_dashboard(activity: str | None = None) -> dict:
|
||||
"""Module-level wrapper: injects write_section into handler."""
|
||||
return _push_dashboard_raw(activity=activity, write_fn=write_section)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# @ BRANCH RESOLUTION (thin wrapper around handler)
|
||||
# =============================================================================
|
||||
|
||||
def resolve_branch_target(branch_ref: str):
|
||||
"""Resolve @branch reference — delegates to handler, logs result."""
|
||||
result = _resolve_branch(branch_ref)
|
||||
if not result["success"]:
|
||||
logger.warning(f"[dev_flow] {result['error']}")
|
||||
return None
|
||||
return result["path"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MODULE INTERFACE
|
||||
# =============================================================================
|
||||
|
||||
def print_introspection():
|
||||
"""Display module introspection info."""
|
||||
console.print()
|
||||
console.print("dplan_flow Module")
|
||||
console.print("Plan management orchestrator — routes plan commands to handlers")
|
||||
console.print()
|
||||
console.print("Connected Handlers:")
|
||||
console.print(" handlers/dplan/")
|
||||
console.print(" - create.py (create_plan — create new plans)")
|
||||
console.print(" - list.py (list_plans — list plans with filters)")
|
||||
console.print(" - status.py (get_status_summary — plan status aggregation)")
|
||||
console.print(" - display.py (show_help — help text display)")
|
||||
console.print(" - close.py (close_plan, get_open_plans — close plans)")
|
||||
console.print(" - counter.py (VALID_PLAN_TYPES — plan type definitions)")
|
||||
console.print(" - registry.py (register_plan, update_plan_status — registry ops)")
|
||||
console.print(" - dashboard.py (push_all — dashboard updates)")
|
||||
console.print(" - branch_resolve.py (resolve_branch_target — @ branch resolution)")
|
||||
console.print(" - closed_plans_registry.py (append_closed_dplan — closed plan tracking)")
|
||||
console.print(" - log_setup.py (prepare_log_file — log file preparation)")
|
||||
console.print(" - background_spawn.py (spawn_post_close — background archival)")
|
||||
console.print()
|
||||
console.print(" External:")
|
||||
console.print(" - aipass.prax (write_section — dashboard section writer)")
|
||||
console.print()
|
||||
|
||||
|
||||
def print_help():
|
||||
"""Display D-PLAN help text — thin wrapper around handler's show_help()."""
|
||||
header("D-PLAN - Development Planning")
|
||||
console.print(show_help())
|
||||
|
||||
|
||||
def handle_command(command: str, args: List[str]) -> bool:
|
||||
"""
|
||||
Handle D-PLAN commands - routes to handlers.
|
||||
|
||||
Args:
|
||||
command: Command to execute ('plan')
|
||||
args: Command arguments
|
||||
|
||||
Returns:
|
||||
True if command was handled, False otherwise
|
||||
"""
|
||||
if command != 'plan':
|
||||
return False
|
||||
|
||||
# Handle --help / -h flag
|
||||
if not args or (args[0] in ('--help', '-h')):
|
||||
print_help()
|
||||
return True
|
||||
|
||||
subcommand = args[0]
|
||||
|
||||
if subcommand == 'create':
|
||||
return _handle_create(args[1:])
|
||||
elif subcommand == 'list':
|
||||
return _handle_list(args[1:])
|
||||
elif subcommand == 'status':
|
||||
return _handle_status(args[1:])
|
||||
elif subcommand == 'close':
|
||||
return _handle_close(args[1:])
|
||||
elif subcommand == 'sync':
|
||||
return _handle_sync()
|
||||
else:
|
||||
error(f"Unknown subcommand: {subcommand}")
|
||||
console.print("Run 'plan --help' for usage")
|
||||
return True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COMMAND HANDLERS (orchestration only)
|
||||
# =============================================================================
|
||||
|
||||
def _handle_create(args: List[str]) -> bool:
|
||||
"""Orchestrate plan creation - delegates to handler"""
|
||||
if args and args[0] in ('--help', '-h'):
|
||||
console.print("\n[bold]USAGE:[/bold]")
|
||||
console.print(" plan create \"topic name\" [--type type] [--tag tag] [--dir subdir] [@branch]")
|
||||
console.print("\n[bold]OPTIONS:[/bold]")
|
||||
console.print(" --type <type> Plan type: dplan (default), bplan")
|
||||
console.print(" --tag <tag> Set plan tag (default: idea)")
|
||||
console.print(" --dir <name> Create plan in dev_planning/<name>/ subdirectory")
|
||||
console.print(" @<branch> Create in target branch's dev_planning/")
|
||||
console.print(f"\n[bold]TAGS:[/bold] {', '.join(VALID_TAGS)}")
|
||||
console.print("\n[bold]EXAMPLES:[/bold]")
|
||||
console.print(" plan create \"new feature design\"")
|
||||
console.print(" plan create \"API upgrade\" --tag upgrade")
|
||||
console.print(" plan create \"revenue model\" --type bplan")
|
||||
console.print(" plan create \"vera improvements\" @vera\n")
|
||||
return True
|
||||
|
||||
if len(args) < 1:
|
||||
error("Usage: plan create \"topic name\" [--type type] [--tag tag] [@branch]")
|
||||
return True
|
||||
|
||||
# Parse arguments: topic and optional flags
|
||||
topic = args[0]
|
||||
subdir = None
|
||||
tag = "idea"
|
||||
plan_type = "dplan"
|
||||
target_path = None
|
||||
target_branch_name = None
|
||||
|
||||
if '--dir' in args:
|
||||
dir_idx = args.index('--dir')
|
||||
if dir_idx + 1 < len(args):
|
||||
subdir = args[dir_idx + 1]
|
||||
else:
|
||||
error("--dir requires a subdirectory name")
|
||||
return True
|
||||
|
||||
if '--tag' in args:
|
||||
tag_idx = args.index('--tag')
|
||||
if tag_idx + 1 < len(args):
|
||||
tag = args[tag_idx + 1].lower()
|
||||
if tag not in VALID_TAGS:
|
||||
error(f"Invalid tag '{tag}'. Valid tags: {', '.join(VALID_TAGS)}")
|
||||
return True
|
||||
else:
|
||||
error("--tag requires a tag name")
|
||||
return True
|
||||
|
||||
if '--type' in args:
|
||||
type_idx = args.index('--type')
|
||||
if type_idx + 1 < len(args):
|
||||
plan_type = args[type_idx + 1].lower()
|
||||
if plan_type not in VALID_PLAN_TYPES:
|
||||
valid = ", ".join(VALID_PLAN_TYPES.keys())
|
||||
error(f"Invalid plan type '{plan_type}'. Valid types: {valid}")
|
||||
return True
|
||||
else:
|
||||
error("--type requires a plan type (dplan, bplan)")
|
||||
return True
|
||||
|
||||
# Check for @branch target or pre-resolved path
|
||||
for arg in args[1:]:
|
||||
if arg.startswith("@") and not arg.startswith("--"):
|
||||
target_branch_name = arg
|
||||
target_path = resolve_branch_target(arg)
|
||||
if target_path is None:
|
||||
error(f"Could not resolve branch target '{arg}'")
|
||||
return True
|
||||
break
|
||||
elif arg.startswith("/") and Path(arg).exists():
|
||||
target_branch_name = f"@{Path(arg).name}"
|
||||
target_path = Path(arg)
|
||||
break
|
||||
|
||||
prefix = VALID_PLAN_TYPES[plan_type]
|
||||
|
||||
# Delegate to handler
|
||||
ok, result, err = create_plan(
|
||||
topic, tag=tag, plan_type=plan_type,
|
||||
target_path=target_path, subdir=subdir
|
||||
)
|
||||
|
||||
if not ok:
|
||||
logger.error(f"[dev_flow] Failed to create plan: {err}")
|
||||
error(f"Failed to create plan: {err}")
|
||||
return True
|
||||
|
||||
logger.info(f"[dev_flow] Created {prefix}-{result['plan_number']:03d}: {result['filename']}")
|
||||
|
||||
if result.get('cache_warning'):
|
||||
logger.warning(f"[dev_flow] {result['cache_warning']}")
|
||||
|
||||
# Register in registry (only for local plans, not @ targets)
|
||||
if not target_path:
|
||||
try:
|
||||
register_plan(
|
||||
plan_number=result['plan_number'],
|
||||
topic=result['topic'],
|
||||
status="planning",
|
||||
tag=tag,
|
||||
file_path=result['path'],
|
||||
date=result['date']
|
||||
)
|
||||
logger.info(f"[dev_flow] Registered {prefix}-{result['plan_number']:03d} in registry")
|
||||
except Exception as e:
|
||||
logger.warning(f"[dev_flow] Failed to register plan: {e}")
|
||||
|
||||
try:
|
||||
activity = f"DPLAN-{result['plan_number']:03d} created ({result['topic'][:30]})"
|
||||
push_dashboard(activity=activity)
|
||||
except Exception as e:
|
||||
logger.warning(f"[dev_flow] Dashboard push failed: {e}")
|
||||
|
||||
# Display result
|
||||
console.print()
|
||||
success(f"Created {prefix}-{result['plan_number']:03d}")
|
||||
console.print(f" [dim]Topic:[/dim] {result['topic']}")
|
||||
console.print(f" [dim]Type:[/dim] {plan_type.upper()}")
|
||||
console.print(f" [dim]Tag:[/dim] {tag}")
|
||||
if target_branch_name:
|
||||
console.print(f" [dim]Target:[/dim] {target_branch_name}")
|
||||
console.print(f" [dim]File:[/dim] {result['path']}")
|
||||
console.print()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_list(args: List[str]) -> bool:
|
||||
"""Orchestrate plan listing with optional filters"""
|
||||
filter_tag = None
|
||||
filter_status = None
|
||||
filter_type = None
|
||||
|
||||
if '--tag' in args:
|
||||
tag_idx = args.index('--tag')
|
||||
if tag_idx + 1 < len(args):
|
||||
filter_tag = args[tag_idx + 1].lower()
|
||||
|
||||
if '--status' in args:
|
||||
status_idx = args.index('--status')
|
||||
if status_idx + 1 < len(args):
|
||||
filter_status = args[status_idx + 1].lower()
|
||||
|
||||
if '--type' in args:
|
||||
type_idx = args.index('--type')
|
||||
if type_idx + 1 < len(args):
|
||||
filter_type = args[type_idx + 1].lower()
|
||||
|
||||
plans, err = list_plans(filter_type=filter_type)
|
||||
|
||||
if err:
|
||||
logger.error(f"[dev_flow] Failed to list plans: {err}")
|
||||
error(f"Failed to list plans: {err}")
|
||||
return True
|
||||
|
||||
if filter_tag:
|
||||
plans = [p for p in plans if p.get("tag") == filter_tag]
|
||||
if filter_status:
|
||||
plans = [p for p in plans if p.get("status") == filter_status]
|
||||
|
||||
console.print()
|
||||
title = "Plans"
|
||||
if filter_type:
|
||||
title = f"{filter_type.upper()}s"
|
||||
filters = []
|
||||
if filter_tag:
|
||||
filters.append(f"tag: {filter_tag}")
|
||||
if filter_status:
|
||||
filters.append(f"status: {filter_status}")
|
||||
if filters:
|
||||
title += f" ({', '.join(filters)})"
|
||||
header(title)
|
||||
console.print()
|
||||
|
||||
if not plans:
|
||||
console.print("[dim]No plans found[/dim]")
|
||||
console.print()
|
||||
return True
|
||||
|
||||
for p in plans:
|
||||
status_icon = get_status_icon(p["status"])
|
||||
tag_display = f"({p['tag']})" if p.get("tag") else ""
|
||||
prefix = p.get("prefix", "DPLAN")
|
||||
|
||||
summary = get_summary(p["number"])
|
||||
if not summary:
|
||||
summary = p.get("description", "")
|
||||
|
||||
line = f" {status_icon} [cyan]{prefix}-{p['number']:03d}[/cyan] | {p['topic'][:30]:<30}"
|
||||
if tag_display:
|
||||
line += f" | [dim]{tag_display}[/dim]"
|
||||
if summary:
|
||||
line += f" — [dim italic]{summary[:50]}[/dim italic]"
|
||||
|
||||
console.print(line)
|
||||
|
||||
console.print()
|
||||
console.print(f"[dim]Total: {len(plans)} plans[/dim]")
|
||||
console.print()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_status(args: List[str]) -> bool:
|
||||
"""Orchestrate status display - delegates to handler"""
|
||||
filter_type = None
|
||||
if '--type' in args:
|
||||
type_idx = args.index('--type')
|
||||
if type_idx + 1 < len(args):
|
||||
filter_type = args[type_idx + 1].lower()
|
||||
|
||||
status_counts, total, err = get_status_summary(filter_type=filter_type)
|
||||
|
||||
if err:
|
||||
logger.error(f"[dev_flow] Failed to get status: {err}")
|
||||
error(f"Failed to get status: {err}")
|
||||
return True
|
||||
|
||||
console.print()
|
||||
title = "Plan Status"
|
||||
if filter_type:
|
||||
title = f"{filter_type.upper()} Status"
|
||||
header(title)
|
||||
console.print()
|
||||
|
||||
console.print(f" [yellow]Planning:[/yellow] {status_counts['planning']}")
|
||||
console.print(f" [blue]In Progress:[/blue] {status_counts['in_progress']}")
|
||||
console.print(f" [green]Ready:[/green] {status_counts['ready']}")
|
||||
console.print(f" [dim]Complete:[/dim] {status_counts['complete']}")
|
||||
console.print(f" [red]Abandoned:[/red] {status_counts['abandoned']}")
|
||||
|
||||
if status_counts["unknown"] > 0:
|
||||
console.print(f" [dim]Unknown:[/dim] {status_counts['unknown']}")
|
||||
|
||||
console.print()
|
||||
console.print(f"[dim]Total: {total} plans[/dim]")
|
||||
console.print()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_close(args: List[str]) -> bool:
|
||||
"""Orchestrate plan closing - delegates to handler, spawns background archival"""
|
||||
if args and args[0] in ('--help', '-h'):
|
||||
console.print("\n[bold]USAGE:[/bold]")
|
||||
console.print(" plan close <number>")
|
||||
console.print(" plan close --all")
|
||||
console.print("\n[bold]EXAMPLES:[/bold]")
|
||||
console.print(" plan close 3")
|
||||
console.print(" plan close DPLAN-003")
|
||||
console.print(" plan close --all\n")
|
||||
return True
|
||||
|
||||
if args and args[0] == '--all':
|
||||
return _handle_close_all()
|
||||
|
||||
if len(args) < 1:
|
||||
error("Usage: plan close <number>")
|
||||
return True
|
||||
|
||||
plan_num, err = normalize_plan_number(args[0])
|
||||
if err:
|
||||
logger.warning(f"[dev_flow] {err}")
|
||||
error(err)
|
||||
return True
|
||||
|
||||
# Step 1/3: Close plan (mark as complete)
|
||||
console.print(f"\n[dim][1/3][/dim] Closing DPLAN-{plan_num:03d}...")
|
||||
ok, result, err = close_plan(plan_num)
|
||||
|
||||
if not ok:
|
||||
logger.warning(f"[dev_flow] Failed to close DPLAN-{plan_num:03d}: {err}")
|
||||
error(err)
|
||||
return True
|
||||
|
||||
logger.info(f"[dev_flow] Closed DPLAN-{plan_num:03d}: {result['topic']}")
|
||||
console.print(f"[green] Marked as complete[/green]")
|
||||
|
||||
# Update registry
|
||||
try:
|
||||
update_plan_status(plan_num, "complete")
|
||||
|
||||
plan_file = Path(result.get('plan_file', ''))
|
||||
if plan_file.exists():
|
||||
summary = generate_description_summary(plan_file)
|
||||
if summary:
|
||||
save_plan_summary(plan_num, summary, "complete", result['topic'], str(plan_file))
|
||||
except Exception as e:
|
||||
logger.warning(f"[dev_flow] Registry update failed: {e}")
|
||||
|
||||
# Push dashboard update
|
||||
try:
|
||||
activity = f"DPLAN-{plan_num:03d} closed ({result['topic'][:30]})"
|
||||
push_dashboard(activity=activity)
|
||||
except Exception as e:
|
||||
logger.warning(f"[dev_flow] Dashboard push failed: {e}")
|
||||
|
||||
# Append to CLOSED_PLANS.local.json via handler
|
||||
reg_result = append_closed_dplan(plan_num, result.get("topic", ""))
|
||||
if reg_result["success"]:
|
||||
logger.info(f"[dev_flow] Updated CLOSED_PLANS registry with {reg_result['plan_id']}")
|
||||
else:
|
||||
logger.warning(f"[dev_flow] CLOSED_PLANS update failed (non-critical): {reg_result['error']}")
|
||||
|
||||
# Step 2/3: Spawn background processing
|
||||
console.print(f"[dim][2/3][/dim] Starting background archival...")
|
||||
try:
|
||||
log_result = prepare_log_file("post_close_runner.log")
|
||||
if not log_result["success"]:
|
||||
raise RuntimeError(log_result["error"])
|
||||
spawn_result = spawn_post_close(log_file_handle=log_result["file_handle"])
|
||||
if not spawn_result["success"]:
|
||||
raise RuntimeError(spawn_result["error"])
|
||||
logger.info(f"[dev_flow] Spawned background post-processing for DPLAN-{plan_num:03d}")
|
||||
console.print(f"[dim] Memory Bank archival running in background[/dim]")
|
||||
except Exception as e:
|
||||
logger.warning(f"[dev_flow] Failed to spawn background processing: {e}")
|
||||
warning("Background archival failed to start - will retry on next close")
|
||||
|
||||
# Step 3/3: Done
|
||||
console.print(f"[dim][3/3][/dim] Finalizing...")
|
||||
console.print()
|
||||
success(f"DPLAN-{plan_num:03d} closed ({result['topic']})")
|
||||
console.print(f" [dim]Previous status:[/dim] {result['old_status']}")
|
||||
console.print(f" [dim]Archive:[/dim] Memory Bank processing in background")
|
||||
console.print()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_close_all() -> bool:
|
||||
"""Close all open plans"""
|
||||
open_plans = get_open_plans()
|
||||
|
||||
if not open_plans:
|
||||
warning("No open plans to close")
|
||||
return True
|
||||
|
||||
warning(f"Found {len(open_plans)} open plan(s) to close:")
|
||||
for p in open_plans:
|
||||
console.print(f" - DPLAN-{p['number']:03d}: {p['topic']}")
|
||||
|
||||
console.print(f"\n[bold]Closing all {len(open_plans)} plan(s)...[/bold]")
|
||||
console.print("─" * 60)
|
||||
|
||||
success_count = 0
|
||||
failure_count = 0
|
||||
|
||||
for p in open_plans:
|
||||
console.print(f"\n[dim]Closing DPLAN-{p['number']:03d}...[/dim]")
|
||||
ok, result, err = close_plan(p['number'])
|
||||
if ok:
|
||||
success_count += 1
|
||||
logger.info(f"[dev_flow] Closed DPLAN-{p['number']:03d}")
|
||||
console.print(f"[green] Marked as complete[/green]")
|
||||
|
||||
try:
|
||||
update_plan_status(p['number'], "complete")
|
||||
except Exception as reg_err:
|
||||
logger.warning(f"[dev_flow] Registry update failed for DPLAN-{p['number']:03d}: {reg_err}")
|
||||
|
||||
# Append to CLOSED_PLANS.local.json via handler
|
||||
reg_result = append_closed_dplan(p['number'], result.get("topic", ""))
|
||||
if reg_result["success"]:
|
||||
logger.info(f"[dev_flow] Updated CLOSED_PLANS registry with {reg_result['plan_id']}")
|
||||
else:
|
||||
logger.warning(f"[dev_flow] CLOSED_PLANS update failed (non-critical): {reg_result['error']}")
|
||||
else:
|
||||
failure_count += 1
|
||||
logger.warning(f"[dev_flow] Failed to close DPLAN-{p['number']:03d}: {err}")
|
||||
error(f" Failed: {err}")
|
||||
|
||||
# Push dashboard
|
||||
try:
|
||||
activity = f"{success_count} plan(s) closed (batch)"
|
||||
push_dashboard(activity=activity)
|
||||
except Exception as e:
|
||||
logger.warning(f"[dev_flow] Dashboard push failed: {e}")
|
||||
|
||||
# Spawn ONE background process for all closed plans
|
||||
if success_count > 0:
|
||||
try:
|
||||
spawn_result = spawn_post_close()
|
||||
if not spawn_result["success"]:
|
||||
raise RuntimeError(spawn_result["error"])
|
||||
logger.info(f"[dev_flow] Spawned background processing for {success_count} closed plan(s)")
|
||||
console.print(f"\n[dim]Background processing started for {success_count} plan(s)[/dim]")
|
||||
except Exception as e:
|
||||
logger.warning(f"[dev_flow] Failed to spawn background processing: {e}")
|
||||
warning("Background processing failed to start")
|
||||
|
||||
console.print("\n" + "=" * 60)
|
||||
console.print("[bold green]CLOSE ALL COMPLETE[/bold green]")
|
||||
console.print(f" - Successfully closed: {success_count}")
|
||||
console.print(f" - Failed: {failure_count}")
|
||||
console.print("=" * 60 + "\n")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_sync() -> bool:
|
||||
"""Sync registry from filesystem and push dashboard"""
|
||||
console.print("\n[dim]Syncing registry from filesystem...[/dim]")
|
||||
|
||||
try:
|
||||
registry = populate_from_filesystem()
|
||||
plan_count = len(registry.get("plans", {}))
|
||||
success(f"Registry synced: {plan_count} plans")
|
||||
except Exception as e:
|
||||
logger.warning(f"[dev_flow] Registry sync failed: {e}")
|
||||
error(f"Registry sync failed: {e}")
|
||||
return True
|
||||
|
||||
try:
|
||||
activity = f"Registry synced ({plan_count} plans)"
|
||||
summary = push_dashboard(activity=activity)
|
||||
total = summary.get("dplan_counts", {}).get("total", 0)
|
||||
console.print(f"[dim]Dashboard updated: {total} plans[/dim]")
|
||||
except Exception as e:
|
||||
logger.warning(f"[dev_flow] Dashboard push failed: {e}")
|
||||
|
||||
console.print()
|
||||
return True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# STANDALONE EXECUTION
|
||||
# =============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) == 1:
|
||||
console.print(print_introspection())
|
||||
sys.exit(0)
|
||||
|
||||
if sys.argv[1] in ['--help', '-h', 'help']:
|
||||
print_help()
|
||||
sys.exit(0)
|
||||
|
||||
subcommand = sys.argv[1]
|
||||
remaining_args = sys.argv[2:] if len(sys.argv) > 2 else []
|
||||
|
||||
if handle_command('plan', [subcommand] + remaining_args):
|
||||
sys.exit(0)
|
||||
else:
|
||||
console.print()
|
||||
console.print("[red]Failed to handle command[/red]")
|
||||
console.print()
|
||||
sys.exit(1)
|
||||
@@ -1,144 +0,0 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: dplan_post_close_runner.py
|
||||
# Description: Background post-close processing for DPLANs
|
||||
# Version: 1.1.0
|
||||
# Created: 2026-02-18
|
||||
# Modified: 2026-02-18
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Post-Close Background Runner for DPLANs
|
||||
|
||||
Runs Memory Bank archival as a background process.
|
||||
Called by dev_flow.py via subprocess.Popen so the close command returns fast.
|
||||
|
||||
Uses a lock file to prevent concurrent execution - if another instance is
|
||||
already running, this one exits silently.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# INFRASTRUCTURE IMPORT PATTERN
|
||||
# dplan_post_close_runner.py → modules/ → apps/ → flow/
|
||||
FLOW_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
# External: CLI console (Rich display) and Prax logger
|
||||
from aipass.cli.apps.modules import console, error, warning
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
MODULE_NAME = "dplan_post_close_runner"
|
||||
|
||||
LOCK_FILE = FLOW_ROOT / ".post_close_runner.lock"
|
||||
|
||||
from aipass.flow.apps.handlers.mbank.process import process_closed_plans
|
||||
|
||||
|
||||
def handle_command(command: str, args: list) -> bool:
|
||||
"""Handle commands routed by the entry point.
|
||||
|
||||
This module is a background utility runner, not a user-facing command.
|
||||
It responds to 'dplan_post_close' for drone routing compatibility
|
||||
and supports --help / -h for introspection.
|
||||
|
||||
Args:
|
||||
command: Command name
|
||||
args: Command arguments
|
||||
|
||||
Returns:
|
||||
True if command was handled, False otherwise
|
||||
"""
|
||||
if command != "dplan_post_close":
|
||||
return False
|
||||
|
||||
if args and args[0] in ("--help", "-h"):
|
||||
print_help()
|
||||
return True
|
||||
|
||||
# Run the post-close processing directly (foreground)
|
||||
if not _acquire_lock():
|
||||
warning("Another instance is already running")
|
||||
return True
|
||||
|
||||
try:
|
||||
result = process_closed_plans()
|
||||
console.print(f"[green]Processing complete:[/green] {result.get('processed', 0)} processed, {result.get('errors', 0)} errors")
|
||||
except Exception as e:
|
||||
logger.error(f"[{MODULE_NAME}] Background processing failed: {e}")
|
||||
error(f"Processing failed: {e}")
|
||||
finally:
|
||||
_release_lock()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _acquire_lock() -> bool:
|
||||
"""Try to acquire lock file. Returns True if acquired, False if another instance is running."""
|
||||
if LOCK_FILE.exists():
|
||||
try:
|
||||
pid = int(LOCK_FILE.read_text().strip())
|
||||
os.kill(pid, 0) # Signal 0 = check if process exists
|
||||
logger.info(f"[{MODULE_NAME}] Another instance running (PID {pid}), exiting")
|
||||
return False
|
||||
except (ValueError, ProcessLookupError, PermissionError):
|
||||
logger.info(f"[{MODULE_NAME}] Stale lock found, taking over")
|
||||
|
||||
LOCK_FILE.write_text(str(os.getpid()))
|
||||
return True
|
||||
|
||||
|
||||
def _release_lock():
|
||||
"""Release the lock file."""
|
||||
try:
|
||||
LOCK_FILE.unlink(missing_ok=True)
|
||||
except OSError as e:
|
||||
logger.warning(f"[{MODULE_NAME}] Failed to release lock file: {e}")
|
||||
|
||||
|
||||
def print_introspection():
|
||||
"""Display module introspection info."""
|
||||
console.print()
|
||||
console.print("dplan_post_close_runner Module")
|
||||
console.print("Background post-close processing for DPLANs — runs Memory Bank archival")
|
||||
console.print()
|
||||
console.print("Connected Handlers:")
|
||||
console.print(" handlers/mbank/ (local)")
|
||||
console.print(" - process.py (process_closed_plans — scan and archive closed plans)")
|
||||
console.print()
|
||||
|
||||
|
||||
def print_help():
|
||||
"""Display help for this background runner."""
|
||||
console.print(f"Usage: python {Path(__file__).name}")
|
||||
console.print()
|
||||
console.print("Post-Close Background Runner for DPLANs")
|
||||
console.print("Runs Memory Bank archival as a background process.")
|
||||
console.print("Called by dev_flow.py via subprocess — not intended for direct use.")
|
||||
console.print()
|
||||
console.print("Options:")
|
||||
console.print(" -h, --help Show this help message")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if '--help' in sys.argv or '-h' in sys.argv:
|
||||
print_help()
|
||||
sys.exit(0)
|
||||
|
||||
if not _acquire_lock():
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
result = process_closed_plans()
|
||||
logger.info(f"[{MODULE_NAME}] Processing complete: {result.get('processed', 0)} processed, {result.get('errors', 0)} errors")
|
||||
for entry in result.get("results", []):
|
||||
status = entry.get("status", "unknown")
|
||||
plan = entry.get("plan", "?")
|
||||
if "error" in status or "stranded" in status:
|
||||
logger.warning(f"[{MODULE_NAME}] {plan}: {status} — {entry.get('error', 'no detail')}")
|
||||
else:
|
||||
logger.info(f"[{MODULE_NAME}] {plan}: {status}")
|
||||
except Exception as e:
|
||||
logger.error(f"[{MODULE_NAME}] Background processing failed: {e}")
|
||||
finally:
|
||||
_release_lock()
|
||||
@@ -30,7 +30,7 @@ Filters:
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
from typing import List
|
||||
|
||||
# INFRASTRUCTURE IMPORT PATTERN
|
||||
_PKG_ROOT = Path(__file__).resolve().parents[3] # file.py -> modules/ -> apps/ -> flow/ -> aipass/
|
||||
@@ -51,7 +51,6 @@ from aipass.flow.apps.handlers.registry.statistics import get_registry_statistic
|
||||
|
||||
# Plan display handler
|
||||
from aipass.flow.apps.handlers.plan.display import (
|
||||
format_plan_info,
|
||||
format_plans_list,
|
||||
format_statistics_summary
|
||||
)
|
||||
@@ -208,10 +207,12 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
console.print("[dim]Valid filters: open, closed, all[/dim]")
|
||||
|
||||
# STEP 2: Execute workflow
|
||||
success = list_plans(filter_type)
|
||||
list_plans(filter_type)
|
||||
|
||||
# STEP 3: Return result
|
||||
return success
|
||||
# STEP 3: Command was handled (even if the operation failed, the error
|
||||
# has already been displayed -- returning False would cause flow.py to
|
||||
# print a spurious "Unknown command" message)
|
||||
return True
|
||||
|
||||
|
||||
# =============================================
|
||||
|
||||
@@ -226,13 +226,15 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
# 2. VALIDATE: Check for parsing errors
|
||||
if error:
|
||||
console.print(format_restore_usage_error())
|
||||
return False
|
||||
return True # Command was handled (error already displayed)
|
||||
|
||||
# 3. EXECUTE: Run workflow orchestrator
|
||||
success = restore_plan(plan_num=plan_num)
|
||||
restore_plan(plan_num=plan_num)
|
||||
|
||||
# 4. RETURN: Result (restore_plan already handles all output)
|
||||
return success
|
||||
# 4. RETURN: True = command was handled (even if the operation failed,
|
||||
# the error has already been displayed -- returning False would cause
|
||||
# flow.py to print a spurious "Unknown command" message)
|
||||
return True
|
||||
|
||||
|
||||
# =============================================
|
||||
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
# DPLAN-003: CLAUDE.md Replacement — aipass.md Architecture
|
||||
|
||||
Tag: architecture
|
||||
|
||||
> Replace all CLAUDE.md files with a single global pointer + per-directory aipass.md. End the cascade confusion permanently.
|
||||
|
||||
## Vision
|
||||
|
||||
One `~/.claude/CLAUDE.md` for the entire system. It says one thing: "read `aipass.md` in your current directory." Each project/branch/agent has an `aipass.md` that IS the startup file. No cascade, no bleed, no repos needed for containment. We control the system, not Claude Code's CLAUDE.md resolution.
|
||||
|
||||
## Current State
|
||||
|
||||
- CLAUDE.md cascade walks up to git root and merges everything — causes bleed between branches, between projects, between agents
|
||||
- Multi-agent setups inside one project are impossible without git sub-repos (ugly, fragile)
|
||||
- Trinity Pattern tried to solve this with hooks + templates but added more confusion (3 different CLAUDE.md files, hidden templates, wrong output format)
|
||||
- Trinity Pattern now disabled/private — hooks removed, pip uninstalled
|
||||
- AIPass project-level `UserPromptSubmit` hooks exist but aren't firing (needs investigation — possibly Claude Code session/scope issue)
|
||||
- `.trinity/` memory files remain in place across projects — data is fine, just the tooling is disabled
|
||||
- Dev-Pass CLAUDE.md still present and bleeding into AIPass (Patrick deleted some, context stale in current session)
|
||||
|
||||
## What Needs Building
|
||||
|
||||
- [ ] Write `~/.claude/CLAUDE.md` — universal pointer: "read aipass.md in your CWD"
|
||||
- [ ] Define `aipass.md` format — what goes in it, how startup works, how branches use it
|
||||
- [ ] Create `aipass.md` for devpulse (replace current CLAUDE.md + branch prompt setup)
|
||||
- [ ] Delete ALL CLAUDE.md files from AIPass repo (root, .claude/, devpulse, all branches)
|
||||
- [ ] Delete ALL CLAUDE.md files from other projects (Speakeasy, feel_good_app, test folders)
|
||||
- [ ] Update spawn to create `aipass.md` instead of CLAUDE.md when scaffolding citizens
|
||||
- [ ] Decide: do AIPass hooks stay in project `.claude/settings.json`? Or do we move prompt injection into `aipass.md` directly?
|
||||
- [ ] Test: fresh session from devpulse with new setup — verify agent gets correct context
|
||||
- [ ] Test: fresh session from feel_good_app — verify no AIPass bleed
|
||||
- [ ] Clean up test folders (TEST-FOLDER-01, 02, 03, TEST_DIR.)
|
||||
|
||||
## Design Decisions
|
||||
|
||||
| Decision | Options | Leaning | Notes |
|
||||
|----------|---------|---------|-------|
|
||||
| Global CLAUDE.md content | A: Just "read aipass.md" / B: Include AI culture/philosophy | A first, evolve | Keep it minimal. One instruction. |
|
||||
| aipass.md location | A: Project root / B: Branch directory / C: Both | C | Root = project-level, branch dir = branch-specific. Branch reads both. |
|
||||
| Memory files (.trinity/) | A: Keep as-is / B: Rename to .aipass/ / C: Leave for now | C | Data is fine. Format can evolve later. Don't touch working files. |
|
||||
| Hook strategy | A: Keep UserPromptSubmit hooks / B: Remove hooks, aipass.md does everything / C: Hybrid | TBD | Hooks are powerful but broken right now. Need to fix or replace. |
|
||||
| Spawn integration | A: Update spawn templates / B: New scaffold command | A | Spawn already creates per-branch files. Just change the template. |
|
||||
|
||||
## Ideas
|
||||
|
||||
- `aipass.md` could be generated/updated by spawn, so format stays consistent across branches
|
||||
- Could include a "last updated" timestamp so agents know if it's stale
|
||||
- The global CLAUDE.md could also mention: "if no aipass.md exists, you're in an unmanaged directory — just be a normal assistant"
|
||||
- CLAUDE.local.md is a real Claude Code feature (confirmed) — could be useful for per-machine overrides without touching aipass.md
|
||||
- Long term: aipass.md could be the universal startup for ANY AI system (Claude, ChatGPT, Gemini) — not tied to Claude Code's CLAUDE.md at all
|
||||
|
||||
## Relationships
|
||||
|
||||
- **Related DPLANs:** DPLAN-002 (AIPass as portable infrastructure)
|
||||
- **Related FPLANs:** None yet
|
||||
- **Owner branch:** devpulse (architecture), spawn (implementation)
|
||||
- **Seedgo standards:** `drone @seedgo audit aipass @branch` | `drone @seedgo standards_query aipass_standards`
|
||||
|
||||
## Notes
|
||||
|
||||
- Trinity Pattern disabled 2026-03-13. Repo made private. Pip uninstalled. Hooks purged from global + all projects. Memory files (.trinity/) preserved.
|
||||
- The UserPromptSubmit hook output format issue was discovered: plain text stdout required, NOT JSON {"output":"..."}. This broke Trinity inject for all external projects since inception.
|
||||
- CLAUDE.local.md confirmed as real Claude Code feature — "user's private project instructions, not checked in"
|
||||
- Patrick: "if I'm confused, imagine someone new. We need to control our own system, not fight Claude Code's cascade."
|
||||
|
||||
---
|
||||
*Created: 2026-03-13*
|
||||
*Updated: 2026-03-13*
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"DPLAN": {
|
||||
"next_number": 4
|
||||
},
|
||||
"next_number": 4
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
# FPLAN-{number} - {subject}
|
||||
|
||||
**Created**: {today}
|
||||
**Branch**: {location}
|
||||
**Status**: Active
|
||||
**Type**: Standard Plan
|
||||
|
||||
---
|
||||
|
||||
## What Are Flow Plans?
|
||||
|
||||
Flow Plans (FPLANs) are for **BUILDING** - autonomous construction of systems, features, modules.
|
||||
|
||||
**This is NOT for:**
|
||||
- Research or exploration (use agents directly)
|
||||
- Quick fixes (just do it)
|
||||
- Discussion or planning (that happens before creating the FPLAN)
|
||||
|
||||
**This IS for:**
|
||||
- Building features or modules
|
||||
- Single focused construction tasks
|
||||
- Sub-plans within a master plan
|
||||
|
||||
---
|
||||
|
||||
## When to Use This vs Master Plan
|
||||
|
||||
| This (Default) | Master Plan |
|
||||
|----------------|-------------|
|
||||
| Single focused task | 3+ phases, complex build |
|
||||
| Self-contained | Roadmap + multiple sub-plans |
|
||||
| Quick build | Multi-session project |
|
||||
| One phase of a master | Entire branch/system build |
|
||||
|
||||
**Need a master plan?** `drone @flow create "subject" master`
|
||||
|
||||
---
|
||||
|
||||
## Branch Directory Structure
|
||||
|
||||
Use dedicated directories - don't scatter files:
|
||||
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `apps/` | Code (modules/, handlers/) |
|
||||
| `tests/` | All test files |
|
||||
| `tools/` | Utility scripts |
|
||||
| `artifacts/` | Agent outputs |
|
||||
| `docs/` | Documentation |
|
||||
|
||||
---
|
||||
|
||||
## Critical: Branch Manager Role
|
||||
|
||||
**You are the ORCHESTRATOR, not the builder.**
|
||||
|
||||
Your 200k context is precious. Burning it on file reads and code writing risks compaction during autonomous work. Agents have clean context - use them for ALL building.
|
||||
|
||||
| You Do (Orchestrator) | Agents Do (Builders) |
|
||||
|-----------------------|----------------------|
|
||||
| Create plans | Write code |
|
||||
| Give instructions | Run tests |
|
||||
| Review output | Read/modify files |
|
||||
| Course correct | Research/exploration |
|
||||
| Update memories | Heavy lifting |
|
||||
| Send status emails | Single-task execution |
|
||||
|
||||
**Pattern:** Instruct agent → Wait for completion → Review output → Next step
|
||||
|
||||
---
|
||||
|
||||
## Seek Branch Expertise
|
||||
|
||||
Don't figure everything out alone. Other branches are domain experts - ask them first.
|
||||
|
||||
**Before building anything that touches another branch's domain:**
|
||||
```bash
|
||||
ai_mail send @branch "Question: [topic]" "I'm working on X and need guidance on Y. What's the best approach?"
|
||||
```
|
||||
|
||||
**Common examples:**
|
||||
- Building something with email? Ask @ai_mail how delivery works
|
||||
- Need routing or @ resolution? Ask @drone
|
||||
- Unsure about standards? Ask @seedgo for reference code
|
||||
- Need persistent storage or search? Ask @memory
|
||||
- Event-driven behavior? Ask @trigger about their event system
|
||||
- Dashboard integration? Ask @devpulse about update_section()
|
||||
|
||||
They have deep memory on their systems. A 1-email question saves you hours of guessing.
|
||||
|
||||
---
|
||||
|
||||
## Notepad
|
||||
|
||||
Keep `notepad.md` in your branch directory as a shared scratchpad during the build. Use it for:
|
||||
- **Status updates** - Quick progress lines so the user can glance without asking
|
||||
- **Questions for the user** - Non-urgent questions that can wait for the next check-in
|
||||
- **Notes to self** - Decisions made, things to revisit, gotchas discovered
|
||||
|
||||
Update it as you work - lightweight, not formal. The user checks it when they want to, skips it when busy.
|
||||
|
||||
---
|
||||
|
||||
## Command Reference
|
||||
|
||||
When unsure about syntax, use `--help`:
|
||||
|
||||
```bash
|
||||
# Flow - Plan management
|
||||
drone @flow create . "subject" # Create plan (. = current dir)
|
||||
drone @flow close FPLAN-XXXX # Close plan
|
||||
drone @flow list # List active plans
|
||||
drone @flow --help # Full help
|
||||
|
||||
# Seedgo - Quality gates
|
||||
drone @seedgo checklist <file> # 10-point check on file
|
||||
drone @seedgo audit @branch # Full branch audit
|
||||
drone @seedgo --help # Full help
|
||||
|
||||
# AI_Mail - Status updates
|
||||
drone @ai_mail send @devpulse "Subject" "Message"
|
||||
drone @ai_mail --help # Full help
|
||||
|
||||
# Discovery
|
||||
drone systems # All available modules
|
||||
drone list @branch # Commands for branch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Planning Phase
|
||||
|
||||
### Goal
|
||||
[What do you want to achieve? Specific end state.]
|
||||
|
||||
### Approach
|
||||
[How will agents tackle this? What instructions will they need?]
|
||||
|
||||
### Reference Documents
|
||||
[List any planning docs, specs, or examples to reference]
|
||||
|
||||
---
|
||||
|
||||
## Agent Preparation (Before Deploying)
|
||||
|
||||
Agents can't work blind. They need context before they build.
|
||||
|
||||
**Your Prep Work (as orchestrator):**
|
||||
1. [ ] Know where agent will work (branch path, key directories)
|
||||
2. [ ] Identify files agent needs to reference or modify
|
||||
3. [ ] Gather any specs, planning docs, or examples to include
|
||||
4. [ ] Prepare COMPLETE instructions (agents are stateless)
|
||||
|
||||
**Agent's First Task (context building):**
|
||||
- Agent should explore/read relevant files BEFORE writing code
|
||||
- "First, read X and Y to understand the current structure"
|
||||
- "Look at Z for the pattern to follow"
|
||||
- Context-first, build-second
|
||||
|
||||
**What Agents DON'T Have:**
|
||||
- No prior conversation history
|
||||
- No memory files loaded automatically
|
||||
- No knowledge of other branches
|
||||
- Only what you put in their instructions
|
||||
|
||||
**Your instructions determine success - be thorough and specific.**
|
||||
|
||||
---
|
||||
|
||||
## Agent Instructions Template
|
||||
```
|
||||
You are working at [BRANCH_PATH].
|
||||
|
||||
TASK: [Specific single task]
|
||||
|
||||
CONTEXT:
|
||||
- [What they need to know]
|
||||
- Reference: [planning docs, existing code to study]
|
||||
- First, READ the relevant files to understand current structure
|
||||
|
||||
DELIVERABLES:
|
||||
- [Specific file or output expected]
|
||||
- Tests → tests/
|
||||
- Reports/logs → artifacts/reports/ or artifacts/logs/
|
||||
|
||||
CONSTRAINTS:
|
||||
- Follow Seedgo standards (3-layer architecture)
|
||||
- Do NOT modify files outside your task scope
|
||||
- CROSS-BRANCH: Never modify other branches' files unless explicitly authorized by the user
|
||||
- 2-ATTEMPT RULE: If something fails twice, note the issue and move on
|
||||
- Do NOT go down rabbit holes debugging
|
||||
|
||||
WHEN COMPLETE:
|
||||
- Verify code runs without syntax errors
|
||||
- List files created/modified
|
||||
- Note any issues encountered (with what was attempted)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Execution Log
|
||||
|
||||
### {today}
|
||||
- [ ] Created FPLAN-{number}
|
||||
- [ ] Agent deployed for: [task]
|
||||
- [ ] Agent completed: [outcome]
|
||||
- [ ] Seedgo checklist passed: [file]
|
||||
- [ ] Memories updated
|
||||
|
||||
**Log Pattern:** Task → Agent → Outcome → Quality check → Next
|
||||
|
||||
**If production stops (critical blocker):**
|
||||
```bash
|
||||
drone @ai_mail send @devpulse "PRODUCTION STOPPED: FPLAN-{number}" "Issue: [description]. Attempted: [what was tried]. Awaiting guidance."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
[Working notes, issues encountered, decisions made]
|
||||
|
||||
---
|
||||
|
||||
## Completion Checklist
|
||||
|
||||
### Before Closing
|
||||
|
||||
- [ ] All goals achieved
|
||||
- [ ] Agent output reviewed and verified
|
||||
- [ ] Seedgo checklist on new code: `drone @seedgo checklist <file>`
|
||||
- [ ] Branch memories updated:
|
||||
- [ ] `BRANCH.local.json` - session/work log
|
||||
- [ ] `BRANCH.observations.json` - patterns learned (if any)
|
||||
- [ ] README.md updated (if build changed status/capabilities)
|
||||
- [ ] Status email sent to @devpulse:
|
||||
```bash
|
||||
drone @ai_mail send @devpulse "FPLAN-{number} Complete" "Summary of what was done, any issues, outcomes"
|
||||
```
|
||||
|
||||
**Completion Order:** Memories → README → Email (README before email - don't report complete with stale docs)
|
||||
|
||||
### Definition of Done
|
||||
[What specifically defines complete for this plan?]
|
||||
|
||||
---
|
||||
|
||||
## Close Command
|
||||
|
||||
When all boxes checked:
|
||||
```bash
|
||||
drone @flow close FPLAN-{number}
|
||||
```
|
||||
@@ -1,70 +0,0 @@
|
||||
# DPLAN-{{NUMBER}}: {{TOPIC}}
|
||||
|
||||
Tag: {{TAG}}
|
||||
|
||||
> One-line description
|
||||
|
||||
---
|
||||
|
||||
## What is a DPLAN?
|
||||
|
||||
Design Plans (DPLANs) are for **THINKING** — capturing ideas, brainstorming, investigating, planning, and making decisions. They are the space where conversations, research, and design work get written down so they can be reclaimed later.
|
||||
|
||||
**This IS for:**
|
||||
- Capturing an idea or concept worth exploring
|
||||
- Brainstorming and design discussions
|
||||
- Investigating a problem — sending agents to research, running tests, gathering data
|
||||
- Planning an upgrade, refactor, or new feature before building it
|
||||
- Recording decisions and the reasoning behind them
|
||||
- Anything that needs to be thought through before (or instead of) executing
|
||||
|
||||
**This is NOT for:**
|
||||
- Building code or executing tasks — that's an FPLAN (Flow Plan)
|
||||
- Quick fixes — just do those directly
|
||||
|
||||
**DPLANs have no fixed structure.** The sections below are starting points. Add sections, remove sections, go wherever the thinking takes you. A DPLAN might be a quick idea capture or a 50-phase investigation — both are valid.
|
||||
|
||||
**When this plan is ready to build**, create an FPLAN: `drone @flow create . "Subject"` (default for focused tasks, `master` for multi-phase builds). The DPLAN stays as the design record.
|
||||
|
||||
**Never trim a DPLAN.** The story — conversations, decisions, dead ends, pivots — is as important as the results.
|
||||
|
||||
---
|
||||
|
||||
## Vision
|
||||
What we're trying to achieve
|
||||
|
||||
## Current State
|
||||
What exists now
|
||||
|
||||
## What Needs Building
|
||||
- [ ] Item 1
|
||||
- [ ] Item 2
|
||||
|
||||
## Design Decisions
|
||||
|
||||
| Decision | Options | Leaning | Notes |
|
||||
|----------|---------|---------|-------|
|
||||
| Example | A / B | A | Why |
|
||||
|
||||
## Ideas
|
||||
Captured ideas, brainstorms, future possibilities. Add freely.
|
||||
|
||||
## Relationships
|
||||
- **Related DPLANs:** None yet
|
||||
- **Related FPLANs:** None yet
|
||||
- **Owner branch:** Who builds this
|
||||
- **Seedgo standards:** `drone @seedgo audit aipass @branch` | `drone @seedgo standards_query aipass_standards`
|
||||
|
||||
## Status
|
||||
- [x] Planning
|
||||
- [ ] In Progress
|
||||
- [ ] Ready for Execution
|
||||
- [ ] Complete
|
||||
- [ ] Abandoned
|
||||
|
||||
## Notes
|
||||
Session notes, discoveries, changes
|
||||
|
||||
---
|
||||
*Created: {{DATE}}*
|
||||
*Updated: {{DATE}}*
|
||||
@@ -1,514 +0,0 @@
|
||||
# FPLAN-{number} - {subject} (MASTER PLAN)
|
||||
|
||||
**Created**: {today}
|
||||
**Branch**: {location}
|
||||
**Status**: Active
|
||||
**Type**: Master Plan (Multi-Phase)
|
||||
|
||||
---
|
||||
|
||||
## What Are Flow Plans?
|
||||
|
||||
Flow Plans (FPLANs) are for **BUILDING** - autonomous construction of systems, features, modules. They're the structured way to execute work without constant human oversight.
|
||||
|
||||
**This is NOT for:**
|
||||
- Research or exploration (use agents directly)
|
||||
- Quick fixes (just do it)
|
||||
- Discussion or planning (that happens before creating the FPLAN)
|
||||
|
||||
**This IS for:**
|
||||
- Building new branches/modules
|
||||
- Implementing features
|
||||
- Multi-phase construction projects
|
||||
- Autonomous execution
|
||||
|
||||
---
|
||||
|
||||
## Master Plan vs Default Plan
|
||||
|
||||
| | Master Plan | Default Plan |
|
||||
|---|-------------|--------------|
|
||||
| **Use when** | 3+ phases, complex build | Single focused task |
|
||||
| **Structure** | Roadmap + sub-plans | Self-contained |
|
||||
| **Phases** | Multiple, sequential | One |
|
||||
| **Sub-plans** | Yes, one per phase | No |
|
||||
| **Typical use** | Build entire branch | One phase of master |
|
||||
|
||||
**Pattern:**
|
||||
```
|
||||
Master Plan (roadmap)
|
||||
├── Sub-plan Phase 1 (default template)
|
||||
├── Sub-plan Phase 2 (default template)
|
||||
├── Sub-plan Phase 3 (default template)
|
||||
└── Sub-plan Phase 4 (default template)
|
||||
```
|
||||
|
||||
**How to start:**
|
||||
1. The user provides planning doc or instructions (coordinate with @devpulse)
|
||||
2. Branch manager reads and understands scope
|
||||
3. Branch manager creates master plan: `drone @flow create . "Build X" master`
|
||||
4. Branch manager fills in phases, then executes autonomously
|
||||
|
||||
---
|
||||
|
||||
## Critical: Branch Manager Role
|
||||
|
||||
**You are the ORCHESTRATOR, not the builder.**
|
||||
|
||||
Your 200k context is precious. Burning it on file reads and code writing risks compaction during autonomous work. Agents have clean context - use them for ALL building.
|
||||
|
||||
| You Do (Orchestrator) | Agents Do (Builders) |
|
||||
|-----------------------|----------------------|
|
||||
| Create plans & sub-plans | Write code |
|
||||
| Define phases | Run tests |
|
||||
| Give agent instructions | Read/modify files |
|
||||
| Review agent output | Research/exploration |
|
||||
| Course correct | Heavy lifting |
|
||||
| Update memories | Single-task execution |
|
||||
| Send status emails | Build deliverables |
|
||||
| Track phase progress | Quality checks on code |
|
||||
|
||||
**Master Plan Pattern:** Define all phases → Create sub-plan for Phase 1 → Deploy agent → Review → Close sub-plan → Email update → Next phase
|
||||
|
||||
---
|
||||
|
||||
## Seek Branch Expertise
|
||||
|
||||
Don't figure everything out alone. Other branches are domain experts - ask them first.
|
||||
|
||||
**Before building anything that touches another branch's domain:**
|
||||
```bash
|
||||
ai_mail send @branch "Question: [topic]" "I'm working on X and need guidance on Y. What's the best approach?"
|
||||
```
|
||||
|
||||
**Common examples:**
|
||||
- Building something with email? Ask @ai_mail how delivery works
|
||||
- Need routing or @ resolution? Ask @drone
|
||||
- Unsure about standards? Ask @seedgo for reference code
|
||||
- Need persistent storage or search? Ask @memory
|
||||
- Event-driven behavior? Ask @trigger about their event system
|
||||
- Dashboard integration? Ask @devpulse about update_section()
|
||||
|
||||
They have deep memory on their systems. A 1-email question saves you hours of guessing. For master plans spanning multiple domains, identify which branches to consult during phase definitions.
|
||||
|
||||
---
|
||||
|
||||
## Notepad
|
||||
|
||||
Keep `notepad.md` in your branch directory as a shared scratchpad during the build. Use it for:
|
||||
- **Status updates** - Quick progress lines so the user can glance without asking
|
||||
- **Questions for the user** - Non-urgent questions that can wait for the next check-in
|
||||
- **Notes to self** - Decisions made, things to revisit, gotchas discovered
|
||||
|
||||
Update it as you work - lightweight, not formal. The user checks it when they want to, skips it when busy. Low friction both ways.
|
||||
|
||||
```bash
|
||||
# Create it at plan start
|
||||
echo "# Notepad - FPLAN-{number}" > notepad.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Command Reference
|
||||
|
||||
When unsure about syntax, use `--help`:
|
||||
|
||||
```bash
|
||||
# Flow - Plan management
|
||||
drone @flow create . "Phase X: subject" # Create sub-plan (. = current dir)
|
||||
drone @flow create . "subject" master # Create master plan
|
||||
drone @flow close FPLAN-XXXX # Close plan
|
||||
drone @flow list # List active plans
|
||||
drone @flow status # Plan status
|
||||
drone @flow --help # Full help
|
||||
|
||||
# Seedgo - Quality gates
|
||||
drone @seedgo checklist <file> # 10-point check on file
|
||||
drone @seedgo audit @branch # Full branch audit (before master close)
|
||||
drone @seedgo --help # Full help
|
||||
|
||||
# AI_Mail - Status updates
|
||||
drone @ai_mail send @devpulse "Subject" "Message"
|
||||
drone @ai_mail inbox # Check your inbox
|
||||
drone @ai_mail --help # Full help
|
||||
|
||||
# Discovery
|
||||
drone systems # All available modules
|
||||
drone list @branch # Commands for branch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What is a Master Plan?
|
||||
|
||||
Master Plans are for **complex multi-phase projects**. You define all phases upfront, then create focused sub-plans for each phase.
|
||||
|
||||
**When to use:**
|
||||
- 3+ distinct sequential phases
|
||||
- Work spanning multiple sessions
|
||||
- Need clear phase completion milestones
|
||||
- Complex builds requiring sustained focus
|
||||
|
||||
**Pattern:** Master Plan = Roadmap | Sub-Plans = Focused Execution
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
### Goal
|
||||
[What is the end state when ALL phases complete?]
|
||||
|
||||
### Reference Documentation
|
||||
[List planning docs, specs, existing code to reference]
|
||||
|
||||
### Success Criteria
|
||||
[What defines DONE for the entire project?]
|
||||
|
||||
---
|
||||
|
||||
## Branch Directory Structure
|
||||
|
||||
Every branch has dedicated directories. Use them correctly:
|
||||
|
||||
```
|
||||
branch/
|
||||
├── apps/ # Code (modules/, handlers/)
|
||||
├── tests/ # All test files go here
|
||||
├── tools/ # Utility scripts, helpers
|
||||
├── artifacts/ # Agent outputs (reports, logs)
|
||||
├── docs/ # Documentation
|
||||
└── logs/ # Execution logs
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Tests → `tests/` (not root, not random locations)
|
||||
- Tools/scripts → `tools/`
|
||||
- Agent artifacts → `artifacts/`
|
||||
- Create subdirs if needed: `mkdir -p artifacts/reports artifacts/logs`
|
||||
- **Never delete** - devpulse manages cleanup
|
||||
- Future: artifacts auto-roll to Memory Bank
|
||||
|
||||
---
|
||||
|
||||
## Phase Definitions
|
||||
|
||||
Define ALL phases before starting work:
|
||||
|
||||
### Phase 1: [Name]
|
||||
**Goal:** [What this phase accomplishes]
|
||||
**Agent Task:** [What the agent will build]
|
||||
**Deliverables:** [Files/outputs expected]
|
||||
|
||||
### Phase 2: [Name]
|
||||
**Goal:** [What this phase accomplishes]
|
||||
**Agent Task:** [What the agent will build]
|
||||
**Deliverables:** [Files/outputs expected]
|
||||
|
||||
### Phase 3: [Name]
|
||||
**Goal:** [What this phase accomplishes]
|
||||
**Agent Task:** [What the agent will build]
|
||||
**Deliverables:** [Files/outputs expected]
|
||||
|
||||
### Phase 4: [Name]
|
||||
**Goal:** [What this phase accomplishes]
|
||||
**Agent Task:** [What the agent will build]
|
||||
**Deliverables:** [Files/outputs expected]
|
||||
|
||||
[Add more phases as needed]
|
||||
|
||||
---
|
||||
|
||||
## Execution Philosophy
|
||||
|
||||
### Autonomous Power-Through
|
||||
|
||||
Master plans are for **autonomous execution**. Don't halt production every phase waiting for review.
|
||||
|
||||
**The Pattern:**
|
||||
- Power through all phases
|
||||
- Accumulate issues as you go
|
||||
- Deal with issues at the end
|
||||
- The user reviews the final result, not every step
|
||||
|
||||
**Why this works:**
|
||||
- Context is precious - don't burn it chasing bugs
|
||||
- Complete picture reveals which issues actually matter
|
||||
- Many "bugs" resolve themselves when later phases complete
|
||||
- Coordination time is for decisions, not babysitting
|
||||
|
||||
### The 2-Attempt Rule
|
||||
|
||||
When agent encounters an issue:
|
||||
|
||||
```
|
||||
Attempt 1 → Failed?
|
||||
↓
|
||||
Attempt 2 → Failed?
|
||||
↓
|
||||
STOP. Mark as issue. Move on.
|
||||
```
|
||||
|
||||
**Do NOT:**
|
||||
- Try 5 different approaches
|
||||
- Go down rabbit holes
|
||||
- Burn context debugging
|
||||
- Stop production for every error
|
||||
|
||||
**DO:**
|
||||
- Note the issue clearly
|
||||
- Note what was tried
|
||||
- Move to next task
|
||||
- Let branch manager decide priority
|
||||
|
||||
### Critical vs Non-Critical Issues
|
||||
|
||||
When you see an issue, decide:
|
||||
|
||||
| Question | If YES → | If NO → |
|
||||
|----------|----------|---------|
|
||||
| Does this block ALL future phases? | STOP. Investigate. | Continue. |
|
||||
| Can the system work around this? | Continue. | STOP. Investigate. |
|
||||
| Is this a syntax/import error? | Quick fix, continue. | - |
|
||||
| Is this a logic/design problem? | Note it. Continue. | - |
|
||||
|
||||
**Critical (stop production):**
|
||||
- Core module won't import at all
|
||||
- Database/file system inaccessible
|
||||
- Fundamental architecture wrong
|
||||
|
||||
**Non-critical (note and continue):**
|
||||
- One command throws error but others work
|
||||
- Registry not updating properly
|
||||
- Edge case not handled
|
||||
- Test failing but code runs
|
||||
|
||||
**Pattern:** Note issue → Continue building → Fix at end with complete picture
|
||||
|
||||
### False Positives Awareness
|
||||
|
||||
Seedgo audits are helpful but not infallible.
|
||||
|
||||
**When Seedgo flags something:**
|
||||
1. Check if the code is actually correct from your understanding
|
||||
2. If you're confident it's right → mark as false positive, move on
|
||||
3. If you're unsure → note it, continue, review later
|
||||
|
||||
**Don't stop production for:**
|
||||
- Style preferences (comments, spacing)
|
||||
- Patterns that differ from Seedgo's but still work
|
||||
- Checks that don't apply to your context
|
||||
|
||||
### Forward Momentum Summary
|
||||
- **Don't stop to fix bugs during phases** - Note them, keep moving
|
||||
- **Get complete picture first** - All phases done, THEN systematic fixes
|
||||
- **Prevents:** Bug-fixing rabbit holes, premature optimization, scope creep
|
||||
- **Review happens at END** - not every phase
|
||||
|
||||
### Production Stop Protocol
|
||||
|
||||
If something causes production to STOP (critical blocker), **immediately email @devpulse**:
|
||||
|
||||
```bash
|
||||
drone @ai_mail send @devpulse "PRODUCTION STOPPED: FPLAN-{number}" "Phase X halted. Issue: [description]. Attempted: [what was tried]. Awaiting guidance."
|
||||
```
|
||||
|
||||
**Never leave a branch stopped without reporting.** The orchestration hub needs visibility into all work.
|
||||
|
||||
### Monitoring Resources
|
||||
|
||||
For quick status checks and debugging, these resources are available:
|
||||
|
||||
| Resource | Location | Purpose |
|
||||
|----------|----------|---------|
|
||||
| Branch logs | `logs/` directory | Local execution logs |
|
||||
| JSON tree | `apps/json_templates/` | Module firing status |
|
||||
| Prax monitor | `drone @prax monitor` | Real-time system events |
|
||||
| Seedgo audit | `drone @seedgo audit @branch` | Code quality check |
|
||||
|
||||
Use these when you need to confirm status or investigate issues.
|
||||
|
||||
### Agent Deployment Per Phase
|
||||
Each phase = focused agent deployment:
|
||||
1. Create sub-plan: `drone @flow create . "Phase X: [name]"`
|
||||
2. Write agent instructions in sub-plan
|
||||
3. Deploy agent with single-task focus
|
||||
4. Review agent output (don't rebuild yourself)
|
||||
5. Seedgo checklist on new code
|
||||
6. Close sub-plan
|
||||
7. Update memories
|
||||
8. Email status to @devpulse
|
||||
9. Next phase
|
||||
|
||||
### Agent Preparation (Before Deploying)
|
||||
|
||||
Agents can't work blind. They need context before they build.
|
||||
|
||||
**Your Prep Work (as orchestrator):**
|
||||
1. [ ] Know where agent will work (branch path, key directories)
|
||||
2. [ ] Identify files agent needs to reference or modify
|
||||
3. [ ] Gather any specs, planning docs, or examples to include
|
||||
4. [ ] Prepare COMPLETE instructions (agents are stateless)
|
||||
|
||||
**Agent's First Task (context building):**
|
||||
- Agent should explore/read relevant files BEFORE writing code
|
||||
- "First, read X and Y to understand the current structure"
|
||||
- "Look at Z for the pattern to follow"
|
||||
- Context-first, build-second
|
||||
|
||||
**What Agents DON'T Have:**
|
||||
- No prior conversation history
|
||||
- No memory files loaded automatically
|
||||
- No knowledge of other branches
|
||||
- Only what you put in their instructions
|
||||
|
||||
**Your instructions determine success - be thorough and specific.**
|
||||
|
||||
### Agent Instructions Template
|
||||
```
|
||||
You are working at [BRANCH_PATH].
|
||||
|
||||
TASK: [Specific single task for this phase]
|
||||
|
||||
CONTEXT:
|
||||
- [What they need to know]
|
||||
- Reference: [planning docs, existing code to study]
|
||||
- First, READ the relevant files to understand current structure
|
||||
|
||||
DELIVERABLES:
|
||||
- [Specific file or output expected]
|
||||
- Tests → tests/
|
||||
- Reports/logs → artifacts/reports/ or artifacts/logs/
|
||||
|
||||
CONSTRAINTS:
|
||||
- Follow Seedgo standards (3-layer architecture: apps/modules/handlers)
|
||||
- Do NOT modify files outside your task scope
|
||||
- CROSS-BRANCH: Never modify other branches' files unless explicitly authorized by the user in the planning doc
|
||||
- 2-ATTEMPT RULE: If something fails twice, note the issue and move on
|
||||
- Do NOT go down rabbit holes debugging
|
||||
|
||||
WHEN COMPLETE:
|
||||
- Verify code runs without syntax errors
|
||||
- List files created/modified
|
||||
- Note any issues encountered (with what was attempted)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase Tracking
|
||||
|
||||
### Phase 1: [Name]
|
||||
- [ ] Sub-plan created: FPLAN-____
|
||||
- [ ] Agent deployed
|
||||
- [ ] Agent completed
|
||||
- [ ] Output reviewed
|
||||
- [ ] Seedgo checklist passed
|
||||
- [ ] Sub-plan closed
|
||||
- [ ] Memories updated
|
||||
- [ ] Email sent to @devpulse
|
||||
- **Status:** Pending / In Progress / Complete
|
||||
- **Notes:** [Outcomes, issues, adjustments]
|
||||
|
||||
### Phase 2: [Name]
|
||||
- [ ] Sub-plan created: FPLAN-____
|
||||
- [ ] Agent deployed
|
||||
- [ ] Agent completed
|
||||
- [ ] Output reviewed
|
||||
- [ ] Seedgo checklist passed
|
||||
- [ ] Sub-plan closed
|
||||
- [ ] Memories updated
|
||||
- [ ] Email sent to @devpulse
|
||||
- **Status:** Pending / In Progress / Complete
|
||||
- **Notes:** [Outcomes, issues, adjustments]
|
||||
|
||||
### Phase 3: [Name]
|
||||
- [ ] Sub-plan created: FPLAN-____
|
||||
- [ ] Agent deployed
|
||||
- [ ] Agent completed
|
||||
- [ ] Output reviewed
|
||||
- [ ] Seedgo checklist passed
|
||||
- [ ] Sub-plan closed
|
||||
- [ ] Memories updated
|
||||
- [ ] Email sent to @devpulse
|
||||
- **Status:** Pending / In Progress / Complete
|
||||
- **Notes:** [Outcomes, issues, adjustments]
|
||||
|
||||
### Phase 4: [Name]
|
||||
- [ ] Sub-plan created: FPLAN-____
|
||||
- [ ] Agent deployed
|
||||
- [ ] Agent completed
|
||||
- [ ] Output reviewed
|
||||
- [ ] Seedgo checklist passed
|
||||
- [ ] Sub-plan closed
|
||||
- [ ] Memories updated
|
||||
- [ ] Email sent to @devpulse
|
||||
- **Status:** Pending / In Progress / Complete
|
||||
- **Notes:** [Outcomes, issues, adjustments]
|
||||
|
||||
[Copy template for additional phases]
|
||||
|
||||
---
|
||||
|
||||
## Issues Log
|
||||
|
||||
Track issues here as you encounter them. Don't fix during build - log and continue.
|
||||
|
||||
| Phase | Issue | Severity | Attempted | Status |
|
||||
|-------|-------|----------|-----------|--------|
|
||||
| 1 | [description] | Low/Med/High | [what was tried] | Open/Resolved |
|
||||
| 2 | [description] | Low/Med/High | [what was tried] | Open/Resolved |
|
||||
|
||||
**Severity Guide:**
|
||||
- **High:** Blocks future phases, must fix before continuing
|
||||
- **Med:** Affects functionality but can work around
|
||||
- **Low:** Cosmetic, edge case, or false positive
|
||||
|
||||
**End of Build:** Review this log. Tackle High→Med→Low. Some Low issues may not need fixing.
|
||||
|
||||
---
|
||||
|
||||
## Master Plan Notes
|
||||
|
||||
**Cross-Phase Patterns:**
|
||||
[Patterns discovered that span multiple phases]
|
||||
|
||||
**Blockers & Resolutions:**
|
||||
[Significant blockers and how resolved]
|
||||
|
||||
**Adjustments:**
|
||||
[Changes to planned phases - scope changes, phases added/merged]
|
||||
|
||||
---
|
||||
|
||||
## Final Completion Checklist
|
||||
|
||||
### Before Closing Master Plan
|
||||
|
||||
- [ ] All phases complete
|
||||
- [ ] All sub-plans closed
|
||||
- [ ] Issues Log reviewed - High/Med issues addressed
|
||||
- [ ] Full branch audit: `drone @seedgo audit @branch`
|
||||
- [ ] Branch memories updated:
|
||||
- [ ] `BRANCH.local.json` - full session log
|
||||
- [ ] `BRANCH.observations.json` - patterns learned
|
||||
- [ ] README.md updated (status, architecture, API - if build changed capabilities)
|
||||
- [ ] Artifacts reviewed (devpulse manages cleanup)
|
||||
- [ ] Final email to @devpulse:
|
||||
```bash
|
||||
drone @ai_mail send @devpulse "FPLAN-{number} MASTER COMPLETE" "Full build summary: phases completed, deliverables, remaining issues (if any)"
|
||||
```
|
||||
|
||||
**Completion Order:** Memories → README → Email (README before email - don't report complete with stale docs)
|
||||
|
||||
**Note:** Devpulse will perform its own Seedgo audit for visibility into the work.
|
||||
|
||||
### Definition of Done
|
||||
[What specifically defines the project complete?]
|
||||
|
||||
---
|
||||
|
||||
## Close Command
|
||||
|
||||
When ALL phases complete and checklist done:
|
||||
```bash
|
||||
drone @flow close FPLAN-{number}
|
||||
```
|
||||
Reference in New Issue
Block a user