feat(system): fix(ai_mail): remove broken _spawn_watchdog auto-spawn from dispatch — devpulse runs its own watchdog (DPLAN-0142 step 1)
Co-Authored-By: @devpulse <devpulse@aipass>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user