Files
msd-core/get-shit-done/workflows/settings-advanced.md
Tom Boucher 5ca646f015 feat(#443): unified cross-provider effort controls + fast-mode-aware routing (#463)
* test(#443): RED unified effort + fast_mode + resolve-execution

All 68 tests failing as expected — no implementation yet.
Covers: effort cascade (tier defaults, overrides, invalid fallthrough),
fast_mode cascade (boolean-only, tier defaults), resolveEffortForTier
escalation, renderEffortForRuntime clamping, resolve-execution CLI,
config schema new keys, QA hostile-input matrix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(#443): unified cross-provider effort + fast_mode knobs and resolve-execution query

Adds config-driven effort control (universal ladder: minimal<low<medium<high<xhigh<max)
and fast_mode propagation knobs, with per-runtime rendering that clamps the unique
tail values (max=Anthropic-only clamps to xhigh on Codex; minimal=Codex-only clamps
to low on Claude).

Key changes:
- config-schema.manifest.json: add effort.default, fast_mode.enabled as validKeys;
  add 4 dynamicKeyPatterns for effort.routing_tier_defaults, effort.agent_overrides,
  fast_mode.routing_tier_defaults, fast_mode.agent_overrides; fix stale _comment
- config-defaults.manifest.json: add effort and fast_mode blocks with tier defaults
- model-catalog.cjs: add EFFORT_RENDERING map, renderEffortForRuntime(), RUNTIMES_WITH_FAST_MODE
- model-profiles.cjs: re-export new catalog exports
- core.cjs: add resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier,
  VALID_EFFORTS, EFFORT_SET, nextEffort; pass effort/fast_mode through loadConfig
- commands.cjs: replace reasoning_effort in cmdResolveModel with unified effort;
  add cmdResolveExecution (superset command with effort_rendered, effort_param,
  effort_propagation, fast_mode, fast_mode_supported)
- gsd-tools.cjs: add resolve-execution case with --effort/--fast-mode/--attempt flags
- tests/feat-443: 69 tests covering cascade, rendering, escalation, CLI, schema, QA matrix
- tests/commands.test.cjs: convert 3 reasoning_effort assertions to unified effort
- docs/CONFIGURATION.md: document effort + fast_mode + resolve-execution sections
- settings-advanced.md: list new effort/fast_mode keys in confirmation table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(#443): remove dead catalog effort lane; unify codex effort through renderEffortForRuntime

- Remove resolveReasoningEffortInternal (catalog-driven effort function) from
  core.cjs and its export; remove from commands.cjs destructure import
- Convert tests/issue-2517-runtime-aware-profiles.test.cjs: all 11 effort
  assertions now use resolveEffortInternal + renderEffortForRuntime; Claude
  effort is first-class (output_config.effort); unknown runtimes assert param===null
- Convert tests/feat-3023-model-phase-types.test.cjs: replace the entire
  resolveReasoningEffortInternal describe with unified effort assertions;
  effort derives from AGENT_DEFAULT_TIERS routing tier, not phase-type tier

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(#443): ADR for unified cross-provider effort + fast-mode routing

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(#443): architecture-level QA invariants + test-strategy doc

Add 48-test integration suite (feat-443-effort-fast-mode.integration.test.cjs)
covering 8 architectural invariants: cross-provider validity (never emit a value
the real API would 400 on), param/channel contract stability, resolve-execution
JSON contract (all 8 keys + correct types), totality across the full 33-agent
registry, fast-mode honesty (claude always fast_mode_supported=false), precedence
first-valid-wins matrix for both effort and fast_mode cascades, dynamic-routing
composition (effort escalation independent of model tier), and config-set round-trip
for all new effort/* and fast_mode/* key namespaces. Append test-strategy section
with invariant rationale and E2E gap documentation to docs/TESTING-SUITES.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(#443): add failing install-wiring tests for effort per-runtime injection (RED)

TDD RED: 10 failing tests covering:
- Claude .md gets effort: injected per tier (planner=xhigh, mapper=low, executor=high)
- Gemini .md does NOT get effort: (already passing — Gemini-safe)
- Codex .toml gets model_reasoning_effort via unified resolver
- Config-driven: effort.agent_overrides drives both Claude .md and Codex .toml
- Source purity: agents/*.md have no effort: key (already passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(#443): wire effort per-runtime at install (Claude .md frontmatter + Codex .toml unified)

- Import AGENT_DEFAULT_TIERS and renderEffortForRuntime from model-catalog.cjs
- Add readGsdEffectiveEffortConfig(targetDir): reads merged effort config from
  .planning/config.json (per-project wins) + ~/.gsd/defaults.json (global fallback),
  same probe pattern as readGsdRuntimeProfileResolver
- Add resolveInstallTimeEffort(effortCfg, agentName): pure function matching
  resolveEffortInternal() precedence (agent_overrides > routing_tier_defaults > default > 'high')
  without loadConfig side-effects (no sub-repo detection, no migration writes)
- Claude agent copy loop: inject `effort: <value>` into frontmatter ONLY for
  runtime === 'claude'; all other .md runtimes (Gemini, Qwen, Hermes, etc.) stay
  effort-free (Gemini-safe source contract preserved in agents/*.md)
- generateCodexAgentToml: add effortCfg param; emit model_reasoning_effort from
  unified resolver (replaces old catalog entry.reasoning_effort); Codex clamps
  max → xhigh via renderEffortForRuntime('codex', ...)
- installCodexConfig: pass readGsdEffectiveEffortConfig(targetDir) to
  generateCodexAgentToml so per-project config wins for Codex .toml too
- Update failing tests to GREEN: 12/12 pass; all 17 install tests pass;
  2847/2848 unit tests pass (1 pre-existing failure: policy-shell-pinning)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(#443): source install effort defaults from manifest (kill drift) + guard test

Replace hardcoded _GSD_EFFORT_MANIFEST_TIER_DEFAULTS and the 'high' fallback in
resolveInstallTimeEffort with values read from config-defaults.manifest.json at
module init, using the same __dirname-relative path install.js already uses for
all shared manifests. Add feat-443-effort-defaults-drift.test.cjs to assert
equality between install.js's runtime constants and the manifest on every CI run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#443): reconcile Codex TOML tests with unified effort design

The #443 unified effort resolver makes generateCodexAgentToml always emit
model_reasoning_effort (driven by resolveInstallTimeEffort, not model_profile_overrides).
The test 'generated TOML omits reasoning_effort when runtime has none' had an
obsolete premise — model_profile_overrides.reasoning_effort:'' no longer suppresses
unified effort. Convert it to assert the new invariant: Codex TOML always carries a
valid model_reasoning_effort from the agent's routing tier (xhigh for gsd-planner,
a heavy-tier agent), while model_profile_overrides model override is still respected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#443): make install.js effort resolution lazy (no load-time side effects breaking launcher-parity)

Replace module-load-time IIFE + hard throw (config-defaults.manifest.json read)
and top-level require of model-catalog.cjs with a lazy _getGsdEffortCatalog()
getter that initialises on first call from resolveInstallTimeEffort /
generateCodexAgentToml / Claude .md effort injection.  Requiring install.js in
unrelated test contexts (e.g. runtime-launcher-parity) no longer triggers
manifest IO or throws, eliminating the load-time side effect that changed
subprocess exit codes / stderr on the bench.

Drift-guard exports (_GSD_EFFORT_MANIFEST_TIER_DEFAULTS / _GSD_EFFORT_MANIFEST_DEFAULT)
preserved as lazy getter properties on module.exports so feat-443-effort-defaults-drift
still validates them without forcing eager load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#443): isolate install-wiring test HOME to stop \$HOME/.claude pollution breaking launcher-parity

runGlobalInstall() now redirects HOME to a per-call isolated tmpdir in addition
to the existing runtime-specific env-var redirects (CLAUDE_CONFIG_DIR,
GEMINI_CONFIG_DIR, CODEX_HOME). This ensures install.js code that uses
os.homedir() directly — including the ~/.cache/gsd update-check deletion,
~/.gsd/defaults.json reads, and any HOME-relative npm subprocess writes —
never touches the real \$HOME during the test.

Without the HOME isolation the install test (which is new to this branch and
is now picked up by Docker's raw \`tests/*.test.cjs\` glob) could write or
delete files under the real \$HOME, causing runtime-launcher-parity test (D)
to fail: (D) asserts a loud non-zero exit when \$RUNTIME_DIR/gsd-tools.cjs is
absent and gsd-tools is not on PATH, but the launcher's \$HOME/.claude fallback
arm succeeds if \$HOME/.claude/get-shit-done/bin/gsd-tools.cjs exists.

Also sets GSD_SKIP_STALE_SDK_CHECK=1 to suppress the \`npm ls -g\` subprocess
that the global installer spawns — irrelevant to effort-wiring assertions,
slow, and potentially writes to ~/.npm cache.

All 12 feat-443 install-wiring assertions preserved. Drift-guard 5/5. Unit
suite 2848/2850 (pre-existing policy-shell-pinning.test.cjs failure on next).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(#443): add changeset fragment for effort + fast-mode routing

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(#443): set GSD_TEST_MODE before requiring install.js in drift-guard test to prevent HOME leak

Without GSD_TEST_MODE=1, require('bin/install.js') runs the module's main
install block (guarded by !GSD_TEST_MODE), performing a real global Claude
install into $HOME/.claude/. On CI ubuntu where node is on standard PATH,
the launcher's $HOME/.claude fallback arm then finds gsd-tools.cjs, causing
runtime-launcher-parity test (D) to exit zero when it must exit non-zero.

Root cause: feat-443-effort-defaults-drift.test.cjs (unit suite) runs
alphabetically before runtime-launcher-parity.test.cjs in the same node
--test invocation. Each runs in a separate worker process but shares the
same HOME. The drift test's install leaks gsd-tools.cjs into that HOME,
then the launcher test's bash subprocess finds it via the $HOME/.claude arm.

Fix: add process.env.GSD_TEST_MODE = '1' at the top of the drift-guard
test, before the require(installPath) call. This matches the pattern used
by feat-443-effort-fast-mode.test.cjs and feat-443-effort-install-wiring
.install.test.cjs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#443): deterministic resolve-execution arg parsing + validate install-time effort (Codex adversarial findings)

Finding 1: resolve-execution --effort low gsd-planner misrouted 'low' as the agent.
Replace find(non-dash) with a proper flag-consuming loop that collects a single
positional; validate missing/extra positionals and malformed --attempt values.

Finding 2: resolveInstallTimeEffort returned unvalidated effort strings (e.g. "ultra")
verbatim. Each precedence layer now checks GSD_EFFORT_SET (imported once from
core.cjs) before accepting a value, mirroring resolveEffortInternal exactly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#443): newline-agnostic effort frontmatter injection (Windows CRLF) + CRLF-safe assertions

Extracts injectEffortFrontmatter(content, effortValue) pure helper that detects
EOL (LF vs CRLF) from the opening '---' line and inserts 'effort: <value>'
before the closing '---' delimiter using the same EOL as the surrounding
frontmatter. Regex now uses /^---\r?\n([\s\S]*?)^---\r?$/m instead of the
LF-only /^(---\n[\s\S]*?)(---)(\n|$)/ that silently skipped CRLF files on
Windows (git core.autocrlf=true checkout).

Also adds 7 unit tests covering LF, CRLF, idempotency, no-frontmatter, and
complex frontmatter cases. Exports injectEffortFrontmatter from module.exports.

Fixes 6 CI failures in tests/feat-443-effort-install-wiring.install.test.cjs
on windows-latest runners (lines 138, 145, 152, 261, 345, 356).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: CI Rebase Check <ci@gsd-redux>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:32:58 -04:00

621 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<purpose>
Interactive configuration of GSD power-user knobs — plan bounce, node repair, subagent timeouts,
inline plan threshold, cross-AI execution, base branch, branch templates, response language,
context window, gitignored search, graphify build timeout, and runtime model tier overrides.
This is a companion to `/gsd:settings` — the common-case prompt there covers model profile,
research/plan_check/verifier toggles, branching strategy, UI/AI phase gates, and worktree
isolation. This advanced command covers everything else that is user-settable, grouped into
seven sections so each prompt batch stays cognitively scoped. Every answer pre-selects the
current value; numeric-input answers that are non-numeric are rejected and re-prompted.
</purpose>
<required_reading>
Read all files referenced by the invoking prompt's execution_context before starting.
</required_reading>
<process>
<step name="ensure_and_load_config">
Ensure config exists and resolve the workstream-aware config path (mirrors `settings.md`):
```bash
_GSD_SHIM_NAME="gsd-tools.cjs"; GSD_TOOLS="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/get-shit-done/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/get-shit-done/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/get-shit-done/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/get-shit-done-redux@latest --claude --local" >&2; exit 1; fi
gsd_run query config-ensure-section
if [[ -z "${GSD_CONFIG_PATH:-}" ]]; then
if [[ -f .planning/active-workstream ]]; then
WS=$(tr -d '\n\r' < .planning/active-workstream)
GSD_CONFIG_PATH=".planning/workstreams/${WS}/config.json"
else
GSD_CONFIG_PATH=".planning/config.json"
fi
fi
```
All subsequent reads and writes go through `$GSD_CONFIG_PATH`. Never hardcode
`.planning/config.json` — workstream installs must route to their own config file.
</step>
<step name="read_current">
```bash
cat "$GSD_CONFIG_PATH"
```
Parse the following current values. If a key is absent, fall back to the documented default
shown in parentheses:
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.inline_plan_threshold` (default: `3`)
Execution Tuning:
- `workflow.node_repair` (default: `true`)
- `workflow.node_repair_budget` (default: `2`)
- `workflow.auto_prune_state` (default: `false`)
Discussion Tuning:
- `workflow.max_discuss_passes` (default: `3`)
Cross-AI Execution:
- `workflow.cross_ai_execution` (default: `false`)
- `workflow.cross_ai_command` (default: `null`)
- `workflow.cross_ai_timeout` (default: `300`)
Git Customization:
- `git.base_branch` (default: `main`)
- `git.phase_branch_template` (default: `gsd/phase-{phase}-{slug}`)
- `git.milestone_branch_template` (default: `gsd/{milestone}-{slug}`)
Runtime / Output:
- `response_language` (default: `null`)
- `context_window` (default: `200000`)
- `search_gitignored` (default: `false`)
- `graphify.build_timeout` (default: `300`)
Runtime Model Tiers:
- `runtime` (default: `null` — reads as `"claude"`)
- `model_profile_overrides.<runtime>.opus` (default: built-in for the runtime, or absent)
- `model_profile_overrides.<runtime>.sonnet` (default: built-in for the runtime, or absent)
- `model_profile_overrides.<runtime>.haiku` (default: built-in for the runtime, or absent)
Each field's **current value is pre-selected** in the prompt rendering below. When the
current value is absent from the config, render the documented default as the pre-selected
option so the user sees what the effective value is.
</step>
<step name="present_settings">
**Text mode (`workflow.text_mode: true` or `--text` flag):** Set `TEXT_MODE=true` if `--text` is
in `$ARGUMENTS` OR `text_mode` is true in config. When `TEXT_MODE=true`, replace every
`AskUserQuestion` call below with a plain-text numbered list and ask the user to type the
choice number or free-text value.
**Numeric-input validation.** For any numeric field (`*_passes`, `*_budget`, `*_timeout`,
`*_threshold`, `context_window`, `graphify.build_timeout`), if the user types a value that
is not a non-negative integer, the workflow MUST reject it, state which value was invalid,
and re-prompt that single field. The minimum accepted value is field-specific and is stated
in each field's prompt below — `workflow.plan_bounce_passes` and `workflow.max_discuss_passes`
require `>= 1`; all other numeric fields accept `>= 0`. An empty input means "keep current"
— the existing value is retained. Non-numeric input is never silently coerced.
**Free-text validation.** For branch template fields (`git.phase_branch_template`,
`git.milestone_branch_template`), if the user supplies a non-default value, it MUST be
non-empty and SHOULD contain at least one `{placeholder}`. A template missing placeholders
is rejected with a message explaining the available variables (`{phase}`, `{slug}`,
`{milestone}`) and re-prompted. An empty input means "keep current."
**Null-allowed fields.** For `response_language`, `workflow.plan_bounce_script`,
`workflow.cross_ai_command`: an empty input clears the field (`null`). A non-empty input is
stored verbatim as a string.
---
### Section 1 — Planning Tuning
```text
AskUserQuestion([
{
question: "Run external plan-bounce validator against generated PLAN.md? (current: <value or false>)",
header: "Plan Bounce",
multiSelect: false,
options: [
{ label: "No (default: false)", description: "Skip external plan validation." },
{ label: "Yes", description: "Pipe each PLAN.md through `plan_bounce_script` and block on non-zero exit." }
]
},
{
question: "How many plan-bounce passes? (current: <value or 2>)",
header: "Bounce Passes",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave the existing value unchanged." },
{ label: "Enter number", description: "Type an integer >= 1. Non-numeric input is rejected and re-prompted. Default: 2" }
]
},
{
question: "Path to plan-bounce validation script? (current: <value or null>)",
header: "Bounce Script",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave existing path unchanged." },
{ label: "Clear (null)", description: "Unset the script path." },
{ label: "Enter path", description: "Type an absolute or repo-relative path. Receives PLAN.md path as first argument." }
]
},
{
question: "Subagent timeout (seconds)? (current: <value or 600>)",
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" }
]
},
{
question: "Inline plan threshold — tasks allowed inline before splitting to PLAN.md? (current: <value or 3>)",
header: "Inline Plan Threshold",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave threshold unchanged." },
{ label: "Enter number", description: "Integer count. Non-numeric rejected. Default: 3" }
]
}
])
```
### Section 2 — Execution Tuning
```text
AskUserQuestion([
{
question: "Enable autonomous node repair on verification failure? (current: <value or true>)",
header: "Node Repair",
multiSelect: false,
options: [
{ label: "Yes (default: true)", description: "Executor retries failed tasks up to the repair budget." },
{ label: "No", description: "Stop on first verification failure." }
]
},
{
question: "Maximum node-repair attempts per failed task? (current: <value or 2>)",
header: "Repair Budget",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave existing budget unchanged." },
{ label: "Enter number", description: "Integer >= 0. Non-numeric rejected. Default: 2" }
]
},
{
question: "Auto-prune stale STATE.md entries at phase boundaries? (current: <value or false>)",
header: "Auto Prune",
multiSelect: false,
options: [
{ label: "No (default: false)", description: "Prompt before pruning." },
{ label: "Yes", description: "Prune stale entries without prompting." }
]
}
])
```
### Section 3 — Discussion Tuning
```text
AskUserQuestion([
{
question: "Maximum discuss-phase question rounds? (current: <value or 3>)",
header: "Max Discuss Passes",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave existing value unchanged." },
{ label: "Enter number", description: "Integer >= 1. Non-numeric rejected. Default: 3. Prevents infinite discussion loops in headless mode." }
]
}
])
```
### Section 4 — Cross-AI Execution
```text
AskUserQuestion([
{
question: "Delegate phase execution to an external AI CLI? (current: <value or false>)",
header: "Cross-AI",
multiSelect: false,
options: [
{ label: "No (default: false)", description: "Use local executor agents." },
{ label: "Yes", description: "Pipe phase prompt to `cross_ai_command` via stdin. Requires command to be set." }
]
},
{
question: "Cross-AI command template? (current: <value or null>)",
header: "Cross-AI Command",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave command unchanged." },
{ label: "Clear (null)", description: "Unset the command." },
{ label: "Enter command", description: "Shell command receiving phase prompt via stdin. Must produce SUMMARY.md-compatible output." }
]
},
{
question: "Cross-AI timeout (seconds)? (current: <value or 300>)",
header: "Cross-AI Timeout",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave timeout unchanged." },
{ label: "Enter seconds", description: "Integer seconds. Non-numeric rejected. Default: 300" }
]
}
])
```
### Section 5 — Git Customization
```text
AskUserQuestion([
{
question: "Git base branch? (current: <value or main>)",
header: "Base Branch",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave base branch unchanged." },
{ label: "Enter branch name", description: "e.g., main, master, develop. Integration branch for phase/milestone branches." }
]
},
{
question: "Phase branch template? (current: <value or gsd/phase-{phase}-{slug}>)",
header: "Phase Template",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave template unchanged." },
{ label: "Enter template", description: "Non-empty string with at least one placeholder. Available: {phase}, {slug}. Non-default values missing placeholders are rejected." }
]
},
{
question: "Milestone branch template? (current: <value or gsd/{milestone}-{slug}>)",
header: "Milestone Template",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave template unchanged." },
{ label: "Enter template", description: "Non-empty string. Available placeholders: {milestone}, {slug}. Non-default values missing placeholders are rejected." }
]
}
])
```
### Section 6 — Runtime / Output
```text
AskUserQuestion([
{
question: "Response language for agent output? (current: <value or null>)",
header: "Language",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave unchanged." },
{ label: "Clear (null)", description: "Use Claude default (English)." },
{ label: "Enter language", description: "Free-text language name or code (e.g., Japanese, pt, ko). Propagates to spawned agents." }
]
},
{
question: "Context window size (tokens)? (current: <value or 200000>)",
header: "Context Window",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave unchanged." },
{ label: "Enter number", description: "Integer. Non-numeric rejected. Default: 200000. Use 1000000 for 1M-context models. Values >= 500000 enable adaptive enrichment." }
]
},
{
question: "Include gitignored files in broad searches? (current: <value or false>)",
header: "Search Gitignored",
multiSelect: false,
options: [
{ label: "No (default: false)", description: "Respect .gitignore during searches." },
{ label: "Yes", description: "Add --no-ignore to broad searches (includes .planning/)." }
]
},
{
question: "Graphify build timeout (seconds)? (current: <value or 300>)",
header: "Graphify Timeout",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave timeout unchanged." },
{ label: "Enter seconds", description: "Integer seconds. Non-numeric rejected. Default: 300" }
]
}
])
```
### Section 7 — Runtime Model Tiers
This section lets the user inspect and override the built-in model IDs GSD resolves for each
profile tier (`opus` / `sonnet` / `haiku`) on their configured runtime.
**Step A — Show current runtime and built-in defaults:**
Read `runtime` from the config (or treat as `"claude"` if absent). Look up the built-in
tier map from the table below. For each tier, also read the current override from
`model_profile_overrides.<runtime>.<tier>` if present.
Built-in tier defaults by runtime:
| Runtime | `opus` | `sonnet` | `haiku` |
|------------|-------------------------------|---------------------------------|-------------------------------|
| `claude` | `claude-opus-4-7` | `claude-sonnet-4-6` | `claude-haiku-4-5` |
| `codex` | `gpt-5.4` | `gpt-5.3-codex` | `gpt-5.4-mini` |
| `gemini` | `gemini-3-pro` | `gemini-3-flash` | `gemini-2.5-flash-lite` |
| `qwen` | `qwen3-max-2026-01-23` | `qwen3-coder-plus` | `qwen3-coder-next` |
| `opencode` | `anthropic/claude-opus-4-7` | `anthropic/claude-sonnet-4-6` | `anthropic/claude-haiku-4-5` |
| `copilot` | `claude-opus-4-7` | `claude-sonnet-4-6` | `claude-haiku-4-5` |
| `hermes` | `anthropic/claude-opus-4-7` | `anthropic/claude-sonnet-4-6` | `anthropic/claude-haiku-4-5` |
| Group B (`kilo`, `cline`, `cursor`, `windsurf`, `augment`, `trae`, `codebuddy`, `antigravity`) | (no built-in default — your runtime handles model selection) | | |
Display a table to the user showing the effective configuration:
```text
Runtime model tiers — runtime: <current runtime or "claude (default)">
| Tier | Built-in default | Current override (if any) |
|--------|-----------------------------------|-----------------------------------|
| opus | <built-in or "(no built-in)"> | <override value or "(none)"> |
| sonnet | <built-in or "(no built-in)"> | <override value or "(none)"> |
| haiku | <built-in or "(no built-in)"> | <override value or "(none)"> |
```
For Group B runtimes (those without a built-in default), show `(no built-in default — your runtime handles model selection)` in the built-in column.
**Step B — Let the user choose a runtime (optional):**
```text
AskUserQuestion([
{
question: "Which runtime group do you want to configure tier overrides for? (current: <runtime or 'claude'>)",
header: "Runtime Group",
multiSelect: false,
options: [
{ label: "Keep current (<runtime>)", description: "Configure overrides for the current runtime." },
{ label: "Common runtimes", description: "claude, codex, gemini, qwen" },
{ label: "Additional runtimes", description: "opencode, copilot, hermes" },
{ label: "Other (Group B or custom)", description: "kilo, cline, cursor, windsurf, augment, trae, codebuddy, antigravity, or a custom runtime string." }
]
}
])
```
If "Common runtimes" is selected, ask:
```text
AskUserQuestion([
{
question: "Choose the runtime:",
header: "Common",
multiSelect: false,
options: [
{ label: "claude", description: "Claude Code / Anthropic CLI." },
{ label: "codex", description: "OpenAI Codex CLI." },
{ label: "gemini", description: "Gemini CLI." },
{ label: "qwen", description: "Qwen CLI." }
]
}
])
```
If "Additional runtimes" is selected, ask:
```text
AskUserQuestion([
{
question: "Choose the runtime:",
header: "Additional",
multiSelect: false,
options: [
{ label: "opencode", description: "OpenCode (uses anthropic/ prefix)." },
{ label: "copilot", description: "GitHub Copilot." },
{ label: "hermes", description: "Hermes (uses anthropic/ prefix)." }
]
}
])
```
If "Other (Group B or custom)" is selected, prompt the user to enter the runtime name as a free-text string.
If the selected runtime differs from the stored `runtime` key, update `runtime` via
`gsd-tools.cjs query config-set runtime <value>` before proceeding to Step C.
**Step C — Configure tier overrides for the selected runtime:**
```text
AskUserQuestion([
{
question: "Override for opus tier? Built-in: <opus default or '(no built-in)'> Current: <override or '(none)'>",
header: "Opus Override",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave unchanged (uses built-in default if no override)." },
{ label: "Clear override", description: "Remove any existing override; fall back to built-in." },
{ label: "Enter model ID", description: "Type the exact model ID string to use for opus-tier agents on this runtime." }
]
},
{
question: "Override for sonnet tier? Built-in: <sonnet default or '(no built-in)'> Current: <override or '(none)'>",
header: "Sonnet Override",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave unchanged." },
{ label: "Clear override", description: "Remove any existing override; fall back to built-in." },
{ label: "Enter model ID", description: "Type the exact model ID string to use for sonnet-tier agents on this runtime." }
]
},
{
question: "Override for haiku tier? Built-in: <haiku default or '(no built-in)'> Current: <override or '(none)'>",
header: "Haiku Override",
multiSelect: false,
options: [
{ label: "Keep current", description: "Leave unchanged." },
{ label: "Clear override", description: "Remove any existing override; fall back to built-in." },
{ label: "Enter model ID", description: "Type the exact model ID string to use for haiku-tier agents on this runtime." }
]
}
])
```
**Step D — Apply the changes:**
For each tier where the user chose "Enter model ID":
```bash
gsd_run query config-set model_profile_overrides.<runtime>.<tier> "<model-id>"
```
For each tier where the user chose "Clear override", remove the key by setting it to null:
```bash
gsd_run query config-set model_profile_overrides.<runtime>.<tier> null
```
"Keep current" selections are skipped entirely. Never write a key the user did not explicitly
change.
</step>
<step name="update_config">
Merge the new settings into the existing config at `$GSD_CONFIG_PATH`. This merge is the
core correctness invariant: **preserve every unrelated key** — do not clobber siblings.
Apply each selected value via `gsd-tools.cjs query config-set <key> <value>` so the central
validator (`isValidConfigKey`) accepts the write and the deep-merge preserves unrelated
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 git.base_branch main
gsd_run query config-set context_window 1000000
# Runtime model tier examples:
gsd_run query config-set runtime gemini
gsd_run query config-set model_profile_overrides.gemini.opus gemini-3-ultra
gsd_run query config-set model_profile_overrides.gemini.haiku null
```
Conceptual shape after merge (unchanged top-level keys like `model_profile`,
`granularity`, `mode`, `brave_search`, `agent_skills.*`, `hooks.context_warnings`, and
anything not listed in Sections 1–7 MUST survive the update):
```json
{
...existing_config,
"workflow": {
...existing_workflow,
"plan_bounce": <new|existing>,
"plan_bounce_passes": <new|existing>,
"plan_bounce_script": <new|existing|null>,
"subagent_timeout": <new|existing>,
"inline_plan_threshold": <new|existing>,
"node_repair": <new|existing>,
"node_repair_budget": <new|existing>,
"auto_prune_state": <new|existing>,
"max_discuss_passes": <new|existing>,
"cross_ai_execution": <new|existing>,
"cross_ai_command": <new|existing|null>,
"cross_ai_timeout": <new|existing>
},
"git": {
...existing_git,
"base_branch": <new|existing>,
"phase_branch_template": <new|existing>,
"milestone_branch_template": <new|existing>
},
"response_language": <new|existing|null>,
"context_window": <new|existing>,
"search_gitignored": <new|existing>,
"graphify": {
...existing_graphify,
"build_timeout": <new|existing>
},
"runtime": <new|existing|null>,
"model_profile_overrides": {
...existing_model_profile_overrides,
"<runtime>": {
...existing_runtime_overrides,
"opus": <new|existing|null>,
"sonnet": <new|existing|null>,
"haiku": <new|existing|null>
}
}
}
```
Never emit a full overwrite of the file that omits keys the user did not touch. Always
route each write through `gsd-tools.cjs query config-set` so sibling preservation is handled by
the central setter.
</step>
<step name="confirm">
Display:
```text
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► ADVANCED SETTINGS UPDATED
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
| Setting | Value |
|--------------------------------------------|-------|
| workflow.plan_bounce | {on/off} |
| workflow.plan_bounce_passes | {n} |
| workflow.plan_bounce_script | {path/null} |
| workflow.subagent_timeout | {seconds} |
| workflow.inline_plan_threshold | {n} |
| workflow.node_repair | {on/off} |
| workflow.node_repair_budget | {n} |
| workflow.auto_prune_state | {on/off} |
| workflow.max_discuss_passes | {n} |
| workflow.cross_ai_execution | {on/off} |
| workflow.cross_ai_command | {cmd/null} |
| workflow.cross_ai_timeout | {seconds} |
| git.base_branch | {branch} |
| git.phase_branch_template | {template} |
| git.milestone_branch_template | {template} |
| response_language | {lang/null} |
| context_window | {tokens} |
| search_gitignored | {on/off} |
| graphify.build_timeout | {seconds} |
| runtime | {runtime/null} |
| model_profile_overrides.<runtime>.opus | {model/built-in/null} |
| model_profile_overrides.<runtime>.sonnet | {model/built-in/null} |
| model_profile_overrides.<runtime>.haiku | {model/built-in/null} |
| effort.default | {low/medium/high/xhigh/max} |
| effort.routing_tier_defaults.light | {low/medium/high/xhigh/max} |
| effort.routing_tier_defaults.standard | {low/medium/high/xhigh/max} |
| effort.routing_tier_defaults.heavy | {low/medium/high/xhigh/max} |
| effort.agent_overrides.<agent-id> | {low/medium/high/xhigh/max} |
| fast_mode.enabled | {true/false} |
| fast_mode.routing_tier_defaults.light | {true/false} |
| fast_mode.routing_tier_defaults.standard | {true/false} |
| fast_mode.routing_tier_defaults.heavy | {true/false} |
| fast_mode.agent_overrides.<agent-id> | {true/false} |
These settings apply to future /gsd:plan-phase, /gsd:execute-phase, /gsd:discuss-phase,
and /gsd:ship runs.
For common-case toggles (model profile, research/plan_check/verifier, branching strategy,
UI/AI phase gates), use /gsd:settings.
```
</step>
</process>
<success_criteria>
- [ ] Current config read from resolved `$GSD_CONFIG_PATH`
- [ ] Seven sections rendered (Planning, Execution, Discussion, Cross-AI, Git, Runtime/Output, Runtime Model Tiers)
- [ ] Every field pre-selected to its current value (or documented default if absent)
- [ ] Numeric inputs validated — non-numeric rejected and re-prompted
- [ ] Branch-template inputs validated — non-default must contain a placeholder
- [ ] Null-allowed fields accept an empty input as a clear
- [ ] Writes routed through `gsd-tools.cjs query config-set` so unrelated keys are preserved
- [ ] Section 7 shows current runtime and built-in tier table
- [ ] Group B runtimes display "(no built-in default — your runtime handles model selection)"
- [ ] Override set/clear/keep paths all work correctly for each tier
- [ ] Confirmation table rendered listing all 23 fields (19 + runtime + 3 tier overrides)
</success_criteria>