|
| 1 | +# ADR-0003: Reporting checkpoint gating at the authoring boundary |
| 2 | + |
| 3 | +- Status: Accepted |
| 4 | +- Date: 2026-08-17 |
| 5 | + |
| 6 | +## Context |
| 7 | + |
| 8 | +On 2026-08-17 a hand-authored 15-node workflow (issue #320) ran 75 minutes to |
| 9 | +`completed` although every one of its five decision checkpoints returned |
| 10 | +`verdict: replan`. The checkpoint nodes carried `report_to_parent: true` but |
| 11 | +their stage dependents declared only `depends_on`, no `condition`. The engine |
| 12 | +spawns a dependent the moment all of its dependencies complete, so each next |
| 13 | +stage started milliseconds (≈12ms) after its checkpoint settled; the wake to |
| 14 | +the parent was terminal-only advisory signal, never a gate. The authoring |
| 15 | +model ignored warning-level feedback, which is how the ungated shape shipped. |
| 16 | + |
| 17 | +Block-compiled graphs already gate dependents on checkpoint verdicts (the |
| 18 | +issue #294 REJECT-checkpoint shape); hand-built node graphs had no equivalent |
| 19 | +check. |
| 20 | + |
| 21 | +## Decision |
| 22 | + |
| 23 | +A `report_to_parent: true` node with dependents is a **reporting checkpoint**. |
| 24 | +Each dependent must gate on the checkpoint's output via `condition` |
| 25 | +(`input_mapping` does not count — it feeds data, it does not gate), or the |
| 26 | +checkpoint must be a reporting leaf, or the node must drop `report_to_parent`. |
| 27 | +`node_defaults.report_to_parent` is honored: a node inheriting the default |
| 28 | +reports the same way. |
| 29 | + |
| 30 | +Enforcement lives in `checkpointGateDiagnostics`, wired only into |
| 31 | +`validatePostCompile`'s structural branch — the authoring start/validate path. |
| 32 | +Every ungated dependent emits one error-severity `dag.invalid` diagnostic in |
| 33 | +both `portable` and `environment` profiles, so `start` and `validate` reject |
| 34 | +the shape before any durable graph exists. |
| 35 | + |
| 36 | +Enforcement is authoring-only by design. `Dag.create` and the replan/extend |
| 37 | +fragment paths stay untouched: the verdict vocabulary is open, the ACCEPT path |
| 38 | +must not wait for the parent, and runtime enforcement would change the |
| 39 | +semantics of every existing graph, including issue #294's wake-chain and |
| 40 | +reopen-extend behavior. |
| 41 | + |
| 42 | +## Consequences |
| 43 | + |
| 44 | +- Ungated reporting checkpoints fail fast at start/validate with a diagnostic |
| 45 | + naming the checkpoint, the dependent, and the three legal fixes. |
| 46 | +- Runtime create, wake chains, and reopen-extend semantics are unchanged; |
| 47 | + trusted internal callers retain full runtime flexibility. |
| 48 | +- Saved and curated workflows were audited: 14 curated block workflows are |
| 49 | + unaffected; only `ultra-flow-route.yaml` and `release-route.yaml` trip the |
| 50 | + new check and are tracked in opencode-dag-config#14. |
| 51 | + |
| 52 | +## Alternatives Considered |
| 53 | + |
| 54 | +- Runtime enforcement at `Dag.create`: rejected — the verdict vocabulary is |
| 55 | + open-ended, the ACCEPT path must not block waiting for the parent, and it |
| 56 | + would change the behavior of every existing graph. |
| 57 | +- Warning-severity diagnostic: rejected — the authoring model ignores |
| 58 | + warnings; that is precisely how the incident happened. |
| 59 | +- A new explicit `gate` field on dependents: rejected — `condition` already |
| 60 | + expresses output gating and the block compiler already emits it; a second |
| 61 | + mechanism would split the gating vocabulary. |
| 62 | + |
| 63 | +## Deferred |
| 64 | + |
| 65 | +- Replan/extend fragments are not checkpoint-gate-checked (coverage gap; no |
| 66 | + date). Runtime flexibility was prioritized; fragment authoring remains |
| 67 | + advisory. |
| 68 | +- Deprecation of advisory wake chains (no date): `report_to_parent` without |
| 69 | + gated dependents stays legal but is a smell worth revisiting once fragment |
| 70 | + coverage exists. |
0 commit comments