test_broker.py (AF_UNIX sockets + openat2) and test_sandbox.py (bwrap) are
Linux-only; module-level pytestmark skips them on windows-latest while leaving
Linux runs unchanged. Unblocks the windows-setup CI check (red since the
sandbox build 0b4ba63).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hoist per-checker SKIP_DIRS to shared SOURCE_SKIP_DIRS (artifacts/dropbox are
output dirs, not source; no git coupling). Diagnostics: python3->sys.executable,
parse/run failures now fail loud instead of silent 0-errors-clean, and pin
pyright resolution with --pythonpath sys.executable. drone bypasses test-only
broker start_background. Proven all-13-branches-100% deterministic in an
unactivated shell (local==CI).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
src/aipass/common was the only non-citizen directory in the agent namespace.
Per @seedgo design review: moved into @aipass (owner) as aipass.aipass.shared,
content byte-identical. @spawn imports across (blessed shared-infra category,
same as aipass.prax/cli). New subprocess guard test pins the bootstrap-safety
invariant (shared/ loads zero branch deps — aipass init stays pre-drone-safe).
9 import/doc sites updated, 9 documented seedgo bypasses (pre-infra leaf,
stdlib-only by design). aipass 480 + spawn 315 tests green, both audits 100%,
repo-wide zero refs to the old path.
Every autonomous agent can launch inside a kernel-enforced mount namespace
(srt -> bwrap+seccomp): reads stay open (shared live FS preserved, bind-mount not
isolation; own-tree writes land live), but rm/python/find/Write on .git or sibling
trees hit EROFS. /tmp + own tree writable; .git RW devpulse, RO builders. Inert by
default behind AIPASS_SANDBOX_ENABLED (off); flag-off path byte-identical to old.
hooks: srt wrapper + per-role build_policy + broker_secret mask; rm_gate demoted.
drone: out-of-sandbox broker (identity allowlist, openat2 RESOLVE_BENEATH, HMAC
handshake over inherited fd, audit); drone rm via broker when sandboxed.
ai_mail: dispatch gate + broker-fd wiring (fail-loud exit -4, never silent).
aipass: doctor Sandbox group + setup.sh prereqs (LOUD on missing).
Proven by a live 16-check red-team suite. seedgo 100% + 2859 tests green across
all 5 touched branches.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same action-gated pattern as the notification handlers — speak() -> 'sound'
return-key. Missed in b26bd7c. Hooks suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Notification handlers (announce, email, stop_sound, tool_sound) return a
'sound' key the engine plays on action instead of calling speak() on every
invocation — quieter and honest (skipped loaders stay silent). Slim
cadence_investigation.md. Tests updated to assert the return-key form. 472/472
hooks green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three ruff call sites called bare `ruff`, absent from the hook subprocess
PATH (ruff lives only in .venv) — logged 'ruff not found' 177x over ~a month,
silently skipping lint+format on every edit. Also `--output-format=text` was
removed from modern ruff. Fixed all three sites to `sys.executable -m ruff`
(the pattern the working pyright leg already uses) + concise format + honest
rc>=2 error logging. Tests now pin the invocation (argv == sys.executable -m
ruff) so a regression fails loud — the subprocess-mock is how this hid.
438/438 hooks tests green; live-verified through the real hook pipeline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause: _HOOK_PATTERN required an action= key that cadence never
emits — it logs the action as the bare second word (fired/skipped).
Extraction failed, so events never reached the styled print_hook_event
renderer (bold-green lightning / dim dot). Pipeline was already correct.
- _HOOK_PATTERN -> bare-word capture: r'\[HOOKS\]\s+(\w+)\s+(\w+)'
- enriched hook event detail (period, offset, short session id)
- corrected docstring that documented the phantom action= format
- tests updated to real production format + real-pipeline test added
- type:ignore on watchdog imports (repo convention)
Verified: 914/914 prax suite green (90 log_watcher). @prax dispatched
for full-pipeline trace; stale monitor process explained the no-show.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cadence redo — verified against the live execution model, not unit tests:
- Counter now advances exactly once per real turn (mtime debounce +
transcript-size token + flock). Fixes the separate-process leapfrog where
global/branch loaders double-incremented and fired erratically.
- Structured [HOOKS] cadence fired|skipped logging; prax monitor renders
hook events distinctly for live visibility.
- Action-gated sound: handlers return a 'sound' key the engine plays only on
real action — skipped loaders are silent (no more false piper every turn).
- Fixed auto_fix.py: leftover speak() NameError (swallowed by broad except)
meant diagnostics silently never ran on any edit. Removed; sound moved to
the error path.
- Tests rewritten to model separate-process execution (leapfrog regression
test added); sound assertions across all refactored handlers. 438 pass.
prax: hook fire/skip event rendering in the live monitor. 913 pass.
README: hardcoded metrics (version/tests/PRs/standards) -> live PyPI+codecov
badges and qualitative wording; killed the 33-vs-36 drift. CHANGELOG W24.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stop re-injecting the global + branch prompts every turn (~3k tokens/turn).
They now fire together every 5th turn; the prior injection persists in context
between fires. Identity + email stay every-turn.
- apps/modules/cadence.py: per-session turn counter (/tmp/aipass-cadence-
{session_id}.json), should_fire(loader)/reset_counter(), DEFAULTS + deep-merge
config (api provider.py pattern). 'drone @hooks cadence' introspection.
- global_loader.py + branch_loader.py: cadence guard via importlib (crash-
isolated); non-fire turn returns empty.
- compact.py: PreCompact resets counter to -1 -> next turn = 0 = all fire
(rebuild context after compaction). New session = fresh counter = all fire.
- hooks_json/custom_config/cadence_config.json: tunable knob (period/offsets/
enabled), one file, no code edits. Data lives in the json home, not the code
dir. Missing file = code DEFAULTS = safe.
- .seedgo/bypass: documented stdlib-json config read (json_handler N/A for a
dispatch engine).
435 tests pass (26 new), seedgo 100%, pyright 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Memory files now reconcile to template, not just clean known fields:
- normalize.py: rewrote from field-targeted cleanup to template-conformance —
strips ANY key not in the template at every level (root/metadata/limits/status).
Kills legacy orphans (old 'st' blocks, active_tasks, current_lines, max_lines)
that field-targeted cleanup was blind to. Fixed _MEMORY_ROOT path (parents[3])
that silently skipped template loading in production.
- memory_watcher.py: wired normalize_memory_file into both scan paths
(check_and_rollover + on_modified) with a write-loop guard — drift now
self-heals on every trigger, no manual run needed.
- line_counter.py: stop writing current_lines (entry-count is the only metric).
- LOCAL/OBSERVATIONS templates: removed line-count fields.
Entry-count is the sole rollover metric, both files, all branches.
873 tests pass, seedgo 100%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reply and send silently truncated multi-line bodies to the first CLI arg.
handle_reply(args[1]) and parse_send_args(rest[1]) dropped args[2:]/rest[2:]
when a body word-split into multiple args. Now join all remaining args.
Backwards-compatible; single-arg messages unchanged. +6 tests (718 total).
Found via @hooks replies arriving as first-line-only (60/48 chars).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verification audit caught a gap: spawn create copies templates/builder/ tree
directly (DEFAULT_TEMPLATE), but builder/.trinity/local.json lacked the todos[]
schema — so freshly spawned branches would not inherit it. Seeded todos[] +
max_todos:10 + todo_text_max_chars:200 + operational note to match @memory's
LOCAL.template.json. Now both spawn-create and template-push paths produce
todos[].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spawn's pre-merge JSON backups dropped a .recovery/ dir at each branch root,
which had accumulated 242 stale auto-gen DASHBOARD backups across 10 branches
(the original .recovery report that started this whole investigation).
- aipass.common.json_ops.backup_json gained optional backup_dir param
(default unchanged = file_path.parent/.recovery, backward-compatible).
- spawn update engine (update_ops.py _merge_json) now passes
branch_dir/.spawn/.recovery as the backup dest -> backups land under the
spawn-managed .spawn/ dir, one namespace, not cluttering branch roots.
- Memory stays in the safety net: no memory-exclusion added; the engine just
never touches .trinity/DASHBOARD on update so it never backs them up.
- 2 new tests (unit: custom backup_dir; integration: backup lands in
.spawn/.recovery). 315 spawn + 438 aipass tests green; seedgo 100% both.
Stale .recovery backups swept separately (untracked/gitignored, local hygiene).
.recovery/ gitignore pattern already covers .spawn/.recovery/.
TDPLAN-0006 P4 — final phase. Closes the spawn update-safety + consolidation
work (P0 dry-run-default, P1 #636 engine, P2 shared lib, P3 import kill, P4
backup relocate).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
init_flow.py:896 was the one place aipass imported spawn's Python directly:
from aipass.spawn.apps.modules.sync_registry import sync_registry
Replaced with subprocess.run(['drone','@spawn','sync-registry','--fix']) — the
command spawn already exposes — matching the aipass init agent -> drone @spawn
create pattern. Graceful degradation preserved: FileNotFoundError (no drone),
non-zero exit, and timeout are all silently skipped so a registry-sync hiccup
never hard-fails an init update. Safe because init update runs on an existing
project where drone is installed (NOT the pre-drone fresh-init path).
aipass branch now has ZERO direct imports of another branch's ENGINE code.
Remaining cross-branch imports are shared SERVICE layers only (cli Rich UI,
prax logger used in 347 files, trigger events) — infrastructure, not duplication.
Verified: zero aipass.spawn imports in .py code; fresh aipass init still
scaffolds (bootstrap pre-drone intact, 69 tests); 438 tests green (4 new for
the subprocess path: success/failure/missing-drone/timeout); seedgo 100%.
TDPLAN-0006 P3. P4 (.recovery relocate) is the last phase.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#636: drone @spawn update would scramble every branch's identity/memory in
one command. On a branch created seconds earlier, --dry-run proposed 30 renames
rotating identity dirs (apps->.trinity->.seedgo->.claude->.archive->.aipass),
README->DASHBOARD, and deep-merged stale template into live .trinity/. Root
cause: the CREATE path regenerated template-registry IDs in filesystem-walk
order (!= the master's hand-crafted IDs), so content-hash + rename-detection
saw a mismatch on a pristine branch. update --all would have destroyed all 13
citizens at once.
P0 — safety by default:
- update + repair are now dry-run by default; --apply required to write.
Forgotten flag = safe preview-only no-op. --dry-run kept as alias.
- doctor_fix.py repair suggestions emit the matching --apply form
(+ aipass test_doctor_fix updated to the new contract).
P1 — engine rebuild (update_ops.py v2.0):
- Path-based named-managed-files model replaces whole-tree hash-diff +
rename-detection. ID divergence is moot — IDs are no longer used.
- .trinity/*, DASHBOARD.local.json, artifacts/birth_certificate.json,
.seedgo/bypass.json = delivered on CREATE only, NEVER touched on update.
- Old ID engine (change_detection.py, reconcile.py) + orphaned tests deleted.
Verified on fresh sandbox: update --dry-run = 0 renames / 0 updates / 0
additions (create==update invariant); no-flag run = dry-run preview, filesystem
byte-identical; 313 spawn tests green; seedgo 100% (all 36 standards).
Closes#636. P2/P3/P4 (shared lib, seam, .recovery relocate) to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drops git check-ignore + git log from readme_check. Runtime dirs tolerated via
static list (_is_runtime_artifact); freshness checks date-presence only, no
history comparison. Local-CI parity proven 13/13 both ways (working tree +
git archive clean checkout). Invariant: a checker never consults git or
.gitignore; only bypass.json excludes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The last 1%: 7 branches scored 99% in CI while 100% locally. Root cause proven
by reproducing CI's exact path (tracked-only tree + real .git): .gitignore
dir-only patterns (trailing slash — logs/, **/*_json/, .trinity/) do NOT match
via 'git check-ignore <bare-path>' when the path is absent from disk (clean
checkout), because git cannot infer 'directory' to apply a dir-only pattern.
The working tree has those dirs on disk, so it matched there — the exact
working-tree-vs-clean-checkout divergence.
_is_gitignored now also tests the trailing-slash form; all 7 readme failures
(cli_json/logs/artifacts/.trinity/ etc flagged 'missing on disk') clear.
Regression test builds a real git repo with dir-only patterns + non-existent
paths. CI gate also now prints failing standards + check messages (says WHY).
Verified: clean tree w/ real .git 13/13 100%; working tree 13/13 100%; seedgo
1053 tests green; pyright 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Archive orphaned hook bridge/probe/manifest modules + their tests to
.archive/ — their only callers were the .claude/hooks scripts disabled
by DPLAN-0184. Seedgo audits hooks via standards; the hooks branch owns
the engine/bridge/handlers. README + bypass.json + prompts updated to
match. 1045 tests green, pyright clean.
The real T1 Windows bug (diagnosed via the now-reverted error-surfacing
probe): aipass init scaffolds fine, then crashes printing its success
banner — Rich writes the success glyphs through a cp1252 stdout
(UnicodeEncodeError 'charmap'). Same class as the drone fix. The aipass
entry point now reconfigure()s stdout/stderr to UTF-8 in place on Windows.
Also: ci.yml's broad 'pytest --rootdir=.' swept in tests/e2e (which build
a wheel via the dedicated e2e-wheel.yml), failing the unit lane since the
harness landed; now --ignore=tests/e2e in both pytest jobs.
route_command error-surfacing probe reverted to honor 'no function change'
(the masked-error mislabel is noted as a separate @aipass recommendation).
Local: e2e 14/14 green, aipass units 24/24, ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
route_command swallowed any handle_command exception and let main() print
a misleading 'Unknown command', hiding real failures (e.g. the Windows
aipass-init error the e2e harness hit). It now also prints the failing
module + traceback to stderr. Bool contract unchanged; 24/24 aipass unit
tests pass. This makes the masked Windows init failure diagnosable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two latent Windows portability bugs caught by the new e2e wiring harness:
- aipass init: _preflight_check ancestor walk crashed on OSError from
un-enumerable Windows drive-root entries (pagefile.sys), swallowed by
route_command as 'Unknown command: init'. Walk now skips unreadable
entries (logged).
- drone @branch: crashed with UnicodeEncodeError ('charmap') printing a
routed branch's captured output via Rich on cp1252 stdout. PYTHONUTF8
only affects child interpreters; entry point now reconfigure()s the live
stdout/stderr to UTF-8.
Pure portability — Linux/macOS behaviour unchanged. e2e suite 14/14 green
locally on Linux. Lets the 3-OS CI verify Windows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 2 of #630. The rm_gate hook + drone rm now own destructive-delete
protection (cross-provider, path-aware, teaching), so the Claude-only blanket
deny is redundant AND harmful (it short-circuits before the hook, suppressing
the teaching message).
- setup.sh: removed Bash(rm -rf*) + Bash(rm -r *) from git_deny (new installs)
- bootstrap.py: removed Bash(rm -rf *) shipped via aipass init (project settings)
- doctor_wire.reconcile_stale_deny(): aipass doctor WARNs on stale rules;
--fix removes them (idempotent, preserves all else) — migration for existing
installs (the 'aipass update should be trusted' goal)
- .aipass/project_hooks.json template: added rm_gate (new projects get it)
Tests: 8 reconcile + 432 aipass total, seedgo 99%. CHANGELOG W23.
#625 (HIGH): drone @git merge passed --delete-branch to gh pr merge
unconditionally, so merging a dev->main PR DELETED the persistent dev branch
on the remote and left the tree on main (next commit silently on main). Now:
- merge looks up the PR head ref and only appends --delete-branch for
non-protected branches; dev/main are never deleted. Unknown head ref fails
SAFE (no delete) — devpulse hardening on top of @drone's protected-branch set.
- after merge, return the working tree to dev (loud warning if it can't).
- branches_handler runs git fetch --prune before git branch -r (no more
'cached lies' reporting deleted branches as live).
- new drone @git prune-temp cleans merged temp PR branches (citizen/*).
#623: status/diff append a '(showing <branch> scope — use --all for full repo)'
footer when scoped, so an empty scoped view isn't mistaken for a clean repo.
Blank-output sub-item not reproducible — documented.
@drone built fixes 1-5 (FPLAN-0236); devpulse added the unknown-head-ref
fail-safe + test and verified independently. drone suite 716 pass, seedgo 99%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolve_branch() validated branch path containment against the primary
registry root even when the branch was found via the AIPASS_HOME fallback,
so external projects (Vera, Daemon) were blocked from calling @api and any
other AIPass branch with 'path escapes project root'.
Add get_branch_with_registry() (non-breaking sibling to get_branch_by_name)
that returns the branch plus the registry it was found in. resolve_branch()
now validates containment against that registry's root. Security preserved:
each branch stays contained within its own declaring registry; genuine
escapes still blocked. 4 new cross-project resolver tests, 58 resolver
tests pass, drone suite 702 pass, seedgo @drone 99%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removed 5 stale Phase-0/Phase-4 bypasses (verified seedgo passes without them
now that aipass is fully built). Restored 3 aipass.py entries (cli/debug_print/
introspection) with accurate current reasons — thin command router, not a
module. Metadata description updated to current operational state. 58→53 entries.
424 tests pass, seedgo 99%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aipass is a user-facing binary — 'aipass doctor' must run the health check,
not describe itself. All 7 modules (doctor, doctor_fix, doctor_wire, handoff,
help_chat, init_flow, profile) hit a no-args→introspection gate (a standard
meant for 'drone @branch <module>' discovery). Bare invocation now runs the
command or shows usage; introspection moved to --info. seedgo introspection
standard bypassed for these binary-invoked modules (documented). 424 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>