Gate agent completion on commits, pushes, and tests; capture real checkpoints

Agents could be marked done while leaving work uncommitted or unpushed:
the Stop gate only ran the test suite, and the headless supervisor's
fallback marked any still-working agent done on a clean process exit
even when the Stop gate never recorded a verdict. Checkmarks also only
ever carried hook-written boilerplate, so the webui had no real
checkpoint to show.

The Stop gate now blocks the turn on any of: failing tests, uncommitted
changes, or commits no origin/* ref contains (rev-list --not
--remotes=origin, so it works for the --no-track worktree branches).
All blockers are reported at once; status 'done' only ever accompanies
a fully passing gate. Working dirs that aren't git checkouts, and repos
without an origin remote, skip the git half so local-only projects
can't deadlock. The supervisor's clean-exit fallback now reconciles a
still-working agent to blocked instead of done — done is a gate
verdict, not an exit code (operator cancels still settle as done).

The agent's final message is captured deterministically from the
session transcript onto the checkmark's where_it_stopped, so the
dashboard always shows a real checkpoint regardless of whether the
agent thought to leave one; a blocked checkmark shows the blockers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7mF6qeryi9nJthaxYkfPm
This commit is contained in:
Claude
2026-07-23 06:49:39 +00:00
parent f03b03ab5c
commit 9ad13b2488
8 changed files with 256 additions and 30 deletions
+3 -1
View File
@@ -81,7 +81,9 @@ def test_spawn_run_streams_events_and_completes(headless_env, tmp_path):
assert [e["seq"] for e in events] == [1, 2, 3]
# last_output is now derived from assistant text — the log the UI shows is real.
assert updated["last_output"] == "working on: build the thing"
assert updated["status"] == "done"
# done is a gate outcome, not an exit code: the fake claude never ran the Stop
# hook, so nothing verified tests/commits/pushes and the agent must not be done.
assert updated["status"] == "blocked"
assert updated["session_id"] == run["session_id"]
assert updated["worker_id"] == "w1"
# The session archive was uploaded at exit for cross-worker resume.