{"slug":"terminal-status","total":86,"limit":100,"offset":0,"since":null,"commits":[{"hash":"f171fc9","date":"2026-09-26 09:15:57 -0700","author":"Steve","subject":"TK-11317: fix deferential-set bypass + ticket-rebind hole; add carry age ceiling + revision continuity","body":"Contrarian review of b594dbb found a reproducible CRITICAL hole: working-state.sh's\nper-prompt UserPromptSubmit hook fires `set green WORKING --deferential --boot` on\nevery prompt. In set(), the deferential-yield guard requires a truthy `previous`, but\nload(owner) returns None for reason owner_changed -- so the very first prompt after a\ntty reuse silently overwrote a carried purple/orange/yellow/lightblue with plain green,\nbypassing settle_owner_change entirely.\n\nFix: set() is now a thin lock-acquiring wrapper around a new _set_locked() write body.\nWhen _set_locked sees previous is None, reason is owner_changed, and the caller is\ndeferential, it settles the owner change FIRST -- via a new _settle_locked() lock-free\nwrite body (never re-enters self.lock(), which would deadlock; flock is not re-entrant\nwithin one process) -- and uses the settled record as `previous`. Its colour is always\n>= yellow priority, so the existing deferential-yield guard right after always fires and\nrepaints the settled record instead of falling through to plain green. A non-deferential\nexplicit set (e.g. /greendot on a genuinely new session) is unchanged -- the new branch\nonly engages for deferential=True.\n\nSame-class hole #2: the interactive `ticket` command and auto-bind's _default_ticket_set\nboth did `previous, _ = store.read(owner)` and silently rebuilt the record as color\n\"none\" whenever reason was actually owner_changed, dropping a carried colour the instant\na ticket got rebound before settle ever ran. Fixed via a new Store.effective_previous(),\nbacked by a shared Store._settlement_preview() (also now used by row()'s pre-backfill\npreview and settle_owner_change itself, so all three can never disagree).\n\nHIGH: prior_attention() now gates every candidate by CARRY_MAX_AGE_S (default 86400s /\n24h, env TERMINAL_STATUS_CARRY_MAX_AGE_S) -- a source older than that, or one whose age\ncan't be determined, is never carried, floors to the yellow \"New owner - status needed\"\nstate instead (never green, never none, no ticket). Age is measured from the ORIGINAL\nevent: a record that is itself already a carry reports its age from its own\ncarried_from[\"at\"], so a relay of owner_changed hops can't keep resetting the clock.\ncarried_from is now {\"pid\",\"started\",\"at\",\"hops\"} -- pid/started/at are the ORIGINAL\ncarrier's identity (preserved across every hop), hops counts the relay length.\n\nLOW: a carry write now continues the raw prior record's own revision (+1) instead of\nresetting to 1, via a new revision_override param threaded through set()/_set_locked.\n\n16 new tests (deferential-bypass repro for both the carried-attention and\nnothing-carried cases, explicit-set-still-wins, staleness ceiling for both JSON and\nlegacy .dot sources, a 3-generation hop chain preserving original provenance, revision\ncontinuity, effective_previous() passthrough, and the ticket-command/auto-bind repro) +\n1 new selftest case (19/19). Negative control: 15/45 new tests fail/error against\nb594dbb, including both deferential-bypass repros\n(verification/TK-11317-integration/negative-control-v2.txt). Full suite green after the\nfix (verification/TK-11317-integration/after-tests-v2.txt): test_terminal_status.py (75),\ntest_owner_change_carry.py (45), test_departed_asks.py (20), selftest (19/19),\ntest_stop_verdict.py (40), test_tk_required_fp_metric.py (13).\n\nCo-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01UqLpisVAyfYM4zoVP7fFya"},{"hash":"468627f","date":"2026-09-26 09:03:11 -0700","author":"Steve Abrams","subject":"TK-12326: durable departed-asks register + resolver so a reused tty's prior asks outlive the carry","body":"Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01VdkK32KZQYqwzJKEUv17jR"},{"hash":"76859a5","date":"2026-09-26 08:54:38 -0700","author":"Steve Abrams","subject":"TK-11317: live acceptance evidence (Claude+Codex readback, same-tty carry, canary PASS)","body":"Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01UqLpisVAyfYM4zoVP7fFya"},{"hash":"b594dbb","date":"2026-09-26 08:48:59 -0700","author":"Steve","subject":"TK-11317: never blank/green-float a prior needs-Steve colour across owner_changed","body":"Steve's TK-11620 guard ruling (2026-09-26): when a live tty is reused by a new\nclaude/codex pid (e.g. after /res), Store.load() reports reason owner_changed\nand the engine used to blindly floor the new record to green -- silently\ndropping whatever purple/orange/yellow/lightblue attention the prior owner\nleft pending.\n\nAdd Store.prior_attention()/prior_owner_info() (read-only: inspect the raw\nJSON record at this tty's path with the owner-identity check skipped, plus\nthe legacy .dot mirror at any mtime, pick the higher-PRIORITY ATTENTION\ncolour) and Store.settle_owner_change() (write: carry that colour forward\nwith carried_from provenance, or floor to yellow \"New owner - status needed\"\nwith owner_status_unverified only when nothing was pending). Both are new\noptional fields on set()'s opt-in-per-call contract, so an ordinary explicit\nset() still clears them.\n\nWire settle_owner_change into backfill()'s and start's owner_changed\nbranches (missing keeps its plain green floor -- there is no prior state to\npreserve there). row() renders the would-be-settled state for the <20s\nwindow before backfill runs, instead of a bare \"status not set\". The\n`ticket` command now explicitly forwards carried_from/owner_status_unverified\nso rebinding a ticket id never drops them.\n\n29 new tests (test_owner_change_carry.py) + 4 new selftest cases. Negative\ncontrol: 27/29 new tests fail/error when run against the tk11317-preimage\ntag (verification/TK-11317-integration/negative-control.txt). Full suite\ngreen after the fix (verification/TK-11317-integration/after-tests.txt):\ntest_terminal_status.py (75), test_owner_change_carry.py (29), selftest\n(18/18), test_stop_verdict.py (40), test_tk_required_fp_metric.py (13).\n\nCo-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01UqLpisVAyfYM4zoVP7fFya"},{"hash":"6d68776","date":"2026-09-24 14:44:43 -0700","author":"Steve Abrams","subject":"Auto-bind a ticket for unbound sessions from the UserPromptSubmit hook (TK-12168)","body":"ticket_binding.discover() is deliberately read-only, so a plain top-level\ninteractive session that never ran `tk` and carries no TK- in its argv sits\nunbound (\"TK REQUIRED\") forever. Adds a new `auto-bind` subcommand\n(cmd_auto_bind) that runs from working-state.sh on every prompt: if the\nsession is still unbound after a substantive prompt, bind the TK- it names,\nor mint one via `tk new` and bind that. Trivial prompts (ok/yes/bare slash\ncommands/<3 words) never mint a ticket; a per-tty flock lock + state file\nprevent duplicate creation; every `tk` failure fails open and rate-limits\nthe retry so a broken `tk` binary is never hammered.\n\nAdds explicit_ticket_in_prompt / is_trivial_prompt / short_topic as pure,\nunit-tested helpers in ticket_binding.py.\n\n14 new tests (TicketPromptHelperTests + AutoBindTests). Confirmed the\nnegative-test rule: on the pre-change tree these all ERROR\n(AttributeError: no cmd_auto_bind) -- exactly the \"TK REQUIRED forever\" gap\nthis closes. Full suite 128/128 after the change (was 114/114 baseline).\n\nCo-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01V29xaoBwcFwJyR4KC43ptn"},{"hash":"5933ea1","date":"2026-09-24 14:22:01 -0700","author":"Steve Abrams","subject":"Narrow the CHILD_SESSION paint guard so a bridge session that owns its own tty can paint (TK-12167)","body":"owner_for_paint() refused ANY caller carrying CLAUDE_CODE_CHILD_SESSION\nunconditionally -- correct for a true Agent-tool subagent (TK-11791's\nsame-PID mirror bug) but also caught a Remote Control (bridge) session\nthat genuinely owns its own pane, since that flag is carried in both\ncases. Live evidence: pid 33946 on ttys003 resolved owner ==\n{tty: ttys003, pid: 33946} (itself) via current_owner(), yet every\ndot-script and `ticket` bind was refused for an hour, leaving the tab\non a stale ticket label.\n\nFix: add _owns_tty_directly(), which reuses the existing\nancestors()/current_owner() machinery to verify the resolved owner is\nour own closest runtime ancestor with no second claude/codex process\nin between (always true today once current_owner() succeeds -- kept\nexplicit for defense-in-depth). Rescue a CHILD_SESSION-tagged paint\nonly when that check passes AND CLAUDE_CODE_BRIDGE_SESSION_ID is also\npresent -- a marker set only for a genuine bridge connection, never a\nplain internal subagent call, so TK-11791's existing \"subagent\nREFUSES\" fixture (byte-identical ancestry, no bridge id) is unchanged.\nUndeterminable ownership still fails closed regardless of the bridge\nmarker.\n\nTests: extended both `selftest` and test_terminal_status.py with the\nthree required cases -- (a) CHILD_SESSION + bridge id + direct\nownership -> paints (confirmed this fails against the pre-fix guard\nvia a scratch copy), (b) CHILD_SESSION + bridge id + a genuine\nintermediate claude/codex process -> still refused, (c) undeterminable\nownership -> still refused. Full selftest 14/14 and the unittest suite\n(test_terminal_status.py + test_stop_verdict.py +\ntest_tk_required_fp_metric.py, 114 tests) all green.\n\nLive-verified from the actual affected bridge session (pid 33946,\nttys003): `current --paintable` now exits 0, `greendot.sh` paints\nsuccessfully, and `terminal_status.py ticket 12167` binds directly\nwith no --tty/--force workaround.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01V29xaoBwcFwJyR4KC43ptn"},{"hash":"199e156","date":"2026-09-20 04:20:25 -0700","author":"Steve Abrams","subject":"Fix --all broadcast footgun when label precedes --all flag (TK-11936)","body":"Before a1139e9 forwarded $@ to the engine, mode=${1:-} was the only --all\ncheck. With $@ forwarding, a call like 'purpledot.sh \"label\" --all claude'\nput the label in $mode (no --all match), fell to *), and forwarded --all raw\nto the engine — which broadcast the caller label to every live session.\n\nFix: scan all remaining args for --all before the paint() dispatch so the\n--all branch always fires regardless of argument order."},{"hash":"c1b8135","date":"2026-09-18 14:57:42 -0700","author":"Steve Abrams","subject":"stop_verdict: read last_assistant_message + background_tasks from the Stop hook stdin (no transcript flush race); D1 done+time outranks the word parked, not W1 (TK-11921)","body":"Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE"},{"hash":"29097cd","date":"2026-09-18 14:54:25 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-18T14:54:24 (1 data files) — tests/stop-verdict/35-done-beats-stale-monitoring.jsonl","body":""},{"hash":"f3313d0","date":"2026-09-18 14:40:16 -0700","author":"Steve Abrams","subject":"stop_verdict: scan the WHOLE closing message for done/wait/park signals (Q1 stays last-line); hook retries while the final assistant record flushes (TK-11921 live miss: 'Done 14:33' → NO_TEXT then IDLE)","body":"Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE"},{"hash":"b5b2887","date":"2026-09-18 14:33:17 -0700","author":"Steve Abrams","subject":"test_stop_verdict: use the sv alias in the null-text regression test","body":"Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE"},{"hash":"24de799","date":"2026-09-18 14:33:04 -0700","author":"Steve Abrams","subject":"stop_verdict: skip null/non-string text blocks (uncaught AttributeError broke the exit-0 invariant); selftest reports a bad fixture header instead of aborting (TK-11921 review)","body":"Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE"},{"hash":"ec7cc19","date":"2026-09-18 14:32:18 -0700","author":"Steve Abrams","subject":"integrations: sync dot-floor.sh with the live TK-11921 Stop hook (install.py deploys this copy)","body":"Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE"},{"hash":"f5399f8","date":"2026-09-18 14:27:06 -0700","author":"Steve Abrams","subject":"stop_verdict: mechanical done/waiting dot verdict for the Stop hook (TK-11921)","body":"Pure decision module + CLI: reads the Stop-hook stdin JSON, tails the\ntranscript, prints ONE JSON verdict (never paints). 36 fixtures, 34 unit\ntests, --selftest, DOT_FLOOR_FAULT negative seam.\n\nCo-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE"},{"hash":"de433d8","date":"2026-09-18 14:19:47 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-18T14:19:46 (36 data files) — tests/stop-verdict/01-done-plain.jsonl tests/stop-verdict/02-done-time.jsonl tests/stop-verdict/03-check-done.jsonl tests/stop-verdict/04-finished.jsonl tests/stop-verdict/05-all-set.jsonl","body":""},{"hash":"9e5ef9e","date":"2026-09-17 10:34:46 -0700","author":"Steve Abrams","subject":"TK-11831: single-flight the iTerm enumeration so a cold herd collapses to ONE osascript","body":"The 30s cross-process cache (TK-11879) collapses REPEAT enumerations but a\nSIMULTANEOUS cold herd — ~59 sessions starting at once, empty cache — all miss\ntogether and each fires its own osascript into iTerm's ONE serial AppleScript\nqueue. That is the exact stampede this ticket is about (628 'osascript rc1'\nblinds in a 7d window, terminal_api unavailable x57 at the 91s peak); the TTL\ncache alone moves repeat-frequency, not the cold-herd tail (codex-check flag).\n\nFix: a NON-BLOCKING cross-process flock so exactly ONE caller enumerates. A peer\nthat fails LOCK_NB waits briefly (default 6s) for the winner to publish its fresh\nmap (the coalesced path) and, only if the winner is still enumerating past the\nwait, falls through to the SAME stale-cache/blind path an osascript timeout\nalready takes — so a peer NEVER fires a second osascript and NEVER manufactures a\nfresh answer it did not measure. Fail-OPEN: any lock error degrades to the prior\ndirect-enumerate behaviour. Kill-switch TERMINAL_STATUS_NO_SINGLEFLIGHT=1.\n\nVerified: 8 concurrent cold scans -> exactly 1 osascript, 7 coalesced (8->1; at\n59 sessions, 59->1). Tests 56->58: coalesce fires zero osascript and returns the\nwinner's map; peer-holds-lock-with-no-map blinds without a second osascript.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TmvM611X4duoLdp1cFmpy2"},{"hash":"2a0da77","date":"2026-09-17 07:30:06 -0700","author":"Steve Abrams","subject":"iterm_sessions: cross-process cache collapses the enum herd + blind falls back to cache, not green (TK-11879)","body":"~49 sessions each shelled an iTerm2 window/tab/session osascript at start;\niTerm's single AppleEvent handler serialized them under load and later ones\nreturned rc 1 (628 'osascript returncode 1' blinds/7d). A blind returned {}\nwhich made the start path paint a confidently-wrong GREEN instead of restoring\na semantic dot.\n\nMirror the proven ps disk-cache (TK-11398/11831): a short-TTL (30s) cross-process\nJSON cache of the {tty:title} map so only the first sibling enumerates and the\nrest read the file. On a blind, fall back to a recently-cached map (<=300s) as\n'stale' instead of {}; only when NO cache exists record enum_blind + return\nunavailable (honest NOT-MEASURED). start restore gate accepts a fresh 'cached'\nmap. Never cache an empty rc-0 enumeration (would poison siblings silently).\n\n+7 tests incl. the negative fault-injection (blind with no cache still records\nenum_blind). All 56 pass. Live-verified: 1 enumerate then cache HIT.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01Jwp6QVPb7cuafHPP7uLyi8"},{"hash":"56c764a","date":"2026-09-17 07:19:05 -0700","author":"Steve Abrams","subject":"dot.sh: auto-place window into its colour column on any successful colour paint (Steve: 'all color continue') — all colours, background+best-effort, TS_NO_AUTOPLACE kill-switch","body":""},{"hash":"a1d75ad","date":"2026-09-17 07:16:31 -0700","author":"Steve Abrams","subject":"backfill: honest heartbeat verdict (kill the hardcoded-PASS false green)","body":"TK-11870. The backfill supervisor's heartbeat wrote verdict:PASS unconditionally.\nTwo false-green shapes it masked (CLAUDE.md TK-11431): (1) a ps-timeout -- the exact\nfailure this ticket is about -- raised inside processes(fresh=True) and wrote NO\nheartbeat at all, a silence byte-identical to an idle box; (2) a partial/degraded\nscan wrote PASS with a shrunken live_owners and no cross-check.\n\nFix: extract a pure, testable backfill_heartbeat() whose verdict is DERIVED from what\nthe cycle measured -- WARN + NOT-MEASURED (live_owners:null) when the scan failed,\nWARN when live tabs exist but none could be asserted this pass, PASS only when it ran\nand could act; live_owners always carried so a measured 0-of-0 stays distinguishable\nfrom 0-of-N. The handler now catches the scan StatusError, writes the honest heartbeat\n(fresh mtime so who-watches-the-watcher liveness still passes, failure in the verdict),\nand stays fail-loud (stderr + exit 1). Ships 4 negative selftests proving it reddens on\nthe injected faults and greens only when it acted (selftest 11/11, unittest 49/49).\n\nAlso: gitignore + untrack data/backfill-latest.json (it rewrote every 20s, thrashing).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01RaaqshnC3QW5DfzotaMZfM"},{"hash":"8b9212d","date":"2026-09-17 07:12:37 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-17T07:12:37 (1 data files) — data/backfill-latest.json","body":""},{"hash":"7f98a1f","date":"2026-09-17 06:40:36 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-17T06:40:35 (1 data files) — data/backfill-latest.json","body":""},{"hash":"a69ef30","date":"2026-09-17 06:08:14 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-17T06:08:13 (1 data files) — data/backfill-latest.json","body":""},{"hash":"751a730","date":"2026-09-17 05:36:33 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-17T05:36:32 (1 data files) — data/backfill-latest.json","body":""},{"hash":"0f3363e","date":"2026-09-17 05:03:30 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-17T05:03:29 (1 data files) — data/backfill-latest.json","body":""},{"hash":"6e61949","date":"2026-09-17 04:30:36 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-17T04:30:36 (1 data files) — data/backfill-latest.json","body":""},{"hash":"21f3aa6","date":"2026-09-17 03:58:39 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-17T03:58:38 (1 data files) — data/backfill-latest.json","body":""},{"hash":"12f7ca5","date":"2026-09-17 03:26:50 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-17T03:26:49 (1 data files) — data/backfill-latest.json","body":""},{"hash":"b6bfc6e","date":"2026-09-17 02:55:09 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-17T02:55:09 (1 data files) — data/backfill-latest.json","body":""},{"hash":"3b0dab7","date":"2026-09-17 02:23:20 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-17T02:23:20 (1 data files) — data/backfill-latest.json","body":""},{"hash":"578331c","date":"2026-09-17 01:51:36 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-17T01:51:35 (1 data files) — data/backfill-latest.json","body":""},{"hash":"0b8f056","date":"2026-09-17 01:19:54 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-17T01:19:54 (1 data files) — data/backfill-latest.json","body":""},{"hash":"2e39534","date":"2026-09-17 00:48:10 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-17T00:48:10 (1 data files) — data/backfill-latest.json","body":""},{"hash":"b6d3125","date":"2026-09-17 00:15:59 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-17T00:15:59 (1 data files) — data/backfill-latest.json","body":""},{"hash":"fb63a6c","date":"2026-09-16 23:44:15 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-16T23:44:15 (1 data files) — data/backfill-latest.json","body":""},{"hash":"fac4ac0","date":"2026-09-16 23:12:18 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-16T23:12:18 (1 data files) — data/backfill-latest.json","body":""},{"hash":"a80d3ff","date":"2026-09-16 22:40:44 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-16T22:40:44 (1 data files) — data/backfill-latest.json","body":""},{"hash":"653afb7","date":"2026-09-16 22:08:59 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-16T22:08:59 (1 data files) — data/backfill-latest.json","body":""},{"hash":"371f0ed","date":"2026-09-16 21:37:04 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-16T21:37:03 (1 data files) — data/backfill-latest.json","body":""},{"hash":"b9f7965","date":"2026-09-16 21:05:13 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-16T21:05:13 (1 data files) — data/backfill-latest.json","body":""},{"hash":"5655f16","date":"2026-09-16 20:33:30 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-16T20:33:30 (1 data files) — data/backfill-latest.json","body":""},{"hash":"42ca032","date":"2026-09-16 20:01:35 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-16T20:01:35 (1 data files) — data/backfill-latest.json","body":""},{"hash":"e34116f","date":"2026-09-16 19:29:55 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-16T19:29:54 (1 data files) — data/backfill-latest.json","body":""},{"hash":"953bf13","date":"2026-09-16 18:58:01 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-16T18:58:01 (1 data files) — data/backfill-latest.json","body":""},{"hash":"611cd54","date":"2026-09-16 18:26:16 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-16T18:26:15 (1 data files) — data/backfill-latest.json","body":""},{"hash":"2d1ea55","date":"2026-09-16 17:53:57 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-16T17:53:57 (1 data files) — data/backfill-latest.json","body":""},{"hash":"9a4cc58","date":"2026-09-16 17:49:14 -0700","author":"Steve Abrams","subject":"backfill: write a bounded liveness heartbeat after the paint","body":"Heartbeat data/backfill-latest.json (verdict PASS + counts) written AFTER the\nside effect so its mtime proves the sole producer actually fired, not just that\nlaunchd reports it running. Lets the job run --quiet (bounded disk) while\nstaying observable. TK-11870.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_012hTBDZLzS7vZzHrjZwVfVd"},{"hash":"3790923","date":"2026-09-16 17:47:02 -0700","author":"Steve Abrams","subject":"backfill: always-on dot self-heal (repaint dropped dots + green-floor recordless live tabs)","body":"TK-11870. Under host overload (~49 sessions each scanning ps -ax) the owner\nscan times out >90s, repaint no-ops, and a tab silently loses its dot -- 68%\nof tabs were dark. Adds a 'backfill' subcommand + Store.backfill() that a\nscheduled external supervisor runs to (a) warm the cross-process disk cache\nwith ONE fresh scan so interactive sessions read it instead of each racing\ntheir own ps -ax (collapses the scan herd), and (b) re-assert every live tab's\ndot: repaint valid records (never mutated), green-floor owner_changed/missing\nlive owners (same rule as the start command), skip invalid/busy. Iterates\nowners(rows) so it paints each owner's OWN tty from its OWN record -- never\nguesses a tty from a label map. Ships negative tests (selftest 7/7): valid\ngated dot is never flattened to green; recordless owner is floored.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_012hTBDZLzS7vZzHrjZwVfVd"},{"hash":"c6b317d","date":"2026-09-16 14:14:32 -0700","author":"Steve Abrams","subject":"TK-11831: harden the ps-enrichment cache against the codex-check findings","body":"A second-model adversarial pass (gpt-5.3-codex, per the standing codex-check\nrule) found real defects in 28cf4f6 -- two of which produce a WRONG ticket\nbinding rather than merely a missed cache, i.e. the confidently-wrong-dot\nclass. Fixed, each with a test that is mutation-verified to go RED:\n\n1. STICKY NEGATIVE CACHE. A pid absent from the ps output (it exited mid-call,\n   or ps was truncated) was cached as \"\" and then never re-read, so ONE\n   transient miss left that tab unbound for the whole TTL. The uncached path\n   retried on the very next paint -- a cache must never be WORSE than what it\n   replaces. Now only a CONCLUSIVE read is cached: for argv the pid must have\n   come back with a non-empty args; for env the pid must be present AND its\n   args prefix must match (so the remainder really is the environment). A\n   settled pid with genuinely no TERM_SESSION_ID is a real answer and is still\n   cached, so those pids stop re-ps'ing and the win is kept.\n\n2. POISON OVERWRITE / LOST UPDATE. ~68 processes read-modify-write this file\n   with no lock. os.replace gives an untearable READ but does not serialise\n   RMW, so a slow writer merging onto its START-OF-PASS snapshot could drop --\n   or blank -- an entry a peer had already learned. Now the merge re-reads the\n   file at write time and NEVER downgrades: an empty value cannot overwrite a\n   non-empty one. Values are immutable per process, so two writers can only\n   ever agree; that makes concurrent writes commutative and lock-free without\n   a CAS, because no writer can lose information.\n\n3. UNVALIDATED CACHE VALUES. The uncached path can only ever put a str into\n   the argv map, so a hand-edited {\"a\": null} served as a hit was a cache\n   answer DIFFERING from the uncached one. Values are now type-checked on read\n   and anything unexpected is simply not a hit.\n\n4. KEY now includes runtime as well as pid+start, so a pid that exec'd into a\n   different runtime misses instead of serving the previous image's argv.\n\n5. Cache file written 0600 (it holds argv snippets + TERM_SESSION_ID), and the\n   TTL comment corrected: a steady all-hit pass writes nothing and therefore\n   does NOT refresh `t`. The TTL is a hygiene bound, not a correctness one --\n   identity is pinned by the key, and expiry costs ONE batched ps over the\n   missing pids, not one per session.\n\nTESTS: 49 pass (was 45; +4). All SEVEN mutations go RED on throwaway copies --\nM1 pid-reuse key, M2 cache bypassed, M3 lossy env, M4 sticky negative,\nM5 poison overwrite, M6 stale-snapshot merge, M7 unvalidated values. Control\nGREEN. A positive-only test on a cache proves nothing.\n\nA/B re-measured after hardening, alternating under live load 44-62 (n=6):\n  ORIG mean 1.79s worst 3.74s -> NEW mean 0.64s worst 0.91s\n  bindings diverged on 0/6 rows.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_014BD2ty1QgqWc2QVUVtYPRQ"},{"hash":"28cf4f6","date":"2026-09-16 14:06:49 -0700","author":"Steve Abrams","subject":"TK-11831: cache discover()'s two ps enrichment calls (the per-turn ps storm)","body":"discover() shelled out to `ps -p <all live pids> -o args=` AND `ps eww` on\nEVERY self-paint -- i.e. every UserPromptSubmit and every Stop hook -- across\n~68 concurrent sessions. Measured 2.46s per paint at 18 live owners and ~4.4s\nunder overnight load: the `ps` storm the watchtower attributed 245% aggregate\nCPU to, above node (112%) and iTerm2 (57%) at the 09:46 peak.\n\nTK-11835 already removed this from the CROSS-TTY path via discover's\nargv={}/env={} skip-seam but deliberately left the SELF path alone, and that\nwas right: measured here, full enrichment binds 13 tickets and the skip path\nbinds 0. Skipping would silently unbind every tab's ticket. So the fix is to\nCACHE, not to skip.\n\nBoth values are IMMUTABLE for the life of a process -- argv is fixed at exec\nand `ps eww` reports the env the process started with -- so a cache keyed on\npid AND start time is exact rather than approximate, and the start time makes\nit safe against pid reuse (a recycled pid misses and is re-read). Only pids\nMISSING from the cache are ps'd, so a steady fleet makes ZERO enrichment ps\ncalls and a newly-spawned session ps's a one-entry pid list, not all ~68.\nShared via an atomically-replaced /tmp file, so 68 sessions share one answer.\n\nFails OPEN at every step: any cache fault degrades to the uncached behaviour,\nexactly as a slow ps already degrades the label but never the paint.\n\nMEASURED A/B against HEAD, alternating under live load 78-101 (n=8):\n  ORIG mean 3.08s worst 9.92s  ->  NEW mean 0.68s worst 1.00s\nBindings IDENTICAL on every iteration (23==23) cold and warm -- a pure caching\nwin, not a behaviour change. The tail is now bounded because there is no ps\ncall left to spiral (an earlier ORIG sample hit 40.9s).\n\nTESTS: 45 pass (was 41; +4). MUTATION-VERIFIED per CLAUDE.md TK-11431\namendment 3 -- all three injected faults go RED on throwaway copies:\n  M1 drop start-time from the key (pid-reuse hole) -> RED\n  M2 cache read always empty (caching bypassed)    -> RED\n  M3 never store the TERM_SESSION_ID (lossy cache) -> RED\nControl GREEN. A positive-only test on a cache proves nothing.\n\nReversible: git revert, or TERMINAL_STATUS_PS_CACHE_TTL=-1 to disable hits.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_014BD2ty1QgqWc2QVUVtYPRQ"},{"hash":"c3c5843","date":"2026-09-16 11:18:54 -0700","author":"Steve","subject":"dot.sh: auto-force retry so bridge/sub sessions paint their dot","body":"A leaked-flag real session (CLAUDE_CODE_CHILD_SESSION set by the iTerm env-leak)\nwas wrongly refused by the subagent-paint guard, forcing a manual --force. Now\nthe shared wrapper retries once with the force opt-in on a bridge refusal, gated\nso it never recurses, never fires on a timeout, and only in a child-session\ncontext; the engine's own headless/nested refusal + selftest (4/4) are untouched.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_017bBRRWJJoCpD1BZK24wvUZ"},{"hash":"f18d087","date":"2026-09-16 11:15:19 -0700","author":"Steve Abrams","subject":"TK-11835: fail-fast cross-tty paint — a busy --tty target is skipped, not hung","body":"The --tty paint (external supervisor / dot sweeps) could hang 30-65s on a\nBUSY target claude session. Measured root cause on Mac2 under overnight load:\n\n1. tickets.discover() runs TWO ps calls per invocation (a `ps -p` argv read\n   AND a `ps eww` env dump over every live session) purely to enrich the tab's\n   ticket LABEL — ~4.4s under load, on EVERY paint before command dispatch, so\n   N concurrent sweepers saturate the process table into the ~30s ps spiral.\n2. Store.lock()'s 65s deadline is a SELF-writer backstop; a cross-tty sweeper\n   inherited it and waited up to 65s while a busy target held its own per-tty\n   lock during its own slow scan.\n\nFix, both scoped to the cross-tty / paint-legacy path only (a session painting\nits OWN tab is byte-identical to before):\n- Skip discover's ps enrichment via its existing argv/env seam ({} = already\n  resolved). The primary events.jsonl binding + known_tickets validation still\n  load fully. Cross-tty startup 3.06s -> 0.67s here (bigger under load).\n- Thread the already-resolved rows into set/set_variant/repaint -> lock ->\n  assert_owner so the in-lock owner check reuses main's table (no re-scan), and\n  add a short env-overridable lock budget (TERMINAL_STATUS_TTY_LOCK_WAIT, 2.5s)\n  so a busy target is reported+skipped fast instead of waited on 65s.\n\nWorst-case cross-tty paint is now bounded to ~3s vs 30-65s. Ships a negative\ntest (test_busy_target_skips_fast_with_short_lock_wait) proving the skip fires\nwithin budget; mutation-verified it goes RED when the wait override is ignored.\n41 tests pass, selftest 4/4.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01QwE2Bg691aQJjEG3XS9LLw"},{"hash":"06cd6c5","date":"2026-09-16 10:13:35 -0700","author":"Steve Abrams","subject":"dot.sh: check-for-color — hard timeout + report result, never a silent hang/no-op","body":"The engine's ps-scan can run to its ~90s ceiling under host load (appeared as a\nhang) and a tool/subagent-shell paint exits nonzero (silent no-op). dot.sh now\nruns every paint under a hard outer timeout (TS_PAINT_TIMEOUT, default 100s) and\nCHECKS the result, printing one clear line (painted/cleared/TIMED OUT/NOT painted)\nso a tab never silently loses its color. Exit code preserved; subagent-paint guard\nin terminal_status.py intentionally untouched (selftest still 4/4).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"5463758","date":"2026-09-16 09:35:06 -0700","author":"Steve Abrams","subject":"TK-11826 (TK-11779 Finding 2): pin ticket_update=None in T1-c preserve block + coherence regression test","body":"The T1-c lightblue preserve branch already cannot mismatch (old label + new\nticket) today — its 'not caller_gave_reason' guard (e0a025a) guarantees\nticket_update is None on that path. Land the credential-desk memo's fix\ndefensively: pin ticket_update=None inside the block so the preserved reason\nstays coherent as a whole (colour+label+ticket) even if a future edit loosens\nthe branch guard. Add test_lightblue_never_mismatches_old_label_with_new_ticket\nto lock the invariant permanently.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"a1139e9","date":"2026-09-16 08:43:25 -0700","author":"Steve Abrams","subject":"dot.sh: forward all set args so --tty/--force reach the engine","body":"The compat shim passed only ${1} as the label, silently dropping engine\nflags. That blocked painting a verified tty when CLAUDE_CODE_CHILD_SESSION\nis leaked into a real interactive session (the iTerm-relaunch env-leak),\nforcing a direct engine call. Forward \"$@\" on the default set branch so\nevery singular color skill (green/orange/purple/…) can target a tty.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01Ufyow1KDpDysm133tajFbh"},{"hash":"ea41eb3","date":"2026-09-15 18:47:40 -0700","author":"Steve Abrams","subject":"TK-11794: green 'waiting' variant — brighter static green (0,255,120) for an idle/waiting green session","body":"A green-only tint parallel to monitoring (teal): distinguishes an idle/waiting\ngreen session from an actively-working one WITHOUT pulsing — green stays SOLID\nper Steve's dot directive. Guarded at set-time (dropped on any non-green base),\naccepted by load()/set()/set-variant validation, wired to CLI --waiting +\nset-variant waiting. Variant precedence: stopped > monitoring > waiting. Render-\nonly rgb; stored title recomputes byte-identically so no record goes invalid.\n39/39 tests pass incl test_waiting_shade_is_brighter_green_and_survives_repaint.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01HKMY3eXqwFUCfqDShfUVnH"},{"hash":"585ee06","date":"2026-09-15 18:33:17 -0700","author":"vp-engineering","subject":"TK-11794: pulse the 4 needs-Steve dot states; green/pink stay solid","body":"Render-time only: the waiting states (yellow/purple/orange/lightblue) carry a\ntwo-frame attention marker (revision-keyed) in the tab badge; green/pink render\nunchanged. Marker is NOT in the stored/validated title, so load() recomputes\nbyte-identically and no existing record goes invalid. Zero background loops,\nzero new subprocess/scan cost. The real animated pulse lives in desktop-dotbar.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_0169ph94FSR3sVnot1p5pNX1"},{"hash":"e0a025a","date":"2026-09-15 18:16:40 -0700","author":"Steve","subject":"terminal-status: keep lightblue label+ticket consistent (TK-11779) - bare set(lightblue) preserves both old label+ticket, explicit reason takes both new; +tests","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01Pnon7dut9MfPekhhm9YY4Q"},{"hash":"df6f28c","date":"2026-09-15 13:34:54 -0700","author":"steve","subject":"T2 (TK-11779): add dead-tty .dot/.json reaper (ps-liveness authority, tar-backed --apply)","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01JuGCNYd1owT7PaPwi9t9sQ"},{"hash":"1286106","date":"2026-09-15 12:44:34 -0700","author":"Steve Abrams","subject":"terminal-status: retry transient EAGAIN in write_terminal so a busy tty can't latch a 24h health FAIL","body":"write_terminal opened the tty O_NONBLOCK then did a single os.write with no EAGAIN\nhandling, so a momentarily-full tty buffer raised BlockingIOError (Errno 35) that\nescaped to the top handler; dot-floor.sh then latched the fleet-health panel FAIL\nfor 24h off a ~1.4% transient. Now: bounded 0.5s retry loop handling EAGAIN + partial\nwrites, raising the already-benign 'Terminal busy; retry' only on a genuine >0.5s wedge.\nVerified: py_compile OK + 3 behavioral tests (real-pty positive, 2xEAGAIN+partial, wedged=0.51s bounded).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01A5jmQ4a4VhZNPTRkVUSeML"},{"hash":"9731e7f","date":"2026-09-14 00:37:26 -0700","author":"Steve Abrams","subject":"TK-11666: measure the TK-REQUIRED false-positive rate (the missing denominator)","body":"Add a READ-ONLY metric answering the question nothing computed — \"of the live\nGREEN sessions labelled TK REQUIRED, how many are PROVABLY TRACKED (false\npositives)?\" — the missing correctness metric that let the pattern recur 3x/day\nand spawned ~24 spurious tickets.\n\nThree honest buckets (TK-11431 rule 1): bound / not_measured (never green, never\nan accusation) / contradicted (the false positive to drive to 0). Carries\npopulation beside observed (\"0 of 0\" != \"0 of 60\"), counts LIVE sessions via\nallcolordots --json (never the stale tab-dots dir), keys on PID not tty.\n\nIndependence: the LABEL is the persisted dot state (what Steve sees) while the\nresolvability probe re-scans the RAW ledger (claude@TERM_SESSION_ID work events)\nand RAW argv itself — it does NOT call the binder it grades, so a binder gap\nthat leaves a tracked session reading TK REQUIRED surfaces as contradicted\ninstead of being laundered into agreement. Coupled to TK-11665: as that binder\nimproves, contradicted → 0.\n\nLaunch-latency guard: a tracked-but-young session is `pending`, not\ncontradicted; an UNKNOWN age is NEVER excused as young (fail-safe toward\ndetecting the false positive). Age uses macOS `ps lstart` (there is no etimes).\n\nVerdict: FAIL contradicted>0 · WARN input-unobservable/zero-sessions (never a\ngreen PASS) · PASS >=1 session and contradicted==0. Ships a negative test\n(--self-test + 13 unit tests) proven to go RED on an injected contradicted row;\nthe test seam is the pure classify() function, so a scheduled run never measures\na fixture. Emits verdict+status (canonical PASS/WARN/FAIL) to the skill's\ndata/latest.json for fleet-health-rollup (no rollup vocabulary change needed).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01UVgEj4WxSqB62ieGpgrixR"},{"hash":"4187d75","date":"2026-09-14 00:30:03 -0700","author":"Steve Abrams","subject":"TK-11665: bind a session's ticket via ledger agent claude@TERM_SESSION_ID","body":"Add a 4th, last-resort binding source to ticket_binding.discover(): when no\nMCP-correlation and no argv ticket bind a live session, resolve the session's\nTERM_SESSION_ID from its process ENV (keyed PID->TSID, never tty) and bind the\nmost recent assign/create/action/comment event logged under claude@<TSID> (the\ntk CLI's fallback identity when a session sets no TK_AGENT).\n\nNo epoch floor: TERM_SESSION_ID is a stable session identity across a --continue\nresume in the same pane, so events predating this process's start are that\nsession's own prior work -- this is the fix for the resumed 'claude --continue'\nclass that permanently read \"TK REQUIRED\". Same `in known` safety as every other\nsource (never invents a ticket); never overrides a bound argv source; cannot\nreach a self-chosen TK_AGENT (the honest KNOWN CEILING). Reads env via `ps eww`\nminus the `-o args=` prefix to avoid the args+env concatenation trap.\n\nShips a positive test (resumed session, pre-start events bind) and a non-vacuous\nnegative test (read-noise / unknown ticket / no TSID / argv-present-no-override\nall correctly abstain) -- proven to go RED on an injected fault.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01UVgEj4WxSqB62ieGpgrixR"},{"hash":"664d1de","date":"2026-09-13 23:30:46 -0700","author":"Steve Abrams","subject":"TK-11672: fix permanently-red terminal-status test via injectable rescan seam","body":"current_owner()'s TK-11398 fresh-rescan escape re-read the LIVE process\ntable (processes(fresh=True)) instead of the passed-in rows fixture, so\nrun inside a live claude session it resolved a real owner and the\nheadless-nested-agent refusal never fired -> test_headless_nested_agent_\ncannot_paint_parent was a standing RED, making the 29-test suite unusable\nas a gate for the 20 consumer scripts on this engine.\n\nAdd an injectable refetch callable (default processes, so production is\nbyte-identical) threaded through both rescan retries; the test injects\nthe fixture as the rescan source. Suite now 29/29; negative test proves\nSEAM-ON raises, SEAM-OFF returns a live owner (the original bug).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01UVgEj4WxSqB62ieGpgrixR"},{"hash":"b85d85b","date":"2026-09-13 16:55:26 -0700","author":"Steve Abrams","subject":"require `export ` + a single declaration before binding a TK_AGENT (TK-11631)","body":"Follow-up to 2315808, which was too loose in two ways the contrarian caught:\n\n  * it matched a bare `TK_AGENT=` ANYWHERE in argv, so a prose mention bound a\n    ticket: \"fix the bug where TK_AGENT=claude-run-11630 fails to bind\" bound\n    TK-11630 with no export and no launch;\n  * it took re.search's FIRST hit with no uniqueness test, making it LESS\n    conservative than the len(ids)==1 rule it front-runs. The argv of an agent\n    working this very ticket carries three mentions and bound the right one only\n    by accident of sentence order -- reverse the sentences and it binds another\n    session's ticket. That is the self-match trap in the memory note\n    detector-argv-substring-self-match, in the one place it hurts most.\n\nThe match now requires BOTH the launcher's literal shape (`export ` prefix and\nthe trailing period, verified at run-ticket.sh:60 and against 44 of 44 live\nsessions) and exactly ONE DISTINCT declared id via findall. Two declarations\nabstain to the old rule rather than picking one. The in-known guard is unchanged.\n`ps -o args=` flattens argv, so the prompt argument's own start is not locatable;\nthis literal is the anchor instead of a positional hack.\n\nControlled live measurement, discover() run three times over ONE shared rows +\nargv snapshot: AGENT off = 41 bound, loose = 44, tight = 44; rescues 3, rebound\n0, lost 0 in both directions. The tightening removes false-bind CAPABILITY and\nchanges nothing on today's population -- no live session carries a bad shape.\n\nEach half is proven load-bearing by an injected fault: dropping `export `,\ndropping the period, or dropping uniqueness each reddens the new test; dropping\nthe in-known guard reddens the anti-invention test; removing the source reddens\nthe positive test.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_011yMsG8b5h9zymuwmtabAMJ"},{"hash":"2315808","date":"2026-09-13 16:25:48 -0700","author":"Steve Abrams","subject":"bind a session by its launcher-declared TK_AGENT, so a slug carrying two TK ids no longer reads TK REQUIRED (TK-11631)","body":"run-ticket.sh bakes `export TK_AGENT=<prefix>-<IDNUM>` into the launch prompt and\nIDNUM is the DRIVING ticket's number alone. The pre-existing argv rule scans for bare\nTK ids and binds only when it finds exactly one, so a session driving a ticket whose\nslug embeds a referenced ticket (TK-11630-tk-11340-...) yields two ids, abstains, and\nis mislabelled TK REQUIRED for its whole life while logging real work on the board.\n54 of 1783 tickets carry that slug shape.\n\nNew source main_process_agent is tried first, still gated on the known-ticket set, and\ndoes not change the single-id rule it falls through to. Measured live on the same\nprocess table: 40 bound -> 42 bound, 0 lost, 0 rebound to a different id.\n\nTests ship with the injected-fault proof required by CLAUDE.md TK-11431 amendment 3:\nremoving the new source reddens the positive test, removing the known-set guard or\nguessing an id reddens the anti-invention test, and a greedy capture that swallows the\nprompt's trailing period reddens the positive test.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_011yMsG8b5h9zymuwmtabAMJ"},{"hash":"4e63168","date":"2026-09-13 09:43:33 -0700","author":"Steve Abrams","subject":"stopped marker must not reuse the lightblue base codepoint","body":"STOPPED_MARK was U+1F535 -- byte-identical to the lightblue base colour. Since\nstatus_title() flattens state + variant into one string, 'state=lightblue' and\n'state=purple, variant=stopped' rendered IDENTICALLY. No string-level reader, and\nno human eye on the tab bar, could tell 'this tab is generically needs-Steve' from\n'this gated tab has gone idle'. Two already-distinct canonical fields were being\ncollapsed onto one glyph, and the consumer disagreements chased this week all\ntraced back to it.\n\nNow U+1F6D1 (stop sign): different SHAPE (octagon vs circle) and different hue (red\nis unused in the palette), so the second axis rides a second visual channel instead\nof overloading colour.\n\nSafe by construction: STOPPED_MARK has exactly one render site and nothing parses\nit, so no reader can break. color_of() matches title.startswith(base dot) and is\nunaffected. Consumers that classify read canonical state/variant; the few that still\ngrep a glyph only OVER-report during the repaint window, never under-report.\nSide benefit: lightblue+stopped rendered a doubled 🔵🔵 and now reads 🔵🛑.\n\nTests: 26 run, 1 failure -- test_headless_nested_agent_cannot_paint_parent, which\nfails identically BEFORE this change. No new failures.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01Vbtgv1MuWftpd3hfWEiZYG"},{"hash":"1c16fa2","date":"2026-09-12 04:05:19 -0700","author":"steve@designerwallcoverings.com","subject":"fix: cache ps scan + fix 60s ceiling exit-code in terminal-status (TK-11398, TK-11505)","body":""},{"hash":"ce04960","date":"2026-09-11 12:20:03 -0700","author":"Steve Abrams","subject":"terminal-status: cross-process proc-table cache + negative-rescan valve (TK-11398)","body":"Paints were taking ~100s and often hitting the 60s ps ceiling and silently\nno-op'ing, leaving tabs showing a PREVIOUS session's dot (observed: a tab\nadvertising a finished, unrelated ticket while working another) -- the\nfalse-green class, since the wall looks authoritative while being wrong.\n\nRoot cause is a thundering herd, not a constant cost. _PROC_CACHE is\nper-PROCESS and every dot call is a fresh python3, so the cache was dead on\narrival and all ~42 live sessions each ran their own full `ps -ax`. Measured:\nthree consecutive scans cost 2.30s / 4.90s / 7.08s -- the scans mutually slow\neach other, so the herd is self-amplifying.\n\nDOCUMENTED DECISION REVERSAL: the in-code scope note said a cross-process file\ncache was \"not worth the risk\" because a stale table could miss a just-started\nsession and wrongly report \"no owning terminal\". Reversed deliberately, by\nclaude-run-11255, with Steve's explicit approval, on the new measured fact\nabove. The objection is preserved rather than traded away: a cached table is\nonly ever trusted for a POSITIVE answer -- every negative ownership conclusion\n(current_owner finding no owning runtime; assert_owner about to refuse a stale\nwriter) re-scans fresh and re-decides before it is allowed to fail. A negative\ncan cost one extra scan; it can never produce a wrong refusal.\n\n_splice_self_chain() is what makes the cache actually pay off: a cached table\npredates the current process, so ancestors(os.getpid()) dead-ended and forced\nthe valve on EVERY call, cancelling the whole benefit (observed directly via\nthe new debug seam: \"HIT\" immediately followed by \"MISS -> scan (fresh=True\nforced)\"). It now walks our own chain with targeted single-pid ps calls\n(0.01-0.10s each, O(depth) not O(2000)) and returns None rather than a partial\nchain, so failure can only cost a scan.\n\n_DISK_TTL is 30s, not 5s: a scan measures 15-70s under load, so a 5s window\nexpires before the next invocation can reach it -- the same trap the existing\nper-PROCESS TTL note describes. Staleness is bounded by the valve, not the TTL.\n\nTERMINAL_STATUS_DEBUG=1 prints proc-cache HIT/MISS to stderr -- a cache whose\nhit rate you cannot observe is a cache you cannot prove works. Opt-in only.\n\nVerified end-to-end, with injected faults (positive-only tests would prove\nnothing about a correctness valve; each fixture asserts it is not a no-op):\n  - warm resolve: 17-72s -> 1.4-3.0s, single HIT, no forced rescan\n  - real paint:   ~100s  -> 4.41s, dot file verified\n  - fault A (70 claude/codex rows stripped from a fresh cache):\n      HIT -> valve -> resolves the REAL owner pid 30729\n  - fault B (my tty rewritten to impostor pid 999001):\n      HIT -> valve -> resolves REAL pid 30729, impostor rejected\n  - `audit` clean, no new drift\n\nComplements 9696608 (per-process TTL 5s->120s, another session, TK-11466\nfollowup), which fixes the intra-run double scan; this fixes the cross-session\nherd. Both are needed.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_012LVUzeaBYQJsjSYSWtQZYy"},{"hash":"9696608","date":"2026-09-11 12:07:07 -0700","author":"Steve Abrams","subject":"terminal-status: fix paint hang — raise per-process PROC_TTL 5s→120s so a single paint scans once, not twice","body":"The 5s cache TTL was shorter than the ~15s (up to 60s) process-table scan, so the\nsecond intra-run processes() call always found the cache expired and re-scanned —\nevery paint paid the scan twice (~120s worst case), leaving a stale two-dot\ntransient on the tab during the slow paint. The cache is per-process (reset each\ninvocation), so a larger TTL only bridges the two calls within one run with zero\ncross-invocation staleness. Verified: paint 120s→13.3s (single scan).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01DSZgKJ86hmzguEeX8iqoZi"},{"hash":"1bc426c","date":"2026-09-11 11:16:33 -0700","author":"Steve Abrams","subject":"set-variant: --tty targeting + don't require caller ancestry (2026-09-11)","body":"Lets an EXTERNAL supervisor mark another session. Two fixes, both found by forcing a\nlaunchd run rather than trusting its exit code:\n\n1. --tty was registered on the shared subcommand loop but NOT on set-variant, and its\n   dispatch resolved the caller's own tty — so it could never target another session.\n2. caller = current_owner(rows) ran UNCONDITIONALLY, before --tty was read. It walks the\n   caller's process ancestry for a terminal; launchd has none, so every scheduled run\n   died on 'No owning Claude/Codex terminal in this process ancestry' while launchctl\n   reported\nwtmp begins Mon May 25 23:28:20 PDT 2026, , and an empty stderr. A textbook false\n   green: loaded, exited clean, accomplished nothing. Caller resolution is now optional\n   when --tty names an explicit target; assert_owner() still validates that target\n   against the live process table, so a dead or reassigned tty is refused as before.\n\nVerified under launchd after the fix: 4 failures -> 0, verdict WARN -> PASS.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"1f88b72","date":"2026-09-11 10:54:06 -0700","author":"Steve Abrams","subject":"terminal_status.py: set-variant --if-blocked (only 🔵-mark needs-Steve stops)","body":"Gates the additive 🔵 stopped-marker to blocked base colours (yellow/purple/orange)\nvia a cheap record read — so the Stop hook marks only stops that actually need\nSteve, not green/pink stops. Verified: purple→🟣🔵, green→no-op.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"43fe598","date":"2026-09-11 10:51:08 -0700","author":"Steve Abrams","subject":"terminal_status.py: fix set-variant — was calling nonexistent Owner.detect()/STORE","body":"The set-variant branch shipped calling Owner.detect() (no such method) and STORE\n(undefined), so it crashed with AttributeError in EVERY session and the additive\n🔵 stopped-marker never worked. Now uses the already-resolved caller + the real\nstore instance, matching every other command. Verified: set-variant stopped paints\nbase+🔵 (e.g. 🟣🔵), set-variant '' clears it.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"c40a4b0","date":"2026-09-11 10:31:24 -0700","author":"Steve Abrams","subject":"terminal-status: add lightblue to PRIORITY (unblock scan crash) + default-sort unknown colors last","body":"The lightbluedot skill (2026-09-11) added 'lightblue' to COLORS but not the\nPRIORITY tuple, so scan() crashed KeyError:'lightblue' whenever any tab was\nlight-blue -- taking down allcolordots and every dot sweep. lightblue ranks\nabove the three per its own spec. Also PRIORITY.get(...,last) so the next new\ncolor can't crash the scanner (TK-11431 consumer-update lesson).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01XGhtD1r4okMDSB33iiEci2"},{"hash":"b37dbc2","date":"2026-09-11 10:15:56 -0700","author":"Steve Abrams","subject":"stopped variant: keep the base colour dot, put 🔵 NEXT TO it (2026-09-11)","body":"Steve: 'keep color dot and place blue dot next to it when stopped … when running\nrevert back to original color dot with that dot next to the orig color dot flashing.'\n\nAdditive marker, never a replacement:  🟣 -> 🟣🔵 stopped -> 🟣 running.\n\nThree defects caught while wiring it, each of which would have shipped a silent no-op:\n  1. set() discarded the variant for any non-green colour ('variant if color == \"green\"'),\n     so --stopped on purple/yellow/orange — the blocked colours this exists for — would\n     have been accepted, reported success, and vanished. stopped is now colour-agnostic;\n     monitoring stays green-only because it is the teal TINT of green.\n  2. All three status_title() call sites built the title WITHOUT the variant, so even a\n     stored variant would not render — and the canonical check at load() compares title\n     to expected, so the record would then be judged invalid and wiped.\n  3. set_variant() first drafted against self.write()/self.painter() as methods; write()\n     does not exist and the real path also updates the legacy .dot mirror, which is what\n     allcolordots and dot-screen-router actually read. Updating only the JSON would have\n     left every consumer showing the old title while the engine reported success.\n\nVerified with negative tests: purple+stopped renders 🟣🔵 and reverts clean; a bogus\nvariant raises; monitoring on a non-green colour is dropped.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"a56bd8c","date":"2026-09-11 10:06:17 -0700","author":"Steve Abrams","subject":"colors: add lightblue = ANY STOP THAT REQUIRES STEVE'S INPUT","body":"Steve, 2026-09-11: 'It should mean. Any stop that requires my input on any terminal.'\n\nAdds the umbrella scan colour 🔵 (0,176,240) 'NEEDS STEVE'. Deliberately overlaps\nyellow/DIRECTION?, purple/GATED and orange/PASTE — Steve chose to KEEP all three, so\nthey still carry what kind of stop it is; lightblue is the single colour to scan when\nyou only want 'has this stopped and does it need me?'.\n\nset_status() rejects any colour not in COLORS, so this entry is what makes 🔵 legal.\nConsumers were updated in the SAME change (allcolordots, dot-screen-router) per the\nstanding rule that a producer must never emit a word its consumer cannot read.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"6eae7e8","date":"2026-09-10 09:32:27 -0700","author":"Steve Abrams","subject":"lock: stop holding the per-tty lock across the 14.84s process-table scan","body":"Measured on this box: processes() (the `ps -axo` scan) = 14.84s, owners() over\nthose rows = 0.00s, and Store.lock()'s wait deadline was 3s. Store.assert_owner\nran the scan INSIDE the lock, so a same-tty writer could never win: 14.84 > 3,\nalways. That is the mechanism behind the \"Terminal status is busy; retry\"\nfailures another session hit live twice today.\n\n63f3fae (raising the ps timeout 8s -> 60s to survive a busy box) made this\nworse by raising the ceiling on lock-hold time from 8s to 60s -- the thundering\nherd an earlier review warned about, now with a number attached.\n\nThree complementary fixes:\n\n1. Short-TTL cache on processes(). One CLI run scans at least twice (resolving\n   the caller, then again inside the lock); this removes the second. Per-PROCESS\n   only -- it deliberately does NOT cache across the ~49 sessions, because a\n   table a few seconds stale could miss a just-started session and wrongly\n   report \"no owning terminal\". TERMINAL_STATUS_PROC_TTL overrides (default 5s).\n\n2. assert_owner(owner, rows=None) accepts an already-resolved table, threaded\n   through lock(owner, rows=None), so the guard is kept but the scan leaves the\n   critical section.\n\n3. Lock wait deadline 3s -> 65s (TERMINAL_STATUS_LOCK_WAIT). With 1+2 the lock\n   is held ~0s, so this is a backstop that should never be reached; it is not\n   licence to hold the lock across expensive work.\n\nMeasured after: in-lock scan 7.14s -> 0.0000s.\n\nVerified: repaint exit 0; status exit 0; dot intact; hook 3x exit 0 with zero\nnew failure records and verdict PASS; and the ownership guard still correctly\nrefuses a stale writer (a forged owner is rejected with \"Terminal owner\nchanged\"), so the safety property is preserved, not traded away.\n\nReversible: git revert.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01PXNMS1TvMiVbE3ckhSaLeT"},{"hash":"fea7539","date":"2026-09-10 08:46:15 -0700","author":"Steve Abrams","subject":"ticket_binding: a slow ps must degrade the label, not kill the paint","body":"Surfaced by the logging added in 29475ed: within minutes of the dot-floor hook\nfinally recording real failures, repaint was failing every 1-3 minutes on this\nbox with `Command '['ps', '-p', <~49 pids>]' timed out after 8 seconds`.\n\nThis call is a THIRD process-table read, in ticket_binding.py rather than\nterminal_status.py, which is why an audit of the engine missed it. With ~49\nlive sessions the pid list is large and 8s was not survivable at load 53.78.\n\nWorse than the timeout was the severity: it raised, which killed the ENTIRE\nrepaint, so no dot was painted at all. The block's own comment calls it \"a\nconservative fallback\" for the ticket LABEL -- optional enrichment. An optional\nenrichment must never take down the paint.\n\nRaised to 60s to match the sibling process-table read, and made non-fatal: on\ntimeout the label is skipped, the blind spot is recorded, and the dot still\npaints. Verified 5/5 consecutive clean hook runs with zero new failures logged.\n\nExtends DTD 2026-09-10 verdict C to the same defect class in a third location.\nReversible: git revert.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01PXNMS1TvMiVbE3ckhSaLeT"},{"hash":"29475ed","date":"2026-09-10 08:33:42 -0700","author":"Steve Abrams","subject":"osascript enumeration: raise timeout to 30s and record blind spots","body":"The iTerm session enumeration ran osascript with timeout=8 and swallowed\nfailure as `return {}, \"unavailable\"` -- no exception, no exit code. That is\nthe false-green class: on a box that hit load 53.78 today (with the sibling\nps call measured at 20.4s), AppleScript enumeration cost scales with tab\ncount (~49 here), so an 8s ceiling was trippable and its failure was\nindistinguishable from \"no sessions\".\n\nIt matters because `start` treats \"unavailable\" as an empty result and falls\nthrough to painting green instead of restoring a sticky semantic dot -- a\nconfidently-wrong paint, worse than a missing one.\n\nRaised to 30s, deliberately NOT the 60s used for the ps call: this runs at\nsession start, where a longer hang would delay startup.\n\nBlind spots now append to the terminal-status-health log via a fail-safe\nrecorder that never raises and never blocks painting.\n\nDTD 2026-09-10 verdict C (4/5). Reversible: git revert.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01PXNMS1TvMiVbE3ckhSaLeT"},{"hash":"e6a426b","date":"2026-09-10 08:21:00 -0700","author":"Steve Abrams","subject":"set: automatic paints yield to sticky semantic dots (orange/purple/yellow)","body":"A UserPromptSubmit hook fires `set green WORKING` on EVERY prompt, and green\noverwrote whatever dot the tab held. So typing into a tab silently erased its\npurple (gated memo), orange (paste waiting) or yellow (question waiting) and\nreplaced it with green = 'nothing needed'. Observed twice in one session.\n\nPRIORITY (orange > purple > yellow > green > pink > none) already existed at\nline 31 but was only ever used to sort scan() output, never to arbitrate a\npaint. This makes set() consult it: a caller passing --deferential will not\noverwrite a state ranked above green; it repaints the existing record and\nreturns it unchanged (no revision bump, no mirror churn).\n\nOnly the automatic per-prompt painter passes --deferential. Every EXPLICIT\npaint stays authoritative, so /greendot, /color, /pinkdot and --off can still\nclear a sticky dot and a tab can still be reused for new work. The teal\nmonitoring variant is green-family and yields too.\n\nVerified with an in-process truth table (11/11): sticky states survive an\nautomatic green; green/pink do not; explicit green/pink/none still win;\nrevision does not bump when the guard yields.\n\nTK-11378, DTD verdict A (6/7). Steve approved 2026-09-10.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01RwTfiTtWZNjLoowhjXdWV1"},{"hash":"63f3fae","date":"2026-09-10 07:58:58 -0700","author":"Steve Abrams","subject":"fix: raise process-table timeout so dot painting survives a busy box","body":"`ps -ax` costs ~4-5s idle and ~30s under load on this workstation (~2k\nprocesses from the MCP fleet), so the 8s ceiling in processes() was\nmarginal at rest and failed outright whenever anything ran in parallel --\nevery color-dot script died with \"timed out after 8 seconds\" and no tab\ncould be painted at all. Raise to 60s and convert the bare TimeoutExpired\ninto an actionable StatusError. The full table is genuinely required\n(owners() proves a tty has exactly ONE live runtime before agreeing to\npaint it), so this is headroom, not a narrower query.\n\nVerified: 26/26 tests pass; current.sh now resolves ttys022 in 9.2s.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01MHKdiEfwLNDHqKgMRbBNS2"},{"hash":"914a88d","date":"2026-09-09 14:55:12 -0700","author":"Steve Abrams","subject":"fix: paint guard resolves ancestry before refusing on CLAUDE_CODE_CHILD_SESSION","body":"The env var is set on any tool-spawned shell (Bash tool, hooks), so it made\n/dot no-op for the real interactive session. Now ownership is resolved by\nancestry first — a top-level claude/codex that is the live owner of its tty\npaints; only a genuine headless/nested agent (or no owning tty) is refused.\nAll 26 tests pass incl. test_headless_nested_agent_cannot_paint_parent.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01VzZkMwx4e9ec829p24cAp3"},{"hash":"418fa09","date":"2026-09-09 14:54:02 -0700","author":"Steve Abrams","subject":"snapshot before child-session paint-guard fix","body":""},{"hash":"03cc6f9","date":"2026-09-09 11:09:49 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-09T11:09:48 (1 data files) — verification/live-journey.json","body":""},{"hash":"2b5c51d","date":"2026-09-09 10:37:25 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-09T10:37:25 (1 data files) — verification/install-plan.json","body":""},{"hash":"2e09c09","date":"2026-09-09 10:04:39 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-09T10:04:38 (1 data files) — verification/install-plan.json","body":""},{"hash":"94e4908","date":"2026-09-09 09:57:10 -0700","author":"Steve Abrams","subject":"Add shared session-bound terminal status engine and regression tests","body":""},{"hash":"f49b9f9","date":"2026-09-09 09:43:48 -0700","author":"Steve Abrams","subject":"Initial terminal status scaffold","body":""}]}