Files
AIPass/src/aipass/flow/apps/modules/dplan_flow.py
T
AIOSAIandClaude Opus 4.6 97952124b5 feat(system): stderr routing migration Phase 3 — 10 branches migrated (FPLAN-0033)
Migrated error()/warning() calls across 10 branches to use CLI display API
which routes to stderr. 48 files modified across ai_mail, api, backup, daemon,
flow, memory, prax, seedgo, spawn, trigger. Seedgo stderr_routing standard
created (24th standard) with full-file scanning. System average 49% → 69%.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 00:41:38 -07:00

592 lines
21 KiB
Python

# =================== AIPass ====================
# Name: dplan_flow.py
# Description: Plan management module (thin orchestrator)
# Version: 5.0.0
# Created: 2025-12-02
# Modified: 2025-12-02
# =============================================
"""
Plan Management Module - Thin Orchestrator
Routes commands to handlers in handlers/dplan/.
Manages numbered, dated planning documents (DPLAN, BPLAN) in dev_planning/.
Supports @ branch resolution for creating plans in other branches.
"""
import sys
from pathlib import Path
from typing import List
# Infrastructure imports (module does the logging)
from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.cli.apps.modules import console, header, success, error, warning
# Handler imports (local handlers in handlers/dplan/)
from aipass.flow.apps.handlers.dplan.create import create_plan
from aipass.flow.apps.handlers.dplan.list import list_plans
from aipass.flow.apps.handlers.dplan.status import get_status_summary, get_status_icon, VALID_TAGS
from aipass.flow.apps.handlers.dplan.display import show_help, print_introspection
from aipass.flow.apps.handlers.dplan.close import (
normalize_plan_number, close_plan, get_open_plans
)
from aipass.flow.apps.handlers.dplan.counter import VALID_PLAN_TYPES
from aipass.flow.apps.handlers.dplan.registry import (
register_plan, update_plan_status, populate_from_filesystem,
get_summary, save_plan_summary, generate_description_summary
)
from aipass.flow.apps.handlers.dplan.dashboard import push_all as _push_dashboard_raw
from aipass.prax.apps.handlers.dashboard.operations import write_section
# Local handlers (file I/O extracted from this module)
from aipass.flow.apps.handlers.dplan.branch_resolve import resolve_branch_target as _resolve_branch
from aipass.flow.apps.handlers.dplan.closed_plans_registry import append_closed_dplan
from aipass.flow.apps.handlers.dplan.log_setup import prepare_log_file
from aipass.flow.apps.handlers.dplan.background_spawn import spawn_post_close
def push_dashboard(activity: str | None = None) -> dict:
"""Module-level wrapper: injects write_section into handler."""
return _push_dashboard_raw(activity=activity, write_fn=write_section)
# =============================================================================
# @ BRANCH RESOLUTION (thin wrapper around handler)
# =============================================================================
def resolve_branch_target(branch_ref: str):
"""Resolve @branch reference — delegates to handler, logs result."""
result = _resolve_branch(branch_ref)
if not result["success"]:
logger.warning(f"[dev_flow] {result['error']}")
return None
return result["path"]
# =============================================================================
# MODULE INTERFACE
# =============================================================================
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("dplan_flow Module")
console.print("Plan management orchestrator — routes plan commands to handlers")
console.print()
console.print("Connected Handlers:")
console.print(" handlers/dplan/")
console.print(" - create.py (create_plan — create new plans)")
console.print(" - list.py (list_plans — list plans with filters)")
console.print(" - status.py (get_status_summary — plan status aggregation)")
console.print(" - display.py (show_help — help text display)")
console.print(" - close.py (close_plan, get_open_plans — close plans)")
console.print(" - counter.py (VALID_PLAN_TYPES — plan type definitions)")
console.print(" - registry.py (register_plan, update_plan_status — registry ops)")
console.print(" - dashboard.py (push_all — dashboard updates)")
console.print(" - branch_resolve.py (resolve_branch_target — @ branch resolution)")
console.print(" - closed_plans_registry.py (append_closed_dplan — closed plan tracking)")
console.print(" - log_setup.py (prepare_log_file — log file preparation)")
console.print(" - background_spawn.py (spawn_post_close — background archival)")
console.print()
console.print(" External:")
console.print(" - aipass.prax (write_section — dashboard section writer)")
console.print()
def print_help():
"""Display D-PLAN help text — thin wrapper around handler's show_help()."""
header("D-PLAN - Development Planning")
console.print(show_help())
def handle_command(command: str, args: List[str]) -> bool:
"""
Handle D-PLAN commands - routes to handlers.
Args:
command: Command to execute ('plan')
args: Command arguments
Returns:
True if command was handled, False otherwise
"""
if command != 'plan':
return False
# Handle --help / -h flag
if not args or (args[0] in ('--help', '-h')):
print_help()
return True
subcommand = args[0]
if subcommand == 'create':
return _handle_create(args[1:])
elif subcommand == 'list':
return _handle_list(args[1:])
elif subcommand == 'status':
return _handle_status(args[1:])
elif subcommand == 'close':
return _handle_close(args[1:])
elif subcommand == 'sync':
return _handle_sync()
else:
error(f"Unknown subcommand: {subcommand}")
console.print("Run 'plan --help' for usage")
return True
# =============================================================================
# COMMAND HANDLERS (orchestration only)
# =============================================================================
def _handle_create(args: List[str]) -> bool:
"""Orchestrate plan creation - delegates to handler"""
if args and args[0] in ('--help', '-h'):
console.print("\n[bold]USAGE:[/bold]")
console.print(" plan create \"topic name\" [--type type] [--tag tag] [--dir subdir] [@branch]")
console.print("\n[bold]OPTIONS:[/bold]")
console.print(" --type <type> Plan type: dplan (default), bplan")
console.print(" --tag <tag> Set plan tag (default: idea)")
console.print(" --dir <name> Create plan in dev_planning/<name>/ subdirectory")
console.print(" @<branch> Create in target branch's dev_planning/")
console.print(f"\n[bold]TAGS:[/bold] {', '.join(VALID_TAGS)}")
console.print("\n[bold]EXAMPLES:[/bold]")
console.print(" plan create \"new feature design\"")
console.print(" plan create \"API upgrade\" --tag upgrade")
console.print(" plan create \"revenue model\" --type bplan")
console.print(" plan create \"vera improvements\" @vera\n")
return True
if len(args) < 1:
error("Usage: plan create \"topic name\" [--type type] [--tag tag] [@branch]")
return True
# Parse arguments: topic and optional flags
topic = args[0]
subdir = None
tag = "idea"
plan_type = "dplan"
target_path = None
target_branch_name = None
if '--dir' in args:
dir_idx = args.index('--dir')
if dir_idx + 1 < len(args):
subdir = args[dir_idx + 1]
else:
error("--dir requires a subdirectory name")
return True
if '--tag' in args:
tag_idx = args.index('--tag')
if tag_idx + 1 < len(args):
tag = args[tag_idx + 1].lower()
if tag not in VALID_TAGS:
error(f"Invalid tag '{tag}'. Valid tags: {', '.join(VALID_TAGS)}")
return True
else:
error("--tag requires a tag name")
return True
if '--type' in args:
type_idx = args.index('--type')
if type_idx + 1 < len(args):
plan_type = args[type_idx + 1].lower()
if plan_type not in VALID_PLAN_TYPES:
valid = ", ".join(VALID_PLAN_TYPES.keys())
error(f"Invalid plan type '{plan_type}'. Valid types: {valid}")
return True
else:
error("--type requires a plan type (dplan, bplan)")
return True
# Check for @branch target or pre-resolved path
for arg in args[1:]:
if arg.startswith("@") and not arg.startswith("--"):
target_branch_name = arg
target_path = resolve_branch_target(arg)
if target_path is None:
error(f"Could not resolve branch target '{arg}'")
return True
break
elif arg.startswith("/") and Path(arg).exists():
target_branch_name = f"@{Path(arg).name}"
target_path = Path(arg)
break
prefix = VALID_PLAN_TYPES[plan_type]
# Delegate to handler
ok, result, err = create_plan(
topic, tag=tag, plan_type=plan_type,
target_path=target_path, subdir=subdir
)
if not ok:
logger.error(f"[dev_flow] Failed to create plan: {err}")
error(f"Failed to create plan: {err}")
return True
logger.info(f"[dev_flow] Created {prefix}-{result['plan_number']:03d}: {result['filename']}")
if result.get('cache_warning'):
logger.warning(f"[dev_flow] {result['cache_warning']}")
# Register in registry (only for local plans, not @ targets)
if not target_path:
try:
register_plan(
plan_number=result['plan_number'],
topic=result['topic'],
status="planning",
tag=tag,
file_path=result['path'],
date=result['date']
)
logger.info(f"[dev_flow] Registered {prefix}-{result['plan_number']:03d} in registry")
except Exception as e:
logger.warning(f"[dev_flow] Failed to register plan: {e}")
try:
activity = f"DPLAN-{result['plan_number']:03d} created ({result['topic'][:30]})"
push_dashboard(activity=activity)
except Exception as e:
logger.warning(f"[dev_flow] Dashboard push failed: {e}")
# Display result
console.print()
success(f"Created {prefix}-{result['plan_number']:03d}")
console.print(f" [dim]Topic:[/dim] {result['topic']}")
console.print(f" [dim]Type:[/dim] {plan_type.upper()}")
console.print(f" [dim]Tag:[/dim] {tag}")
if target_branch_name:
console.print(f" [dim]Target:[/dim] {target_branch_name}")
console.print(f" [dim]File:[/dim] {result['path']}")
console.print()
return True
def _handle_list(args: List[str]) -> bool:
"""Orchestrate plan listing with optional filters"""
filter_tag = None
filter_status = None
filter_type = None
if '--tag' in args:
tag_idx = args.index('--tag')
if tag_idx + 1 < len(args):
filter_tag = args[tag_idx + 1].lower()
if '--status' in args:
status_idx = args.index('--status')
if status_idx + 1 < len(args):
filter_status = args[status_idx + 1].lower()
if '--type' in args:
type_idx = args.index('--type')
if type_idx + 1 < len(args):
filter_type = args[type_idx + 1].lower()
plans, err = list_plans(filter_type=filter_type)
if err:
logger.error(f"[dev_flow] Failed to list plans: {err}")
error(f"Failed to list plans: {err}")
return True
if filter_tag:
plans = [p for p in plans if p.get("tag") == filter_tag]
if filter_status:
plans = [p for p in plans if p.get("status") == filter_status]
console.print()
title = "Plans"
if filter_type:
title = f"{filter_type.upper()}s"
filters = []
if filter_tag:
filters.append(f"tag: {filter_tag}")
if filter_status:
filters.append(f"status: {filter_status}")
if filters:
title += f" ({', '.join(filters)})"
header(title)
console.print()
if not plans:
console.print("[dim]No plans found[/dim]")
console.print()
return True
for p in plans:
status_icon = get_status_icon(p["status"])
tag_display = f"({p['tag']})" if p.get("tag") else ""
prefix = p.get("prefix", "DPLAN")
summary = get_summary(p["number"])
if not summary:
summary = p.get("description", "")
line = f" {status_icon} [cyan]{prefix}-{p['number']:03d}[/cyan] | {p['topic'][:30]:<30}"
if tag_display:
line += f" | [dim]{tag_display}[/dim]"
if summary:
line += f" — [dim italic]{summary[:50]}[/dim italic]"
console.print(line)
console.print()
console.print(f"[dim]Total: {len(plans)} plans[/dim]")
console.print()
return True
def _handle_status(args: List[str]) -> bool:
"""Orchestrate status display - delegates to handler"""
filter_type = None
if '--type' in args:
type_idx = args.index('--type')
if type_idx + 1 < len(args):
filter_type = args[type_idx + 1].lower()
status_counts, total, err = get_status_summary(filter_type=filter_type)
if err:
logger.error(f"[dev_flow] Failed to get status: {err}")
error(f"Failed to get status: {err}")
return True
console.print()
title = "Plan Status"
if filter_type:
title = f"{filter_type.upper()} Status"
header(title)
console.print()
console.print(f" [yellow]Planning:[/yellow] {status_counts['planning']}")
console.print(f" [blue]In Progress:[/blue] {status_counts['in_progress']}")
console.print(f" [green]Ready:[/green] {status_counts['ready']}")
console.print(f" [dim]Complete:[/dim] {status_counts['complete']}")
console.print(f" [red]Abandoned:[/red] {status_counts['abandoned']}")
if status_counts["unknown"] > 0:
console.print(f" [dim]Unknown:[/dim] {status_counts['unknown']}")
console.print()
console.print(f"[dim]Total: {total} plans[/dim]")
console.print()
return True
def _handle_close(args: List[str]) -> bool:
"""Orchestrate plan closing - delegates to handler, spawns background archival"""
if args and args[0] in ('--help', '-h'):
console.print("\n[bold]USAGE:[/bold]")
console.print(" plan close <number>")
console.print(" plan close --all")
console.print("\n[bold]EXAMPLES:[/bold]")
console.print(" plan close 3")
console.print(" plan close DPLAN-003")
console.print(" plan close --all\n")
return True
if args and args[0] == '--all':
return _handle_close_all()
if len(args) < 1:
error("Usage: plan close <number>")
return True
plan_num, err = normalize_plan_number(args[0])
if err:
logger.warning(f"[dev_flow] {err}")
error(err)
return True
# Step 1/3: Close plan (mark as complete)
console.print(f"\n[dim][1/3][/dim] Closing DPLAN-{plan_num:03d}...")
ok, result, err = close_plan(plan_num)
if not ok:
logger.warning(f"[dev_flow] Failed to close DPLAN-{plan_num:03d}: {err}")
error(err)
return True
logger.info(f"[dev_flow] Closed DPLAN-{plan_num:03d}: {result['topic']}")
console.print(f"[green] Marked as complete[/green]")
# Update registry
try:
update_plan_status(plan_num, "complete")
plan_file = Path(result.get('plan_file', ''))
if plan_file.exists():
summary = generate_description_summary(plan_file)
if summary:
save_plan_summary(plan_num, summary, "complete", result['topic'], str(plan_file))
except Exception as e:
logger.warning(f"[dev_flow] Registry update failed: {e}")
# Push dashboard update
try:
activity = f"DPLAN-{plan_num:03d} closed ({result['topic'][:30]})"
push_dashboard(activity=activity)
except Exception as e:
logger.warning(f"[dev_flow] Dashboard push failed: {e}")
# Append to CLOSED_PLANS.local.json via handler
reg_result = append_closed_dplan(plan_num, result.get("topic", ""))
if reg_result["success"]:
logger.info(f"[dev_flow] Updated CLOSED_PLANS registry with {reg_result['plan_id']}")
else:
logger.warning(f"[dev_flow] CLOSED_PLANS update failed (non-critical): {reg_result['error']}")
# Step 2/3: Spawn background processing
console.print(f"[dim][2/3][/dim] Starting background archival...")
try:
log_result = prepare_log_file("post_close_runner.log")
if not log_result["success"]:
raise RuntimeError(log_result["error"])
spawn_result = spawn_post_close(log_file_handle=log_result["file_handle"])
if not spawn_result["success"]:
raise RuntimeError(spawn_result["error"])
logger.info(f"[dev_flow] Spawned background post-processing for DPLAN-{plan_num:03d}")
console.print(f"[dim] Memory Bank archival running in background[/dim]")
except Exception as e:
logger.warning(f"[dev_flow] Failed to spawn background processing: {e}")
warning("Background archival failed to start - will retry on next close")
# Step 3/3: Done
console.print(f"[dim][3/3][/dim] Finalizing...")
console.print()
success(f"DPLAN-{plan_num:03d} closed ({result['topic']})")
console.print(f" [dim]Previous status:[/dim] {result['old_status']}")
console.print(f" [dim]Archive:[/dim] Memory Bank processing in background")
console.print()
return True
def _handle_close_all() -> bool:
"""Close all open plans"""
open_plans = get_open_plans()
if not open_plans:
warning("No open plans to close")
return True
console.print(f"\n[bold yellow]Found {len(open_plans)} open plan(s) to close:[/bold yellow]")
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)