From ae459853f0a29716b8a2bc83d582ff4099b5853b Mon Sep 17 00:00:00 2001 From: AIPass Date: Sat, 28 Mar 2026 21:58:29 -0700 Subject: [PATCH] =?UTF-8?q?fix(system):=20S59-S60=20cross-branch=20fixes?= =?UTF-8?q?=20=E2=80=94=20stale=20refs,=20queue=20spam,=20close=20bug,=20t?= =?UTF-8?q?emplate=20(#141)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Daemon: 14 stale @dev_central→@devpulse and @seed→@seedgo refs fixed across 12 files - Prax: event_queue spam eliminated (144k logs/4hrs), cortex→spawn refs, bypass added - Flow: close_ops hardened with shutil.move + auto-healing for orphaned plans - Spawn: builder template handler guard populated, conftest registry protection added - Seedgo: handler guard pytest exception, test_json_handler fix - Commons: extract_mentions dedup bug (SELECT DISTINCT LOWER) - 7 branches: @seed→@seedgo in handlers/__init__.py - Memory: @seed→@seedgo handler ref Co-authored-by: Claude Opus 4.6 (1M context) --- .gitignore | 1 + src/aipass/ai_mail/apps/handlers/__init__.py | 2 +- src/aipass/backup/apps/handlers/__init__.py | 2 +- src/aipass/backup/conftest.py | 10 + src/aipass/cli/apps/handlers/__init__.py | 2 +- src/aipass/daemon/apps/handlers/__init__.py | 2 +- .../apps/handlers/actions/action_processor.py | 2 +- .../apps/handlers/actions/actions_registry.py | 2 +- .../apps/handlers/schedule/task_registry.py | 6 +- src/aipass/daemon/apps/modules/actions.py | 4 +- src/aipass/daemon/apps/modules/schedule.py | 4 +- src/aipass/daemon/apps/plugins/__init__.py | 2 +- src/aipass/daemon/apps/plugins/daily_audit.py | 12 +- src/aipass/daemon/apps/scheduler_cron.py | 2 +- .../daemon/tests/test_actions_registry.py | 6 +- src/aipass/daemon/tests/test_task_registry.py | 6 +- .../flow/apps/handlers/mbank/process.py | 5 +- .../flow/apps/handlers/plan/close_ops.py | 9 + src/aipass/memory/apps/handlers/__init__.py | 2 +- .../memory/config/.plans_processed.json | 18 +- src/aipass/prax/.seedgo/bypass.json | 5 + src/aipass/prax/apps/handlers/__init__.py | 2 +- .../apps/handlers/logging/introspection.py | 2 +- .../apps/handlers/monitoring/event_queue.py | 2 - .../apps/handlers/monitoring/log_watcher.py | 6 +- src/aipass/seedgo/apps/handlers/__init__.py | 6 +- src/aipass/seedgo/tests/test_json_handler.py | 11 +- .../builder/.spawn/.template_registry.json | 239 ++++++++++-------- .../builder/apps/handlers/__init__.py | 88 +++++++ src/aipass/spawn/tests/conftest.py | 30 +++ .../apps/handlers/identity/identity_ops.py | 2 +- 31 files changed, 339 insertions(+), 153 deletions(-) create mode 100644 src/aipass/backup/conftest.py diff --git a/.gitignore b/.gitignore index 79234611..f7aff645 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,4 @@ src/aipass/memory/config/fragmented_memory_state.json .vscode src/aipass/memory/config/memory_bank.config.json src/aipass/spawn/templates/builder/.spawn/.template_registry.json +branch_audits _only diff --git a/src/aipass/ai_mail/apps/handlers/__init__.py b/src/aipass/ai_mail/apps/handlers/__init__.py index b1a66134..662db649 100755 --- a/src/aipass/ai_mail/apps/handlers/__init__.py +++ b/src/aipass/ai_mail/apps/handlers/__init__.py @@ -102,7 +102,7 @@ def _guard_branch_access(): f" from {MY_BRANCH}.apps.modules.logger import logger\n" f"\n" f" For full standards guide:\n" - f" drone @seed handlers\n" + f" drone @seedgo handlers\n" f"{'='*60}" ) diff --git a/src/aipass/backup/apps/handlers/__init__.py b/src/aipass/backup/apps/handlers/__init__.py index 14e5bfeb..c31cccd0 100755 --- a/src/aipass/backup/apps/handlers/__init__.py +++ b/src/aipass/backup/apps/handlers/__init__.py @@ -104,7 +104,7 @@ def _guard_branch_access(): f" from {MY_BRANCH}.apps.modules.logger import logger\n" f"\n" f" For full standards guide:\n" - f" drone @seed handlers\n" + f" drone @seedgo handlers\n" f"{'='*60}" ) diff --git a/src/aipass/backup/conftest.py b/src/aipass/backup/conftest.py new file mode 100644 index 00000000..27e56695 --- /dev/null +++ b/src/aipass/backup/conftest.py @@ -0,0 +1,10 @@ +"""Pytest configuration for backup branch. + +Excludes backup data directories from test collection to prevent +pytest from recursing into system_snapshot and versioned_backup +which contain full copies of the AIPass repo tree. +""" + +collect_ignore_glob = [ + "backups/*", +] diff --git a/src/aipass/cli/apps/handlers/__init__.py b/src/aipass/cli/apps/handlers/__init__.py index 5b5bfdc0..7acaaf85 100755 --- a/src/aipass/cli/apps/handlers/__init__.py +++ b/src/aipass/cli/apps/handlers/__init__.py @@ -123,7 +123,7 @@ def _guard_branch_access(): f" from {MY_BRANCH}.apps.modules.logger import logger\n" f"\n" f" For full standards guide:\n" - f" drone @seed handlers\n" + f" drone @seedgo handlers\n" f"{'='*60}" ) diff --git a/src/aipass/daemon/apps/handlers/__init__.py b/src/aipass/daemon/apps/handlers/__init__.py index 09f37dc6..dd60f81e 100644 --- a/src/aipass/daemon/apps/handlers/__init__.py +++ b/src/aipass/daemon/apps/handlers/__init__.py @@ -104,7 +104,7 @@ def _guard_branch_access(): f" from {MY_BRANCH}.apps.modules.logger import logger\n" f"\n" f" For full standards guide:\n" - f" drone @seed handlers\n" + f" drone @seedgo handlers\n" f"{'='*60}" ) diff --git a/src/aipass/daemon/apps/handlers/actions/action_processor.py b/src/aipass/daemon/apps/handlers/actions/action_processor.py index 17cc5c84..1af9a611 100644 --- a/src/aipass/daemon/apps/handlers/actions/action_processor.py +++ b/src/aipass/daemon/apps/handlers/actions/action_processor.py @@ -182,7 +182,7 @@ def _dispatch_action( message=action.get("prompt", name), from_branch='@daemon', auto_execute=True, - reply_to='@dev_central', + reply_to='@devpulse', ) if email_sent: assert mark_reminder_completed is not None diff --git a/src/aipass/daemon/apps/handlers/actions/actions_registry.py b/src/aipass/daemon/apps/handlers/actions/actions_registry.py index 3f39bea2..48a7aa36 100644 --- a/src/aipass/daemon/apps/handlers/actions/actions_registry.py +++ b/src/aipass/daemon/apps/handlers/actions/actions_registry.py @@ -112,7 +112,7 @@ def create_action( name: Human-readable action name (e.g., "daily_audit") action_type: "plugin" | "schedule" | "reminder" schedule_type: "daily" | "hourly" | "interval" | "once" - target_branch: Target branch email (e.g., "@seed") + target_branch: Target branch email (e.g., "@seedgo") prompt: What the dispatched agent should do time: For daily: "HH:MM", for hourly: "MM" interval_minutes: For interval schedule type diff --git a/src/aipass/daemon/apps/handlers/schedule/task_registry.py b/src/aipass/daemon/apps/handlers/schedule/task_registry.py index 92a8b11c..ee0d5830 100644 --- a/src/aipass/daemon/apps/handlers/schedule/task_registry.py +++ b/src/aipass/daemon/apps/handlers/schedule/task_registry.py @@ -179,7 +179,7 @@ def create_task( Args: task: Brief description of the task/follow-up due_date: When to trigger (supports "7d", "1w", "YYYY-MM-DD") - recipient: Target branch (e.g., "@dev_central") + recipient: Target branch (e.g., "@devpulse") message: Message to deliver when due Returns: @@ -501,7 +501,7 @@ def process_due_tasks_batch( message=email_body, from_branch='@daemon', auto_execute=True, - reply_to='@dev_central', + reply_to='@devpulse', ) if email_sent: @@ -574,7 +574,7 @@ if __name__ == "__main__": test_task = create_task( task="Test backup health check", due_date="7d", - recipient="@dev_central", + recipient="@devpulse", message="Please verify backup systems are healthy" ) console.print(f" Created task: {test_task['id']}") diff --git a/src/aipass/daemon/apps/modules/actions.py b/src/aipass/daemon/apps/modules/actions.py index 2909700f..8248fae7 100644 --- a/src/aipass/daemon/apps/modules/actions.py +++ b/src/aipass/daemon/apps/modules/actions.py @@ -207,7 +207,7 @@ def print_help() -> None: console.print() console.print("[yellow]SET SCHEDULE:[/yellow]") - console.print(' set schedule @seed "Run audit" daily 04:00') + console.print(' set schedule @seedgo "Run audit" daily 04:00') console.print(' set schedule @vera "Heartbeat" interval 240') console.print(' set schedule @flow "Check plans" hourly 30') console.print(" [dim]Types: daily HH:MM, hourly MM, interval MINUTES[/dim]") @@ -264,7 +264,7 @@ def _handle_set_reminder(args: List[str]) -> bool: date_str = args[0] message = args[1] - target_branch = "@dev_central" # Default reminder target + target_branch = "@devpulse" # Default reminder target # Parse --to flag if "--to" in args: diff --git a/src/aipass/daemon/apps/modules/schedule.py b/src/aipass/daemon/apps/modules/schedule.py index 63fc9d98..185c214a 100644 --- a/src/aipass/daemon/apps/modules/schedule.py +++ b/src/aipass/daemon/apps/modules/schedule.py @@ -155,7 +155,7 @@ def _print_help() -> None: console.print("[yellow]CREATE OPTIONS:[/yellow]") console.print(" --due (Required) Due date: 1d, 7d, 2w, 1m, or ISO date (2026-02-15)") - console.print(" --to (Required) Recipient branch (e.g., @flow, @seed)") + console.print(" --to (Required) Recipient branch (e.g., @flow, @seedgo)") console.print(" --message (Optional) Additional details for the follow-up") console.print() @@ -164,7 +164,7 @@ def _print_help() -> None: console.print(' schedule create "Check FPLAN-0290 status" --due 7d --to @flow') console.print() console.print(' # Follow up with Seed about code review in 2 weeks') - console.print(' schedule create "Code review follow-up" --due 2w --to @seed --message "Review PR #45"') + console.print(' schedule create "Code review follow-up" --due 2w --to @seedgo --message "Review PR #45"') console.print() console.print(' # Check all due tasks and send reminder emails') console.print(' schedule run-due') diff --git a/src/aipass/daemon/apps/plugins/__init__.py b/src/aipass/daemon/apps/plugins/__init__.py index 4bd742e8..8511a27f 100644 --- a/src/aipass/daemon/apps/plugins/__init__.py +++ b/src/aipass/daemon/apps/plugins/__init__.py @@ -32,7 +32,7 @@ PLUGIN_CONFIG Schema: "time": str | None, # For daily: "HH:MM" (24h). For hourly: minute "MM" "interval_minutes": int | None, # For interval: minutes between runs "enabled": bool, # Plugin active/inactive toggle - "branch": str, # Target branch email (e.g., "@seed") + "branch": str, # Target branch email (e.g., "@seedgo") "fresh": bool, # True = fresh session, False = resume "max_turns": int, # Max agent turns (safety limit) "prompt": str, # What the spawned agent should do diff --git a/src/aipass/daemon/apps/plugins/daily_audit.py b/src/aipass/daemon/apps/plugins/daily_audit.py index 2d1fb82d..bf1865c1 100644 --- a/src/aipass/daemon/apps/plugins/daily_audit.py +++ b/src/aipass/daemon/apps/plugins/daily_audit.py @@ -9,9 +9,9 @@ """ Daily Standards Audit Plugin -Wakes @seed daily at 04:00 with fresh context to run a full system audit. -Seed checks BRANCH_REGISTRY completeness, runs drone @seed audit @all, -fixes non-compliance issues, and emails a summary to @dev_central. +Wakes @seedgo daily at 04:00 with fresh context to run a full system audit. +Seed checks BRANCH_REGISTRY completeness, runs drone @seedgo audit @all, +fixes non-compliance issues, and emails a summary to @devpulse. """ from aipass.prax import logger @@ -22,15 +22,15 @@ PLUGIN_CONFIG = { "time": "04:00", "interval_minutes": None, "enabled": True, - "branch": "@seed", + "branch": "@seedgo", "fresh": True, "max_turns": 50, "prompt": ( "Daily maintenance audit. " "1) Read BRANCH_REGISTRY.json - confirm all branches are registered and paths exist. " - "2) Run drone @seed audit @all - check standards compliance across all branches. " + "2) Run drone @seedgo audit @all - check standards compliance across all branches. " "3) Fix any non-compliance issues you can fix directly. " - "4) Email summary to @dev_central with: branches audited, pass/fail counts, " + "4) Email summary to @devpulse with: branches audited, pass/fail counts, " "issues found, issues fixed, remaining issues. " "5) Update your memories with audit results." ), diff --git a/src/aipass/daemon/apps/scheduler_cron.py b/src/aipass/daemon/apps/scheduler_cron.py index 9c29660a..5b9859c6 100755 --- a/src/aipass/daemon/apps/scheduler_cron.py +++ b/src/aipass/daemon/apps/scheduler_cron.py @@ -228,7 +228,7 @@ def _process_single_task(task: dict, results: dict) -> None: message=email_body, from_branch='@daemon', auto_execute=True, - reply_to='@dev_central', + reply_to='@devpulse', ) if email_sent: diff --git a/src/aipass/daemon/tests/test_actions_registry.py b/src/aipass/daemon/tests/test_actions_registry.py index 7574fa18..fc52b227 100644 --- a/src/aipass/daemon/tests/test_actions_registry.py +++ b/src/aipass/daemon/tests/test_actions_registry.py @@ -57,7 +57,7 @@ class TestCreate: name="test_audit", action_type="schedule", schedule_type="daily", - target_branch="@seed", + target_branch="@seedgo", prompt="Run audit", time="04:00", fresh=True, @@ -68,7 +68,7 @@ class TestCreate: assert action["type"] == "schedule" assert action["schedule_type"] == "daily" assert action["time"] == "04:00" - assert action["target_branch"] == "@seed" + assert action["target_branch"] == "@seedgo" assert action["enabled"] is True assert action["last_run"] is None assert action["completed"] is None @@ -88,7 +88,7 @@ class TestCreate: name="Check VERA progress", action_type="reminder", schedule_type="once", - target_branch="@dev_central", + target_branch="@devpulse", prompt="Check VERA progress", due_date="2026-03-11", ) diff --git a/src/aipass/daemon/tests/test_task_registry.py b/src/aipass/daemon/tests/test_task_registry.py index 0d24a965..c21f1b64 100644 --- a/src/aipass/daemon/tests/test_task_registry.py +++ b/src/aipass/daemon/tests/test_task_registry.py @@ -174,11 +174,11 @@ class TestCreateTask: task = create_task( task="Check backup health", due_date="7d", - recipient="@dev_central", + recipient="@devpulse", message="Verify backup systems", ) assert task["task"] == "Check backup health" - assert task["recipient"] == "@dev_central" + assert task["recipient"] == "@devpulse" assert task["message"] == "Verify backup systems" assert task["status"] == "pending" assert len(task["id"]) == 16 @@ -192,7 +192,7 @@ class TestCreateTask: create_task( task="persisted task", due_date="1d", - recipient="@seed", + recipient="@seedgo", message="msg", ) raw = json.loads(isolate_registry.read_text(encoding="utf-8")) diff --git a/src/aipass/flow/apps/handlers/mbank/process.py b/src/aipass/flow/apps/handlers/mbank/process.py index 9ca72766..14456806 100644 --- a/src/aipass/flow/apps/handlers/mbank/process.py +++ b/src/aipass/flow/apps/handlers/mbank/process.py @@ -25,6 +25,7 @@ _PKG_ROOT = Path(__file__).resolve().parents[4] # Standard imports import json +import shutil from datetime import datetime, timezone from typing import Dict, List, Any @@ -421,8 +422,8 @@ def archive_plan(plan_path: Path) -> bool: # Store source path for verification source_path = Path(plan_path) - # Attempt move - plan_path.rename(destination) + # Attempt move (shutil.move handles cross-filesystem moves) + shutil.move(str(plan_path), str(destination)) # VERIFICATION LAYER: Confirm move actually happened if not destination.exists(): diff --git a/src/aipass/flow/apps/handlers/plan/close_ops.py b/src/aipass/flow/apps/handlers/plan/close_ops.py index b64c3aed..7d809068 100644 --- a/src/aipass/flow/apps/handlers/plan/close_ops.py +++ b/src/aipass/flow/apps/handlers/plan/close_ops.py @@ -228,6 +228,15 @@ def close_plan_impl(plan_num: Any = None, confirm: bool = False, from aipass.flow.apps.handlers.mbank.process import archive_plan if archive_plan(plan_file): logger.info(f"[{MODULE_NAME}] Cleaned up orphaned file for {plan_label}: {plan_file}") + # Update registry flags that were missed on the failed first close + 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) messages.append({"type": "success", "text": " Orphaned file archived successfully"}) else: logger.warning(f"[{MODULE_NAME}] Failed to archive orphaned file for {plan_label}: {plan_file}") diff --git a/src/aipass/memory/apps/handlers/__init__.py b/src/aipass/memory/apps/handlers/__init__.py index f1082d01..e1e29a07 100644 --- a/src/aipass/memory/apps/handlers/__init__.py +++ b/src/aipass/memory/apps/handlers/__init__.py @@ -104,7 +104,7 @@ def _guard_branch_access(): f" from {MY_BRANCH}.apps.modules.logger import logger\n" f"\n" f" For full standards guide:\n" - f" drone @seed handlers\n" + f" drone @seedgo handlers\n" f"{'='*60}" ) diff --git a/src/aipass/memory/config/.plans_processed.json b/src/aipass/memory/config/.plans_processed.json index 83e515b7..c8dc3b12 100644 --- a/src/aipass/memory/config/.plans_processed.json +++ b/src/aipass/memory/config/.plans_processed.json @@ -134,5 +134,21 @@ "DPLAN-0068_self_audit_compliance_sprint_2026-03-27.md": "2026-03-27T23:22:05.681987", "DPLAN-0070_self_audit_compliance_sprint_2026-03-27.md": "2026-03-27T23:22:41.790812", "DPLAN-0066_self_audit_compliance_sprint_2026-03-27.md": "2026-03-27T23:22:41.790824", - "DPLAN-0072_self_audit_compliance_sprint_2026-03-28.md": "2026-03-28T00:32:51.757552" + "DPLAN-0072_self_audit_compliance_sprint_2026-03-28.md": "2026-03-28T00:32:51.757552", + "DPLAN-0052_branch_audit_sprint_silent_catch_wave_2026-03-23.md": "2026-03-28T12:27:09.971335", + "DPLAN-0055_persistent_citizen_git_branches_2026-03-24.md": "2026-03-28T12:27:25.886644", + "DPLAN-0057_night_shift_drone_compliance_branch_audits_sy_2026-03-24.md": "2026-03-28T12:27:38.467210", + "DPLAN-0063_self_audit_compliance_sprint_2026-03-27.md": "2026-03-28T12:27:58.368678", + "DPLAN-0064_self_audit_compliance_sprint_2026-03-27.md": "2026-03-28T12:28:15.007738", + "DPLAN-0065_self_audit_compliance_sprint_2026-03-27.md": "2026-03-28T12:28:31.421009", + "DPLAN-0069_100_seedgo_compliance_sprint_2026-03-27.md": "2026-03-28T12:28:47.782884", + "DPLAN-0071_self_audit_compliance_sprint_2026-03-28.md": "2026-03-28T12:29:04.294942", + "FPLAN-0134_persistent_citizen_git_branches_drone_build_2026-03-24.md": "2026-03-28T19:43:55.209451", + "DPLAN-0018_commons_bug_investigation_fts5_search_visitor_2026-03-17.md": "2026-03-28T19:44:25.813768", + "DPLAN-0019_module_rename_remove__module_suffix_from_comm_2026-03-17.md": "2026-03-28T19:44:44.216623", + "DPLAN-0054_bypass_accountability_tracker_2026-03-23.md": "2026-03-28T19:51:55.904984", + "DPLAN-0059_seedgo_test_quality_standard_deep_coverage_en_2026-03-24.md": "2026-03-28T19:52:31.066363", + "DPLAN-0060_custom_function_scanner_seedgo_module_for_bra_2026-03-24.md": "2026-03-28T19:52:49.501398", + "DPLAN-0061_expand_test_quality_checker_to_all_standard_c_2026-03-24.md": "2026-03-28T19:53:13.017860", + "FPLAN-0138_s59_walkthrough_fixes_2026-03-28.md": "2026-03-28T19:53:32.097935" } \ No newline at end of file diff --git a/src/aipass/prax/.seedgo/bypass.json b/src/aipass/prax/.seedgo/bypass.json index e75bf201..030c71e0 100644 --- a/src/aipass/prax/.seedgo/bypass.json +++ b/src/aipass/prax/.seedgo/bypass.json @@ -406,6 +406,11 @@ "file": "apps/", "standard": "architecture", "reason": "Template files managed by spawn — prax cannot add/remove template files. Template structure is defined in spawn/templates/builder/ and applied at branch creation time." + }, + { + "file": "apps/handlers/monitoring/event_queue.py", + "standard": "silent_catch", + "reason": "queue.Empty is expected control flow from PriorityQueue.get(timeout=N), not an error. Logging it produces 144,000 noise entries per 4hrs of monitoring (10 polls/sec). The empty state IS the normal idle state." } ], "notes": { diff --git a/src/aipass/prax/apps/handlers/__init__.py b/src/aipass/prax/apps/handlers/__init__.py index c2a68bdb..e0d00a45 100755 --- a/src/aipass/prax/apps/handlers/__init__.py +++ b/src/aipass/prax/apps/handlers/__init__.py @@ -104,7 +104,7 @@ def _guard_branch_access(): f" from {MY_BRANCH}.apps.modules.logger import logger\n" f"\n" f" For full standards guide:\n" - f" drone @seed handlers\n" + f" drone @seedgo handlers\n" f"{'='*60}" ) diff --git a/src/aipass/prax/apps/handlers/logging/introspection.py b/src/aipass/prax/apps/handlers/logging/introspection.py index 1a16fe05..50a163a0 100755 --- a/src/aipass/prax/apps/handlers/logging/introspection.py +++ b/src/aipass/prax/apps/handlers/logging/introspection.py @@ -60,7 +60,7 @@ def get_calling_module() -> str: """Detect calling module from stack trace Returns: - Module name (e.g., 'drone', 'flow', 'cortex') or 'unknown_module' + Module name (e.g., 'drone', 'flow', 'spawn') or 'unknown_module' """ caller_path = _find_external_caller_path() if caller_path: diff --git a/src/aipass/prax/apps/handlers/monitoring/event_queue.py b/src/aipass/prax/apps/handlers/monitoring/event_queue.py index 57f4e432..84a6b471 100644 --- a/src/aipass/prax/apps/handlers/monitoring/event_queue.py +++ b/src/aipass/prax/apps/handlers/monitoring/event_queue.py @@ -79,7 +79,6 @@ class MonitoringQueue: try: return self.queue.get(timeout=timeout) except Empty: - logger.info("[event_queue] Queue empty on dequeue poll") return None def flush(self): @@ -89,7 +88,6 @@ class MonitoringQueue: try: self.queue.get_nowait() except Empty: - logger.info("[event_queue] Flush complete (queue drained)") break self.recent_events.clear() diff --git a/src/aipass/prax/apps/handlers/monitoring/log_watcher.py b/src/aipass/prax/apps/handlers/monitoring/log_watcher.py index 2a071124..55f36982 100644 --- a/src/aipass/prax/apps/handlers/monitoring/log_watcher.py +++ b/src/aipass/prax/apps/handlers/monitoring/log_watcher.py @@ -254,12 +254,12 @@ class LogFileWatcher(FileSystemEventHandler): elif "search" in log_line.lower() and "query" in log_line.lower(): return {'command': "memory_bank search", 'caller': None, 'target': None} - # Pattern 10: Cortex operations (direct python3 calls) - if "[cortex]" in log_line.lower(): + # Pattern 10: Spawn operations (direct python3 calls) + if "[spawn]" in log_line.lower(): if "Creating" in log_line and "branch" in log_line.lower(): match = re.search(r"Creating\s+(?:branch\s+)?(\w+)", log_line) target = match.group(1).upper() if match else None - return {'command': "cortex create branch", 'caller': None, 'target': target} + return {'command': "spawn create branch", 'caller': None, 'target': target} # Pattern 11: Trigger operations (direct python3 calls) if "[trigger]" in log_line.lower(): diff --git a/src/aipass/seedgo/apps/handlers/__init__.py b/src/aipass/seedgo/apps/handlers/__init__.py index 65b2cb17..2a1a1dae 100644 --- a/src/aipass/seedgo/apps/handlers/__init__.py +++ b/src/aipass/seedgo/apps/handlers/__init__.py @@ -77,6 +77,10 @@ def _guard_branch_access(): return # Allow command-line Python through return # Allow if truly can't determine + # Allow pytest — test files need to import handlers for testing + if "pytest" in caller_file or "/_pytest/" in caller_file: + return + # Check if caller is from our branch # MY_BRANCH is "aipass.seedgo" (dotted), but filesystem uses "/aipass/seedgo/" branch_path = "/" + MY_BRANCH.replace(".", "/") + "/" @@ -104,7 +108,7 @@ def _guard_branch_access(): f" from {MY_BRANCH}.apps.modules.logger import logger\n" f"\n" f" For full standards guide:\n" - f" drone @seed handlers\n" + f" drone @seedgo handlers\n" f"{'='*60}" ) diff --git a/src/aipass/seedgo/tests/test_json_handler.py b/src/aipass/seedgo/tests/test_json_handler.py index 2e01b6fd..2ab1ba57 100644 --- a/src/aipass/seedgo/tests/test_json_handler.py +++ b/src/aipass/seedgo/tests/test_json_handler.py @@ -645,11 +645,18 @@ def test_get_json_path_returns_pathlib_path(tmp_path: Path) -> None: def test_ensure_no_overwrite_existing(tmp_path: Path) -> None: - """no_overwrite: ensure_json_exists does not overwrite already_exists data.""" + """no_overwrite: ensure_json_exists does not overwrite already_exists valid data.""" json_dir = _json_dir_as_path(tmp_path) json_dir.mkdir(parents=True, exist_ok=True) target = json_dir / "preserve_config.json" - target.write_text('{"custom": "data"}', encoding="utf-8") + # Write a VALID config structure with an extra custom key + valid_config = { + "module_name": "preserve", + "version": "1.0.0", + "config": {"auto_save": True, "enabled": True}, + "custom": "data", + } + target.write_text(json.dumps(valid_config), encoding="utf-8") json_handler.ensure_json_exists("preserve", "config") data = json.loads(target.read_text(encoding="utf-8")) assert data.get("custom") == "data", "Must not overwrite existing file" diff --git a/src/aipass/spawn/templates/builder/.spawn/.template_registry.json b/src/aipass/spawn/templates/builder/.spawn/.template_registry.json index 5aa9147b..eda2c54d 100644 --- a/src/aipass/spawn/templates/builder/.spawn/.template_registry.json +++ b/src/aipass/spawn/templates/builder/.spawn/.template_registry.json @@ -1,7 +1,7 @@ { "metadata": { "version": "1.0.0", - "last_updated": "2026-03-25", + "last_updated": "2026-03-28", "description": "Template file tracking registry for ID-based updates" }, "files": { @@ -17,6 +17,12 @@ "content_hash": "c9702fe2cc21", "has_branch_placeholder": false }, + "f015": { + "path": ".aipass/README.md", + "name": "README.md", + "content_hash": "f42d87684fdf", + "has_branch_placeholder": false + }, "f003": { "path": ".aipass/aipass_local_prompt.md", "name": "aipass_local_prompt.md", @@ -29,6 +35,12 @@ "content_hash": "93d3fcb74f23", "has_branch_placeholder": false }, + "f025": { + "path": ".claude/README.md", + "name": "README.md", + "content_hash": "adb0ce8c53c1", + "has_branch_placeholder": false + }, "f005": { "path": ".claude/settings.local.json", "name": "settings.local.json", @@ -41,12 +53,24 @@ "content_hash": "841dedb922da", "has_branch_placeholder": false }, + "f027": { + "path": ".seedgo/README.md", + "name": "README.md", + "content_hash": "ea03468bbf16", + "has_branch_placeholder": false + }, "f007": { "path": ".seedgo/bypass.json", "name": "bypass.json", "content_hash": "0ac90a35515b", "has_branch_placeholder": false }, + "f028": { + "path": ".trinity/README.md", + "name": "README.md", + "content_hash": "f461c9b16fc5", + "has_branch_placeholder": false + }, "f008": { "path": ".trinity/local.json", "name": "local.json", @@ -83,102 +107,18 @@ "content_hash": "059295ad4d4e", "has_branch_placeholder": false }, - "f014": { - "path": "apps/__init__.py", - "name": "__init__.py", - "content_hash": "41b011f487af", - "has_branch_placeholder": false - }, - "f017": { - "path": "apps/handlers/__init__.py", - "name": "__init__.py", - "content_hash": "e3b0c44298fc", - "has_branch_placeholder": false - }, - "f018": { - "path": "apps/{{BRANCH}}.py", - "name": "{{BRANCH}}.py", - "content_hash": "fc71b424c10b", - "has_branch_placeholder": true - }, - "f019": { - "path": "artifacts/birth_certificate.json", - "name": "birth_certificate.json", - "content_hash": "1ec401f4e397", - "has_branch_placeholder": false - }, - "f020": { - "path": "docs/README.md", - "name": "README.md", - "content_hash": "2434da568727", - "has_branch_placeholder": false - }, - "f021": { - "path": "dropbox/README.md", - "name": "README.md", - "content_hash": "9e1e9b71f93b", - "has_branch_placeholder": false - }, - "f022": { - "path": "pytest.ini", - "name": "pytest.ini", - "content_hash": "7b39ba7bca40", - "has_branch_placeholder": false - }, - "f023": { - "path": "tests/__init__.py", - "name": "__init__.py", - "content_hash": "881f06bb6574", - "has_branch_placeholder": false - }, - "f026": { - "path": "{{BRANCH}}_json/README.md", - "name": "README.md", - "content_hash": "e64fa555e7b8", - "has_branch_placeholder": false - }, - "f016": { - "path": "apps/modules/__init__.py", - "name": "__init__.py", - "content_hash": "e3b0c44298fc", - "has_branch_placeholder": false - }, - "f024": { - "path": "tests/conftest.py", - "name": "conftest.py", - "content_hash": "5d98b049957b", - "has_branch_placeholder": false - }, - "f015": { - "path": ".aipass/README.md", - "name": "README.md", - "content_hash": "f42d87684fdf", - "has_branch_placeholder": false - }, - "f025": { - "path": ".claude/README.md", - "name": "README.md", - "content_hash": "adb0ce8c53c1", - "has_branch_placeholder": false - }, - "f027": { - "path": ".seedgo/README.md", - "name": "README.md", - "content_hash": "ea03468bbf16", - "has_branch_placeholder": false - }, - "f028": { - "path": ".trinity/README.md", - "name": "README.md", - "content_hash": "f461c9b16fc5", - "has_branch_placeholder": false - }, "f029": { "path": "apps/README.md", "name": "README.md", "content_hash": "92a956009e0e", "has_branch_placeholder": false }, + "f014": { + "path": "apps/__init__.py", + "name": "__init__.py", + "content_hash": "41b011f487af", + "has_branch_placeholder": false + }, "f030": { "path": "apps/handlers/README.md", "name": "README.md", @@ -191,17 +131,23 @@ "content_hash": "a4cf0a8e3b4f", "has_branch_placeholder": false }, + "f033": { + "path": "apps/modules/__init__.py", + "name": "__init__.py", + "content_hash": "e3b0c44298fc", + "has_branch_placeholder": false + }, "f032": { "path": "apps/plugins/README.md", "name": "README.md", "content_hash": "d1e4e2b98c38", "has_branch_placeholder": false }, - "f033": { - "path": "apps/plugins/__init__.py", - "name": "__init__.py", - "content_hash": "e3b0c44298fc", - "has_branch_placeholder": false + "f018": { + "path": "apps/{{BRANCH}}.py", + "name": "{{BRANCH}}.py", + "content_hash": "fc71b424c10b", + "has_branch_placeholder": true }, "f034": { "path": "artifacts/README.md", @@ -209,6 +155,18 @@ "content_hash": "de20d11e5cfd", "has_branch_placeholder": false }, + "f019": { + "path": "artifacts/birth_certificate.json", + "name": "birth_certificate.json", + "content_hash": "1ec401f4e397", + "has_branch_placeholder": false + }, + "f020": { + "path": "docs/README.md", + "name": "README.md", + "content_hash": "2434da568727", + "has_branch_placeholder": false + }, "f035": { "path": "docs.local/README.md", "name": "README.md", @@ -221,12 +179,24 @@ "content_hash": "e3e5a6b9c9c5", "has_branch_placeholder": false }, + "f021": { + "path": "dropbox/README.md", + "name": "README.md", + "content_hash": "9e1e9b71f93b", + "has_branch_placeholder": false + }, "f037": { "path": "logs/README.md", "name": "README.md", "content_hash": "4ca207af6bd3", "has_branch_placeholder": false }, + "f022": { + "path": "pytest.ini", + "name": "pytest.ini", + "content_hash": "7b39ba7bca40", + "has_branch_placeholder": false + }, "f038": { "path": "templates/README.md", "name": "README.md", @@ -239,17 +209,59 @@ "content_hash": "c157895c9b27", "has_branch_placeholder": false }, + "f023": { + "path": "tests/__init__.py", + "name": "__init__.py", + "content_hash": "881f06bb6574", + "has_branch_placeholder": false + }, + "f024": { + "path": "tests/conftest.py", + "name": "conftest.py", + "content_hash": "5d98b049957b", + "has_branch_placeholder": false + }, "f040": { "path": "tools/README.md", "name": "README.md", "content_hash": "3c7eaedb16ac", "has_branch_placeholder": false }, + "f026": { + "path": "{{BRANCH}}_json/README.md", + "name": "README.md", + "content_hash": "e64fa555e7b8", + "has_branch_placeholder": false + }, "f041": { "path": "{{BRANCH}}_json/custom_config/README.md", "name": "README.md", "content_hash": "28e9ae373563", "has_branch_placeholder": false + }, + "f017": { + "path": "apps/handlers/__init__.py", + "name": "__init__.py", + "content_hash": "55afc5eddbe0", + "has_branch_placeholder": false + }, + "f016": { + "path": ".spawn/.registry_ignore.json", + "name": ".registry_ignore.json", + "content_hash": "34f5e7ff7e01", + "has_branch_placeholder": false + }, + "f042": { + "path": ".spawn/README.md", + "name": "README.md", + "content_hash": "e22ad5337efd", + "has_branch_placeholder": false + }, + "f043": { + "path": "apps/plugins/__init__.py", + "name": "__init__.py", + "content_hash": "e3b0c44298fc", + "has_branch_placeholder": false } }, "directories": { @@ -318,11 +330,26 @@ "name": "docs", "has_branch_placeholder": false }, + "d019": { + "path": "docs.local", + "name": "docs.local", + "has_branch_placeholder": false + }, + "d020": { + "path": "docs.local/sub_agent_drops", + "name": "sub_agent_drops", + "has_branch_placeholder": false + }, "d014": { "path": "dropbox", "name": "dropbox", "has_branch_placeholder": false }, + "d021": { + "path": "logs", + "name": "logs", + "has_branch_placeholder": false + }, "d015": { "path": "templates", "name": "templates", @@ -343,25 +370,15 @@ "name": "{{BRANCH}}_json", "has_branch_placeholder": true }, - "d019": { - "path": "docs.local", - "name": "docs.local", - "has_branch_placeholder": false - }, - "d020": { - "path": "docs.local/sub_agent_drops", - "name": "sub_agent_drops", - "has_branch_placeholder": false - }, - "d021": { - "path": "logs", - "name": "logs", - "has_branch_placeholder": false - }, "d022": { "path": "{{BRANCH}}_json/custom_config", "name": "custom_config", "has_branch_placeholder": false + }, + "d023": { + "path": ".spawn", + "name": ".spawn", + "has_branch_placeholder": false } } } diff --git a/src/aipass/spawn/templates/builder/apps/handlers/__init__.py b/src/aipass/spawn/templates/builder/apps/handlers/__init__.py index e69de29b..d8ba28a3 100644 --- a/src/aipass/spawn/templates/builder/apps/handlers/__init__.py +++ b/src/aipass/spawn/templates/builder/apps/handlers/__init__.py @@ -0,0 +1,88 @@ +"""{{BRANCHNAME}} handlers package - Security protected.""" + +import inspect +from pathlib import Path + +MY_BRANCH = "aipass.{{BRANCH}}" + + +def _find_real_caller(): + """Walk the stack to find the actual file that triggered this import. + + Skips this file, importlib internals, and frozen modules. + Returns tuple: (file_path, import_line) or (None, None). + """ + stack = inspect.stack() + this_file = str(Path(__file__).resolve()) + + for frame_info in stack: + filename = frame_info.filename + + if this_file in str(Path(filename).resolve()): + continue + + if filename.startswith("<") or "importlib" in filename: + continue + + import_line = None + if frame_info.code_context: + import_line = frame_info.code_context[0].strip() + + return str(Path(filename).resolve()), import_line + + return None, None + + +def _extract_branch_name(filepath: str) -> str: + """Extract branch name from a file path.""" + parts = Path(filepath).parts + for i, part in enumerate(parts): + if part == "aipass": + if i + 1 < len(parts): + return parts[i + 1] + return "unknown" + + +def _guard_branch_access(): + """Block cross-branch handler imports. + + Only code from within the '{{BRANCH}}' branch can import these handlers. + External branches must use aipass.{{BRANCH}}.apps.modules instead. + """ + caller_file, import_line = _find_real_caller() + + if caller_file is None: + stack = inspect.stack() + for frame in stack: + if frame.filename in ("", ""): + return + return + + branch_path = "/" + MY_BRANCH.replace(".", "/") + "/" + if branch_path in caller_file: + return + + caller_branch = _extract_branch_name(caller_file) + caller_filename = Path(caller_file).name + blocked_import = import_line if import_line else "unknown" + + raise ImportError( + f"\n{'='*60}\n" + f"ACCESS DENIED: Cross-branch handler import blocked\n" + f"{'='*60}\n" + f" Caller branch: {caller_branch}\n" + f" Caller file: {caller_filename}\n" + f" Blocked: {blocked_import}\n" + f"\n" + f" Handlers are internal to their branch.\n" + f" Use the module API instead:\n" + f" from {MY_BRANCH}.apps.modules. import \n" + f"\n" + f" For full standards guide:\n" + f" drone @seedgo handlers\n" + f"{'='*60}" + ) + + +# Run guard at import time +_guard_branch_access() diff --git a/src/aipass/spawn/tests/conftest.py b/src/aipass/spawn/tests/conftest.py index d62a8202..2c162c8f 100644 --- a/src/aipass/spawn/tests/conftest.py +++ b/src/aipass/spawn/tests/conftest.py @@ -1,11 +1,41 @@ """Shared test fixtures for spawn test suite.""" import json +import shutil import pytest from pathlib import Path from unittest.mock import patch, MagicMock +# --------------------------------------------------------------------------- +# Registry backup/restore — prevents test ghost entries in AIPASS_REGISTRY.json +# --------------------------------------------------------------------------- + +def _find_registry_path() -> Path: + """Locate AIPASS_REGISTRY.json from the spawn branch.""" + return Path(__file__).resolve().parents[4] / "AIPASS_REGISTRY.json" + + +@pytest.fixture(autouse=True, scope="session") +def _protect_registry(): + """Backup AIPASS_REGISTRY.json before the test session, restore after. + + Prevents tests that call spawn_agent/grant_passport without a + registry_path override from permanently polluting the real registry. + """ + reg = _find_registry_path() + backup = reg.with_suffix(".json.test_backup") + + if reg.exists(): + shutil.copy2(reg, backup) + + yield + + if backup.exists(): + shutil.copy2(backup, reg) + backup.unlink() + + @pytest.fixture def sample_data(): """Pre-populated JSON test data for spawn operations.""" diff --git a/src/commons/apps/handlers/identity/identity_ops.py b/src/commons/apps/handlers/identity/identity_ops.py index 16326d9f..273fac5e 100644 --- a/src/commons/apps/handlers/identity/identity_ops.py +++ b/src/commons/apps/handlers/identity/identity_ops.py @@ -297,7 +297,7 @@ def extract_mentions(content: str) -> List[str]: conn = get_db() placeholders = ",".join("?" * len(mentioned)) - query = f"SELECT branch_name FROM agents WHERE LOWER(branch_name) IN ({placeholders})" + query = f"SELECT DISTINCT LOWER(branch_name) FROM agents WHERE LOWER(branch_name) IN ({placeholders})" rows = conn.execute(query, mentioned).fetchall() close_db(conn)