`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
@@ -1322,7 +1322,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
||||
const fsx = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const { REVIEWER_LANES, mergeReviewerLanes } = require('./lib/review-lane-descriptor.cjs');
|
||||
const { resolveLanePlan } = require('./lib/review-lane-invocation.cjs');
|
||||
const { resolveLanePlan, resolveLaneEffort } = require('./lib/review-lane-invocation.cjs');
|
||||
const modelCatalog = require('./lib/model-catalog.cjs');
|
||||
const runner = require('./lib/review-lane-runner.cjs');
|
||||
const cfgLoader = require('./lib/config-loader.cjs');
|
||||
const capabilityLoader = require('./lib/capability-loader.cjs');
|
||||
@@ -1334,13 +1335,15 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
||||
const sub = args[1];
|
||||
// Fail fast on an unrecognized subcommand. Without this check, `sub` fell through
|
||||
// to the `sub !== 'invoke'` usage-error branch far below (after loading the
|
||||
// capability registry AND building a per-lane plan for every lane — which itself
|
||||
// spawns one child `query resolve-execution` process per lane via `effortFor`,
|
||||
// up to 12 subprocess spawns for the default lane set) before ever reporting the
|
||||
// error. That made an invalid subcommand slow instead of instant, and under bench
|
||||
// load (many sequential node spawns) `review-lane bogus` could exceed a caller's
|
||||
// spawn timeout and be killed before writing anything to stderr — the CI-observed
|
||||
// failure was empty stdout AND stderr, not the expected usage message (#3148).
|
||||
// capability registry AND building a per-lane plan for every lane) before ever
|
||||
// reporting the error. That made an invalid subcommand slow instead of instant, and
|
||||
// under bench load `review-lane bogus` could exceed a caller's spawn timeout and be
|
||||
// killed before writing anything to stderr — the CI-observed failure was empty stdout
|
||||
// AND stderr, not the expected usage message (#3148). The plan path used to be far
|
||||
// heavier still: it spawned one child `query resolve-execution` process PER LANE to
|
||||
// fetch effort, up to 12 subprocess spawns for the default set. #4255 resolves effort
|
||||
// in-process from the lane's own declaration, so that cost is gone; the fail-fast
|
||||
// check stays because building 12 plans is still work an unrecognized sub should skip.
|
||||
// `plan`/`invoke` are the only subs that need the expensive plan-building path
|
||||
// below; `sections`/`flags` return earlier still. Anything else errors here, before
|
||||
// any of that work starts.
|
||||
@@ -1427,46 +1430,29 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
||||
return;
|
||||
}
|
||||
|
||||
// Effort argv is resolved per lane by the host's own execution policy, through the SAME
|
||||
// `resolve-execution` surface the bash legs used (`--host <slug>`), so the host's negotiated
|
||||
// effortSurface still decides whether an argument is emitted and the catalog still owns the
|
||||
// syntax (ADR-1239 #2481, ADR-443's escalation ladder). `cmdResolveExecution` writes to
|
||||
// stdout and exits, so it cannot be called in-process for a value — this spawns the same
|
||||
// bounded query the legs did, once per selected lane. A lane whose slug is not a known host
|
||||
// resolves to no effort argument at all.
|
||||
// Effort argv is resolved from the LANE's own review configuration (#4255), then rendered
|
||||
// through the host's negotiated `effortSurface` so ADR-1239/#2481's trust-boundary invariant
|
||||
// still decides whether an argument is emitted at all and the catalog still owns the syntax.
|
||||
//
|
||||
// NOT `--raw` and NOT `--pick` (#2295). `--raw` prints only the resolved EFFORT ('low') with
|
||||
// no host-specific rendering at all. `--pick effort_argv_string` used to be the answer — the
|
||||
// rendered array re-joined into a string ('-c model_reasoning_effort=low') — but the caller
|
||||
// then had to `.split(/\s+/)` that string back apart to get an argv array, and re-splitting a
|
||||
// string the callee just joined is a lossy round trip: any argv element that legitimately
|
||||
// contains a space would come back split into two argv elements, corrupting the very argv it
|
||||
// was rendered to preserve. Reading the UNPICKED object instead gives both `effort_argv` (a
|
||||
// real string array, used verbatim, no re-splitting) and `effort_argv_value` (the bare level,
|
||||
// #2295's `plan.effort`) from the one spawn.
|
||||
const EMPTY_EFFORT = { argv: [], value: null };
|
||||
const effortFor = (slug) => {
|
||||
// What this replaced: a `query resolve-execution gsd-plan-checker --host <slug>` spawn per
|
||||
// lane. The agent id was a hardcoded literal, so the `--host` argument 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 reviewer 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 the stub read as a crash.
|
||||
//
|
||||
// `resolveLaneEffort` is pure and lives beside the other lane resolution; this closure only
|
||||
// injects the rendering, which needs the registry and the catalog.
|
||||
const renderLaneEffort = (host, level) => {
|
||||
try {
|
||||
const r = cp.spawnSync(
|
||||
process.execPath,
|
||||
[__filename, 'query', 'resolve-execution', 'gsd-plan-checker', '--host', slug],
|
||||
{ cwd, encoding: 'utf8', timeout: 15000, killSignal: 'SIGKILL', maxBuffer: 1024 * 1024 },
|
||||
);
|
||||
if (r.status !== 0) return EMPTY_EFFORT;
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(String(r.stdout || ''));
|
||||
} catch { return EMPTY_EFFORT; }
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return EMPTY_EFFORT;
|
||||
const argv = Array.isArray(parsed.effort_argv)
|
||||
? parsed.effort_argv.filter((a) => typeof a === 'string' && a !== '')
|
||||
: [];
|
||||
const value = typeof parsed.effort_argv_value === 'string' && parsed.effort_argv_value
|
||||
? parsed.effort_argv_value
|
||||
: null;
|
||||
return { argv, value };
|
||||
} catch { return EMPTY_EFFORT; }
|
||||
const surface = commands.effortSurfaceForHost(cwd, host);
|
||||
const r = modelCatalog.renderEffortArgv(host, level, surface);
|
||||
return { argv: Array.isArray(r && r.argv) ? r.argv : [], value: (r && r.value) || null };
|
||||
} catch { return { argv: [], value: null }; }
|
||||
};
|
||||
const effortFor = (lane) => resolveLaneEffort(lane, configGet, renderLaneEffort);
|
||||
|
||||
/**
|
||||
* Per-lane prompt budget (#2797 semantics, preserved exactly).
|
||||
@@ -1498,7 +1484,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
||||
// so losing all of them to one bad manifest is strictly worse. Belt and braces on purpose.
|
||||
let r;
|
||||
try {
|
||||
const effort = effortFor(slug);
|
||||
const effort = effortFor(lane);
|
||||
r = resolveLanePlan({ lane, configGet, runDir, repoRoot, effortArgs: effort.argv, effortValue: effort.value });
|
||||
} catch (e) {
|
||||
return { slug, ok: false, reason: 'malformed_lane', detail: `resolver threw: ${e && e.message ? e.message : String(e)}` };
|
||||
@@ -1658,7 +1644,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
||||
`model override (it declares no modelConfigKey). The review will use the CLI's own default.\n`,
|
||||
);
|
||||
}
|
||||
const instanceEffort = effortFor(entry.slug);
|
||||
const instanceEffort = effortFor(lane);
|
||||
const overridden = resolveLanePlan({
|
||||
lane,
|
||||
configGet: (k) => (key && k === key ? instanceModel : configGet(k)),
|
||||
|
||||
@@ -233,6 +233,8 @@ const capabilities = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.antigravity",
|
||||
"modelConfigKey": "review.models.agy",
|
||||
"effortConfigKey": null,
|
||||
"defaultEffort": null,
|
||||
"handler": "antigravity"
|
||||
},
|
||||
"config": {
|
||||
@@ -650,6 +652,8 @@ const capabilities = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.claude",
|
||||
"modelConfigKey": "review.models.claude",
|
||||
"effortConfigKey": "review.effort.claude",
|
||||
"defaultEffort": "high",
|
||||
"handler": null
|
||||
},
|
||||
"config": {
|
||||
@@ -667,6 +671,11 @@ const capabilities = {
|
||||
"type": "number",
|
||||
"default": -1,
|
||||
"description": "Outer wall-clock timeout override (seconds) for the Claude reviewer lane. Unset is -1, a sentinel: 0 or a negative number is also treated as unset (a timeout has no legitimate zero/negative value), so no second sentinel is needed. Falls back to the lane's built-in timeoutFloorMs when unset."
|
||||
},
|
||||
"review.effort.claude": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Reasoning effort for the Claude reviewer lane: minimal, low, medium, high, xhigh, max, or inherit. Unset falls back to the lane's declared review default (high); inherit emits no effort argument so the CLI's own configuration decides. An unrecognized value falls back to the lane default rather than being forwarded."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1093,6 +1102,8 @@ const capabilities = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.coderabbit",
|
||||
"modelConfigKey": null,
|
||||
"effortConfigKey": null,
|
||||
"defaultEffort": null,
|
||||
"handler": null
|
||||
},
|
||||
"config": {
|
||||
@@ -1252,6 +1263,8 @@ const capabilities = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.codex",
|
||||
"modelConfigKey": "review.models.codex",
|
||||
"effortConfigKey": "review.effort.codex",
|
||||
"defaultEffort": "high",
|
||||
"handler": null
|
||||
},
|
||||
"config": {
|
||||
@@ -1269,6 +1282,11 @@ const capabilities = {
|
||||
"type": "number",
|
||||
"default": -1,
|
||||
"description": "Outer wall-clock timeout override (seconds) for the Codex reviewer lane. Unset is -1, a sentinel: 0 or a negative number is also treated as unset (a timeout has no legitimate zero/negative value), so no second sentinel is needed. Falls back to the lane's built-in timeoutFloorMs when unset."
|
||||
},
|
||||
"review.effort.codex": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Reasoning effort for the Codex reviewer lane: minimal, low, medium, high, xhigh, max, or inherit. Unset falls back to the lane's declared review default (high); inherit emits no effort argument so the CLI's own configuration decides. An unrecognized value falls back to the lane default rather than being forwarded."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1524,6 +1542,8 @@ const capabilities = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.cursor",
|
||||
"modelConfigKey": "review.models.cursor",
|
||||
"effortConfigKey": null,
|
||||
"defaultEffort": null,
|
||||
"handler": null
|
||||
},
|
||||
"config": {
|
||||
@@ -1805,6 +1825,8 @@ const capabilities = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.gemini",
|
||||
"modelConfigKey": "review.models.gemini",
|
||||
"effortConfigKey": null,
|
||||
"defaultEffort": null,
|
||||
"handler": null
|
||||
},
|
||||
"config": {
|
||||
@@ -2409,6 +2431,8 @@ const capabilities = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.kimi-code",
|
||||
"modelConfigKey": "review.models.kimi-code",
|
||||
"effortConfigKey": null,
|
||||
"defaultEffort": null,
|
||||
"handler": null
|
||||
},
|
||||
"config": {
|
||||
@@ -2521,6 +2545,8 @@ const capabilities = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.llama_cpp",
|
||||
"modelConfigKey": "review.models.llama_cpp",
|
||||
"effortConfigKey": null,
|
||||
"defaultEffort": null,
|
||||
"handler": "openai-compatible"
|
||||
},
|
||||
"config": {
|
||||
@@ -2585,6 +2611,8 @@ const capabilities = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.lm_studio",
|
||||
"modelConfigKey": "review.models.lm_studio",
|
||||
"effortConfigKey": null,
|
||||
"defaultEffort": null,
|
||||
"handler": "openai-compatible"
|
||||
},
|
||||
"config": {
|
||||
@@ -2873,6 +2901,8 @@ const capabilities = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.ollama",
|
||||
"modelConfigKey": "review.models.ollama",
|
||||
"effortConfigKey": null,
|
||||
"defaultEffort": null,
|
||||
"handler": "openai-compatible"
|
||||
},
|
||||
"config": {
|
||||
@@ -3066,6 +3096,8 @@ const capabilities = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.opencode",
|
||||
"modelConfigKey": "review.models.opencode",
|
||||
"effortConfigKey": "review.effort.opencode",
|
||||
"defaultEffort": "high",
|
||||
"handler": "opencode"
|
||||
},
|
||||
"config": {
|
||||
@@ -3083,6 +3115,11 @@ const capabilities = {
|
||||
"type": "number",
|
||||
"default": -1,
|
||||
"description": "Outer wall-clock timeout override (seconds) for the OpenCode reviewer lane. Unset is -1, a sentinel: 0 or a negative number is also treated as unset (a timeout has no legitimate zero/negative value), so no second sentinel is needed. Falls back to the lane's built-in timeoutFloorMs when unset."
|
||||
},
|
||||
"review.effort.opencode": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Reasoning effort for the OpenCode reviewer lane: minimal, low, medium, high, xhigh, max, or inherit. Unset falls back to the lane's declared review default (high); inherit emits no effort argument so the CLI's own configuration decides. An unrecognized value falls back to the lane default rather than being forwarded."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3425,6 +3462,8 @@ const capabilities = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.qwen",
|
||||
"modelConfigKey": null,
|
||||
"effortConfigKey": null,
|
||||
"defaultEffort": null,
|
||||
"handler": null
|
||||
},
|
||||
"config": {
|
||||
@@ -4869,6 +4908,7 @@ const configKeys = {
|
||||
"review.models.claude": "claude",
|
||||
"review.max_prompt_tokens_per_reviewer.claude": "claude",
|
||||
"review.timeouts.claude": "claude",
|
||||
"review.effort.claude": "claude",
|
||||
"claude_orchestration.enabled": "claude-orchestration",
|
||||
"claude_orchestration.execution_backend": "claude-orchestration",
|
||||
"claude_orchestration.min_agent_sdk_version": "claude-orchestration",
|
||||
@@ -4879,6 +4919,7 @@ const configKeys = {
|
||||
"review.models.codex": "codex",
|
||||
"review.max_prompt_tokens_per_reviewer.codex": "codex",
|
||||
"review.timeouts.codex": "codex",
|
||||
"review.effort.codex": "codex",
|
||||
"review.models.cursor": "cursor",
|
||||
"review.max_prompt_tokens_per_reviewer.cursor": "cursor",
|
||||
"workflow.drift_threshold": "drift",
|
||||
@@ -4928,6 +4969,7 @@ const configKeys = {
|
||||
"review.models.opencode": "opencode",
|
||||
"review.max_prompt_tokens_per_reviewer.opencode": "opencode",
|
||||
"review.timeouts.opencode": "opencode",
|
||||
"review.effort.opencode": "opencode",
|
||||
"workflow.pattern_mapper": "pattern-mapper",
|
||||
"profile-pipeline.enabled": "profile-pipeline",
|
||||
"review.max_prompt_tokens_per_reviewer.qwen": "qwen",
|
||||
@@ -5007,6 +5049,12 @@ const configSchema = {
|
||||
"default": -1,
|
||||
"description": "Outer wall-clock timeout override (seconds) for the Claude reviewer lane. Unset is -1, a sentinel: 0 or a negative number is also treated as unset (a timeout has no legitimate zero/negative value), so no second sentinel is needed. Falls back to the lane's built-in timeoutFloorMs when unset."
|
||||
},
|
||||
"review.effort.claude": {
|
||||
"owner": "claude",
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Reasoning effort for the Claude reviewer lane: minimal, low, medium, high, xhigh, max, or inherit. Unset falls back to the lane's declared review default (high); inherit emits no effort argument so the CLI's own configuration decides. An unrecognized value falls back to the lane default rather than being forwarded."
|
||||
},
|
||||
"claude_orchestration.enabled": {
|
||||
"owner": "claude-orchestration",
|
||||
"type": "boolean",
|
||||
@@ -5081,6 +5129,12 @@ const configSchema = {
|
||||
"default": -1,
|
||||
"description": "Outer wall-clock timeout override (seconds) for the Codex reviewer lane. Unset is -1, a sentinel: 0 or a negative number is also treated as unset (a timeout has no legitimate zero/negative value), so no second sentinel is needed. Falls back to the lane's built-in timeoutFloorMs when unset."
|
||||
},
|
||||
"review.effort.codex": {
|
||||
"owner": "codex",
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Reasoning effort for the Codex reviewer lane: minimal, low, medium, high, xhigh, max, or inherit. Unset falls back to the lane's declared review default (high); inherit emits no effort argument so the CLI's own configuration decides. An unrecognized value falls back to the lane default rather than being forwarded."
|
||||
},
|
||||
"review.models.cursor": {
|
||||
"owner": "cursor",
|
||||
"type": "string",
|
||||
@@ -5391,6 +5445,12 @@ const configSchema = {
|
||||
"default": -1,
|
||||
"description": "Outer wall-clock timeout override (seconds) for the OpenCode reviewer lane. Unset is -1, a sentinel: 0 or a negative number is also treated as unset (a timeout has no legitimate zero/negative value), so no second sentinel is needed. Falls back to the lane's built-in timeoutFloorMs when unset."
|
||||
},
|
||||
"review.effort.opencode": {
|
||||
"owner": "opencode",
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Reasoning effort for the OpenCode reviewer lane: minimal, low, medium, high, xhigh, max, or inherit. Unset falls back to the lane's declared review default (high); inherit emits no effort argument so the CLI's own configuration decides. An unrecognized value falls back to the lane default rather than being forwarded."
|
||||
},
|
||||
"workflow.pattern_mapper": {
|
||||
"owner": "pattern-mapper",
|
||||
"type": "boolean",
|
||||
@@ -5638,6 +5698,8 @@ const runtimes = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.antigravity",
|
||||
"modelConfigKey": "review.models.agy",
|
||||
"effortConfigKey": null,
|
||||
"defaultEffort": null,
|
||||
"handler": "antigravity"
|
||||
},
|
||||
"config": {
|
||||
@@ -5926,6 +5988,8 @@ const runtimes = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.claude",
|
||||
"modelConfigKey": "review.models.claude",
|
||||
"effortConfigKey": "review.effort.claude",
|
||||
"defaultEffort": "high",
|
||||
"handler": null
|
||||
},
|
||||
"config": {
|
||||
@@ -5943,6 +6007,11 @@ const runtimes = {
|
||||
"type": "number",
|
||||
"default": -1,
|
||||
"description": "Outer wall-clock timeout override (seconds) for the Claude reviewer lane. Unset is -1, a sentinel: 0 or a negative number is also treated as unset (a timeout has no legitimate zero/negative value), so no second sentinel is needed. Falls back to the lane's built-in timeoutFloorMs when unset."
|
||||
},
|
||||
"review.effort.claude": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Reasoning effort for the Claude reviewer lane: minimal, low, medium, high, xhigh, max, or inherit. Unset falls back to the lane's declared review default (high); inherit emits no effort argument so the CLI's own configuration decides. An unrecognized value falls back to the lane default rather than being forwarded."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6306,6 +6375,8 @@ const runtimes = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.codex",
|
||||
"modelConfigKey": "review.models.codex",
|
||||
"effortConfigKey": "review.effort.codex",
|
||||
"defaultEffort": "high",
|
||||
"handler": null
|
||||
},
|
||||
"config": {
|
||||
@@ -6323,6 +6394,11 @@ const runtimes = {
|
||||
"type": "number",
|
||||
"default": -1,
|
||||
"description": "Outer wall-clock timeout override (seconds) for the Codex reviewer lane. Unset is -1, a sentinel: 0 or a negative number is also treated as unset (a timeout has no legitimate zero/negative value), so no second sentinel is needed. Falls back to the lane's built-in timeoutFloorMs when unset."
|
||||
},
|
||||
"review.effort.codex": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Reasoning effort for the Codex reviewer lane: minimal, low, medium, high, xhigh, max, or inherit. Unset falls back to the lane's declared review default (high); inherit emits no effort argument so the CLI's own configuration decides. An unrecognized value falls back to the lane default rather than being forwarded."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6578,6 +6654,8 @@ const runtimes = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.cursor",
|
||||
"modelConfigKey": "review.models.cursor",
|
||||
"effortConfigKey": null,
|
||||
"defaultEffort": null,
|
||||
"handler": null
|
||||
},
|
||||
"config": {
|
||||
@@ -7084,6 +7162,8 @@ const runtimes = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.kimi-code",
|
||||
"modelConfigKey": "review.models.kimi-code",
|
||||
"effortConfigKey": null,
|
||||
"defaultEffort": null,
|
||||
"handler": null
|
||||
},
|
||||
"config": {
|
||||
@@ -7272,6 +7352,8 @@ const runtimes = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.opencode",
|
||||
"modelConfigKey": "review.models.opencode",
|
||||
"effortConfigKey": "review.effort.opencode",
|
||||
"defaultEffort": "high",
|
||||
"handler": "opencode"
|
||||
},
|
||||
"config": {
|
||||
@@ -7289,6 +7371,11 @@ const runtimes = {
|
||||
"type": "number",
|
||||
"default": -1,
|
||||
"description": "Outer wall-clock timeout override (seconds) for the OpenCode reviewer lane. Unset is -1, a sentinel: 0 or a negative number is also treated as unset (a timeout has no legitimate zero/negative value), so no second sentinel is needed. Falls back to the lane's built-in timeoutFloorMs when unset."
|
||||
},
|
||||
"review.effort.opencode": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Reasoning effort for the OpenCode reviewer lane: minimal, low, medium, high, xhigh, max, or inherit. Unset falls back to the lane's declared review default (high); inherit emits no effort argument so the CLI's own configuration decides. An unrecognized value falls back to the lane default rather than being forwarded."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7500,6 +7587,8 @@ const runtimes = {
|
||||
"requiresBinaries": [],
|
||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.qwen",
|
||||
"modelConfigKey": null,
|
||||
"effortConfigKey": null,
|
||||
"defaultEffort": null,
|
||||
"handler": null
|
||||
},
|
||||
"config": {
|
||||
|
||||
@@ -1849,9 +1849,26 @@ const KNOWN_REVIEWER_FIELDS = new Set([
|
||||
// `timeoutConfigKey` added by #3274, same optional/backward-compatible shape as
|
||||
// `modelConfigKey` above: a manifest authored before this field existed must keep validating.
|
||||
'timeoutConfigKey',
|
||||
// `effortConfigKey` / `defaultEffort` added by #4255, same optional/backward-compatible shape
|
||||
// as the two above. Lane effort used to be resolved by querying the `gsd-plan-checker` AGENT
|
||||
// through a hardcoded id, so every prompt-fed lane ran at that verifier's frontmatter effort;
|
||||
// it is a property of the review, so the lane declares it. A manifest authored before these
|
||||
// fields existed must keep validating — absent is read as "this lane declares no review
|
||||
// effort", which emits no effort argument at all.
|
||||
'effortConfigKey',
|
||||
'defaultEffort',
|
||||
'handler',
|
||||
]);
|
||||
|
||||
/**
|
||||
* The effort levels a lane may declare as its review default (#4255, #3533 vocabulary).
|
||||
*
|
||||
* `inherit` is deliberately NOT a member. It is a legitimate CONFIGURED value — it selects "emit
|
||||
* no argument, the CLI's own configuration decides" — but as a DECLARED default it would be a
|
||||
* second spelling of `null` and split one behaviour across two shapes.
|
||||
*/
|
||||
const REVIEWER_EFFORT_LEVELS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
|
||||
|
||||
/**
|
||||
* The closed `runtime.hostBehaviors` vocabulary (ADR-1016, closed via #2801).
|
||||
*
|
||||
@@ -2407,6 +2424,39 @@ function validateReviewerBodyFields(cap) {
|
||||
);
|
||||
}
|
||||
|
||||
// OPTIONAL, mirroring the two keys above (#4255). Absent/null means "this lane declares no
|
||||
// review effort", which is the shape that emits no effort argument at all — the correct state
|
||||
// for the nine lanes with no effort channel to feed. An empty string is neither.
|
||||
if (r.effortConfigKey !== undefined && r.effortConfigKey !== null &&
|
||||
(typeof r.effortConfigKey !== 'string' || r.effortConfigKey.length === 0)) {
|
||||
errors.push(
|
||||
ctx + ' reviewer.effortConfigKey must be a dotted config key or null ' +
|
||||
'(got: ' + describeValue(r.effortConfigKey) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// A declared default must be a level the effort axis knows, or the lane would render an
|
||||
// argument the reviewer CLI rejects and kill itself on every run. Closed vocabulary, checked
|
||||
// here rather than at resolution time so a malformed manifest fails at the trust boundary.
|
||||
if (r.defaultEffort !== undefined && r.defaultEffort !== null
|
||||
&& !(typeof r.defaultEffort === 'string' && REVIEWER_EFFORT_LEVELS.has(r.defaultEffort))) {
|
||||
errors.push(
|
||||
ctx + ' reviewer.defaultEffort must be one of ' +
|
||||
Array.from(REVIEWER_EFFORT_LEVELS).join('/') + ' or null ' +
|
||||
'(got: ' + describeValue(r.defaultEffort) + ')',
|
||||
);
|
||||
}
|
||||
|
||||
// A default with nothing to configure it through is a value the operator cannot change — the
|
||||
// shape #4255 exists to end. Declared together or not at all.
|
||||
if ((r.defaultEffort !== undefined && r.defaultEffort !== null)
|
||||
&& (r.effortConfigKey === undefined || r.effortConfigKey === null)) {
|
||||
errors.push(
|
||||
ctx + ' reviewer.defaultEffort is declared without an effortConfigKey, so the level ' +
|
||||
'could never be overridden',
|
||||
);
|
||||
}
|
||||
|
||||
// `null` is the declared "no per-lane budget"; an empty string is not.
|
||||
if (r.promptBudgetKey !== null && (typeof r.promptBudgetKey !== 'string' || r.promptBudgetKey.length === 0)) {
|
||||
errors.push(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user