feat(flow): unified plan pipeline — plugin architecture for FPLAN/DPLAN parity

Refactor flow's plan system from two parallel pipelines (FPLAN + DPLAN) into
one unified, plugin-based architecture. Plan types are data plugins — adding
a new type means dropping a folder in plan_types/, zero code changes.

- Unified create: `drone @flow create . "subject" dplan` now works correctly
- Unified close: detects plan type from filename prefix, routes to correct registry
- Unified list: aggregates plans across all per-type registries
- Per-type registries: fplan_registry.json + dplan_registry.json (4-digit counters)
- Archive to backup/processed_plans/ (was flow/processed_plans/)
- Vector processing wired to memory's plans_processor on close
- Display shows correct prefix (DPLAN-0007 not FPLAN-0007)
- Archived old dual-pipeline code: dplan_flow.py, handlers/dplan/, dev_planning/

Design: DPLAN-0072 | Execution: FPLAN-0078 (master, 4 phases)

Co-Authored-By: @flow <flow@aipass>
This commit is contained in:
AIOSAI
2026-03-17 17:23:11 -07:00
co-authored by @flow
parent 18acb30f83
commit 202c2294a7
38 changed files with 476 additions and 3976 deletions
+40 -29
View File
@@ -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*
+10 -29
View File
@@ -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()
@@ -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:
@@ -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
@@ -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
@@ -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`
@@ -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
"""
@@ -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}",
}
@@ -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}",
}
@@ -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
}, "")
@@ -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),
}
@@ -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
@@ -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}"
@@ -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
@@ -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 <subcommand> [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 <number> - 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 <type> - Plan type: dplan (default), bplan
--tag <tag> - Filter by tag (list) or set tag (create)
--status <s> - Filter by status (list only)
--dir <name> - Create in dev_planning/<name>/ subdirectory
@<branch> - 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)
@@ -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, ""
@@ -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}",
}
@@ -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 ""
@@ -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 ""
@@ -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, ""
+110 -172
View File
@@ -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)}
@@ -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", {})
+108 -33
View File
@@ -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)
+30 -21
View File
@@ -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(
@@ -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
+51 -22
View File
@@ -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,
@@ -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
+28 -21
View File
@@ -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
@@ -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:
@@ -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
+4 -4
View File
@@ -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)
-591
View File
@@ -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 <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
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)
@@ -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()
@@ -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*
@@ -1,6 +0,0 @@
{
"DPLAN": {
"next_number": 4
},
"next_number": 4
}
-253
View File
@@ -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 <file> # 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 <file>`
- [ ] 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}
```
@@ -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}}*
-514
View File
@@ -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 <file> # 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}
```