Problem
ADR-003 (binding) defines a single transport cascade — named pipe working/runtime/hud.pipe → HTTP localhost:7823/api/event → JSONL working/logs/events.jsonl — implemented once in bin/write-hud-state.sh. Every producer is supposed to emit through it. Three producers instead reimplement the cascade inline, and their diverged copies are silently breaking delivery:
-
shft/afk.sh writes to a dead JSONL path. _push_afk_event() (shft/afk.sh:71-88) writes the JSONL fallback to $WORKING_DIR/events.jsonl — i.e. working/events.jsonl, one directory above the canonical working/logs/events.jsonl the daemon actually polls (bin/hud-daemon.js:44, created and watched by start-hud.sh:30). With no pipe reachable, an AFK run's info events (~4 per run, lines 164/305/310) are written to a file neither the daemon nor any skill reads (all consumers read working/logs/events.jsonl — e.g. skills/error-audit/SKILL.md:101).
-
shft/afk.sh pipe writes are unconditional. The inline emitter appends to the pipe whenever [[ -p "$_pipe" ]] (line 83) — without the _can_use_pipe() daemon-alive check (write-hud-state.sh:42-48) — so a stale FIFO left by a crashed daemon blocks the backgrounded writer at the printf open, and a missing working/ at all appends to $CTRL_DIR (a read-only mount in the AFK Docker setup, per hud-daemon.js:14).
-
shft/once.sh never emits. HITL runs produce only claude stderr; nothing touches the HUD, so sessions the user is actively watching are invisible in the dashboard — a regression from detect-context.sh / hook producers.
-
detect-context.sh maintains a third inline copy (lines 140-169), already diverged: it omits the HTTP fallback tier that the cascade defines, writes directly to the pipe with no daemon-alive check, and must duplicate _LOG_DIR/_PIPE paths on every cd().
Why this drift keeps happening: subprocess origins (ctrlshft-claude, hud-session.sh, init scripts) run with HOME overridden to the consumer dir, so afk.sh's $CTRL_DIR-derived paths do not resolve to the $DOTFILES dir the daemon uses (bin/hud-daemon.js:39). The two sides have no shared path source and no test bridges them.
Architecture impact
Scope
shft/afk.sh — delete _push_afk_event(); replace the three call sites with sourcing bin/write-hud-state.sh (same pattern hooks/hud-session.sh:19 already uses) and call write_hud_event. If sourcing is unacceptable in the subprocess, shell out once per run to the script's existing CLI mode (write-hud-state.sh bridge-event reads JSON from stdin — reuse it).
shft/once.sh — source bin/write-hud-state.sh and emit info events for session start/end (&& once mode).
bin/detect-context.sh — collapse the inline block (lines 140-169) to write_hud_event "context" "Active contexts: $contexts" with the project/path overrides the function already supports; keep the ide field by extending write_hud_event with an optional ide arg (daemon already reads/stores ide).
- Canonicalize the runtime-dir source — resolve
_RUNTIME_DIR/_LOG_DIR/_PIPE/_JSONL precedence (DOTFILES first, then DOTFILES_HOME) once in write-hud-state.sh so subprocess origins resolve the same path the daemon computes; add a comment noting the fork trap so future producers reference the single source rather than re-deriving.
Acceptance criteria
test/lifecycle.sh (or a new test/hud-afk-paths.sh wired into run-all) asserts that an AFK run (or a simulated _push_afk_event replacement) delivers its events into working/logs/events.jsonl / the pipe, not working/events.jsonl.
- A test asserts
write_hud_event works when sourced from $HOME pointing at a consumer dir with DOTFILES_HOME set (the ctrlshft-claude subprocess case).
grep -rn "events.jsonl" shft once.sh over the repo finds no producer writing to a non-logs/ path.
detect-context.sh emits the same context payload shape as before (project, projectPath, contexts, ide) — verified through the daemon's handleContext regex branch — after collapsing to the shared function.
- Live check: with the daemon running,
bash shft/once.sh and one manual shft/afk.sh 1 round each surface a session/event row in the HUD (/api/state).
Non-goals
Proposed by: automated source:architecture-review pass.
Problem
ADR-003 (binding) defines a single transport cascade — named pipe
working/runtime/hud.pipe→ HTTPlocalhost:7823/api/event→ JSONLworking/logs/events.jsonl— implemented once inbin/write-hud-state.sh. Every producer is supposed to emit through it. Three producers instead reimplement the cascade inline, and their diverged copies are silently breaking delivery:shft/afk.shwrites to a dead JSONL path._push_afk_event()(shft/afk.sh:71-88) writes the JSONL fallback to$WORKING_DIR/events.jsonl— i.e.working/events.jsonl, one directory above the canonicalworking/logs/events.jsonlthe daemon actually polls (bin/hud-daemon.js:44, created and watched bystart-hud.sh:30). With no pipe reachable, an AFK run'sinfoevents (~4 per run, lines 164/305/310) are written to a file neither the daemon nor any skill reads (all consumers readworking/logs/events.jsonl— e.g.skills/error-audit/SKILL.md:101).shft/afk.shpipe writes are unconditional. The inline emitter appends to the pipe whenever[[ -p "$_pipe" ]](line 83) — without the_can_use_pipe()daemon-alive check (write-hud-state.sh:42-48) — so a stale FIFO left by a crashed daemon blocks the backgrounded writer at theprintfopen, and a missingworking/at all appends to$CTRL_DIR(a read-only mount in the AFK Docker setup, perhud-daemon.js:14).shft/once.shnever emits. HITL runs produce onlyclaudestderr; nothing touches the HUD, so sessions the user is actively watching are invisible in the dashboard — a regression fromdetect-context.sh/ hook producers.detect-context.shmaintains a third inline copy (lines 140-169), already diverged: it omits the HTTP fallback tier that the cascade defines, writes directly to the pipe with no daemon-alive check, and must duplicate_LOG_DIR/_PIPEpaths on everycd().Why this drift keeps happening: subprocess origins (
ctrlshft-claude,hud-session.sh, init scripts) run withHOMEoverridden to the consumer dir, soafk.sh's$CTRL_DIR-derived paths do not resolve to the$DOTFILESdir the daemon uses (bin/hud-daemon.js:39). The two sides have no shared path source and no test bridges them.Architecture impact
source:architecture-reviewissue owns the producer-side delivery path. This is the partner fix that makes Define a single-source HUD event schema to enforce the producer↔daemon contract across bash, Python, and JS #277's contract enforceable on the emission side, and it is a prerequisite for Persist safety-hook denials as HUD events to build the blocked-mistake audit trail (ADR-006 gap #2) #295's hook events to be observable.Scope
shft/afk.sh— delete_push_afk_event(); replace the three call sites with sourcingbin/write-hud-state.sh(same patternhooks/hud-session.sh:19already uses) and callwrite_hud_event. If sourcing is unacceptable in the subprocess, shell out once per run to the script's existing CLI mode (write-hud-state.sh bridge-eventreads JSON from stdin — reuse it).shft/once.sh— sourcebin/write-hud-state.shand emitinfoevents for session start/end (&& oncemode).bin/detect-context.sh— collapse the inline block (lines 140-169) towrite_hud_event "context" "Active contexts: $contexts"with the project/path overrides the function already supports; keep theidefield by extendingwrite_hud_eventwith an optionalidearg (daemon already reads/storeside)._RUNTIME_DIR/_LOG_DIR/_PIPE/_JSONLprecedence (DOTFILESfirst, thenDOTFILES_HOME) once inwrite-hud-state.shso subprocess origins resolve the same path the daemon computes; add a comment noting the fork trap so future producers reference the single source rather than re-deriving.Acceptance criteria
test/lifecycle.sh(or a newtest/hud-afk-paths.shwired into run-all) asserts that an AFK run (or a simulated_push_afk_eventreplacement) delivers its events intoworking/logs/events.jsonl/ the pipe, notworking/events.jsonl.write_hud_eventworks when sourced from$HOMEpointing at a consumer dir withDOTFILES_HOMEset (thectrlshft-claudesubprocess case).grep -rn "events.jsonl" shft once.shover the repo finds no producer writing to a non-logs/path.detect-context.shemits the samecontextpayload shape as before (project, projectPath, contexts, ide) — verified through the daemon'shandleContextregex branch — after collapsing to the shared function.bash shft/once.shand one manualshft/afk.sh 1round each surface a session/event row in the HUD (/api/state).Non-goals
write-hud-state.sh.bridge/*(itshud.pyalready delegates to the canonical script).Proposed by: automated
source:architecture-reviewpass.