fix(#4255): resolve reviewer-lane effort from the lane, not from gsd-plan-checker (#4275)

`review-lane plan` resolved every cross-AI reviewer lane's reasoning effort by
spawning `query resolve-execution gsd-plan-checker --host <slug>`. The agent id
was a hardcoded literal, so `--host` chose only the argv RENDERING while the
LEVEL always came from the installed plan-checker's frontmatter — `low` under
every shipped model profile. Every prompt-fed lane therefore ran at a fast
structural verifier's effort, and because the rendered argument is a CLI config
override it silently beat the effort the operator had configured for that CLI.
At `low` a large source-grounded prompt makes a model end its turn with no final
message, so the lane came back empty and its stub read as a crash.

Effort is a property of the review, so the lane declares it. Two new fields on
ReviewerLane — `effortConfigKey` (`review.effort.<slug>`) and `defaultEffort` —
carried through each capability manifest and the generated registry, set on the
three lanes with an argv effort channel and null on the other nine. A new pure
`resolveLaneEffort()` resolves config key -> lane default -> nothing, where
"nothing" emits no effort argument at all and the reviewer CLI's own
configuration decides; `inherit` selects that path explicitly and an
unrecognized level falls back to the lane default rather than being forwarded to
a CLI that would reject it. The host's negotiated effortSurface still gates the
rendering, so ADR-1239/#2481's trust boundary holds on this path too. Resolving
in-process also removes up to twelve subprocess spawns per review.

The empty-output stub now names the effort the lane ran at and distinguishes a
clean exit from a timeout kill, a non-zero exit, and a process that never ran —
`status` is null for both a timeout and a signal, so those were indistinguishable
before. The hint is hedged: a clean empty exit is most often a model stopping
short, but it is also consistent with a CLI writing its output elsewhere.

Also: the capability validator now knows both fields, rejects a malformed key or
an out-of-vocabulary default, and rejects a default declared without a config
key (a level the operator could never override). An existing end-to-end row in
tests/effort-surface-axis.test.cjs asserted the old coupling; it now configures
the lane's own key and pins the decoupling in the same real spawn, with the
agent execution tier set to a level that must not appear.

Emitted-Drift-Ack-Growth: review.md — the effort/model resolution-order table this fix adds. The workflow is where an operator looks to find out which knob set a lane's model and effort; leaving the new key undocumented there is the same invisibility that made the plan-checker coupling survive this long.

Emitted-Drift-Ack-Growth: review.md — the effort/model resolution-order table this fix adds. The workflow is where an operator looks to find out which knob set a lane's model and effort, so leaving the new key undocumented there is the same invisibility that let the plan-checker coupling survive.

Claude-Session: https://claude.ai/code/session_01CRMEuzNMWn3gs5uUW2ghcF

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
Behruz Nassre Esfahani
2026-09-05 02:25:44 -07:00
committed by GitHub
parent 925a363879
commit 5ad9a36f35
26 changed files with 759 additions and 61 deletions

View File

@@ -354,6 +354,20 @@ declared in the manifest — timeout floor, probe, prompt/output channel, empty-
behaviour that data genuinely cannot express is a named first-party `handler` (ADR-2782 D6), never
a bespoke block here.
**Effort and model resolution (#4255).** A lane's reasoning effort and model each resolve through
their own declared key, and the resolution order is inspectable rather than implicit:
| piece | order, highest first |
|---|---|
| model | pinned reviewer-instance `--model` → the lane's `modelConfigKey` (`review.models.<slug>`) → the CLI's own default |
| effort | the lane's `effortConfigKey` (`review.effort.<slug>`) → the lane's declared `defaultEffort` → **nothing emitted**, so the CLI's own configuration applies |
Both come from the LANE. Effort in particular is never read from an agent's execution settings:
until #4255 it was resolved by querying `gsd-plan-checker`, so every prompt-fed lane ran at that
verifier's `low` and, because the rendered argument is a CLI config override, it silently beat the
effort the operator had configured for the reviewer CLI itself. A lane that declares no effort
emits no argument at all — a value borrowed from an unrelated agent is worse than no value.
**Timeout guidance (#2194):** prompt-fed source-grounded reviews are slow — measured ~570 s for
Codex at `xhigh` effort and ~525 s for headless Claude on a large plan set. Each lane declares its
own `timeoutFloorMs` and the runner enforces it internally, but the **Bash tool call wrapping the