# Changelog All notable changes to AIPass will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Entries are grouped by merge under a dated section header (`YYYY-MM-DD`). Package releases follow [SemVer](https://semver.org/) and are tracked by the git tag and PyPI version — not the changelog header. --- ## [2026-07-21] **feat(prax)** — Commons live social feed in the monitor (DPLAN-0257, Patrick ask verbatim): `drone @prax monitor run commons` now streams The Commons' chatter — posts, comments, votes, reactions — room-tagged with mood coloring, monitor-style. ~10-event backfill on open, then 1.5s id-cursor polling. Read-only by construction (`mode=ro` sqlite URI — write attempt refused, verified live); commons stays the only writer, zero commons-side changes. Branch-log tail still reachable via `monitor run commons --logs`; mixed branch lists unchanged. `--relay` rides the existing Telegram relay path. 33 new tests, prax suite 1065 green, audit 100% (52 files). Door-tested live: devpulse posted/replied/reacted while the feed streamed every event. Built by @prax. **fix(hooks)** — two DPLAN-0253 backlog hardenings (DPLAN-0256 clear): engine handler timeout + presence_gate PID-reuse defense. `_run_handler` now runs handler-type hooks on a daemon thread joined with the hooks.json `timeout` field (default 30s) — a hung handler returns TIMEOUT loud (engine.jsonl + sound) and the event moves on; daemon thread chosen over ThreadPoolExecutor so a stuck orphan can never hang interpreter exit. presence_gate occupancy no longer trusts `os.kill(pid, 0)` alone: `procStart` (CC session file) is matched against `/proc//stat` field 22 so a kernel-recycled PID can't impersonate a dead session — closes the gap before observe-only ever flips to enforcement. Missing procStart / non-Linux falls back to liveness-only, logged. 15 new tests, suite 1272 green, seedgo 31/31 both files. Built by @hooks. **fix(trigger)** — runaway-log alerts get the 24h TTL every other mute already had (DPLAN-0256 backlog clear): `_write_alert()` hardcoded `expires_at: None`, so alerts.json entries nagged forever while medic branch mutes self-expired. New `DEFAULT_ALERT_TTL_SECONDS = 86400` (matches medic_state's `DEFAULT_MUTE_SECONDS`) with a `forever` escape hatch threaded through `handle_runaway_log_detected()`. 2 new tests, trigger suite 621 green, audit 100%. Built by @trigger. **feat(drone)** — joint-decision gate on `drone @git merge` (DPLAN-0256, Patrick ruling S330: merges are always done together, never accidental). The gate sits in `_handle_merge` before the plugin import — `merge_pr()` is unreachable without confirmation. A real terminal gets an interactive y/N prompt; headless callers (agent Bash) are refused unless `--confirm` is passed explicitly. Every gate decision (confirm / tty-yes / tty-abort / headless-refused) is logged via json_handler. 6 new tests (86 green), live-fired refusal verified, seedgo 31/31. **feat(skills)** — telegram user_message_relay joins the sound layer: relay events now carry their own sound key so an inbound user message is audible like every other hook event (59/59 + 252 green). **fix(devpulse)** — watchdog stall threshold 120s → 300s: the 120s no-JSONL-activity heuristic fired `[watchdog.stall]` on healthy agents doing long tool calls; 300s matches observed real-stall behavior (verified live S330). Branch `.claude/settings.local.json` carries the devpulse `autoCompactWindow: 350000` dial (Patrick ruling S326 — devpulse compacts ~292k, dispatched agents stay pinned at 200k). **fix(seedgo)** — checker accuracy arc (S330): AST-based import analysis lands in the checkers (dead_code, encapsulation, handlers, readme, test_quality, unused_function) — 13 false positives eliminated fleet-wide, 2 real hooks imports that legitimately bypass the pattern documented instead of suppressed. branch_audit, checklist and ignore_handler aligned; provider hooks snapshot fixture refreshed; stale bypass entries for deleted tools purged across branches (devpulse, memory, seedgo, hooks). Fleet audit 100%. **feat(hooks)** — hook sound layer + temporal grounding. Sounds now mirror the log across the hook fleet (prompt, lifecycle, notification, security handlers) — audible liveness for the whole layer, verified live (2465 green, audit 100). New `prompt/temporal.py`: tiny always-on UserPromptSubmit handler injecting one line of local date/time/weekday/part-of-day every turn — live clock each fire, host timezone via `astimezone()` (clones see their own local time). Wired on both wires (`.aipass/hooks.json` + provider manifest). **feat(aipass)** — `aipass adopt` + shared scaffold refactor: adopt turns an existing `projects/` directory into a full AIPass project (registry, resident agent, `.aipass`/`.claude` scaffold) — every write additive, nothing existing overwritten; unlike `aipass new` it starts from a directory with its own content and git history. New `shared/` package (`project_home.py`, `scaffold_content.py`) gives init/new/adopt one source of truth per helper — `handlers/init/scaffold_content.py` moved there, no per-command copies to drift. Proven live adopting aipass-site (doctor 31/0). Spawn template registry synced (template bug chain me→spawn→aipass, fixed S329). 786 tests green, audit-clean. ## [2026-07-20] **docs(projects)** — `projects/README.md`: the projects section now ships in the repo (the `!projects/README.md` gitignore whitelist existed since the aipass-new design but the file was never written). Explains the project model: **private by default** — each project is its own local git repo, fully ignored by the AIPass repo, and publishing is an explicit opt-in step (Patrick ruling 2026-07-20). Opens the public roster with **Earmark** ([AIOSAI/earmark](https://github.com/AIOSAI/earmark)), the first public AIPass project — a VS Code read-aloud extension with local Piper TTS and true pause/resume, born, built, and published 2026-07-20. **fix(hooks)** — persistent_alert dedup + loud trust-break banner (5-agent trace round follow-ups, DPLAN-0253 tail): - persistent_alert's once-per-session sound dedup lived in a module-global set, but every bridge call is a fresh process — TTS would have announced on every prompt while any alert was active. Replaced with session+alert-keyed tempdir guard files (context_gauge idiom); banner capped at 10 alerts with an "...and N more" note. - Trust-registry breaks are now LOUD: any `.aipass/hooks.json` change breaks the enrolled hash and silently disabled the entire hook layer (bit us live for 2+ hours — tier prompts, security gates, everything dark, one log-file WARNING as the only signal). New `is_hash_mismatch()` distinguishes a genuine break from never-enrolled; `trust_break_banner()` does a config-independent walk+hash check; the engine emits a full-width banner once per prompt via the presence_gate bridge call. No auto-heal — re-enrollment stays a deliberate human checkpoint. Live-fired: hash broken → banner; restored → healthy. 16 new tests, suite 1206 green, seedgo 100%. - Go-live day for the whole handler roster: 11/12 manifest entries wired into provider settings by devpulse with Patrick accepting (user_message_relay held: synchronous Telegram call + full prompt text off-machine — needs a background send and an explicit call first). @hooks branch prompt corrected and hardened: two-wires checklist + mandatory provider-wire flag in every build reply. **feat(hooks)** — auto-compact prep: context gauge + mechanical snapshot (DPLAN-0253, built by @hooks, two rounds): - `context_gauge` (UserPromptSubmit) — reads live context fill from the session transcript every prompt (cheap 50KB tail), resolves the branch's compact window (env > branch `settings.local.json` `autoCompactWindow` > 200k), and injects a "run /prep NOW" nudge at 80% of the compact trigger, escalating at 95% — once per threshold per session. Memory prep happens before auto-compact takes the choice away, on every branch including dispatched agents. - `pre_compact_prep` (PreCompact) — stamps a mechanical AUTO-COMPACT SNAPSHOT session entry into the compacting branch's `.trinity/local.json`: context fill %, active dispatch locks, open plans, git state, inbox unread. Templated from live state, defensive (malformed memory = log + skip, never raises). - Shared `context_window` module: bounded transcript tail reader + per-branch window resolver. 36 new tests; suite 1190 green; seedgo 100%. - Round 2 root-cause fix: handlers wired only in `.aipass/hooks.json` never fire on name-scoped events — UserPromptSubmit and PreCompact invoke the bridge per-handler from provider settings. Both handlers now have `provider_manifest.json` entries; @hooks' branch prompt corrected (it taught the old one-entry-per-event model) with a "new handler? check the provider wire" reminder. Go-live needs the user's `~/.claude/settings.json` synced from the manifest + fresh sessions. ## [2026-07-19] **feat(ai_mail)** — dispatched agents default to Sonnet 5 (Patrick ruling S326): - wake.py model resolution passes bare aliases (`sonnet`/`opus`/`haiku`) straight to the Claude CLI, which resolves latest-in-class — the pinned-ID MODEL_MAP is gone and can never go stale again. Default flips opus → sonnet. - dispatch_monitor pins `CLAUDE_CODE_AUTO_COMPACT_WINDOW=200000` on every spawned agent — Sonnet 5 is 1M-context native, and without the pin every dispatched agent would silently inherit a 1M window. E2E-proven: a live dispatched probe reported `claude-sonnet-5` + `WINDOW=200000` from inside. - Follow-up landed same morning: the "daemon gap" was a name collision — the unpatched `spawn_agent()` was ai_mail's own inbox-poller (`handlers/dispatch/daemon.py`), not the @daemon branch. It now passes `--model DEFAULT_MODEL` (imported from wake.py, single source); the 200k pin was already covered via the shared dispatch_monitor wrapper. @daemon's scheduled wakes import `wake_branch` directly and were covered from the start. **fix(skills)** — Telegram poll 5xx now triggers network backoff (medic loop, autonomous @skills fix): `HTTPError` ≥500 in `poll_updates` raises `_NetworkPollError` instead of falling through to rapid-fire retry; 4xx still logs and returns. Three new tests (502/503 backoff, 429 stays out). **fix(spawn, commons, prax, hooks)** — S304 audit fix campaign, Track A (DPLAN-0250, four owner dispatches verified + committed by devpulse): - **spawn** — shared `is_protected()` (infrastructure floor / registry owner / active passport) now guards both pollution repair and branch delete; repair no longer flags the live `src/aipass/aipass` branch, and deleting a protected or actively-passported branch is refused with the reason. - **commons** — branch-name resolution lowercased across all five ops sites (trade/artifact/profile/welcome/search) to match identity normalization; gifting and trading work again (guards had never matched since mid-June). - **prax** — pytest detection now also checks `_pytest` in `sys.modules`, so `patch.dict(os.environ, clear=True)` in test suites can no longer strip the guard and freeze prod-path log handlers into the setup cache. - **hooks** — the two static-path JSONL writers (engine diagnostics, telegram delivery log) resolve their path per-write and route to the tmp test dir under pytest; a full 1154-test suite run now adds zero lines to prod logs (marker-bounded proof). Also: `/prep` now checks the cross-project feedback box every run — the S304 "unread since April" backlog (F46) is processed to zero and can't silently rot again. **fix(aipass)** — `aipass doctor` no longer invents errors on a healthy repo (S304 F14-17): `.backup` and spawn `templates` dirs excluded from the agent scan, relative registry paths anchored against the project root instead of the caller's CWD (running doctor from inside a branch dir showed 5 fake missing-branch errors), and info-severity findings now render as warnings instead of pass checkmarks. Remaining findings on this repo are genuine. **fix(flow)** — registry aggregate writes are lock+atomic (S304 F85 residual): `save_branch_registry` and `save_central` were bare `open`+`json.dump`; both now use the O_EXCL lockfile + temp-file + `os.replace` pattern from the earlier `save_registry.py` fix. Proven with a 6-process concurrent-write hammer. Also investigated (N1): FPLAN-0313/0314 closed blank because `is_template_content()`'s line-count threshold fires before its bracket-marker check on default templates — guard fix queued, registry annotation is the maintainer's call. ## [2026-07-18] **fix(tests)** — the immortal `MagicMock/LOG_FILE/` directory is dead: a hooks engine test patched `diagnostics.LOG_FILE` with a bare Mock, and prax's `append_jsonl` turned the mock's fspath into a real `mkdir` — every test run re-minted an empty `MagicMock/LOG_FILE/` in the runner's CWD (found in repo root, devpulse, and hooks). Test now patches a real tmp path; 100/100 green, clean-CWD run verified to mint nothing. **docs** — merge playbook gains a site drift-check step (DPLAN-0249 follow-on): every merge run now asks whether install commands, onboarding flow, agent count, or the platform/CLI story changed — if yes, aipass.ai must be updated the same day. Codifies the S323 ruling that the site is a projection of the README, never its own source of facts. Template edit by @flow, one checklist line in the post-merge section. **docs** — root README v3 restructure (DPLAN-0249): single-funnel story with zero duplicated commands (install, `aipass new`/`init run`, trees, drone examples each taught exactly once), hero link line to aipass.ai/PyPI/r/AIPass, three reserved gif slots. Positioning ruling: the README tells only the Claude Code on Linux/WSL story — Codex/macOS/Windows mentions and the Roadmap section removed (code support unchanged; Docker distribution will serve those users later). Earlier same day: stale demo.gif embed dropped (#701) and aipass.ai realigned to the v2.7.3 front door. **v2.7.3** — the onboarding chain: from `git clone` to a conversation with an agent that remembers you. Install's three dead-ends are gone — the default `init` path, headless runs, and `aipass new` all now end where they should: `install` chains through the guided init and **opens a live conversation with the AIPass concierge**, first prompt authored with the install report in its context. The concierge's Welcome Mode (research-backed opener, one name-ask, deferred setup triage, hooks-first health check via a real @hooks dispatch, every suggestion with its exact command) was proven in a live multi-turn door-test — including the second-session payoff: relaunch, and it picks up mid-task where you left off. Plus `aipass new` and the front-door overhaul below. ### Added (onboarding chain — TDPLAN-0014) - **Install→chat handoff**: after init returns, install `launch_inline`s the concierge with an authored first prompt (fresh-install recognition + binary report). TTY-only; headless returns cleanly. - **Welcome Mode** in the concierge branch prompt: capability opener with 3–5 concrete starters, single graceful name-ask, ~turn-5 setup push ("every machine is different"), hooks-first verification incl. trust-registry enrollment, setup plan seeded from the cross-OS checklist, Windows→WSL recommendation, prax-monitor + hooksound tips, exact copy-paste command with every suggestion. - **Feedback pulse** (@hooks): one ignorable line every ~10 turns with the repo feedback link — `aipass feedback on/off` (alias for `drone @hooks feedback`) turns it off. Registered disabled for the AIPass host itself. 25 tests. - **Dead-end kills**: empty-template init now runs handoff + report stages (default path ends in the conversation); non-interactive installs complete with defaults and exit 0 (headless stage 9 prints the launch command instead of spawning); `aipass new` auto-launches into the new project's manager agent on a TTY with a printed fallback and Ctrl-C escape line. - **Unified handoff prompt**: one `INIT_PROMPT` constant (was two drifting strings in init_flow vs handoff). ### Fixed (onboarding chain) - Non-TTY `aipass init run` crashed with EOFError at the first prompt (caught in a live door-test after unit suites ran green — the prompt layer now auto-detects non-TTY and takes defaults). - `aipass new` outside an AIPass environment now exits 1 instead of 0. - Empty-template handoff messaging no longer claims an agent exists ("Your project is ready", resolved absolute path instead of `cd .`). - Stage numbering shows a skip notice instead of silently jumping 5→8. ## [2026-07-17] **v2.7.3 (first pass)** — `aipass new` and the front-door overhaul. The `projects/` directory is now a first-class playground: `aipass new ` creates a fully isolated project — own registry, own git repo with a birth commit, born deployable — with a full framework resident agent that answers `drone @` from inside the project while staying invisible to the host roster. ai_mail enforces the project boundary (cross-project mail is refused with a pointer to the feedback channel). A live door-test of the aipass CLI exposed a blind spot in the audit — perfect structural scores on an unusable front door — so seedgo grew two user-facing-quality standards (`cli_ux`, `readme_quality`) and a fleet-100 campaign brought every branch's help output and README to the house pattern: 17/17 branches at 100%. ### Added - **`aipass new `** (module + handler): creates `projects/` with registry-first credential linkage (`registry.metadata.id == passport.citizenship.registry_id`), empty/python templates, interactive template + agent prompts (flags for scripted use), a full framework agent (entry point, modules/handlers skeleton, trinity set, mailbox, tier files), git init + birth commit, and next-step output. 49 tests. - **seedgo standards 41–42**: `cli_ux` (8 AST checks — two-tier help, Rich console, styled title, purpose line, --help pointer, Usage, Examples, no exposed internal plumbing) and `readme_quality` (Quick Start with runnable code block, stranger accessibility, invoke/entry-point match, early what-description). 36 tests + case-resolution regression tests. - **ai_mail cross-project boundary**: sender and recipient project roots compared on delivery; cross-project sends refused with a feedback-channel pointer. Fail-open for internal/unregistered sends. 13 tests. - **Root `.gitignore`**: `projects/*` ignored (each project is its own repo); only the future catalog README stays trackable. ### Added (second pass — the agent becomes a real citizen) - **`aipass new` agents are now spawn-issued full citizens** (FPLAN-0334): the hand-rolled scaffold in the new_project handler is retired for a `spawn_agent()` call against @spawn's new `project_agent` template — branch prompt, structured mailbox, birth certificate, trinity trio, dashboard, house-pattern entry point, and a branch-style README. One authority issues citizens; project agents inherit template evolution for free. - **Agent home = `src///`**, mirroring the host's `src/aipass/` layout (door-test ruling: the project root is never an agent home). Seat paths are relative like host seats; the registry walk stops at the first project registry. The first agent is the project's **manager** (`citizen_class: manager` — its devpulse), named after the project. - **Birth-commit hygiene**: the `.venv` symlink (absolute host path) and the registry lock file are no longer tracked in new projects' birth commits. - **Boundary verified live in all four directions**: host↔project email and dispatch all refused — project→host lands on the ai_mail cross-project check with its feedback-channel pointer, closing the leak found in the S319 prototype probes. ### Changed - **aipass front door rebuilt**: `--help` now follows the house pattern with a curated command list, usage, and examples (internal plumbing hidden — `doctor_fix`/`doctor_wire` renamed underscore-private); `aipass help` shows the Q&A screen instead of falling through to the module dump; README rewritten to pass the stranger test with a Quick Start and the correct invocation. - **Fleet-100 sweep**: 15 branches gained Quick Start READMEs and/or Usage/Examples help sections — each owner fixed their own front against the new gate. - **Debug_Print detector hardened**: the regex-based checker matched `print(` inside string literals (flagging cli_ux_check's own error messages — the auditor was the last branch under 100%). String content is now stripped before matching, with a regression test; plus a depth-5 nesting refactor in the same file. **v2.7.2** — everything merged since v2.7.1, headlined by the compass decision engine v2: curation with supersedes links + write-time conflict advisories (Track 1), and ambient recall — rated past decisions now surface verbatim into live sessions on matching prompts, governed by session caps and spacing (Track 2). Also in this release: the plan close pipeline completes itself (auto-vectorization + crash-safe registry writes), drone's 3-layer subprocess timeout policy with a collision-free `--drone-timeout` flag, plan-number memory search that pins the exact plan, a fleet-wide seedgo 100% restoration, and SSH-signed commits now verifying on GitHub. Details in the sections below (2026-07-16 carries the full stories). ### Changed - **Release cadence ruling: every dev→main merge ships a PATCH bump + tag by default.** PyPI tracks main, always current; version numbers carry no significance during beta — the big jump is reserved for beta exit. - **Merge playbook SOP refined from live run PPLAN-0010.** The raw `git fetch origin main:main` step (now blocked by the git gate) is replaced with `drone @git sync` in both places it appeared, and the template opens with the exact create command (`drone @flow create . "Merge summary" merge pplan` — template name before type), closing the trap where the wrong arg order silently stamps the default template. ## [2026-07-16] ### Added - **Compass ambient recall — Track 2 (DPLAN-0246/FPLAN-0332): rated decisions surface unprompted.** On every user prompt, a new hooks handler (`compass_recall`, registered in `.aipass/hooks.json` only) queries compass FTS with the prompt text and injects matching rulings VERBATIM — `[BAD] #56: ` — tidbits, never vibes. Three branches, one pipeline, each piece behind a modules/-boundary API: devpulse's `recall_decisions()` (side-effect-free scored candidates; rare-token evidence scoring + a query-side stopword filter so greeting/filler words can't fake relevance) + `mark_surfaced()` (counts only real injections); @memory's pure `should_surface()` governance (promoted from the dormant symbolic engine: threshold, 5/session cap, 10-message spacing — first surface exempt, 300s cooldown, dedup; state-in/state-out, caller persists); @hooks' 90-line handler + engine per-handler budget (errors never block a prompt — `compass_recall_unreachable` log signature for @trigger's watcher). Live acceptance matrix through the real bridge: topic-with-history prompts recall the right ruling (a CI prompt surfaced the red-CI-never-parked ruling), small talk and greetings stay silent, repeat prompts gate on spacing. Review caught and fixed pre-ship: wrong payload key (`userInput` → `prompt`), phantom `CLAUDE_CODE_SESSION_ID` env (session id is stdin-payload-only), spacing gate blocking the first surface, and a trust registry re-enrollment gap that silently disabled ALL project hooks for 20 minutes after the hooks.json edit. 446 devpulse + 1011 memory + 1129 hooks tests green; seedgo 31/31 on every touched module. - **Compass curation v2 Track 1 (DPLAN-0246/FPLAN-0331): supersedes links + write-time conflict check.** A correcting compass entry now archives and links what it replaces in one transaction (`compass add --supersedes N`); query renders both directions ("supersedes #N" / "ARCHIVED — superseded by #M") so a retracted decision can never masquerade as current truth. Every `compass add` FTS-checks the new text against active entries and prints a non-blocking "possible conflict with #X" advisory — flag-and-ask, no LLM, no auto-resolve (boardroom ruling). New `compass note ` command (FTS re-index proven by test), `--include-archived` query flag (the avoid-list is finally searchable), dead `score` column removed from all code surfaces (kept inert on disk — zero migration risk). Idempotent PRAGMA-checked migration ran clean on the production store (128 rows, no loss); the four fresh-eyes-audit archive pairs got their links backfilled. /prep now runs one `compass review` per session — curation living in a path that already runs, the lesson of all three compass eras. 435 devpulse tests green, seedgo 31/31 on both touched modules. - **Close pipeline completes itself (DPLAN-0245): auto-vectorization + crash-safe registry writes + drone timeout policy.** Closing a plan now produces all side effects from one command — `post_close_runner` invokes @memory's plan intake directly after archival (detached, loud on failure, drains any backlog it finds), so plans can no longer silently pile up unvectorized. Plan registry saves (@flow `save_registry` + mbank `save_flow_registry`) now use the O_EXCL lockfile + atomic tempfile-and-replace pattern, closing the same lost-update race class fixed earlier in CLOSED_PLANS. @drone gained a 3-layer timeout policy: per-command overrides (`@memory process-plans` 120s, `@flow close` 90s), a `--timeout N` flag, 30s default — replacing the flat 30s guillotine that killed legitimate long commands mid-pipeline; the timeout error now says how to override. Proven end-to-end live: one `drone @flow close` on a throwaway plan yielded archive + vectors + ledger + registry with zero manual steps, and the auto-trigger swept a pre-existing backlog file on its first run. 730 flow + 878 drone tests green, seedgo 100%. ### Fixed - **CI seedgo gate back to 100% across all 17 branches.** The Track 2 compass recall code left three branches at 99%: @hooks' compass_recall handler was missing json_handler operation logging and had two silent catches (now logged); @memory's governance module held its implementation in modules/ (moved to handlers/governance/engine.py with modules/governance.py as the thin re-export — the cross-branch import path is unchanged and live-E2E verified through the real bridge); devpulse's README test count had drifted (309 → 348). Audits re-run per branch: 100% overall, all suites green. - **Plan-number memory search hits the exact plan.** Searching a plan ID ('DPLAN-0244', 'fplan 0332' — any case, dash or space) now pins the exact plan as the top result at 100%, via a metadata lookup on the vector store's source-file field instead of embedding similarity (which treats all plan IDs as near-identical strings and never surfaced the target). Patrick ruling: searching a plan number must return that plan first. Semantic search quality for normal queries is unchanged. Also purged 193 junk vectors — throwaway probe/flaky test plans from scratchpad sessions (dv4 batch, probe_test_plan, throwaway_e2e_proof) that had leaked into the store. 1011 memory tests green. - **drone --timeout collision: router flag swallowed module flags.** The DPLAN-0245 subprocess-timeout flag consumed the first `--timeout` token anywhere in argv, so module-level flags silently vanished — watchdog's `--timeout 1800` never arrived and long watches died at the 600s default (live repro x2). Drone's flag is now namespaced `--drone-timeout`; plain `--timeout` passes through untouched to the target module, with a regression test pinning the passthrough. Per-command overrides intact. 879 drone tests green, seedgo 100%. - **@memory command routing eaten by the new governance module.** The governance module shipped in Track 2 had the wrong `handle_command` signature (`args: list` instead of `command: str, args: list`) and always returned True, so auto-discovery routed EVERY @memory command through it first — `drone @memory search` answered "governance: unknown command 's'". Fixed to the standard signature returning False for commands not its own; search verified live (135 results). Library modules must decline commands they don't own or they silently hijack the whole CLI. 1011 memory tests green, seedgo 31/31. ## [2026-07-15] ### Fixed - **Plan vectorization pipeline unwedged (DPLAN-0245): 57 closed plans were silently missing from semantic memory since mid-June.** Vector IDs were pure content hashes, so identical template boilerplate across different plans produced duplicate IDs within one ChromaDB upsert — the store rejected the entire batch, and the all-or-nothing intake retried the same failing batch forever. Fixed in @memory: IDs are now salted with the source filename when present (rollover hashes unchanged — no re-vectorization churn), in-batch dedup as a safety net, and `process_plans()` now runs per-file with the manifest saved after each success so a poison file can never wedge the queue again. Backlog drained and verified: 229/229 archived plans vectorized, 1112 chunks, formerly-lost plans answering semantic queries at 85%+ similarity. 990 memory tests green. - **CLOSED_PLANS ledger append race (@flow): concurrent plan closes lost entries.** `append_to_closed_plans` was an unlocked read-modify-write; the S314 bulk sweep lost 18 of 21 entries to it (reconciled by hand). Now guarded by an `O_CREAT|O_EXCL` lockfile with retry/backoff, and the previously silent append failure is surfaced in close output and logs. 730 flow tests green. - **Telegram routine read-timeouts no longer logged as errors (@skills, Patrick ruling): ends the medic wake-loop.** A routine long-poll read timeout (`socket.timeout` — an `OSError` subclass) slipped past the earlier `URLError`-only guard into the network-outage path, logging ERROR once per episode (~576 lines/30h) and waking @trigger's medic each time. The `_is_routine_read_timeout` guard now covers the `OSError` handler too, and the genuine-outage episode-start line is demoted ERROR→WARNING (backoff self-heals; recovery already logs INFO; medic only fires on ERROR/CRITICAL). Real failures still log ERROR. 825 telegram tests green. ### Security - **Hook config trust model hardening (DPLAN-0244): closes a zero-interaction RCE from untrusted `.aipass/hooks.json`.** The hook loader walked up from CWD and trusted any `.aipass/hooks.json` it found; since the bridge is wired globally in provider settings, a hostile repo shipping a `command`-type hook could execute arbitrary shell on `SessionStart` with no user interaction. Fixed with defense-in-depth. **Layer A (engine):** per-project configs may no longer run `command`-type hooks (refused via an unconditionally-stamped `_source` provenance flag), and handler paths are gated to the `aipass.*` namespace. **Layer B (loader + CLI):** a trusted-project registry (`~/.aipass/trusted_projects.json`, path + sha256) that the loader checks fail-closed; on upgrade it bootstraps **only** the `$AIPASS_HOME` install (never trust-on-first-use of an arbitrary directory); `aipass init`/`init update` auto-enroll, and new `aipass trust`/`revoke` commands manage enrollment. Both gates proven to block the attack independently via a live acceptance test driving the real bridge with a real payload. 1105 hooks + 133 aipass tests green. Origin: external scan (false positive at `engine.py:37`) whose triage surfaced the real adjacent hole. ### Added - **Supply-chain hardening pass (DPLAN-0243): commit signing + hash-pinned CI tooling + release provenance.** All commits are now SSH-signed via a dedicated repo-scoped signing key (first signed commit 9048666c, verified `Good "git" signature`). Every standalone pip tool install across the four CI workflows now installs `--require-hashes` from lock files in `.github/requirements/` (pip/ruff/build/pytest/pip-audit), generated with full multi-platform hash coverage — including the Windows `colorama` marker dependency that naive Linux-side pinning silently drops. `publish.yml` gained a SHA-pinned build-provenance attestation step (activates on the next release), and Dependabot now watches the new lock directory as a grouped `pip` ecosystem. Editable `-e .` installs untouched. Full 31-check CI matrix green on the change. Driven by the OpenSSF Scorecard gaps surfaced via hvtracker.net (HVTrust 82.0, #1 in Multi-Agent Systems); detector-gap correction filed upstream as YugantM/hvtracker#186 (Claude Code-native projects misread as "no Anthropic dependency"). ### Fixed - **CI green pass on the runaway-log PR — every red was ours, every fix verified.** Morning-after triage of PR#696's failing checks: the test matrices' only failure was the known parked flake, but lint and the seedgo audit were genuinely red from the previous night's new code. One `ruff format` on trigger's runaway-handler tests fixed lint. The audit findings went back to their owners by dispatch: @hooks built the branch's missing json_handler and wired it into `persistent_alert`/`alert_dismiss`, added the introspection no-args gate, and flattened `_menu_live()`'s nesting (1071 tests green); @prax refactored `rate_tracker.py` to dependency injection — the module layer now injects `logs_dir` and `trigger.fire` via `configure()`, so the handler carries no cross-handler or handler→module imports (1028 tests green). Both branches re-audit at 100% across all 41 standards, independently verified. Detection re-proven live post-refactor with a fresh planted log storm. Also trimmed the tier-1 navmap prompt (9.3k → 7.9k chars, under its ~8k injection cap) with the comms doctrine intact. - **Windows flake pinned: `test_is_pid_alive_dead` escaped the ca096295 sweep.** That commit's rule — tests mocking `os.kill` must pin `sys.platform="linux"` because Windows takes the ctypes OpenProcess path and never reaches the mock — was applied to every pid-liveness test except this one. It only failed when PID 1234 happened to be alive on the runner (environment lottery, first hit today). Pinned like its siblings. The remaining Windows session_boot reds and the relay mtime-cache flake predate this PR and stay parked. - **The parked reds, unparked — Patrick's ruling: red CI is never parked.** "If CI is red, it's because you or I left it red." Both remaining reds fixed by their owners the same hour. @prax root-caused the relay mtime-cache flake: the test only passed when two writes landed in the same mtime-granularity window (true locally, false on CI runners) — fixed by pinning mtime with `os.utime` so the cache contract is tested deterministically, proven 50/50 + 20/20 loops. @hooks root-caused the four Windows session_boot reds: the boot path's `_tmux_session_exists()` ran a real `subprocess.run(["tmux", ...])` that Windows runners can't satisfy (WinError 2) — mocked in all four tests per the ca096295 convention, leaving `execvp` (already mocked) as the only terminal call. Ruling recorded in compass; the "forget CI" era is over. - **Burst-evasion closed: bursty runaways can no longer slip past the rate tracker.** Found live during the morning's chain verification: any single below-threshold 10-second scan window zero-reset the sustain counter, so a bursty writer (20 short lines every 6 seconds — 200 lines/min average, the exact retry-loop-with-sleep shape of the TG relay incident) ran 4 minutes undetected. @prax's fix (rate_tracker v1.2.0): severity now evaluates `max(instant_rate, 60s window average)` — continuous writers behave exactly as before (instant rate dominates), bursts sustain through their gap windows, and subsidence still clears as zeros fill the window. Four new burst tests; live-proven with the previously-evading storm pattern: `RUNAWAY WARNING: prax_burst_storm_test.log — 191 lines/min sustained 120s` in the tracker log, fired from the restarted running service. Detection evidence now spans all three storm shapes: continuous fast (332/min), continuous moderate (257/min), bursty (191/min). ### Added - **TG streaming v2 polish (DPLAN-0229): the last two finalize paths now honor the streaming flag.** v1 shipped with a deliberate gap — when logs were active mid-turn or the final response exceeded 4096 chars, the Stop hook fell back to "Done." + a fresh message, orphaning the streamed bubble. @hooks threaded `streaming` through `_deliver_chunks`: logs-active now reconcile-edits the streamed message with the final formatted response, and multi-chunk edits chunk 1 in place then sends [2/N]+ as continuations. Batch mode is verified zero-change (regression tests for both paths), plus an edit-fail fallback. 6 new tests, 1077 green. Live streamed-turn proof pending Patrick's next streaming session — honestly flagged, not faked. ## [2026-07-14] ### Fixed - **Prax TG relay gets the same offline backoff as the bots.** Found in Patrick's live plug-pull test: the bots went quiet correctly, but the monitor→Telegram relay kept logging `Send failed` every ~5 seconds (89 lines, no backoff) — and each failed-send error was re-ingested by the log watcher, feeding the relay more events to fail on. Now network-class send failures put the relay in offline mode: doubling backoff (1s→60s cap), flush gate skips sends while offline so the monitor loop never blocks and viewers keep rendering, log-once semantics (one enter line, one 5-minute summary, one recovery line with drop count), full reset on first successful send. 11 new tests; 1007 prax green. - **TG bots no longer hot-spin when the internet drops.** Live find from Patrick's on-location tether outage: DNS failure makes `urlopen` fail instantly (no 30s long-poll wait), so the shared poll loop retried as fast as it could — up to 13 ERROR lines/second per bot, all 5 bots spinning for the whole offline window (rotation saved the disk; nothing saved the CPU, and the flood tripped the medic circuit breaker fleet-wide). Now network-class poll failures (DNS/connection/socket, classified via `_NetworkPollError`) back off exponentially 1s→60s cap and reset on the first successful poll, with log-once semantics: one "unreachable, backing off" line, one summary per 5 minutes while offline, one recovery line with suppressed count. Routine long-poll read-timeouts (expected getUpdates behavior, ~863 medic-suppressed events/day) no longer log at all. Bots still self-recover the moment connectivity returns. 25 new tests; 822 TG + 252 skills green. ### Added - **Runaway-log detection + escalation — designed and built by the agents themselves.** Patrick's mission brief went to @prax as lead ("I don't want it to be you" — devpulse relayed requirements, not a design): prax researched, collaborated with @hooks and @trigger by mail, wrote DPLAN-0242, and ran the build as TDPLAN-0013 across three branches. The system: @prax `rate_tracker` watches every log in `system_logs/` for volume (not content — orthogonal to medic), disk-persisted state, WARNING at >100 lines/min sustained 2 min / CRITICAL at >10 lines/sec 1 min, per-file suppression, runs as a 4th monitor thread, plus `drone @prax log-health` for an at-a-glance rate overview. @trigger registers the new `runaway_log_detected` event and dispatches to the responsible branch with a per-file 30-min cooldown deliberately independent of medic's circuit breaker (a storm can't silence both systems), UNKNOWN attribution falls back to @prax, and every alert is written to `.aipass/alerts.json`. @hooks `persistent_alert` injects an advisory banner into every agent's prompt until the alert is fixed or dismissed (`drone @hooks dismiss `) — general-purpose, any agent can raise alerts. Devpulse verification found and fixed the last-mile gaps: both hooks pieces stopped at the first `.aipass/` dir walking up (every branch has one — the banner could never render), and hooks.json registration alone isn't deployment — the handler needed manual wiring into `~/.claude/settings.json` (agents can't edit it; documented for future handlers). Live-fire acceptance: a planted 240 lines/min storm was detected at 257 lines/min sustained 120s → event → dispatch → **@aipass woke autonomously, root-caused the test writer down to its PID and loop shape, triaged no-action** → alerts.json → banner renders → dismiss clears. ~77 new tests across four branches, suites green (prax 1028, trigger 619, hooks 1071), seedgo 98–100%. - **Citizens wake each other freely — wake-back for everyone, devpulse unwakeable by design.** Patrick's ruling after two team-mission stalls in one evening (prax emailed sleeping collaborators; trigger replied instead of dispatching back — replies never wake, and wake-back was owner-gated so agent-to-agent dispatch never woke the sender). @ai_mail removed the owner-gate: any citizen sender is woken when its dispatched agent completes (proven live: "@trigger woken after @aipass completed" — first citizen wake-back ever). @devpulse is now structurally unwakeable via a `citizen_class: manager` check on every wake path — mail always lands, wake always skips, no longer dependent on an interactive session happening to be open. The gate removal exposed a self-wake loop within minutes (wake-back sessions were attributed to @ai_mail as sender, so ai_mail kept waking itself; the depth cap stopped it after one cycle) — fixed the same night: self-wake guard + wake-back sessions carry no sender, so chains terminate at the original dispatcher. 765 ai_mail tests green. The navmap gained a "Talking to other agents" section: dispatch vs email semantics, team-relay discipline, and the manager exception. - **Medic is back on — and the loop is proven live.** Off since 2026-05-10 (a pytest fixture storm flooded the error registry; the off switch was pulled to stop the noise and forgotten for 65 days). Three fixes made re-enable safe: (1) @prax: pytest logging routes to a temp dir when `PYTEST_CURRENT_TEST` is set — test fixtures can never pollute production `logs/` again (the storm class that caused the shutdown); (2) @trigger: circuit breaker self-heals — open breakers half-open on read, close on a successful probe, cooldown decays to base (previously `half_open` was a terminal trap and only manual reset recovered); (3) @trigger: **TTL mutes** — `medic mute @branch` and `medic off` now auto-expire after 24h by default (`--for 48h/7d` custom, `--forever` explicit kill switch; temp `off` keeps detection running). Agents doing build work mute themselves and never have to remember to unmute — the permanent switch that got medic forgotten no longer exists. Breadcrumbs shipped: ai_mail footer + navmap tell every agent to mute before build work. Live-fire proof: a planted commons SQL bug was detected, dispatched, and fixed byte-identical by @commons in 105 seconds (15/15 tests green); a real TG poll error was correctly triaged NOT ACTIONABLE; organic instance-lock noise was correctly triaged LOW/expected. @skills/@api on 7-day mutes until the TG poll-level fix lands. 993 prax + 603 trigger tests green. - **Prax monitor: concurrent viewers — laptop and Telegram mirror side by side.** Patrick's ruling after being locked out of his own monitor three times: *processes are not agents; display processes must never be single-instance.* The instance lock is gone from the display path — any number of `monitor run` viewers start and render concurrently. The lock is scoped to the one true single-writer responsibility: the Telegram relay (`relay.pid`, held by `prax-monitor.service`); extra instances run viewer-only, so no TG double-sends. The misleading "kill the existing process" error is dead. 998 prax tests green, 3 new concurrent-viewer tests; live-verified: interactive Mission Control rendering while the TG relay service runs untouched. - **Telegram user-comment mirror: the TG chat now shows the whole conversation, whichever door you speak through.** Patrick's spec from the live cross-door drill: his own messages typed in the terminal or claude.ai remote never appeared in TG — only the replies did. New `user_message_relay` UserPromptSubmit handler (@skills-built, self-contained in the telegram skill, registered by @hooks as the last, crash-isolated entry) posts genuine user messages to the branch's TG chat with an origin tag, silently (`disable_notification`). Noise fences keep it human-only: system/task notifications, slash-command output, dispatch wake prompts, sub-agent prompts, TG-origin echoes, and consecutive dupes are all skipped (structural session-type detection was investigated and rejected — it's session-wide, would eat genuine mid-flight messages). Inbound hardening rides along: stale pending files cleaned before each write, and an undelivered-response overwrite now logs a warning instead of silently losing the reply. 47 new TG tests; registration execution-proven via engine.jsonl and the positive path live-verified — a terminal-door message delivered to the real TG chat. TG dormancy/proactive push deliberately untouched (design chat with Patrick pending). ### Fixed - **TG bot heartbeat race: delivered replies no longer flip back to "Processing…".** Patrick watched his answered bubble get overwritten live: a heartbeat thread stuck >5s in a slow Telegram edit call survived its stop (the join timed out), woke to a *shared* stop Event the next message had already cleared, and re-edited the old placeholder with "Processing… (elapsed)" over the delivered reply. Fixed structurally (@skills, devpulse root-cause brief): a generation counter captured per heartbeat thread — any stale thread breaks before every edit — plus a delivered re-check immediately before each edit call in both batch and streaming loops. Second bug in the same window: rapid-fire messages (photo + text in one turn) overwrite the bot's single pending slot, stranding the earlier placeholder frozen; superseded placeholders are now finalized to "⏭ Superseded by newer message" in both message and file paths. 6 new heartbeat tests; full TG suite 797 green (devpulse-verified). Deployment lesson from the same morning: bot fixes aren't live until the systemd units restart — commit ≠ deploy. - **TG mirror live-test fixes: main-chat messages mirror, TG messages don't echo.** Patrick's first morning test caught what 47 green tests missed: the relay's sub-agent skip blocked ALL daemon-backed main chats (they run with `--agent claude`, so `agent_type="claude"` — and real sub-agents never fire UserPromptSubmit at all; the filter's premise was empirically wrong across the entire engine log). Skip is now agent_id-based (defensive, never observed). Second catch from tracing his test: TG messages inject into tmux as raw text — no `via Telegram:` marker — so the TG-origin filter never matched and every TG message would have echoed back once the first fix landed. New structural gate: the bot stores the injected prompt in its pending file; the relay skips a prompt that text-matches a fresh undelivered pending entry. Mirror proven live by Patrick across both directions ("success :)"). 791 TG tests green. - **DPLAN-0241 round 4 (night shift): user flags survive every launch path, and every session is born with an honest name.** R6 — the bug behind Patrick's approve-everything chat: the boot menu suppressed its bypass defaults when the user passed `--permission-mode` himself, but only the fresh-launch path threaded the user's flags into the exec — resume, takeover, continue, and dead-window paths all launched flagless. `extra_args` now threads through ALL launch paths (headless `-p` included). R7 — auto-namer: every launch is stamped `--name -` (flag live-verified on claude 2.1.209; a user-passed `-n/--name` wins), so made-up auto-names can no longer hide which chat is which. Plus four drill nits: new-over-all ABORTS if the daemon stop fails (one brain even in failure paths), close-all's failure hint no longer recommends the mechanism that just failed, `exit`/`q`/`quit` quietly leave every menu, session rows stay rich (PID, kind, name, age). Surgical-stop probe: `op:kill` exists in the daemon's Unix-socket control protocol (per-job bg stop, 8-char sessionId prefix, no auth) — documented in DPLAN-0241, deliberately NOT shipped: undocumented internal protocol. 1048 hooks tests green (102 session_boot, 11 real-binary CLI contract). ## [2026-07-13] ### Fixed - **DPLAN-0241 rounds 2-3: Enter IS the takeover — background chats reopen as normal terminal chats.** Live incident round two (Patrick's laptop, 23:00): the boot menu's resume for a background chat opened the `claude agents` viewer, which dispatched his typed message as a brand-new bg job WITHOUT bypass permissions — and the shipped stop path called `claude agents stop`, a subcommand that does not exist (987 mocked tests never noticed). All fixed by @hooks across two rounds, every CLI fact live-verified against claude 2.1.208: phantom stop removed (bg close is now honest — no per-job stop exists in the CLI; SIGTERM never used on bg, the daemon respawns it); Enter on a live bg session now takes the chat over — `claude daemon stop --any` (returncode-checked, blast-radius listing + y/N confirm when other branches' bg sessions would also stop) then `--resume ` inside tmux with bypass; ALL interactive launches tmux-wrapped so a closed terminal is always recoverable; multi-session menu shows real session names, requires an explicit pick, and its new/close paths stop-first honestly; new real-binary CLI contract test tier (20 tests probing every claude flag/subcommand our code invokes — the phantom-subcommand class is now structurally unshippable). 1025 hooks tests green. North-star architecture recorded from Patrick's rulings: one conversation per branch; TG/claude.ai/ terminal are views of it; agents bind to the machine, not the interface. - **Session management overhaul (DPLAN-0241): one brain per branch, attach-first boot menu, honest session listings.** Born from a live incident — Patrick locked out of a running chat for an hour. Root causes, all fixed by @hooks: the bashrc boot shim hijacked EVERY `claude` invocation (so `claude agents`, the real attach path, never executed) — now intercepts only bare/`--permission-mode` launches; session_boot printed one PID from a list and advised `kill` for daemon-managed background sessions (which respawn — the unwinnable loop) — now a 3-option boot menu (resume / start-new-closes-old / close) with per-kind proper stops; presence_gate (single-session enforcement) had NEVER run in production (`provider_wired: false`, absent from settings.json, zero engine entries ever) and carried two latent bugs (self-PID resolver matched comm=="claude" but CC binaries are version-named; agent_type skip waved through daemon bg sessions) — both fixed, wired, shipped OBSERVE-ONLY for a soak period per prior-art recall (the gate false-blocked a real resume in the PRESENCE-file era); wire_verify no longer excludes unwired security hooks from its check (enabled-but-unwired = ERROR); new `drone @hooks sessions` + `sessions reclaim` one-command reset; session listings/names standardized to `PID · branch · short-id · kind · age`. Verified live: gate's first production run correctly logged a would-block for a real duplicate session without self-blocking. 987 hooks tests green (26 new/updated). ## [2026-07-12] ### Added - **Telegram log-stream control: `/logs` on branch bots + interactive Prax Monitor chat.** The per-branch session LogStreamer auto-started on first message hardwired to full firehose with no off switch; the Prax Monitor relay chat was send-only — no command menu, and anything typed there was silently never read (nothing polled that token). @skills added `/logs on|errors|off|status` to all branch bots (preference persisted per chat, honored by the auto-start; 33 tests) and a new `PraxMonitorBot` receiver service (`telegram-bot@prax_monitor`) with `/pause /resume /errors /all /status` and a registered command menu (34 tests). @prax made the relay honor the shared control file (`~/.aipass/telegram_bots/ prax_monitor_control.json`, frozen contract: paused + level) each 5s flush — paused discards, `errors` filters to WARNING/ERROR/CRITICAL (17 tests). Live-verified end-to-end from Telegram Web: `/errors` silenced INFO batches within one flush, `/all` restored them. ### Fixed - **Legacy `builder` citizen_class migration + birth-certificate template (fixes #692).** `builder` was renamed to `aipass_framework` on 2026-07-01 (13463c0c) as a pure rename, but passports minted pre-rename kept the retired name, and the seedgo Architecture checker requires `spawn/templates//` — hard-capping those citizens below 100% (Vera Studio's @vera/@writer stuck at 99%; same legacy class found in 6 external projects). @spawn completed the rename instead of resurrecting a `builder` template: `sync-registry --fix` now migrates the exact value `builder` → `aipass_framework` in passports (idempotent, dry-run safe, 3 new tests), so external projects self-heal via `aipass doctor --fix`. Also fixed the template leftover that kept minting the retired name: `birth_certificate.json` now renders `{{CITIZEN_CLASS}}` like the passport does. Verified: dry-run against Vera Studio's live registry plans exactly the two migrations with zero writes; spawn 347 tests green. #695 closed won't-fix (armed Monitor-tool watchdog is the dispatch indicator; always-arm is the rule). - **Order-dependent `test_missing_file` + skills test litter (fixes #694).** Root cause was @prax's `json_handler_module` fixture popping EVERY branch's json_handler from `sys.modules` (never restored), orphaning the module object @skills' conftest had patched — `test_missing_file` then re-imported a fresh module pointed at the real `skills_json/`, planted `ghost_config.json`, and failed on it every later full-repo run (the only failure in an 11k-test sweep). @prax scoped the eviction to `aipass.prax.*` via `monkeypatch.delitem` (auto-restore). @skills made all 4 resilience tests hermetic (patch `SKILLS_JSON_DIR` → `tmp_path` inside the test body, immune to sys.modules state), fully-qualified the legacy bare `skills.` `BRANCH_MODULE` in 3 test files (the source of the remaining litter), and fixed a latent wrong-variable assert. Verified: original failing pair now passes both orders, prax+skills+spawn 1576 tests green, `skills_json/` stays clean after a full run. ## [2026-07-11] ### Added - **Owner seating made permanent + self-healing for every project (DPLAN-0239, fixes #693).** The owner-capability guard was correct but the DATA was never seeded: every project created before 2026-07-10 had its owner only in the self-editable passport, never in the sealed registry (8/8 external projects unseated; AIPass's own registry was missing `metadata.id` with 13 entries sharing one stale id). Identity model settled: registry `metadata.id` = project credential (passports conform); branch-entry `registry_id` = set-once PER-CITIZEN UUID minted at entry creation; entry `owner:true` = the authority gate (first agent), chosen by ONE shared heuristic (`pick_owner_branch`: manager → passport owner → first-created). New: `drone @spawn sync-registry --check [--json]` (read-only, 7 health flags, pinned JSON schema) and `--fix [--dry-run]` (idempotent reconcile: seat owner, majority-consensus restore of `metadata.id`, mint citizen UIDs, align passports; dry-run fully read-only; never moves a seated owner). `aipass doctor` renders owner health per flag; `doctor --fix`, `install`, and `init update` delegate repair to spawn — existing/external projects self-heal on next update (the missing DPLAN-0231 PART-4 trigger). The adopt path now seats owners; `placeholders.py` resolves the registry from the target dir (was CWD) and fails loud. @hooks `auto_watchdog` now injects the real Monitor-tool watchdog command with the actual @target (was a dead one-liner + `run_in_background`, which cannot wake a session). Deployed live: AIPass + 6 external projects reconciled and verified clean — VERA is now seated owner of Vera Studio (`is_owner('@vera') = True`, was refused). Owners built (spawn 343 / aipass 673 / hooks 961 tests green); devpulse verified every diff, live-ran every stage, full-repo sweep 9364 passed (1 pre-existing skills litter fail → #694). ### Changed - **Fleet seedgo compliance sweep — every branch to 100% (issues #686, #661).** Overnight campaign bringing all branches to 100% on the seedgo standard pack. #686 (Subcommand_Help, per the #685 contract): entry points intercept ` --help` before dispatch, so `--help` shows help instead of executing. #661 (Output_Routing): status/error console output routed through the shared `@cli` `success()/error()/warning()` helpers instead of raw `console.print` markup. Owners self-audited and self-fixed their own branches; devpulse verified each diff + re-ran each audit and committed per wave. Landed so far: spawn, drone, flow, daemon, prax, ai_mail, backup, seedgo, memory, trigger, api, cli, aipass, commons — all 14 offenders now at 100%. **Fleet: 17/17 branches at 100% seedgo compliance** (hooks, skills, devpulse were already compliant). Owners self-audited and self-fixed; devpulse verified every diff, re-ran each branch's full test suite, and committed per wave. A full 17-branch test run (~10,349 tests) surfaced one pre-existing flaky test in drone (`test_pr_no_branch_dir` / `test_pr_no_args` lacked cwd isolation, so a real checkout's findable passport made the auth path pass unexpectedly) — given `monkeypatch.chdir(tmp_path)` isolation to match its sibling test, so the full suite is now deterministically green. ### Fixed - **Watchdog Monitor wake no longer double-fires (#693 follow-on, reported by VERA via the feedback channel).** The `watchdog agent` reminder banner ("invoke via Monitor tool, not run_in_background") printed to STDOUT at arm time, and the harness Monitor tool treats every stdout line as a wake event — so every armed watchdog fired a spurious wake the instant it armed, then the real wake at completion. Rerouted to stderr (`err_console`); stdout now carries completion/stall events only, matching the contract the agent handler already followed. Verified live: exactly one wake, on real exit. The devpulse README watchdog/feedback sections were also rewritten to document the owner gate, the 3-step Monitor wake mechanic, why no passive wake can exist, and the 600 s default timeout. - **Watchdog agent tests thread-race flakes made deterministic (devpulse).** Four tests patched the GLOBAL `time.sleep` with stateful/side-effecting fakes; prax's logger spawns daemon threads on first log, which executed the fakes concurrently with the test (advancing a fake clock, unlinking the fixture lock early, or re-truncating `last_bounce.json` mid-read in `_classify_exit` → `exit_code=None`). All fakes are now thread-scoped via a caller-frame guard: only sleeps from the agent module trigger the test's side effect; foreign threads get a real 1 ms sleep. - **seedgo-audit back to 100 % after the S300 commits (PR659).** Two 99 % regressions from that day's own work: `aipass` `doctor.py` `_fix_owner_seating` had two silent catches (now log via prax like the sibling check function), and the devpulse README claimed 407 tests where the readme checker counts test functions (corrected to 309). - **Two more non-hermetic ai_mail tests made deterministic (PR659).** With the full suite now running on varied CI runners, `test_get_pid_cwd_darwin_failure` and `test_is_zombie_linux_no_proc` intermittently failed: they called the real `lsof` (via `subprocess.run`) and real `open("/proc/…")` for a fixed PID (999 / 99999), so on a runner where that PID happened to exist they returned a non-`None` result instead of the expected failure. Mocked `subprocess.run` and `builtins.open` so the tests assert the failure contract without touching real process/`/proc` state. Test-only; deterministic across repeated runs. - **Windows CI cross-platform fixes — `windows-setup` green (PR659).** Fixing the telegram collection errors unmasked 14 pre-existing Windows-only failures across six branches. Two root causes. **(1) pid-liveness tests** (ai_mail, flow, hooks, skills) mocked `os.kill`, but the production `_is_pid_alive` already branches to a ctypes `OpenProcess` path on Windows and never reaches `os.kill`, so the mocks had no effect and the real path ran instead — pinned `sys.platform` to `linux` in those tests (or patched `_is_pid_alive` directly) so they exercise the POSIX contract deterministically on every platform. **(2) POSIX path assumptions** — prax's jsonl test hardcoded `/some/path` (backslashes under `str(Path)` on Windows) now asserts against `str(test_path)`; hooks' rollover test compares `repr()` (matches `%r` logging); ai_mail's darwin lsof-parser test uses a fixed POSIX path; and seedgo's `is_bypassed()` now normalizes the rule file via `Path(rule_file).as_posix()` before matching (the one production fix — Windows backslash rule paths never matched the forward-slash file path). 10 files (9 test, 1 code); owners self-fixed, devpulse verified every diff + Linux no-regression (525 changed-test assertions green). - **Flaky `test_deletes_old_system_log` made deterministic (@prax log-sweep tests).** The sweep integration test reached `log_watchdog._get_system_logs_dir` through a `_get_sweep()` wrapper and patched it by string path; a sibling test (`test_logging_handlers.py`) `sys.modules.pop`s and reimports `log_watchdog`, creating a second module object — so the string patch could target a different object than the function's `__globals__`, the sweep scanned the real (empty) `system_logs/`, removed 0 files, and `assert files_removed == 1` failed intermittently (the same commit passed in one CI run and failed in another). Switched to a direct `import log_watchdog as lw` + `patch.object(lw, …)` (shared module `__dict__`) and patched `json_handler` to block real file I/O. Test-only (1 file); prax suite 978 green, two full-repo runs 11,019 passed each, sweep tests deterministic across repeated runs. - **Telegram skill tests made CI-safe — full-repo collection + hermeticity (issue #691).** The 16 test files under `skills/lib/telegram/tests/` imported handlers via bare `from apps.handlers…`, which collided with other branches' `apps` packages during full-repo CI collection (~16 ImportError collection errors → CI red on Linux + Windows). Converted to fully-qualified `aipass.skills.lib.telegram.apps.handlers.*` imports (and matching `mock.patch` targets). Verifying that fix surfaced a second problem the imports had exposed: ~11 tests reached the live Telegram API (`base_bot.run → _set_command_menu → set_bot_commands → urlopen`) — they had never run in CI before because they failed at collection. Added a session-scoped autouse `_block_network` conftest fixture that patches `urlopen` on the four network-using telegram modules (both bare and fully-qualified import paths, each guarded) so any test attempting a live HTTP call fails loud instead of hanging. A full-repo CI run then exposed a third layer the isolated suites had hidden: `handler.py` and its routing tests still used bare `from apps.handlers.X import Y` / `mock.patch("apps. handlers.X…")`, which resolve to the *wrong* branch's `apps` in a whole-repo run (AttributeError / ModuleNotFoundError at runtime — 17 `test_handler_routing` failures). Fully-qualified those to `aipass.skills.lib.telegram.apps.handlers.*` in both `handler.py` (7 lazy imports, now house-rule compliant) and the tests; the skill's runtime behaviour is unchanged (verified via `drone @skills run telegram`). Net: full-repo collection 0 errors and the whole 11k-test suite green; telegram suite 663 passed / 0 failed / 0 hangs, fully hermetic; coverage intact (import and patch-target rewiring only — zero assertion changes). - **`aipass install` from a throwaway path can no longer hijack the machine-wide `AIPASS_HOME` (issue #688).** A probe install run from a `/tmp` scratchpad had rewritten `~/.claude/settings.json` `env.AIPASS_HOME`, silently pointing every Claude Code session on the machine at a dead temp tree (stale python, stale hooks — surfaced as bogus ImportErrors in unrelated work). Three defenses: `bootstrap.is_throwaway_path()` gates the settings write itself (temp dirs + scratchpads never land in global settings); `run_install` refuses a throwaway home loudly with `--force-global-home` as the explicit override; and `aipass doctor` gains a `global AIPASS_HOME` check that flags a nonexistent or throwaway path with fix guidance. +11 tests. Ships with a probe-hygiene SOP (`aipass/docs/probe_hygiene.md`): temp installs are used, deleted, gone — nothing permanent may point at a temp path. Tests made location-independent so the suite is green from any cwd and from a `/tmp` clean-room extraction, not just the repo root. (built by @aipass, verified + test-hardened by devpulse against the real hijack path) ## [2026-07-10] ### Added - **Owner-capability model — project ownership sealed in the registry, and the owner is woken back when a dispatched agent completes (issue #678).** The directed-wake round-trip grew into an access-control primitive: watchdog / feedback / wake-back are owner-only privileges, and the owner (first agent / `citizen_class: manager` — devpulse in AIPass) is resolved from the *sealed* `*_REGISTRY.json`, not the self-editable passport (no self-grant). Three parts built in parallel against a frozen `is_owner` contract: `@spawn` writes `owner` + `registry_id` into registry entries and exposes `get_owner()` / `is_owner()` (`ensure_project_has_owner` now keys off the manager signal, not the created-date heuristic that mislabeled `@aipass`); `@hooks` adds a `registry_gate` PreToolUse handler that blocks raw writes/edits/deletes of `*_REGISTRY.json` and redirects to `drone @spawn` (per-clause bypass defeats compound-command smuggling; reads stay allowed); `@ai_mail` reslopes the dispatch wake-back from a `SKIP_SENDERS` blocklist to an `is_owner` allowlist. Cross-part verified end-to-end by devpulse with the real resolver (gate 13/13 incl. compound-smuggle, wake-back owner/non-owner/depth-cap). (built by @spawn + @hooks + @ai_mail, verified by devpulse) - **`subcommand_help` seedgo standard — entry points must intercept ` --help` before dispatch (issue #685, split from #665 item 3).** `drone @X --help` had no framework contract: drone (a router, not a standards enforcer) forwards `--help` as a positional, so behavior was per-branch — 8/16 missed, and two branches *executed* the subcommand instead of showing help. seedgo now owns the contract: a new AST checker flags entry points that don't guard ` --help` (explicit `remaining_args[0]` guard or argparse `parse_known_args`). 21 tests, cwd-portable. 7/17 branches comply; the 10 offenders are tracked as a fleet migration (#686). (@seedgo, verified devpulse) - **`windows_compat` now detects `os.kill(pid, 0)` liveness probes, not just documents them (issue #682).** `os.kill(pid, 0)` resolves to `TerminateProcess` on Windows — it *kills* the target instead of probing it. The checker documented the anti-pattern but never flagged it in source. A new detector recognizes the valid early-return platform guard (so the reference impl `watchdog/agent.py` isn't false-flagged) while catching genuinely unguarded sites. 6 tests; verified across the fleet (guarded ref passes, 10 offenders caught → fleet migration #684). (@seedgo, verified devpulse) - **`append_jsonl` — a sanctioned rotating JSONL writer + a 30-day stale-log sweep (issue #673).** Branches wrote `.jsonl` via raw `open('a')`, bypassing prax rotation (which was `.log`-only) — unbounded log growth. `from aipass.prax import append_jsonl` gives 500 KB / 1-backup atomic (`os.replace`) rotation with zero dependency on the prax logging pipeline (recursion-safe for @trigger's event handlers), and `drone @prax log-audit sweep` deletes logs older than 30 days across system + branch logs. The raw appenders in @backup (1), @hooks (2), and @trigger (11 `.log` sites → `.jsonl`, plus downstream medic readers) all adopted it — zero raw log appenders remain fleet-wide. (@prax + @backup/@hooks/@trigger, verified devpulse) - **Hook engine: Codex bridge + portable test suite (issue #635, DPLAN-0184 leftovers).** The engine now drives Codex hooks the same way it drives Claude: new `handlers/bridges/codex.py` mirrors the claude.py bridge (same `EventType:hook_name` dispatch) with Codex protocol normalization — stdin remaps `input`→`tool_input`, stdout wraps in the `hookSpecificOutput` envelope (`additionalContext` for injection, `permissionDecision` + `permissionDecisionReason` for blocks — fixing the known DPLAN-0205 bugs: missing reason, wrong field name). And `drone @hooks test` is a portable drop-in runner that fires every hook from `.aipass/hooks.json` with mock data per event type and reports fired/blocked/disabled/crashed with timing (`--verbose` previews output). 23 new tests (12 bridge + 11 runner), seedgo 31/31 both. (built by @hooks, verified by devpulse) ### Fixed - **hooks/bridge: `-p` headless invocations no longer routed through tmux (issue #677, DPLAN-0226 fine-tune leftover).** The boot wrapper (`session_boot.py`) applied its tmux/session-lookup/live-attach logic to every invocation — wrong for `claude -p`, a non-interactive one-shot that never registers in `~/.claude/sessions`. The wrapper now detects `-p` in extra_args and short-circuits to direct `execvp` of claude — no tmux, no session lookup. +5 tests (39 pass). (built by @hooks, verified by devpulse) - **Owner-capability PART 4 — devpulse's `watchdog` + `feedback` now gate on the sealed-registry owner, and cross-project (issue #681).** Closes the owner-capability model (#678): the last two owner-only tools were still gated by a hardcoded `cwd.name == "devpulse"` check — which, it turns out, was a **no-op through drone**: drone runs a routed module with `cwd=`, so the module's own `Path.cwd()` is *always* the devpulse tree and can't identify the caller (a `@flow` caller sailed straight through). A new shared `handlers/owner/guard.py` resolves the *real* caller from the env drone sets (`AIPASS_CALLER_BRANCH` / `AIPASS_CALLER_CWD`) and checks it against the sealed owner via the frozen `is_owner(email, start_path)` contract — so it works in any project (devpulse in AIPass, whoever owns elsewhere), not a hardcoded name. `feedback send` stays open (it's the inbound channel any agent uses to drop feedback to the owner); every mailbox read/manage verb is owner-only. Fail-safe: if no owner is sealed yet (old/partial install) or the resolver can't import, it falls back to the legacy devpulse-path heuristic so existing installs never hard-break. Live-verified end-to-end: owner allowed, `@flow` denied on both tools, `send` open. 18 new tests (15 guard + 3 gate), branch audit 100%. (built + verified by devpulse) - **seedgo `json_structure` now sanctions `custom_config/` for operator-editable config (issue #643).** The standard said "`{branch}_json/` root, one directory, no splits" and the checker ignored subdirs, so `custom_config/` (home of operator-tunable runtime config like `cadence_config.json`, `memory.config.json`) was an undocumented convention. `json_structure_check.py` gained an `ALLOWED_JSON_SUBDIRS` allowlist and a `check_branch_post()` that validates `{branch}_json/` subdirs — `custom_config/` and hidden dirs (`.archive`) pass, any other split is flagged. `json_structure_content.py` documents the directory structure and operator-config location. The subdir check honors `.seedgo/bypass.json` (bypass rules are threaded through `check_branch_post` → `_check_json_dir_structure`), so a branch can sanction a legitimate data subdir while unsanctioned + unbypassed splits still fail. 7 new tests. (The new check surfaced `devpulse_json/compass/` — the devpulse Compass SQLite/FTS5 decision store, which needs its own directory — now sanctioned via a documented devpulse bypass; audit confirms Json_Structure back to 100%.) - **`git_gate` block messages now guide external users instead of dead-ending (issue #620).** A blocked raw `git`/`gh` command previously just errored. The block message now explains *why* git is enforced (prevents cross-agent state conflicts), lists the key `drone @git` commands (commit, smart-sync, sync, pr, checkout), points to `drone @git --help`, and shows how to disable the gate in isolation (`git_gate.enabled = false` in `.aipass/hooks.json`) — verified against the engine, which skips a disabled hook per-hook without affecting other hooks or `drone @git`. The combined `GIT_GH_REDIRECT` was split into distinct `GIT_REDIRECT` + `GH_REDIRECT`; `EDIT_REDIRECT` also shows the disable path. An init notice was added to the `project_hooks.json` template and the on/off story documented in the hooks README. 6 new tests (86 in `test_git_gate`). - **Telegram `/create` + `/cancel` are now gated to the base @aipass bot (issue #644).** Every per-branch bot inherited `BaseBot`'s `/create` + `/cancel` and could mint new bots — but Patrick designated the base @aipass bot as the *sole* spawner. `base_bot.py` now guards on bot identity (branch bots carry a `branch_name`; the base bot's is `None`): `_dispatch_command` returns `False` for `create`/`cancel` on a branch bot (falls through to normal handling), and `get_custom_commands` advertises them only for the base bot. Rode along in the same @skills pass: fail-loud fixes to `botfather_client.py` (issues #669.2/#669.3, already closed) — `_load_telethon_config` now raises `RuntimeError` naming the config path and the `drone @api set-secret telegram telethon_config` command instead of silently returning `None` — plus poll-offset test coverage (#668). 133 telegram tests pass, seedgo 31/31 on both source files. - **seedgo no longer lints throwaway code (issue #675).** A single disposable POC used to fire 8 standard violations (architecture, meta, shebang…). The audit and checklist now skip any file resolved under a system temp dir (`tempfile.gettempdir()` / `/tmp`, cross-platform) or a `scratchpad` path, and a new `--prototype` flag (plus an in-file `# seedgo: prototype` marker in the first 5 lines) exempts disposable code explicitly. Wired into `branch_audit._collect_py_files` (throwaway filter) and `checklist.run_checklist` (early-return skip). 6 new tests; live-verified that a `/tmp` file and a marker-tagged file both report "✓ (skip)". - **The `claude()` boot shim now ships and installs on onboarding (issue #666).** Its installer (`hooks/tools/install_boot_shim.sh`) lived under a gitignored `tools/` dir — never version-controlled, never shipped — so the attach-if-live / start-in-tmux boot feature (and presence-gate-via-boot) was dev-local only; a macOS user could not attach/resume their session. A root `.gitignore` negation now tracks exactly that one file (`tools/` re-ignored, only the installer whitelisted — README stays out), and `setup.sh` runs it right after hook installation (idempotent via a marker check, non-fatal on error, venv Python resolved from the script's own location for POSIX/Windows). Fresh clones and `aipass install` now get the shim. - **Interactive-occupancy detection is now cross-platform — the wake-back guard no longer goes blind on macOS (issue #680).** `_is_branch_occupied()` and `_read_session_type()` (duplicated in `dispatch/wake.py` and `dispatch/daemon.py`) read `/proc/{pid}/cwd` + `/proc/{pid}/environ`, which do not exist on macOS — so occupancy always resolved `False` there and an external wake-back could spawn a *second* Claude session on an already-interactive branch (double-session, weakening the TDPLAN-0012/#678 interactive-dispatcher guard). The per-PID cwd and session-type probes are now extracted into platform helpers: `_get_pid_cwd` (Linux `/proc` readlink, macOS `lsof -a -p PID -d cwd -Fn`) and `_read_session_type_darwin` (`ps -p PID -wwE`), applied identically in both files. Fail-safe: an unreadable cwd/env logs at info and continues — never crashes the wake path. +11 tests (macOS cwd, macOS session type, zombie, unsupported platform), seedgo 31/31 on both files. - **SubagentStop gate no longer runs its ~600ms seedgo check on every internal turn (issue #606).** Claude Code creates an internal agent per response turn with an empty `agent_type`, so the `subagent_gate` handler was firing its full `drone @git status` + seedgo modified-files check on every turn, not just when a real Agent-tool sub-agent completed. `handle()` now early-returns `_ALLOW` when `agent_type` is empty; the full check runs only for a real sub-agent (non-empty `agent_type`). Piper speech is a separate notification hook and is unaffected — the trust layer stays visible. 3 new tests (empty skip, missing-key skip, real-agent full check), 17/17 green. - **Watchdog stall detector no longer false-fires on a long single tool call, and a real stall now reaches devpulse live (issue #634).** Liveness was inferred purely from JSONL file-size growth, so an agent doing one genuinely long operation (big read, long-running Bash, heavy compute) wrote no new lines for the span and was misread as `STALLED` while actively working. `watch_agent` now also treats an in-flight `tool_use` (the assistant's last transcript entry while a tool runs) as activity — verified live against real Claude Code transcripts: the `tool_use` line is written at tool *start* and persists for the whole call. Part 2: the stall (and a new long-running-tool advisory, plus a resumed signal) is emitted to **stdout** — which the Monitor-tool wrapper surfaces as a live event — instead of only `stderr`+logger, which Monitor captures but never relays. Stall logic extracted into a `StallTracker` for clarity; +9 tests (142 green), devpulse audit 100%. (devpulse) - **`aipass install` shows progress during the slow dependency build, and a README quick-start command is corrected (issue #665, items 6–7).** The editable install of the `[memory]` extras ran with `pip --quiet`, going silent for minutes during wheel builds — it looked hung; dropped `--quiet` on that step and set expectation in the echo. And `README.md` showed `drone @seedgo audit my_project`, which fails (`audit` takes a registered pack name) — corrected to `audit aipass`. Remaining #665 items (version, --help names, subcommand --help, placeholders, hints, crash-vs- unknown) span multiple owners and stay open. (devpulse) - **`aipass`/`drone` first-contact papercuts resolved — issue #665 fully closed (items 1, 2, 4, 5, 8).** `aipass --version` now reads package metadata (was hardcoded `0.1.0`); `aipass --help` lists real `COMMAND` names, not file stems (`help` not `help_chat`, `init` not `init_flow`); a crashing or unimportable handler surfaces its real cause instead of `Unknown command` (@aipass). `drone systems` placeholder descriptions now derive from each branch's passport/README, fixed in code so they survive registry regen — the earlier gitignored data edit didn't (@spawn). Bare-mode hints point to working commands — `drone @daemon --help` (there is no `daemon` binary) and the standard `drone @memory --help` (@daemon, @memory). Item 3 became the #685 standard. (multi-branch, verified devpulse) - **`os.kill(pid, 0)` liveness probes across the fleet are now Windows-safe (issue #684).** On Windows `os.kill(pid, 0)` maps to `TerminateProcess` — the "probe" kills the target. Nine sites across @ai_mail (dispatch daemon/wake), @drone (git lock handler), @flow (runner lock), @hooks (cc_sessions/presence) and @devpulse (watchdog registry) now early-return to an `OpenProcess` + `GetExitCodeProcess` check on win32, mirroring the `watchdog/agent.py` reference. The #682 checker confirms 0 unguarded sites remain (down from 10); the last one, `tools/git_lock_tool.py`, is split to #687 (blocked by the tool's pre-existing gate debt). (fleet migration, verified devpulse) - **Telegram poll loop no longer re-drains a rate-limited backlog; systemd suicide-loop + silent config fallback fixed (issues #668, #669).** #668: the poll loop advanced the update offset *after* processing, so a rate-limited/erroring update never advanced it — the same backlog re-fetched in a flood loop. The offset now advances *before* `process_update`, so a consumed update never pins it. #669: (1) systemd unit gets `KillMode=process` so a restart isn't killed by the old instance's cgroup teardown (suicide-loop); (2) `create_bot_via_botfather` now **raises** with an actionable message (naming the `set-secret` fix) instead of silently returning `None` when telethon config is missing (fail-honestly); (3) stale config-mechanism docstrings corrected. Also Windows-hardened `_is_pid_alive`/`_check_lock` and switched `TEMP_DIR` to `tempfile.gettempdir()`. 653 telegram tests green. (@skills, verified devpulse) - **Rollover `_find_repo_root` now fails loud, and `edit_gate` warns on over-count memory sections (issue #683, #664 follow-up).** The PreCompact rollover hook's `_find_repo_root` returned `None` silently when `AIPASS_HOME`/cwd was wrong — the exact silent-skip that hid #664 for months; it now logs a `logger.error` with the `AIPASS_HOME` value and cwd before returning. And `edit_gate` enforced per-entry *character* caps but not entry *counts*, so a branch could drift past its count cap between rollovers; a soft `_check_section_counts` now warns (never blocks), reading the same `memory.config.json` rollover caps @memory uses. +14 tests (70 green); both live-proven (bad root → error logged; over-cap → warn, no block). (@hooks, verified devpulse) - **`is_owner()` now case-folds — `is_owner('DEVPULSE')` matches `is_owner('devpulse')` (issue #679).** The spawn-registry resolver (`registry.py:382`) `@`-normalized the email but never lowercased, so a mixed-case branch name (registry names are mixed-case: `DEVPULSE` vs `devpulse`) returned `False` against the seated owner. Harmless today (the only caller lowercases first) but the frozen TDPLAN-0012 contract promises normalization, and PART-4 owner-gating may pass a raw name. Now lowercases both sides; verified live (every case variant of the owner → True, non-owners → False) + a case-insensitivity test (316 green). (@spawn, verified devpulse) - **`aipass install` no longer hard-fails (exit 2, silently) when it can't create global symlinks (issue #660 follow-up).** `setup.sh` runs under `set -euo pipefail`; the #660 `safe_symlink` refactor returns `2` on `ln` failure, but the call sites captured that code on the *next* line (`rc=$?`), so `set -e` killed the installer at the symlink step — before the `~/.local/bin` fallback (built for exactly the no-sudo case) could run. Any sudo-less environment (containers, CI, locked-down machines) got a silent exit 2 with no symlinks, despite an otherwise-complete install. Fixed all three call sites to `rc=0; safe_symlink … || rc=$?` (set-e-safe). Proven in docker: a sudo-less install now falls back to `~/.local/bin` and exits 0. (devpulse) - **`drone @devpulse watchdog agent` no longer reports failure on a successful watch (issue #661).** Its "invoke via Monitor tool" reminder was printed through `cli.error()`, which — after the #661 exit-code work — trips a process failure flag, so every successful watch exited non-zero with a red X. Rerouted to a dim console note; genuine argument errors still `error()` → exit 2. (devpulse) - **The prax monitor now holds a single-instance lock, so a duplicate/orphan monitor can't double-send Telegram relay messages (issue #671).** A new `instance_lock` handler writes a pidfile (`prax_json/monitor.pid`, outside the tailed `system_logs/`) with a liveness check: `acquire()` runs before relay init and refuses to start (fail-loud, naming the holding PID) if a live monitor already holds the lock, reclaims a stale pidfile when the recorded PID is dead, and `release()` clears it on shutdown. The liveness probe is platform-branched — POSIX `os.kill(pid, 0)`, Windows `OpenProcess`/`GetExitCodeProcess` (a raw `os.kill(pid, 0)` *terminates* the target on Windows). `monitor.py` was also split under the 600-line limit (`pid_cache` extracted). +25 tests. (built by @prax, verified by devpulse) - **`aipass init update` now refreshes `AGENTS.md` and prunes stale managed cruft (issue #676).** Two gaps: (1) `update_project` synced `AGENTS.md` from a `.aipass/project_AGENTS.md` template that never existed, so the branch silently no-op'd and `AGENTS.md` was never refreshed on update (only `CLAUDE.md`, whose template exists, synced) — added the template and reconciled create/update to one source; (2) the update was additive-only — added a whitelist-scoped cleanup pass (`_STALE_MANAGED_FILES`, currently the retired `aipass_global_prompt.md`) that removes only positively-identified managed artifacts, logs every removal, and never touches user-owned files. The template also had to be un-ignored in `.aipass/.gitignore` (allowlist) or it would never have shipped — caught in verify. +7 tests; live repro confirms update emits `AGENTS.md` and clears a planted cruft file. (built by @aipass, verified by devpulse — incl. the gitignore ship-gap) - **External-project branches now auto-roll — rollover discovery is no longer cwd-scoped (issue #664).** Branch discovery only saw registries reachable by walking up from the caller's cwd, so branches living solely in an external project's `*_REGISTRY.json` were never reached by rollovers fired from the AIPass tree (the PreCompact hook runs with cwd = repo root) — their `.trinity` files grew unbounded (one hit 110 key_learnings against a 15 cap) and vector stores went stale. `@memory` added a persisted `known_registries.json` (gitignored per-install data) that records every external registry seen via the cwd walk, so discovery reaches them regardless of caller cwd; stale/deleted registry paths are filtered on load. Plus a soft entry-**count** guard at write time (warns, never blocks) since the write gates only enforced char caps. The remaining hooks-side harden (`_find_repo_root` fail-loud + the `edit_gate` count-guard) is filed for `@hooks`. +12 tests; live repro confirms a rollover fired from the AIPass root now reaches an external-registry branch. (built by @memory, verified by devpulse) --- ## [2026-07-09] ### Added - **Exit-code contract foundation — failing commands can now exit non-zero (issue #661, in progress).** CLI error paths printed an error but returned exit `0`, so `$?`-checking callers (core to running `drone` as a subprocess) were told success on failure. The dispatch contract was a 2-state bool (`handled` / `not-mine`) with no way to say "handled *and* failed". `@cli` now exposes a process-level failure flag + `resolve_exit(handled)` (→ `0`/`1`/`2`), and `error()` auto-trips the flag — so any failure routed through `error()` gets a correct non-zero exit with zero per-site edits, and it can't regress. Inert until a branch's `main()` adopts it. `@seedgo` added an `output_routing` standard (39th checker) flagging user-facing status output that bypasses the cli helpers — 254 sites across 14 branches, the migration checklist. `devpulse` is the first adopter (`main()`→`resolve_exit`, feedback migrated to `error()`, exit `2`/`0`/`1` verified, 100% seedgo). Fleet migration to follow. (built by @cli + @seedgo) ### Fixed - **`@trigger` no longer rewrites its 44KB `trigger_data.json` on every log event (issue #674).** The branch log watcher persisted dedup hashes and log positions with two separate full-file rewrites *per event*, so a log burst churned the file ~1-2×/sec (surfaced by prax monitoring). Replaced the per-event/counter writes with a debounced coalescing writer: events set a dirty flag and both keys are written in a single atomic write at most once per 5s, with a forced flush on watcher stop so nothing is lost on clean shutdown. Also confirmed the retired `bulletin_created` event handler no longer loads or warns (it lives in `.archive/` with no live references; scrubbed stale README/bypass mentions). 564 trigger tests green (+6 debounce tests). - **`aipass install` no longer silently repoints your global `drone`/`aipass` symlinks (issue #660).** `setup.sh` force-overwrote the global CLI symlinks with `ln -sf` on every run, no check and no opt-out — so `aipass install --path /tmp/scratch` "to try it" silently hijacked your real global commands to the scratch tree, which broke them once `/tmp` cleared, disconnected from the cause. A new `safe_symlink` guard refuses to repoint a symlink that points at a *different* install: it prints a loud from→to warning and leaves the existing link untouched unless you pass `--force-symlink`; `--no-symlink` opts out of symlinking entirely. Both flags thread through `aipass install`. Fresh installs and same-location reinstalls behave exactly as before. Adds a `safe_symlink` regression test (`tests/setup_symlink_guard_test.sh`) and 3 flag-forwarding tests; the touched install output was migrated to `@cli` helpers (#661). - **`drone @flow close` no longer reports a false "timed out after 30s" on a successful close (issue #662).** A single-plan close committed early (plan marked closed, file archived) and then ran memory vectorization *synchronously* — `drone @memory process-plans` — inline. On the cold first close of a session that crossed drone's 30s executor timeout, so drone killed the flow subprocess and returned exit `1` **after** the close had fully committed. An autonomous agent reading that exit code would retry or abandon an already-closed plan. `close_plan_impl` now honors its long-existing `spawn_background` flag: single close fires the already-detached `_spawn_background_runner` (the same path `close_all` uses) and returns immediately after archive; vectorization runs in the background. Also removed the handler's cross-handler imports (archive/trigger now injected). Verified live: a real close returns in ~5s at exit 0 ("Vectorizing in background") vs the prior 30s-timeout risk. 730 flow tests green (+2 new). - **`aipass doctor` no longer hangs on non-interactive stdin (issue #663).** The auto-wire `[y/N]` prompt called `input()` with no tty guard, so a caller with a blocking-but-idle stdin (a script, CI job, or subprocess whose stdin never sends EOF) hung `doctor` indefinitely — reading as a crash from the flagship "check my system" command a new user runs first. `prompt_auto_wire` now guards the prompt with `sys.stdin.isatty()`: a non-tty stdin declines the auto-wire (prints the manual-wire warning) instead of blocking. Verified against the exact repro — a blocking non-tty stdin that never EOFs now completes instead of hanging until killed. Adds 3 regression tests. - **macOS session lock-out: the boot wrapper can now see tmux sessions on macOS.** `session_boot` decided whether a live Claude session lived inside tmux by walking the process tree through `/proc//status` — Linux-only. On macOS (no `/proc`) that walk always failed, so the wrapper concluded every live session was "outside tmux" and refused to attach, locking the user out of their own session in an unbreakable loop. Replaced the `/proc` read with a portable `ps -o ppid=` ancestry walk (Linux + macOS). Also: both the boot warning and the presence-gate block now spell out the exact recovery command (`kill && claude`, `command claude --resume`) instead of a vague "kill it first", and the wrapper no longer doubles `--permission-mode` when the user passes it explicitly. New/updated tests, hooks suite 791 green. (built by @hooks) - **Boot-shim installer no longer bakes a hardcoded user path.** `install_boot_shim.sh` hardcoded `/home/patrick/Projects/AIPass/.venv/bin/python` into the `claude()` shell function — wrong on any other machine or user. It now resolves the venv interpreter from the script's own location (POSIX `.venv/bin/python`, Windows/git-bash `.venv/Scripts/python.exe`, else PATH `python3`) and bakes the correct one at install time. - **Silent hook-wiring break: provider settings could be left half-wired with no warning.** A stale `setup.sh` merge orphaned the `SessionStart` hook event to an empty `[]` — the key existed but nothing fired — written silently, and it went unnoticed for weeks because CI skips the provider-settings snapshot test (it needs `~/.claude/settings.json`, absent in CI). Root cause: the merge stripped every AIPass bridge entry per event, then re-added only events still present in its own hook list, orphaning any event it no longer defined. The merge now drops such an event entirely (and says so) instead of emitting an empty array. Also corrected the stale snapshot fixture (dropped the dormant `presence_gate`, which by design ships wired only in project config, and added `SessionStart:cadence_reset`) and marked `presence_gate` `provider_wired: false` so the wiring checker knows it is intentionally not provider-wired. - **`json_handler.load_json` crashed on an empty/whitespace file (#667).** Under concurrent audit + tests a writer could truncate a JSON file in the window between `ensure_json_exists` and `load_json`'s own read, raising `JSONDecodeError`. `load_json` now guards an empty/whitespace read and falls back to the type's default template; a non-empty but malformed file still raises (fail honestly). 3 new tests, red-green proven. ### Added - **`drone @hooks verify` — hook-wiring integrity checker.** Cross-checks `~/.claude/settings.json` against `.aipass/hooks.json` and fails loud on empty provider hook arrays, orphaned entries, enabled handlers with no provider bridge, and duplicate (matcher-aware) entries — so a half-wired hook can never rot silently again. `aipass doctor` now runs this check under Services and re-verifies after `--fix`. 40+ new tests. (built by @hooks + @aipass) ## [2026-07-07] ### Fixed - **Drive sync now respects `.backupignore` on the sync path.** The ignore spec was applied at backup time only — anything already inside `.backup/versioned/` got uploaded regardless. Real case: Vera-Studio's store carried 37K legacy `node_modules` files (92% of the store), turning a KB-sized sync into a 7-8 hour crawl (Drive uploads are per-file API round-trips — latency-bound, not bandwidth-bound; the clean store syncs in ~13 min). `drive_sync` now re-filters store files through the project's `.backupignore` before upload and logs the ignored count. Also fixed: `json_handler.log_operation` crashed on `Path` objects (`PosixPath is not JSON serializable`) — now serializes with `default=str`. 2 new tests, backup suite 247 green. (built by @backup) ### Added - **Fresh-context grounding: cadence reset on new chat / clear / compact.** Both prompt loaders (tier0 kernel + navmap) now run at period 5, and a new `SessionStart` hook resets the cadence counter on `startup`/`clear` (skips `resume` — restored context already carries grounding; `compact` was already reset via PreCompact). Net effect: the first message of every fresh context gets full grounding, then every 5th turn after. Wired end-to-end: handler (`session_start.py`), project config (`.aipass/hooks.json` + the `project_hooks.json` template for external projects), and `setup.sh` seeds the provider `SessionStart` entry for new installs. Proven end-to-end from a real fresh-user clone of dev in Docker — 19/19 assertions via the new `tests/docker_dev_verify.sh` (bridge-era; supersedes the stale `docker_clone_test.sh`). (built by @hooks + @devpulse) ### Fixed - **`aipass` ≠ drone-routed — misroutes now guide instead of crash.** `aipass` is the user's front-door CLI, deliberately not resolvable by drone. But `drone aipass` misdirected, `drone @aipass` crashed with a traceback, and `aipass @drone` dead-ended. All three now print clear guidance (what aipass is, what drone is, how to reach each). Kernel + navmap prompts updated so agents know the exception. (built by @drone + @aipass) --- ## [2026-07-06] ### Fixed - **prax log watchdog now covers branch `logs/` dirs — `.jsonl` runaway growth caught.** Rotation was hardcoded to `.log` files, and several branches write `.jsonl` logs via raw `open(path, "a")` appenders that bypass prax entirely — `hooks/logs/engine.jsonl` had grown to 63 MB, `backup/logs/operations.jsonl` to 31 MB, `trigger/logs/medic_suppressed.log` to 7 MB, all unrotated. The log-watchdog safety net also only scanned `system_logs/*.log`. @prax extended it: `scan_branch_log_files()` sweeps every `src/aipass/*/logs/` for `.log` + `.jsonl` (WARN at 1 MB unrotated, CRITICAL at 10 MB), `enforce_branch_log_limits()` truncates flagged files to the last 5000 lines, and `drone @prax log-audit` now reports both system and branch scopes. 11 new tests, full prax suite 947 green. The raw-appender writers themselves still need per-owner caps — routed to @hooks, @backup, @trigger. (built by @prax) --- ## [2026-07-05] ### Fixed - **HVTrust badge restored in the root README.** hvtracker corrected the methodology v4.1 grade-computation bug (issue #109); the badge shows the right grade again, so the temporary comment-out from earlier today is reverted. - **Installer no longer destroys a user's custom Claude Code hooks (DPLAN-0234 Strand C).** setup.sh used to write `settings["hooks"]` wholesale — anyone with their own hooks in `~/.claude/settings.json` lost them on install or re-run. Now it merges: every AIPass bridge entry (identified by the `bridges/claude.py` marker) is refreshed, while user-wired hooks and custom events are preserved. Verified against fixtures: custom hooks survive, stale AIPass entries are replaced without duplicates, and the fresh-install output is shape-identical to before (7 events, 6 UserPromptSubmit + 6 PreCompact entries). Found while fire-testing a fresh install's hooks in Docker — all 17 wired hook entries pass on a cold Linux clone (real kernel/navmap/branch prompt bytes, git gate blocks, clean no-ops on empty state). - **Windows fresh installs get a working hook bridge.** setup.sh wrote the Claude bridge command with `.venv/bin/python3` on every OS — but Windows venvs put the interpreter at `.venv/Scripts/python.exe` and have no `bin/`, so hooks on a fresh Windows install pointed at a nonexistent python and would never fire. The bridge string is now OS-aware (bash passes `IS_WINDOWS` into the hook-install step). @hooks assessed the rest of the chain: `$AIPASS_HOME` expansion works on Windows because Claude Code runs hooks via Git Bash (which must exist for setup.sh to have run), and the bridge itself has zero POSIX assumptions — the interpreter path was the only gap. Verified: both OS modes produce the right bridge string, merge marker unchanged, custom-hook preservation intact. (assessed by @hooks) ### Added - **`./aipass` — repo-root cold-clone launcher (DPLAN-0234 Strand B).** The branded entry point for the clone-first flow: `git clone`, `cd AIPass`, `./aipass install` — three commands to a working AIPass. Stdlib-only bash (zero deps, runs before anything is installed): pre-setup, only the `install` verb exists and delegates to `setup.sh` with full flag pass-through (`--no-init` / `--with-init` / `--project`); any other verb prints help pointing at `./aipass install`. Post-setup the launcher turns transparent — it execs the venv `aipass` binary for everything, so `./aipass doctor` just works. Bare `aipass` always resolves to the PATH binary; the launcher only ever runs as an explicit `./aipass`. 13 launcher tests (file properties, pre-setup help, install delegation, post-setup forwarding), @aipass suite 622 green, seedgo 100%. README Quick Start now leads with `./aipass install`. (built by @aipass) - **`./setup.sh` chains into `aipass init run` — clone-first one-command install (DPLAN-0234 Strand A).** Distribution is git-clone, not pip: the framework changes constantly, so a PyPI snapshot goes stale while a clone is always current HEAD. Now `git clone && cd AIPass && ./setup.sh` takes you from cold clone to a working first project in one command: on interactive terminals, setup ends by launching the guided `aipass init run` in a sibling directory (default `~/aipass-project`, prompt to choose — init refuses to run inside the engine tree). New flags mirror `aipass install`'s handoff rules: `--no-init` skips, `--with-init` forces even headless (init chains `--non-interactive`), `--project ` picks the target. CI and piped shells skip automatically (`CI` env or no tty), so the windows/macos-test workflows that run bare `bash setup.sh` are untouched. `install.py` now calls `setup.sh --no-init` since install owns its own init handoff — no double-scaffold. Proven in clean-room Docker, both runs exit 0: bare headless run skips init with a hint; `--with-init` run chains init to `✓ Project initialized.` with the project dir scaffolded. 39/39 install tests, seedgo 30/30. README Quick Start updated to the one-command flow. - **`aipass install` — one-command framework bootstrap.** The missing half of `pip install aipass`: a single command resolves the install home (default `~/AIPass`), git-clones the public repo, runs `setup.sh` (venv, editable install, hook wiring), verifies the toolchain, and auto-launches `aipass init` in the same terminal — so `pip install aipass && aipass install` bootstraps the whole system with nobody the wiser. New auto-discovered `install.py` module (zero shared-code edits) with flags `--non-interactive / --path / --here / --no-init / --with-init / --project / --dry-run`; 39 unit tests, seedgo 30/30, @aipass suite green. Proven in a clean-room Docker image (nothing pre-baked) across two runs, both exit 0: `pip install` (local wheel) → clone → `setup.sh` (17 branches registered, 13 bootstrapped, hooks wired into `~/.claude/settings.json`, `AIPASS_HOME` set, `drone`/`aipass` on PATH) → live `drone systems`, and `--with-init` chaining straight into `aipass init` to completion. Ships install progress bars, an install→init handoff, and doctor coverage. (built by @aipass, DPLAN-0233 — PyPI release bump pending) ### Changed - **HVTrust badge temporarily hidden in the root README.** hvtracker's methodology v4.1 recalibration is miscomputing the grade (showing D/~10 while the detail-page dimensions sum to ~78); the badge is commented out until it's corrected. Filed upstream as hvtracker issue #109 — restore when resolved. - **devpulse branch prompt — sole-git-writer clarity.** Added a note to the git section: because no other agent can commit, merge, or push anywhere, dirty cross-branch files are always someone's live WIP, safe to leave and pick up later — never a loose end needing handoff. ## [2026-07-03] Post-2.6.1 cycle — **unreleased** (held for a later merge). ### Changed - **All 17 branches now pass the standards audit at 100% — Windows-compat hardening across the board.** Added `sys.stdout/stderr.reconfigure()` UTF-8 guards (getattr form) to every Rich/CLI entry point, and platform-branched POSIX-only subprocess kwargs (`start_new_session` → `CREATE_NEW_PROCESS_GROUP` on win32). The seedgo `windows_compat` checker now credits the getattr guard form (not just direct `.reconfigure()` calls), with a locking regression test. Swept per-branch via dispatch; checker fix by @seedgo. Verified by a full 17/17 audit (pyright clean). ### Fixed - **Telegram replies no longer overwrite the previous message.** The Stop-hook out-path (`hooks/.../notification/telegram_response.py`) reused a stale `processing_message_id`: after a successful delivery, `_advance_pending` kept the pending file but never cleared the placeholder id, so any reply that fired without a fresh "Processing…" bubble (remote/mirror input, multi-Stop turns) re-*edited* the same Telegram message instead of posting a new one — every response clobbered the last. Now clears `processing_message_id` after the first delivery, so subsequent Stops fall through to `_send_with_retry` (a new message). Root-caused live on the devpulse bot and proven by the delivery log flipping `edit`→`send`; +2 regression tests in `TestAdvancePending` (114/114). (fixed by @hooks, `f42a98b`, PR #651 — not yet merged) - **`template` audit checker no longer false-flags documentation *about* templates.** The advisory stale-template checker (`seedgo/.../template_check.py`) matched its marker strings anywhere in a file, so it fired on prose and code that merely *mention* the markers rather than on un-rendered stubs — flagging 5 branches, only 3 of them real. Three root causes, all fixed: (1) it globbed **`.trinity/*.json`**, scanning live memory (`local.json`, `observations.json`) that naturally accumulates marker mentions (seedgo's own note "Detects NEEDS CONFIGURATION", prax's note about `template_pusher` restoring `{{BRANCHNAME}}`); now scans **`passport.json` only**, the sole spawn-templated trinity file. (2) the single-curly `{…}` regex ran on every `.md` and matched inline JSON / f-strings / code paths in READMEs (`{"new": 3}`, `{e}`, `apps/plugins/{name}/`); now runs on the branch **prompt only** (the spawn README template has no single-curly placeholders). (3) the definitive-marker scan matched `{{BRANCH}}` inside markdown inline code — e.g. spawn's README documenting ``Replace `{{BRANCH}}`…``, which is scaffolding docs, not a stub. For `.md` files, fenced + inline code is now stripped once up front before **both** scans (`passport.json` still scans raw). Safe because real stubs carry markers in prose/headings (the `## Status: NEEDS CONFIGURATION` line), never exclusively in code. Verified system-wide: `Template` avg **80% → 94%**, the two pure false positives (seedgo memory, spawn README) cleared to `100%`, only the three genuine unconfigured prompt stubs (cli/drone/prax) still flag. +7 tests (24/24), full suite green. (fixed by @seedgo across 3 dispatched passes, verified by @devpulse) - **cli / drone / prax branch prompts configured** (were spawn stubs). The three branches the template checker correctly flagged had never had their `.aipass/aipass_local_prompt.md` filled in — they booted with a `NEEDS CONFIGURATION` placeholder and no branch-specific identity. Each branch wrote its own real prompt (identity, key commands, architecture, critical rules, integration points; ~63–67 lines, `PROMPT_STYLE.md` format); all three now score `Template 100%`. (written by @cli/@drone/@prax, dispatched + verified by @devpulse) ## [2026-07-02] Released as **2.6.1**. Rolls up the DPLAN-0226 / FPLAN-0289 / TDPLAN-0010 / FPLAN-0298 batch (unified Telegram↔Claude Code bridge, single-session presence gate, live Telegram streaming, `aipass init` template selector + portability, `@backup share`) — all documented under `[2026-07-01]` — plus the CI stabilization below. ### Added - **`drone @git tag ` — guarded release-tag automation (post-2.6.1).** Devpulse-tier verb that pushes a release tag with no manual step: fetches `origin`, refuses unless the tag's `X.Y.Z` matches **both** `pyproject.toml` and `src/aipass/__init__.py` on `origin/main` (version guard) and the tag doesn't already exist (exists guard), then tags `origin/main` and pushes — firing `publish.yml`. `drone @git tag --list` lists tags. Removes the merge playbook's last manual `git tag`/`push` step, so releases need zero user input. (built by @drone, S274) ### Fixed - **CI green — six regressions from the DPLAN-0226 / FPLAN-0289 / TDPLAN-0010 batch (PR #646).** The dev branch had gone red across `seedgo-audit`, the `test` matrix, and Windows; root-caused and fixed at source: - **seedgo** — the new `template_check` advisory checker was gating CI. `branch_audit.py` averaged *all* checker scores into the branch total, so `template_check`'s `ADVISORY=True` was never honored and it dragged 7 branches below the 100% floor on legitimate README brace-examples. Added a `gating_scores` filter that excludes `ADVISORY is True` checkers before computing the average (strict `is True` to avoid MagicMock false-positives) and exposed `advisory_standards` in the audit output. Also refreshed the provider hooks snapshot fixture to include the `presence_gate` `UserPromptSubmit` hook (FPLAN-0289), fixing 4 `test_hooks_snapshot` tests. - **hooks** — `cc_sessions.py` (added by the bridge, `f6cbe34`) was missing its README entry and a seedgo `modules` bypass (it reads external `~/.claude/sessions/*.json`, not branch data, so `json_handler` is the wrong tool — same precedent as `presence.py`). Added both. - **spawn** — retired the `passport(disabled).py` / `passport_ops(disabled).py` pair to `.archive/`; the `(disabled)` suffix kept them visible to the type checker, which flagged a broken cross-import between them. - **ai_mail** — `test_child_inherits_broker_fd` gave its throwaway test branch a real `.trinity/passport.json` so the broker's new `.trinity`-marker resolution (`f914ab6`) can resolve it and permit the delete. - **spawn** — the `builder→aipass_framework` template rename (`13463c0`) left `.gitignore` exceptions pointing at the old `templates/builder/` path, so `DASHBOARD.local.json` + ~10 other template files were silently untracked since the rename — present on disk (dirty tree passed) but absent from clean clones/CI, so `test_full_spawn` failed only in a clean checkout. Fixed all 23 `.gitignore` exception paths and committed the now-visible template scaffolding. - **skills** — `test_streaming` asserted a `+1` newline byte, but `write_text` text mode translates `\n`→`\r\n` on Windows (2 bytes), failing `windows-setup` only. Switched the test's transcript writes to `write_bytes()` for deterministic LF; production `_tail_transcript_bytes` was already CRLF-safe. ## [2026-07-01] ### Added - **`aipass init` is now a template selector (TDPLAN-0010)** — `init` presents a chooser with **`empty project`** at the top, pre-selected as the default (creates just the project folder, no scaffold), and **`aipass_framework`** below it (the full AIPass agent framework — the old always-on behavior, now opt-in). Flag and positional forms both work: `aipass init --list` (branches before the `--` catch-all) and `aipass init