Written from the run itself, not from notes Every figure re-checked against live gh · 2026-08-01
How do you get 37 agents to finish, and prove it?
The goal contract, the delivery mechanism, and the verification rule · including the five bugs and one false alarm found while driving it.
17 DONE REPORTS · 0 OVERCLAIMS
docs.propflowai.coFLEET DRIVE
02 /VERDICT
Lead with the answer
Every session that reported DONE was telling the truth.
Thirty items across two repos, each re-checked with a live gh --json state call rather than taken from the report. Not one session overclaimed a merge.
REPORTS
17
sessions reporting DONE
RE-CHECKED
30
PRs + issues, live gh
OVERCLAIMS
0
across every report
BUGS FOUND
5
in the driving tools
The precision was real, and it was the fleet's, not the auditor's. Three sessions volunteered corrections against their own interest before anyone asked.
Thirty-seven live sessions, and no one could say what was open.
What we had
A roster
Names, a status field, and a last-activity time. Duplicates from a restart tool. Sessions finished for over a week still listed. The status field itself read working for a session idle fifteen hours.
What we needed
A disposition
For every session: done with evidence, actively owned, or closed with a named reason. Nothing in between, and nothing taken on the session's own word.
A roster tells you a session exists. It never tells you whether the work is finished.
claude agents --json · 2026-07-31 audit03
04 /METHOD
The rule that prevents the mess
A session that accumulates a second goal is how we got here.
01 · NAME
New effort gets a new name and its own directory
Never reuse a worktree · never a -vN suffix — that is what made the fleet unreadable.
02 · SCOPE
A brief with a single goal and an explicit not-this list
What it must not touch is as load-bearing as what it must do.
03 · RECORD
Written to the ledger before it starts
Context must never live only inside a child that can die with it.
04 · CLOSE
Outcome folded back, then the session ends
A finished session left running is indistinguishable from a stalled one.
primary/BRIEF.md04
05 /METHOD
What actually gets sent
A goal states the finish line, the proof, and the escalation.
Not "wrap up your work". The directive delivered to every session named all three, so a session could tell on its own whether it was done.
Finish line
Every PR and issue I own is MERGED or CLOSED — or explicitly handed off by naming who takes it.
Proof
A live gh call. Not memory, not PR prose.
Report
Message the driving thread with one line of status plus remaining open items.
Escalation
Ordinary judgment → a subagent, act on its answer. Money, customer-facing sends, destructive, or arming a capability → the human, and park refusing-by-default.
"Handed off by naming who takes it" is the clause that did the most work — it makes "not mine" a complete answer instead of an excuse.
the delivered /goal text · queue/*.txt05
06 /METHOD
Why a goal alone is not enough
The goal says when to stop. A separate gate decides whether it may.
Gate 1 · the goal
Self-assessed
The session judges its own conditions and keeps working until they hold. Cheap, and it runs on every turn — but it is the pursuer grading its own pursuit.
Gate 2 · the drive gate
External
A stop hook that refuses the ending when the conditions are not met, and says which one failed. It rejected this thread's own first completion — seventeen sessions were still unaccounted for.
A goal judged only by the session pursuing it will be judged generously. The gate that turned one premature "done" into a further hour of work paid for the whole design.
A pane accepts send-keys whether or not anyone is attached, so the Agent view and a human watching over the shoulder never break the channel.
driver picks sid
→
resolve → pane
→
status check
→
send-keys + Enter
Refuses busy targets
Typing into a turn in progress lands text at a moment the session did not choose — or answers a permission prompt on its behalf.
Never --force a queue
Waiting is the feature. The refusal is the whole reason to prefer this over a broadcast.
agent-tools/msg · agent-tools/ra07
08 /MECHANISM
What to do when every target is busy
You cannot schedule an agent's attention, so you wait for it.
Ten directives went out to sessions mid-turn. A watcher polls, and delivers each one the moment its session goes idle. Sequencing beats forcing.
LOCK
Exactly one watcher, enforced by mkdir
Two racing watchers both see the same idle session and both send before either removes the file. Two started; the second refused.
DEADLINE
A bounded life, not a daemon
A watcher that outlives the work it was serving becomes a source of surprise messages.
GONE
A target that leaves the roster is dropped, and logged
Silent drops read as delivery.
tmp/deliver-when-idle.sh08
09 /BUG 1
The first bug the run surfaced
The watcher dequeued on a success that only described the send.
msg exiting zero means tmux accepted the keystrokes. It says nothing about whether the session ingested them. The queue file was removed on that signal.
The old proof
Exit code 0
Available instantly, always true when the pane exists, and completely silent about the outcome that matters.
The honest proof
The text in the target's own transcript
Confirmed before the queue file is removed, retried, and capped at three attempts so a session that can never ingest is not typed into forever.
Every layer of this stack had the same flaw — the log recorded what the driver did, never what the fleet received.
tmp/deliver-when-idle.sh · arrived()09
10 /BUG 2
Reported by four sessions · corrected twice, including here
A message meant for one session was typed into another session's live turn.
This slide first said "resolution was fine, the fault was pane selection."That was wrong. Both layers were broken independently, and fixing either alone still misdelivers.
Wrong session
_match searched sid, title and first-user text in one pass over recency-ordered rows — so a session that merely mentioned an id outranked the session itself. The goal text put the driver's id into every recipient's transcript. #162
Wrong pane
A proven host (the pane's argv names the sid) and an inferred one (walk a pid to a pane) were pooled and ranked by recency, so the guess could outrank the proof. #161
Invisible either way
The confirmation line was byte-identical in the failing and working case but for the pane name — matching title, matching [sid]. Two sessions read it as success in good faith. It now prints (proof) or (INFERRED). #163
Shared shape
In all three, proof and a fuzzy match were treated as equals — and the fuzzy one won on recency.
A test passing against both implementations proves neither.
The three obvious tests for the routing fix would have passed against the broken code too, because the real fixture had only one plausible host. So the suite carries a control that models the old ranking and asserts it picks the impostor.
If deleting the fix does not turn a test red, the test was decoration. Run the break before trusting the green.
Also pinned
The regression the fix could cause
Tiering could have made every interactive session unreachable — a worse failure than the misroute. One test holds that door shut.
Gate
5086 passed, 3 skipped
ruff clean · mypy clean across 201 files · full suite green before push.
tests/test_agent_tools_session_fleet.py11
12 /VERIFY
The rule shipped inside the goal itself
The obvious merge check gives false negatives on every squash.
Ask git merge-base --is-ancestor whether a branch landed and a squash-merged PR answers no — the branch commits genuinely are not ancestors. Sessions that used it would have reported finished work as unfinished.
gh state
→
squash commit
→
merge-base
→
content on main
One session caught a real stranding this way — a squash merge left a follow-up commit behind, and only the content check on origin/main saw it.
the /goal text · confirmed by 4ed1b118, 30469c5a12
13 /PATTERN
The thing worth taking away
Every bad conclusion came from an instrument answering a different question.
01
Branch name in a transcript ≠ ownership · gave one session 27 PRs; it had run gh pr list once
02
The verifier sat inside its own corpus · printed all 53 open PRs, then owned every one
03
Turn count ≠ activity · autonomous sessions have ~0 human turns by construction
04
Regex adjacency ≠ assertion · flagged 11 overclaims; all 11 honest on re-read
05
A zero from a broken instrument · grep on a packed binary, 0 for symbols that must exist
06
A goal judged by its pursuer · recorded abandoned; had shipped in three merged PRs
07
send-keys success ≠ delivery · the watcher dequeued on the send, not the arrival
08
A needle from the wrong message · mine — next slide
docs.propflowai.co/a/verification-learnings13
14 /PATTERN
Owning it on the record
I reported two deliveries as never arriving. They had arrived.
Auditing the twelve sent messages, I searched every session's transcript for one other session's message body — but the queued messages were per-session and tailored. The needle answered "did this session receive 562c7fae's message", not "did it receive its own".
Why I trusted it
A positive control had caught a different bug in the same detector minutes earlier — a needle keeping a literal /goal , which never appears, because slash commands are stored split into command-name and command-args.
The real lesson
Validating an instrument on one axis does not validate it on the others. Half-validated felt like validated.
Cost
Two messages re-queued; removed before the watcher double-delivered. One redundant send landed.
primary/VERIFIED.md · wave 214
15 /EVIDENCE
Not everything was a finding
The sessions corrected themselves, unprompted and against their own interest.
Retracted a cause
Bad lead, named
"That was an unchecked guess in my own pre-compact handoff that you inherited as fact." Then found the real cause — a silenced error rendering identically to an empty result.
Refused credit
Not my work
Corrected the driver's framing: the file was another session's, and it was red, so it was abandoned work — not finished work sitting unpushed.
Left it unexplained
Unreproduced
One test failed once and passed on re-run. Recorded as unreproduced rather than attributed to a nearby known finding it superficially resembled.
This is the behaviour the goal was trying to buy, and it appeared without being asked for.
reports from 30469c5a · d1b5d6d3 · 4ed1b11815
16 /EVIDENCE
What a driving thread adds
A blocked session had the observation right and the cause wrong.
A PR got zero workflow runs across four trigger attempts — new PR, close/reopen, empty commit, fresh PR — while other branches ran minutes apart. Correctly ruled out an outage. Attributed it to GitHub.
Pull-request workflows run against the merge ref. When the branch conflicts, that ref cannot be built, so no run is ever created — silently, with nothing shown on the PR.
Why the retries failed
None of them made the merge ref buildable.
Control
A clean branch pushed the same hour: 2 runs, immediately.
Passing the report along would have left it blocked. The driver's job is the check the blocked session could not run on itself.
gh api pulls/159 · runs?branch=… total_count 016
17 /RULES
If you drive a fleet next
The short version.
Do
These paid for themselves
State the finish line, the proof, and the escalation in the goal itself · re-check every claim yourself with a live call · make the session name who takes what it drops · run a positive control before believing a negative · let an external gate decide when you are done · write a resume note before compaction.
Don't
These cost time
Force a message into a mid-turn session · trust a roster status field · treat a transcript as evidence of what a session did · delegate anything that arms a capability or moves money · restart a rate-limited session — the account is spent, so wait · report a defect before re-reading the sentence that produced it.
The single highest-yield habit: before believing any measurement, ask what question the instrument actually answered.
the whole run17
18 /CLOSE
The fleet was honest. The instruments were not.
Zero overclaims across 30 re-checked items — and every wrong conclusion in this run came from a measurement that answered a different question.