`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:
committed by
GitHub
parent
925a363879
commit
5ad9a36f35
@@ -580,14 +580,20 @@ describe('#2481 — ADR-443 mechanism callers, as they actually exist', () => {
|
||||
describe('#2481 review workflow resolves effort per reviewer', () => {
|
||||
test('shipped orchestration: the live claude lane genuinely receives --effort <level> in its spawned argv', () => {
|
||||
// Phase 5b (#2799) moved the call out of review.md's per-lane bash and into the review-lane
|
||||
// route's `effortFor()`, which SPAWNS `query resolve-execution … --pick effort_argv_string`
|
||||
// once per selected lane and folds the result into that lane's argv template. A text grep for
|
||||
// the string "resolve-execution" in gsd-tools.cjs would pass even if effortFor's result were
|
||||
// silently dropped before reaching the spawned reviewer, or if the call were dead code. This
|
||||
// drives the REAL `review-lane invoke` route end-to-end — real cp.spawnSync, a real project
|
||||
// config, a real claude-shaped shim on PATH — and inspects the argv the shim actually received,
|
||||
// which is the only way to prove the resolved effort reaches the invocation rather than merely
|
||||
// that some file mentions the command name.
|
||||
// route's `effortFor()`, which folds a resolved level into that lane's argv template. A text
|
||||
// grep in gsd-tools.cjs would pass even if the result were silently dropped before reaching
|
||||
// the spawned reviewer, or if the call were dead code. This drives the REAL `review-lane
|
||||
// invoke` route end-to-end — real cp.spawnSync, a real project config, a real claude-shaped
|
||||
// shim on PATH — and inspects the argv the shim actually received, which is the only way to
|
||||
// prove the resolved effort reaches the invocation rather than merely that some file mentions
|
||||
// the command name.
|
||||
//
|
||||
// #4255 changed WHERE the level comes from, not whether it must arrive. It used to be read
|
||||
// from the `gsd-plan-checker` AGENT's execution settings via a hardcoded id, so this row
|
||||
// configured `effort.routing_tier_defaults` and expected that value in the reviewer's argv.
|
||||
// A reviewer lane is not that agent, and the coupling is the bug. The row now configures the
|
||||
// lane's OWN `review.effort.claude` and pins the decoupling in the same spawn: the execution
|
||||
// routing tier is set to a DIFFERENT level, and leaking it into the reviewer is a failure.
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2481-orchestration-e2e-'));
|
||||
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2481-orchestration-project-'));
|
||||
try {
|
||||
@@ -637,9 +643,14 @@ describe('#2481 review workflow resolves effort per reviewer', () => {
|
||||
fs.mkdirSync(path.join(projectDir, '.planning'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(projectDir, '.planning', 'config.json'),
|
||||
// #3531: pin every tier so the expected value is agent-independent —
|
||||
// the reviewer lane's tier decides, not effort.default.
|
||||
JSON.stringify({ effort: { routing_tier_defaults: { light: 'xhigh', standard: 'xhigh', heavy: 'xhigh' } } }, null, 2),
|
||||
// Two levels, deliberately different (#4255). `review.effort.claude` is the reviewer
|
||||
// lane's own key and is what must reach the shim. `effort.routing_tier_defaults` drives
|
||||
// the AGENT execution axis and is pinned across every tier to a level that must NOT
|
||||
// appear — before #4255 it, through gsd-plan-checker, was the only thing that could.
|
||||
JSON.stringify({
|
||||
review: { effort: { claude: 'xhigh' } },
|
||||
effort: { routing_tier_defaults: { light: 'minimal', standard: 'minimal', heavy: 'minimal' } },
|
||||
}, null, 2),
|
||||
);
|
||||
|
||||
const r = cp.spawnSync(
|
||||
@@ -663,7 +674,15 @@ describe('#2481 review workflow resolves effort per reviewer', () => {
|
||||
const argv = fs.readFileSync(seenArgv, 'utf8').trim().split(/\r?\n/);
|
||||
assert.ok(
|
||||
argv.includes('--effort') && argv.includes('xhigh'),
|
||||
`resolved effort ("xhigh") did not reach the spawned claude reviewer's argv: ${JSON.stringify(argv)}`,
|
||||
`the lane's own review effort ("xhigh") did not reach the spawned claude reviewer's argv: ${JSON.stringify(argv)}`,
|
||||
);
|
||||
// The decoupling half (#4255), and the reason this row is worth a real spawn: the agent
|
||||
// execution tier is pinned to `minimal` above. Seeing it here would mean a reviewer lane is
|
||||
// still taking its effort from an agent's execution settings.
|
||||
assert.ok(
|
||||
!argv.includes('minimal'),
|
||||
'the AGENT execution routing tier leaked into the reviewer lane\'s argv: '
|
||||
+ `${JSON.stringify(argv)} — a lane's effort must come from its own review key`,
|
||||
);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
|
||||
Reference in New Issue
Block a user