Split out of #41 so that PR #46 can close it without deleting this work.
#46 ships the Stop hook, which is the primary mechanism: it fires at the moment a builder ends its turn mid-phase, synchronously, at zero latency. That covers the two reported incidents.
It cannot see three things, all of which look identical from outside:
- A builder whose process died. No turn ends, so no Stop hook fires. Remedy is
afx spawn <id> --resume.
- A builder wedged inside a turn that never ends — a foreground poll loop, or a wait on a producer that already died (
roles/builder.md documents a 45-minute case). The hook only runs at turn end, which never arrives.
- A builder whose pane is alive but whose harness has no Stop-hook support. The hook is emitted only for the Claude harness, via
CLAUDE_HARNESS. opencode and codex builders get nothing.
Signal
Unchanged from #41: Tower already classifies every agent pane idle-vs-busy through the render gate (gate-profiles.ts carries an idleIndicatorPattern per harness; classifyBuffer in render-gate.ts distinguishes busy from idle from drafting). That is the same machinery afx send uses to decide deliverability, so it is load-bearing and measured against committed fixtures. Porch knows the phase. Nothing joins them.
Constraints
Carried over, and the first two are hard requirements:
- Route through the mailbox, never a raw PTY write. Spec 1313 made
afx send mailbox-first for a reason. held is an acceptable outcome; force-injection is not.
- A nudge is never a gate approval. It must not read as authorization to pass a gate. Note the Stop hook's own gate predicate is
status === 'pending' && requested_at — a bare pending means "seeded at project creation, never reached", which is what made the first version of that hook a total no-op.
- Silence on the ambiguous case. No gate profile, or
no-live-pty, means the watchdog cannot tell idle from busy and must say so to the architect rather than nudging on a guess.
- Do not derive the stall from
updated_at. Spec 1470 documents why: next() writes no state on the normal path, so updated_at stays pinned for the whole of a healthy build.
- Log every nudge. A transcript with unexplained continue-messages is harder to debug, not easier.
Known cost to design around
The Stop hook nudges once per cycle, and stop_hook_active resets on each user message — so a human or architect asking a mid-phase builder a question already pays one forced extra turn. A watchdog layered on top must not compound that.
Split out of #41 so that PR #46 can close it without deleting this work.
#46 ships the Stop hook, which is the primary mechanism: it fires at the moment a builder ends its turn mid-phase, synchronously, at zero latency. That covers the two reported incidents.
It cannot see three things, all of which look identical from outside:
afx spawn <id> --resume.roles/builder.mddocuments a 45-minute case). The hook only runs at turn end, which never arrives.CLAUDE_HARNESS. opencode and codex builders get nothing.Signal
Unchanged from #41: Tower already classifies every agent pane idle-vs-busy through the render gate (
gate-profiles.tscarries anidleIndicatorPatternper harness;classifyBufferinrender-gate.tsdistinguishes busy from idle from drafting). That is the same machineryafx senduses to decide deliverability, so it is load-bearing and measured against committed fixtures. Porch knows the phase. Nothing joins them.Constraints
Carried over, and the first two are hard requirements:
afx sendmailbox-first for a reason.heldis an acceptable outcome; force-injection is not.status === 'pending' && requested_at— a barependingmeans "seeded at project creation, never reached", which is what made the first version of that hook a total no-op.no-live-pty, means the watchdog cannot tell idle from busy and must say so to the architect rather than nudging on a guess.updated_at. Spec 1470 documents why:next()writes no state on the normal path, soupdated_atstays pinned for the whole of a healthy build.Known cost to design around
The Stop hook nudges once per cycle, and
stop_hook_activeresets on each user message — so a human or architect asking a mid-phase builder a question already pays one forced extra turn. A watchdog layered on top must not compound that.