enhance(#2295): record the resolved model per reviewer in REVIEWS.md frontmatter (#3649)

* test(#2295): failing-first coverage for per-lane resolved-model recording

* feat(#2295): record the resolved model per reviewer lane

* docs(#2295): document the recorded reviewer model and its provenance

* fix(#2295): refuse control characters in a recorded model value

* test(#2295): correct watermark assertions for the widened mark shape

* fix(#2295): anchor the role-manipulation injection pattern at a word boundary

* feat(#2295): record the applied reasoning effort in the model value

* chore(#2295): backfill changeset pr number

* chore(#2295): restore em-dash in changeset body

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-19 08:59:37 -04:00
committed by GitHub
parent 1adf6d2245
commit 1bf73d957b
15 changed files with 1328 additions and 75 deletions

View File

@@ -1307,30 +1307,45 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
return;
}
// Effort argv is resolved per lane by the host's own execution policy, exactly as the legs did
// via `resolve-execution … --pick effort_argv_string`. A lane whose slug is not a known host
// simply gets none.
// Resolved through the SAME `resolve-execution` surface the bash legs used
// (`--host <slug> --pick effort_argv_string`), 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 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.
//
// 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) => {
try {
const r = cp.spawnSync(
process.execPath,
[__filename, 'query', 'resolve-execution', 'gsd-plan-checker',
// NOT `--raw`: that prints the resolved EFFORT ('low'), ignoring --pick. The picked
// field is what carries the host-specific syntax ('--effort low' for claude,
// '-c model_reasoning_effort=low' for codex), which is the whole point of asking.
'--host', slug, '--pick', 'effort_argv_string'],
[__filename, 'query', 'resolve-execution', 'gsd-plan-checker', '--host', slug],
{ cwd, encoding: 'utf8', timeout: 15000, killSignal: 'SIGKILL', maxBuffer: 1024 * 1024 },
);
if (r.status !== 0) return [];
const s = String(r.stdout || '').trim();
return s ? s.split(/\s+/).filter(Boolean) : [];
} catch { return []; }
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; }
};
/**
@@ -1363,7 +1378,8 @@ 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 {
r = resolveLanePlan({ lane, configGet, runDir, repoRoot, effortArgs: effortFor(slug) });
const effort = effortFor(slug);
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)}` };
}
@@ -1522,12 +1538,14 @@ 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 overridden = resolveLanePlan({
lane,
configGet: (k) => (key && k === key ? instanceModel : configGet(k)),
runDir,
repoRoot,
effortArgs: effortFor(entry.slug),
effortArgs: instanceEffort.argv,
effortValue: instanceEffort.value,
});
if (overridden.ok) {
// Preserve any instance retargeting already applied above.

View File

@@ -423,12 +423,31 @@ names, each gets its own `## <Adapter> Review (<instance>)` section, and ≥2 sa
instances print a one-line shared-adapter caveat. Format in
`gsd-core/references/reviewer-instances.md`.
**Resolved model (#2295):** each lane's `review-lane invoke --json` line in
`{run_dir}/gsd-review-lane-results.jsonl` carries a `model` object; render `models:` from
its `value` and `model_sources:` from its `source`. Both maps carry exactly one entry per
reviewer that appears in `reviewers:` — the two key sets always match. Write the literal
`unknown` rather than omitting a key: an omitted key is indistinguishable from the feature
not having run, and a reader must be able to tell *no model recorded* from *nothing to
look at*. Emit every `models:`/`model_sources:` value as a DOUBLE-QUOTED YAML scalar — a
legitimate model id can contain `:` (`llama3:70b`, `qwen2.5:7b`), which is unquotable as a
bare scalar; a control character is already refused at the recording seam, so quoting is
what closes the remaining `:`/`#`/leading-`-` cases. When GSD applied a reasoning effort to
a lane, its `value` already carries a `(reasoning=<level>)` suffix (e.g.
`gpt-5.6-sol (reasoning=high)`) — render it as-is, without re-deriving or re-formatting it.
```markdown
---
phase: {N}
reviewers: [gemini, claude, codex, coderabbit, opencode, qwen, cursor, antigravity, ollama, lm_studio, llama_cpp] # populate at runtime with only the reviewers actually invoked
reviewed_at: {ISO timestamp}
plans_reviewed: [{list of PLAN.md files}]
models: # resolved model per reviewer; `unknown` when not recoverable
codex: "gpt-5.6-sol (reasoning=low)"
antigravity: "unknown"
model_sources: # how each value above was determined
codex: "banner"
antigravity: "unknown"
trimmed_reviewers: # only present if at least one reviewer was trimmed
ollama:
budget: 6000