diff --git a/CHANGELOG.md b/CHANGELOG.md index ed26386..d4615af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## 0.45.1 — 2026-10-09 + +### fix(hooks): bg-task-stop-guard says when a background subagent is actually done (BRO-2815) + +I dogfooded #134 on a real Paseo turn (agent acb1ab0b, session cc0aedf2). The guard blocked the turn-end on a +background subagent, and the session waited in the foreground as told. But its until-loop grepped the +subagent's output file for the token it expected back. That file is the subagent's transcript, and it already +holds the subagent's own prompt, so the loop matched in 2 s. The model then reported a verdict it had not +received; the real one landed 56 s later. + +**No grep on that transcript proves a subagent is done.** My first fix proposed `"stop_reason":"end_turn"` as +the completion condition. Cross-review checked it against the 400 most recent of the 2,202 subagent +transcripts under `~/.claude/projects/*/*/subagents/`: +- 60 of the 359 that contain `end_turn` have an earlier one: + - in 48, a SendMessage resumed the agent after an end_turn; + - in 7, the agent's own background shell woke it; + - in 3, the final message was split across two lines; + - in 2, the agent kept working with no resume. +- 14 never write `end_turn` at all, because they end on a synthetic session-limit message. + +The re-prompt now says that a subagent is finished only when its `` for that task id has +been delivered. That notification arrives after a foreground call returns. A transcript grep is a cue to look +again, never proof. The warning about a shell left running behind an ended turn is back. + +`tests/bg-task-stop-guard.test.sh` pins the rule, and a 16th mutant that drops it is killed. + ## 0.45.0 — 2026-10-09 ### feat(hooks): under Paseo, a turn cannot end while the session's own background task is still running (BRO-2815) diff --git a/VERSION b/VERSION index bcce5d0..ff12df2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.45.0 +0.45.1 diff --git a/hooks/bg-task-stop-guard.py b/hooks/bg-task-stop-guard.py index e381484..3c54606 100755 --- a/hooks/bg-task-stop-guard.py +++ b/hooks/bg-task-stop-guard.py @@ -108,8 +108,13 @@ def reason(tasks): "the condition the task produces works, e.g. polling its output file or " "`gh pr checks`, with timeout under 10 minutes; run it again if it times out. " "A long leading `sleep` is refused by the harness.\n" - " - A subagent is done when its completion notification arrives. Its output " - "file can show an ended turn while a shell it left behind is still working.\n" + " - A subagent is finished only when its for that task id has " + "been delivered to you (it arrives after a foreground call returns). Its output file " + "is its transcript. That file already holds the subagent's own prompt, and an " + "end_turn in it can be followed by more work: a resume, or a shell the subagent " + "left running. So a grep on the file is a cue to look again, never proof the " + "subagent is done. Keep making bounded foreground waits until the notification " + "has arrived, and only then use the result.\n" " - For a CI wait, re-run `p9 watch ` in the foreground (exit 8 means run it again).\n" "If a task is meant to outlive this turn (a dev server, a standing monitor), say " "so in one line and end the turn. This guard never asks twice about the same task. " diff --git a/tests/bg-task-stop-guard.test.sh b/tests/bg-task-stop-guard.test.sh index a91d192..be89992 100755 --- a/tests/bg-task-stop-guard.test.sh +++ b/tests/bg-task-stop-guard.test.sh @@ -72,6 +72,10 @@ if [ "$RC" = 2 ] && grep -q "FOREGROUND" <<<"$ERR" && grep -q "subagent a269ed5d else bad "pending subagent under Paseo: rc=$RC"; fi if grep -q '"verdict": "BLOCK"' "$STATE/bg-task-guard.jsonl" 2>/dev/null; then ok "BLOCK logged"; else bad "BLOCK not logged"; fi +if grep -q "only when its " <<<"$ERR" && grep -q "never proof" <<<"$ERR" && grep -q "left running" <<<"$ERR"; then + ok "re-prompt: a subagent is done at its notification; a transcript grep is never proof" +else bad "re-prompt lacks the subagent completion rule"; fi + run "$FX/shell-pending.json" "${PASEO[@]}" if [ "$RC" = 2 ] && grep -q "sleep 90; echo done" <<<"$ERR"; then ok "pending shell (or Monitor) under Paseo: exit 2, names the command"; else bad "pending shell under Paseo: rc=$RC"; fi @@ -215,6 +219,8 @@ if [ -z "$QUIET" ]; then "lifetime-cap|$G|LIFE_MAX = 20|LIFE_MAX = 99" "prompt-cap|$G|PROMPT_MAX = 2|PROMPT_MAX = 99" "id-key|$G|return str(t.get(\"id\") or|return str(__import__(\"time\").time_ns()) or str(t.get(\"id\") or" + "subagent-wait|$G|never proof|proof" + "shell-warning|$G|left running|finished" "opt-out|$G|== \"0\":|== \"never\":" "fail-closed|$G| except Exception: return 0| except Exception: