feat(#3653): add review.models.cursor — wire modelArg/modelConfigKey for the cursor reviewer lane (#4160)
* feat(#3653): add review.models.cursor — wire modelArg/modelConfigKey for the cursor reviewer lane cursor-agent exposes --model (204 selectable models) but the cursor lane declared modelArg: null / modelConfigKey: null, so review.models.cursor was rejected as an unknown config key and the #1517 reviewer-instances escape hatch silently discarded a configured model at modelExpansion. Wire the lane the same way codex already is: inject {{model}} into args right after -p, set modelArg to --model, and declare modelConfigKey as review.models.cursor plus its config schema entry. An unconfigured lane still invokes byte-identically to today. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(#3653): add changeset for review.models.cursor Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3653): update co-change surfaces that assumed cursor has no model key gsd-test surfaced three surfaces still hardcoding "cursor declares no modelConfigKey", broken by wiring review.models.cursor: - tests/reviewer-config-federation.test.cjs: the #3691-narrows-#2797 invariant test listed cursor among lanes that must own no model key. - tests/settings-integrations.test.cjs: the #3651 keyless-lane test listed cursor as keyless, including a live config-set assertion that now correctly succeeds instead of failing (swapped to qwen). - gsd-core/workflows/settings-integrations.md: the settable-keys enumeration and two prose call-outs still named cursor as keyless. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * chore(#3653): acknowledge deliberate growth of settings-integrations.md settings-integrations.md grew 4 bytes because it now enumerates review.models.cursor as a settable key alongside the other reviewer lanes, matching the modelConfigKey wired for cursor in this PR. Emitted-Drift-Ack-Growth: settings-integrations.md — adds review.models.cursor to the settable-keys enumeration and removes cursor from the two keyless-lane call-outs, matching #3653's modelConfigKey change Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * chore(#3653): fix malformed Emitted-Drift-Ack-Growth trailer The previous commit's trailer was separated from Co-Authored-By by a blank line, splitting it into an earlier, non-trailer paragraph — git's trailer parser only recognizes the last contiguous block. Restating it here immediately adjacent to Co-Authored-By so both parse as trailers. Emitted-Drift-Ack-Growth: settings-integrations.md — adds review.models.cursor to the settable-keys enumeration and removes cursor from the two keyless-lane call-outs, matching #3653's modelConfigKey change Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(#3653): backfill changeset pr number pr:0 -> pr:4160 now that https://github.com/open-gsd/gsd-core/pull/4160 exists. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/merry-wasps-sprint.md
Normal file
5
.changeset/merry-wasps-sprint.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
type: Added
|
||||||
|
pr: 4160
|
||||||
|
---
|
||||||
|
**`review.models.cursor` now pins the Cursor reviewer lane's model** — the lane previously discarded any configured model because it declared no `--model` flag; it now injects one exactly like the `codex` lane. (#3653)
|
||||||
@@ -129,6 +129,7 @@
|
|||||||
"binary": "cursor-agent",
|
"binary": "cursor-agent",
|
||||||
"args": [
|
"args": [
|
||||||
"-p",
|
"-p",
|
||||||
|
"{{model}}",
|
||||||
"--mode",
|
"--mode",
|
||||||
"ask",
|
"ask",
|
||||||
"--trust",
|
"--trust",
|
||||||
@@ -138,7 +139,7 @@
|
|||||||
],
|
],
|
||||||
"promptChannel": "argv-file-ref",
|
"promptChannel": "argv-file-ref",
|
||||||
"outputChannel": "stdout",
|
"outputChannel": "stdout",
|
||||||
"modelArg": null,
|
"modelArg": "--model",
|
||||||
"effortChannel": "none"
|
"effortChannel": "none"
|
||||||
},
|
},
|
||||||
"timeoutFloorMs": 900000,
|
"timeoutFloorMs": 900000,
|
||||||
@@ -148,10 +149,15 @@
|
|||||||
"evidenceClass": "source-grounded",
|
"evidenceClass": "source-grounded",
|
||||||
"requiresBinaries": [],
|
"requiresBinaries": [],
|
||||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.cursor",
|
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.cursor",
|
||||||
"modelConfigKey": null,
|
"modelConfigKey": "review.models.cursor",
|
||||||
"handler": null
|
"handler": null
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
"review.models.cursor": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"description": "Model passed to the Cursor reviewer lane."
|
||||||
|
},
|
||||||
"review.max_prompt_tokens_per_reviewer.cursor": {
|
"review.max_prompt_tokens_per_reviewer.cursor": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"default": -1,
|
"default": -1,
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ The key suffix is **not** always the lane slug. Each lane declares the config ke
|
|||||||
| `review.models.codex` | string | `null` | Model id for Codex review (injected into --model), e.g. `"gpt-5"` |
|
| `review.models.codex` | string | `null` | Model id for Codex review (injected into --model), e.g. `"gpt-5"` |
|
||||||
| `review.models.gemini` | string | `null` | Model id for Gemini review (injected into -m), e.g. `"gemini-2.5-pro"` |
|
| `review.models.gemini` | string | `null` | Model id for Gemini review (injected into -m), e.g. `"gemini-2.5-pro"` |
|
||||||
| `review.models.opencode` | string | `null` | Model id for OpenCode review (injected into --model), e.g. `"claude-sonnet-4"` |
|
| `review.models.opencode` | string | `null` | Model id for OpenCode review (injected into --model), e.g. `"claude-sonnet-4"` |
|
||||||
|
| `review.models.cursor` | string | `null` | Model id for Cursor review (injected into --model), e.g. `"cursor-grok-4.5-high"` |
|
||||||
| `review.models.kimi-code` | string | `null` | Model id for Kimi Code review (injected into -m) |
|
| `review.models.kimi-code` | string | `null` | Model id for Kimi Code review (injected into -m) |
|
||||||
|
|
||||||
### Resolved model recording (#2295)
|
### Resolved model recording (#2295)
|
||||||
@@ -269,7 +270,7 @@ which schema validates them moved.
|
|||||||
One consequence follows: `<cli>` must now name a **declared reviewer lane**. Previously any slug
|
One consequence follows: `<cli>` must now name a **declared reviewer lane**. Previously any slug
|
||||||
matching `[a-zA-Z0-9_-]+` was accepted, so a typo or a key left over from a removed reviewer
|
matching `[a-zA-Z0-9_-]+` was accepted, so a typo or a key left over from a removed reviewer
|
||||||
validated silently and was never read. Such a key is now rejected by `config-set`. The declared
|
validated silently and was never read. Such a key is now rejected by `config-set`. The declared
|
||||||
lanes are `gemini`, `claude`, `codex`, `opencode`, `agy` (the Antigravity lane — its key suffix is
|
lanes are `gemini`, `claude`, `codex`, `opencode`, `cursor`, `agy` (the Antigravity lane — its key suffix is
|
||||||
the CLI's own name, not the lane slug), `ollama`, `lm_studio` and `llama_cpp`.
|
the CLI's own name, not the lane slug), `ollama`, `lm_studio` and `llama_cpp`.
|
||||||
|
|
||||||
The same applies to `review.max_prompt_tokens_per_reviewer.<slug>`. `review.max_prompt_tokens`
|
The same applies to `review.max_prompt_tokens_per_reviewer.<slug>`. `review.max_prompt_tokens`
|
||||||
@@ -288,9 +289,10 @@ falls back to that lane's built-in floor. For the `antigravity` lane specificall
|
|||||||
derives its native `agy --print-timeout` flag (roughly 60 seconds under the configured outer
|
derives its native `agy --print-timeout` flag (roughly 60 seconds under the configured outer
|
||||||
value), so raising `review.timeouts.antigravity` raises both bounds together — this is how to fix
|
value), so raising `review.timeouts.antigravity` raises both bounds together — this is how to fix
|
||||||
a reviewer lane being killed mid-run on a large plan set: `gsd config-set review.timeouts.antigravity 900`.
|
a reviewer lane being killed mid-run on a large plan set: `gsd config-set review.timeouts.antigravity 900`.
|
||||||
Three lanes — `qwen`, `cursor`, and `coderabbit` — take neither a model flag nor a host and do not
|
Two lanes — `qwen` and `coderabbit` — take neither a model flag nor a host and do not federate a
|
||||||
federate a timeout key either, matching the same narrow key-ownership invariant their
|
timeout key either, matching the same narrow key-ownership invariant their `review.models.*`/host
|
||||||
`review.models.*`/host keys already follow (each owns only its own prompt-budget key).
|
keys already follow (each owns only its own prompt-budget key). `cursor` gained a model flag
|
||||||
|
(`review.models.cursor`, #3653) but still owns no federated timeout key of its own.
|
||||||
|
|
||||||
### Reviewer defaults for `/gsd-review`
|
### Reviewer defaults for `/gsd-review`
|
||||||
|
|
||||||
@@ -1280,6 +1282,7 @@ Configure per-CLI model selection for `/gsd-review`. When set, overrides the CLI
|
|||||||
| `review.models.claude` | string | (CLI default) | Model used when `--claude` reviewer is invoked |
|
| `review.models.claude` | string | (CLI default) | Model used when `--claude` reviewer is invoked |
|
||||||
| `review.models.codex` | string | (CLI default) | Model used when `--codex` reviewer is invoked |
|
| `review.models.codex` | string | (CLI default) | Model used when `--codex` reviewer is invoked |
|
||||||
| `review.models.opencode` | string | (CLI default) | Model used when `--opencode` reviewer is invoked |
|
| `review.models.opencode` | string | (CLI default) | Model used when `--opencode` reviewer is invoked |
|
||||||
|
| `review.models.cursor` | string | (CLI default) | Model used when the `--cursor` reviewer is invoked (injected into `--model`) |
|
||||||
| `review.models.agy` | string | (CLI default) | Model used when the `--antigravity` / `--agy` reviewer is invoked. The key suffix is the CLI's own name (`agy`), not the lane slug — the lane declares which key it reads, so the two need not match |
|
| `review.models.agy` | string | (CLI default) | Model used when the `--antigravity` / `--agy` reviewer is invoked. The key suffix is the CLI's own name (`agy`), not the lane slug — the lane declares which key it reads, so the two need not match |
|
||||||
| `review.models.kimi-code` | string | (CLI default) | Model used when the `--kimi-code` reviewer is invoked (injected into `-m`) |
|
| `review.models.kimi-code` | string | (CLI default) | Model used when the `--kimi-code` reviewer is invoked (injected into `-m`) |
|
||||||
| `review.models.ollama` | string | (server default) | Model name passed to Ollama when `--ollama` reviewer is invoked. If unset, the first available model reported by the server is used (e.g. `llama3`). Set to a specific tag: `gsd config-set review.models.ollama codellama` |
|
| `review.models.ollama` | string | (server default) | Model name passed to Ollama when `--ollama` reviewer is invoked. If unset, the first available model reported by the server is used (e.g. `llama3`). Set to a specific tag: `gsd config-set review.models.ollama codellama` |
|
||||||
|
|||||||
@@ -1480,6 +1480,7 @@ const capabilities = {
|
|||||||
"binary": "cursor-agent",
|
"binary": "cursor-agent",
|
||||||
"args": [
|
"args": [
|
||||||
"-p",
|
"-p",
|
||||||
|
"{{model}}",
|
||||||
"--mode",
|
"--mode",
|
||||||
"ask",
|
"ask",
|
||||||
"--trust",
|
"--trust",
|
||||||
@@ -1489,7 +1490,7 @@ const capabilities = {
|
|||||||
],
|
],
|
||||||
"promptChannel": "argv-file-ref",
|
"promptChannel": "argv-file-ref",
|
||||||
"outputChannel": "stdout",
|
"outputChannel": "stdout",
|
||||||
"modelArg": null,
|
"modelArg": "--model",
|
||||||
"effortChannel": "none"
|
"effortChannel": "none"
|
||||||
},
|
},
|
||||||
"timeoutFloorMs": 900000,
|
"timeoutFloorMs": 900000,
|
||||||
@@ -1499,10 +1500,15 @@ const capabilities = {
|
|||||||
"evidenceClass": "source-grounded",
|
"evidenceClass": "source-grounded",
|
||||||
"requiresBinaries": [],
|
"requiresBinaries": [],
|
||||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.cursor",
|
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.cursor",
|
||||||
"modelConfigKey": null,
|
"modelConfigKey": "review.models.cursor",
|
||||||
"handler": null
|
"handler": null
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
"review.models.cursor": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"description": "Model passed to the Cursor reviewer lane."
|
||||||
|
},
|
||||||
"review.max_prompt_tokens_per_reviewer.cursor": {
|
"review.max_prompt_tokens_per_reviewer.cursor": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"default": -1,
|
"default": -1,
|
||||||
@@ -4833,6 +4839,7 @@ const configKeys = {
|
|||||||
"review.models.codex": "codex",
|
"review.models.codex": "codex",
|
||||||
"review.max_prompt_tokens_per_reviewer.codex": "codex",
|
"review.max_prompt_tokens_per_reviewer.codex": "codex",
|
||||||
"review.timeouts.codex": "codex",
|
"review.timeouts.codex": "codex",
|
||||||
|
"review.models.cursor": "cursor",
|
||||||
"review.max_prompt_tokens_per_reviewer.cursor": "cursor",
|
"review.max_prompt_tokens_per_reviewer.cursor": "cursor",
|
||||||
"workflow.drift_threshold": "drift",
|
"workflow.drift_threshold": "drift",
|
||||||
"workflow.drift_action": "drift",
|
"workflow.drift_action": "drift",
|
||||||
@@ -5024,6 +5031,12 @@ const configSchema = {
|
|||||||
"default": -1,
|
"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."
|
"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.models.cursor": {
|
||||||
|
"owner": "cursor",
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"description": "Model passed to the Cursor reviewer lane."
|
||||||
|
},
|
||||||
"review.max_prompt_tokens_per_reviewer.cursor": {
|
"review.max_prompt_tokens_per_reviewer.cursor": {
|
||||||
"owner": "cursor",
|
"owner": "cursor",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
@@ -6494,6 +6507,7 @@ const runtimes = {
|
|||||||
"binary": "cursor-agent",
|
"binary": "cursor-agent",
|
||||||
"args": [
|
"args": [
|
||||||
"-p",
|
"-p",
|
||||||
|
"{{model}}",
|
||||||
"--mode",
|
"--mode",
|
||||||
"ask",
|
"ask",
|
||||||
"--trust",
|
"--trust",
|
||||||
@@ -6503,7 +6517,7 @@ const runtimes = {
|
|||||||
],
|
],
|
||||||
"promptChannel": "argv-file-ref",
|
"promptChannel": "argv-file-ref",
|
||||||
"outputChannel": "stdout",
|
"outputChannel": "stdout",
|
||||||
"modelArg": null,
|
"modelArg": "--model",
|
||||||
"effortChannel": "none"
|
"effortChannel": "none"
|
||||||
},
|
},
|
||||||
"timeoutFloorMs": 900000,
|
"timeoutFloorMs": 900000,
|
||||||
@@ -6513,10 +6527,15 @@ const runtimes = {
|
|||||||
"evidenceClass": "source-grounded",
|
"evidenceClass": "source-grounded",
|
||||||
"requiresBinaries": [],
|
"requiresBinaries": [],
|
||||||
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.cursor",
|
"promptBudgetKey": "review.max_prompt_tokens_per_reviewer.cursor",
|
||||||
"modelConfigKey": null,
|
"modelConfigKey": "review.models.cursor",
|
||||||
"handler": null
|
"handler": null
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
"review.models.cursor": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"description": "Model passed to the Cursor reviewer lane."
|
||||||
|
},
|
||||||
"review.max_prompt_tokens_per_reviewer.cursor": {
|
"review.max_prompt_tokens_per_reviewer.cursor": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"default": -1,
|
"default": -1,
|
||||||
|
|||||||
@@ -163,12 +163,13 @@ namespace — any other slug fails with `Unknown config key`.
|
|||||||
Settable keys (the shipped registry's model-bearing lanes):
|
Settable keys (the shipped registry's model-bearing lanes):
|
||||||
|
|
||||||
`review.models.agy` (Antigravity), `review.models.claude`, `review.models.codex`,
|
`review.models.agy` (Antigravity), `review.models.claude`, `review.models.codex`,
|
||||||
`review.models.gemini`, `review.models.kimi-code`, `review.models.llama_cpp`,
|
`review.models.cursor`, `review.models.gemini`, `review.models.kimi-code`,
|
||||||
`review.models.lm_studio`, `review.models.ollama`, `review.models.opencode`.
|
`review.models.llama_cpp`, `review.models.lm_studio`, `review.models.ollama`,
|
||||||
|
`review.models.opencode`.
|
||||||
|
|
||||||
Reviewer lanes `cursor`, `qwen`, and `coderabbit` declare no `modelConfigKey` —
|
Reviewer lanes `qwen` and `coderabbit` declare no `modelConfigKey` — there is
|
||||||
there is nothing to configure for them here (whether they should have a
|
nothing to configure for them here (whether they should have a per-lane model
|
||||||
per-lane model key is a separate question, out of scope for this workflow).
|
key is a separate question, out of scope for this workflow).
|
||||||
If the user asks for one of those, say exactly that and skip.
|
If the user asks for one of those, say exactly that and skip.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -208,8 +209,8 @@ If it is not one of the settable keys, print:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
Rejected: review.models.<slug> is not settable — only the reviewer lanes whose
|
Rejected: review.models.<slug> is not settable — only the reviewer lanes whose
|
||||||
keys are enumerated above can be configured here. (cursor, qwen, and
|
keys are enumerated above can be configured here. (qwen and coderabbit have no
|
||||||
coderabbit have no per-lane model key.)
|
per-lane model key.)
|
||||||
```
|
```
|
||||||
|
|
||||||
and re-prompt.
|
and re-prompt.
|
||||||
|
|||||||
@@ -195,10 +195,11 @@ interface ReviewerLaneCommon {
|
|||||||
* config value that is not a positive finite number, falls back to `timeoutFloorMs` unchanged.
|
* config value that is not a positive finite number, falls back to `timeoutFloorMs` unchanged.
|
||||||
* For a lane whose `args` template ALSO carries a native tool-side timeout (antigravity's
|
* For a lane whose `args` template ALSO carries a native tool-side timeout (antigravity's
|
||||||
* `--print-timeout`, via the `{{nativeTimeout}}` ARGV_PLACEHOLDER), the resolved value feeds both
|
* `--print-timeout`, via the `{{nativeTimeout}}` ARGV_PLACEHOLDER), the resolved value feeds both
|
||||||
* levels — see `resolveLanePlan`'s expansion of that token. Three lanes — `qwen`, `cursor`,
|
* levels — see `resolveLanePlan`'s expansion of that token. Two lanes — `qwen` and `coderabbit` —
|
||||||
* `coderabbit` — accept neither a model flag nor a host and own no `review.timeouts.<slug>` key
|
* accept neither a model flag nor a host and own no `review.timeouts.<slug>` key either, matching
|
||||||
* either, matching the same narrow key-ownership invariant `modelConfigKey` already follows for
|
* the same narrow key-ownership invariant `modelConfigKey` already follows for them (#3691 narrows
|
||||||
* them (#3691 narrows #2797).
|
* #2797). `cursor` gained a model flag (`review.models.cursor`, #3653) but still owns no
|
||||||
|
* `review.timeouts.cursor` key of its own.
|
||||||
*/
|
*/
|
||||||
timeoutConfigKey: string | null;
|
timeoutConfigKey: string | null;
|
||||||
emptyOutput: EmptyOutputPolicy;
|
emptyOutput: EmptyOutputPolicy;
|
||||||
@@ -431,10 +432,10 @@ export const REVIEWER_LANES: ReadonlyArray<ReviewerLane> = Object.freeze([
|
|||||||
probe: { kind: 'command-exists', binary: 'cursor-agent' },
|
probe: { kind: 'command-exists', binary: 'cursor-agent' },
|
||||||
invoke: {
|
invoke: {
|
||||||
binary: 'cursor-agent',
|
binary: 'cursor-agent',
|
||||||
args: ['-p', '--mode', 'ask', '--trust', '--output-format', 'text', '{{prompt}}'],
|
args: ['-p', '{{model}}', '--mode', 'ask', '--trust', '--output-format', 'text', '{{prompt}}'],
|
||||||
promptChannel: 'argv-file-ref',
|
promptChannel: 'argv-file-ref',
|
||||||
outputChannel: 'stdout',
|
outputChannel: 'stdout',
|
||||||
modelArg: null,
|
modelArg: '--model',
|
||||||
effortChannel: 'none',
|
effortChannel: 'none',
|
||||||
},
|
},
|
||||||
timeoutFloorMs: 900_000,
|
timeoutFloorMs: 900_000,
|
||||||
@@ -444,7 +445,8 @@ export const REVIEWER_LANES: ReadonlyArray<ReviewerLane> = Object.freeze([
|
|||||||
evidenceClass: 'source-grounded',
|
evidenceClass: 'source-grounded',
|
||||||
requiresBinaries: [],
|
requiresBinaries: [],
|
||||||
promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.cursor',
|
promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.cursor',
|
||||||
modelConfigKey: null,
|
// #3653: cursor-agent exposes --model (204 selectable models); wired the same as codex.
|
||||||
|
modelConfigKey: 'review.models.cursor',
|
||||||
handler: null,
|
handler: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const FULL_CONFIG = {
|
|||||||
'review.models.claude': 'C',
|
'review.models.claude': 'C',
|
||||||
'review.models.codex': 'X',
|
'review.models.codex': 'X',
|
||||||
'review.models.opencode': 'O',
|
'review.models.opencode': 'O',
|
||||||
|
'review.models.cursor': 'U',
|
||||||
'review.models.agy': 'A',
|
'review.models.agy': 'A',
|
||||||
'review.models.kimi-code': 'K',
|
'review.models.kimi-code': 'K',
|
||||||
'review.models.ollama': 'M',
|
'review.models.ollama': 'M',
|
||||||
@@ -85,7 +86,7 @@ const GOLDEN = [
|
|||||||
{ slug: 'coderabbit', binary: 'coderabbit', argv: ['review', '--prompt-only'], stdin: false, out: 'stdout', timeout: 360000 },
|
{ slug: 'coderabbit', binary: 'coderabbit', argv: ['review', '--prompt-only'], stdin: false, out: 'stdout', timeout: 360000 },
|
||||||
{ slug: 'opencode', binary: 'opencode', argv: ['run', '--model', 'O', '--effort', 'high', '--format', 'json', '-'], stdin: true, out: 'stdout', timeout: 660000 },
|
{ slug: 'opencode', binary: 'opencode', argv: ['run', '--model', 'O', '--effort', 'high', '--format', 'json', '-'], stdin: true, out: 'stdout', timeout: 660000 },
|
||||||
{ slug: 'qwen', binary: 'qwen', argv: ['-'], stdin: true, out: 'stdout', timeout: 900000 },
|
{ slug: 'qwen', binary: 'qwen', argv: ['-'], stdin: true, out: 'stdout', timeout: 900000 },
|
||||||
{ slug: 'cursor', binary: 'cursor-agent', argv: ['-p', '--mode', 'ask', '--trust', '--output-format', 'text', FILE_REF], stdin: false, out: 'stdout', timeout: 900000 },
|
{ slug: 'cursor', binary: 'cursor-agent', argv: ['-p', '--model', 'U', '--mode', 'ask', '--trust', '--output-format', 'text', FILE_REF], stdin: false, out: 'stdout', timeout: 900000 },
|
||||||
// resolveLanePlan fully resolves {{nativeTimeout}} itself (#3274) — this row proves the
|
// resolveLanePlan fully resolves {{nativeTimeout}} itself (#3274) — this row proves the
|
||||||
// unconfigured default reproduces the original literal exactly.
|
// unconfigured default reproduces the original literal exactly.
|
||||||
{ slug: 'antigravity', binary: 'agy', argv: ['--print-timeout', '540s', '--model', 'A', '-p', FILE_REF], stdin: false, out: 'stdout', timeout: 600000 },
|
{ slug: 'antigravity', binary: 'agy', argv: ['--print-timeout', '540s', '--model', 'A', '-p', FILE_REF], stdin: false, out: 'stdout', timeout: 600000 },
|
||||||
@@ -277,14 +278,35 @@ describe('reviewer lane invocation — model resolution', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('a lane declaring no model key emits no model argument', () => {
|
test('a lane declaring no model key emits no model argument', () => {
|
||||||
for (const slug of ['qwen', 'cursor', 'coderabbit']) {
|
for (const slug of ['qwen', 'coderabbit']) {
|
||||||
const lane = REVIEWER_LANES.find((l) => l.slug === slug);
|
const lane = REVIEWER_LANES.find((l) => l.slug === slug);
|
||||||
assert.equal(lane.modelConfigKey, null, `${slug} should declare no model key`);
|
assert.equal(lane.modelConfigKey, null, `${slug} should declare no model key`);
|
||||||
const r = resolve(slug, { config: { 'review.models.qwen': 'X', 'review.models.cursor': 'X' } });
|
const r = resolve(slug, { config: { 'review.models.qwen': 'X' } });
|
||||||
assert.ok(!r.plan.argv.includes('X'));
|
assert.ok(!r.plan.argv.includes('X'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('cursor now declares a model key and arg (#3653) — no longer null', () => {
|
||||||
|
const lane = REVIEWER_LANES.find((l) => l.slug === 'cursor');
|
||||||
|
assert.equal(lane.modelConfigKey, 'review.models.cursor');
|
||||||
|
assert.equal(lane.invoke.modelArg, '--model');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an unconfigured cursor lane invokes exactly as it does today (#3653, byte-identical)', () => {
|
||||||
|
const r = resolve('cursor', { config: {} });
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
r.plan.argv,
|
||||||
|
['-p', '--mode', 'ask', '--trust', '--output-format', 'text', r.plan.argv[r.plan.argv.length - 1]],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a configured review.models.cursor reaches --model, positioned right after -p (#3653)', () => {
|
||||||
|
const r = resolve('cursor', { config: { 'review.models.cursor': 'cursor-grok-4.5-high' } });
|
||||||
|
const i = r.plan.argv.indexOf('-p');
|
||||||
|
assert.equal(r.plan.argv[i + 1], '--model');
|
||||||
|
assert.equal(r.plan.argv[i + 2], 'cursor-grok-4.5-high');
|
||||||
|
});
|
||||||
|
|
||||||
test('unset, empty, whitespace and the literal string "null" all mean unconfigured', () => {
|
test('unset, empty, whitespace and the literal string "null" all mean unconfigured', () => {
|
||||||
// `"null"` is the four literal characters `config-get --raw` prints for a missing key — every
|
// `"null"` is the four literal characters `config-get --raw` prints for a missing key — every
|
||||||
// bash leg tested for it. A config written by an older workflow can still contain it.
|
// bash leg tested for it. A config written by an older workflow can still contain it.
|
||||||
|
|||||||
@@ -102,17 +102,20 @@ describe('reviewer config federation — provenance actually moved (#2797)', ()
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('a lane with no model flag and no host owns no MODEL or HOST key (#3691 narrows #2797)', () => {
|
test('a lane with no model flag and no host owns no MODEL or HOST key (#3691 narrows #2797)', () => {
|
||||||
// Absent-safe (ADR-2782 D4): qwen, cursor and coderabbit take neither a model
|
// Absent-safe (ADR-2782 D4): qwen and coderabbit take neither a model
|
||||||
// argument nor a host. Under #2797 that meant "declares nothing" — the only
|
// argument nor a host. Under #2797 that meant "declares nothing" — the only
|
||||||
// way a lane owned a config key was via a model flag or a host. #3691 gave
|
// way a lane owned a config key was via a model flag or a host. #3691 gave
|
||||||
// every CLI lane a `review.max_prompt_tokens_per_reviewer.<slug>` key, a
|
// every CLI lane a `review.max_prompt_tokens_per_reviewer.<slug>` key, a
|
||||||
// third legitimate reason to own a key, so qwen/cursor/coderabbit now
|
// third legitimate reason to own a key, so qwen/coderabbit now legitimately
|
||||||
// legitimately own their own budget key. The part of the #2797 invariant
|
// own their own budget key. `cursor` gained a real model flag (#3653) and
|
||||||
// that still holds — a lane must never own a MODEL or HOST key, or another
|
// now legitimately owns `review.models.cursor` too, so it is excluded from
|
||||||
// lane's budget key, it has no use for — is what this asserts directly.
|
// this loop. The part of the #2797 invariant that still holds — a lane must
|
||||||
|
// never own a MODEL or HOST key it has no use for, or another lane's budget
|
||||||
|
// key — is what this asserts directly for the two lanes that still have
|
||||||
|
// neither.
|
||||||
for (const [key, entry] of Object.entries(registry.configSchema || {})) {
|
for (const [key, entry] of Object.entries(registry.configSchema || {})) {
|
||||||
const owner = entry && entry.owner;
|
const owner = entry && entry.owner;
|
||||||
if (!['qwen', 'cursor', 'coderabbit'].includes(owner)) continue;
|
if (!['qwen', 'coderabbit'].includes(owner)) continue;
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!key.startsWith('review.models.') && !key.endsWith('_host'),
|
!key.startsWith('review.models.') && !key.endsWith('_host'),
|
||||||
`${owner} must not own a model or host key, but owns "${key}"`,
|
`${owner} must not own a model or host key, but owns "${key}"`,
|
||||||
|
|||||||
@@ -211,7 +211,8 @@ describe('#3651 workflow — review.models settable-set rule', () => {
|
|||||||
test('keyless reviewer lanes have no settable review.models key (#3651)', (t) => {
|
test('keyless reviewer lanes have no settable review.models key (#3651)', (t) => {
|
||||||
// Lanes whose capability declares modelConfigKey: null — the workflow used
|
// Lanes whose capability declares modelConfigKey: null — the workflow used
|
||||||
// to walk users into writing these keys, and config-set rejects them.
|
// to walk users into writing these keys, and config-set rejects them.
|
||||||
for (const keyless of ['cursor', 'qwen', 'coderabbit']) {
|
// `cursor` gained a real modelConfigKey (#3653) and is no longer keyless.
|
||||||
|
for (const keyless of ['qwen', 'coderabbit']) {
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!isValidConfigKey(`review.models.${keyless}`),
|
!isValidConfigKey(`review.models.${keyless}`),
|
||||||
`review.models.${keyless} must not validate (lane declares no modelConfigKey)`
|
`review.models.${keyless} must not validate (lane declares no modelConfigKey)`
|
||||||
@@ -227,7 +228,7 @@ describe('#3651 workflow — review.models settable-set rule', () => {
|
|||||||
const tmp = createTempProject();
|
const tmp = createTempProject();
|
||||||
t.after(() => cleanup(tmp));
|
t.after(() => cleanup(tmp));
|
||||||
runGsdTools(['config-ensure-section'], tmp);
|
runGsdTools(['config-ensure-section'], tmp);
|
||||||
const r = runGsdTools(['config-set', 'review.models.cursor', 'cursor-model'], tmp);
|
const r = runGsdTools(['config-set', 'review.models.qwen', 'qwen-model'], tmp);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!r.success,
|
!r.success,
|
||||||
'config-set must reject a keyless lane — the exact error the old workflow steered users into'
|
'config-set must reject a keyless lane — the exact error the old workflow steered users into'
|
||||||
|
|||||||
Reference in New Issue
Block a user