Adds physical blockers so agents cannot bypass the correct delivery paths:
1. pre_edit_gate.py v1.3.0 (.claude/hooks/) — two new Track E rules:
- Rule 1: block any write to *.ai_mail.local/inbox.json (use drone @ai_mail email)
- Rule 2: block cross-branch writes unless CWD branch is in TRUSTED_CROSS_WRITERS
2. permissions.py (seedgo/apps/modules/) — single source of truth:
- TRUSTED_CROSS_WRITERS = ("devpulse", "seedgo", "spawn")
- is_trusted_caller(name), identify_caller(cwd)
3. drone auth.py — ALLOWED_CALLERS now imported from permissions.py
(extended from ["devpulse"] to all three trusted cross-writers)
4. ai_mail delivery.py — deliver_to_inbox_file() helper added:
- Single canonical path for direct-path inbox writes, always fires notify-send
- reply.py _deliver_via_reply_path() backdoor routes through this helper
5. inbox_audit.py (seedgo/apps/modules/) — drone @seedgo audit inbox-ids:
- Scans all inbox.json files for non-8-hex message ids
- Alerts with drone @ai_mail email command when violations found
6. test_hooks_track_e.py — 26 tests, all passing (359 total in suite)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an observational probe harness for every Claude Code hook event type.
Used to verify hook wiring, scaffold new hooks, and answer Q12 (subagent
hook propagation / env-var matrix).
## What ships
**7 probe scripts** at `.claude/hooks/probes/`:
- probe_pre_tool_use.py, probe_post_tool_use.py, probe_user_prompt_submit.py
- probe_subagent_stop.py, probe_pre_compact.py, probe_stop.py, probe_notification.py
Each probe: reads stdin JSON, appends one entry to last_ping.jsonl
(APPEND mode, never overwrites), exits 0 always. Fields recorded:
event, tool, cwd, agent_id, timestamp, script_elapsed_ms, cli_version,
env_has_claude_project_dir, env_has_aipass_home.
Pure stdlib, no aipass imports. Silent fail on any exception.
OPT-IN — not auto-wired in settings.json (each probe docstring has
the settings.json snippet to enable it).
**drone @seedgo hooks probe** — new seedgo module (apps/modules/hooks.py):
- No flags: reads last_ping.jsonl, prints Rich [PROBE] table of recent entries
- --subagent: spawns claude -p headless, reads probe log, reports whether
PostToolUse / SubagentStop fired (definitive Q12 data for headless mode)
- --matrix: analyzes last_ping.jsonl by event type, reports env var
propagation patterns, writes Q12_findings_2026-04-20.md
**Tests**: 69 new tests in tests/test_hooks_probe.py covering stdin parse,
output shape, malformed input resilience, and module dispatch.
402 total tests pass.
**README.md** at .claude/hooks/probes/README.md — enable instructions,
example output, flag reference.
last_ping.jsonl added to .gitignore (live log, not source).
Two tests failed on macOS due to the /var/folders → /private/var/folders symlink:
- test_relative_paths_resolved (line 126: string startswith mismatch)
- test_find_registry_from_child_dir (line 377: Path == comparison)
Root cause: tempfile.mkdtemp() returns the unresolved /var/folders/... form on Mac. Production code (PR #361 / 8a5fbf6) correctly calls Path.resolve() which follows the symlink and canonicalizes to /private/var/folders/.... The test fixture's registry_dir stored the unresolved form, so one side of each comparison had /private/ and the other didn't.
One-line fix: .resolve() the fixture path too so both sides are canonical on every platform.
Platform behavior:
- Linux: no-op (no symlink, path already canonical) — was passing, stays passing
- macOS: follows /var/folders → /private/var/folders — was failing (2/33), now passing (33/33)
- Windows: normalizes short-path to long-path consistently with production code — was passing, stays passing
Verified locally on macOS 12.7.6 Intel: 33/33 in test_registry_handler.py green after fix.
Surfaced during the Mac install feedback session on issue #360. Linux @devpulse green-lit the direct PR.
Follow-up recommendation (NOT this PR, separate cleanup): migrate registry_dir fixture to pytest's built-in tmp_path, which returns a pre-resolved Path. Eliminates the class of fixture bug entirely across the test suite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_get_json_handler() was reading sys.modules[] directly after _fresh_json_handler
had just popped the module out. importlib.import_module() replaces the lookup so
the module is actually imported fresh each test. Also removes dead reload/pop
code from the fixture that never ran. Fixes 27 failures (issue #360 finding A2).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pyproject.toml: add memory = ["numpy>=2.0", "chromadb>=1.0"] optional-deps group
- setup.sh: install .[dev,memory] so fresh-clone pytest works end-to-end
- test_vector.py: gate with pytest.importorskip("numpy"/"chromadb") — skip cleanly without extras
- memory_watcher.py: _check_vector_deps() probes venv at startup; health report now honest when chromadb absent
- bypass.json: 4 entries covering test_vector.py seedgo false-positives (architecture/docs/encapsulation/meta)
- dispatch/daemon.py: resolve relative branch_path to absolute before use
- dispatch/dispatch_monitor.py: resolve lock_file path so claude cwd is always absolute
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(devpulse): watchdog: breadcrumb on exit + default timeout 1800s to 600s (FPLAN-0189)
Co-Authored-By: @devpulse <devpulse@aipass>
* feat(dispatch): auto-spawn watchdog after every dispatch (FPLAN-0189 Task A)
_orchestrate_dispatch_send now spawns `drone @devpulse watchdog agent <target>`
as a detached background process (start_new_session=True) after a successful
wake. cwd=devpulse_path bypasses _guard_caller's cross-branch rejection.
--no-watchdog flag opts out. 6 new tests cover registry lookup, path
resolution, and error paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: @devpulse <devpulse@aipass>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fresh-install testing on macOS 12 Intel surfaced five friction points that
blocked or silently broke installation for users without admin rights.
All fixes are Mac-only, gated behind a new IS_MACOS flag. Linux and
Windows paths are untouched. See #353 for full context.
1. Stock macOS 12 ships /usr/bin/python3 at 3.9.6; setup.sh aborts at
the 3.10+ minimum check. Fix: probe versioned python3.10-3.13
binaries before falling back to plain python3.
2. Homebrew auto-install is not a universal fallback. Non-admin Mac
accounts cannot install Homebrew at all because its installer
requires admin/sudo. Fix: add uv (astral.sh/uv) as a no-sudo,
no-admin fallback. uv drops into ~/.local/bin via curl and downloads
a prebuilt standalone Python 3.11 to ~/.local/share/uv/python; the
venv is created from that.
3. macOS defaults to zsh since Catalina (2019). Writing AIPASS_HOME to
~/.bashrc silently fails because zsh does not source it. Fix: on
Mac, pick ~/.zshrc (or ~/.bash_profile for bash) based on SHELL.
4. The final symlink used sudo ln -sf /usr/local/bin/drone, which
prompts for a password on Mac and breaks entirely for non-admin
users. Fix: on Mac, link into ~/.local/bin (user-writable, no sudo)
and ensure it is on PATH via the profile rc.
5. HOOK_PYTHON was set to plain python3 on Unix. On Mac that resolves
to /usr/bin/python3 (3.9.6), which cannot parse hook scripts using
PEP 604 union syntax (X | None). Hooks silently crash on every
prompt. Fix: on Mac, point HOOK_PYTHON at the venv python (3.11)
that setup just built.
Tested end-to-end on a fresh clone, non-admin user, zsh, Intel: all 11
branches bootstrapped, drone CLI works, hooks fire and execute cleanly,
ai_mail delivery verified, sub-agents spawn successfully.
Refs #353
Co-authored-by: Paddy <padddypaddypaddy-boop@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(seedgo): fix(bypass): resolve relative registry paths in get_branch_from_path and _load_bypass_for_file — bypass.json entries silently failed for all checklist invocations because branch paths from registry are relative (src/aipass/seedgo) but were compared directly against absolute resolved file paths
feat(system): fix(windows-ci): activate venv in Verify step so drone binary is on PATH
PR #330 fixed the pip bootstrap (ensurepip error-swallowing in setup.sh). With that fix working, the Verify step in windows-test.yml now fails at 'drone: command not found' (exit 127). Root cause: each CI step spawns a fresh shell, so the venv activation from setup.sh doesn't carry to Verify. drone lives at .venv/Scripts/drone.exe which isn't on the default PATH.
Fix: prepend 'source .venv/Scripts/activate' to the Verify step so drone is findable.
Stacked on PR #330 conceptually — both fixes needed for windows-test.yml to actually pass.
Combines both fixes needed to get windows-test.yml actually passing:
1. setup.sh: stop swallowing ensurepip errors (quiet + 2>/dev/null + || true was
hiding real failures — pip was silently not installed). Add get-pip.py fallback
and hard pip verification.
2. windows-test.yml: add 'source .venv/Scripts/activate' to Verify step. Each CI
step gets a fresh shell — setup.sh's venv activation doesn't carry over, so
drone wasn't on PATH in the subsequent step.
Together, these should take Windows CI from the 'silent failure every run since
creation' state to actually green. Supersedes PRs #330 and #331 which had the
fixes on separate branches (neither green alone).
Co-authored-by: @devpulse <devpulse@aipass>
* feat(system): fix(windows-ci): combine pip bootstrap fix + venv activation in Verify step
Combines both fixes needed to get windows-test.yml actually passing:
1. setup.sh: stop swallowing ensurepip errors (quiet + 2>/dev/null + || true was
hiding real failures — pip was silently not installed). Add get-pip.py fallback
and hard pip verification.
2. windows-test.yml: add 'source .venv/Scripts/activate' to Verify step. Each CI
step gets a fresh shell — setup.sh's venv activation doesn't carry over, so
drone wasn't on PATH in the subsequent step.
Together, these should take Windows CI from the 'silent failure every run since
creation' state to actually green. Supersedes PRs #330 and #331 which had the
fixes on separate branches (neither green alone).
Co-Authored-By: @devpulse <devpulse@aipass>
* feat(system): chore(lint): ruff auto-fix sweep — 303 errors across 178 files (F401 unused imports + F541 f-string placeholders + F811 redefined); restored report_error re-export + added logger call in errors.py
Co-Authored-By: @devpulse <devpulse@aipass>
---------
Co-authored-by: @devpulse <devpulse@aipass>
PR #330 fixed the pip bootstrap (ensurepip error-swallowing in setup.sh). With that fix working, the Verify step in windows-test.yml now fails at 'drone: command not found' (exit 127). Root cause: each CI step spawns a fresh shell, so the venv activation from setup.sh doesn't carry to Verify. drone lives at .venv/Scripts/drone.exe which isn't on the default PATH.
Fix: prepend 'source .venv/Scripts/activate' to the Verify step so drone is findable.
Stacked on PR #330 conceptually — both fixes needed for windows-test.yml to actually pass.
Co-Authored-By: @devpulse <devpulse@aipass>