Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 `<task-notification>` 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)
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.45.0
0.45.1
9 changes: 7 additions & 2 deletions hooks/bg-task-stop-guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <task-notification> 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 <pr>` 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. "
Expand Down
6 changes: 6 additions & 0 deletions tests/bg-task-stop-guard.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <task-notification>" <<<"$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

Expand Down Expand Up @@ -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:
Expand Down
Loading