Merge pull request #91 from AIOSAI/feat/flow-featflow-template-registry-system-intros
feat(flow): feat(flow): template registry system, introspection/help overhaul, no-fallback errors
This commit is contained in:
@@ -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 |
|
||||
|------|--------|---------------|-----------|
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 <dir> <PREFIX>[/dim]")
|
||||
console.print()
|
||||
return True
|
||||
|
||||
# STEP 2: Execute workflow
|
||||
success, num, loc, tmpl, error = create_plan(
|
||||
|
||||
@@ -281,7 +281,7 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
console.print()
|
||||
return True
|
||||
|
||||
console.print(f"[yellow]Found {len(unregistered)} unregistered directory(ies):[/yellow]")
|
||||
warning(f"Found {len(unregistered)} unregistered directory(ies):")
|
||||
console.print()
|
||||
|
||||
for entry in unregistered:
|
||||
|
||||
Reference in New Issue
Block a user