Wave Gating — The Three-Gate Contract
When a wave cell runs, its output is a stream of text. Judging success from that text alone — grepping for "error" or checking the exit code — is how a fully successful cell gets marked failed and a cell that never ran gets marked done. The three-gate contract replaces string-matching with three concrete, independent checks applied in order.
Why output text is never a success signal
On 2026-07-31 a wave cell fixing wbRun's unanchored grep -qi "error:" was marked ❌ by the identical guard still inlined in wave.js — the bug classified its own fix as a failure. In the same run, three cells that never started were indistinguishable from it by output alone.
A /wbWork log legitimately contains Error:, failed and cannot whenever the task is about error handling — and those are exactly the rows where a false verdict costs most. Neither channel (exit code or output text) is sufficient alone: an unusable model can exit 0 having done nothing, and a fully successful cell can print Error: a dozen times.
The three gates
Classify every cell by its three gates in order — never by matching strings in its output.
| Gate | Question | Signal |
|---|---|---|
| 1 · Infra | Did the agent run at all? | CLI exit code, plus anchored fatal patterns (^Error: Model not found, insufficient credits, rate limit, quota, authentication) |
| 2 · Artifact | Did it write what it was required to write? | tasks/task_<ID>/task_<ID>_report_<scope>_<date>.md exists |
| 3 · Oracle | Does the task's own Verify command pass? | Run the row's Verify cell; exit 0 = pass |
These gates are per id, never per dispatch. A merged cell (--id=3,8) runs one agent over two rows: each id gets its own G2 (its own task report) and its own G3 (its own oracle). The cell's verdict is the worst id's verdict.
The verdict table
| G1 | G2 | G3 | Verdict | Done box |
|---|---|---|---|---|
| ✗ | — | — | INFRA — never ran | ⬜ |
| ✓ | ✗ | — | NO-OP — ran, produced nothing | ⬜ |
| ✓ | ✗ | — | MISPLACED — ran, report found outside expected <DD>/tasks/task_<N>/ path | ⬜ (ATTEMPTED class) |
| ✓ | ✓ | ✗ | ATTEMPTED — ran, produced a report, oracle failed | ⬜ |
| ✓ | ✓ | ✗ | BACKGROUNDED — ran, long Verify sent to background, session ended early | ⬜ (ATTEMPTED class) |
| ✓ | ✓ | ✓ | DONE | ✅<br><model> |
| ✓ | ✓ | ✓ | UNVERIFIED — ran on a first pass with an unchanged tree (nothing changed) | ⬜ (ATTEMPTED class) |
Done boxes are checked only on 1-2-3 green (DONE). Everything else stays ⬜ with its verdict and the reason in Wave notes.
I1: Long Verify Backgrounded → NO-OP
Symptom: The validator writes "I'm now waiting on the background oracle selftest run (and a fallback wakeup in ~10 min)" and the session ends with no Validation section (G2 [2]: NO-OP). Cause: A long Verify script inherits the 10-minute Bash cap and is sent to the background by the agent, terminating the session early. G2 cannot distinguish "backgrounded" from "did nothing". What wb-flow now does: Exports long Bash timeouts in the cell environment. G2 classifies logs containing a background/wakeup marker with no Validation section as BACKGROUNDED (an ATTEMPTED class). What the operator still must do: Review the failure in wave notes and manually re-run or wait for the process to complete in the foreground.
I3: Misplaced Report → False NO-OP
Symptom: G2 prints NO-OP — no task report for a task that successfully completed its work. Cause: The task report was written one level too high (<DD>/tasks/task_1/) instead of the explicit expected report path. G2 strictly checks only one path. What wb-flow now does: Searches the reports/<YYYY>/<MM>/<DD>/ tree. If the report is found elsewhere, it prints MISPLACED — <path> and marks it ATTEMPTED. What the operator still must do: Move the misplaced report to the canonical path by hand (or rely on the orchestrator's auto-move) before re-gating.
Cross-Scope Snapshots and First-Run UNVERIFIED (I4)
G2 relies on file system snapshots taken before a wave begins. By default, the snapshot covers only the plan's scope.
I4: Cross-Scope Writes InvisibleSymptom: CHANGED: none is printed despite successful cross-scope edits, leading to tree unchanged, but oracle passes and a false DONE for hollow rows. Cause: The snapshot find command only captured the plan's root directory, completely missing changes in shared workspaces. What wb-flow now does: Extracts every repo path named in the row's Task and Verify cells that resolves outside the scope and adds those directories to the snapshot. A first execution with an unchanged tree is now graded UNVERIFIED — nothing changed, not DONE. What the operator still must do: Ensure cross-scope edits are explicitly mentioned in the Task and Verify cells so the generator tracks them.
Per-id gating for merged cells
When two rows share a plan file, a model, and a role column — and both are small enough that losing the parallelism costs less than paying the agent's start-up context twice — they merge into one dispatch. The generated script runs one agent over both rows and prints a verdict per id:
G2 [3]: PASS G3 [3]: PASS VERDICT [3]: DONE
G2 [8]: NO-OP — no task report at tasks/task_8/task_8_report_*.md
VERDICT [8]: NO-OP
PER-ID: DONE=[3] UNVERIFIED=[] ATTEMPTED=[] NO-OP=[8]
VERDICT: NO-OP — 8The cell exits on its worst id, but the orchestrator reads the PER-ID: line: check 3's Done box, leave 8 at ⬜ with its verdict. Do not fail a merged cell wholesale — a red exit code there routinely carries a genuinely green id.
Pipelined Validation (I2)
Validation does not wait for the entire work wave to complete. Phase 2 (✅ validate) executes in a pipelined fashion: Each validator cell waits only on the process IDs (PIDs) of the specific work cells whose task IDs it covers.
I2: Validation idles behind the slowest work cellSymptom: A fast task finishes in 1.5 minutes, but its validation doesn't start until 73 minutes later when the slowest task in the wave finally finishes. Cause: The orchestrator imposed a hard phase barrier between the work wave and the validate wave, making all validator tasks wait on the completion of the entire work wave. What wb-flow now does: Employs pipelined validation. Each validator cell waits only on the specific PIDs of the work cells it covers. Fast tasks are validated immediately without waiting for slower siblings. What the operator still must do: Monitor the terminal outputs since validations and slower works will now stream concurrently.
Gating a /wbExplain prelude
A dispatch cell that holds both /wbExplain <plan.md> --id=<N> --as="…" and /wbWork <plan.md> --id=<N> runs them sequentially in one lane. The gates still fire once — after the work command. The explain is a prelude: it writes task_<N>_details_*.md and upgrades the plan's explanation link, but its success is measured by whether the work that followed it produced a task report and passed its oracle.
Human-verified rows
A task whose Verify column starts with human: has no runnable oracle. The wave script reports G3: SKIPPED — human-verified row and sets the verdict to UNVERIFIED. This is not a failure — the plan author deliberately wrote a non-machine-checkable assertion — but it is also not a DONE. Only a human can close it.
Model fallback fires on Gate 1 only
When a cell fails at G1 (the agent never ran), the model fallback chain advances to the next model. A different provider and a different rate-limit window might succeed.
When a cell fails at G2 or G3, the fallback stops. The blocker is the task, not the agent — a second model meets the same wall at double the cost. G2 means the task produced nothing; G3 means it failed its own definition of done. Neither is a credential problem. Re-dispatch after a G2/G3 failure is a human decision.
Progress reporting and liveness
The generated script embeds the plan's Est. Time (mins) column and runs a heartbeat every ~30s:
── ⏱️ 12m30s elapsed ──
🔄 5 ~28% of 45m (12m elapsed, CPU 34s)
🔄 7 OVER est 60m by 2m (CPU 112s)idle (time since the log last grew) is not a kill signal. Agents buffer while composing — a thinking cell looks identical to a dead one by idle alone. Use a composite:
- Live process (
pgrep -f -- "--id=<id> ") - Advancing CPU time between samples (
ps -o times=)
A cell that is OVER est and has flat CPU across two samples is the real kill signal. Either alone is not. And a third case the composite still misses: a cell that delegates to sub-agents. Its parent shows low CPU and a static log while children do the real work. When both signals look dead, read the log's last lines before concluding anything — a delegating agent usually names what it dispatched.
← Concepts Hub · Home
