From 260e7e2fae601295531799b665e84cf090012429 Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Thu, 9 Apr 2026 21:03:37 -0700 Subject: [PATCH] =?UTF-8?q?feat(system):=20feat(system):=20S82:=20core=201?= =?UTF-8?q?0=20cleanup=20=E2=80=94=20AGENTS/GEMINI=20prompts,=20STATUS.loc?= =?UTF-8?q?al,=20sandbox=20+=20vision=20docs,=20branch=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: @devpulse --- .aipass/aipass_global_prompt.md | 2 +- AGENTS.md | 2 +- GEMINI.md | 2 +- .../ai_mail/.aipass/aipass_local_prompt.md | 5 - src/aipass/ai_mail/.seedgo/bypass.json | 20 -- src/aipass/ai_mail/README.md | 8 +- src/aipass/ai_mail/apps/ai_mail.py | 2 - .../apps/handlers/monitoring/__init__.py | 10 - .../apps/handlers/monitoring/memory.py | 141 --------- .../apps/handlers/registry/__init__.py | 4 - .../ai_mail/apps/handlers/registry/load.py | 56 ---- .../ai_mail/apps/handlers/registry/update.py | 272 ------------------ .../ai_mail/apps/modules/branch_ping.py | 232 --------------- src/aipass/ai_mail/tests/test_branch_ping.py | 263 ----------------- .../ai_mail/tests/test_monitoring_memory.py | 196 ------------- .../devpulse/.aipass/aipass_local_prompt.md | 4 +- src/aipass/drone/README.md | 1 - src/aipass/drone/apps/drone.py | 4 +- src/aipass/flow/README.md | 4 +- .../flow/apps/handlers/mbank/process.py | 2 +- .../flow/apps/handlers/plan/restore_ops.py | 11 +- .../apps/handlers/template/registry_ops.py | 53 ++++ .../memory/apps/handlers/monitor/detector.py | 5 +- .../apps/handlers/monitor/memory_watcher.py | 2 +- .../apps/handlers/rollover/extractor.py | 12 +- .../memory/apps/handlers/templates/differ.py | 2 +- .../memory/apps/handlers/templates/pusher.py | 2 +- src/aipass/seedgo/apps/seedgo.py | 12 +- src/aipass/spawn/apps/modules/delete.py | 9 + src/aipass/spawn/apps/modules/update.py | 11 + 30 files changed, 123 insertions(+), 1226 deletions(-) delete mode 100644 src/aipass/ai_mail/apps/handlers/monitoring/__init__.py delete mode 100644 src/aipass/ai_mail/apps/handlers/monitoring/memory.py delete mode 100644 src/aipass/ai_mail/apps/handlers/registry/load.py delete mode 100644 src/aipass/ai_mail/apps/handlers/registry/update.py delete mode 100644 src/aipass/ai_mail/apps/modules/branch_ping.py delete mode 100644 src/aipass/ai_mail/tests/test_branch_ping.py delete mode 100644 src/aipass/ai_mail/tests/test_monitoring_memory.py diff --git a/.aipass/aipass_global_prompt.md b/.aipass/aipass_global_prompt.md index 9e6a4bd6..ba753e09 100644 --- a/.aipass/aipass_global_prompt.md +++ b/.aipass/aipass_global_prompt.md @@ -39,7 +39,7 @@ src/aipass/{name}/ ~/.secrets/aipass/ # API keys, tokens, credentials (outside repo, cross-platform) ``` -**15 branches:** drone, seedgo, prax, cli, flow, ai_mail, api, trigger, spawn, devpulse, backup, daemon, memory, commons, skills +**10 core branches:** drone, seedgo, prax, cli, flow, ai_mail, trigger, spawn, memory, devpulse ## Commands diff --git a/AGENTS.md b/AGENTS.md index d174a40b..6a882c49 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ src/aipass/{name}/ └── README.md ``` -15 branches: drone, seedgo, prax, cli, flow, ai_mail, api, trigger, spawn, devpulse, backup, daemon, memory, commons, skills +10 core branches: drone, seedgo, prax, cli, flow, ai_mail, trigger, spawn, memory, devpulse ## Commands diff --git a/GEMINI.md b/GEMINI.md index d174a40b..6a882c49 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -18,7 +18,7 @@ src/aipass/{name}/ └── README.md ``` -15 branches: drone, seedgo, prax, cli, flow, ai_mail, api, trigger, spawn, devpulse, backup, daemon, memory, commons, skills +10 core branches: drone, seedgo, prax, cli, flow, ai_mail, trigger, spawn, memory, devpulse ## Commands diff --git a/src/aipass/ai_mail/.aipass/aipass_local_prompt.md b/src/aipass/ai_mail/.aipass/aipass_local_prompt.md index 52df0206..2c284ea6 100644 --- a/src/aipass/ai_mail/.aipass/aipass_local_prompt.md +++ b/src/aipass/ai_mail/.aipass/aipass_local_prompt.md @@ -28,7 +28,6 @@ apps/ modules/ email.py # Orchestrates send/inbox/view/close/reply/sent/contacts dispatch.py # Orchestrates dispatch (send + wake combined) - branch_ping.py # Memory health monitoring handlers/ paths.py # Shared find_repo_root() utility central_writer.py # Central inbox stats aggregation @@ -57,13 +56,9 @@ apps/ dashboard_sync.py # Dashboard integration registry/ read.py # Registry reading + get_all_branches() - load.py # Registry file loading - update.py # Registry health updates users/ user.py # Current user detection (get_current_user) branch_detection.py # CWD/env-based branch identity detection - monitoring/ - memory.py # Memory file health checks json_utils/ json_handler.py # Auto-creating JSON system ``` diff --git a/src/aipass/ai_mail/.seedgo/bypass.json b/src/aipass/ai_mail/.seedgo/bypass.json index 6a93401b..24a3fcf9 100644 --- a/src/aipass/ai_mail/.seedgo/bypass.json +++ b/src/aipass/ai_mail/.seedgo/bypass.json @@ -20,11 +20,6 @@ "standard": "deep_nesting", "reason": "handle_command() depth 4 — entry point router, nested subcommand dispatch is inherent to the pattern" }, - { - "file": "apps/modules/branch_ping.py", - "standard": "deep_nesting", - "reason": "handle_command() depth 4 — command routing with multiple subcommands, same dispatch pattern" - }, { "file": "apps/handlers/dispatch/daemon.py", "standard": "deep_nesting", @@ -190,11 +185,6 @@ "standard": "documentation", "reason": "spawn_agent() has a full docstring — seedgo AST detection mismatch on multiline function signature." }, - { - "file": "apps/handlers/registry/update.py", - "standard": "documentation", - "reason": "ping_registry() has a full docstring — seedgo AST detection mismatch on multiline function signature." - }, { "file": "apps/modules/email.py", "standard": "deep_nesting", @@ -215,16 +205,6 @@ "standard": "deep_nesting", "reason": "calculate_age() depth 4 — timestamp parsing with multiple fallback formats" }, - { - "file": "apps/handlers/registry/update.py", - "standard": "deep_nesting", - "reason": "_calculate_statistics() depth 5 — JSON traversal for registry stats with nested key checking" - }, - { - "file": "apps/handlers/monitoring/errors.py", - "standard": "handlers", - "reason": "Archived — file moved to .archive/errors(disabled).py. No longer in active codebase." - }, { "file": "apps/handlers/central_writer.py", "standard": "handlers", diff --git a/src/aipass/ai_mail/README.md b/src/aipass/ai_mail/README.md index 1195381d..25e61063 100644 --- a/src/aipass/ai_mail/README.md +++ b/src/aipass/ai_mail/README.md @@ -9,7 +9,7 @@ --- -**Status:** Operational. Core email workflow (send/inbox/reply/close), dispatch system (with startup timeout + auto-retry), daemon, desktop notifications all working. Seedgo 99%. 322 tests. +**Status:** Operational. Core email workflow (send/inbox/reply/close), dispatch system (with startup timeout + auto-retry), daemon, desktop notifications all working. Seedgo 99%. ## Commands / Usage @@ -44,8 +44,6 @@ The `dispatch` command sends an email and wakes the target branch in one step. A - Startup health check: monitors JSONL session files for 90s, kills if no activity - Auto-retry: 3 strikes (resume, resume, fresh) before bounce - Failed agents trigger bounce emails back to sender -- `ping` commands exit 0 but produce no visible output in drone context *(partial)* - ## Architecture Follows the standard AIPass 3-layer pattern: @@ -57,14 +55,12 @@ ai_mail/ │ ├── modules/ │ │ ├── email.py # Send, inbox, view, reply, close, contacts │ │ ├── dispatch.py # Dispatch status, daemon, wake -│ │ └── branch_ping.py # Branch health monitoring │ └── handlers/ │ ├── email/ # Delivery, formatting, inbox ops, purge, reply │ ├── dispatch/ # Daemon, wake, dispatch_monitor, status -│ ├── registry/ # Branch registry read/update/load +│ ├── registry/ # Branch registry read │ ├── users/ # Branch detection, user lookup │ ├── json_utils/ # JSON I/O helpers (load_json, save_json) -│ ├── monitoring/ # Memory health, error tracking │ ├── paths.py # Shared find_repo_root() utility │ ├── notify.py # Desktop notifications (dbus) │ └── central_writer.py # Central inbox stats aggregation diff --git a/src/aipass/ai_mail/apps/ai_mail.py b/src/aipass/ai_mail/apps/ai_mail.py index 289c083b..8c489a22 100644 --- a/src/aipass/ai_mail/apps/ai_mail.py +++ b/src/aipass/ai_mail/apps/ai_mail.py @@ -65,8 +65,6 @@ COMMANDS: close - Close email(s) without reply (archives) sent - View sent messages contacts - Manage contacts - ping - Memory health check - EMAIL LIFECYCLE (v2): new → opened → closed - new: Just arrived, never viewed diff --git a/src/aipass/ai_mail/apps/handlers/monitoring/__init__.py b/src/aipass/ai_mail/apps/handlers/monitoring/__init__.py deleted file mode 100644 index 632e80d5..00000000 --- a/src/aipass/ai_mail/apps/handlers/monitoring/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Monitoring Handlers - AI Mail Monitoring Domain - -Independent handlers for monitoring operations: -- memory.py: Memory health checking and line counting -- errors.py: Error log parsing and deduplication -- status.py: Status determination and reporting - -All handlers are independent (no cross-domain imports). -""" diff --git a/src/aipass/ai_mail/apps/handlers/monitoring/memory.py b/src/aipass/ai_mail/apps/handlers/monitoring/memory.py deleted file mode 100644 index 8288beb6..00000000 --- a/src/aipass/ai_mail/apps/handlers/monitoring/memory.py +++ /dev/null @@ -1,141 +0,0 @@ -# =================== AIPass ==================== -# Name: memory.py -# Description: Memory Health Handler -# Version: 1.0.0 -# Created: 2025-11-15 -# Modified: 2025-11-15 -# ============================================= - -""" -Memory Health Handler - -Independent handler for memory file monitoring logic. -Provides functions for counting file lines and determining health status. - -Architecture: -- No cross-domain imports (independent handler) -- Provides: line counting, status determination -- Used by: monitoring modules -""" - -# ============================================= -# IMPORTS -# ============================================= -from pathlib import Path - -from aipass.prax.apps.modules.logger import system_logger as logger -from aipass.ai_mail.apps.handlers.json import json_handler - - -# ============================================= -# CONSTANTS -# ============================================= - -# Health status thresholds -THRESHOLD_GREEN_MAX = 400 -THRESHOLD_YELLOW_MIN = 401 -THRESHOLD_YELLOW_MAX = 550 -THRESHOLD_RED_MIN = 551 -THRESHOLD_EMAIL_TRIGGER = 600 - -# Status indicators -STATUS_GREEN = "🟢 Healthy" -STATUS_YELLOW = "🟡 Approaching" -STATUS_RED = "🔴 Compress Now" - -# ============================================= -# CORE FUNCTIONS -# ============================================= - -def count_file_lines(file_path: Path | str) -> int: - """ - Count lines in a file - - Args: - file_path: Path to file to count - - Returns: - Number of lines in file, or 0 if file doesn't exist or error - """ - file_path = Path(file_path) - - if not file_path.exists(): - return 0 - - try: - with open(file_path, 'r', encoding='utf-8') as f: - return len(f.readlines()) - except Exception as e: - logger.warning("[memory] Failed to count lines in %s: %s", file_path, e) - return 0 - - -def get_status_from_count(line_count: int) -> str: - """ - Determine health status from line count - - Args: - line_count: Number of lines in file - - Returns: - Status string with emoji indicator - """ - if line_count <= THRESHOLD_GREEN_MAX: - return STATUS_GREEN - elif THRESHOLD_YELLOW_MIN <= line_count <= THRESHOLD_YELLOW_MAX: - return STATUS_YELLOW - else: - return STATUS_RED - - -def should_send_email(line_count: int) -> bool: - """ - Determine if email notification should be sent - - Args: - line_count: Number of lines in file - - Returns: - True if line count exceeds email trigger threshold - """ - return line_count >= THRESHOLD_EMAIL_TRIGGER - - -def get_health_info(file_path: Path | str) -> dict: - """ - Get complete health information for a file - - Args: - file_path: Path to file to analyze - - Returns: - Dict with line_count, status, needs_email - """ - json_handler.log_operation("get_health_info", {"file_path": str(file_path)}) - - line_count = count_file_lines(file_path) - - return { - "line_count": line_count, - "status": get_status_from_count(line_count), - "needs_email": should_send_email(line_count), - "file_path": str(file_path) - } - - -if __name__ == "__main__": - from aipass.cli.apps.modules import console - console.print("\n" + "="*70) - console.print("MEMORY HEALTH HANDLER") - console.print("="*70) - console.print("\nFunctions provided:") - console.print(" - count_file_lines(file_path) -> int") - console.print(" - get_status_from_count(line_count) -> str") - console.print(" - should_send_email(line_count) -> bool") - console.print(" - get_health_info(file_path) -> dict") - console.print("\nThresholds:") - console.print(f" Green: 0-{THRESHOLD_GREEN_MAX} lines") - console.print(f" Yellow: {THRESHOLD_YELLOW_MIN}-{THRESHOLD_YELLOW_MAX} lines") - console.print(f" Red: {THRESHOLD_RED_MIN}+ lines") - console.print(f" Email trigger: {THRESHOLD_EMAIL_TRIGGER}+ lines") - console.print("\n" + "="*70 + "\n") diff --git a/src/aipass/ai_mail/apps/handlers/registry/__init__.py b/src/aipass/ai_mail/apps/handlers/registry/__init__.py index c983e265..d101e390 100644 --- a/src/aipass/ai_mail/apps/handlers/registry/__init__.py +++ b/src/aipass/ai_mail/apps/handlers/registry/__init__.py @@ -1,5 +1 @@ """Registry Handlers - Branch registry operations for AI_Mail""" - -from .load import load_registry - -__all__ = ['load_registry'] diff --git a/src/aipass/ai_mail/apps/handlers/registry/load.py b/src/aipass/ai_mail/apps/handlers/registry/load.py deleted file mode 100644 index 6d13455b..00000000 --- a/src/aipass/ai_mail/apps/handlers/registry/load.py +++ /dev/null @@ -1,56 +0,0 @@ -# =================== AIPass ==================== -# Name: load.py -# Description: Registry Loading Handler -# Version: 1.0.0 -# Created: 2025-11-15 -# Modified: 2025-11-15 -# ============================================= - -""" -Registry Loading Handler - Loads registry files from disk. -""" - -import json -from pathlib import Path -from typing import Dict - -from aipass.ai_mail.apps.handlers.json import json_handler - - -def load_registry(registry_file: Path) -> Dict: - """ - Load branch registry from file. - - Args: - registry_file: Path to the registry JSON file - - Returns: - Registry dictionary with structure: - { - "last_updated": str, - "active_branches": dict, - "statistics": { - "total_branches": int, - "green_status": int, - "yellow_status": int, - "red_status": int - } - } - """ - json_handler.log_operation("load_registry", {"registry_file": str(registry_file)}) - - if not registry_file.exists(): - return { - "last_updated": "", - "active_branches": {}, - "statistics": { - "total_branches": 0, - "green_status": 0, - "yellow_status": 0, - "red_status": 0 - } - } - - # Direct file read for registry (non-standard JSON location) - with open(registry_file, 'r', encoding='utf-8') as f: - return json.load(f) diff --git a/src/aipass/ai_mail/apps/handlers/registry/update.py b/src/aipass/ai_mail/apps/handlers/registry/update.py deleted file mode 100644 index 1b6ea00a..00000000 --- a/src/aipass/ai_mail/apps/handlers/registry/update.py +++ /dev/null @@ -1,272 +0,0 @@ -# =================== AIPass ==================== -# Name: update.py -# Description: Registry Update Handler -# Version: 1.0.0 -# Created: 2025-11-15 -# Modified: 2025-11-15 -# ============================================= - -""" -Registry Update Handler - -Handles updating branch registry with ping data including: -- Updating registry with branch status -- Recording ping timestamps -- Maintaining statistics (green/yellow/red counts) - -Handler Independence: -- No module imports from ai_mail -- Only uses Prax logger and standard library -- Fully transportable and self-contained -""" - -import json -from pathlib import Path -from datetime import datetime -from typing import Dict, Tuple - -from aipass.prax.apps.modules.logger import system_logger as logger -from aipass.ai_mail.apps.handlers.json import json_handler - - -# Constants -MODULE_NAME = "registry.update" -_AI_MAIL_ROOT = Path(__file__).resolve().parents[3] # ai_mail/ -AI_MAIL_JSON = _AI_MAIL_ROOT / ".ai_mail.local" -REGISTRY_PATH = AI_MAIL_JSON / "local_memory_monitor_registry.json" -THRESHOLDS = { - "green": (0, 400), - "yellow": (401, 550), - "red": (551, float('inf')) -} - - -def ping_registry( - branch_name: str, - branch_path: Path, - local_status: Dict, - obs_status: Dict -) -> bool: - """ - Update registry with branch status. - - Args: - branch_name: Name of branch (e.g., "FLOW", "AIPASS.admin") - branch_path: Full path to branch directory - local_status: Dict with {"line_count": int, "status": str} - obs_status: Dict with {"line_count": int, "status": str} - - Returns: - True if registry updated successfully, False otherwise - """ - json_handler.log_operation("ping_registry", {"branch_name": branch_name, "branch_path": str(branch_path)}) - - try: - # Ensure registry directory exists - REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True) - - # Load or create registry - if REGISTRY_PATH.exists(): - with open(REGISTRY_PATH, 'r', encoding='utf-8') as f: - registry = json.load(f) - else: - registry = _create_empty_registry() - - # Update branch entry - registry["active_branches"][str(branch_path)] = { - "branch_name": branch_name, - "last_ping": datetime.now().isoformat(), - "local_md": local_status, - "observations_md": obs_status - } - - # Update statistics - registry["last_updated"] = datetime.now().isoformat() - registry["statistics"] = _calculate_statistics(registry) - - # Save registry - with open(REGISTRY_PATH, 'w', encoding='utf-8') as f: - json.dump(registry, f, indent=2) - - return True - - except Exception as e: - logger.warning("[registry] ping_registry failed for %s: %s", branch_name, e) - return False - - -def _create_empty_registry() -> Dict: - """ - Create empty registry structure. - - Returns: - Empty registry dict - """ - return { - "last_updated": "", - "active_branches": {}, - "statistics": { - "total_branches": 0, - "green_status": 0, - "yellow_status": 0, - "red_status": 0 - } - } - - -def _calculate_statistics(registry: Dict) -> Dict: - """ - Calculate statistics from registry data. - - Args: - registry: Full registry dict - - Returns: - Statistics dict with counts - """ - green, yellow, red = 0, 0, 0 - - for branch_data in registry["active_branches"].values(): - for file_type in ["local_md", "observations_md"]: - status = branch_data.get(file_type, {}).get("status", "") - if status == "green": - green += 1 - elif status == "yellow": - yellow += 1 - elif status == "red": - red += 1 - - return { - "total_branches": len(registry["active_branches"]), - "green_status": green, - "yellow_status": yellow, - "red_status": red - } - - -def get_status_from_count(line_count: int) -> str: - """ - Determine status based on line count. - - Args: - line_count: Number of lines in file - - Returns: - Status code: "green", "yellow", or "red" - """ - if THRESHOLDS["green"][0] <= line_count <= THRESHOLDS["green"][1]: - return "green" - elif THRESHOLDS["yellow"][0] <= line_count <= THRESHOLDS["yellow"][1]: - return "yellow" - else: # red threshold - return "red" - - -def count_file_lines(file_path: Path) -> int: - """ - Count total lines in file. - - Args: - file_path: Path to file to count - - Returns: - Number of lines in file, 0 if file doesn't exist - """ - if not file_path.exists(): - return 0 - - try: - with open(file_path, 'r', encoding='utf-8') as f: - return len(f.readlines()) - except Exception as e: - logger.warning("[registry] count_file_lines failed for %s: %s", file_path, e) - return 0 - - -def update_json_memory_health( - file_path: Path, - line_count: int, - status_code: str -) -> bool: - """ - Update memory_health in JSON file metadata. - - Args: - file_path: Path to JSON file - line_count: Current line count - status_code: Status ("green", "yellow", "red") - - Returns: - True if updated successfully, False otherwise - """ - if not file_path.exists(): - return False - - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - - # Update memory health in metadata - if "metadata" in data and "memory_health" in data["metadata"]: - data["metadata"]["memory_health"]["current_lines"] = line_count - data["metadata"]["memory_health"]["status"] = status_code - - # Save updated file - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=2, ensure_ascii=False) - - return True - else: - return False - - except Exception as e: - logger.warning("[registry] update_json_memory_health failed for %s: %s", file_path, e) - return False - - -def get_branch_context() -> Tuple[str, Path]: - """ - Determine current branch name and directory. - - Returns: - Tuple of (branch_name, branch_path) - """ - cwd = Path.cwd() - - # Special case: root directory - if cwd == Path("/"): - return "AIPASS.admin", cwd - - # Extract branch name from last directory in path - branch_folder = cwd.name.replace("-", "_") - branch_name = branch_folder.upper() - - return branch_name, cwd - - -if __name__ == "__main__": - from aipass.cli.apps.modules import console - console.print("\n" + "="*70) - console.print("AI_MAIL HANDLER: registry/update.py") - console.print("="*70) - console.print("\nRegistry Update Handler") - console.print() - console.print("FUNCTIONS PROVIDED:") - console.print(" - ping_registry(branch_name, branch_path, local_status, obs_status) -> bool") - console.print(" - get_status_from_count(line_count) -> str") - console.print(" - count_file_lines(file_path) -> int") - console.print(" - update_json_memory_health(file_path, line_count, status_code) -> bool") - console.print(" - get_branch_context() -> Tuple[str, Path]") - console.print() - console.print("THRESHOLDS:") - console.print(f" Green: {THRESHOLDS['green'][0]} - {THRESHOLDS['green'][1]} lines") - console.print(f" Yellow: {THRESHOLDS['yellow'][0]} - {THRESHOLDS['yellow'][1]} lines") - console.print(f" Red: {THRESHOLDS['red'][0]}+ lines") - console.print() - console.print("TESTING:") - - branch_name, branch_path = get_branch_context() - console.print(f"\nCurrent branch: {branch_name}") - console.print(f"Current path: {branch_path}") - - console.print("\n" + "="*70 + "\n") diff --git a/src/aipass/ai_mail/apps/modules/branch_ping.py b/src/aipass/ai_mail/apps/modules/branch_ping.py deleted file mode 100644 index cb829608..00000000 --- a/src/aipass/ai_mail/apps/modules/branch_ping.py +++ /dev/null @@ -1,232 +0,0 @@ -# =================== AIPass ==================== -# Name: branch_ping.py -# Description: Branch Ping Orchestration Module -# Version: 1.0.0 -# Created: 2025-11-15 -# Modified: 2025-11-15 -# ============================================= - -""" -Branch Ping Orchestration Module - -Orchestrates branch memory health monitoring - delegates all logic to handlers. -Commands: ping, status, registry, thresholds -""" - -import sys -import argparse -from pathlib import Path -from typing import List - -from aipass.prax.apps.modules.logger import system_logger as logger - -# CLI services for formatting -from aipass.cli.apps.modules import console, error -from rich.panel import Panel - -# Import handlers -from aipass.ai_mail.apps.handlers.monitoring.memory import count_file_lines, get_status_from_count, get_health_info -from aipass.ai_mail.apps.handlers.registry.update import ( - ping_registry, - get_branch_context, - update_json_memory_health -) -from aipass.ai_mail.apps.handlers.json import json_handler - -MODULE_NAME = "branch_ping" -THRESHOLDS = {"green": (0, 400), "yellow": (401, 550), "red": (551, float('inf'))} - -def handle_ping(verbose: bool = False) -> bool: - """Execute ping command - orchestrate health check""" - try: - branch_name, cwd = get_branch_context() - local_file = cwd / ".trinity" / "local.json" - obs_file = cwd / ".trinity" / "observations.json" - - local_count = count_file_lines(local_file) - obs_count = count_file_lines(obs_file) - local_status_code = get_status_from_count(local_count) - obs_status_code = get_status_from_count(obs_count) - - update_json_memory_health(local_file, local_count, local_status_code) - update_json_memory_health(obs_file, obs_count, obs_status_code) - - local_status = {"line_count": local_count, "status": local_status_code} - obs_status = {"line_count": obs_count, "status": obs_status_code} - ping_registry(branch_name, cwd, local_status, obs_status) - - json_handler.log_operation("ping_executed", {"branch": branch_name, "local_count": local_count, "obs_count": obs_count}) - - if verbose: - console.print(f"Ping successful for {branch_name}") - console.print(f" .trinity/local.json: {local_count} lines ({local_status_code})") - console.print(f" .trinity/observations.json: {obs_count} lines ({obs_status_code})") - return True - except Exception as e: - logger.error(f"Ping failed: {e}") - if verbose: - error(f"Ping failed: {e}") - return False - - -def handle_status() -> bool: - """Show current memory health status""" - try: - branch_name, cwd = get_branch_context() - local_file = cwd / ".trinity" / "local.json" - obs_file = cwd / ".trinity" / "observations.json" - - local_health = get_health_info(local_file) - obs_health = get_health_info(obs_file) - - console.print(f"\nBranch: {branch_name}\nDirectory: {cwd}") - console.print(f"\nMemory Health Status:") - console.print(f" .trinity/local.json: {local_health['line_count']} lines ({local_health['status']})") - console.print(f" .trinity/observations.json: {obs_health['line_count']} lines ({obs_health['status']})\n") - return True - except Exception as e: - logger.error(f"Status check failed: {e}") - error(f"Error getting status: {e}") - return False - - -def handle_registry() -> bool: - """View registry contents""" - try: - from aipass.ai_mail.apps.handlers.registry.update import REGISTRY_PATH - from aipass.ai_mail.apps.handlers.registry.load import load_registry - - if not REGISTRY_PATH.exists(): - console.print("Registry not yet created") - return True - - registry = load_registry(REGISTRY_PATH) - - console.print("\nMemory Health Registry") - console.print(f"Last Updated: {registry.get('last_updated', 'N/A')}\n") - stats = registry.get('statistics', {}) - console.print(f"Statistics:") - console.print(f" Total Branches: {stats.get('total_branches', 0)}") - console.print(f" Green: {stats.get('green_status', 0)}, Yellow: {stats.get('yellow_status', 0)}, Red: {stats.get('red_status', 0)}\n") - return True - except Exception as e: - logger.error(f"Registry read failed: {e}") - error(f"Error reading registry: {e}") - return False - - -def handle_thresholds() -> bool: - """Show compression thresholds""" - console.print("\nMemory Compression Thresholds:") - console.print(f" Green: 0 - {THRESHOLDS['green'][1]} lines") - console.print(f" Yellow: {THRESHOLDS['yellow'][0]} - {THRESHOLDS['yellow'][1]} lines") - console.print(f" Red: {THRESHOLDS['red'][0]}+ lines (compression required)") - console.print() - return True - - -def print_help(): - """Print help output using argparse""" - parser = argparse.ArgumentParser( - description='Branch Ping - Memory Health Monitoring Module', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -COMMANDS: - ping - Execute health check and update registry - status - Show current memory status for this branch - registry - View registry contents and statistics - thresholds - Show compression thresholds - -USAGE: - drone @ai_mail branch_ping - drone @ai_mail branch_ping --help - -EXAMPLES: - # Check memory health and update registry - drone @ai_mail branch_ping ping - - # View current status - drone @ai_mail branch_ping status - - # View registry - drone @ai_mail branch_ping registry - - # Show thresholds - drone @ai_mail branch_ping thresholds - """ - ) - console.print(parser.format_help()) - - -def handle_command(command: str, args: List[str]) -> bool: - """Handle incoming command - main orchestration entry point""" - # Check if this module handles this command - if command not in ["ping", "status", "registry", "thresholds"]: - return False - - # Handle help flag - if args and args[0] in ['--help', '-h', 'help']: - print_help() - return True - - if not args: - print_introspection() - return True - - if command == "ping": - return handle_ping("--verbose" in args or "-v" in args) - elif command == "status": - return handle_status() - elif command == "registry": - return handle_registry() - elif command == "thresholds": - return handle_thresholds() - return False - - -def print_introspection(): - """Display module introspection info.""" - console.print() - console.print("branch_ping Module") - console.print("Orchestrates branch memory health monitoring: ping, status, registry, and thresholds.") - console.print() - console.print("Connected Handlers:") - console.print(" handlers/monitoring/") - console.print(" - memory.py (count_file_lines — count lines in a memory file)") - console.print(" - memory.py (get_status_from_count — derive health status from line count)") - console.print(" handlers/registry/") - console.print(" - update.py (ping_registry — update memory health registry for a branch)") - console.print(" - update.py (get_branch_context — resolve current branch name and directory)") - console.print(" - update.py (update_json_memory_health — write health metadata into memory file)") - console.print(" - load.py (load_registry — load the memory health registry file)") - console.print(" handlers/json_utils/") - console.print(" - json_handler.py (log_operation — log structured operation to JSON)") - console.print() - - -if __name__ == "__main__": - # Handle --help flag - if len(sys.argv) > 1 and sys.argv[1] in ['--help', '-h', 'help']: - console.print() - console.print(Panel("[bold cyan]BRANCH PING - Memory Health Monitoring[/bold cyan]", expand=False)) - console.print() - console.print("[yellow]Commands:[/yellow] ping, status, registry, thresholds, --help") - console.print() - console.print("[bold]USAGE:[/bold]") - console.print(" drone @ai_mail branch_ping ") - console.print(" drone @ai_mail branch_ping --help") - console.print() - console.print("[bold]COMMANDS:[/bold]") - console.print(" [cyan]ping[/cyan] - Execute health check") - console.print(" [cyan]status[/cyan] - Show current memory status") - console.print(" [cyan]registry[/cyan] - View registry contents") - console.print(" [cyan]thresholds[/cyan] - Show compression thresholds") - console.print() - sys.exit(0) - - console.print() - console.print(Panel("[bold cyan]BRANCH PING ORCHESTRATION MODULE[/bold cyan]", expand=False)) - console.print() - console.print("[yellow]Commands:[/yellow] ping, status, registry, thresholds") - console.print("[dim]Usage: drone @ai_mail branch_ping [command][/dim]") - console.print() diff --git a/src/aipass/ai_mail/tests/test_branch_ping.py b/src/aipass/ai_mail/tests/test_branch_ping.py deleted file mode 100644 index 9903ee93..00000000 --- a/src/aipass/ai_mail/tests/test_branch_ping.py +++ /dev/null @@ -1,263 +0,0 @@ -# =================== AIPass ==================== -# Name: test_branch_ping.py -# Description: Tests for branch ping orchestration module -# Version: 1.0.0 -# Created: 2026-04-03 -# Modified: 2026-04-03 -# ============================================= - -"""Tests for branch_ping module -- command routing and orchestration.""" - -import pytest -from pathlib import Path -from unittest.mock import MagicMock, patch - -import aipass.ai_mail.apps.modules.branch_ping as mod - - -# --- Fixtures -------------------------------------------------------- - - -@pytest.fixture(autouse=True) -def _suppress_log_operation(monkeypatch): - """Prevent json_handler.log_operation from touching real files.""" - monkeypatch.setattr(mod, "json_handler", MagicMock()) - - -@pytest.fixture(autouse=True) -def _suppress_logger(monkeypatch): - """Suppress logger output during tests.""" - monkeypatch.setattr(mod, "logger", MagicMock()) - - -@pytest.fixture(autouse=True) -def _mock_console(monkeypatch): - """Mock console and error to prevent real rich output.""" - mock_console = MagicMock() - monkeypatch.setattr(mod, "console", mock_console) - monkeypatch.setattr(mod, "error", MagicMock()) - return mock_console - - -# --- handle_command tests --------------------------------------------- - - -def test_handle_command_returns_false_for_unknown_command(): - """Returns False for commands not handled by this module.""" - assert mod.handle_command("unknown_cmd", ["run"]) is False - - -def test_handle_command_returns_false_for_empty_unknown(): - """Returns False for another unrecognized command.""" - assert mod.handle_command("deploy", []) is False - - -def test_handle_command_routes_ping(monkeypatch): - """Routes 'ping' command to handle_ping.""" - mock_ping = MagicMock(return_value=True) - monkeypatch.setattr(mod, "handle_ping", mock_ping) - - result = mod.handle_command("ping", ["run"]) - assert result is True - mock_ping.assert_called_once() - - -def test_handle_command_routes_ping_with_verbose(monkeypatch): - """Routes 'ping' with --verbose flag.""" - mock_ping = MagicMock(return_value=True) - monkeypatch.setattr(mod, "handle_ping", mock_ping) - - mod.handle_command("ping", ["--verbose"]) - mock_ping.assert_called_once_with(True) - - -def test_handle_command_routes_status(monkeypatch): - """Routes 'status' command to handle_status.""" - mock_status = MagicMock(return_value=True) - monkeypatch.setattr(mod, "handle_status", mock_status) - - result = mod.handle_command("status", ["run"]) - assert result is True - mock_status.assert_called_once() - - -def test_handle_command_routes_registry(monkeypatch): - """Routes 'registry' command to handle_registry.""" - mock_registry = MagicMock(return_value=True) - monkeypatch.setattr(mod, "handle_registry", mock_registry) - - result = mod.handle_command("registry", ["run"]) - assert result is True - mock_registry.assert_called_once() - - -def test_handle_command_routes_thresholds(): - """Routes 'thresholds' command to handle_thresholds.""" - result = mod.handle_command("thresholds", ["run"]) - assert result is True - - -def test_handle_command_help_flag(monkeypatch): - """--help flag calls print_help and returns True.""" - mock_help = MagicMock() - monkeypatch.setattr(mod, "print_help", mock_help) - - result = mod.handle_command("ping", ["--help"]) - assert result is True - mock_help.assert_called_once() - - -def test_handle_command_h_flag(monkeypatch): - """-h flag calls print_help and returns True.""" - mock_help = MagicMock() - monkeypatch.setattr(mod, "print_help", mock_help) - - result = mod.handle_command("status", ["-h"]) - assert result is True - mock_help.assert_called_once() - - -def test_handle_command_help_word(monkeypatch): - """'help' as first arg calls print_help and returns True.""" - mock_help = MagicMock() - monkeypatch.setattr(mod, "print_help", mock_help) - - result = mod.handle_command("registry", ["help"]) - assert result is True - mock_help.assert_called_once() - - -def test_handle_command_no_args_calls_introspection(monkeypatch): - """No args calls print_introspection and returns True.""" - mock_introspect = MagicMock() - monkeypatch.setattr(mod, "print_introspection", mock_introspect) - - result = mod.handle_command("ping", []) - assert result is True - mock_introspect.assert_called_once() - - -# --- handle_thresholds tests ------------------------------------------ - - -def test_handle_thresholds_returns_true(): - """handle_thresholds returns True.""" - assert mod.handle_thresholds() is True - - -def test_handle_thresholds_prints_values(_mock_console): - """handle_thresholds prints threshold range values.""" - mod.handle_thresholds() - - printed = " ".join(str(c) for c in _mock_console.print.call_args_list) - assert "400" in printed - assert "401" in printed - assert "550" in printed - assert "551" in printed - - -# --- handle_ping tests ------------------------------------------------ - - -def test_handle_ping_returns_true_on_success(monkeypatch): - """handle_ping returns True when all handlers succeed.""" - monkeypatch.setattr(mod, "get_branch_context", lambda: ("test-branch", Path("/tmp/test"))) - monkeypatch.setattr(mod, "count_file_lines", lambda f: 100) - monkeypatch.setattr(mod, "get_status_from_count", lambda c: "green") - monkeypatch.setattr(mod, "update_json_memory_health", MagicMock()) - monkeypatch.setattr(mod, "ping_registry", MagicMock()) - - assert mod.handle_ping(verbose=False) is True - - -def test_handle_ping_verbose_prints_output(monkeypatch, _mock_console): - """handle_ping with verbose=True prints status info.""" - monkeypatch.setattr(mod, "get_branch_context", lambda: ("my-branch", Path("/tmp/test"))) - monkeypatch.setattr(mod, "count_file_lines", lambda f: 250) - monkeypatch.setattr(mod, "get_status_from_count", lambda c: "yellow") - monkeypatch.setattr(mod, "update_json_memory_health", MagicMock()) - monkeypatch.setattr(mod, "ping_registry", MagicMock()) - - mod.handle_ping(verbose=True) - - printed = " ".join(str(c) for c in _mock_console.print.call_args_list) - assert "my-branch" in printed - assert "250" in printed - assert "yellow" in printed - - -def test_handle_ping_returns_false_on_exception(monkeypatch): - """handle_ping returns False when get_branch_context raises.""" - def raise_error(): - raise RuntimeError("git not found") - - monkeypatch.setattr(mod, "get_branch_context", raise_error) - - assert mod.handle_ping(verbose=False) is False - - -def test_handle_ping_calls_ping_registry(monkeypatch): - """handle_ping calls ping_registry with correct arguments.""" - mock_ping_reg = MagicMock() - monkeypatch.setattr(mod, "get_branch_context", lambda: ("feat-x", Path("/tmp/feat"))) - monkeypatch.setattr(mod, "count_file_lines", lambda f: 50) - monkeypatch.setattr(mod, "get_status_from_count", lambda c: "green") - monkeypatch.setattr(mod, "update_json_memory_health", MagicMock()) - monkeypatch.setattr(mod, "ping_registry", mock_ping_reg) - - mod.handle_ping(verbose=False) - - mock_ping_reg.assert_called_once_with( - "feat-x", - Path("/tmp/feat"), - {"line_count": 50, "status": "green"}, - {"line_count": 50, "status": "green"}, - ) - - -# --- handle_status tests --------------------------------------------- - - -def test_handle_status_returns_true_on_success(monkeypatch): - """handle_status returns True when handlers succeed.""" - monkeypatch.setattr(mod, "get_branch_context", lambda: ("main", Path("/tmp/main"))) - monkeypatch.setattr(mod, "get_health_info", lambda f: {"line_count": 100, "status": "green"}) - - assert mod.handle_status() is True - - -def test_handle_status_returns_false_on_exception(monkeypatch): - """handle_status returns False when an exception is raised.""" - def raise_error(): - raise RuntimeError("file not found") - - monkeypatch.setattr(mod, "get_branch_context", raise_error) - - assert mod.handle_status() is False - - -# --- print_introspection tests ---------------------------------------- - - -def test_print_introspection_prints_module_info(_mock_console): - """print_introspection prints module name and handler info.""" - mod.print_introspection() - - printed = " ".join(str(c) for c in _mock_console.print.call_args_list) - assert "branch_ping" in printed - assert "handlers/monitoring/" in printed - assert "handlers/registry/" in printed - - -# --- print_help tests ------------------------------------------------- - - -def test_print_help_prints_command_list(_mock_console): - """print_help prints available commands.""" - mod.print_help() - - printed = " ".join(str(c) for c in _mock_console.print.call_args_list) - assert "ping" in printed - assert "status" in printed - assert "registry" in printed - assert "thresholds" in printed diff --git a/src/aipass/ai_mail/tests/test_monitoring_memory.py b/src/aipass/ai_mail/tests/test_monitoring_memory.py deleted file mode 100644 index 9fb60ba1..00000000 --- a/src/aipass/ai_mail/tests/test_monitoring_memory.py +++ /dev/null @@ -1,196 +0,0 @@ -# =================== AIPass ==================== -# Name: test_monitoring_memory.py -# Description: Tests for memory health handler -# Version: 1.0.0 -# Created: 2026-03-24 -# Modified: 2026-03-24 -# ============================================= - -"""Tests for memory health handler -- line counting and health status.""" - -import pytest -from pathlib import Path -from unittest.mock import patch - -from aipass.ai_mail.apps.handlers.monitoring.memory import ( - count_file_lines, - get_status_from_count, - should_send_email, - get_health_info, - THRESHOLD_GREEN_MAX, - THRESHOLD_YELLOW_MIN, - THRESHOLD_YELLOW_MAX, - THRESHOLD_EMAIL_TRIGGER, - STATUS_GREEN, - STATUS_YELLOW, - STATUS_RED, -) - - -# ---- Fixtures ------------------------------------------------ - - -@pytest.fixture(autouse=True) -def _silence_json_handler(): - """Prevent log_operation from writing real JSON files during tests.""" - with patch("aipass.ai_mail.apps.handlers.monitoring.memory.json_handler") as mock_jh: - mock_jh.log_operation.return_value = True - yield mock_jh - - -# ---- count_file_lines tests ---------------------------------- - - -def test_count_file_lines_missing(tmp_path): - """Nonexistent file returns 0.""" - result = count_file_lines(tmp_path / "nonexistent_file.txt") - - assert isinstance(result, int) - assert result == 0 - - -def test_count_file_lines_empty(tmp_path): - """Empty file returns 0.""" - empty_file = tmp_path / "empty.txt" - empty_file.write_text("", encoding="utf-8") - - result = count_file_lines(empty_file) - - assert isinstance(result, int) - assert result == 0 - - -def test_count_file_lines_content(tmp_path): - """File with 5 lines returns 5.""" - file_path = tmp_path / "five_lines.txt" - file_path.write_text("line1\nline2\nline3\nline4\nline5\n", encoding="utf-8") - - result = count_file_lines(file_path) - - assert isinstance(result, int) - assert result == 5 - - -# ---- get_status_from_count tests ----------------------------- - - -def test_status_green(): - """100 lines is well within green threshold -- returns STATUS_GREEN.""" - assert 100 <= THRESHOLD_GREEN_MAX, "Precondition: 100 must be in green range" - - result = get_status_from_count(100) - - assert isinstance(result, str) - assert result == STATUS_GREEN - - -def test_status_yellow(): - """450 lines falls in yellow range -- returns STATUS_YELLOW.""" - assert THRESHOLD_YELLOW_MIN <= 450 <= THRESHOLD_YELLOW_MAX, "Precondition: 450 must be in yellow range" - - result = get_status_from_count(450) - - assert isinstance(result, str) - assert result == STATUS_YELLOW - - -def test_status_red(): - """600 lines is in red zone -- returns STATUS_RED.""" - assert 600 > THRESHOLD_YELLOW_MAX, "Precondition: 600 must be above yellow range" - - result = get_status_from_count(600) - - assert isinstance(result, str) - assert result == STATUS_RED - - -def test_status_green_at_boundary(): - """Exactly THRESHOLD_GREEN_MAX (400) should still be green.""" - result = get_status_from_count(THRESHOLD_GREEN_MAX) - assert result == STATUS_GREEN - - -def test_status_yellow_at_lower_boundary(): - """Exactly THRESHOLD_YELLOW_MIN (401) should be yellow.""" - result = get_status_from_count(THRESHOLD_YELLOW_MIN) - assert result == STATUS_YELLOW - - -def test_status_yellow_at_upper_boundary(): - """Exactly THRESHOLD_YELLOW_MAX (550) should still be yellow.""" - result = get_status_from_count(THRESHOLD_YELLOW_MAX) - assert result == STATUS_YELLOW - - -def test_status_red_at_boundary(): - """One above THRESHOLD_YELLOW_MAX (551) should be red.""" - result = get_status_from_count(THRESHOLD_YELLOW_MAX + 1) - assert result == STATUS_RED - - -def test_status_zero_lines(): - """Zero lines should be green.""" - result = get_status_from_count(0) - assert result == STATUS_GREEN - - -# ---- should_send_email tests --------------------------------- - - -def test_should_send_email_below(): - """500 is below THRESHOLD_EMAIL_TRIGGER (600) -- no email.""" - assert 500 < THRESHOLD_EMAIL_TRIGGER, "Precondition: 500 must be below trigger" - - result = should_send_email(500) - - assert isinstance(result, bool) - assert result is False - - -def test_should_send_email_one_below_threshold(): - """599 is one below THRESHOLD_EMAIL_TRIGGER -- no email.""" - result = should_send_email(THRESHOLD_EMAIL_TRIGGER - 1) - assert isinstance(result, bool) - assert result is False - - -def test_should_send_email_at_threshold(): - """Exactly at THRESHOLD_EMAIL_TRIGGER -- email should be sent.""" - result = should_send_email(THRESHOLD_EMAIL_TRIGGER) - assert isinstance(result, bool) - assert result is True - - -def test_should_send_email_above_threshold(): - """Well above THRESHOLD_EMAIL_TRIGGER -- email should be sent.""" - result = should_send_email(THRESHOLD_EMAIL_TRIGGER + 100) - assert isinstance(result, bool) - assert result is True - - -# ---- get_health_info tests ----------------------------------- - - -def test_get_health_info(tmp_path): - """10-line file returns correct health dict.""" - file_path = tmp_path / "ten_lines.txt" - lines = "\n".join(f"line {i}" for i in range(1, 11)) + "\n" - file_path.write_text(lines, encoding="utf-8") - - result = get_health_info(file_path) - - assert isinstance(result, dict) - # Verify exactly these keys exist -- no more, no fewer - assert set(result.keys()) == {"line_count", "status", "needs_email", "file_path"} - - # Verify exact values and types - assert result["line_count"] == 10 - assert isinstance(result["line_count"], int) - - assert result["status"] == STATUS_GREEN - assert isinstance(result["status"], str) - - assert result["needs_email"] is False - assert isinstance(result["needs_email"], bool) - - assert result["file_path"] == str(file_path) diff --git a/src/aipass/devpulse/.aipass/aipass_local_prompt.md b/src/aipass/devpulse/.aipass/aipass_local_prompt.md index 1d12ded2..ffef0963 100644 --- a/src/aipass/devpulse/.aipass/aipass_local_prompt.md +++ b/src/aipass/devpulse/.aipass/aipass_local_prompt.md @@ -60,9 +60,9 @@ drone @flow list open # Active plans drone systems # All branches ``` -## Branches (15) +## Branches (10 core) -drone, seedgo, prax, cli, ai_mail, flow, spawn, trigger, api, backup, daemon, memory, commons (`src/commons/`), skills (`src/skills/`), devpulse (you — no apps/, coordinates via dispatch + agents) +drone, seedgo, prax, cli, ai_mail, flow, spawn, trigger, memory, devpulse (you — no apps/, coordinates via dispatch + agents) ## Your Project diff --git a/src/aipass/drone/README.md b/src/aipass/drone/README.md index 78d8ec09..e6b00790 100644 --- a/src/aipass/drone/README.md +++ b/src/aipass/drone/README.md @@ -146,7 +146,6 @@ Commands in the interactive tuple bypass capture and inherit the terminal direct | Branch | Reason | |----------|-----------------------------------------------| | `cli` | User-facing CLI with Rich formatted output | -| `backup` | Heavy-IO branch (file scanning, Google APIs) | To add: edit `interactive_commands` or `interactive_branches` in `_handle_target()` in `apps/drone.py`. diff --git a/src/aipass/drone/apps/drone.py b/src/aipass/drone/apps/drone.py index bdf0367a..1f01cb51 100644 --- a/src/aipass/drone/apps/drone.py +++ b/src/aipass/drone/apps/drone.py @@ -288,7 +288,7 @@ def _handle_custom_command(args: list[str]) -> int: # Interactive detection -- same logic as _handle_target interactive_commands = ("monitor", "audit") - interactive_branches = ("cli", "backup") + interactive_branches = ("cli",) interactive = command in interactive_commands or module_name in interactive_branches try: @@ -322,7 +322,7 @@ def _handle_target(args: List[str]) -> int: # Per-command: specific commands that need live terminal (progress bars, TUI). # Per-branch: all commands from that branch get interactive mode (Rich CLI). interactive_commands = ("monitor", "audit") - interactive_branches = ("cli", "backup") + interactive_branches = ("cli",) first_cmd = rest[0] if rest and rest[0] != "--help" else None needs_interactive = ( first_cmd in interactive_commands or module_name in interactive_branches diff --git a/src/aipass/flow/README.md b/src/aipass/flow/README.md index 3b2b83d1..a343f1d5 100644 --- a/src/aipass/flow/README.md +++ b/src/aipass/flow/README.md @@ -14,7 +14,7 @@ ### What I Do - Create numbered plans from type-specific templates via `templates/` plugins - Unified create/close/list commands for all plan types (FPLAN, DPLAN, RPLAN, TDPLAN, ...) -- Close plans with foreground archival to `backup/processed_plans/` +- Close plans with foreground archival to `.backup/processed_plans/` - Vector intake on close via `drone @memory process-plans` with chroma verification - List and filter plans across branches and plan types - Restore plans from backups @@ -114,7 +114,7 @@ Plans follow the convention `{PREFIX}-{NNNN}_topic_slug_YYYY-MM-DD.md` where NNN On `drone @flow close`: 1. Template check (fast-delete empty templates) 2. Mark as closed in registry -3. Archive to `backup/processed_plans/` (foreground, sets processed/cleanup flags atomically) +3. Archive to `.backup/processed_plans/` (foreground, sets processed/cleanup flags atomically) 4. Vector intake: `drone @memory process-plans` + `is_plan_vectorized()` verification 5. Dashboard updates (local + central + branch) 6. Append to `CLOSED_PLANS.local.json` diff --git a/src/aipass/flow/apps/handlers/mbank/process.py b/src/aipass/flow/apps/handlers/mbank/process.py index e8b77e03..a007948b 100644 --- a/src/aipass/flow/apps/handlers/mbank/process.py +++ b/src/aipass/flow/apps/handlers/mbank/process.py @@ -52,7 +52,7 @@ def _find_repo_root() -> Path: _REPO_ROOT = _find_repo_root() MEMORY_PATH = _REPO_ROOT / "src" / "aipass" / "memory" -PROCESSED_PLANS_DIR = _PKG_ROOT / "backup" / "processed_plans" +PROCESSED_PLANS_DIR = _REPO_ROOT / ".backup" / "processed_plans" REGISTRY_FILE = FLOW_JSON_DIR / "fplan_registry.json" # ============================================= diff --git a/src/aipass/flow/apps/handlers/plan/restore_ops.py b/src/aipass/flow/apps/handlers/plan/restore_ops.py index 09e99bda..14092e7c 100644 --- a/src/aipass/flow/apps/handlers/plan/restore_ops.py +++ b/src/aipass/flow/apps/handlers/plan/restore_ops.py @@ -33,7 +33,16 @@ from aipass.flow.apps.handlers.json import json_handler _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" + +def _find_repo_root() -> Path: + """Walk up to find the 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() + +PROCESSED_PLANS_DIR = _find_repo_root() / ".backup" / "processed_plans" MODULE_NAME = "restore_plan" diff --git a/src/aipass/flow/apps/handlers/template/registry_ops.py b/src/aipass/flow/apps/handlers/template/registry_ops.py index 1bf66c87..86a44919 100644 --- a/src/aipass/flow/apps/handlers/template/registry_ops.py +++ b/src/aipass/flow/apps/handlers/template/registry_ops.py @@ -179,6 +179,59 @@ def load_registry() -> Dict[str, Any]: ) save_registry(data) + # Auto-register: detect new template directories and register them + templates_dir = FLOW_ROOT / "templates" + if templates_dir.is_dir(): + registered = set(data["types"].keys()) + used_prefixes = { + entry.get("prefix", "").upper() + for entry in data["types"].values() + } + changed = False + for child in sorted(templates_dir.iterdir()): + if not child.is_dir() or child.name.startswith(("_", ".")) or child.name == "__pycache__": + continue + if child.name in registered: + continue + md_files = list(child.glob("*.md")) + if not md_files: + continue + # Derive prefix: first letter of first word + "PLAN" + first_word = child.name.split("_")[0] + prefix = (first_word[0].upper() + "PLAN") if first_word else "XPLAN" + # Avoid collisions — append second letter if needed + if prefix in used_prefixes and len(first_word) > 1: + prefix = (first_word[:2].upper() + "PLAN") + if prefix in used_prefixes: + continue # Can't auto-assign — needs manual registration + shorthand = prefix.lower() + data["types"][child.name] = { + "prefix": prefix, + "shorthand": shorthand, + "created": _today(), + "registered_by": "auto", + } + used_prefixes.add(prefix) + registered.add(child.name) + changed = True + logger.info( + "[%s] Auto-registered new type '%s' with prefix %s", + MODULE_NAME, + child.name, + prefix, + ) + # Create empty plan registry for new type + plan_reg = FLOW_ROOT / "flow_json" / f"{shorthand}_registry.json" + if not plan_reg.exists(): + try: + plan_reg.parent.mkdir(parents=True, exist_ok=True) + with open(plan_reg, "w", encoding="utf-8") as fh: + json.dump({"next_number": 1, "plans": {}, "last_updated": _today()}, fh, indent=2) + except OSError as exc: + logger.warning("[%s] Failed to create plan registry for %s: %s", MODULE_NAME, child.name, exc) + if changed: + save_registry(data) + return data diff --git a/src/aipass/memory/apps/handlers/monitor/detector.py b/src/aipass/memory/apps/handlers/monitor/detector.py index 5cf5e265..6205d8ec 100644 --- a/src/aipass/memory/apps/handlers/monitor/detector.py +++ b/src/aipass/memory/apps/handlers/monitor/detector.py @@ -119,7 +119,10 @@ def _get_memory_file_path(branch: Dict, memory_type: str) -> Path | None: Returns: Path to memory file, or None if not found """ - branch_path = Path(branch.get('path', '')) + raw_path = branch.get('path', '') + if not raw_path: + return None + branch_path = Path(raw_path) if not branch_path.exists(): return None diff --git a/src/aipass/memory/apps/handlers/monitor/memory_watcher.py b/src/aipass/memory/apps/handlers/monitor/memory_watcher.py index 914ded9f..01ee5d56 100644 --- a/src/aipass/memory/apps/handlers/monitor/memory_watcher.py +++ b/src/aipass/memory/apps/handlers/monitor/memory_watcher.py @@ -554,7 +554,7 @@ def start_memory_watcher() -> Dict[str, Any]: watched_paths = [] for branch_path in branch_paths: try: - new_observer.schedule(watcher, str(branch_path), recursive=False) + new_observer.schedule(watcher, str(branch_path), recursive=True) watched_paths.append(str(branch_path)) except Exception as e: logger.warning(f"[memory_watcher] Failed to schedule watcher for {branch_path}: {e}") diff --git a/src/aipass/memory/apps/handlers/rollover/extractor.py b/src/aipass/memory/apps/handlers/rollover/extractor.py index acfb562d..ab440859 100644 --- a/src/aipass/memory/apps/handlers/rollover/extractor.py +++ b/src/aipass/memory/apps/handlers/rollover/extractor.py @@ -61,7 +61,11 @@ def create_rollover_backup(file_path: Path) -> Dict[str, Any]: """ try: # Create .backup directory in branch root - backup_dir = file_path.parent / '.backup' + # For .trinity/ files, go up to branch root; otherwise use file's parent + if file_path.parent.name == '.trinity': + backup_dir = file_path.parent.parent / '.backup' + else: + backup_dir = file_path.parent / '.backup' backup_dir.mkdir(exist_ok=True) # Backup filename: rollover_backup.json (always overwrites) @@ -98,7 +102,11 @@ def restore_from_backup(file_path: Path) -> Dict[str, Any]: Dict with restore status """ try: - backup_dir = file_path.parent / '.backup' + # Match backup location from create_rollover_backup + if file_path.parent.name == '.trinity': + backup_dir = file_path.parent.parent / '.backup' + else: + backup_dir = file_path.parent / '.backup' backup_name = f'rollover_backup_{file_path.name}' backup_path = backup_dir / backup_name diff --git a/src/aipass/memory/apps/handlers/templates/differ.py b/src/aipass/memory/apps/handlers/templates/differ.py index 21b03d3e..eabfbc25 100644 --- a/src/aipass/memory/apps/handlers/templates/differ.py +++ b/src/aipass/memory/apps/handlers/templates/differ.py @@ -34,7 +34,7 @@ from aipass.memory.apps.handlers.json import json_handler # PATH SETUP # ============================================================================= -MEMORY_ROOT = Path(__file__).resolve().parent.parent.parent # handlers/templates/differ.py -> memory/ +MEMORY_ROOT = Path(__file__).resolve().parent.parent.parent.parent # handlers/templates/differ.py -> apps -> handlers -> templates -> memory/ # ============================================================================= # CONSTANTS diff --git a/src/aipass/memory/apps/handlers/templates/pusher.py b/src/aipass/memory/apps/handlers/templates/pusher.py index 32a88ca4..845507b1 100644 --- a/src/aipass/memory/apps/handlers/templates/pusher.py +++ b/src/aipass/memory/apps/handlers/templates/pusher.py @@ -42,7 +42,7 @@ from aipass.memory.apps.handlers.json.memory_files import ( # PATH SETUP # ============================================================================= -MEMORY_ROOT = Path(__file__).resolve().parent.parent.parent # handlers/templates/pusher.py -> memory/ +MEMORY_ROOT = Path(__file__).resolve().parent.parent.parent.parent # handlers/templates/pusher.py -> apps -> handlers -> templates -> memory/ def _find_repo_root() -> Path: diff --git a/src/aipass/seedgo/apps/seedgo.py b/src/aipass/seedgo/apps/seedgo.py index 11b9015d..379f855c 100644 --- a/src/aipass/seedgo/apps/seedgo.py +++ b/src/aipass/seedgo/apps/seedgo.py @@ -202,11 +202,21 @@ def print_help() -> None: console.print(" [green]drone @seedgo diagnostics @flow[/green] [dim]# Single branch diagnostics[/dim]") console.print() + console.print("[yellow]Proof (Self-Check):[/yellow]") + console.print(" [green]drone @seedgo proof[/green] [dim]# List available proof packs[/dim]") + console.print(" [green]drone @seedgo proof aipass[/green] [dim]# Run all proofs for aipass pack[/dim]") + console.print(" [green]drone @seedgo proof_query[/green] [dim]# Query proof pack content[/dim]") + console.print() + + console.print("[yellow]Test Map:[/yellow]") + console.print(" [green]drone @seedgo test_map @flow[/green] [dim]# Function test coverage map for branch[/dim]") + console.print() + console.print("─" * 70) console.print() # Commands line for drone discovery - console.print("[dim]Commands: audit, standards_audit, standards_query, checklist, diagnostics, diagnostics_audit, readme, readme_update, --help[/dim]") + console.print("[dim]Commands: audit, standards_audit, standards_query, checklist, diagnostics, diagnostics_audit, proof, proof_query, test_map, readme, readme_update, --help[/dim]") console.print() diff --git a/src/aipass/spawn/apps/modules/delete.py b/src/aipass/spawn/apps/modules/delete.py index 2029084c..e6400721 100644 --- a/src/aipass/spawn/apps/modules/delete.py +++ b/src/aipass/spawn/apps/modules/delete.py @@ -75,6 +75,15 @@ def handle_delete(args: list[str]) -> int: Returns exit code (0=success, 1=failure). """ + # Intercept --help before processing (argparse has add_help=False) + if "--help" in args or "-h" in args: + warning("Usage: drone @spawn delete <@branch> [--yes] [--dry-run]") + console.print() + console.print(" [green]@branch[/green] Branch to archive and deregister") + console.print(" [green]--yes[/green] Skip confirmation prompt") + console.print(" [green]--dry-run[/green] Preview what would happen without changes") + return 0 + if not args: warning("Usage: drone @spawn delete <@branch> [--yes] [--dry-run]") console.print() diff --git a/src/aipass/spawn/apps/modules/update.py b/src/aipass/spawn/apps/modules/update.py index 083a570e..e1bc0797 100644 --- a/src/aipass/spawn/apps/modules/update.py +++ b/src/aipass/spawn/apps/modules/update.py @@ -82,6 +82,17 @@ def handle_update(args: list[str]) -> int: Returns exit code (0=success, 1=failure). """ + # Intercept --help before processing (argparse has add_help=False) + if "--help" in args or "-h" in args: + warning("Usage: drone @spawn update <@branch|class --all> [--dry-run] [--trace]") + console.print() + console.print(" [green]@branch[/green] Update a single branch (uses its own class)") + console.print(" [green]builder --all[/green] Update all builder-class branches") + console.print(" [green]birthright --all[/green] Update all birthright-class branches") + console.print(" [green]--dry-run[/green] Preview changes without modifying files") + console.print(" [green]--trace[/green] Enable verbose logging") + return 0 + if not args: warning("Usage: drone @spawn update <@branch|class --all> [--dry-run] [--trace]") console.print()