diff --git a/.claude/hooks/.diagnostics_state.json b/.claude/hooks/.diagnostics_state.json new file mode 100644 index 00000000..04893a83 --- /dev/null +++ b/.claude/hooks/.diagnostics_state.json @@ -0,0 +1 @@ +{"file": "/home/patrick/Projects/AIPass/src/aipass/seedgo/apps/modules/inbox_audit.py", "errors": [{"line": 86, "message": "E501: Line too long (125 > 120)"}]} \ No newline at end of file diff --git a/src/aipass/ai_mail/.seedgo/bypass.json b/src/aipass/ai_mail/.seedgo/bypass.json index 52985d5c..78422dd4 100644 --- a/src/aipass/ai_mail/.seedgo/bypass.json +++ b/src/aipass/ai_mail/.seedgo/bypass.json @@ -13,12 +13,12 @@ { "file": "apps/handlers/json_utils/json_handler.py", "standard": "json_structure", - "reason": "This IS the json_handler implementation \u2014 cannot import itself." + "reason": "This IS the json_handler implementation — cannot import itself." }, { "file": "apps/modules/dispatch.py", "standard": "deep_nesting", - "reason": "handle_command() depth 4 \u2014 entry point router, nested subcommand dispatch is inherent to the pattern" + "reason": "handle_command() depth 4 — entry point router, nested subcommand dispatch is inherent to the pattern" }, { "file": "apps/handlers/dispatch/daemon.py", @@ -28,17 +28,17 @@ { "file": "apps/handlers/dispatch/wake.py", "standard": "deep_nesting", - "reason": "3 functions: _check_lock() depth 4 (lock validation with PID checks and age), _is_branch_occupied() depth 5 (process inspection, filters by session type \u2014 must check /proc), resolve_branch() depth 4 (AIPass registry scan + caller-registry fallback for cross-project dispatch)" + "reason": "3 functions: _check_lock() depth 4 (lock validation with PID checks and age), _is_branch_occupied() depth 5 (process inspection, filters by session type — must check /proc), resolve_branch() depth 4 (AIPass registry scan + caller-registry fallback for cross-project dispatch)" }, { "file": "apps/handlers/email/send.py", "standard": "deep_nesting", - "reason": "collect_interactive_input() depth 4 \u2014 multiple input() calls each need independent try/except for EOF and KeyboardInterrupt" + "reason": "collect_interactive_input() depth 4 — multiple input() calls each need independent try/except for EOF and KeyboardInterrupt" }, { "file": "apps/handlers/email/inbox_ops.py", "standard": "deep_nesting", - "reason": "load_inbox() depth 4 \u2014 auto-migration between old and new inbox formats requires nested schema checks" + "reason": "load_inbox() depth 4 — auto-migration between old and new inbox formats requires nested schema checks" }, { "file": "apps/handlers/email/delivery.py", @@ -48,32 +48,32 @@ { "file": "apps/handlers/email/inbox_cleanup.py", "standard": "deep_nesting", - "reason": "mark_read_and_archive() depth 4 \u2014 lock context manager + inbox loading + message search + archive" + "reason": "mark_read_and_archive() depth 4 — lock context manager + inbox loading + message search + archive" }, { "file": "apps/handlers/email/inbox_lock.py", "standard": "deep_nesting", - "reason": "inbox_lock() depth 6 \u2014 cross-platform file locking context manager (POSIX fcntl vs Windows msvcrt), platform branching is inherent" + "reason": "inbox_lock() depth 6 — cross-platform file locking context manager (POSIX fcntl vs Windows msvcrt), platform branching is inherent" }, { "file": "apps/handlers/users/user.py", "standard": "deep_nesting", - "reason": "2 functions: get_user_by_email() depth 4, get_all_users() depth 4 \u2014 registry lookup with path normalization and validation" + "reason": "2 functions: get_user_by_email() depth 4, get_all_users() depth 4 — registry lookup with path normalization and validation" }, { "file": "apps/handlers/email/dashboard_sync.py", "standard": "handlers", - "reason": "Imports prax.apps.modules.dashboard.write_section \u2014 cross-branch module import required for dashboard integration. No ai_mail module wraps this." + "reason": "Imports prax.apps.modules.dashboard.write_section — cross-branch module import required for dashboard integration. No ai_mail module wraps this." }, { "file": "apps/handlers/email/delivery.py", "standard": "handlers", - "reason": "Imports json_utils.json_handler (load_json, save_json) \u2014 shared handler utility for JSON I/O, same-branch cross-handler import. Also imports registry.read.get_all_branches \u2014 consolidated from dual implementation per DPLAN-0036. Also lazy-imports email.contacts.register_contact inside _auto_register_contact() and _auto_register_sender() for post-delivery contact registration (DPLAN-0121 Phase 5). Also lazy-imports registry.read.get_caller_project_branches inside _load_caller_project_branches() to delegate to shared cross-project registry implementation (issue #283)." + "reason": "Imports json_utils.json_handler (load_json, save_json) — shared handler utility for JSON I/O, same-branch cross-handler import. Also imports registry.read.get_all_branches — consolidated from dual implementation per DPLAN-0036. Also lazy-imports email.contacts.register_contact inside _auto_register_contact() and _auto_register_sender() for post-delivery contact registration (DPLAN-0121 Phase 5). Also lazy-imports registry.read.get_caller_project_branches inside _load_caller_project_branches() to delegate to shared cross-project registry implementation (issue #283)." }, { "file": "apps/handlers/email/inbox_cleanup.py", "standard": "handlers", - "reason": "Imports central_writer.update_central \u2014 same-branch cross-handler import for registry status updates on inbox changes." + "reason": "Imports central_writer.update_central — same-branch cross-handler import for registry status updates on inbox changes." }, { "file": "apps/handlers/email/reply.py", @@ -83,112 +83,112 @@ { "file": "apps/handlers/email/send.py", "standard": "handlers", - "reason": "Imports trigger.apps.modules.core.trigger \u2014 cross-branch module import for firing email_sent events. trigger integration is by design." + "reason": "Imports trigger.apps.modules.core.trigger — cross-branch module import for firing email_sent events. trigger integration is by design." }, { "file": "apps/handlers/dispatch/daemon.py", "standard": "handlers", - "reason": "Imports notify.send_notification \u2014 same-branch cross-handler import for desktop notifications on dispatch events." + "reason": "Imports notify.send_notification — same-branch cross-handler import for desktop notifications on dispatch events." }, { "file": "apps/handlers/dispatch/dispatch_monitor.py", "standard": "handlers", - "reason": "Imports notify.send_notification \u2014 same-branch cross-handler import for bounce/completion notifications." + "reason": "Imports notify.send_notification — same-branch cross-handler import for bounce/completion notifications." }, { "file": "apps/handlers/dispatch/wake.py", "standard": "handlers", - "reason": "Imports notify.send_notification \u2014 same-branch cross-handler import for wake completion notifications. Lazy-imports registry.read.get_caller_project_branches inside resolve_branch() for cross-project branch resolution. Same cross-handler pattern as reply.py, delivery.py, and branch_detection.py." + "reason": "Imports notify.send_notification — same-branch cross-handler import for wake completion notifications. Lazy-imports registry.read.get_caller_project_branches inside resolve_branch() for cross-project branch resolution. Same cross-handler pattern as reply.py, delivery.py, and branch_detection.py." }, { "file": "apps/handlers/dispatch/dispatch_monitor.py", "standard": "naming", - "reason": "dispatch_monitor.py in dispatch/ dir \u2014 renaming to monitor.py would break all 3 callers (wake.py, daemon.py, MONITOR_SCRIPT references) and confuse with prax monitor." + "reason": "dispatch_monitor.py in dispatch/ dir — renaming to monitor.py would break all 3 callers (wake.py, daemon.py, MONITOR_SCRIPT references) and confuse with prax monitor." }, { "file": "apps/handlers/email/close_ops.py", "standard": "naming", - "reason": "False positive \u2014 results, closed_count, failed_count are local variables, not module-level constants." + "reason": "False positive — results, closed_count, failed_count are local variables, not module-level constants." }, { "file": "apps/handlers/email/create.py", "standard": "naming", - "reason": "False positive \u2014 _append_footer is a function reference stored in a local variable, not a module-level constant." + "reason": "False positive — _append_footer is a function reference stored in a local variable, not a module-level constant." }, { "file": "apps/handlers/email/dashboard_sync.py", "standard": "naming", - "reason": "False positive \u2014 _write_section is a lazy-import function reference, not a module-level constant." + "reason": "False positive — _write_section is a lazy-import function reference, not a module-level constant." }, { "file": "apps/handlers/email/delivery.py", "standard": "naming", - "reason": "False positive \u2014 path_to_email, to_branch, matched are local variables, not module-level constants." + "reason": "False positive — path_to_email, to_branch, matched are local variables, not module-level constants." }, { "file": "apps/handlers/email/inbox_cleanup.py", "standard": "naming", - "reason": "False positive \u2014 _inbox_lock is a lazy-import function reference, not a module-level constant." + "reason": "False positive — _inbox_lock is a lazy-import function reference, not a module-level constant." }, { "file": "apps/handlers/email/inbox_ops.py", "standard": "naming", - "reason": "False positive \u2014 _inbox_lock is a lazy-import function reference, not a module-level constant." + "reason": "False positive — _inbox_lock is a lazy-import function reference, not a module-level constant." }, { "file": "apps/handlers/email/inbox_resolve.py", "standard": "naming", - "reason": "False positive \u2014 target_branch, mailbox_path are local variables, not module-level constants." + "reason": "False positive — target_branch, mailbox_path are local variables, not module-level constants." }, { "file": "apps/handlers/email/reply.py", "standard": "naming", - "reason": "False positive \u2014 error_msg, reply_email_data, target_branch are local variables, not module-level constants." + "reason": "False positive — error_msg, reply_email_data, target_branch are local variables, not module-level constants." }, { "file": "apps/handlers/dispatch/daemon.py", "standard": "naming", - "reason": "False positive \u2014 claude_cmd, monitor_cmd, stdout are local variables in spawn_agent(), not module-level constants." + "reason": "False positive — claude_cmd, monitor_cmd, stdout are local variables in spawn_agent(), not module-level constants." }, { "file": "apps/handlers/email/close_ops.py", "standard": "documentation", - "reason": "batch_close() has a full docstring \u2014 seedgo AST detection mismatch on multiline function signature." + "reason": "batch_close() has a full docstring — seedgo AST detection mismatch on multiline function signature." }, { "file": "apps/handlers/email/delivery.py", "standard": "documentation", - "reason": "deliver_email_to_branch() has a full docstring \u2014 seedgo AST detection mismatch on multiline function signature." + "reason": "deliver_email_to_branch() has a full docstring — seedgo AST detection mismatch on multiline function signature." }, { "file": "apps/handlers/email/error_dispatch.py", "standard": "documentation", - "reason": "dispatch_send_error() has a full docstring \u2014 seedgo AST detection mismatch on multiline function signature." + "reason": "dispatch_send_error() has a full docstring — seedgo AST detection mismatch on multiline function signature." }, { "file": "apps/handlers/email/inbox_resolve.py", "standard": "documentation", - "reason": "resolve_inbox_target() has a full docstring \u2014 seedgo AST detection mismatch on multiline function signature." + "reason": "resolve_inbox_target() has a full docstring — seedgo AST detection mismatch on multiline function signature." }, { "file": "apps/handlers/email/reply.py", "standard": "documentation", - "reason": "send_reply() has a full docstring \u2014 seedgo AST detection mismatch on multiline function signature." + "reason": "send_reply() has a full docstring — seedgo AST detection mismatch on multiline function signature." }, { "file": "apps/handlers/email/send.py", "standard": "documentation", - "reason": "resolve_sender_info() has a full docstring \u2014 seedgo AST detection mismatch on multiline function signature." + "reason": "resolve_sender_info() has a full docstring — seedgo AST detection mismatch on multiline function signature." }, { "file": "apps/handlers/email/send_args.py", "standard": "documentation", - "reason": "resolve_dispatch_target() has a full docstring \u2014 seedgo AST detection mismatch on multiline function signature." + "reason": "resolve_dispatch_target() has a full docstring — seedgo AST detection mismatch on multiline function signature." }, { "file": "apps/handlers/dispatch/daemon.py", "standard": "documentation", - "reason": "spawn_agent() has a full docstring \u2014 seedgo AST detection mismatch on multiline function signature." + "reason": "spawn_agent() has a full docstring — seedgo AST detection mismatch on multiline function signature." }, { "file": "apps/modules/email.py", @@ -198,52 +198,52 @@ { "file": "apps/handlers/email/dashboard_sync.py", "standard": "deep_nesting", - "reason": "_human_readable_age() depth 5, _calculate_section_data() depth 5 \u2014 timestamp parsing with multiple fallback formats" + "reason": "_human_readable_age() depth 5, _calculate_section_data() depth 5 — timestamp parsing with multiple fallback formats" }, { "file": "apps/handlers/dispatch/dispatch_monitor.py", "standard": "deep_nesting", - "reason": "_send_bounce() depth 4, main() depth 5 \u2014 subprocess management with error handling and log rotation" + "reason": "_send_bounce() depth 4, main() depth 5 — subprocess management with error handling and log rotation" }, { "file": "apps/handlers/dispatch/status.py", "standard": "deep_nesting", - "reason": "calculate_age() depth 4 \u2014 timestamp parsing with multiple fallback formats" + "reason": "calculate_age() depth 4 — timestamp parsing with multiple fallback formats" }, { "file": "apps/handlers/central_writer.py", "standard": "handlers", - "reason": "Imports paths.find_repo_root \u2014 shared utility consolidated from 8 copies per DPLAN-0036." + "reason": "Imports paths.find_repo_root — shared utility consolidated from 8 copies per DPLAN-0036." }, { "file": "apps/handlers/registry/read.py", "standard": "handlers", - "reason": "Imports paths.find_repo_root \u2014 shared utility consolidated from 8 copies per DPLAN-0036." + "reason": "Imports paths.find_repo_root — shared utility consolidated from 8 copies per DPLAN-0036." }, { "file": "apps/handlers/registry/read.py", "standard": "deep_nesting", - "reason": "get_caller_project_branches() depth 4 \u2014 walks directory tree with per-registry-file parsing, list/dict branch format detection, and path resolution. Same pattern as delivery.py _load_caller_project_branches()." + "reason": "get_caller_project_branches() depth 4 — walks directory tree with per-registry-file parsing, list/dict branch format detection, and path resolution. Same pattern as delivery.py _load_caller_project_branches()." }, { "file": "apps/handlers/email/format.py", "standard": "handlers", - "reason": "Imports paths.find_repo_root \u2014 shared utility consolidated from 8 copies per DPLAN-0036." + "reason": "Imports paths.find_repo_root — shared utility consolidated from 8 copies per DPLAN-0036." }, { "file": "apps/handlers/email/purge.py", "standard": "handlers", - "reason": "Imports paths.find_repo_root \u2014 shared utility consolidated from 8 copies per DPLAN-0036." + "reason": "Imports paths.find_repo_root — shared utility consolidated from 8 copies per DPLAN-0036." }, { "file": "apps/handlers/users/branch_detection.py", "standard": "handlers", - "reason": "Imports paths.find_repo_root \u2014 shared utility consolidated from 8 copies per DPLAN-0036. Also lazy-imports email.contacts.get_contact inside _get_contact_info() for contacts-first sender detection (DPLAN-0121 Phase 5)." + "reason": "Imports paths.find_repo_root — shared utility consolidated from 8 copies per DPLAN-0036. Also lazy-imports email.contacts.get_contact inside _get_contact_info() for contacts-first sender detection (DPLAN-0121 Phase 5)." }, { "file": "apps/handlers/email/contacts.py", "standard": "handlers", - "reason": "Imports paths.find_repo_root \u2014 shared utility for CONTACTS_FILE path resolution. Same pattern as registry/read.py and other handlers per DPLAN-0036." + "reason": "Imports paths.find_repo_root — shared utility for CONTACTS_FILE path resolution. Same pattern as registry/read.py and other handlers per DPLAN-0036." }, { "file": "apps/handlers/email/identity.py", @@ -253,7 +253,7 @@ { "file": "tests/test_delivery.py", "standard": "architecture", - "reason": "Test file lives in tests/ directory \u2014 not subject to 3-layer app structure rule." + "reason": "Test file lives in tests/ directory — not subject to 3-layer app structure rule." }, { "file": "tests/test_delivery.py", @@ -263,7 +263,7 @@ { "file": "tests/test_send_identity.py", "standard": "architecture", - "reason": "Test file lives in tests/ directory \u2014 not subject to 3-layer app structure rule." + "reason": "Test file lives in tests/ directory — not subject to 3-layer app structure rule." }, { "file": "tests/test_send_identity.py", @@ -273,12 +273,12 @@ { "file": "tests/test_send_identity.py", "standard": "documentation", - "reason": "Test helper functions (_load_active_source, fixtures) are private/internal \u2014 docstring requirement does not apply to test infrastructure." + "reason": "Test helper functions (_load_active_source, fixtures) are private/internal — docstring requirement does not apply to test infrastructure." }, { "file": "tests/test_contacts.py", "standard": "architecture", - "reason": "Test file lives in tests/ directory \u2014 not subject to 3-layer app structure rule." + "reason": "Test file lives in tests/ directory — not subject to 3-layer app structure rule." }, { "file": "tests/test_contacts.py", @@ -288,7 +288,7 @@ { "file": "tests/test_identity.py", "standard": "architecture", - "reason": "Test file lives in tests/ directory \u2014 not subject to 3-layer app structure rule." + "reason": "Test file lives in tests/ directory — not subject to 3-layer app structure rule." }, { "file": "tests/test_identity.py", @@ -298,7 +298,7 @@ { "file": "tests/test_registry_read.py", "standard": "architecture", - "reason": "Test file lives in tests/ directory \u2014 not subject to 3-layer app structure rule." + "reason": "Test file lives in tests/ directory — not subject to 3-layer app structure rule." }, { "file": "tests/test_registry_read.py", @@ -308,12 +308,12 @@ { "file": "tests/test_registry_read.py", "standard": "documentation", - "reason": "Test fixture functions (registry_file) are private test infrastructure \u2014 docstring requirement does not apply to pytest fixtures." + "reason": "Test fixture functions (registry_file) are private test infrastructure — docstring requirement does not apply to pytest fixtures." }, { "file": "tests/test_wake.py", "standard": "architecture", - "reason": "Test file lives in tests/ directory \u2014 not subject to 3-layer app structure rule." + "reason": "Test file lives in tests/ directory — not subject to 3-layer app structure rule." }, { "file": "tests/test_wake.py", @@ -323,22 +323,12 @@ { "file": "tests/test_wake.py", "standard": "documentation", - "reason": "Test helper functions (_fake_open_factory, _raise_process_lookup, repo_root fixture) are private test infrastructure \u2014 docstring requirement does not apply to test helpers." - }, - { - "file": "tests/test_dispatch_watchdog.py", - "standard": "architecture", - "reason": "Test file lives in tests/ directory \u2014 not subject to 3-layer app structure rule." - }, - { - "file": "tests/test_dispatch_watchdog.py", - "standard": "encapsulation", - "reason": "Unit tests must access _spawn_watchdog directly to verify internal spawn behavior. Module entry-point-only rule does not apply to tests." + "reason": "Test helper functions (_fake_open_factory, _raise_process_lookup, repo_root fixture) are private test infrastructure — docstring requirement does not apply to test helpers." }, { "file": "tests/test_wake_blocklist.py", "standard": "architecture", - "reason": "Test file lives in tests/ directory \u2014 not subject to 3-layer app structure rule." + "reason": "Test file lives in tests/ directory — not subject to 3-layer app structure rule." }, { "file": "tests/test_wake_blocklist.py", @@ -348,7 +338,7 @@ { "file": "tests/test_daemon.py", "standard": "architecture", - "reason": "Test file lives in tests/ directory \u2014 not subject to 3-layer app structure rule." + "reason": "Test file lives in tests/ directory — not subject to 3-layer app structure rule." }, { "file": "tests/test_daemon.py", diff --git a/src/aipass/ai_mail/apps/modules/dispatch.py b/src/aipass/ai_mail/apps/modules/dispatch.py index 64f94bcd..4da05121 100644 --- a/src/aipass/ai_mail/apps/modules/dispatch.py +++ b/src/aipass/ai_mail/apps/modules/dispatch.py @@ -13,7 +13,6 @@ Orchestrates dispatch commands: status tracking and daemon management. Delegates all business logic to handlers. """ -import subprocess import sys from pathlib import Path from typing import List @@ -36,9 +35,8 @@ COMMANDS: dispatch wake @branch - Wake only (no email sent) DISPATCH (send + wake): - drone @ai_mail dispatch @branch "Subject" "Body" # Send + wake + watchdog - drone @ai_mail dispatch @branch "Subject" "Body" --no-watchdog # Send + wake, no watchdog - drone @ai_mail dispatch @branch "Subject" "Body" --fresh # Send + fresh wake + watchdog + drone @ai_mail dispatch @branch "Subject" "Body" # Send + wake + drone @ai_mail dispatch @branch "Subject" "Body" --fresh # Send + fresh wake drone @ai_mail dispatch @branch "Subject" "Body" --model opus # Send + wake with Opus drone @ai_mail dispatch @branch "Subject" "Body" --no-memory-save @@ -213,49 +211,11 @@ def _orchestrate_wake(args: List[str]) -> bool: return success -def _spawn_watchdog(target: str, repo_root: Path) -> bool: - """Detach a watchdog process to wake devpulse when the dispatched agent exits. - - Spawns drone @devpulse watchdog agent with cwd=devpulse branch path - so _guard_caller() accepts the cross-branch invocation. The process is fully - detached (start_new_session=True) so dispatch returns immediately. - - Returns True if watchdog was spawned successfully. - """ - from aipass.ai_mail.apps.handlers.registry.read import get_branch_by_email - - branch_info = get_branch_by_email("@devpulse") - if not branch_info: - logger.warning("[dispatch] watchdog auto-spawn: devpulse not found in registry") - return False - p = Path(branch_info.get("path", "")) - devpulse_path = p if p.is_absolute() else (repo_root / p) - - if not devpulse_path.exists(): - logger.warning("[dispatch] watchdog auto-spawn: devpulse path not found on disk") - return False - - try: - proc = subprocess.Popen( - ["drone", "@devpulse", "watchdog", "agent", target], - cwd=str(devpulse_path), - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - start_new_session=True, - ) - logger.info("[dispatch] watchdog auto-spawned for %s (PID %d)", target, proc.pid) - return True - except (FileNotFoundError, OSError) as e: - logger.warning("[dispatch] watchdog auto-spawn failed for %s: %s", target, e) - return False - - def _orchestrate_dispatch_send(args: List[str]) -> bool: """Orchestrate combined dispatch: send email with --dispatch flag + wake branch.""" # Parse flags use_fresh = False no_memory_save = False - no_watchdog = False from_branch = None use_model = None filtered = [] @@ -269,10 +229,6 @@ def _orchestrate_dispatch_send(args: List[str]) -> bool: no_memory_save = True i += 1 continue - if args[i] == "--no-watchdog": - no_watchdog = True - i += 1 - continue if args[i] == "--from" and i + 1 < len(args): from_branch = args[i + 1] i += 2 @@ -380,13 +336,6 @@ def _orchestrate_dispatch_send(args: List[str]) -> bool: logger.warning("[dispatch] Wake failed for %s — email was sent", target) error(f"Email sent but wake failed — retry: drone @ai_mail dispatch wake {target}") - # --- Step 3: Auto-spawn watchdog (skipped if wake failed or --no-watchdog) --- - if wake_ok and not no_watchdog: - if _spawn_watchdog(target, _repo_root): - console.print(f"[dim]Watchdog armed for {target}[/dim]") - else: - logger.info("[dispatch] Watchdog auto-spawn skipped (devpulse not found or spawn failed)") - return True diff --git a/src/aipass/ai_mail/tests/test_dispatch_watchdog.py b/src/aipass/ai_mail/tests/test_dispatch_watchdog.py deleted file mode 100644 index 61916d87..00000000 --- a/src/aipass/ai_mail/tests/test_dispatch_watchdog.py +++ /dev/null @@ -1,96 +0,0 @@ -# =================== AIPass ==================== -# Name: test_dispatch_watchdog.py -# Description: Tests for watchdog auto-spawn in dispatch pipeline -# Version: 1.1.0 -# Created: 2026-04-19 -# Modified: 2026-04-22 -# ============================================= - -"""Tests for _spawn_watchdog() — watchdog auto-spawn in dispatch pipeline.""" - -from unittest.mock import patch, MagicMock -import pytest - -import aipass.ai_mail.apps.modules.dispatch as dispatch_mod - - -_spawn_watchdog = getattr(dispatch_mod, "_spawn_watchdog") - -_POPEN_PATH = "aipass.ai_mail.apps.modules.dispatch.subprocess.Popen" -_GET_BRANCH = "aipass.ai_mail.apps.handlers.registry.read.get_branch_by_email" - - -@pytest.fixture(autouse=True) -def _suppress_log_operation(monkeypatch): - """Prevent json_handler.log_operation from touching real files.""" - monkeypatch.setattr( - "aipass.ai_mail.apps.modules.dispatch.json_handler.log_operation", - lambda *a, **kw: None, - ) - - -class TestSpawnWatchdog: - """Tests for _spawn_watchdog() — registry lookup + detached Popen.""" - - def test_spawns_with_devpulse_cwd(self, tmp_path): - """_spawn_watchdog sets cwd=devpulse_path when spawning.""" - devpulse_path = tmp_path / "devpulse" - devpulse_path.mkdir() - - fake_proc = MagicMock() - fake_proc.pid = 42 - - with ( - patch(_GET_BRANCH, return_value={"path": str(devpulse_path), "email": "@devpulse"}), - patch(_POPEN_PATH, return_value=fake_proc) as mock_popen, - ): - result = _spawn_watchdog("@drone", tmp_path) - - assert result is True - mock_popen.assert_called_once() - call_kwargs = mock_popen.call_args - assert call_kwargs[0][0] == ["drone", "@devpulse", "watchdog", "agent", "@drone"] - assert call_kwargs[1]["cwd"] == str(devpulse_path) - assert call_kwargs[1]["start_new_session"] is True - - def test_returns_false_when_devpulse_not_in_registry(self, tmp_path): - """Returns False when devpulse not found in registry.""" - with patch(_GET_BRANCH, return_value=None): - result = _spawn_watchdog("@drone", tmp_path) - assert result is False - - def test_returns_false_when_devpulse_path_missing(self, tmp_path): - """Returns False when devpulse path from registry does not exist on disk.""" - with patch(_GET_BRANCH, return_value={"path": str(tmp_path / "nonexistent"), "email": "@devpulse"}): - result = _spawn_watchdog("@drone", tmp_path) - assert result is False - - def test_returns_false_when_drone_not_found(self, tmp_path): - """Returns False when 'drone' binary not on PATH (FileNotFoundError).""" - devpulse_path = tmp_path / "devpulse" - devpulse_path.mkdir() - - with ( - patch(_GET_BRANCH, return_value={"path": str(devpulse_path), "email": "@devpulse"}), - patch(_POPEN_PATH, side_effect=FileNotFoundError("drone not found")), - ): - result = _spawn_watchdog("@drone", tmp_path) - - assert result is False - - def test_resolves_relative_path_in_registry(self, tmp_path): - """Resolves relative devpulse path relative to repo_root.""" - devpulse_path = tmp_path / "src" / "devpulse" - devpulse_path.mkdir(parents=True) - - fake_proc = MagicMock() - fake_proc.pid = 99 - - with ( - patch(_GET_BRANCH, return_value={"path": "src/devpulse", "email": "@devpulse"}), - patch(_POPEN_PATH, return_value=fake_proc) as mock_popen, - ): - result = _spawn_watchdog("@flow", tmp_path) - - assert result is True - assert mock_popen.call_args[1]["cwd"] == str(devpulse_path) diff --git a/src/aipass/api/CLAUDE.md b/src/aipass/api/CLAUDE.md deleted file mode 100644 index aee385a8..00000000 --- a/src/aipass/api/CLAUDE.md +++ /dev/null @@ -1,42 +0,0 @@ -# API - -## Startup - -On any greeting, silently read these files and run the commands — no narration, no announcing steps. Just do it and respond with the status. - -**Read:** `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `README.md`, `STATUS.local.md` -**Check:** If `.ai_mail.local/inbox.json` exists, read it. Process any mail. -**Run:** `git status` - -## Identity - -You are **API** — an AIPass citizen. - -- **Module:** `aipass.api` -- **Role:** api_gateway -- **Purpose:** Centralized external API gateway — authenticated service clients for all external APIs - -## Memories - -Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`. - -- `local.json` — Session history, key learnings, active tasks -- `observations.json` — Collaboration patterns, insights -- `passport.json` — Identity (rarely changes) - -## AIPass Context - -This branch is part of the AIPass multi-agent framework. Key concepts: - -- **Branch** — your directory (`src/aipass/api/`). Your home. -- **Citizen** — the identity that lives in a branch. Has a passport, memories, mailbox. -- **Agent** — a disposable worker spawned for a task. No passport, no memory. - -## Commands - -``` -drone systems # List available infrastructure -drone @ai_mail inbox # Check mailbox -drone @ai_mail send @branch "Subject" "Body" # Send mail -drone @seedgo audit @api # Run standards audit -``` diff --git a/src/aipass/cli/CLAUDE.md b/src/aipass/cli/CLAUDE.md deleted file mode 100644 index dc56a1bd..00000000 --- a/src/aipass/cli/CLAUDE.md +++ /dev/null @@ -1,42 +0,0 @@ -# CLI - -## Startup - -On any greeting, silently read these files and run the commands — no narration, no announcing steps. Just do it and respond with the status. - -**Read:** `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `README.md`, `STATUS.local.md` -**Check:** If `.ai_mail.local/inbox.json` exists, read it. Process any mail. -**Run:** `git status` - -## Identity - -You are **CLI** — an AIPass citizen. - -- **Module:** `aipass.cli` -- **Role:** display_formatter -- **Purpose:** Display formatting with Rich, project bootstrapping via aipass init - -## Memories - -Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`. - -- `local.json` — Session history, key learnings, active tasks -- `observations.json` — Collaboration patterns, insights -- `passport.json` — Identity (rarely changes) - -## AIPass Context - -This branch is part of the AIPass multi-agent framework. Key concepts: - -- **Branch** — your directory (`src/aipass/cli/`). Your home. -- **Citizen** — the identity that lives in a branch. Has a passport, memories, mailbox. -- **Agent** — a disposable worker spawned for a task. No passport, no memory. - -## Commands - -``` -drone systems # List available infrastructure -drone @ai_mail inbox # Check mailbox -drone @ai_mail send @branch "Subject" "Body" # Send mail -drone @seedgo audit @cli # Run standards audit -``` diff --git a/src/aipass/cli/apps/handlers/init/bootstrap.py b/src/aipass/cli/apps/handlers/init/bootstrap.py index d7614eac..b0e09e2e 100644 --- a/src/aipass/cli/apps/handlers/init/bootstrap.py +++ b/src/aipass/cli/apps/handlers/init/bootstrap.py @@ -380,6 +380,14 @@ def update_project(target: Path) -> dict: """ target = target.resolve() + # Guard: refuse to update the AIPass source repo itself. The source repo + # has hand-maintained production files that must not be overwritten with + # generic templates. External projects created via `aipass init` are fine. + if (target / "src" / "aipass").is_dir() and (target / "pyproject.toml").exists(): + raise ValueError( + "Cannot update the AIPass source repository — its files are hand-maintained, not template-generated" + ) + # Locate the project registry to confirm this is an AIPass project and # derive the project name without parsing JSON (filename encodes the name). registry_files = list(target.glob("*_REGISTRY.json")) diff --git a/src/aipass/devpulse/README.md b/src/aipass/devpulse/README.md index 467dbbe5..8a79fb1e 100644 --- a/src/aipass/devpulse/README.md +++ b/src/aipass/devpulse/README.md @@ -67,11 +67,13 @@ Devpulse commands are accessed via `drone @devpulse `: - `feedback reply "msg"` — reply to sender - `feedback send "subject" "body"` — receive feedback from another agent -## Integration +## Integration Points -**Depends on:** drone (routing), prax (logging), cli (display), ai_mail (dispatch), seedgo (audits), flow (plans) +### Depends On +drone (routing), prax (logging), cli (display), ai_mail (dispatch), seedgo (audits), flow (plans) -**Provides to:** All branches via dispatch orchestration. Watchdog monitoring for any dispatched agent. Feedback channel for cross-project communication. +### Provides To +All branches via dispatch orchestration. Watchdog monitoring for any dispatched agent. Feedback channel for cross-project communication. *Last updated: 2026-04-22* diff --git a/src/aipass/flow/CLAUDE.md b/src/aipass/flow/CLAUDE.md deleted file mode 100644 index 77d84fc7..00000000 --- a/src/aipass/flow/CLAUDE.md +++ /dev/null @@ -1,42 +0,0 @@ -# flow - -## Startup - -On any greeting, silently read these files and run the commands — no narration, no announcing steps. Just do it and respond with the status. - -**Read:** `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `README.md`, `STATUS.local.md` -**Check:** If `.ai_mail.local/inbox.json` exists, read it. Process any mail. -**Run:** `git status` - -## Identity - -You are **flow** — an AIPass citizen. - -- **Module:** `aipass.flow` -- **Role:** workflow_planner -- **Purpose:** Workflow planning and tracking - -## Memories - -Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`. - -- `local.json` — Session history, key learnings, active tasks -- `observations.json` — Collaboration patterns, insights -- `passport.json` — Identity (rarely changes) - -## AIPass Context - -This branch is part of the AIPass multi-agent framework. Key concepts: - -- **Branch** — your directory (`src/aipass/flow/`). Your home. -- **Citizen** — the identity that lives in a branch. Has a passport, memories, mailbox. -- **Agent** — a disposable worker spawned for a task. No passport, no memory. - -## Commands - -``` -drone systems # List available infrastructure -drone @ai_mail inbox # Check mailbox -drone @ai_mail send @branch "Subject" "Body" # Send mail -drone @seedgo audit @flow # Run standards audit -``` diff --git a/src/aipass/memory/apps/handlers/monitor/memory_watcher.py b/src/aipass/memory/apps/handlers/monitor/memory_watcher.py index b84174b5..6427225b 100644 --- a/src/aipass/memory/apps/handlers/monitor/memory_watcher.py +++ b/src/aipass/memory/apps/handlers/monitor/memory_watcher.py @@ -91,10 +91,10 @@ def _get_rollover_threshold(branch_name: str, file_path: Path | None = None) -> data = json.load(f) metadata = data.get("document_metadata", {}) - # v2 files use entry-count limits, not line limits + # v2 files use entry-count limits — return -1 so caller uses detector schema_version = metadata.get("schema_version", "1.0.0") if schema_version.startswith("2"): - return 999999 # Never trigger line-based rollover for v2 + return -1 file_limit = metadata.get("limits", {}).get("max_lines") if file_limit is not None: @@ -204,9 +204,6 @@ def check_and_rollover() -> Dict[str, Any]: lines_synced = 0 for branch_path in branch_paths: branch = Path(branch_path) - # Extract branch name from path (last component, uppercase) - branch_name = branch.name.upper() - # Find memory files in .trinity/ subdirectory trinity_dir = branch / ".trinity" if not trinity_dir.exists(): @@ -215,9 +212,6 @@ def check_and_rollover() -> Dict[str, Any]: for memory_file in trinity_dir.glob(pattern): results["files_checked"] += 1 - # Get threshold per file (file metadata > branch config > default) - threshold = _get_rollover_threshold(branch_name, memory_file) - try: line_count = len(memory_file.read_text(encoding="utf-8").splitlines()) @@ -231,14 +225,16 @@ def check_and_rollover() -> Dict[str, Any]: sync_result = update_line_count(memory_file) if sync_result.get("success"): lines_synced += 1 - # Re-read actual line count after metadata update - line_count = len(memory_file.read_text(encoding="utf-8").splitlines()) except Exception as e: logger.warning(f"[memory_watcher] Non-critical metadata sync failed for {memory_file}: {e}") - if line_count > threshold: + # Use detector for trigger decision (handles both v1 line-based and v2 entry-count) + from aipass.memory.apps.handlers.monitor.detector import _should_rollover + + triggered, _, _, _, _ = _should_rollover(memory_file) + if triggered: results["files_over_limit"].append( - {"file": str(memory_file), "lines": line_count, "threshold": threshold} + {"file": str(memory_file), "lines": line_count, "threshold": 0} ) except Exception as e: logger.warning(f"[memory_watcher] Failed to read memory file {memory_file}: {e}") diff --git a/src/aipass/memory/templates/.template_version.json b/src/aipass/memory/templates/.template_version.json new file mode 100644 index 00000000..918795a9 --- /dev/null +++ b/src/aipass/memory/templates/.template_version.json @@ -0,0 +1,16 @@ +{ + "last_push": "2026-04-22 20:07:31", + "last_push_branches": [ + "AI_MAIL", + "API", + "CLI", + "DEVPULSE", + "DRONE", + "FLOW", + "MEMORY", + "PRAX", + "SEEDGO", + "SPAWN", + "TRIGGER" + ] +} diff --git a/src/aipass/seedgo/CLAUDE.md b/src/aipass/seedgo/CLAUDE.md deleted file mode 100644 index c804fb82..00000000 --- a/src/aipass/seedgo/CLAUDE.md +++ /dev/null @@ -1,42 +0,0 @@ -# SEEDGO - -## Startup - -On any greeting, silently read these files and run the commands — no narration, no announcing steps. Just do it and respond with the status. - -**Read:** `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `README.md`, `STATUS.local.md` -**Check:** If `.ai_mail.local/inbox.json` exists, read it. Process any mail. -**Run:** `git status` - -## Identity - -You are **SEEDGO** — an AIPass citizen. - -- **Module:** `aipass.seedgo` -- **Role:** Standards Auditor -- **Purpose:** Standards compliance through pluggable checker packs - -## Memories - -Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`. - -- `local.json` — Session history, key learnings, active tasks -- `observations.json` — Collaboration patterns, insights -- `passport.json` — Identity (rarely changes) - -## AIPass Context - -This branch is part of the AIPass multi-agent framework. Key concepts: - -- **Branch** — your directory (`src/aipass/seedgo/`). Your home. -- **Citizen** — the identity that lives in a branch. Has a passport, memories, mailbox. -- **Agent** — a disposable worker spawned for a task. No passport, no memory. - -## Commands - -``` -drone systems # List available infrastructure -drone @ai_mail inbox # Check mailbox -drone @ai_mail send @branch "Subject" "Body" # Send mail -drone @seedgo audit @seedgo # Run standards audit on self -``` diff --git a/src/aipass/seedgo/apps/handlers/bypass/bypass_handler.py b/src/aipass/seedgo/apps/handlers/bypass/bypass_handler.py index 61cb9e40..a30c265b 100644 --- a/src/aipass/seedgo/apps/handlers/bypass/bypass_handler.py +++ b/src/aipass/seedgo/apps/handlers/bypass/bypass_handler.py @@ -57,18 +57,20 @@ BYPASS_TEMPLATE = { def _find_registry() -> Path: - """Find AIPASS_REGISTRY.json by walking up from this file's location.""" + """Find *_REGISTRY.json — CWD-first for external project support, then __file__ fallback.""" + cwd = Path.cwd() + for parent in [cwd] + list(cwd.parents): + matches = sorted(parent.glob("*_REGISTRY.json")) + if matches: + return matches[0] current = Path(__file__).resolve().parent for parent in [current] + list(current.parents): - candidate = parent / "AIPASS_REGISTRY.json" - if candidate.exists(): - return candidate + matches = sorted(parent.glob("*_REGISTRY.json")) + if matches: + return matches[0] return Path.cwd() / "AIPASS_REGISTRY.json" -REGISTRY_PATH = _find_registry() - - # ============================================================================= # PUBLIC API # ============================================================================= @@ -85,16 +87,17 @@ def get_branch_from_path(file_path: str) -> Optional[Dict[str, Any]]: Branch dict with name, path, etc. or None if not in a branch """ try: - if not REGISTRY_PATH.exists(): + registry_path = _find_registry() + if not registry_path.exists(): logger.warning("[bypass_handler] AIPASS_REGISTRY.json not found") return None - with open(REGISTRY_PATH, "r", encoding="utf-8") as f: + with open(registry_path, "r", encoding="utf-8") as f: registry = json.load(f) file_path = str(Path(file_path).resolve()) - registry_dir = REGISTRY_PATH.parent + registry_dir = registry_path.parent def _resolve(raw: str) -> str: p = Path(raw) diff --git a/src/aipass/seedgo/apps/handlers/diagnostics/discovery.py b/src/aipass/seedgo/apps/handlers/diagnostics/discovery.py index 21554e37..79673827 100644 --- a/src/aipass/seedgo/apps/handlers/diagnostics/discovery.py +++ b/src/aipass/seedgo/apps/handlers/diagnostics/discovery.py @@ -21,12 +21,17 @@ from aipass.seedgo.apps.handlers.json import json_handler def _find_registry() -> Path: - """Find AIPASS_REGISTRY.json by walking up from this file's location.""" + """Find *_REGISTRY.json — CWD-first for external project support, then __file__ fallback.""" + cwd = Path.cwd() + for parent in [cwd] + list(cwd.parents): + matches = sorted(parent.glob("*_REGISTRY.json")) + if matches: + return matches[0] current = Path(__file__).resolve().parent for parent in [current] + list(current.parents): - candidate = parent / "AIPASS_REGISTRY.json" - if candidate.exists(): - return candidate + matches = sorted(parent.glob("*_REGISTRY.json")) + if matches: + return matches[0] return Path.cwd() / "AIPASS_REGISTRY.json" diff --git a/src/aipass/seedgo/apps/handlers/readme/readme_ops.py b/src/aipass/seedgo/apps/handlers/readme/readme_ops.py index e40fce28..a0a7a290 100644 --- a/src/aipass/seedgo/apps/handlers/readme/readme_ops.py +++ b/src/aipass/seedgo/apps/handlers/readme/readme_ops.py @@ -31,16 +31,20 @@ from aipass.seedgo.apps.handlers.json import json_handler def _find_registry() -> Path: - """Find AIPASS_REGISTRY.json by walking up from this file's location.""" + """Find *_REGISTRY.json — CWD-first for external project support, then __file__ fallback.""" + cwd = Path.cwd() + for parent in [cwd] + list(cwd.parents): + matches = sorted(parent.glob("*_REGISTRY.json")) + if matches: + return matches[0] current = Path(__file__).resolve().parent for parent in [current] + list(current.parents): - candidate = parent / "AIPASS_REGISTRY.json" - if candidate.exists(): - return candidate + matches = sorted(parent.glob("*_REGISTRY.json")) + if matches: + return matches[0] return Path.cwd() / "AIPASS_REGISTRY.json" -REGISTRY_PATH = _find_registry() # Generator lives in same handlers/standards/ directory as this file GENERATOR_PATH = Path(__file__).resolve().parent / "readme_generator.py" @@ -69,14 +73,15 @@ def resolve_branch(branch_arg: str) -> Optional[Dict]: Returns: Branch dict from registry, or None if not found """ - if not REGISTRY_PATH.exists(): + registry_path = _find_registry() + if not registry_path.exists(): return None try: - content = REGISTRY_PATH.read_text(encoding="utf-8") + content = registry_path.read_text(encoding="utf-8") registry = json.loads(content) except (json.JSONDecodeError, OSError): - logger.info("Cannot read registry for branch resolution: %s", REGISTRY_PATH) + logger.info("Cannot read registry for branch resolution: %s", registry_path) return None # Strip @ prefix and normalize @@ -101,15 +106,16 @@ def get_all_branches() -> List[Dict]: Returns: List of branch dicts, or empty list on failure """ - if not REGISTRY_PATH.exists(): + registry_path = _find_registry() + if not registry_path.exists(): return [] try: - content = REGISTRY_PATH.read_text(encoding="utf-8") + content = registry_path.read_text(encoding="utf-8") registry = json.loads(content) return registry.get("branches", []) except (json.JSONDecodeError, OSError): - logger.info("Cannot read registry for branch listing: %s", REGISTRY_PATH) + logger.info("Cannot read registry for branch listing: %s", registry_path) return [] diff --git a/src/aipass/seedgo/apps/modules/hooks_ext.py b/src/aipass/seedgo/apps/modules/hooks_ext.py index f636a2c8..16837a46 100644 --- a/src/aipass/seedgo/apps/modules/hooks_ext.py +++ b/src/aipass/seedgo/apps/modules/hooks_ext.py @@ -238,6 +238,8 @@ def print_introspection() -> None: def handle_command(command: str, args: list) -> bool: """Not a primary drone module — delegates to hooks.py for routing.""" + if command != "hooks_ext": + return False if not args: print_introspection() return True diff --git a/src/aipass/seedgo/apps/modules/inbox_audit.py b/src/aipass/seedgo/apps/modules/inbox_audit.py index 5bc1626f..b8ebee87 100644 --- a/src/aipass/seedgo/apps/modules/inbox_audit.py +++ b/src/aipass/seedgo/apps/modules/inbox_audit.py @@ -105,16 +105,16 @@ def print_introspection() -> None: def handle_command(command: str, args: List[str]) -> bool: """Handle `audit inbox-ids` — return True only for that exact subcommand.""" + if command == "inbox_audit": + if not args: + print_introspection() + return True + if args[0] in ("--help", "-h", "help"): + print_introspection() + return True if command not in ("audit", "standards_audit"): return False - if not args: - print_introspection() - return True - if args[0] in ("--help", "-h", "help"): - console.print("Usage: drone @seedgo audit inbox-ids") - console.print(" Scans all .ai_mail.local/inbox.json files for non-8-hex message ids.") - return True - if args[0] != "inbox-ids": + if not args or args[0] != "inbox-ids": return False _run_inbox_id_scan() return True diff --git a/src/aipass/spawn/CLAUDE.md b/src/aipass/spawn/CLAUDE.md deleted file mode 100644 index 183e7220..00000000 --- a/src/aipass/spawn/CLAUDE.md +++ /dev/null @@ -1,42 +0,0 @@ -# SPAWN - -## Startup - -On any greeting, silently read these files and run the commands — no narration, no announcing steps. Just do it and respond with the status. - -**Read:** `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `README.md`, `STATUS.local.md` -**Check:** If `.ai_mail.local/inbox.json` exists, read it. Process any mail. -**Run:** `git status` - -## Identity - -You are **SPAWN** — an AIPass citizen. - -- **Module:** `aipass.spawn` -- **Role:** agent_factory -- **Purpose:** Agent creation and template management. Creates new agent branches from bundled template, replaces placeholders, registers in AIPASS_REGISTRY.json. - -## Memories - -Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`. - -- `local.json` — Session history, key learnings, active tasks -- `observations.json` — Collaboration patterns, insights -- `passport.json` — Identity (rarely changes) - -## AIPass Context - -This branch is part of the AIPass multi-agent framework. Key concepts: - -- **Branch** — your directory (`src/aipass/spawn/`). Your home. -- **Citizen** — the identity that lives in a branch. Has a passport, memories, mailbox. -- **Agent** — a disposable worker spawned for a task. No passport, no memory. - -## Commands - -``` -drone systems # List available infrastructure -drone @ai_mail inbox # Check mailbox -drone @ai_mail send @branch "Subject" "Body" # Send mail -drone @seedgo audit @spawn # Run standards audit -``` diff --git a/src/aipass/spawn/templates/builder/.spawn/.template_registry.json b/src/aipass/spawn/templates/builder/.spawn/.template_registry.json index cbd426b2..cfc0d8cb 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-04-15", + "last_updated": "2026-04-22", "description": "Template file tracking registry for ID-based updates" }, "files": { @@ -101,12 +101,6 @@ "content_hash": "9b1d3a691272", "has_branch_placeholder": false }, - "f015": { - "path": "CLAUDE.md", - "name": "CLAUDE.md", - "content_hash": "e24c75348c4f", - "has_branch_placeholder": false - }, "f016": { "path": "DASHBOARD.local.json", "name": "DASHBOARD.local.json", @@ -143,10 +137,10 @@ "content_hash": "2e4f4a0c1b47", "has_branch_placeholder": false }, - "f022": { - "path": "apps/handlers/__init__.py", - "name": "__init__.py", - "content_hash": "55afc5eddbe0", + "f044": { + "path": "apps/integrations/README.md", + "name": "README.md", + "content_hash": "31c09afe1299", "has_branch_placeholder": false }, "f023": { @@ -155,7 +149,7 @@ "content_hash": "a4cf0a8e3b4f", "has_branch_placeholder": false }, - "f026": { + "f045": { "path": "apps/modules/__init__.py", "name": "__init__.py", "content_hash": "e3b0c44298fc", @@ -167,12 +161,6 @@ "content_hash": "d1e4e2b98c38", "has_branch_placeholder": false }, - "f027": { - "path": "apps/{{BRANCH}}.py", - "name": "{{BRANCH}}.py", - "content_hash": "e3893ff95aa5", - "has_branch_placeholder": true - }, "f028": { "path": "artifacts/README.md", "name": "README.md", @@ -239,12 +227,6 @@ "content_hash": "881f06bb6574", "has_branch_placeholder": false }, - "f039": { - "path": "tests/conftest.py", - "name": "conftest.py", - "content_hash": "5d98b049957b", - "has_branch_placeholder": false - }, "f040": { "path": "tools/README.md", "name": "README.md", @@ -263,13 +245,25 @@ "content_hash": "28e9ae373563", "has_branch_placeholder": false }, - "f044": { - "path": "apps/integrations/README.md", - "name": "README.md", - "content_hash": "31c09afe1299", + "f022": { + "path": "apps/handlers/__init__.py", + "name": "__init__.py", + "content_hash": "dbfc0e044461", "has_branch_placeholder": false }, - "f045": { + "f027": { + "path": "apps/{{BRANCH}}.py", + "name": "{{BRANCH}}.py", + "content_hash": "39db2c4f8160", + "has_branch_placeholder": true + }, + "f039": { + "path": "tests/conftest.py", + "name": "conftest.py", + "content_hash": "97f220799d19", + "has_branch_placeholder": false + }, + "f015": { "path": "apps/plugins/__init__.py", "name": "__init__.py", "content_hash": "e3b0c44298fc", @@ -282,11 +276,6 @@ "name": ".ai_mail.local", "has_branch_placeholder": false }, - "d002": { - "path": ".ai_mail.local/sent", - "name": "sent", - "has_branch_placeholder": false - }, "d003": { "path": ".aipass", "name": ".aipass", @@ -327,6 +316,11 @@ "name": "handlers", "has_branch_placeholder": false }, + "d024": { + "path": "apps/integrations", + "name": "integrations", + "has_branch_placeholder": false + }, "d010": { "path": "apps/modules", "name": "modules", @@ -391,11 +385,6 @@ "path": "{{BRANCH}}_json/custom_config", "name": "custom_config", "has_branch_placeholder": false - }, - "d024": { - "path": "apps/integrations", - "name": "integrations", - "has_branch_placeholder": false } } } diff --git a/src/aipass/spawn/templates/builder/CLAUDE.md b/src/aipass/spawn/templates/builder/CLAUDE.md deleted file mode 100644 index 43544d67..00000000 --- a/src/aipass/spawn/templates/builder/CLAUDE.md +++ /dev/null @@ -1,42 +0,0 @@ -# {{BRANCHNAME}} - -## Startup - -On any greeting, silently read these files and run the commands — no narration, no announcing steps. Just do it and respond with the status. - -**Read:** `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `README.md`, `STATUS.local.md` -**Check:** If `.ai_mail.local/inbox.json` exists, read it. Process any mail. -**Run:** `git status` - -## Identity - -You are **{{BRANCHNAME}}** — an AIPass citizen. - -- **Module:** `aipass.{{MODULE}}` -- **Role:** {{ROLE}} -- **Purpose:** {{PURPOSE_BRIEF}} - -## Memories - -Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`. - -- `local.json` — Session history, key learnings, active tasks -- `observations.json` — Collaboration patterns, insights -- `passport.json` — Identity (rarely changes) - -## AIPass Context - -This branch is part of the AIPass multi-agent framework. Key concepts: - -- **Branch** — your directory (`src/aipass/{{BRANCH}}/`). Your home. -- **Citizen** — the identity that lives in a branch. Has a passport, memories, mailbox. -- **Agent** — a disposable worker spawned for a task. No passport, no memory. - -## Commands - -``` -drone systems # List available infrastructure -drone @ai_mail inbox # Check mailbox -drone @ai_mail send @branch "Subject" "Body" # Send mail -drone @seedgo audit @{{BRANCH}} # Run standards audit -``` diff --git a/src/aipass/spawn/tests/test_citizen_classes.py b/src/aipass/spawn/tests/test_citizen_classes.py index 44ff2ab7..d4b976c1 100644 --- a/src/aipass/spawn/tests/test_citizen_classes.py +++ b/src/aipass/spawn/tests/test_citizen_classes.py @@ -341,17 +341,12 @@ class TestTemplateStructure: spawn_root = Path(__file__).parents[1] assert not (spawn_root / "templates" / "agent.template").exists() - def test_builder_template_has_claude_md(self): - """Builder template includes CLAUDE.md with startup protocol.""" + def test_builder_template_has_no_claude_md(self): + """Builder template should NOT include CLAUDE.md — project root covers it.""" from aipass.spawn.apps.handlers.class_registry import get_template_dir builder = get_template_dir("builder") - claude_md = builder / "CLAUDE.md" - assert claude_md.exists(), "Builder template must include CLAUDE.md" - content = claude_md.read_text() - assert "{{BRANCHNAME}}" in content - assert "Startup" in content - assert ".trinity/passport.json" in content + assert not (builder / "CLAUDE.md").exists() def test_builder_template_has_local_prompt(self): """Builder template includes non-empty local prompt.""" @@ -373,19 +368,14 @@ class TestTemplateStructure: class TestAgentScaffoldContent: """Tests verifying created agents have useful content.""" - def test_created_agent_has_claude_md(self, tmp_path): - """Created agent should have CLAUDE.md with branch name substituted.""" + def test_created_agent_has_no_claude_md(self, tmp_path): + """Branches should NOT have CLAUDE.md — project root covers it.""" from aipass.spawn.apps.modules.core import _spawn_agent target = tmp_path / "content_test" _spawn_agent(str(target), role="Tester", purpose="Testing scaffold") - claude_md = target / "CLAUDE.md" - assert claude_md.exists() - content = claude_md.read_text() - assert "CONTENT_TEST" in content - assert "{{BRANCHNAME}}" not in content - assert ".trinity/passport.json" in content + assert not (target / "CLAUDE.md").exists() def test_created_agent_local_prompt_has_content(self, tmp_path): """Created agent's local prompt should reference branch identity.""" @@ -400,15 +390,17 @@ class TestAgentScaffoldContent: assert "PROMPT_AGENT" in content assert len(content) > 100 - def test_created_agent_claude_md_has_role(self, tmp_path): - """CLAUDE.md should include the agent's role if provided.""" + def test_created_agent_passport_has_role(self, tmp_path): + """Passport should include the agent's role if provided.""" + import json + from aipass.spawn.apps.modules.core import _spawn_agent target = tmp_path / "role_test" _spawn_agent(str(target), role="Data Analyst", purpose="Reports") - content = (target / "CLAUDE.md").read_text() - assert "Data Analyst" in content + passport = json.loads((target / ".trinity" / "passport.json").read_text()) + assert passport["identity"]["role"] == "Data Analyst" # =============================================================================