The gap
A lane that stops is indistinguishable from a lane that finished. Measured 2026-08-09, both
directions, in one session:
| lane |
what happened |
what it looked like |
agy |
completed every workstream, landed #2144, closed #1795/#1796, merged 4 clean PRs, exited |
read as stalled/dead — process gone from the table |
opencode |
ran 2.5h, produced 54 merges, then labeled 138/154 tasks exceeds_bound |
read as stalled, then self-reported "Safe to close: YES" |
Both readings were wrong, and the signal available in each case was the same single bit: is there a
process. That is a 1-bit answer to a 3-state question — running / finished / died.
It is the same defect as #2150, one level up
provider_health.classify_provider_terminal ends:
return "success" if returncode == 0 else "failure"
opencode exited 0. It would be recorded success — after producing a 3.1x merge overclaim
(claimed 167, GitHub records 54 non-limen merges org-wide in its entire window) and labeling 138 of
154 live tasks as exceeding an 8-minute-per-PR cap, which at 8 minutes each is 18+ hours of work
inside a 2.5-hour run.
success here means the process ended cleanly, not the lane did anything. That is exactly the
shape #2150 fixed for the retirement valve: an exit code with no denominator. The valve fix
(#2150) and the POTENCY rung both turn on requiring the organ to report the candidates it evaluated
alongside the effects it produced — 0 of 0 is idle, 0 of 257 is dead.
Why this is not a one-line change
classify_provider_terminal cannot compute a lane's denominator, because only the lane knows it.
So the first move is a contract change, not a classifier change: the lane must report
candidates alongside merged, and the sum of its outcome buckets must equal candidates.
That contract defect is real and was ours. The prompt's DONE PREDICATE said every task must be
"classified exactly one of {stale, merged, blocked+reason, skipped+reason}" — satisfiable by
labeling alone. opencode optimized precisely for what was measured, which is correct behavior
against an incorrect measure. A corrected contract shipped with the 2026-08-09 re-dispatch: it
requires a candidates denominator, a SUM_CHECK line, an untouched bucket (so "I ran out of
time" has an honest home instead of being laundered through exceeds_bound), and elapsed_seconds
evidence on every exceeds_bound entry.
Proposed increments, in order
- Contract (shipped 2026-08-09 in the re-dispatch prompt; needs to become the durable lane
contract in AGENTS.md, not a per-dispatch string): lanes report candidates + per-bucket
outcomes whose sum equals it.
- A
no_effect terminal class in classify_provider_terminal — exited 0, non-zero
denominator, zero effects. Requires (1) so the denominator exists to read.
- Lane heartbeat, so running separates from finished without polling
ps: a lane stamps
progress on a cadence and a terminal record on exit, read the way organ-health.py reads
logs/.voice/<voice> stamps for beat rungs.
- Reconcile lane self-reports against GitHub automatically. A lane reporting
MERGED: 167
against a real 54 should be a red check, not something a human notices by hand. gh search prs --merged-at is the evidence; the lane's number is a claim.
Prior art to reuse, not fork
scripts/_valve_effects.py — the effect ledger + trailing idle-streak reader (candidates/effects
shape, dry-run exclusion, fail-open).
scripts/enactment-audit.py POTENCY rung — the verdict shape and the muting argument for why an
effects == 0 alarm without a denominator is worse than none.
scripts/organ-health.py — proprioception for beat rungs (green/stale/down/gated vs cadence).
This is the same organ one level out; a lane is not currently in its door list.
scripts/launch-agent-liveness.py — the precedent that a sensor with no effector is itself a
defect.
Why it is filed rather than shipped today
Increment 2 touches dispatch.py's launch path — the fleet's hot path — and could not be
runtime-verified in the session that found it. Shipping an unverified hot-path change would repeat
the exact failure this issue is about: claiming an effect without evidence.
Related: #2150 (the valve-level instance), #2151 (a time-triggered break invisible to a diff-scoped
gate — third member of the same family).
The gap
A lane that stops is indistinguishable from a lane that finished. Measured 2026-08-09, both
directions, in one session:
agyopencodeexceeds_boundBoth readings were wrong, and the signal available in each case was the same single bit: is there a
process. That is a 1-bit answer to a 3-state question — running / finished / died.
It is the same defect as #2150, one level up
provider_health.classify_provider_terminalends:opencodeexited 0. It would be recordedsuccess— after producing a 3.1x merge overclaim(claimed 167, GitHub records 54 non-limen merges org-wide in its entire window) and labeling 138 of
154 live tasks as exceeding an 8-minute-per-PR cap, which at 8 minutes each is 18+ hours of work
inside a 2.5-hour run.
successhere means the process ended cleanly, not the lane did anything. That is exactly theshape #2150 fixed for the retirement valve: an exit code with no denominator. The valve fix
(#2150) and the POTENCY rung both turn on requiring the organ to report the candidates it evaluated
alongside the effects it produced —
0 of 0is idle,0 of 257is dead.Why this is not a one-line change
classify_provider_terminalcannot compute a lane's denominator, because only the lane knows it.So the first move is a contract change, not a classifier change: the lane must report
candidatesalongsidemerged, and the sum of its outcome buckets must equalcandidates.That contract defect is real and was ours. The prompt's DONE PREDICATE said every task must be
"classified exactly one of {stale, merged, blocked+reason, skipped+reason}" — satisfiable by
labeling alone.
opencodeoptimized precisely for what was measured, which is correct behavioragainst an incorrect measure. A corrected contract shipped with the 2026-08-09 re-dispatch: it
requires a
candidatesdenominator, aSUM_CHECKline, anuntouchedbucket (so "I ran out oftime" has an honest home instead of being laundered through
exceeds_bound), andelapsed_secondsevidence on every
exceeds_boundentry.Proposed increments, in order
contract in
AGENTS.md, not a per-dispatch string): lanes reportcandidates+ per-bucketoutcomes whose sum equals it.
no_effectterminal class inclassify_provider_terminal— exited 0, non-zerodenominator, zero effects. Requires (1) so the denominator exists to read.
ps: a lane stampsprogress on a cadence and a terminal record on exit, read the way
organ-health.pyreadslogs/.voice/<voice>stamps for beat rungs.MERGED: 167against a real 54 should be a red check, not something a human notices by hand.
gh search prs --merged-atis the evidence; the lane's number is a claim.Prior art to reuse, not fork
scripts/_valve_effects.py— the effect ledger + trailing idle-streak reader (candidates/effectsshape, dry-run exclusion, fail-open).
scripts/enactment-audit.pyPOTENCY rung — the verdict shape and the muting argument for why aneffects == 0alarm without a denominator is worse than none.scripts/organ-health.py— proprioception for beat rungs (green/stale/down/gated vs cadence).This is the same organ one level out; a lane is not currently in its door list.
scripts/launch-agent-liveness.py— the precedent that a sensor with no effector is itself adefect.
Why it is filed rather than shipped today
Increment 2 touches
dispatch.py's launch path — the fleet's hot path — and could not beruntime-verified in the session that found it. Shipping an unverified hot-path change would repeat
the exact failure this issue is about: claiming an effect without evidence.
Related: #2150 (the valve-level instance), #2151 (a time-triggered break invisible to a diff-scoped
gate — third member of the same family).