diff --git a/src/aipass/flow/.aipass/aipass_local_prompt.md b/src/aipass/flow/.aipass/aipass_local_prompt.md index 00468151..66be784b 100644 --- a/src/aipass/flow/.aipass/aipass_local_prompt.md +++ b/src/aipass/flow/.aipass/aipass_local_prompt.md @@ -20,16 +20,20 @@ drone @flow restore FPLAN-0042 # Reopen a closed plan - `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/`. -- `templates/` -- Data-only plugins. Each subdirectory has `plan_type.json` config + Markdown templates. -- `flow_json/` -- Per-type JSON registries (`fplan_registry.json`, `dplan_registry.json`). +- `templates/` -- Plan type directories. Each subdirectory contains Markdown templates. Registered via `drone @flow register`. +- `flow_json/` -- Registries: per-type plan registries + `template_registry.json` (plan type definitions). -## Plan Type Plugins +## Plan Type System -Plan types are DATA, not code. Each plugin directory under `templates/` contains: -- `plan_type.json` -- prefix, digits, registry_file, available_templates, default_template -- Markdown template files (e.g. default.md, master.md) +Plan types are filesystem-driven. Drop a directory with `.md` templates into `templates/`, register it, done: +```bash +drone @flow register testing TPLAN # Register new type +drone @flow unregister testing # Remove type +drone @flow templates # List registered types +drone @flow scan # Find unregistered directories +``` -Discovered at runtime by `apps/handlers/template/plan_type_loader.py`. Add a new type by creating a new directory with these files. +Discovered at runtime by `plan_type_loader.py` + `registry_ops.py`. No per-directory JSON config needed. | Type | Prefix | Registry File | Templates | |------|--------|---------------|-----------| diff --git a/src/aipass/flow/HPLAN-0001_design_topic_2026-03-18.md b/src/aipass/flow/HPLAN-0001_design_topic_2026-03-18.md new file mode 100644 index 00000000..e69de29b diff --git a/src/aipass/flow/PPLAN-0002_design_topic_2026-03-18.md b/src/aipass/flow/PPLAN-0002_design_topic_2026-03-18.md new file mode 100644 index 00000000..e69de29b diff --git a/src/aipass/flow/apps/flow.py b/src/aipass/flow/apps/flow.py index 096b4330..0bbe0c98 100755 --- a/src/aipass/flow/apps/flow.py +++ b/src/aipass/flow/apps/flow.py @@ -245,7 +245,14 @@ def print_help(modules: List[Any]): console.print(" [dim]drone @flow close --all[/dim]") console.print() console.print(" [yellow]List plans:[/yellow]") - console.print(" [dim]drone @flow list[/dim] [dim]# All plan types[/dim]") + console.print(" [dim]drone @flow list open[/dim] [dim]# Open plans[/dim]") + console.print(" [dim]drone @flow list all[/dim] [dim]# All plans[/dim]") + console.print() + console.print(" [yellow]Templates:[/yellow]") + console.print(" [dim]drone @flow templates[/dim] [dim]# List registered types[/dim]") + console.print(" [dim]drone @flow scan[/dim] [dim]# Find unregistered dirs[/dim]") + console.print(" [dim]drone @flow register testing TPLAN[/dim] [dim]# Register new type[/dim]") + console.print(" [dim]drone @flow unregister testing[/dim] [dim]# Remove type[/dim]") console.print() console.print("─" * 70) console.print() diff --git a/src/aipass/flow/apps/handlers/plan/create_ops.py b/src/aipass/flow/apps/handlers/plan/create_ops.py index 91573a57..a1d4fce7 100644 --- a/src/aipass/flow/apps/handlers/plan/create_ops.py +++ b/src/aipass/flow/apps/handlers/plan/create_ops.py @@ -170,15 +170,30 @@ def create_plan_impl( PLAN_FILE = target_dir / f"{prefix}-{formatted_num}_{date_str}.md" # STEP 7: Get template content - # Resolve template path from plan_type_config when available + # Resolve template path from plan_type_config — no fallback template_path: Path | None = None if plan_type_config is not None: tmpl_name = plan_type_config.get("default_template", "default") tmpl_dir: Path | None = plan_type_config.get("_directory") if tmpl_dir is not None: + available = sorted(p for p in tmpl_dir.glob("*.md")) if tmpl_dir.is_dir() else [] candidate = tmpl_dir / f"{tmpl_name}.md" if candidate.is_file(): + # Exact template match template_path = candidate + elif len(available) == 1: + # Single template in directory — use it regardless of name + template_path = available[0] + elif len(available) > 1: + names = [p.stem for p in available] + error_msg = ( + f"Multiple templates in {tmpl_dir.name}/. " + f"Specify which one: {names}" + ) + return False, 0, "", "", error_msg, [] + else: + error_msg = f"No templates found in {tmpl_dir.name}/" + return False, 0, "", "", error_msg, [] try: CONTENT = get_template( diff --git a/src/aipass/flow/apps/handlers/plan/display.py b/src/aipass/flow/apps/handlers/plan/display.py index 8bcbeec4..2f1a5031 100644 --- a/src/aipass/flow/apps/handlers/plan/display.py +++ b/src/aipass/flow/apps/handlers/plan/display.py @@ -328,7 +328,9 @@ 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" {display_prefix}-{plan_key} [{status:>6}] {location:<30} {subject:<40} {created}" + # Use original plan number if available (set by list_ops merge), else use key + display_num = plan_info.get("_plan_num", plan_key) + return f" {display_prefix}-{display_num} [{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 176fb3b3..04b072ba 100644 --- a/src/aipass/flow/apps/handlers/plan/list_ops.py +++ b/src/aipass/flow/apps/handlers/plan/list_ops.py @@ -90,9 +90,12 @@ def list_plans_impl( registry = load_registry(registry_file=reg_file) source_prefix = reg_prefix_map.get(reg_file, "FPLAN") for plan_num, plan_info in registry.get("plans", {}).items(): - # Tag each plan with its source prefix for display + # Tag each plan with its source prefix and original number for display plan_info["_source_prefix"] = source_prefix - merged_plans[plan_num] = plan_info + plan_info["_plan_num"] = plan_num + # Use prefix-qualified key to avoid collisions across registries + merge_key = f"{source_prefix}-{plan_num}" + merged_plans[merge_key] = plan_info except Exception: continue else: diff --git a/src/aipass/flow/apps/handlers/template/get_template.py b/src/aipass/flow/apps/handlers/template/get_template.py index 642dc517..9d3564df 100644 --- a/src/aipass/flow/apps/handlers/template/get_template.py +++ b/src/aipass/flow/apps/handlers/template/get_template.py @@ -66,21 +66,20 @@ def _find_template_file(template_name: str) -> Path: """ search_paths = _template_search_dirs() - # Look for the requested template + # Look for the requested template — no fallback candidate = search_paths[0] / f"{template_name}.md" if candidate.exists(): return candidate - # Fallback to default template - default_candidate = search_paths[0] / f"{DEFAULT_TEMPLATE}.md" - if default_candidate.exists(): - return default_candidate - - # Nothing found – raise helpful error + # Not found — error with available templates + available = [ + p.stem for p in search_paths[0].iterdir() + if p.suffix == ".md" + ] if search_paths[0].is_dir() else [] searched = ", ".join(str(path) for path in search_paths) error_msg = ( - f"Templates not found. Searched for '{template_name}.md' and " - f"'{DEFAULT_TEMPLATE}.md' in: {searched}" + f"Template '{template_name}' not found in: {searched}. " + f"Available: {available if available else 'none'}" ) raise FileNotFoundError(error_msg) diff --git a/src/aipass/flow/apps/modules/create_plan.py b/src/aipass/flow/apps/modules/create_plan.py index 3823ae92..babf7c9e 100755 --- a/src/aipass/flow/apps/modules/create_plan.py +++ b/src/aipass/flow/apps/modules/create_plan.py @@ -44,7 +44,7 @@ from aipass.prax.apps.modules.logger import system_logger as logger from aipass.flow.apps.handlers.json import json_handler # CLI services for display -from aipass.cli.apps.modules import console, warning +from aipass.cli.apps.modules import console, error as cli_error, warning # Registry handlers (cross-domain - OK for modules) from aipass.flow.apps.handlers.registry.load_registry import load_registry @@ -264,8 +264,13 @@ def handle_command(command: str, args: List[str]) -> bool: # STEP 1b: Resolve plan type config (for prefix/digits in display) try: plan_type_config = get_plan_type(plan_type_key) - except ValueError: - plan_type_config = None + except ValueError as exc: + cli_error(str(exc)) + console.print() + console.print("[dim]Registered types: drone @flow templates[/dim]") + console.print("[dim]Register new: drone @flow register