radio prompt-hook: surface pending inbox via UserPromptSubmit context injection (#164)#173
Conversation
… injection (#164) An idle agent fires no hooks, so when the send-time zellij wake fails (no writable pane, stale TAB_ID, missing session file) queued messages stay invisible until a later wake happens to succeed. UserPromptSubmit hook stdout IS injected into the model's context (unlike Stop), so: - add `radio prompt-hook`: marks the role busy (old `radio busy` behavior preserved), then prints a one-line summary of any unread inbox — id, from, intent, pr/issue per message. Empty inbox prints nothing (zero context noise); missing $TASK_FORCE_ROLE is a silent exit 0 (#93 semantics). - switch the UserPromptSubmit hook from `radio busy` to it in this repo's .claude/settings.json and the four claude-* task-init installers; PostToolUse keeps plain `radio busy`. - generalize the #163 upgrade_stop jq migration to upgrade_cmd and use it to rewrite legacy `radio busy` UserPromptSubmit hooks in place on task-init re-runs; extend the #172 stray-hook warning to UserPromptSubmit. Closes #164 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review — PR #173 (spec: #164)Verdict: changes requested — 2 correctness findings, 1 release gap, 2 explicitly-requested items from the issue notes that were skipped, plus optional hardening. First, credit where due: Correctness
Release
Efficiency
Issue-notes compliance (both were explicit in the #164 notes)
Optional (non-blocking)
Findings 2+4 are one combined fix; 1 is small guards; 3, 5, 6 are mechanical. Happy to re-review the delta. |
… injection (#164) — review round 2 PR #173 review findings: - fail open on every prompt-hook path (a nonzero UserPromptSubmit exit blocks AND erases the user's typed prompt): _update_state snapshots the session file before the awk rewrite (the #140 cascade could unlink it mid-flight — observed live as `awk: can't open file` — and _atomic_write would resurrect an empty file), _hook_role_gate extends the #93 no-role gate to set-but-invalid roles for hook entrypoints, and the state flip in cmd_prompt_hook is best-effort after the summary is printed. - fence the frontmatter parse: new _inbox_summary helper does one awk pass per message scoped to the --- block, so body lines can't fabricate optional pr=/issue= fields; values are clamped to a safe charset so a crafted intent can't forge extra summary entries. 1 fork per message instead of 5. - extract _hook_payload_field/_read_hook_payload (shared by stop-hook and unregister; #168 needs both) per the issue notes. - CHANGELOG entry with the load-bearing "re-run task-init" upgrading note. - drift-guard the 4-way installer hook block: radio-install-hooks group, 3 sentinel regions + manifest entries; stray-hook check parameterized per-event. - tests: invalid-role gates, body-line pr: leak, forged-intent clamp, unreadable-session-file fail-open, stranded-message sequence (stop_hook_active=true → next prompt surfaces), single-run variant warning. 840/840 pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round 2 pushed (d113b25). All findings addressed:
Optional items: intent/from sanitization done inside the summary awk (charset clamp; forged Also added the issue-notes sequence test: message queued during a drain turn ( Full suite 840/840, drift clean. |
Supplemental — round-2 guidance (posted before you radio re-review, to save a round trip)A late-running review pass surfaced these; the first is about the round-2 code currently in your worktree, the rest apply to round 1 as well:
The helper extraction, payload-parse sharing, and concurrent-read tolerance in round 2 all look right. |
… injection (#164) — round-2 supplemental - cmd_prompt_hook flips busy FIRST again, before any inbox work (guarded with || true — fail-open is preserved): deferring the flip re-opened the #172 idle-window race where cmd_send write-chars into a pane whose turn has already started. - _hook_role_gate notes an ignored invalid-role toggle on stderr when stdin is a TTY (manual `radio busy`), still exit 0; hook invocations pipe stdin and stay silent. - README: PostToolUse row documents why it deliberately stays `radio busy` (not prompt-hook); kiro-parity sentence corrected — kiro keeps plain `radio busy` on userPromptSubmit (no injection mechanism; #146). - workflow docs (.claude/gh-workflow.md + 4 steering templates): companion paragraph telling agents the injected `[radio] N unread message(s): …` line is the canonical radio channel, not user-typed text. 840/840 tests, drift clean (21 groups). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Supplemental addressed (304f5df):
840/840 tests, drift clean. |
Re-review — 304f5df (rounds 2+3 verified together)All six round-1 findings and all five supplemental points confirmed fixed:
Independently verified by reading the functions, running check-drift, and confirming all 4 CI checks green on 304f5df. LGTM — ready to merge. |
Closes #164
Problem
An idle agent fires no hooks, so if the send-time zellij wake fails (no writable pane, stale TAB_ID, missing session file) the queued message is invisible until a later wake happens to succeed — landing precisely on recipients that are ready to work.
Changes
bin/radio: newradio prompt-hooksubcommand. Marks the role busy (preserving the oldradio busyUserPromptSubmit behavior), then — if the inbox is non-empty — prints a one-line summary ([radio] N unread message(s): <id> from=… intent=… pr=… | …) which Claude Code injects into the model's context. Empty inbox prints nothing; no$TASK_FORCE_ROLEis a silent exit 0 (Radio hooks must no-op when $TASK_FORCE_ROLE is unset (don't block plainclaudesessions) #93 semantics). The summary works even when the session file is missing (the failure mode from the Radio session corruption: unregister cascade → re-seed loses TAB_ID + LOADOUT (breaks --auto wake-up) #140 cascade), since the inbox scan doesn't depend on the session file..claude/settings.json(this repo): UserPromptSubmit hook switched fromradio busytoradio prompt-hook.claude-{gh,local,notion,jira}/bin/task-init: installradio prompt-hookon UserPromptSubmit; PostToolUse keeps plainradio busy. The radio stop-hook: flush queued messages when the agent goes idle (Stop-hook block JSON) #163upgrade_stopjq migration is generalized toupgrade_cmd(arr; old; new)and reused to rewrite legacyradio busyUserPromptSubmit hooks in place on re-run; the radio stop-hook: flush queued messages when the agent goes idle (#163) #172 stray-hook warning now also covers UserPromptSubmit.README.md: command table + hooks table updated.Verification
tests/radio_prompt_hook.bats(new, 10 tests): both queued messages listed in one line; empty inbox → zero stdout; no-role → silent exit 0; busy transition; inbox not consumed;issue=/pr=field handling; missing-session-file resilience; usage.tests/claude_gh_task_init.bats(+3): UserPromptSubmit =radio prompt-hook; legacyradio busyupgraded in place while PostToolUse is untouched; non-byte-exact variant preserved with a loud WARNING.env -u TASK_FORCE_AUTO_SUBMIT ./run_tests.sh; the env-unset is needed because this worker session itself runs undertask-work --auto, which leaksTASK_FORCE_AUTO_SUBMIT=1into three pre-existingradio_auto_submitassertions — unrelated to this change).tools/check-drift.sh: 18 groups clean.🤖 Generated with Claude Code