diff --git a/src/aipass/flow/.aipass/aipass_local_prompt.md b/src/aipass/flow/.aipass/aipass_local_prompt.md index 4f2ab83e..d4632cdc 100644 --- a/src/aipass/flow/.aipass/aipass_local_prompt.md +++ b/src/aipass/flow/.aipass/aipass_local_prompt.md @@ -1,14 +1,63 @@ -# FLOW Branch-Local Context - +# Flow -- Plan Lifecycle Management -> Auto-created by aipass init. Customize for your branch. +Flow is AIPass's unified plan lifecycle system. It creates, tracks, closes, and archives numbered work plans across multiple plan types (FPLAN, DPLAN) via a data-driven plugin architecture. -## Status: NEEDS CONFIGURATION +## Commands -This file is injected into every AI conversation when working from this branch directory. Configure it with: +```bash +drone @flow create . "Subject" # FPLAN (default) +drone @flow create . "Subject" master # FPLAN master template +drone @flow create . "Design topic" dplan # DPLAN +drone @flow close FPLAN-0042 # Close specific plan +drone @flow close --all # Close all open plans +drone @flow list # List open plans (all types) +drone @flow list all # List all plans +drone @flow restore FPLAN-0042 # Reopen a closed plan +``` -- Who this branch is (role, purpose) -- Key commands and workflows -- Architecture overview -- Critical files and operational rules -- Integration points with other branches +## Architecture + +- `apps/flow.py` -- Entry point. Auto-discovers modules in `apps/modules/` via `handle_command()` convention. +- `apps/modules/` -- Thin orchestrators. No business logic. Route to handlers and display results. +- `apps/handlers/` -- Implementation. Grouped by domain: `plan/`, `registry/`, `template/`, `dashboard/`, `mbank/`, `summary/`. +- `plan_types/` -- Data-only plugins. Each subdirectory has `plan_type.json` config + `templates/` with Markdown templates. +- `flow_json/` -- Per-type JSON registries (`fplan_registry.json`, `dplan_registry.json`). + +## Plan Type Plugins + +Plan types are DATA, not code. Each plugin directory under `plan_types/` contains: +- `plan_type.json` -- prefix, digits, registry_file, available_templates, default_template +- `templates/` -- Markdown plan templates + +Discovered at runtime by `apps/handlers/template/plan_type_loader.py`. Add a new type by creating a new directory with these files. + +| Type | Prefix | Registry File | Templates | +|------|--------|---------------|-----------| +| flow_plans | FPLAN | fplan_registry.json | default, master | +| dev_plans | DPLAN | dplan_registry.json | default | + +## Critical Files + +- `apps/flow.py` -- CLI entry point, module discovery, command routing +- `apps/modules/create_plan.py` -- Plan creation orchestrator +- `apps/modules/close_plan.py` -- Plan closure orchestrator (async post-processing, archival) +- `apps/modules/list_plans.py` -- Multi-registry plan listing +- `apps/handlers/plan/list_ops.py` -- Merges plans from all registries for display +- `apps/handlers/plan/display.py` -- All formatting functions (prefix-aware) +- `apps/handlers/plan/close_ops.py` -- Close implementation (file ops, registry update, vector intake) +- `apps/handlers/template/plan_type_loader.py` -- Plugin discovery and config resolution +- `apps/handlers/registry/load_registry.py` -- Registry loader (supports per-type registry files) + +## Integration Points + +- **aipass.cli** -- Rich console output (`console`, `header`, `success`, `error`, `warning`) +- **aipass.prax** -- System logger +- **aipass.memory** -- Vector intake pipeline on plan close +- **aipass.trigger** -- Startup events and branch dashboard updates + +## Conventions + +- Modules return `True` from `handle_command()` when the command was recognized (even on failure), `False` only for "not my command". +- Plan IDs follow `{PREFIX}-{NNNN}_topic_slug_YYYY-MM-DD.md`. +- All file I/O uses `pathlib.Path` and `encoding='utf-8'`. +- Handlers are stateless functions; modules inject dependencies. diff --git a/src/aipass/flow/apps/handlers/plan/display.py b/src/aipass/flow/apps/handlers/plan/display.py index 0262234b..5bd254e1 100644 --- a/src/aipass/flow/apps/handlers/plan/display.py +++ b/src/aipass/flow/apps/handlers/plan/display.py @@ -300,13 +300,18 @@ def format_plan_info(plan_key: str, plan_info: Dict[str, Any], prefix: str = "FP Args: plan_key: Plan number (e.g., "0001") plan_info: Plan metadata dictionary - prefix: Plan prefix (e.g. "FPLAN", "DPLAN") + prefix: Plan prefix (e.g. "FPLAN", "DPLAN"). + Overridden by ``_source_prefix`` in *plan_info* when present + (set by list_ops when merging multi-registry plans). Returns: Formatted string with plan details """ from datetime import datetime + # Use source prefix if tagged by list_ops, otherwise fall back to arg + display_prefix = plan_info.get("_source_prefix", prefix) + subject = plan_info.get("subject", "No subject") location = plan_info.get("relative_path", "unknown") status = plan_info.get("status", "unknown") @@ -320,7 +325,7 @@ def format_plan_info(plan_key: str, plan_info: Dict[str, Any], prefix: str = "FP except (ValueError, AttributeError): pass # Keep original value if parsing fails - return f" {prefix}-{plan_key} [{status:>6}] {location:<30} {subject:<40} {created}" + return f" {display_prefix}-{plan_key} [{status:>6}] {location:<30} {subject:<40} {created}" def format_plans_list( diff --git a/src/aipass/flow/apps/handlers/plan/list_ops.py b/src/aipass/flow/apps/handlers/plan/list_ops.py index 177eaf98..4180fb5c 100644 --- a/src/aipass/flow/apps/handlers/plan/list_ops.py +++ b/src/aipass/flow/apps/handlers/plan/list_ops.py @@ -16,7 +16,7 @@ Usage: from aipass.flow.apps.handlers.plan.list_ops import list_plans_impl """ -from typing import Dict, Any +from typing import Dict, Any, Tuple from aipass.prax import logger @@ -27,20 +27,29 @@ 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.""" +def _get_all_registry_info() -> Tuple[list[str], Dict[str, str]]: + """Return per-type registry filenames and a registry_file -> prefix map. + + Returns: + (registry_files, prefix_map) where prefix_map maps e.g. + "dplan_registry.json" -> "DPLAN". Empty list means caller + should fall back to the default registry. + """ try: from aipass.flow.apps.handlers.template.plan_type_loader import discover_plan_types # type: ignore[import-not-found] files: list[str] = [] + prefix_map: Dict[str, str] = {} for _key, config in discover_plan_types().items(): rf = config.get("registry_file") - if rf and rf not in files: - files.append(rf) + if rf: + prefix_map[rf] = config.get("prefix", "FPLAN") + if rf not in files: + files.append(rf) if files: - return files + return files, prefix_map except Exception: pass - return [] # empty means caller should fall back to default + return [], {} # empty means caller should fall back to default # ============================================= @@ -72,14 +81,16 @@ def list_plans_impl( try: # STEP 1: Load ALL per-type registries and merge plans merged_plans: Dict[str, Any] = {} - reg_files = _get_all_registry_files() + reg_files, reg_prefix_map = _get_all_registry_info() if reg_files: for reg_file in reg_files: try: registry = load_registry(registry_file=reg_file) + source_prefix = reg_prefix_map.get(reg_file, "FPLAN") for plan_num, plan_info in registry.get("plans", {}).items(): - # Prefix the key to avoid collisions across registries + # Tag each plan with its source prefix for display + plan_info["_source_prefix"] = source_prefix merged_plans[plan_num] = plan_info except Exception: continue diff --git a/src/aipass/flow/apps/modules/close_plan.py b/src/aipass/flow/apps/modules/close_plan.py index 2887ea6a..1971680c 100644 --- a/src/aipass/flow/apps/modules/close_plan.py +++ b/src/aipass/flow/apps/modules/close_plan.py @@ -290,13 +290,15 @@ def handle_command(command: str, args: List[str]) -> bool: # 2. VALIDATE: Check for parsing errors if error: console.print(format_delete_usage_error()) - return False + return True # Command was handled (error already displayed) # 3. EXECUTE: Run workflow orchestrator - success = close_plan(plan_num=plan_num, confirm=confirm, all_plans=all_plans) + close_plan(plan_num=plan_num, confirm=confirm, all_plans=all_plans) - # 4. RETURN: Result (close_plan already handles all output) - return success + # 4. RETURN: True = command was handled (even if the operation failed, + # the error has already been displayed -- returning False would cause + # flow.py to print a spurious "Unknown command" message) + return True # ============================================= diff --git a/src/aipass/flow/apps/modules/create_plan.py b/src/aipass/flow/apps/modules/create_plan.py index 1cbdd7ef..6ecad6c9 100755 --- a/src/aipass/flow/apps/modules/create_plan.py +++ b/src/aipass/flow/apps/modules/create_plan.py @@ -275,11 +275,10 @@ def handle_command(command: str, args: List[str]) -> bool: ) console.print(result_msg) - # Return boolean result - if success: - return True - else: - return False + # Command was handled (even if the operation failed, the error has + # already been displayed -- returning False would cause flow.py to + # print a spurious "Unknown command" message) + return True # ============================================= diff --git a/src/aipass/flow/apps/modules/list_plans.py b/src/aipass/flow/apps/modules/list_plans.py index a5ffdc12..b055f87d 100755 --- a/src/aipass/flow/apps/modules/list_plans.py +++ b/src/aipass/flow/apps/modules/list_plans.py @@ -30,7 +30,7 @@ Filters: import sys from pathlib import Path -from typing import List, Dict, Any +from typing import List # INFRASTRUCTURE IMPORT PATTERN _PKG_ROOT = Path(__file__).resolve().parents[3] # file.py -> modules/ -> apps/ -> flow/ -> aipass/ @@ -51,7 +51,6 @@ from aipass.flow.apps.handlers.registry.statistics import get_registry_statistic # Plan display handler from aipass.flow.apps.handlers.plan.display import ( - format_plan_info, format_plans_list, format_statistics_summary ) @@ -208,10 +207,12 @@ def handle_command(command: str, args: List[str]) -> bool: console.print("[dim]Valid filters: open, closed, all[/dim]") # STEP 2: Execute workflow - success = list_plans(filter_type) + list_plans(filter_type) - # STEP 3: Return result - return success + # STEP 3: Command was handled (even if the operation failed, the error + # has already been displayed -- returning False would cause flow.py to + # print a spurious "Unknown command" message) + return True # ============================================= diff --git a/src/aipass/flow/apps/modules/restore_plan.py b/src/aipass/flow/apps/modules/restore_plan.py index 1de28a87..5cd0e151 100644 --- a/src/aipass/flow/apps/modules/restore_plan.py +++ b/src/aipass/flow/apps/modules/restore_plan.py @@ -226,13 +226,15 @@ def handle_command(command: str, args: List[str]) -> bool: # 2. VALIDATE: Check for parsing errors if error: console.print(format_restore_usage_error()) - return False + return True # Command was handled (error already displayed) # 3. EXECUTE: Run workflow orchestrator - success = restore_plan(plan_num=plan_num) + restore_plan(plan_num=plan_num) - # 4. RETURN: Result (restore_plan already handles all output) - return success + # 4. RETURN: True = command was handled (even if the operation failed, + # the error has already been displayed -- returning False would cause + # flow.py to print a spurious "Unknown command" message) + return True # =============================================