diff --git a/.changeset/nimble-mice-climb.md b/.changeset/nimble-mice-climb.md new file mode 100644 index 000000000..d656faa73 --- /dev/null +++ b/.changeset/nimble-mice-climb.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1299 +--- +**Config docs/prompts now match the consumers** — `workflow.subagent_timeout` is documented in milliseconds (default 300000), not "seconds (default 600)" (a user who entered 600 got a 600 ms timeout); `review.models.` is documented as a bare model id injected into `--model`/`-m`, not a shell command; and `workflow.test_command` / `workflow.build_command` (consumed by verify-phase, execute-phase, audit-fix, and the post-merge gate) are now accepted by `config set` and documented. (#1296) diff --git a/docs/CLI-TOOLS.md b/docs/CLI-TOOLS.md index 04e22b2a5..05bf93c37 100644 --- a/docs/CLI-TOOLS.md +++ b/docs/CLI-TOOLS.md @@ -549,12 +549,12 @@ User-facing entry point: `/gsd-graphify` (see [Command Reference](COMMANDS.md#gs ## Reviewer CLI Routing -`review.models.` maps a reviewer flavor to a shell command invoked by the code-review workflow. Set via [`/gsd-config --integrations`](COMMANDS.md#gsd-config) or directly: +`review.models.` maps a reviewer flavor to a bare model id injected into the CLI's `--model` (or `-m`) flag by the code-review workflow. Set via [`/gsd-config --integrations`](COMMANDS.md#gsd-config) or directly: ```bash -node gsd-tools.cjs config-set review.models.codex "codex exec --model gpt-5" -node gsd-tools.cjs config-set review.models.gemini "gemini -m gemini-2.5-pro" -node gsd-tools.cjs config-set review.models.opencode "opencode run --model claude-sonnet-4" +node gsd-tools.cjs config-set review.models.codex "gpt-5" +node gsd-tools.cjs config-set review.models.gemini "gemini-2.5-pro" +node gsd-tools.cjs config-set review.models.opencode "claude-sonnet-4" node gsd-tools.cjs config-set review.models.claude "" # clear — fall back to session model ``` diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index efce61aed..780824afa 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -192,14 +192,14 @@ API key fields accept a string value (the key itself). They can also be set to t ### Code-review CLI routing -`review.models.` maps a reviewer flavor to a shell command. The code-review workflow shells out using this command when a matching flavor is requested. +`review.models.` maps a reviewer flavor to a bare model id. The code-review workflow injects this value into the CLI's `--model` (or `-m`) flag when invoking the reviewer. | Setting | Type | Default | Description | |---------|------|---------|-------------| -| `review.models.claude` | string | (session model) | Command for Claude-flavored review. Defaults to the session model when unset | -| `review.models.codex` | string | `null` | Command for Codex review, e.g. `"codex exec --model gpt-5"` | -| `review.models.gemini` | string | `null` | Command for Gemini review, e.g. `"gemini -m gemini-2.5-pro"` | -| `review.models.opencode` | string | `null` | Command for OpenCode review, e.g. `"opencode run --model claude-sonnet-4"` | +| `review.models.claude` | string | (session model) | Model id for Claude-flavored review. Defaults to the session model when unset | +| `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.opencode` | string | `null` | Model id for OpenCode review (injected into --model), e.g. `"claude-sonnet-4"` | The `` slug is validated against `[a-zA-Z0-9_-]+`. Empty or path-containing slugs are rejected by `config-set`. @@ -267,7 +267,7 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin | `workflow.ai_integration_phase` | boolean | `true` | Enable the `/gsd-ai-integration-phase` command. When `false`, the command exits with a configuration gate message | | `workflow.auto_prune_state` | boolean | `false` | When `true`, automatically prune stale entries from STATE.md at phase boundaries instead of prompting | | `workflow.pattern_mapper` | boolean | `true` | Run the `gsd-pattern-mapper` agent between research and planning to map new files to existing codebase analogs | -| `workflow.subagent_timeout` | number | `600` | Timeout in seconds for individual subagent invocations. Increase for long-running research or execution phases | +| `workflow.subagent_timeout` | number | `300000` | Timeout in milliseconds for parallel subagent tasks (e.g. codebase mapping). Increase for large codebases or slower models. Default: 300000 (5 minutes) | | `executor.stall_detect_interval_minutes` | number | `5` | Minutes between executor stall checks while an executor agent is active. The execute-phase orchestrator uses this cadence to inspect recent commits and avoid waiting forever on a silent agent. | | `executor.stall_threshold_minutes` | number | `10` | Minutes without executor completion or expected-branch commit activity before execute-phase offers recovery choices for a possible stalled executor. | | `workflow.inline_plan_threshold` | number | `3` | Maximum number of tasks in a phase before the planner generates a separate PLAN.md file instead of inlining tasks in the prompt | diff --git a/gsd-core/bin/shared/config-schema.manifest.json b/gsd-core/bin/shared/config-schema.manifest.json index ee34b0b17..0d0327da3 100644 --- a/gsd-core/bin/shared/config-schema.manifest.json +++ b/gsd-core/bin/shared/config-schema.manifest.json @@ -53,6 +53,8 @@ "workflow.cross_ai_command", "workflow.cross_ai_timeout", "workflow.subagent_timeout", + "workflow.test_command", + "workflow.build_command", "executor.stall_detect_interval_minutes", "executor.stall_threshold_minutes", "workflow.inline_plan_threshold", diff --git a/gsd-core/references/planning-config.md b/gsd-core/references/planning-config.md index d50369b2c..997855b3a 100644 --- a/gsd-core/references/planning-config.md +++ b/gsd-core/references/planning-config.md @@ -36,6 +36,8 @@ Configuration options for `.planning/` directory behavior. | `git.quick_branch_template` | `null` | Optional branch template for quick-task runs | | `workflow.use_worktrees` | `true` | Whether executor agents run in isolated git worktrees. Set to `false` to disable worktrees — agents execute sequentially on the main working tree instead. Recommended for solo developers or when worktree merges cause issues. Note: if your branch is ahead of `origin/HEAD` (a diverged milestone or feature branch), GSD auto-degrades to sequential and prints a warning; set `worktree.baseRef:"head"` in `.claude/settings.local.json` to restore parallel execution. See the branch-divergence note below. | | `workflow.subagent_timeout` | `300000` | Timeout in milliseconds for parallel subagent tasks (e.g. codebase mapping). Increase for large codebases or slower models. Default: 300000 (5 minutes). | +| `workflow.test_command` | `null` | Custom shell command run as the regression/test gate by verify-phase, execute-phase, audit-fix, and post-merge-gate. When unset, GSD auto-detects (Makefile / package.json / Cargo.toml / go.mod / pyproject.toml). Example: `npm test`. | +| `workflow.build_command` | `null` | Custom shell command run as the build gate by the post-merge gate. When unset, the build step is skipped/auto-detected. Example: `npm run build`. | | `workflow.inline_plan_threshold` | `2` | Plans with this many tasks or fewer execute inline (Pattern C) instead of spawning a subagent. Avoids ~14K token spawn overhead for small plans. Set to `0` to always spawn subagents. | | `manager.flags.discuss` | `""` | Flags passed to `/gsd:discuss-phase` when dispatched from manager (e.g. `"--auto --analyze"`) | | `manager.flags.plan` | `""` | Flags passed to plan workflow when dispatched from manager | @@ -262,6 +264,8 @@ Set via `workflow.*` namespace in config.json (e.g., `"workflow": { "research": | `workflow.skip_discuss` | boolean | `false` | `true`, `false` | Skip discuss phase entirely | | `workflow.use_worktrees` | boolean | `true` | `true`, `false` | Run executor agents in isolated git worktrees | | `workflow.subagent_timeout` | number | `300000` | Any positive integer (ms) | Timeout for parallel subagent tasks (default: 5 minutes) | +| `workflow.test_command` | string\|null | `null` | Any shell command | Regression/test gate command run by verify-phase, execute-phase, audit-fix, and post-merge-gate. Unset → GSD auto-detects (Makefile / package.json / Cargo.toml / go.mod / pyproject.toml). | +| `workflow.build_command` | string\|null | `null` | Any shell command | Build gate command run by the post-merge gate. Unset → build step auto-detected/skipped. | | `workflow.inline_plan_threshold` | number | `2` | `0`–`10` | Plans with ≤N tasks execute inline instead of spawning a subagent | | `workflow.code_review` | boolean | `true` | `true`, `false` | Enable built-in code review step in the ship workflow | | `workflow.code_review_depth` | string | `"standard"` | `"light"`, `"standard"`, `"deep"` | Depth level for code review analysis in the ship workflow | diff --git a/gsd-core/workflows/settings-advanced.md b/gsd-core/workflows/settings-advanced.md index aaa9f1500..76f452a7b 100644 --- a/gsd-core/workflows/settings-advanced.md +++ b/gsd-core/workflows/settings-advanced.md @@ -50,7 +50,7 @@ Planning Tuning: - `workflow.plan_bounce` (default: `false`) - `workflow.plan_bounce_passes` (default: `2`) - `workflow.plan_bounce_script` (default: `null`) -- `workflow.subagent_timeout` (default: `600`) +- `workflow.subagent_timeout` (default: `300000`) - `workflow.inline_plan_threshold` (default: `3`) Execution Tuning: @@ -155,12 +155,12 @@ AskUserQuestion([ ] }, { - question: "Subagent timeout (seconds)? (current: )", + question: "Subagent timeout (milliseconds)? (current: )", header: "Subagent Timeout", multiSelect: false, options: [ { label: "Keep current", description: "Leave timeout unchanged." }, - { label: "Enter seconds", description: "Integer number of seconds. Non-numeric rejected. Default: 600" } + { label: "Enter milliseconds", description: "Integer number of milliseconds. Non-numeric rejected. Default: 300000 (5 minutes)." } ] }, { @@ -498,7 +498,7 @@ keys and sibling sub-objects. ```bash # Example — only write keys the user changed. "Keep current" selections are skipped. gsd_run query config-set workflow.plan_bounce_passes 5 -gsd_run query config-set workflow.subagent_timeout 900 +gsd_run query config-set workflow.subagent_timeout 300000 gsd_run query config-set git.base_branch main gsd_run query config-set context_window 1000000 # Runtime model tier examples: @@ -751,7 +751,7 @@ Display: | workflow.plan_bounce | {on/off} | | workflow.plan_bounce_passes | {n} | | workflow.plan_bounce_script | {path/null} | -| workflow.subagent_timeout | {seconds} | +| workflow.subagent_timeout | {milliseconds} | | workflow.inline_plan_threshold | {n} | | workflow.node_repair | {on/off} | | workflow.node_repair_budget | {n} | diff --git a/gsd-core/workflows/settings-integrations.md b/gsd-core/workflows/settings-integrations.md index 103fd4506..4dc0aeeb9 100644 --- a/gsd-core/workflows/settings-integrations.md +++ b/gsd-core/workflows/settings-integrations.md @@ -173,20 +173,20 @@ AskUserQuestion([ multiSelect: false, options: [ { label: "Claude", description: "review.models.claude — defaults to session model when unset" }, - { label: "Codex", description: "review.models.codex — e.g. 'codex exec --model gpt-5'" }, - { label: "Gemini", description: "review.models.gemini — e.g. 'gemini -m gemini-2.5-pro'" }, - { label: "OpenCode", description: "review.models.opencode — e.g. 'opencode run --model claude-sonnet-4'" } + { label: "Codex", description: "review.models.codex — bare model id injected into --model, e.g. 'gpt-5'" }, + { label: "Gemini", description: "review.models.gemini — bare model id injected into -m, e.g. 'gemini-2.5-pro'" }, + { label: "OpenCode", description: "review.models.opencode — bare model id injected into --model, e.g. 'claude-sonnet-4'" } ] } ]) ``` For the selected CLI, show the current value (or `(unset)`) and offer -Leave / Replace / Clear, followed by a text-input prompt for the new command +Leave / Replace / Clear, followed by a text-input prompt for the model id string. Write via: ```bash -gsd_run query config-set review.models. "" +gsd_run query config-set review.models. "" ``` After each update, return to the "Review model CLI mapping — what next?" question. diff --git a/tests/config-field-docs.test.cjs b/tests/config-field-docs.test.cjs index 5d330776d..490c8af64 100644 --- a/tests/config-field-docs.test.cjs +++ b/tests/config-field-docs.test.cjs @@ -1,4 +1,5 @@ // allow-test-rule: docs-parity +// allow-test-rule: source-text-is-the-product — settings-advanced.md prompt text is the deployed contract (#1216) // Extracts CONFIG_DEFAULTS keys from config-loader.cjs source to verify planning-config.md // stays in sync. The canonical list of defaults lives in source; there is no runtime // API to enumerate them. Source inspection is the only practical parity check here. @@ -208,4 +209,149 @@ describe('config-field-docs', () => { 'planning-config.md must mention the plan_checker flat-key alias' ); }); + + test('workflow.test_command is documented in planning-config.md (#1216)', () => { + assert.ok( + content.includes('`workflow.test_command`'), + 'planning-config.md must document workflow.test_command' + ); + // Must appear specifically in the Complete Field Reference section + const completeRefSection = content.slice(content.indexOf('## Complete Field Reference')); + assert.ok( + completeRefSection.includes('`workflow.test_command`'), + 'planning-config.md Complete Field Reference must include workflow.test_command' + ); + }); + + test('workflow.build_command is documented in planning-config.md (#1216)', () => { + assert.ok( + content.includes('`workflow.build_command`'), + 'planning-config.md must document workflow.build_command' + ); + // Must appear specifically in the Complete Field Reference section + const completeRefSection = content.slice(content.indexOf('## Complete Field Reference')); + assert.ok( + completeRefSection.includes('`workflow.build_command`'), + 'planning-config.md Complete Field Reference must include workflow.build_command' + ); + }); +}); + +// ─── CONFIGURATION.md parity (#1216) ──────────────────────────────────────── + +describe('CONFIGURATION.md parity (#1216)', () => { + const DOCS_CONFIG_PATH = path.join(__dirname, '..', 'docs', 'CONFIGURATION.md'); + const SETTINGS_ADVANCED_PATH = path.join( + __dirname, + '..', + 'gsd-core', + 'workflows', + 'settings-advanced.md', + ); + + let docsContent; + let settingsAdvancedContent; + + before(() => { + docsContent = fs.readFileSync(DOCS_CONFIG_PATH, 'utf-8'); + settingsAdvancedContent = fs.readFileSync(SETTINGS_ADVANCED_PATH, 'utf-8'); + }); + + test('CONFIGURATION.md workflow.subagent_timeout describes milliseconds, not seconds (#1216)', () => { + assert.ok( + docsContent.includes('millisecond') || docsContent.includes('milliseconds'), + 'CONFIGURATION.md workflow.subagent_timeout must use the word "millisecond(s)"' + ); + assert.ok( + !docsContent.match(/\|\s*`workflow\.subagent_timeout`[^|]*\|\s*`?600`?\s*\|/), + 'CONFIGURATION.md workflow.subagent_timeout must NOT have default 600 (that was the seconds default)' + ); + }); + + test('CONFIGURATION.md workflow.subagent_timeout default is 300000 (#1216)', () => { + // Row-scoped: the actual table row for workflow.subagent_timeout must contain 300000 + assert.ok( + /\|\s*`workflow\.subagent_timeout`\s*\|[^|]*\|\s*`?300000`?\s*\|/.test(docsContent), + 'CONFIGURATION.md workflow.subagent_timeout table row must have default 300000' + ); + }); + + test('settings-advanced.md subagent_timeout prompt says milliseconds, not seconds (#1216)', () => { + assert.ok( + settingsAdvancedContent.includes('millisecond') || + settingsAdvancedContent.includes('milliseconds'), + 'settings-advanced.md subagent_timeout prompt must use "millisecond(s)"' + ); + assert.ok( + !settingsAdvancedContent.includes('Integer number of seconds'), + 'settings-advanced.md must NOT say "Integer number of seconds" for subagent_timeout' + ); + }); + + test('settings-advanced.md subagent_timeout prompt default is 300000 not 600 (#1216)', () => { + assert.ok( + !settingsAdvancedContent.match(/value or 600/), + 'settings-advanced.md must NOT show 600 as the subagent_timeout default' + ); + assert.ok( + settingsAdvancedContent.includes('300000'), + 'settings-advanced.md must show 300000 as the subagent_timeout default' + ); + }); + + test('settings-advanced.md parse-default list must NOT show subagent_timeout default 600 (#1216)', () => { + // Line 53 regression: the parse-default list item must use 300000, not 600 + assert.ok( + !(/`workflow\.subagent_timeout`[^\n]*default:[^\n]*`?600`?/.test(settingsAdvancedContent)), + 'settings-advanced.md must NOT list subagent_timeout default as 600 (stale seconds default)' + ); + }); + + test('settings-advanced.md confirmation table must NOT label subagent_timeout as {seconds} (#1216)', () => { + // Line 754 regression: the confirmation table row must say {milliseconds}, not {seconds} + assert.ok( + !(/workflow\.subagent_timeout\s*\|\s*\{seconds\}/.test(settingsAdvancedContent)), + 'settings-advanced.md confirmation table must NOT label subagent_timeout as {seconds}' + ); + }); + + test('settings-advanced.md bash example must NOT use subagent_timeout 900 (#1216)', () => { + // Line 501 regression: the bash example must not show the stale 900 value + assert.ok( + !(/subagent_timeout 900\b/.test(settingsAdvancedContent)), + 'settings-advanced.md bash example must NOT set subagent_timeout to 900 (stale seconds value)' + ); + }); + + test('CONFIGURATION.md review.models rows do not show shell command examples (#1216)', () => { + // The Integration Settings section (around line 195-202) used to have + // shell-command examples like "codex exec --model gpt-5". After the fix + // those rows must describe model ids, not full commands. + assert.ok( + !docsContent.includes('"codex exec --model'), + 'CONFIGURATION.md must NOT contain "codex exec --model" shell command example' + ); + assert.ok( + !docsContent.includes('"opencode run --model'), + 'CONFIGURATION.md must NOT contain "opencode run --model" shell command example' + ); + assert.ok( + !docsContent.includes('"gemini -m gemini'), + 'CONFIGURATION.md must NOT contain "gemini -m gemini..." shell command example' + ); + }); + + test('workflow.test_command is documented in CONFIGURATION.md (#1216)', () => { + assert.ok( + docsContent.includes('`workflow.test_command`'), + 'CONFIGURATION.md must document workflow.test_command' + ); + }); + + test('workflow.build_command is documented in CONFIGURATION.md (#1216)', () => { + assert.ok( + docsContent.includes('`workflow.build_command`'), + 'CONFIGURATION.md must document workflow.build_command' + ); + }); }); diff --git a/tests/config.test.cjs b/tests/config.test.cjs index 7457767e2..d75cb3ecc 100644 --- a/tests/config.test.cjs +++ b/tests/config.test.cjs @@ -1418,3 +1418,47 @@ describe('plan_review.source_grounding and plan_review.source_grounding_authorit ); }); }); + +// ─── config-set workflow.test_command (#1216) ──────────────────────────────── + +describe('config-set workflow.test_command (#1216)', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + runGsdTools('config-ensure-section', tmpDir); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('config-set accepts workflow.test_command', () => { + const result = runGsdTools(['config-set', 'workflow.test_command', 'npm test'], tmpDir); + assert.ok(result.success, `config-set should accept workflow.test_command: ${result.error}`); + const config = readConfig(tmpDir); + assert.strictEqual(config.workflow?.test_command, 'npm test', 'value must be persisted'); + }); + + test('config-set workflow.test_command persists a custom make command', () => { + const result = runGsdTools(['config-set', 'workflow.test_command', 'make test'], tmpDir); + assert.ok(result.success, `config-set should accept workflow.test_command: ${result.error}`); + const config = readConfig(tmpDir); + assert.strictEqual(config.workflow?.test_command, 'make test', 'make test must be persisted'); + }); + + test('config-get workflow.test_command returns the set value', () => { + runGsdTools(['config-set', 'workflow.test_command', 'cargo test'], tmpDir); + const result = runGsdTools('config-get workflow.test_command', tmpDir); + assert.ok(result.success, `config-get should return workflow.test_command: ${result.error}`); + const output = JSON.parse(result.output); + assert.strictEqual(output, 'cargo test', 'config-get must return the persisted value'); + }); + + test('config-set accepts workflow.build_command', () => { + const result = runGsdTools(['config-set', 'workflow.build_command', 'npm run build'], tmpDir); + assert.ok(result.success, `config-set should accept workflow.build_command: ${result.error}`); + const config = readConfig(tmpDir); + assert.strictEqual(config.workflow?.build_command, 'npm run build', 'value must be persisted'); + }); +}); diff --git a/tests/gsd-settings-advanced.test.cjs b/tests/gsd-settings-advanced.test.cjs index d2d7f017c..646ef6fe2 100644 --- a/tests/gsd-settings-advanced.test.cjs +++ b/tests/gsd-settings-advanced.test.cjs @@ -41,7 +41,7 @@ const SPEC_FIELDS = { { key: 'workflow.plan_bounce', default: 'false' }, { key: 'workflow.plan_bounce_passes', default: '2' }, { key: 'workflow.plan_bounce_script', default: 'null' }, - { key: 'workflow.subagent_timeout', default: '600' }, + { key: 'workflow.subagent_timeout', default: '300000' }, { key: 'workflow.inline_plan_threshold', default: '3' }, ], execution: [ diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index 47d96e563..751bc9da9 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -66,8 +66,8 @@ "scan.md": 7688, "secure-phase.md": 12282, "session-report.md": 4044, - "settings-advanced.md": 39621, - "settings-integrations.md": 15801, + "settings-advanced.md": 39666, + "settings-integrations.md": 15848, "settings.md": 33413, "ship.md": 24388, "sketch-wrap-up.md": 14223,