feat(flow): feat(flow): foreground archival, --dry-run, template auto-heal, vector verify pipeline (#97)

* feat(prax): monitor restoration phases 1-3, dead module cleanup, help cleanup

Co-Authored-By: @prax <prax@aipass>

* feat(prax): introspection 100%, dead module cleanup, stale logs removed

Co-Authored-By: @prax <prax@aipass>

* feat(memory): config fix + verify command for plan vectorization

Co-Authored-By: @memory <memory@aipass>

* feat(memory): fix plan vectorization: batch subprocess, decouple from startup, add process-plans command

Co-Authored-By: @memory <memory@aipass>

* feat(memory): update plans_processed manifest after batch vectorization of 113 files

Co-Authored-By: @memory <memory@aipass>

* feat(flow): feat(flow): foreground archival, --dry-run, template auto-heal, vector verify pipeline

Co-Authored-By: @flow <flow@aipass>

---------

Co-authored-by: @prax <prax@aipass>
Co-authored-by: @memory <memory@aipass>
Co-authored-by: @flow <flow@aipass>
This commit is contained in:
AIPass
2026-03-18 23:51:15 -07:00
committed by GitHub
co-authored by @prax @memory @flow
parent d714535ad3
commit 55080d5452
20 changed files with 771 additions and 109 deletions
+14 -4
View File
@@ -29,6 +29,8 @@ from datetime import datetime, timezone
from typing import Dict, List, Optional, Any
from aipass.flow.apps.handlers.json import json_handler
from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.cli.apps.modules import error as cli_error, warning as cli_warning
# AI summarization removed — OpenRouter API no longer needed here
# from aipass.api.apps.modules.openrouter_client import get_response
@@ -689,7 +691,10 @@ def verify_and_heal_orphaned_plans() -> Dict[str, Any]:
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:
# Heal ANY closed plan whose file still sits at its original location.
# Covers both fully-processed plans and auto-closed orphans that
# never entered the post-close pipeline.
if plan_info.get("status") == "closed":
original_path = Path(plan_info.get("file_path", ""))
if original_path.exists():
orphans_found += 1
@@ -757,12 +762,17 @@ def process_closed_plans() -> Dict[str, Any]:
if archive_success:
processed_count += 1
# Best-effort vector processing
# Vector processing — errors go to prax log + console
try:
from aipass.memory.apps.handlers.intake.plans_processor import process_plans # type: ignore[import-not-found]
process_plans()
except Exception:
pass
logger.info("[mbank] Vector intake completed for %s", plan_label)
except ImportError:
logger.error("[mbank] Vector intake FAILED for %s — plans_processor not found", plan_label)
cli_error(f"Vector intake unavailable — memory plans_processor not found ({plan_label})")
except Exception as vec_err:
logger.error("[mbank] Vector intake FAILED for %s: %s", plan_label, vec_err)
cli_error(f"Vector intake failed for {plan_label}: {vec_err}")
results.append({"plan": plan_label, "status": "archived", "correlation_id": correlation_id})
else:
error_count += 1
+96 -17
View File
@@ -108,6 +108,7 @@ def _spawn_background_runner():
def close_plan_impl(plan_num: Any = None, confirm: bool = False,
all_plans: bool = False, spawn_background: bool = True,
dry_run: bool = False,
# Dependencies injected from module
normalize_plan_number: Any = None,
load_registry: Any = None,
@@ -131,6 +132,7 @@ def close_plan_impl(plan_num: Any = None, confirm: bool = False,
all_plans: If True, close all open plans (default False)
spawn_background: Whether to spawn background post-processing (default True).
Set False when called from close_all_plans() to avoid race condition.
dry_run: If True, preview what would be closed without taking action (default False)
(remaining args): Handler/service dependencies injected by module
Returns:
@@ -141,7 +143,7 @@ def close_plan_impl(plan_num: Any = None, confirm: bool = False,
# Handle --all flag
if all_plans:
return close_all_plans_fn(confirm)
return close_all_plans_fn(confirm, dry_run=dry_run)
# Single plan closure
if not plan_num:
@@ -194,6 +196,24 @@ def close_plan_impl(plan_num: Any = None, confirm: bool = False,
# Extract prefix for display functions (e.g. "FPLAN", "DPLAN")
plan_prefix = _extract_prefix(plan_label) or "FPLAN"
# DRY RUN: Preview what would be closed, then return early
if dry_run:
location = plan_info.get("location", "unknown")
subject = plan_info.get("subject", "No subject")
status = plan_info.get("status", "unknown")
messages.append({"type": "dim", "text": f"[DRY RUN] Would close {plan_label}"})
messages.append({"type": "dim", "text": f" Location: {location}"})
messages.append({"type": "dim", "text": f" Subject: {subject}"})
messages.append({"type": "dim", "text": f" Status: {status}"})
messages.append({"type": "dim", "text": "No action taken."})
logger.info(f"[{MODULE_NAME}] Dry run: would close {plan_label}")
return {
"success": True,
"messages": messages,
"plan_key": plan_key,
"cancelled": False,
}
# 4. IDEMPOTENCY CHECK: Prevent double-closing (with orphan cleanup)
if plan_info['status'] == 'closed':
closed_date = plan_info.get('closed', 'unknown')
@@ -300,21 +320,57 @@ def close_plan_impl(plan_num: Any = None, confirm: bool = False,
"cancelled": False,
}
# --- Step 3/5: Background processing ---
if spawn_background:
messages.append({"type": "step", "text": "[3/5] Starting background processing..."})
try:
_spawn_background_runner()
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}")
messages.append({"type": "warning", "text": " Background runner not found - will retry on next close"})
except Exception as e:
logger.warning(f"[{MODULE_NAME}] Failed to spawn background post-processing: {e}")
messages.append({"type": "warning", "text": " Background archival failed to start - will retry on next close"})
else:
messages.append({"type": "step", "text": "[3/5] Background processing deferred (batch mode)"})
# --- Step 3/5: Archive plan to processed_plans ---
messages.append({"type": "step", "text": "[3/5] Archiving plan..."})
try:
from aipass.flow.apps.handlers.mbank.process import archive_plan
archive_success = archive_plan(plan_file)
if archive_success:
# Set flags on same registry object we already have in memory
plan_info["processed"] = True
plan_info["processed_date"] = datetime.now(timezone.utc).isoformat()
plan_info["cleanup_completed"] = True
plan_info["cleanup_date"] = datetime.now(timezone.utc).isoformat()
if reg_file:
save_registry(registry, registry_file=reg_file)
else:
save_registry(registry)
logger.info(f"[{MODULE_NAME}] Archived {plan_label} to processed_plans")
messages.append({"type": "dim", "text": " Plan archived to processed_plans/"})
else:
logger.error(f"[{MODULE_NAME}] Failed to archive {plan_label}")
messages.append({"type": "warning", "text": " Archive failed — plan file not moved"})
except Exception as e:
logger.error(f"[{MODULE_NAME}] Archive error for {plan_label}: {e}")
messages.append({"type": "warning", "text": f" Archive error: {e}"})
# --- Vector intake + verification ---
# Trigger memory's plan processor via drone (no cross-branch imports)
try:
subprocess.run(
["drone", "@memory", "process-plans"],
capture_output=True, timeout=30,
)
except Exception:
pass # Best effort — verification below reports actual status
# Verify vectorization via memory's verify module
try:
from aipass.memory.apps.modules.verify import is_plan_vectorized # type: ignore[import-not-found]
result = is_plan_vectorized(plan_label)
if result.get("found"):
chunk_count = result.get("count", 0)
logger.info(f"[{MODULE_NAME}] Vectorized: {plan_label} ({chunk_count} chunks)")
messages.append({"type": "dim", "text": f" Vectorized: {chunk_count} chunks in chroma"})
else:
logger.warning(f"[{MODULE_NAME}] NOT vectorized: {plan_label}")
messages.append({"type": "warning", "text": " NOT vectorized — check drone @memory process-plans"})
except ImportError:
logger.warning(f"[{MODULE_NAME}] Vector verify unavailable — memory verify module not found")
messages.append({"type": "warning", "text": " Vector status: unknown (memory verify not available)"})
except Exception as vec_err:
logger.warning(f"[{MODULE_NAME}] Vector verify failed: {vec_err}")
messages.append({"type": "warning", "text": f" Vector status: unknown ({vec_err})"})
# --- Step 4/5: Update dashboards ---
messages.append({"type": "step", "text": "[4/5] Updating dashboards..."})
@@ -385,7 +441,7 @@ def close_plan_impl(plan_num: Any = None, confirm: bool = False,
}
def close_all_plans_impl(confirm: bool = False,
def close_all_plans_impl(confirm: bool = False, dry_run: bool = False,
# Dependencies injected from module
get_open_plans: Any = None,
close_plan_fn: Any = None) -> Dict[str, Any]:
@@ -394,6 +450,7 @@ def close_all_plans_impl(confirm: bool = False,
Args:
confirm: Whether to ask for bulk confirmation (default False, auto-confirms)
dry_run: If True, preview what would be closed without taking action (default False)
get_open_plans: Handler function to get open plans
close_plan_fn: Function to close a single plan (the module's close_plan)
@@ -416,6 +473,28 @@ def close_all_plans_impl(confirm: bool = False,
"total": 0,
}
# DRY RUN: Preview all plans that would be closed, then return early
if dry_run:
messages.append({"type": "dim", "text": f"[DRY RUN] Would close {len(open_plans)} plan(s):"})
for plan_num, plan_info in open_plans:
subject = plan_info.get("subject", "No subject")
location = plan_info.get("location", "unknown")
# Derive prefix from file_path if available
plan_file = Path(plan_info.get("file_path", ""))
plan_label = plan_file.stem if plan_file.name else f"PLAN-{plan_num}"
prefix = _extract_prefix(plan_label) or "FPLAN"
display_id = f"{prefix}-{plan_num}"
messages.append({"type": "dim", "text": f" {display_id:<14}{location:<14}{subject}"})
messages.append({"type": "dim", "text": "No action taken."})
logger.info(f"[{MODULE_NAME}] Dry run: would close {len(open_plans)} plan(s)")
return {
"success": True,
"messages": messages,
"success_count": 0,
"failure_count": 0,
"total": len(open_plans),
}
# Build plan list for display
plan_list = []
for plan_num, plan_info in open_plans:
@@ -99,45 +99,53 @@ def parse_delete_command_args(args: List[str]) -> Tuple[str | None, bool, str |
return plan_num, confirm, None
def parse_close_command_args(args: List[str]) -> Tuple[str | None, bool, bool, str | None]:
def parse_close_command_args(args: List[str]) -> Tuple[str | None, bool, bool, bool, str | None]:
"""
Parse arguments for close command
Auto-confirms by default (running 'close' IS the intent).
Use --confirm or --interactive to explicitly request a confirmation prompt.
--yes/-y kept for backwards compatibility (now redundant, already auto-confirms).
--dry-run or --preview previews what would be closed without taking action.
Args:
args: Command arguments
Returns:
Tuple of (plan_num, confirm, all_plans, error_message)
Tuple of (plan_num, confirm, all_plans, dry_run, error_message)
- plan_num: Plan number from first arg, or None if --all or missing
- confirm: True only if --confirm or --interactive flag present, False otherwise
- all_plans: True if --all flag present, False otherwise
- dry_run: True if --dry-run or --preview flag present, False otherwise
- error_message: None if valid, error string if invalid args
Examples:
>>> parse_close_command_args(["42"])
("42", False, False, None)
("42", False, False, False, None)
>>> parse_close_command_args(["42", "--yes"])
("42", False, False, None)
("42", False, False, False, None)
>>> parse_close_command_args(["42", "--confirm"])
("42", True, False, None)
("42", True, False, False, None)
>>> parse_close_command_args(["42", "--interactive"])
("42", True, False, None)
("42", True, False, False, None)
>>> parse_close_command_args(["--all"])
(None, False, True, None)
(None, False, True, False, None)
>>> parse_close_command_args(["--all", "--confirm"])
(None, True, True, None)
(None, True, True, False, None)
>>> parse_close_command_args(["42", "--dry-run"])
("42", False, False, True, None)
>>> parse_close_command_args(["--all", "--preview"])
(None, False, True, True, None)
>>> parse_close_command_args([])
(None, False, False, "Plan number or --all required")
(None, False, False, False, "Plan number or --all required")
"""
# Check for --all flag
all_plans = '--all' in args
@@ -147,18 +155,21 @@ def parse_close_command_args(args: List[str]) -> Tuple[str | None, bool, bool, s
# --yes/-y kept for backwards compat (redundant, already auto-confirms)
confirm = '--confirm' in args or '--interactive' in args
# Check for --dry-run or --preview flag
dry_run = '--dry-run' in args or '--preview' in args
# If --all, plan_num is None
if all_plans:
return None, confirm, True, None
return None, confirm, True, dry_run, None
# Otherwise, need plan number
# Filter out flag args to find the plan number
non_flag_args = [a for a in args if not a.startswith('--') and a not in ('-y',)]
if not non_flag_args:
return None, False, False, "Plan number or --all required"
return None, False, False, dry_run, "Plan number or --all required"
plan_num = non_flag_args[0]
return plan_num, confirm, False, None
return plan_num, confirm, False, dry_run, None
def parse_restore_command_args(args: List[str]) -> Tuple[str | None, str | None]:
@@ -147,6 +147,38 @@ def load_registry() -> Dict[str, Any]:
"type_count": len(data["types"]),
}
# Auto-heal: prune orphaned types (directory deleted but registry entry remains)
templates_dir = FLOW_ROOT / "templates"
orphaned = [
dir_name
for dir_name in data["types"]
if dir_name not in _PROTECTED_TYPES
and not (templates_dir / dir_name).is_dir()
]
if orphaned:
plan_registry_dir = FLOW_ROOT / "flow_json"
for dir_name in orphaned:
entry = data["types"][dir_name]
shorthand = entry.get("shorthand", entry.get("prefix", "").lower())
logger.info(
"[%s] Auto-pruning orphaned type '%s' (directory missing)",
MODULE_NAME,
dir_name,
)
del data["types"][dir_name]
# Clean up the per-type plan registry JSON
if shorthand:
plan_reg = plan_registry_dir / f"{shorthand}_registry.json"
if plan_reg.exists():
plan_reg.unlink()
logger.info(
"[%s] Removed orphaned plan registry: %s",
MODULE_NAME,
plan_reg.name,
)
save_registry(data)
return data
+10 -4
View File
@@ -185,11 +185,13 @@ def print_help():
console.print("[yellow]OPTIONS:[/yellow]")
console.print(" --all Close all open plans")
console.print(" --confirm Interactive confirmation prompt")
console.print(" --dry-run Preview what would be closed (no action taken)")
console.print()
console.print("[yellow]EXAMPLES:[/yellow]")
console.print(" [dim]drone @flow close FPLAN-0042[/dim] # Close specific plan")
console.print(" [dim]drone @flow close DPLAN-0005[/dim] # Close a DPLAN")
console.print(" [dim]drone @flow close --all[/dim] # Close all open plans")
console.print(" [dim]drone @flow close --all --dry-run[/dim] # Preview close-all")
console.print()
@@ -197,7 +199,7 @@ def print_help():
# CLOSE PLAN WORKFLOW (thin orchestrator)
# =============================================
def close_plan(plan_num: str | None = None, confirm: bool = False, all_plans: bool = False, spawn_background: bool = True) -> bool:
def close_plan(plan_num: str | None = None, confirm: bool = False, all_plans: bool = False, spawn_background: bool = True, dry_run: bool = False) -> bool:
"""
Orchestrate plan closure workflow (thin orchestrator)
@@ -218,6 +220,7 @@ def close_plan(plan_num: str | None = None, confirm: bool = False, all_plans: bo
all_plans: If True, close all open plans (default False)
spawn_background: Whether to spawn background post-processing (default True).
Set False when called from close_all_plans() to avoid race condition.
dry_run: If True, preview what would be closed without taking action (default False)
Returns:
True if successful, False otherwise
@@ -227,6 +230,7 @@ def close_plan(plan_num: str | None = None, confirm: bool = False, all_plans: bo
confirm=confirm,
all_plans=all_plans,
spawn_background=spawn_background,
dry_run=dry_run,
# Inject dependencies
normalize_plan_number=normalize_plan_number,
load_registry=load_registry,
@@ -249,18 +253,20 @@ def close_plan(plan_num: str | None = None, confirm: bool = False, all_plans: bo
return bool(result)
def close_all_plans(confirm: bool = False) -> bool:
def close_all_plans(confirm: bool = False, dry_run: bool = False) -> bool:
"""
Close all open plans in one operation (thin orchestrator)
Args:
confirm: Whether to ask for bulk confirmation (default False, auto-confirms)
dry_run: If True, preview what would be closed without taking action (default False)
Returns:
True if at least one plan closed successfully, False otherwise
"""
result = close_all_plans_impl(
confirm=confirm,
dry_run=dry_run,
get_open_plans=get_open_plans,
close_plan_fn=close_plan,
)
@@ -313,7 +319,7 @@ def handle_command(command: str, args: List[str]) -> bool:
)
# 1. PARSE ARGS: Use command_parser handler
plan_num, confirm, all_plans, error = parse_close_command_args(args)
plan_num, confirm, all_plans, dry_run, error = parse_close_command_args(args)
# 2. VALIDATE: Check for parsing errors
if error:
@@ -321,7 +327,7 @@ def handle_command(command: str, args: List[str]) -> bool:
return True # Command was handled (error already displayed)
# 3. EXECUTE: Run workflow orchestrator
close_plan(plan_num=plan_num, confirm=confirm, all_plans=all_plans)
close_plan(plan_num=plan_num, confirm=confirm, all_plans=all_plans, dry_run=dry_run)
# 4. RETURN: True = command was handled (even if the operation failed,
# the error has already been displayed -- returning False would cause
@@ -1,13 +0,0 @@
# {plan_number}: {subject}
Tag: {tag}
> Testing template — auto-discovered from filesystem
---
## Notes
---
*Created: {today}*
*Updated: {today}*
@@ -36,7 +36,18 @@ logger = get_system_logger()
# =============================================================================
_MEMORY_ROOT = Path(__file__).resolve().parents[3]
CENTRAL_FILE = _MEMORY_ROOT / "central" / "memory_bank.central.json"
def _find_repo_root() -> Path:
"""Walk up from this file to find repo root (contains AIPASS_REGISTRY.json)."""
current = Path(__file__).resolve().parent
for parent in [current] + list(current.parents):
if (parent / "AIPASS_REGISTRY.json").exists():
return parent
return Path.cwd()
CENTRAL_FILE = _find_repo_root() / ".ai_central" / "MEMORY.central.json"
CHROMA_DB_PATH = _MEMORY_ROOT / ".chroma"
ARCHIVE_DIR = _MEMORY_ROOT / ".archive"
@@ -38,7 +38,6 @@ _MEMORY_ROOT = Path(__file__).resolve().parents[3]
# CONSTANTS
# =============================================================================
CENTRAL_FILE = _MEMORY_ROOT / "central" / "memory_bank.central.json"
CONFIG_PATH = _MEMORY_ROOT / "config" / "memory_bank.config.json"
TEMPLATE_VERSION_FILE = _MEMORY_ROOT / "templates" / ".template_version.json"
@@ -52,6 +51,7 @@ def _find_repo_root() -> Path:
return Path.cwd()
CENTRAL_FILE = _find_repo_root() / ".ai_central" / "MEMORY.central.json"
AIPASS_REGISTRY = _find_repo_root() / "AIPASS_REGISTRY.json"
# Near-rollover threshold: branches with fewer than this many lines remaining
@@ -259,10 +259,16 @@ def process_plans() -> Dict[str, Any]:
logger.info(f"[plans] Found {len(unprocessed)} unprocessed plan files")
total_chunks = 0
files_processed = 0
errors = []
# -- Phase 1: Read all files, chunk them, collect texts + metadatas ----------
all_texts: List[str] = []
all_metadatas: List[Dict[str, str]] = []
# Track which files produced chunks (for manifest update)
files_with_chunks: List[Path] = []
# Files with 0 chunks still get marked in manifest (e.g. template content)
files_without_chunks: List[Path] = []
for plan_file in unprocessed:
try:
text = plan_file.read_text(encoding='utf-8')
@@ -270,58 +276,65 @@ def process_plans() -> Dict[str, Any]:
errors.append(f'{plan_file.name}: read error: {e}')
continue
# Chunk the plan
chunks = _chunk_plan_text(text, plan_file.name)
if not chunks:
manifest[plan_file.name] = datetime.now().isoformat()
files_without_chunks.append(plan_file)
continue
# Extract texts and build metadata
texts = [c['text'] for c in chunks]
metadatas = [
{
files_with_chunks.append(plan_file)
for c in chunks:
all_texts.append(c['text'])
all_metadatas.append({
'source_file': plan_file.name,
'section': c['section'],
'processed_at': datetime.now().isoformat(),
'type': 'plan'
}
for c in chunks
]
})
# Embed
embed_result = _embed_texts(texts)
if not embed_result.get('success'):
errors.append(f"{plan_file.name}: embed error: {embed_result.get('error')}")
continue
total_chunks = len(all_texts)
files_processed = 0
embeddings = embed_result.get('embeddings', [])
if not embeddings:
errors.append(f'{plan_file.name}: no embeddings returned')
continue
# Store
store_result = _store_vectors(embeddings, texts, metadatas, collection_name)
if not store_result.get('success'):
errors.append(f"{plan_file.name}: store error: {store_result.get('error')}")
continue
# Mark as processed
# Mark empty-chunk files in manifest immediately (nothing to embed)
for plan_file in files_without_chunks:
manifest[plan_file.name] = datetime.now().isoformat()
files_processed += 1
total_chunks += len(chunks)
logger.info(f"[plans] Processed {plan_file.name}: {len(chunks)} chunks vectorized")
# Save manifest
# -- Phase 2: Batch embed + store (single subprocess each) ------------------
if all_texts:
logger.info(f"[plans] Batch embedding {total_chunks} chunks from {len(files_with_chunks)} files")
embed_result = _embed_texts(all_texts)
if not embed_result.get('success'):
error_msg = f"batch embed error: {embed_result.get('error')}"
logger.error(f"[plans] {error_msg}")
errors.append(error_msg)
else:
embeddings = embed_result.get('embeddings', [])
if not embeddings:
errors.append('batch embed returned no embeddings')
else:
store_result = _store_vectors(embeddings, all_texts, all_metadatas, collection_name)
if not store_result.get('success'):
error_msg = f"batch store error: {store_result.get('error')}"
logger.error(f"[plans] {error_msg}")
errors.append(error_msg)
else:
# Success — mark all chunk-producing files in manifest
for plan_file in files_with_chunks:
manifest[plan_file.name] = datetime.now().isoformat()
files_processed = len(files_with_chunks)
logger.info(f"[plans] Batch complete: {files_processed} files, {total_chunks} chunks vectorized")
# Save manifest (includes empty-chunk files even if embedding failed)
_save_manifest(manifest)
result = {
'success': files_processed > 0 or not errors,
result: Dict[str, Any] = {
'success': files_processed > 0 or (not errors and not files_with_chunks),
'files_processed': files_processed,
'total_chunks': total_chunks,
'total_chunks': total_chunks if files_processed > 0 else 0,
}
if errors:
result['errors'] = errors
json_handler.log_operation("process_plans", {"files_processed": files_processed, "total_chunks": total_chunks, "success": result['success']})
json_handler.log_operation("process_plans", {"files_processed": files_processed, "total_chunks": result['total_chunks'], "success": result['success']})
return result
@@ -295,12 +295,14 @@ def _check_memory_pool() -> Dict[str, Any]:
def _check_plans() -> Dict[str, Any]:
"""
Check plans directory for files to vectorize.
Check plans directory for unprocessed files (count only).
Processes any plan files that haven't been vectorized yet.
Does NOT call process_plans() — that spawns heavy ML subprocesses.
Only counts pending files and reports. Use 'drone @memory process-plans'
to trigger actual vectorization.
Returns:
Dict with processing status
Dict with pending file count
"""
import json
@@ -308,7 +310,7 @@ def _check_plans() -> Dict[str, Any]:
# Load config
try:
with open(config_path) as f:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
plans_config = config.get('plans', {})
except Exception:
@@ -320,7 +322,8 @@ def _check_plans() -> Dict[str, Any]:
# Get plans path and count files (supports absolute paths)
plans_dir = plans_config.get('path', 'plans')
plans_path = Path(plans_dir) if Path(plans_dir).is_absolute() else _MEMORY_ROOT / plans_dir
repo_root = _find_repo_root()
plans_path = Path(plans_dir) if Path(plans_dir).is_absolute() else repo_root / plans_dir
extensions = plans_config.get('supported_extensions', ['.md'])
if not plans_path.exists():
@@ -333,21 +336,24 @@ def _check_plans() -> Dict[str, Any]:
file_count = len(files)
if file_count == 0:
return {'success': True, 'files_in_plans': 0, 'action': 'none'}
return {'success': True, 'pending_files': 0, 'action': 'count_only'}
# Process plans to vectors
try:
# NOTE: intake module not yet ported to aipass.memory package
from aipass.memory.apps.handlers.intake.plans_processor import process_plans # type: ignore[import-not-found]
result = process_plans()
return {
'success': result.get('success', False),
'files_processed': result.get('files_processed', 0),
'total_chunks': result.get('total_chunks', 0),
'action': 'processed'
}
except Exception as e:
return {'success': False, 'error': str(e), 'action': 'failed'}
# Load manifest to count unprocessed files
manifest_path = _MEMORY_ROOT / "config" / ".plans_processed.json"
manifest: Dict[str, str] = {}
if manifest_path.exists():
try:
manifest = json.loads(manifest_path.read_text(encoding='utf-8'))
except Exception:
pass
pending = [f for f in files if f.name not in manifest]
pending_count = len(pending)
if pending_count > 0:
logger.info(f"[plans] {pending_count} plans pending vectorization. Run: drone @memory process-plans")
return {'success': True, 'pending_files': pending_count, 'action': 'count_only'}
def _check_code_archive() -> Dict[str, Any]:
@@ -107,6 +107,49 @@ def _list_collections(db_path=None):
}
def _check_plan(plan_label, db_path=None):
"""Check if a plan has been vectorized in ChromaDB.
Args:
plan_label: Plan label to search for (e.g., "FPLAN-0126")
db_path: Optional path to Chroma database
Returns:
Dict with found status, count of matching chunks, and source files
"""
client = _get_client(db_path)
collection_name = "flow_flow_plans"
try:
collection = client.get_collection(collection_name, embedding_function=None)
except Exception:
return {
'success': True,
'found': False,
'count': 0,
'source_files': [],
'message': f'Collection {collection_name} does not exist'
}
result = collection.get(include=["metadatas"])
metadatas = result.get('metadatas', [])
matching_files = set()
match_count = 0
for metadata in metadatas:
source_file = metadata.get('source_file', '')
if plan_label in source_file:
match_count += 1
matching_files.add(source_file)
return {
'success': True,
'found': match_count > 0,
'count': match_count,
'source_files': sorted(matching_files)
}
def _search_vectors(query_embedding, branch=None, memory_type=None, n_results=5, db_path=None):
"""Search for similar vectors."""
client = _get_client(db_path)
@@ -186,6 +229,11 @@ def main():
n_results=input_data.get('n_results', 5),
db_path=input_data.get('db_path')
)
elif operation == 'check_plan':
result = _check_plan(
plan_label=input_data.get('plan_label'),
db_path=input_data.get('db_path')
)
else:
result = {'success': False, 'error': f'Unknown operation: {operation}'}
@@ -133,6 +133,10 @@ def handle_command(command: str, args: List[str]) -> bool:
sync_line_counts()
return True
elif command == 'process-plans':
process_plans_command()
return True
return False
@@ -226,6 +230,61 @@ def run_rollover() -> bool:
return success_count > 0
# =============================================================================
# PLAN VECTORIZATION
# =============================================================================
def process_plans_command() -> None:
"""
Process pending plan files into vector storage.
Batches all chunks from all files into a single embed + store call.
"""
console.print()
console.print(Panel.fit(
"[bold cyan]Memory - Process Plans[/bold cyan]",
border_style="cyan",
box=box.ROUNDED
))
console.print()
console.print("[cyan]Processing plan files into vector storage...[/cyan]")
console.print()
try:
from ..handlers.intake.plans_processor import process_plans
result = process_plans()
except Exception as e:
error(f"Plan processing failed: {e}")
return
if not result.get('success'):
error(result.get('error', 'Unknown error'))
if result.get('errors'):
for err in result['errors']:
error(err)
return
files_processed = result.get('files_processed', 0)
total_chunks = result.get('total_chunks', 0)
reason = result.get('reason', '')
if files_processed == 0 and reason:
console.print(f"[green]>[/green] {reason}")
elif files_processed == 0:
console.print("[green]>[/green] No new plans to process")
else:
console.print(f"[green]>[/green] Processed {files_processed} files ({total_chunks} chunks vectorized)")
if result.get('errors'):
console.print()
for err in result['errors']:
error(err)
console.print()
json_handler.log_operation("process_plans_command", {"files_processed": files_processed, "total_chunks": total_chunks})
# =============================================================================
# LINE COUNT SYNC
# =============================================================================
+285
View File
@@ -0,0 +1,285 @@
# =================== AIPass ====================
# Name: verify.py
# Description: Plan Verification Module
# Version: 0.1.0
# Created: 2026-03-18
# Modified: 2026-03-18
# =============================================
"""
Plan Verification Module
Checks whether a plan has been vectorized in ChromaDB.
Purpose:
Thin orchestration layer - calls chroma_subprocess via subprocess
to query the flow_flow_plans collection for a given plan label.
"""
import subprocess
import json
import os
import sys
from pathlib import Path
from typing import List
from aipass.cli.apps.modules import console, error
from aipass.memory.apps.handlers.json import json_handler
# =============================================================================
# INFRASTRUCTURE SETUP
# =============================================================================
# Subprocess script for ChromaDB operations (run in memory venv)
_HANDLERS_DIR = Path(__file__).resolve().parent.parent / "handlers"
CHROMA_SUBPROCESS_SCRIPT = _HANDLERS_DIR / "storage" / "chroma_subprocess.py"
# Memory venv python -- auto-detect from memory/.venv/ or use env var override
_MEMORY_ROOT = Path(__file__).resolve().parents[2]
_MEMORY_VENV_PYTHON = _MEMORY_ROOT / ".venv" / "bin" / "python"
def _get_memory_python() -> str:
"""Get the Python executable for memory ML operations."""
env_override = os.environ.get("AIPASS_MEMORY_PYTHON")
if env_override:
return env_override
if _MEMORY_VENV_PYTHON.exists():
return str(_MEMORY_VENV_PYTHON)
return sys.executable
# =============================================================================
# COMMAND HANDLERS
# =============================================================================
def handle_command(command: str, args: List[str]) -> bool:
"""
Handle verify commands with seedgo-compliant introspection.
Routing:
verify (no args) -> print_introspection()
verify --help/-h/help -> print_help()
verify <plan_label> -> check plan vectorization
Args:
command: Command name
args: Additional arguments (plan label)
Returns:
True if command handled, False otherwise
"""
# Top-level help (backward compat -- entry point may send these)
if command in ('--help', '-h', 'help'):
print_help()
return True
if command == 'verify':
# No args -> introspection (seedgo standard)
if not args:
print_introspection()
return True
# --help / -h / help -> full help
if args[0] in ('--help', '-h', 'help'):
print_help()
return True
# First arg is the plan label
plan_label = args[0]
_verify_plan(plan_label)
return True
return False
# =============================================================================
# VERIFICATION LOGIC
# =============================================================================
def _check_plan_subprocess(plan_label: str) -> dict:
"""
Check plan vectorization via chroma_subprocess.
Args:
plan_label: Plan label to check (e.g., "FPLAN-0126")
Returns:
Dict with success, found, count, source_files
"""
python_path = _get_memory_python()
input_data = json.dumps({
'operation': 'check_plan',
'plan_label': plan_label,
})
try:
result = subprocess.run(
[python_path, str(CHROMA_SUBPROCESS_SCRIPT)],
input=input_data,
capture_output=True,
text=True,
timeout=60
)
if result.returncode != 0:
return {'success': False, 'error': result.stderr or 'Subprocess failed'}
return json.loads(result.stdout)
except subprocess.TimeoutExpired:
return {'success': False, 'error': 'Check operation timed out'}
except json.JSONDecodeError as e:
return {'success': False, 'error': f'Invalid JSON response: {e}'}
except Exception as e:
return {'success': False, 'error': str(e)}
def is_plan_vectorized(plan_label: str) -> dict:
"""
Check if a plan has been vectorized in ChromaDB.
Programmatic API for use by other modules.
Args:
plan_label: Plan label to check (e.g., "FPLAN-0126")
Returns:
Dict with keys: success, found, count, source_files
"""
return _check_plan_subprocess(plan_label)
def _verify_plan(plan_label: str) -> None:
"""
Verify a plan's vectorization status and display the result.
Args:
plan_label: Plan label to check (e.g., "FPLAN-0126")
"""
result = _check_plan_subprocess(plan_label)
if not result.get('success'):
error(result.get('error', 'Unknown error'))
json_handler.log_operation("verify_plan", {"plan_label": plan_label, "success": False})
return
found = result.get('found', False)
count = result.get('count', 0)
console.print()
if found:
console.print(f" Plan {plan_label}: [green]Vectorized[/green] ({count} chunks)")
else:
console.print(f" Plan {plan_label}: [red]NOT vectorized[/red]")
console.print()
json_handler.log_operation("verify_plan", {
"plan_label": plan_label,
"found": found,
"count": count,
"success": True,
})
# =============================================================================
# INTROSPECTION
# =============================================================================
def _discover_handlers() -> dict[str, list[str]]:
"""Auto-discover handler directories and their Python files.
Scans the handlers/ directory relative to this module.
Returns:
Dict mapping handler directory name to list of .py filenames
(excluding __init__.py and __pycache__).
"""
handlers_dir = Path(__file__).resolve().parent.parent / "handlers"
result: dict[str, list[str]] = {}
if not handlers_dir.exists():
return result
for d in sorted(handlers_dir.iterdir()):
if not d.is_dir() or d.name.startswith("__"):
continue
py_files = sorted(
f.name for f in d.iterdir()
if f.is_file() and f.suffix == ".py" and f.name != "__init__.py"
)
if py_files:
result[d.name] = py_files
return result
def print_introspection() -> None:
"""Display module introspection info (seedgo standard).
Called when 'verify' is invoked with no arguments.
Shows module identity, connected handlers, and next-step hints.
"""
console.print()
console.print("[bold cyan]verify Module[/bold cyan]")
console.print("Checks whether a plan has been vectorized in ChromaDB")
console.print()
# Connected handlers (auto-discovered)
handlers = _discover_handlers()
console.print("[yellow]Connected Handlers:[/yellow]")
if handlers:
for dir_name, files in handlers.items():
file_list = ", ".join(files)
console.print(f" [cyan]handlers/{dir_name}/[/cyan] [dim]{file_list}[/dim]")
else:
console.print(" [dim]No handlers found[/dim]")
console.print()
# Next-step hints
console.print("[yellow]Next:[/yellow]")
console.print(' [green]drone @memory verify FPLAN-0126[/green] [dim]# Check if plan is vectorized[/dim]')
console.print(" [green]drone @memory verify --help[/green] [dim]# Full usage guide[/dim]")
console.print()
def print_help() -> None:
"""Display verify module help."""
console.print()
console.print("[bold cyan]Verify Module - Plan Vectorization Check[/bold cyan]")
console.print()
console.print("[bold]USAGE:[/bold]")
console.print(" drone @memory verify <plan_label>")
console.print()
console.print("[bold]COMMANDS:[/bold]")
console.print(" [cyan]verify <plan_label>[/cyan] Check if a plan is vectorized in ChromaDB")
console.print(" [cyan]help[/cyan] Show this help message")
console.print()
console.print("[bold]EXAMPLES:[/bold]")
console.print(" [dim]drone @memory verify FPLAN-0126[/dim]")
console.print(" [dim]drone @memory verify HPLAN-0001[/dim]")
console.print()
console.print("[bold]HOW IT WORKS:[/bold]")
console.print(" 1. Query the flow_flow_plans ChromaDB collection")
console.print(" 2. Filter entries by source_file metadata matching the plan label")
console.print(" 3. Report vectorization status and chunk count")
console.print()
# =============================================================================
# STANDALONE EXECUTION
# =============================================================================
if __name__ == "__main__":
# No args -> introspection (seedgo standard)
if len(sys.argv) < 2:
handle_command('verify', [])
sys.exit(0)
# --help -> full help
if sys.argv[1] in ('--help', '-h', 'help'):
handle_command('verify', ['--help'])
sys.exit(0)
# Execute command via handle_command
command = sys.argv[1]
if not handle_command(command, sys.argv[2:]):
console.print(f"[red]Unknown command:[/red] {command}")
console.print("Run with [cyan]help[/cyan] for available commands")
sys.exit(1)
+116 -1
View File
@@ -7,5 +7,120 @@
"FPLAN-0032_cli_stderr_standardization_phase_1_add_e_2026-03-13.md": "2026-03-17T17:04:20.978306",
"FPLAN-0034_fplan_0033_wave_1_stderr_migration_api_s_2026-03-14.md": "2026-03-17T17:04:31.760541",
"FPLAN-0031_drone_stderr_and_error_propagation_inves_2026-03-13.md": "2026-03-17T17:04:42.782966",
"FPLAN-0047_memory_rollover_introspection_2026-03-15.md": "2026-03-17T17:04:54.278205"
"FPLAN-0047_memory_rollover_introspection_2026-03-15.md": "2026-03-17T17:04:54.278205",
"PPLAN-0001_test_pplan_auto_resolve_2026-03-18.md": "2026-03-18T22:54:42.823250",
"FPLAN-0130_e2e_vector_test_2026-03-18.md": "2026-03-18T22:55:51.288347",
"FPLAN-0118_vector_logging_test_2026-03-18.md": "2026-03-18T22:55:51.288363",
"FPLAN-0112_verify_fplan_2026-03-18.md": "2026-03-18T22:55:51.288366",
"FPLAN-0059_prax_introspection_gates_11_modules_2026-03-17.md": "2026-03-18T22:55:51.288369",
"FPLAN-0058_simplify_logging_execute_two_tier_refact_2026-03-16.md": "2026-03-18T22:55:51.288372",
"FPLAN-0007_seedgo_architecture_checker_activation_2026-03-07.md": "2026-03-18T22:55:51.288374",
"FPLAN-0054_symbolic_module_handlers_import_moderniz_2026-03-16.md": "2026-03-18T22:55:51.288377",
"FPLAN-0097_fix_commons_integration_gaps_fts5_sync_visito_2026-03-17.md": "2026-03-18T22:55:51.288379",
"FPLAN-0073_json_structure_standard_compliance_memor_2026-03-17.md": "2026-03-18T22:55:51.288381",
"FPLAN-0078_unified_plan_pipeline_plugin_architectur_2026-03-17.md": "2026-03-18T22:55:51.288384",
"DPLAN-0010_test_dplan_for_brainstorming_notification_sys_2026-03-17.md": "2026-03-18T22:55:51.288386",
"DPLAN-0028_verify_dplan_2026-03-18.md": "2026-03-18T22:55:51.288389",
"DPLAN-0007_final_verify_dplan_2026-03-17.md": "2026-03-18T22:55:51.288392",
"FPLAN-0128_config_fix_verify_command_for_plan_vectorizat_2026-03-18.md": "2026-03-18T22:55:51.288394",
"FPLAN-0122_pyc_bust_test_2026-03-18.md": "2026-03-18T22:55:51.288396",
"FPLAN-0065_dplan_0056_phase_2_seedgo_json_self_comp_2026-03-17.md": "2026-03-18T22:55:51.288398",
"FPLAN-0036_api_as_centralized_external_service_gate_2026-03-14.md": "2026-03-18T22:55:51.288401",
"FPLAN-0129_vector_verify_test_2026-03-18.md": "2026-03-18T22:55:51.288403",
"FPLAN-0060_prax_diagnostics_type_errors_cleanup_2026-03-17.md": "2026-03-18T22:55:51.288405",
"FPLAN-0126_foreground_archive_test_2026-03-18.md": "2026-03-18T22:55:51.288407",
"FPLAN-0124_dispatch_ux_redesign_merge_sendwake_into_sing_2026-03-18.md": "2026-03-18T22:55:51.288409",
"FPLAN-0082_close_pipeline_test_2026-03-17.md": "2026-03-18T22:55:51.288412",
"FPLAN-0069_spawn_seedgo_100_compliance_introspectio_2026-03-17.md": "2026-03-18T22:55:51.288414",
"DPLAN-0008_drone_git_module_safe_pr_workflow_with_lockfi_2026-03-17.md": "2026-03-18T22:55:51.288416",
"TPLAN-0001_test_tplan_via_registry_2026-03-18.md": "2026-03-18T22:55:51.288418",
"FPLAN-0064_daemon_branch_health_cleanup_2026-03-17.md": "2026-03-18T22:55:51.288420",
"DPLAN-0025_prax_monitor_restoration_rich_output_caller_a_2026-03-18.md": "2026-03-18T22:55:51.288423",
"FPLAN-0028_registry_isolation_phase_1_2026-03-13.md": "2026-03-18T22:55:51.288425",
"FPLAN-0026_memory_bank_full_operational_baseline_2026-03-12.md": "2026-03-18T22:55:51.288427",
"FPLAN-0008_spawn_rebuild_full_branch_lifecycle_mana_2026-03-07.md": "2026-03-18T22:55:51.288430",
"DPLAN-0027_registry_integration_test_dplan_2026-03-18.md": "2026-03-18T22:55:51.288432",
"FPLAN-0066_api_seedgo_compliance_cleanup_2026-03-17.md": "2026-03-18T22:55:51.288434",
"FPLAN-0074_commons_full_compliance_system_verificat_2026-03-17.md": "2026-03-18T22:55:51.288437",
"FPLAN-0051_memory_templates_modernization_spawn_han_2026-03-15.md": "2026-03-18T22:55:51.288440",
"FPLAN-0067_cli_seedgo_compliance_json_structure_wir_2026-03-17.md": "2026-03-18T22:55:51.288442",
"FPLAN-0063_seedgo_100_compliance_backup_cleanup_2026-03-17.md": "2026-03-18T22:55:51.288445",
"FPLAN-0056_three_json_pattern_redesign_2026-03-16.md": "2026-03-18T22:55:51.288447",
"FPLAN-0021_wire_dplans_into_flow_cli_router_and_cle_2026-03-10.md": "2026-03-18T22:55:51.288449",
"FPLAN-0116_heal_test_trigger_2026-03-18.md": "2026-03-18T22:55:51.288451",
"FPLAN-0089_shakedown_fplan_building_a_user_dashboard_2026-03-17.md": "2026-03-18T22:55:51.288453",
"FPLAN-0098_command_routing_fix_module_rename_2026-03-17.md": "2026-03-18T22:55:51.288456",
"FPLAN-0123_final_error_test_2026-03-18.md": "2026-03-18T22:55:51.288458",
"FPLAN-0045_memory_cli_introspection_overhaul_2026-03-15.md": "2026-03-18T22:55:51.288460",
"FPLAN-0106_monitor_phase_1_visible_degradation_polling_f_2026-03-18.md": "2026-03-18T22:55:51.288462",
"FPLAN-0020_seedgo_standard_handler_activation_2026-03-09.md": "2026-03-18T22:55:51.288464",
"FPLAN-0019_seedgo_standards_query_module_2026-03-09.md": "2026-03-18T22:55:51.288467",
"FPLAN-0094_drone_git_module_phase_1_core_build_2026-03-17.md": "2026-03-18T22:55:51.288469",
"FPLAN-0061_prax_architecture_and_readme_compliance_2026-03-17.md": "2026-03-18T22:55:51.288471",
"TPLAN-0001_test_tplan_auto_discovery_2026-03-18.md": "2026-03-18T22:55:51.288473",
"FPLAN-0092_phase_3_command_activation_management_cli_2026-03-17.md": "2026-03-18T22:55:51.288475",
"DPLAN-0014_shakedown_dplan_in_drone_2026-03-17.md": "2026-03-18T22:55:51.288477",
"FPLAN-0071_trigger_json_structure_compliance_2026-03-17.md": "2026-03-18T22:55:51.288479",
"FPLAN-0070_skills_branch_seedgo_100_compliance_2026-03-17.md": "2026-03-18T22:55:51.288481",
"DPLAN-0022_auto_discovery_template_system_remove_plan_ty_2026-03-18.md": "2026-03-18T22:55:51.288484",
"FPLAN-0042_drone_test_suite_multi_round_audit_proce_2026-03-14.md": "2026-03-18T22:55:51.288486",
"FPLAN-0038_dashboard_and_status_pipeline_full_lifec_2026-03-14.md": "2026-03-18T22:55:51.288488",
"DPLAN-0013_shakedown_dplan_notification_system_design_2026-03-17.md": "2026-03-18T22:55:51.288490",
"FPLAN-0018_seedgo_one_path_audit_architecture_2026-03-09.md": "2026-03-18T22:55:51.288492",
"TPLAN-0002_test_tplan_2026-03-18.md": "2026-03-18T22:55:51.288495",
"FPLAN-0084_phase_1_command_registry_infrastructure_2026-03-17.md": "2026-03-18T22:55:51.288497",
"FPLAN-0029_aipass_as_operating_system_2026-03-13.md": "2026-03-18T22:55:51.288499",
"FPLAN-0039_dashboard_pipeline_full_lifecycle_2026-03-14.md": "2026-03-18T22:55:51.288501",
"DPLAN-0026_template_registry_system_drone_command_regist_2026-03-18.md": "2026-03-18T22:55:51.288504",
"FPLAN-0050_subagent_auto_fix_hook_hook_cleanup_2026-03-15.md": "2026-03-18T22:55:51.288506",
"FPLAN-0127_full_output_test_2026-03-18.md": "2026-03-18T22:55:51.288508",
"FPLAN-0081_json_handler_foundation_replace_with_standard_2026-03-17.md": "2026-03-18T22:55:51.288510",
"FPLAN-0104_test_auto_discovery_fplan_2026-03-18.md": "2026-03-18T22:55:51.288512",
"FPLAN-0119_error_visibility_test_2026-03-18.md": "2026-03-18T22:55:51.288514",
"FPLAN-0048_dplan_0046_execution_seedgo_reach_standa_2026-03-15.md": "2026-03-18T22:55:51.288516",
"FPLAN-0085_test_default_fplan_with_dummy_content_about_b_2026-03-17.md": "2026-03-18T22:55:51.288519",
"FPLAN-0077_daemon_json_structure_standard_complianc_2026-03-17.md": "2026-03-18T22:55:51.288521",
"FPLAN-0096_seedgo_compliance_flow_to_100_2026-03-17.md": "2026-03-18T22:55:51.288523",
"FPLAN-0068_json_structure_compliance_prax_1_to_100_2026-03-17.md": "2026-03-18T22:55:51.288525",
"FPLAN-0107_template_registry_system_registry_ops_handler_2026-03-18.md": "2026-03-18T22:55:51.288527",
"FPLAN-0057_simplify_logging_remove_hierarchical_ret_2026-03-16.md": "2026-03-18T22:55:51.288529",
"FPLAN-0076_commons_seedgo_compliance_quick_wins_enc_2026-03-17.md": "2026-03-18T22:55:51.288531",
"FPLAN-0027_aipass_as_operating_system_2026-03-13.md": "2026-03-18T22:55:51.288535",
"FPLAN-0083_activated_commands_scan_restore_human_cli_2026-03-17.md": "2026-03-18T22:55:51.288537",
"DPLAN-0021_test_template_wiring_2026-03-18.md": "2026-03-18T22:55:51.288539",
"FPLAN-0037_backup_split_config_handler_ignore_patte_2026-03-14.md": "2026-03-18T22:55:51.288542",
"DPLAN-0016_seedgo_compliance_flow_branch_to_100_2026-03-17.md": "2026-03-18T22:55:51.288544",
"FPLAN-0046_seedgo_reach_compliance_culture_system_w_2026-03-15.md": "2026-03-18T22:55:51.288546",
"DPLAN-0005_dplan_close_test_2026-03-17.md": "2026-03-18T22:55:51.288548",
"FPLAN-0115_verify_registry_bug_2026-03-18.md": "2026-03-18T22:55:51.288550",
"FPLAN-0109_monitor_phase_2_caller_attribution_via_drone__2026-03-18.md": "2026-03-18T22:55:51.288552",
"FPLAN-0101_test_fplan_default_2026-03-18.md": "2026-03-18T22:55:51.288555",
"FPLAN-0062_wire_post_rollover_chain_in_orchestrator_2026-03-17.md": "2026-03-18T22:55:51.288557",
"DPLAN-0006_activated_commands_scan_restore_human_cli_2026-03-17.md": "2026-03-18T22:55:51.288559",
"FPLAN-0005_spawn_branch_scaffold_audit_2026-03-07.md": "2026-03-18T22:55:51.288561",
"FPLAN-0030_registry_uuid_migration_add_metadataid_t_2026-03-13.md": "2026-03-18T22:55:51.288563",
"FPLAN-0121_loud_error_test_2026-03-18.md": "2026-03-18T22:55:51.288565",
"FPLAN-0117_vector_intake_test_2026-03-18.md": "2026-03-18T22:55:51.288567",
"FPLAN-0017_implement_two_level_introspection_standa_2026-03-08.md": "2026-03-18T22:55:51.288570",
"DPLAN-0012_hook_management_seedgo_owns_hooks_cleanup_cro_2026-03-17.md": "2026-03-18T22:55:51.288572",
"FPLAN-0055_memory_subsystem_activation_triggers_log_2026-03-16.md": "2026-03-18T22:55:51.288574",
"DPLAN-0009_flow_system_shakedown_full_integration_test_2026-03-17.md": "2026-03-18T22:55:51.288576",
"FPLAN-0120_cache_cleared_test_2026-03-18.md": "2026-03-18T22:55:51.288578",
"FPLAN-0088_phase_2_module_scanning_2026-03-17.md": "2026-03-18T22:55:51.288580",
"FPLAN-0103_auto_discovery_template_refactor_2026-03-18.md": "2026-03-18T22:55:51.288582",
"FPLAN-0075_auto_fix_execution_restoring_determinist_2026-03-17.md": "2026-03-18T22:55:51.288584",
"FPLAN-0043_whitelist_file_size_cap_for_backup_scann_2026-03-14.md": "2026-03-18T22:55:51.288587",
"DPLAN-0023_test_auto_discovery_dplan_2026-03-18.md": "2026-03-18T22:55:51.288589",
"FPLAN-0044_aipass_front_door_user_discovery_entry_p_2026-03-15.md": "2026-03-18T22:55:51.288591",
"DPLAN-0024_design_investigation_2026-03-18.md": "2026-03-18T22:55:51.288593",
"FPLAN-0114_auto_heal_test_2026-03-18.md": "2026-03-18T22:55:51.288595",
"FPLAN-0108_registry_integration_test_fplan_2026-03-18.md": "2026-03-18T22:55:51.288597",
"FPLAN-0035_backup_operational_snapshot_versioned_2026-03-14.md": "2026-03-18T22:55:51.288600",
"FPLAN-0091_shakedown_fplan_in_drone_2026-03-17.md": "2026-03-18T22:55:51.288602",
"FPLAN-0009_citizen_classes_template_system_passport_2026-03-07.md": "2026-03-18T22:55:51.288604",
"FPLAN-0053_hook_migration_project_level_settings_2026-03-16.md": "2026-03-18T22:55:51.288606",
"FPLAN-0125_flag_trace_test_2026-03-18.md": "2026-03-18T22:55:51.288608",
"FPLAN-0072_unified_plan_pipeline_fplandplan_parity__2026-03-17.md": "2026-03-18T22:55:51.288610",
"FPLAN-0052_templates_module_modernization_spawn_han_2026-03-15.md": "2026-03-18T22:55:51.288612",
"FPLAN-0131_final_e2e_test_2026-03-18.md": "2026-03-18T23:28:27.324501"
}
@@ -16,7 +16,7 @@
},
"plans": {
"enabled": true,
"path": "src/aipass/flow/processed_plans",
"path": "src/aipass/backup/processed_plans",
"supported_extensions": [".md"],
"collection_name": "flow_plans"
}