Creative ideas + design notes
Commits with substantial prose (≥120 chars) — the rationale behind each move.
f171fc9 · 2026-09-26 · TK-11317: fix deferential-set bypass + ticket-rebind hole; add carry age ceiling + revision continuity
Contrarian review of b594dbb found a reproducible CRITICAL hole: working-state.sh's
per-prompt UserPromptSubmit hook fires `set green WORKING --deferential --boot` on
every prompt. In set(), the deferential-yield guard requires a truthy `previous`, but
load(owner) returns None for reason owner_changed -- so the very first prompt after a
tty reuse silently overwrote a carried purple/orange/yellow/lightblue with plain green,
bypassing settle_owner_change entirely.
Fix: set() is now a thin lock-acquiring wrapper around a new _set_locked() write body.
When _set_locked sees previous is None, reason is owner_changed, and the caller is
deferential, it settles the owner change FIRST -- via a new _settle_locked() lock-free
write body (never re-enters self.lock(), which would deadlock; flock is not re-entrant
within one process) -- and uses the settled record as `previous`. Its colour is always
>= yellow priority, so the existing deferential-yield guard right after always fires and
repaints the settled record instead of falling through to plain green. A non-deferential
explicit set (e.g. /greendot on a genuinely new session) is unchanged -- the new branch
only engages for deferential=True.
Same-class hole #2: the interactive `ticket` command and auto-bind's _default_ticket_set
both did `previous, _ = store.read(owner)` and silently rebuilt the record as color
"none" whenever reason was actually owner_changed, dropping a carried colour the instant
a ticket got rebound before settle ever ran. Fixed via a new Store.effective_previous(),
backed by a shared Store._settlement_preview() (also now used by row()'s pre-backfill
preview and settle_owner_change itself, so all three can never disagree).
HIGH: prior_attention() now gates every candidate by CARRY_MAX_AGE_S (default 86400s /
24h, env TERMINAL_STATUS_CARRY_MAX_AGE_S) -- a source older than that, or one whose age
can't be determined, is never carried, floors to the yellow "New owner - status needed"
state instead (never green, never none, no ticket). Age is measured from the ORIGINAL
event: a record that is itself already a carry reports its age from its own
carried_from["at"], so a relay of owner_changed hops can't keep resetting the clock.
carried_from is now {"pid","started","at","hops"} -- pid/started/at are the ORIGINAL
carrier's identity (preserved across every hop), hops counts the relay length.
LOW: a carry write now continues the raw prior record's own revision (+1) instead of
resetting to 1, via a new revision_override param threaded through set()/_set_locked.
16 new tests (deferential-bypass repro for both the carried-attention and
nothing-carried cases, explicit-set-still-wins, staleness ceiling for both JSON and
legacy .dot sources, a 3-generation hop chain preserving original provenance, revision
continuity, effective_previous() passthrough, and the ticket-command/auto-bind repro) +
1 new selftest case (19/19). Negative control: 15/45 new tests fail/error against
b594dbb, including both deferential-bypass repros
(verification/TK-11317-integration/negative-control-v2.txt). Full suite green after the
fix (verification/TK-11317-integration/after-tests-v2.txt): test_terminal_status.py (75),
test_owner_change_carry.py (45), test_departed_asks.py (20), selftest (19/19),
test_stop_verdict.py (40), test_tk_required_fp_metric.py (13).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqLpisVAyfYM4zoVP7fFya
468627f · 2026-09-26 · TK-12326: durable departed-asks register + resolver so a reused tty's prior asks outlive the carry
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdkK32KZQYqwzJKEUv17jR
76859a5 · 2026-09-26 · TK-11317: live acceptance evidence (Claude+Codex readback, same-tty carry, canary PASS)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqLpisVAyfYM4zoVP7fFya
b594dbb · 2026-09-26 · TK-11317: never blank/green-float a prior needs-Steve colour across owner_changed
Steve's TK-11620 guard ruling (2026-09-26): when a live tty is reused by a new
claude/codex pid (e.g. after /res), Store.load() reports reason owner_changed
and the engine used to blindly floor the new record to green -- silently
dropping whatever purple/orange/yellow/lightblue attention the prior owner
left pending.
Add Store.prior_attention()/prior_owner_info() (read-only: inspect the raw
JSON record at this tty's path with the owner-identity check skipped, plus
the legacy .dot mirror at any mtime, pick the higher-PRIORITY ATTENTION
colour) and Store.settle_owner_change() (write: carry that colour forward
with carried_from provenance, or floor to yellow "New owner - status needed"
with owner_status_unverified only when nothing was pending). Both are new
optional fields on set()'s opt-in-per-call contract, so an ordinary explicit
set() still clears them.
Wire settle_owner_change into backfill()'s and start's owner_changed
branches (missing keeps its plain green floor -- there is no prior state to
preserve there). row() renders the would-be-settled state for the <20s
window before backfill runs, instead of a bare "status not set". The
`ticket` command now explicitly forwards carried_from/owner_status_unverified
so rebinding a ticket id never drops them.
29 new tests (test_owner_change_carry.py) + 4 new selftest cases. Negative
control: 27/29 new tests fail/error when run against the tk11317-preimage
tag (verification/TK-11317-integration/negative-control.txt). Full suite
green after the fix (verification/TK-11317-integration/after-tests.txt):
test_terminal_status.py (75), test_owner_change_carry.py (29), selftest
(18/18), test_stop_verdict.py (40), test_tk_required_fp_metric.py (13).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqLpisVAyfYM4zoVP7fFya
6d68776 · 2026-09-24 · Auto-bind a ticket for unbound sessions from the UserPromptSubmit hook (TK-12168)
ticket_binding.discover() is deliberately read-only, so a plain top-level
interactive session that never ran `tk` and carries no TK- in its argv sits
unbound ("TK REQUIRED") forever. Adds a new `auto-bind` subcommand
(cmd_auto_bind) that runs from working-state.sh on every prompt: if the
session is still unbound after a substantive prompt, bind the TK- it names,
or mint one via `tk new` and bind that. Trivial prompts (ok/yes/bare slash
commands/<3 words) never mint a ticket; a per-tty flock lock + state file
prevent duplicate creation; every `tk` failure fails open and rate-limits
the retry so a broken `tk` binary is never hammered.
Adds explicit_ticket_in_prompt / is_trivial_prompt / short_topic as pure,
unit-tested helpers in ticket_binding.py.
14 new tests (TicketPromptHelperTests + AutoBindTests). Confirmed the
negative-test rule: on the pre-change tree these all ERROR
(AttributeError: no cmd_auto_bind) -- exactly the "TK REQUIRED forever" gap
this closes. Full suite 128/128 after the change (was 114/114 baseline).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V29xaoBwcFwJyR4KC43ptn
5933ea1 · 2026-09-24 · Narrow the CHILD_SESSION paint guard so a bridge session that owns its own tty can paint (TK-12167)
owner_for_paint() refused ANY caller carrying CLAUDE_CODE_CHILD_SESSION
unconditionally -- correct for a true Agent-tool subagent (TK-11791's
same-PID mirror bug) but also caught a Remote Control (bridge) session
that genuinely owns its own pane, since that flag is carried in both
cases. Live evidence: pid 33946 on ttys003 resolved owner ==
{tty: ttys003, pid: 33946} (itself) via current_owner(), yet every
dot-script and `ticket` bind was refused for an hour, leaving the tab
on a stale ticket label.
Fix: add _owns_tty_directly(), which reuses the existing
ancestors()/current_owner() machinery to verify the resolved owner is
our own closest runtime ancestor with no second claude/codex process
in between (always true today once current_owner() succeeds -- kept
explicit for defense-in-depth). Rescue a CHILD_SESSION-tagged paint
only when that check passes AND CLAUDE_CODE_BRIDGE_SESSION_ID is also
present -- a marker set only for a genuine bridge connection, never a
plain internal subagent call, so TK-11791's existing "subagent
REFUSES" fixture (byte-identical ancestry, no bridge id) is unchanged.
Undeterminable ownership still fails closed regardless of the bridge
marker.
Tests: extended both `selftest` and test_terminal_status.py with the
three required cases -- (a) CHILD_SESSION + bridge id + direct
ownership -> paints (confirmed this fails against the pre-fix guard
via a scratch copy), (b) CHILD_SESSION + bridge id + a genuine
intermediate claude/codex process -> still refused, (c) undeterminable
ownership -> still refused. Full selftest 14/14 and the unittest suite
(test_terminal_status.py + test_stop_verdict.py +
test_tk_required_fp_metric.py, 114 tests) all green.
Live-verified from the actual affected bridge session (pid 33946,
ttys003): `current --paintable` now exits 0, `greendot.sh` paints
successfully, and `terminal_status.py ticket 12167` binds directly
with no --tty/--force workaround.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V29xaoBwcFwJyR4KC43ptn
199e156 · 2026-09-20 · Fix --all broadcast footgun when label precedes --all flag (TK-11936)
Before a1139e9 forwarded $@ to the engine, mode=${1:-} was the only --all
check. With $@ forwarding, a call like 'purpledot.sh "label" --all claude'
put the label in $mode (no --all match), fell to *), and forwarded --all raw
to the engine — which broadcast the caller label to every live session.
Fix: scan all remaining args for --all before the paint() dispatch so the
--all branch always fires regardless of argument order.
c1b8135 · 2026-09-18 · 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)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE
f3313d0 · 2026-09-18 · 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)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE
b5b2887 · 2026-09-18 · test_stop_verdict: use the sv alias in the null-text regression test
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE
24de799 · 2026-09-18 · 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)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE
ec7cc19 · 2026-09-18 · integrations: sync dot-floor.sh with the live TK-11921 Stop hook (install.py deploys this copy)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE
f5399f8 · 2026-09-18 · stop_verdict: mechanical done/waiting dot verdict for the Stop hook (TK-11921)
Pure decision module + CLI: reads the Stop-hook stdin JSON, tails the
transcript, prints ONE JSON verdict (never paints). 36 fixtures, 34 unit
tests, --selftest, DOT_FLOOR_FAULT negative seam.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YT3be6iadPzjKJDiaTasEE
9e5ef9e · 2026-09-17 · TK-11831: single-flight the iTerm enumeration so a cold herd collapses to ONE osascript
The 30s cross-process cache (TK-11879) collapses REPEAT enumerations but a
SIMULTANEOUS cold herd — ~59 sessions starting at once, empty cache — all miss
together and each fires its own osascript into iTerm's ONE serial AppleScript
queue. That is the exact stampede this ticket is about (628 'osascript rc1'
blinds in a 7d window, terminal_api unavailable x57 at the 91s peak); the TTL
cache alone moves repeat-frequency, not the cold-herd tail (codex-check flag).
Fix: a NON-BLOCKING cross-process flock so exactly ONE caller enumerates. A peer
that fails LOCK_NB waits briefly (default 6s) for the winner to publish its fresh
map (the coalesced path) and, only if the winner is still enumerating past the
wait, falls through to the SAME stale-cache/blind path an osascript timeout
already takes — so a peer NEVER fires a second osascript and NEVER manufactures a
fresh answer it did not measure. Fail-OPEN: any lock error degrades to the prior
direct-enumerate behaviour. Kill-switch TERMINAL_STATUS_NO_SINGLEFLIGHT=1.
Verified: 8 concurrent cold scans -> exactly 1 osascript, 7 coalesced (8->1; at
59 sessions, 59->1). Tests 56->58: coalesce fires zero osascript and returns the
winner's map; peer-holds-lock-with-no-map blinds without a second osascript.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmvM611X4duoLdp1cFmpy2
2a0da77 · 2026-09-17 · iterm_sessions: cross-process cache collapses the enum herd + blind falls back to cache, not green (TK-11879)
~49 sessions each shelled an iTerm2 window/tab/session osascript at start;
iTerm's single AppleEvent handler serialized them under load and later ones
returned rc 1 (628 'osascript returncode 1' blinds/7d). A blind returned {}
which made the start path paint a confidently-wrong GREEN instead of restoring
a semantic dot.
Mirror the proven ps disk-cache (TK-11398/11831): a short-TTL (30s) cross-process
JSON cache of the {tty:title} map so only the first sibling enumerates and the
rest read the file. On a blind, fall back to a recently-cached map (<=300s) as
'stale' instead of {}; only when NO cache exists record enum_blind + return
unavailable (honest NOT-MEASURED). start restore gate accepts a fresh 'cached'
map. Never cache an empty rc-0 enumeration (would poison siblings silently).
+7 tests incl. the negative fault-injection (blind with no cache still records
enum_blind). All 56 pass. Live-verified: 1 enumerate then cache HIT.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwp6QVPb7cuafHPP7uLyi8
a1d75ad · 2026-09-17 · backfill: honest heartbeat verdict (kill the hardcoded-PASS false green)
TK-11870. The backfill supervisor's heartbeat wrote verdict:PASS unconditionally.
Two false-green shapes it masked (CLAUDE.md TK-11431): (1) a ps-timeout -- the exact
failure this ticket is about -- raised inside processes(fresh=True) and wrote NO
heartbeat at all, a silence byte-identical to an idle box; (2) a partial/degraded
scan wrote PASS with a shrunken live_owners and no cross-check.
Fix: extract a pure, testable backfill_heartbeat() whose verdict is DERIVED from what
the cycle measured -- WARN + NOT-MEASURED (live_owners:null) when the scan failed,
WARN when live tabs exist but none could be asserted this pass, PASS only when it ran
and could act; live_owners always carried so a measured 0-of-0 stays distinguishable
from 0-of-N. The handler now catches the scan StatusError, writes the honest heartbeat
(fresh mtime so who-watches-the-watcher liveness still passes, failure in the verdict),
and stays fail-loud (stderr + exit 1). Ships 4 negative selftests proving it reddens on
the injected faults and greens only when it acted (selftest 11/11, unittest 49/49).
Also: gitignore + untrack data/backfill-latest.json (it rewrote every 20s, thrashing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RaaqshnC3QW5DfzotaMZfM
9a4cc58 · 2026-09-16 · backfill: write a bounded liveness heartbeat after the paint
Heartbeat data/backfill-latest.json (verdict PASS + counts) written AFTER the
side effect so its mtime proves the sole producer actually fired, not just that
launchd reports it running. Lets the job run --quiet (bounded disk) while
staying observable. TK-11870.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hTBDZLzS7vZzHrjZwVfVd
3790923 · 2026-09-16 · backfill: always-on dot self-heal (repaint dropped dots + green-floor recordless live tabs)
TK-11870. Under host overload (~49 sessions each scanning ps -ax) the owner
scan times out >90s, repaint no-ops, and a tab silently loses its dot -- 68%
of tabs were dark. Adds a 'backfill' subcommand + Store.backfill() that a
scheduled external supervisor runs to (a) warm the cross-process disk cache
with ONE fresh scan so interactive sessions read it instead of each racing
their own ps -ax (collapses the scan herd), and (b) re-assert every live tab's
dot: repaint valid records (never mutated), green-floor owner_changed/missing
live owners (same rule as the start command), skip invalid/busy. Iterates
owners(rows) so it paints each owner's OWN tty from its OWN record -- never
guesses a tty from a label map. Ships negative tests (selftest 7/7): valid
gated dot is never flattened to green; recordless owner is floored.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hTBDZLzS7vZzHrjZwVfVd
c6b317d · 2026-09-16 · TK-11831: harden the ps-enrichment cache against the codex-check findings
A second-model adversarial pass (gpt-5.3-codex, per the standing codex-check
rule) found real defects in 28cf4f6 -- two of which produce a WRONG ticket
binding rather than merely a missed cache, i.e. the confidently-wrong-dot
class. Fixed, each with a test that is mutation-verified to go RED:
1. STICKY NEGATIVE CACHE. A pid absent from the ps output (it exited mid-call,
or ps was truncated) was cached as "" and then never re-read, so ONE
transient miss left that tab unbound for the whole TTL. The uncached path
retried on the very next paint -- a cache must never be WORSE than what it
replaces. Now only a CONCLUSIVE read is cached: for argv the pid must have
come back with a non-empty args; for env the pid must be present AND its
args prefix must match (so the remainder really is the environment). A
settled pid with genuinely no TERM_SESSION_ID is a real answer and is still
cached, so those pids stop re-ps'ing and the win is kept.
2. POISON OVERWRITE / LOST UPDATE. ~68 processes read-modify-write this file
with no lock. os.replace gives an untearable READ but does not serialise
RMW, so a slow writer merging onto its START-OF-PASS snapshot could drop --
or blank -- an entry a peer had already learned. Now the merge re-reads the
file at write time and NEVER downgrades: an empty value cannot overwrite a
non-empty one. Values are immutable per process, so two writers can only
ever agree; that makes concurrent writes commutative and lock-free without
a CAS, because no writer can lose information.
3. UNVALIDATED CACHE VALUES. The uncached path can only ever put a str into
the argv map, so a hand-edited {"a": null} served as a hit was a cache
answer DIFFERING from the uncached one. Values are now type-checked on read
and anything unexpected is simply not a hit.
4. KEY now includes runtime as well as pid+start, so a pid that exec'd into a
different runtime misses instead of serving the previous image's argv.
5. Cache file written 0600 (it holds argv snippets + TERM_SESSION_ID), and the
TTL comment corrected: a steady all-hit pass writes nothing and therefore
does NOT refresh `t`. The TTL is a hygiene bound, not a correctness one --
identity is pinned by the key, and expiry costs ONE batched ps over the
missing pids, not one per session.
TESTS: 49 pass (was 45; +4). All SEVEN mutations go RED on throwaway copies --
M1 pid-reuse key, M2 cache bypassed, M3 lossy env, M4 sticky negative,
M5 poison overwrite, M6 stale-snapshot merge, M7 unvalidated values. Control
GREEN. A positive-only test on a cache proves nothing.
A/B re-measured after hardening, alternating under live load 44-62 (n=6):
ORIG mean 1.79s worst 3.74s -> NEW mean 0.64s worst 0.91s
bindings diverged on 0/6 rows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BD2ty1QgqWc2QVUVtYPRQ
28cf4f6 · 2026-09-16 · TK-11831: cache discover()'s two ps enrichment calls (the per-turn ps storm)
discover() shelled out to `ps -p <all live pids> -o args=` AND `ps eww` on
EVERY self-paint -- i.e. every UserPromptSubmit and every Stop hook -- across
~68 concurrent sessions. Measured 2.46s per paint at 18 live owners and ~4.4s
under overnight load: the `ps` storm the watchtower attributed 245% aggregate
CPU to, above node (112%) and iTerm2 (57%) at the 09:46 peak.
TK-11835 already removed this from the CROSS-TTY path via discover's
argv={}/env={} skip-seam but deliberately left the SELF path alone, and that
was right: measured here, full enrichment binds 13 tickets and the skip path
binds 0. Skipping would silently unbind every tab's ticket. So the fix is to
CACHE, not to skip.
Both values are IMMUTABLE for the life of a process -- argv is fixed at exec
and `ps eww` reports the env the process started with -- so a cache keyed on
pid AND start time is exact rather than approximate, and the start time makes
it safe against pid reuse (a recycled pid misses and is re-read). Only pids
MISSING from the cache are ps'd, so a steady fleet makes ZERO enrichment ps
calls and a newly-spawned session ps's a one-entry pid list, not all ~68.
Shared via an atomically-replaced /tmp file, so 68 sessions share one answer.
Fails OPEN at every step: any cache fault degrades to the uncached behaviour,
exactly as a slow ps already degrades the label but never the paint.
MEASURED A/B against HEAD, alternating under live load 78-101 (n=8):
ORIG mean 3.08s worst 9.92s -> NEW mean 0.68s worst 1.00s
Bindings IDENTICAL on every iteration (23==23) cold and warm -- a pure caching
win, not a behaviour change. The tail is now bounded because there is no ps
call left to spiral (an earlier ORIG sample hit 40.9s).
TESTS: 45 pass (was 41; +4). MUTATION-VERIFIED per CLAUDE.md TK-11431
amendment 3 -- all three injected faults go RED on throwaway copies:
M1 drop start-time from the key (pid-reuse hole) -> RED
M2 cache read always empty (caching bypassed) -> RED
M3 never store the TERM_SESSION_ID (lossy cache) -> RED
Control GREEN. A positive-only test on a cache proves nothing.
Reversible: git revert, or TERMINAL_STATUS_PS_CACHE_TTL=-1 to disable hits.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BD2ty1QgqWc2QVUVtYPRQ
c3c5843 · 2026-09-16 · dot.sh: auto-force retry so bridge/sub sessions paint their dot
A leaked-flag real session (CLAUDE_CODE_CHILD_SESSION set by the iTerm env-leak)
was wrongly refused by the subagent-paint guard, forcing a manual --force. Now
the shared wrapper retries once with the force opt-in on a bridge refusal, gated
so it never recurses, never fires on a timeout, and only in a child-session
context; the engine's own headless/nested refusal + selftest (4/4) are untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bBRRWJJoCpD1BZK24wvUZ
f18d087 · 2026-09-16 · TK-11835: fail-fast cross-tty paint — a busy --tty target is skipped, not hung
The --tty paint (external supervisor / dot sweeps) could hang 30-65s on a
BUSY target claude session. Measured root cause on Mac2 under overnight load:
1. tickets.discover() runs TWO ps calls per invocation (a `ps -p` argv read
AND a `ps eww` env dump over every live session) purely to enrich the tab's
ticket LABEL — ~4.4s under load, on EVERY paint before command dispatch, so
N concurrent sweepers saturate the process table into the ~30s ps spiral.
2. Store.lock()'s 65s deadline is a SELF-writer backstop; a cross-tty sweeper
inherited it and waited up to 65s while a busy target held its own per-tty
lock during its own slow scan.
Fix, both scoped to the cross-tty / paint-legacy path only (a session painting
its OWN tab is byte-identical to before):
- Skip discover's ps enrichment via its existing argv/env seam ({} = already
resolved). The primary events.jsonl binding + known_tickets validation still
load fully. Cross-tty startup 3.06s -> 0.67s here (bigger under load).
- Thread the already-resolved rows into set/set_variant/repaint -> lock ->
assert_owner so the in-lock owner check reuses main's table (no re-scan), and
add a short env-overridable lock budget (TERMINAL_STATUS_TTY_LOCK_WAIT, 2.5s)
so a busy target is reported+skipped fast instead of waited on 65s.
Worst-case cross-tty paint is now bounded to ~3s vs 30-65s. Ships a negative
test (test_busy_target_skips_fast_with_short_lock_wait) proving the skip fires
within budget; mutation-verified it goes RED when the wait override is ignored.
41 tests pass, selftest 4/4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QwE2Bg691aQJjEG3XS9LLw
06cd6c5 · 2026-09-16 · dot.sh: check-for-color — hard timeout + report result, never a silent hang/no-op
The engine's ps-scan can run to its ~90s ceiling under host load (appeared as a
hang) and a tool/subagent-shell paint exits nonzero (silent no-op). dot.sh now
runs every paint under a hard outer timeout (TS_PAINT_TIMEOUT, default 100s) and
CHECKS the result, printing one clear line (painted/cleared/TIMED OUT/NOT painted)
so a tab never silently loses its color. Exit code preserved; subagent-paint guard
in terminal_status.py intentionally untouched (selftest still 4/4).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5463758 · 2026-09-16 · TK-11826 (TK-11779 Finding 2): pin ticket_update=None in T1-c preserve block + coherence regression test
The T1-c lightblue preserve branch already cannot mismatch (old label + new
ticket) today — its 'not caller_gave_reason' guard (e0a025a) guarantees
ticket_update is None on that path. Land the credential-desk memo's fix
defensively: pin ticket_update=None inside the block so the preserved reason
stays coherent as a whole (colour+label+ticket) even if a future edit loosens
the branch guard. Add test_lightblue_never_mismatches_old_label_with_new_ticket
to lock the invariant permanently.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
a1139e9 · 2026-09-16 · dot.sh: forward all set args so --tty/--force reach the engine
The compat shim passed only ${1} as the label, silently dropping engine
flags. That blocked painting a verified tty when CLAUDE_CODE_CHILD_SESSION
is leaked into a real interactive session (the iTerm-relaunch env-leak),
forcing a direct engine call. Forward "$@" on the default set branch so
every singular color skill (green/orange/purple/…) can target a tty.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ufyow1KDpDysm133tajFbh
ea41eb3 · 2026-09-15 · TK-11794: green 'waiting' variant — brighter static green (0,255,120) for an idle/waiting green session
A green-only tint parallel to monitoring (teal): distinguishes an idle/waiting
green session from an actively-working one WITHOUT pulsing — green stays SOLID
per Steve's dot directive. Guarded at set-time (dropped on any non-green base),
accepted by load()/set()/set-variant validation, wired to CLI --waiting +
set-variant waiting. Variant precedence: stopped > monitoring > waiting. Render-
only rgb; stored title recomputes byte-identically so no record goes invalid.
39/39 tests pass incl test_waiting_shade_is_brighter_green_and_survives_repaint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKMY3eXqwFUCfqDShfUVnH
585ee06 · 2026-09-15 · TK-11794: pulse the 4 needs-Steve dot states; green/pink stay solid
Render-time only: the waiting states (yellow/purple/orange/lightblue) carry a
two-frame attention marker (revision-keyed) in the tab badge; green/pink render
unchanged. Marker is NOT in the stored/validated title, so load() recomputes
byte-identically and no existing record goes invalid. Zero background loops,
zero new subprocess/scan cost. The real animated pulse lives in desktop-dotbar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169ph94FSR3sVnot1p5pNX1
e0a025a · 2026-09-15 · terminal-status: keep lightblue label+ticket consistent (TK-11779) - bare set(lightblue) preserves both old label+ticket, explicit reason takes both new; +tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pnon7dut9MfPekhhm9YY4Q
df6f28c · 2026-09-15 · T2 (TK-11779): add dead-tty .dot/.json reaper (ps-liveness authority, tar-backed --apply)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuGCNYd1owT7PaPwi9t9sQ
1286106 · 2026-09-15 · terminal-status: retry transient EAGAIN in write_terminal so a busy tty can't latch a 24h health FAIL
write_terminal opened the tty O_NONBLOCK then did a single os.write with no EAGAIN
handling, so a momentarily-full tty buffer raised BlockingIOError (Errno 35) that
escaped to the top handler; dot-floor.sh then latched the fleet-health panel FAIL
for 24h off a ~1.4% transient. Now: bounded 0.5s retry loop handling EAGAIN + partial
writes, raising the already-benign 'Terminal busy; retry' only on a genuine >0.5s wedge.
Verified: py_compile OK + 3 behavioral tests (real-pty positive, 2xEAGAIN+partial, wedged=0.51s bounded).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A5jmQ4a4VhZNPTRkVUSeML
9731e7f · 2026-09-14 · TK-11666: measure the TK-REQUIRED false-positive rate (the missing denominator)
Add a READ-ONLY metric answering the question nothing computed — "of the live
GREEN sessions labelled TK REQUIRED, how many are PROVABLY TRACKED (false
positives)?" — the missing correctness metric that let the pattern recur 3x/day
and spawned ~24 spurious tickets.
Three honest buckets (TK-11431 rule 1): bound / not_measured (never green, never
an accusation) / contradicted (the false positive to drive to 0). Carries
population beside observed ("0 of 0" != "0 of 60"), counts LIVE sessions via
allcolordots --json (never the stale tab-dots dir), keys on PID not tty.
Independence: the LABEL is the persisted dot state (what Steve sees) while the
resolvability probe re-scans the RAW ledger (claude@TERM_SESSION_ID work events)
and RAW argv itself — it does NOT call the binder it grades, so a binder gap
that leaves a tracked session reading TK REQUIRED surfaces as contradicted
instead of being laundered into agreement. Coupled to TK-11665: as that binder
improves, contradicted → 0.
Launch-latency guard: a tracked-but-young session is `pending`, not
contradicted; an UNKNOWN age is NEVER excused as young (fail-safe toward
detecting the false positive). Age uses macOS `ps lstart` (there is no etimes).
Verdict: FAIL contradicted>0 · WARN input-unobservable/zero-sessions (never a
green PASS) · PASS >=1 session and contradicted==0. Ships a negative test
(--self-test + 13 unit tests) proven to go RED on an injected contradicted row;
the test seam is the pure classify() function, so a scheduled run never measures
a fixture. Emits verdict+status (canonical PASS/WARN/FAIL) to the skill's
data/latest.json for fleet-health-rollup (no rollup vocabulary change needed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVgEj4WxSqB62ieGpgrixR
4187d75 · 2026-09-14 · TK-11665: bind a session's ticket via ledger agent claude@TERM_SESSION_ID
Add a 4th, last-resort binding source to ticket_binding.discover(): when no
MCP-correlation and no argv ticket bind a live session, resolve the session's
TERM_SESSION_ID from its process ENV (keyed PID->TSID, never tty) and bind the
most recent assign/create/action/comment event logged under claude@<TSID> (the
tk CLI's fallback identity when a session sets no TK_AGENT).
No epoch floor: TERM_SESSION_ID is a stable session identity across a --continue
resume in the same pane, so events predating this process's start are that
session's own prior work -- this is the fix for the resumed 'claude --continue'
class that permanently read "TK REQUIRED". Same `in known` safety as every other
source (never invents a ticket); never overrides a bound argv source; cannot
reach a self-chosen TK_AGENT (the honest KNOWN CEILING). Reads env via `ps eww`
minus the `-o args=` prefix to avoid the args+env concatenation trap.
Ships a positive test (resumed session, pre-start events bind) and a non-vacuous
negative test (read-noise / unknown ticket / no TSID / argv-present-no-override
all correctly abstain) -- proven to go RED on an injected fault.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVgEj4WxSqB62ieGpgrixR
664d1de · 2026-09-13 · TK-11672: fix permanently-red terminal-status test via injectable rescan seam
current_owner()'s TK-11398 fresh-rescan escape re-read the LIVE process
table (processes(fresh=True)) instead of the passed-in rows fixture, so
run inside a live claude session it resolved a real owner and the
headless-nested-agent refusal never fired -> test_headless_nested_agent_
cannot_paint_parent was a standing RED, making the 29-test suite unusable
as a gate for the 20 consumer scripts on this engine.
Add an injectable refetch callable (default processes, so production is
byte-identical) threaded through both rescan retries; the test injects
the fixture as the rescan source. Suite now 29/29; negative test proves
SEAM-ON raises, SEAM-OFF returns a live owner (the original bug).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVgEj4WxSqB62ieGpgrixR
b85d85b · 2026-09-13 · require `export ` + a single declaration before binding a TK_AGENT (TK-11631)
Follow-up to 2315808, which was too loose in two ways the contrarian caught:
* it matched a bare `TK_AGENT=` ANYWHERE in argv, so a prose mention bound a
ticket: "fix the bug where TK_AGENT=claude-run-11630 fails to bind" bound
TK-11630 with no export and no launch;
* it took re.search's FIRST hit with no uniqueness test, making it LESS
conservative than the len(ids)==1 rule it front-runs. The argv of an agent
working this very ticket carries three mentions and bound the right one only
by accident of sentence order -- reverse the sentences and it binds another
session's ticket. That is the self-match trap in the memory note
detector-argv-substring-self-match, in the one place it hurts most.
The match now requires BOTH the launcher's literal shape (`export ` prefix and
the trailing period, verified at run-ticket.sh:60 and against 44 of 44 live
sessions) and exactly ONE DISTINCT declared id via findall. Two declarations
abstain to the old rule rather than picking one. The in-known guard is unchanged.
`ps -o args=` flattens argv, so the prompt argument's own start is not locatable;
this literal is the anchor instead of a positional hack.
Controlled live measurement, discover() run three times over ONE shared rows +
argv snapshot: AGENT off = 41 bound, loose = 44, tight = 44; rescues 3, rebound
0, lost 0 in both directions. The tightening removes false-bind CAPABILITY and
changes nothing on today's population -- no live session carries a bad shape.
Each half is proven load-bearing by an injected fault: dropping `export `,
dropping the period, or dropping uniqueness each reddens the new test; dropping
the in-known guard reddens the anti-invention test; removing the source reddens
the positive test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011yMsG8b5h9zymuwmtabAMJ
2315808 · 2026-09-13 · bind a session by its launcher-declared TK_AGENT, so a slug carrying two TK ids no longer reads TK REQUIRED (TK-11631)
run-ticket.sh bakes `export TK_AGENT=<prefix>-<IDNUM>` into the launch prompt and
IDNUM is the DRIVING ticket's number alone. The pre-existing argv rule scans for bare
TK ids and binds only when it finds exactly one, so a session driving a ticket whose
slug embeds a referenced ticket (TK-11630-tk-11340-...) yields two ids, abstains, and
is mislabelled TK REQUIRED for its whole life while logging real work on the board.
54 of 1783 tickets carry that slug shape.
New source main_process_agent is tried first, still gated on the known-ticket set, and
does not change the single-id rule it falls through to. Measured live on the same
process table: 40 bound -> 42 bound, 0 lost, 0 rebound to a different id.
Tests ship with the injected-fault proof required by CLAUDE.md TK-11431 amendment 3:
removing the new source reddens the positive test, removing the known-set guard or
guessing an id reddens the anti-invention test, and a greedy capture that swallows the
prompt's trailing period reddens the positive test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011yMsG8b5h9zymuwmtabAMJ
4e63168 · 2026-09-13 · stopped marker must not reuse the lightblue base codepoint
STOPPED_MARK was U+1F535 -- byte-identical to the lightblue base colour. Since
status_title() flattens state + variant into one string, 'state=lightblue' and
'state=purple, variant=stopped' rendered IDENTICALLY. No string-level reader, and
no human eye on the tab bar, could tell 'this tab is generically needs-Steve' from
'this gated tab has gone idle'. Two already-distinct canonical fields were being
collapsed onto one glyph, and the consumer disagreements chased this week all
traced back to it.
Now U+1F6D1 (stop sign): different SHAPE (octagon vs circle) and different hue (red
is unused in the palette), so the second axis rides a second visual channel instead
of overloading colour.
Safe by construction: STOPPED_MARK has exactly one render site and nothing parses
it, so no reader can break. color_of() matches title.startswith(base dot) and is
unaffected. Consumers that classify read canonical state/variant; the few that still
grep a glyph only OVER-report during the repaint window, never under-report.
Side benefit: lightblue+stopped rendered a doubled 🔵🔵 and now reads 🔵🛑.
Tests: 26 run, 1 failure -- test_headless_nested_agent_cannot_paint_parent, which
fails identically BEFORE this change. No new failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbtgv1MuWftpd3hfWEiZYG
ce04960 · 2026-09-11 · terminal-status: cross-process proc-table cache + negative-rescan valve (TK-11398)
Paints were taking ~100s and often hitting the 60s ps ceiling and silently
no-op'ing, leaving tabs showing a PREVIOUS session's dot (observed: a tab
advertising a finished, unrelated ticket while working another) -- the
false-green class, since the wall looks authoritative while being wrong.
Root cause is a thundering herd, not a constant cost. _PROC_CACHE is
per-PROCESS and every dot call is a fresh python3, so the cache was dead on
arrival and all ~42 live sessions each ran their own full `ps -ax`. Measured:
three consecutive scans cost 2.30s / 4.90s / 7.08s -- the scans mutually slow
each other, so the herd is self-amplifying.
DOCUMENTED DECISION REVERSAL: the in-code scope note said a cross-process file
cache was "not worth the risk" because a stale table could miss a just-started
session and wrongly report "no owning terminal". Reversed deliberately, by
claude-run-11255, with Steve's explicit approval, on the new measured fact
above. The objection is preserved rather than traded away: a cached table is
only ever trusted for a POSITIVE answer -- every negative ownership conclusion
(current_owner finding no owning runtime; assert_owner about to refuse a stale
writer) re-scans fresh and re-decides before it is allowed to fail. A negative
can cost one extra scan; it can never produce a wrong refusal.
_splice_self_chain() is what makes the cache actually pay off: a cached table
predates the current process, so ancestors(os.getpid()) dead-ended and forced
the valve on EVERY call, cancelling the whole benefit (observed directly via
the new debug seam: "HIT" immediately followed by "MISS -> scan (fresh=True
forced)"). It now walks our own chain with targeted single-pid ps calls
(0.01-0.10s each, O(depth) not O(2000)) and returns None rather than a partial
chain, so failure can only cost a scan.
_DISK_TTL is 30s, not 5s: a scan measures 15-70s under load, so a 5s window
expires before the next invocation can reach it -- the same trap the existing
per-PROCESS TTL note describes. Staleness is bounded by the valve, not the TTL.
TERMINAL_STATUS_DEBUG=1 prints proc-cache HIT/MISS to stderr -- a cache whose
hit rate you cannot observe is a cache you cannot prove works. Opt-in only.
Verified end-to-end, with injected faults (positive-only tests would prove
nothing about a correctness valve; each fixture asserts it is not a no-op):
- warm resolve: 17-72s -> 1.4-3.0s, single HIT, no forced rescan
- real paint: ~100s -> 4.41s, dot file verified
- fault A (70 claude/codex rows stripped from a fresh cache):
HIT -> valve -> resolves the REAL owner pid 30729
- fault B (my tty rewritten to impostor pid 999001):
HIT -> valve -> resolves REAL pid 30729, impostor rejected
- `audit` clean, no new drift
Complements 9696608 (per-process TTL 5s->120s, another session, TK-11466
followup), which fixes the intra-run double scan; this fixes the cross-session
herd. Both are needed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012LVUzeaBYQJsjSYSWtQZYy
9696608 · 2026-09-11 · terminal-status: fix paint hang — raise per-process PROC_TTL 5s→120s so a single paint scans once, not twice
The 5s cache TTL was shorter than the ~15s (up to 60s) process-table scan, so the
second intra-run processes() call always found the cache expired and re-scanned —
every paint paid the scan twice (~120s worst case), leaving a stale two-dot
transient on the tab during the slow paint. The cache is per-process (reset each
invocation), so a larger TTL only bridges the two calls within one run with zero
cross-invocation staleness. Verified: paint 120s→13.3s (single scan).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSZgKJ86hmzguEeX8iqoZi
1bc426c · 2026-09-11 · set-variant: --tty targeting + don't require caller ancestry (2026-09-11)
Lets an EXTERNAL supervisor mark another session. Two fixes, both found by forcing a
launchd run rather than trusting its exit code:
1. --tty was registered on the shared subcommand loop but NOT on set-variant, and its
dispatch resolved the caller's own tty — so it could never target another session.
2. caller = current_owner(rows) ran UNCONDITIONALLY, before --tty was read. It walks the
caller's process ancestry for a terminal; launchd has none, so every scheduled run
died on 'No owning Claude/Codex terminal in this process ancestry' while launchctl
reported
wtmp begins Mon May 25 23:28:20 PDT 2026, , and an empty stderr. A textbook false
green: loaded, exited clean, accomplished nothing. Caller resolution is now optional
when --tty names an explicit target; assert_owner() still validates that target
against the live process table, so a dead or reassigned tty is refused as before.
Verified under launchd after the fix: 4 failures -> 0, verdict WARN -> PASS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1f88b72 · 2026-09-11 · terminal_status.py: set-variant --if-blocked (only 🔵-mark needs-Steve stops)
Gates the additive 🔵 stopped-marker to blocked base colours (yellow/purple/orange)
via a cheap record read — so the Stop hook marks only stops that actually need
Steve, not green/pink stops. Verified: purple→🟣🔵, green→no-op.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
43fe598 · 2026-09-11 · terminal_status.py: fix set-variant — was calling nonexistent Owner.detect()/STORE
The set-variant branch shipped calling Owner.detect() (no such method) and STORE
(undefined), so it crashed with AttributeError in EVERY session and the additive
🔵 stopped-marker never worked. Now uses the already-resolved caller + the real
store instance, matching every other command. Verified: set-variant stopped paints
base+🔵 (e.g. 🟣🔵), set-variant '' clears it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
c40a4b0 · 2026-09-11 · terminal-status: add lightblue to PRIORITY (unblock scan crash) + default-sort unknown colors last
The lightbluedot skill (2026-09-11) added 'lightblue' to COLORS but not the
PRIORITY tuple, so scan() crashed KeyError:'lightblue' whenever any tab was
light-blue -- taking down allcolordots and every dot sweep. lightblue ranks
above the three per its own spec. Also PRIORITY.get(...,last) so the next new
color can't crash the scanner (TK-11431 consumer-update lesson).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XGhtD1r4okMDSB33iiEci2
b37dbc2 · 2026-09-11 · stopped variant: keep the base colour dot, put 🔵 NEXT TO it (2026-09-11)
Steve: 'keep color dot and place blue dot next to it when stopped … when running
revert back to original color dot with that dot next to the orig color dot flashing.'
Additive marker, never a replacement: 🟣 -> 🟣🔵 stopped -> 🟣 running.
Three defects caught while wiring it, each of which would have shipped a silent no-op:
1. set() discarded the variant for any non-green colour ('variant if color == "green"'),
so --stopped on purple/yellow/orange — the blocked colours this exists for — would
have been accepted, reported success, and vanished. stopped is now colour-agnostic;
monitoring stays green-only because it is the teal TINT of green.
2. All three status_title() call sites built the title WITHOUT the variant, so even a
stored variant would not render — and the canonical check at load() compares title
to expected, so the record would then be judged invalid and wiped.
3. set_variant() first drafted against self.write()/self.painter() as methods; write()
does not exist and the real path also updates the legacy .dot mirror, which is what
allcolordots and dot-screen-router actually read. Updating only the JSON would have
left every consumer showing the old title while the engine reported success.
Verified with negative tests: purple+stopped renders 🟣🔵 and reverts clean; a bogus
variant raises; monitoring on a non-green colour is dropped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a56bd8c · 2026-09-11 · colors: add lightblue = ANY STOP THAT REQUIRES STEVE'S INPUT
Steve, 2026-09-11: 'It should mean. Any stop that requires my input on any terminal.'
Adds the umbrella scan colour 🔵 (0,176,240) 'NEEDS STEVE'. Deliberately overlaps
yellow/DIRECTION?, purple/GATED and orange/PASTE — Steve chose to KEEP all three, so
they still carry what kind of stop it is; lightblue is the single colour to scan when
you only want 'has this stopped and does it need me?'.
set_status() rejects any colour not in COLORS, so this entry is what makes 🔵 legal.
Consumers were updated in the SAME change (allcolordots, dot-screen-router) per the
standing rule that a producer must never emit a word its consumer cannot read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6eae7e8 · 2026-09-10 · lock: stop holding the per-tty lock across the 14.84s process-table scan
Measured on this box: processes() (the `ps -axo` scan) = 14.84s, owners() over
those rows = 0.00s, and Store.lock()'s wait deadline was 3s. Store.assert_owner
ran the scan INSIDE the lock, so a same-tty writer could never win: 14.84 > 3,
always. That is the mechanism behind the "Terminal status is busy; retry"
failures another session hit live twice today.
63f3fae (raising the ps timeout 8s -> 60s to survive a busy box) made this
worse by raising the ceiling on lock-hold time from 8s to 60s -- the thundering
herd an earlier review warned about, now with a number attached.
Three complementary fixes:
1. Short-TTL cache on processes(). One CLI run scans at least twice (resolving
the caller, then again inside the lock); this removes the second. Per-PROCESS
only -- it deliberately does NOT cache across the ~49 sessions, because a
table a few seconds stale could miss a just-started session and wrongly
report "no owning terminal". TERMINAL_STATUS_PROC_TTL overrides (default 5s).
2. assert_owner(owner, rows=None) accepts an already-resolved table, threaded
through lock(owner, rows=None), so the guard is kept but the scan leaves the
critical section.
3. Lock wait deadline 3s -> 65s (TERMINAL_STATUS_LOCK_WAIT). With 1+2 the lock
is held ~0s, so this is a backstop that should never be reached; it is not
licence to hold the lock across expensive work.
Measured after: in-lock scan 7.14s -> 0.0000s.
Verified: repaint exit 0; status exit 0; dot intact; hook 3x exit 0 with zero
new failure records and verdict PASS; and the ownership guard still correctly
refuses a stale writer (a forged owner is rejected with "Terminal owner
changed"), so the safety property is preserved, not traded away.
Reversible: git revert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXNMS1TvMiVbE3ckhSaLeT
fea7539 · 2026-09-10 · ticket_binding: a slow ps must degrade the label, not kill the paint
Surfaced by the logging added in 29475ed: within minutes of the dot-floor hook
finally recording real failures, repaint was failing every 1-3 minutes on this
box with `Command '['ps', '-p', <~49 pids>]' timed out after 8 seconds`.
This call is a THIRD process-table read, in ticket_binding.py rather than
terminal_status.py, which is why an audit of the engine missed it. With ~49
live sessions the pid list is large and 8s was not survivable at load 53.78.
Worse than the timeout was the severity: it raised, which killed the ENTIRE
repaint, so no dot was painted at all. The block's own comment calls it "a
conservative fallback" for the ticket LABEL -- optional enrichment. An optional
enrichment must never take down the paint.
Raised to 60s to match the sibling process-table read, and made non-fatal: on
timeout the label is skipped, the blind spot is recorded, and the dot still
paints. Verified 5/5 consecutive clean hook runs with zero new failures logged.
Extends DTD 2026-09-10 verdict C to the same defect class in a third location.
Reversible: git revert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXNMS1TvMiVbE3ckhSaLeT
29475ed · 2026-09-10 · osascript enumeration: raise timeout to 30s and record blind spots
The iTerm session enumeration ran osascript with timeout=8 and swallowed
failure as `return {}, "unavailable"` -- no exception, no exit code. That is
the false-green class: on a box that hit load 53.78 today (with the sibling
ps call measured at 20.4s), AppleScript enumeration cost scales with tab
count (~49 here), so an 8s ceiling was trippable and its failure was
indistinguishable from "no sessions".
It matters because `start` treats "unavailable" as an empty result and falls
through to painting green instead of restoring a sticky semantic dot -- a
confidently-wrong paint, worse than a missing one.
Raised to 30s, deliberately NOT the 60s used for the ps call: this runs at
session start, where a longer hang would delay startup.
Blind spots now append to the terminal-status-health log via a fail-safe
recorder that never raises and never blocks painting.
DTD 2026-09-10 verdict C (4/5). Reversible: git revert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXNMS1TvMiVbE3ckhSaLeT
e6a426b · 2026-09-10 · set: automatic paints yield to sticky semantic dots (orange/purple/yellow)
A UserPromptSubmit hook fires `set green WORKING` on EVERY prompt, and green
overwrote whatever dot the tab held. So typing into a tab silently erased its
purple (gated memo), orange (paste waiting) or yellow (question waiting) and
replaced it with green = 'nothing needed'. Observed twice in one session.
PRIORITY (orange > purple > yellow > green > pink > none) already existed at
line 31 but was only ever used to sort scan() output, never to arbitrate a
paint. This makes set() consult it: a caller passing --deferential will not
overwrite a state ranked above green; it repaints the existing record and
returns it unchanged (no revision bump, no mirror churn).
Only the automatic per-prompt painter passes --deferential. Every EXPLICIT
paint stays authoritative, so /greendot, /color, /pinkdot and --off can still
clear a sticky dot and a tab can still be reused for new work. The teal
monitoring variant is green-family and yields too.
Verified with an in-process truth table (11/11): sticky states survive an
automatic green; green/pink do not; explicit green/pink/none still win;
revision does not bump when the guard yields.
TK-11378, DTD verdict A (6/7). Steve approved 2026-09-10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwTfiTtWZNjLoowhjXdWV1
63f3fae · 2026-09-10 · fix: raise process-table timeout so dot painting survives a busy box
`ps -ax` costs ~4-5s idle and ~30s under load on this workstation (~2k
processes from the MCP fleet), so the 8s ceiling in processes() was
marginal at rest and failed outright whenever anything ran in parallel --
every color-dot script died with "timed out after 8 seconds" and no tab
could be painted at all. Raise to 60s and convert the bare TimeoutExpired
into an actionable StatusError. The full table is genuinely required
(owners() proves a tty has exactly ONE live runtime before agreeing to
paint it), so this is headroom, not a narrower query.
Verified: 26/26 tests pass; current.sh now resolves ttys022 in 9.2s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHKdiEfwLNDHqKgMRbBNS2
914a88d · 2026-09-09 · fix: paint guard resolves ancestry before refusing on CLAUDE_CODE_CHILD_SESSION
The env var is set on any tool-spawned shell (Bash tool, hooks), so it made
/dot no-op for the real interactive session. Now ownership is resolved by
ancestry first — a top-level claude/codex that is the live owner of its tty
paints; only a genuine headless/nested agent (or no owning tty) is refused.
All 26 tests pass incl. test_headless_nested_agent_cannot_paint_parent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzZkMwx4e9ec829p24cAp3