diff --git a/src/aipass/flow/README.md b/src/aipass/flow/README.md index 13b6eb9b..665cbd1d 100644 --- a/src/aipass/flow/README.md +++ b/src/aipass/flow/README.md @@ -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* diff --git a/src/aipass/flow/apps/flow.py b/src/aipass/flow/apps/flow.py index 09a2a4d6..096b4330 100755 --- a/src/aipass/flow/apps/flow.py +++ b/src/aipass/flow/apps/flow.py @@ -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() diff --git a/src/aipass/flow/apps/handlers/dashboard/push_branch_dashboard.py b/src/aipass/flow/apps/handlers/dashboard/push_branch_dashboard.py index 3c18dbe8..11a3b58a 100644 --- a/src/aipass/flow/apps/handlers/dashboard/push_branch_dashboard.py +++ b/src/aipass/flow/apps/handlers/dashboard/push_branch_dashboard.py @@ -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: diff --git a/src/aipass/flow/apps/handlers/dashboard/push_central.py b/src/aipass/flow/apps/handlers/dashboard/push_central.py index 53a0e97e..c0399354 100644 --- a/src/aipass/flow/apps/handlers/dashboard/push_central.py +++ b/src/aipass/flow/apps/handlers/dashboard/push_central.py @@ -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 diff --git a/src/aipass/flow/apps/handlers/dashboard/update_local.py b/src/aipass/flow/apps/handlers/dashboard/update_local.py index 6a38b843..59f41f43 100644 --- a/src/aipass/flow/apps/handlers/dashboard/update_local.py +++ b/src/aipass/flow/apps/handlers/dashboard/update_local.py @@ -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 diff --git a/src/aipass/flow/apps/handlers/dplan/EXTRACTION_NOTE.md b/src/aipass/flow/apps/handlers/dplan/EXTRACTION_NOTE.md deleted file mode 100644 index ac263935..00000000 --- a/src/aipass/flow/apps/handlers/dplan/EXTRACTION_NOTE.md +++ /dev/null @@ -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` diff --git a/src/aipass/flow/apps/handlers/dplan/__init__.py b/src/aipass/flow/apps/handlers/dplan/__init__.py deleted file mode 100644 index 0a9b94b9..00000000 --- a/src/aipass/flow/apps/handlers/dplan/__init__.py +++ /dev/null @@ -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 -""" diff --git a/src/aipass/flow/apps/handlers/dplan/background_spawn.py b/src/aipass/flow/apps/handlers/dplan/background_spawn.py deleted file mode 100644 index 787908e5..00000000 --- a/src/aipass/flow/apps/handlers/dplan/background_spawn.py +++ /dev/null @@ -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}", - } diff --git a/src/aipass/flow/apps/handlers/dplan/branch_resolve.py b/src/aipass/flow/apps/handlers/dplan/branch_resolve.py deleted file mode 100644 index 9cf5528c..00000000 --- a/src/aipass/flow/apps/handlers/dplan/branch_resolve.py +++ /dev/null @@ -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}", - } diff --git a/src/aipass/flow/apps/handlers/dplan/close.py b/src/aipass/flow/apps/handlers/dplan/close.py deleted file mode 100644 index d3322176..00000000 --- a/src/aipass/flow/apps/handlers/dplan/close.py +++ /dev/null @@ -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 - }, "") diff --git a/src/aipass/flow/apps/handlers/dplan/closed_plans_registry.py b/src/aipass/flow/apps/handlers/dplan/closed_plans_registry.py deleted file mode 100644 index 0203cf37..00000000 --- a/src/aipass/flow/apps/handlers/dplan/closed_plans_registry.py +++ /dev/null @@ -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), - } diff --git a/src/aipass/flow/apps/handlers/dplan/counter.py b/src/aipass/flow/apps/handlers/dplan/counter.py deleted file mode 100644 index 9a5cfe19..00000000 --- a/src/aipass/flow/apps/handlers/dplan/counter.py +++ /dev/null @@ -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 diff --git a/src/aipass/flow/apps/handlers/dplan/create.py b/src/aipass/flow/apps/handlers/dplan/create.py deleted file mode 100644 index 28aa69fe..00000000 --- a/src/aipass/flow/apps/handlers/dplan/create.py +++ /dev/null @@ -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}" diff --git a/src/aipass/flow/apps/handlers/dplan/dashboard.py b/src/aipass/flow/apps/handlers/dplan/dashboard.py deleted file mode 100644 index 7877fecd..00000000 --- a/src/aipass/flow/apps/handlers/dplan/dashboard.py +++ /dev/null @@ -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 diff --git a/src/aipass/flow/apps/handlers/dplan/display.py b/src/aipass/flow/apps/handlers/dplan/display.py deleted file mode 100644 index e1d1e0d4..00000000 --- a/src/aipass/flow/apps/handlers/dplan/display.py +++ /dev/null @@ -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 [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 - 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 - Plan type: dplan (default), bplan - --tag - Filter by tag (list) or set tag (create) - --status - Filter by status (list only) - --dir - Create in dev_planning// subdirectory - @ - 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) diff --git a/src/aipass/flow/apps/handlers/dplan/list.py b/src/aipass/flow/apps/handlers/dplan/list.py deleted file mode 100644 index 894537dd..00000000 --- a/src/aipass/flow/apps/handlers/dplan/list.py +++ /dev/null @@ -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, "" diff --git a/src/aipass/flow/apps/handlers/dplan/log_setup.py b/src/aipass/flow/apps/handlers/dplan/log_setup.py deleted file mode 100644 index 44599dfe..00000000 --- a/src/aipass/flow/apps/handlers/dplan/log_setup.py +++ /dev/null @@ -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}", - } diff --git a/src/aipass/flow/apps/handlers/dplan/registry.py b/src/aipass/flow/apps/handlers/dplan/registry.py deleted file mode 100644 index 20cf3339..00000000 --- a/src/aipass/flow/apps/handlers/dplan/registry.py +++ /dev/null @@ -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 "" diff --git a/src/aipass/flow/apps/handlers/dplan/status.py b/src/aipass/flow/apps/handlers/dplan/status.py deleted file mode 100644 index 74238183..00000000 --- a/src/aipass/flow/apps/handlers/dplan/status.py +++ /dev/null @@ -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 "" diff --git a/src/aipass/flow/apps/handlers/dplan/template.py b/src/aipass/flow/apps/handlers/dplan/template.py deleted file mode 100644 index 99406542..00000000 --- a/src/aipass/flow/apps/handlers/dplan/template.py +++ /dev/null @@ -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, "" diff --git a/src/aipass/flow/apps/handlers/mbank/process.py b/src/aipass/flow/apps/handlers/mbank/process.py index 3cc1c492..c5e1be52 100644 --- a/src/aipass/flow/apps/handlers/mbank/process.py +++ b/src/aipass/flow/apps/handlers/mbank/process.py @@ -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)} diff --git a/src/aipass/flow/apps/handlers/plan/aggregate_ops.py b/src/aipass/flow/apps/handlers/plan/aggregate_ops.py index 854de355..5d2f644a 100644 --- a/src/aipass/flow/apps/handlers/plan/aggregate_ops.py +++ b/src/aipass/flow/apps/handlers/plan/aggregate_ops.py @@ -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", {}) diff --git a/src/aipass/flow/apps/handlers/plan/close_ops.py b/src/aipass/flow/apps/handlers/plan/close_ops.py index 1e7ea907..56745289 100644 --- a/src/aipass/flow/apps/handlers/plan/close_ops.py +++ b/src/aipass/flow/apps/handlers/plan/close_ops.py @@ -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) diff --git a/src/aipass/flow/apps/handlers/plan/display.py b/src/aipass/flow/apps/handlers/plan/display.py index 033c6dc8..0262234b 100644 --- a/src/aipass/flow/apps/handlers/plan/display.py +++ b/src/aipass/flow/apps/handlers/plan/display.py @@ -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,13 +293,14 @@ 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") Returns: Formatted string with plan details @@ -311,7 +320,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" {prefix}-{plan_key} [{status:>6}] {location:<30} {subject:<40} {created}" def format_plans_list( diff --git a/src/aipass/flow/apps/handlers/plan/get_open_plans.py b/src/aipass/flow/apps/handlers/plan/get_open_plans.py index 1999aeb0..8baf0eee 100644 --- a/src/aipass/flow/apps/handlers/plan/get_open_plans.py +++ b/src/aipass/flow/apps/handlers/plan/get_open_plans.py @@ -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 diff --git a/src/aipass/flow/apps/handlers/plan/list_ops.py b/src/aipass/flow/apps/handlers/plan/list_ops.py index 453fa9e0..177eaf98 100644 --- a/src/aipass/flow/apps/handlers/plan/list_ops.py +++ b/src/aipass/flow/apps/handlers/plan/list_ops.py @@ -1,16 +1,16 @@ # =================== 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 @@ -19,7 +19,6 @@ Usage: from typing import Dict, Any from aipass.prax import logger -# logger imported from aipass.prax # ============================================= # CONFIGURATION @@ -28,6 +27,22 @@ from aipass.prax import logger MODULE_NAME = "list_plans" +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 [] # empty means caller should fall back to default + + # ============================================= # LIST PLANS IMPLEMENTATION # ============================================= @@ -35,13 +50,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 +70,29 @@ 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 = _get_all_registry_files() - # 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) + for plan_num, plan_info in registry.get("plans", {}).items(): + # Prefix the key to avoid collisions across registries + 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 +101,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 +126,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, diff --git a/src/aipass/flow/apps/handlers/plan/restore_ops.py b/src/aipass/flow/apps/handlers/plan/restore_ops.py index daa50262..9782548d 100644 --- a/src/aipass/flow/apps/handlers/plan/restore_ops.py +++ b/src/aipass/flow/apps/handlers/plan/restore_ops.py @@ -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 diff --git a/src/aipass/flow/apps/handlers/plan/validator.py b/src/aipass/flow/apps/handlers/plan/validator.py index 0e32b7a2..72e954dc 100644 --- a/src/aipass/flow/apps/handlers/plan/validator.py +++ b/src/aipass/flow/apps/handlers/plan/validator.py @@ -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 diff --git a/src/aipass/flow/apps/handlers/registry/load_registry.py b/src/aipass/flow/apps/handlers/registry/load_registry.py index e88169ec..444c7d9d 100644 --- a/src/aipass/flow/apps/handlers/registry/load_registry.py +++ b/src/aipass/flow/apps/handlers/registry/load_registry.py @@ -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: diff --git a/src/aipass/flow/apps/handlers/registry/save_registry.py b/src/aipass/flow/apps/handlers/registry/save_registry.py index 0a6fdb95..bc51cd39 100644 --- a/src/aipass/flow/apps/handlers/registry/save_registry.py +++ b/src/aipass/flow/apps/handlers/registry/save_registry.py @@ -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 diff --git a/src/aipass/flow/apps/modules/close_plan.py b/src/aipass/flow/apps/modules/close_plan.py index a96e5a4a..2887ea6a 100644 --- a/src/aipass/flow/apps/modules/close_plan.py +++ b/src/aipass/flow/apps/modules/close_plan.py @@ -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) diff --git a/src/aipass/flow/apps/modules/dplan_flow.py b/src/aipass/flow/apps/modules/dplan_flow.py deleted file mode 100644 index 43c037d0..00000000 --- a/src/aipass/flow/apps/modules/dplan_flow.py +++ /dev/null @@ -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 Plan type: dplan (default), bplan") - console.print(" --tag Set plan tag (default: idea)") - console.print(" --dir Create plan in dev_planning// subdirectory") - console.print(" @ 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 ") - 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 ") - 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) diff --git a/src/aipass/flow/apps/modules/dplan_post_close_runner.py b/src/aipass/flow/apps/modules/dplan_post_close_runner.py deleted file mode 100644 index 7ef68daf..00000000 --- a/src/aipass/flow/apps/modules/dplan_post_close_runner.py +++ /dev/null @@ -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() diff --git a/src/aipass/flow/dev_planning/DPLAN-003_claudemd_replacement_aipassmd_architectu_2026-03-13.md b/src/aipass/flow/dev_planning/DPLAN-003_claudemd_replacement_aipassmd_architectu_2026-03-13.md deleted file mode 100644 index 6a410bcc..00000000 --- a/src/aipass/flow/dev_planning/DPLAN-003_claudemd_replacement_aipassmd_architectu_2026-03-13.md +++ /dev/null @@ -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* diff --git a/src/aipass/flow/dev_planning/counter.json b/src/aipass/flow/dev_planning/counter.json deleted file mode 100644 index a92077ec..00000000 --- a/src/aipass/flow/dev_planning/counter.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "DPLAN": { - "next_number": 4 - }, - "next_number": 4 -} \ No newline at end of file diff --git a/src/aipass/flow/templates/default.md b/src/aipass/flow/templates/default.md deleted file mode 100644 index c783c2e0..00000000 --- a/src/aipass/flow/templates/default.md +++ /dev/null @@ -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 # 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 ` -- [ ] 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} -``` diff --git a/src/aipass/flow/templates/dplan_default.md b/src/aipass/flow/templates/dplan_default.md deleted file mode 100644 index 333ec9d2..00000000 --- a/src/aipass/flow/templates/dplan_default.md +++ /dev/null @@ -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}}* diff --git a/src/aipass/flow/templates/master.md b/src/aipass/flow/templates/master.md deleted file mode 100644 index 280baffe..00000000 --- a/src/aipass/flow/templates/master.md +++ /dev/null @@ -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 # 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} -```