Merge pull request #408 from AIOSAI/seedgo/fix-find-registry-cwd-first

fix(seedgo): CWD-first _find_registry() in 3 handlers
This commit is contained in:
AIPass
2026-04-22 21:09:20 -07:00
committed by GitHub
21 changed files with 183 additions and 572 deletions
+1
View File
@@ -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)"}]}
+54 -64
View File
@@ -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",
+2 -53
View File
@@ -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 <target> 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
@@ -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)
-42
View File
@@ -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
```
-42
View File
@@ -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
```
@@ -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"))
+5 -3
View File
@@ -67,11 +67,13 @@ Devpulse commands are accessed via `drone @devpulse <command>`:
- `feedback reply <id> "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*
-42
View File
@@ -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
```
@@ -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}")
@@ -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"
]
}
-42
View File
@@ -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
```
@@ -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)
@@ -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"
@@ -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 []
@@ -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
@@ -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
-42
View File
@@ -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
```
@@ -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
}
}
}
@@ -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
```
+12 -20
View File
@@ -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"
# =============================================================================