diff --git a/commands/gsd/settings-advanced.md b/commands/gsd/settings-advanced.md
new file mode 100644
index 000000000..9dcac4ad8
--- /dev/null
+++ b/commands/gsd/settings-advanced.md
@@ -0,0 +1,39 @@
+---
+name: gsd:settings-advanced
+description: Power-user configuration — plan bounce, timeouts, branch templates, cross-AI execution, runtime knobs
+allowed-tools:
+ - Read
+ - Write
+ - Bash
+ - AskUserQuestion
+---
+
+
+Interactive configuration of GSD power-user knobs that don't belong in the common-case `/gsd:settings` prompt.
+
+Routes to the settings-advanced workflow which handles:
+- Config existence ensuring (workstream-aware path resolution)
+- Current settings reading and parsing
+- Sectioned prompts: Planning Tuning, Execution Tuning, Discussion Tuning, Cross-AI Execution, Git Customization, Runtime / Output
+- Config merging that preserves every unrelated key
+- Confirmation table display
+
+Use `/gsd:settings` for the common-case toggles (model profile, research/plan_check/verifier, branching strategy, context warnings). Use `/gsd:settings-advanced` once those are set and you want to tune the internals.
+
+
+
+@~/.claude/get-shit-done/workflows/settings-advanced.md
+
+
+
+**Follow the settings-advanced workflow** from `@~/.claude/get-shit-done/workflows/settings-advanced.md`.
+
+The workflow handles all logic including:
+1. Config file creation with defaults if missing (via `gsd-sdk query config-ensure-section`)
+2. Current config reading
+3. Six sectioned AskUserQuestion batches with current values pre-selected
+4. Numeric-input validation (non-numeric rejected, empty input keeps current)
+5. Answer parsing and config merging (preserves unrelated keys)
+6. File writing (atomic)
+7. Confirmation table display
+
diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md
index 6c7a9dbe3..bf49a70f0 100644
--- a/docs/COMMANDS.md
+++ b/docs/COMMANDS.md
@@ -1052,6 +1052,29 @@ All answers are merged via `gsd-sdk query config-set` into the resolved project
/gsd-settings # Interactive config
```
+### `/gsd-settings-advanced`
+
+Interactive configuration of power-user knobs — plan bounce, subagent timeouts, branch templates, cross-AI delegation, context window, and runtime output. Use after `/gsd-settings` once the common-case toggles are dialed in.
+
+Six sections, each a focused prompt batch:
+
+| Section | Keys |
+|---------|------|
+| Planning Tuning | `workflow.plan_bounce`, `workflow.plan_bounce_passes`, `workflow.plan_bounce_script`, `workflow.subagent_timeout`, `workflow.inline_plan_threshold` |
+| Execution Tuning | `workflow.node_repair`, `workflow.node_repair_budget`, `workflow.auto_prune_state` |
+| Discussion Tuning | `workflow.max_discuss_passes` |
+| Cross-AI Execution | `workflow.cross_ai_execution`, `workflow.cross_ai_command`, `workflow.cross_ai_timeout` |
+| Git Customization | `git.base_branch`, `git.phase_branch_template`, `git.milestone_branch_template` |
+| Runtime / Output | `response_language`, `context_window`, `search_gitignored`, `graphify.build_timeout` |
+
+Current values are pre-selected; an empty input keeps the existing value. Numeric fields reject non-numeric input and re-prompt. Null-allowed fields (`plan_bounce_script`, `cross_ai_command`, `response_language`) accept an empty input as a clear. Writes route through `gsd-sdk query config-set`, which preserves every unrelated key.
+
+```bash
+/gsd-settings-advanced # Six-section interactive config
+```
+
+See [CONFIGURATION.md](CONFIGURATION.md) for the full schema and defaults.
+
### `/gsd-set-profile`
Quick profile switch.
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index d44348488..1080f02fc 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -114,6 +114,7 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new
| `model_profile` | enum | `quality`, `balanced`, `budget`, `inherit` | `balanced` | Model tier for each agent (see [Model Profiles](#model-profiles)) |
| `project_code` | string | any short string | (none) | Prefix for phase directory names (e.g., `"ABC"` produces `ABC-01-setup/`). Added in v1.31 |
| `response_language` | string | language code | (none) | Language for agent responses (e.g., `"pt"`, `"ko"`, `"ja"`). Propagates to all spawned agents for cross-phase language consistency. Added in v1.32 |
+| `context_window` | number | any integer | `200000` | Context window size in tokens. Set `1000000` for 1M-context models (e.g., `claude-opus-4-7[1m]`). Values `>= 500000` enable adaptive context enrichment (full-body reads of prior SUMMARY.md, deeper anti-pattern reads). Configured via `/gsd-settings-advanced`. |
| `context_profile` | string | `dev`, `research`, `review` | (none) | Execution context preset that applies a pre-configured bundle of mode, model, and workflow settings for the current type of work. Added in v1.34 |
| `claude_md_path` | string | any file path | `./CLAUDE.md` | Custom output path for the generated CLAUDE.md file. Useful for monorepos or projects that need CLAUDE.md in a non-root location. Defaults to `./CLAUDE.md` at the project root. Added in v1.36 |
| `claude_md_assembly.mode` | enum | `embed`, `link` | `embed` | Controls how managed sections are written into CLAUDE.md. `embed` (default) inlines content between GSD markers. `link` writes `@.planning/` instead — Claude Code expands the reference at runtime, reducing CLAUDE.md size by ~65% on typical projects. `link` only applies to sections that have a real source file; `workflow` and fallback sections always embed. Per-block overrides: `claude_md_assembly.blocks.` (e.g. `claude_md_assembly.blocks.architecture: link`). Added in v1.38 |
diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json
index 3af04df8b..916d97fbd 100644
--- a/docs/INVENTORY-MANIFEST.json
+++ b/docs/INVENTORY-MANIFEST.json
@@ -1,5 +1,5 @@
{
- "generated": "2026-04-20",
+ "generated": "2026-04-22",
"families": {
"agents": [
"gsd-advisor-researcher",
@@ -103,6 +103,7 @@
"/gsd-session-report",
"/gsd-set-profile",
"/gsd-settings",
+ "/gsd-settings-advanced",
"/gsd-ship",
"/gsd-sketch",
"/gsd-sketch-wrap-up",
@@ -110,11 +111,11 @@
"/gsd-spike",
"/gsd-spike-wrap-up",
"/gsd-stats",
+ "/gsd-sync-skills",
"/gsd-thread",
"/gsd-ui-phase",
"/gsd-ui-review",
"/gsd-ultraplan-phase",
- "/gsd-sync-skills",
"/gsd-undo",
"/gsd-update",
"/gsd-validate-phase",
@@ -185,6 +186,7 @@
"scan.md",
"secure-phase.md",
"session-report.md",
+ "settings-advanced.md",
"settings.md",
"ship.md",
"sketch-wrap-up.md",
diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md
index 7ec26381c..7f643b07e 100644
--- a/docs/INVENTORY.md
+++ b/docs/INVENTORY.md
@@ -54,7 +54,7 @@ Full roster at `agents/gsd-*.md`. The "Primary doc" column flags whether [`docs/
---
-## Commands (83 shipped)
+## Commands (84 shipped)
Full roster at `commands/gsd/*.md`. The groupings below mirror `docs/COMMANDS.md` section order; each row carries the command name, a one-line role derived from the command's frontmatter `description:`, and a link to the source file. `tests/command-count-sync.test.cjs` locks the count against the filesystem.
@@ -163,6 +163,7 @@ Full roster at `commands/gsd/*.md`. The groupings below mirror `docs/COMMANDS.md
| `/gsd-sketch-wrap-up` | Package sketch design findings into a persistent project skill for future build conversations. | [commands/gsd/sketch-wrap-up.md](../commands/gsd/sketch-wrap-up.md) |
| `/gsd-profile-user` | Generate developer behavioral profile and Claude-discoverable artifacts. | [commands/gsd/profile-user.md](../commands/gsd/profile-user.md) |
| `/gsd-settings` | Configure GSD workflow toggles and model profile. | [commands/gsd/settings.md](../commands/gsd/settings.md) |
+| `/gsd-settings-advanced` | Power-user configuration — plan bounce, timeouts, branch templates, cross-AI execution, runtime knobs. | [commands/gsd/settings-advanced.md](../commands/gsd/settings-advanced.md) |
| `/gsd-set-profile` | Switch model profile for GSD agents (quality/balanced/budget/inherit). | [commands/gsd/set-profile.md](../commands/gsd/set-profile.md) |
| `/gsd-pr-branch` | Create a clean PR branch by filtering out `.planning/` commits. | [commands/gsd/pr-branch.md](../commands/gsd/pr-branch.md) |
| `/gsd-sync-skills` | Sync managed GSD skill directories across runtime roots for multi-runtime users. | [commands/gsd/sync-skills.md](../commands/gsd/sync-skills.md) |
@@ -173,7 +174,7 @@ Full roster at `commands/gsd/*.md`. The groupings below mirror `docs/COMMANDS.md
---
-## Workflows (81 shipped)
+## Workflows (82 shipped)
Full roster at `get-shit-done/workflows/*.md`. Workflows are thin orchestrators that commands reference internally; most are not read directly by end users. Rows below map each workflow file to its role (derived from the `` block) and, where applicable, to the command that invokes it.
@@ -243,6 +244,7 @@ Full roster at `get-shit-done/workflows/*.md`. Workflows are thin orchestrators
| `secure-phase.md` | Retroactive threat-mitigation audit for a completed phase. | `/gsd-secure-phase` |
| `session-report.md` | Session report — token usage, work summary, outcomes. | `/gsd-session-report` |
| `settings.md` | Configure GSD workflow toggles and model profile. | `/gsd-settings`, `/gsd-set-profile` |
+| `settings-advanced.md` | Configure GSD power-user knobs — plan bounce, timeouts, branch templates, cross-AI execution, runtime knobs. | `/gsd-settings-advanced` |
| `ship.md` | Create PR, run review, and prepare for merge after verification. | `/gsd-ship` |
| `sketch.md` | Explore design directions through throwaway HTML mockups with 2-3 variants per sketch. | `/gsd-sketch` |
| `sketch-wrap-up.md` | Curate sketch findings and package them as a persistent `sketch-findings-[project]` skill. | `/gsd-sketch-wrap-up` |
diff --git a/get-shit-done/bin/lib/config-schema.cjs b/get-shit-done/bin/lib/config-schema.cjs
index d91ff6c1b..478a80a49 100644
--- a/get-shit-done/bin/lib/config-schema.cjs
+++ b/get-shit-done/bin/lib/config-schema.cjs
@@ -54,6 +54,7 @@ const VALID_CONFIG_KEYS = new Set([
'project_code', 'phase_naming',
'manager.flags.discuss', 'manager.flags.plan', 'manager.flags.execute',
'response_language',
+ 'context_window',
'intel.enabled',
'graphify.enabled',
'graphify.build_timeout',
diff --git a/get-shit-done/workflows/settings-advanced.md b/get-shit-done/workflows/settings-advanced.md
new file mode 100644
index 000000000..35a038bc7
--- /dev/null
+++ b/get-shit-done/workflows/settings-advanced.md
@@ -0,0 +1,435 @@
+
+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, and graphify build timeout.
+
+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
+six 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.
+
+
+
+Read all files referenced by the invoking prompt's execution_context before starting.
+
+
+
+
+
+Ensure config exists and resolve the workstream-aware config path (mirrors `settings.md`):
+
+```bash
+gsd-sdk 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.
+
+
+
+```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`)
+
+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.
+
+
+
+
+**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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ 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: )",
+ header: "Graphify Timeout",
+ multiSelect: false,
+ options: [
+ { label: "Keep current", description: "Leave timeout unchanged." },
+ { label: "Enter seconds", description: "Integer seconds. Non-numeric rejected. Default: 300" }
+ ]
+ }
+])
+```
+
+
+
+
+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-sdk query config-set ` 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-sdk query config-set workflow.plan_bounce_passes 5
+gsd-sdk query config-set workflow.subagent_timeout 900
+gsd-sdk query config-set git.base_branch main
+gsd-sdk query config-set context_window 1000000
+```
+
+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–6 MUST survive the update):
+
+```json
+{
+ ...existing_config,
+ "workflow": {
+ ...existing_workflow,
+ "plan_bounce": ,
+ "plan_bounce_passes": ,
+ "plan_bounce_script": ,
+ "subagent_timeout": ,
+ "inline_plan_threshold": ,
+ "node_repair": ,
+ "node_repair_budget": ,
+ "auto_prune_state": ,
+ "max_discuss_passes": ,
+ "cross_ai_execution": ,
+ "cross_ai_command": ,
+ "cross_ai_timeout":
+ },
+ "git": {
+ ...existing_git,
+ "base_branch": ,
+ "phase_branch_template": ,
+ "milestone_branch_template":
+ },
+ "response_language": ,
+ "context_window": ,
+ "search_gitignored": ,
+ "graphify": {
+ ...existing_graphify,
+ "build_timeout":
+ }
+}
+```
+
+Never emit a full overwrite of the file that omits keys the user did not touch. Always
+route each write through `gsd-sdk query config-set` so sibling preservation is handled by
+the central setter.
+
+
+
+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} |
+
+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.
+```
+
+
+
+
+
+- [ ] Current config read from resolved `$GSD_CONFIG_PATH`
+- [ ] Six sections rendered (Planning, Execution, Discussion, Cross-AI, Git, Runtime)
+- [ ] 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-sdk query config-set` so unrelated keys are preserved
+- [ ] Confirmation table rendered listing all 19 fields
+
diff --git a/get-shit-done/workflows/settings.md b/get-shit-done/workflows/settings.md
index 23073c8e6..4866ea4bb 100644
--- a/get-shit-done/workflows/settings.md
+++ b/get-shit-done/workflows/settings.md
@@ -455,6 +455,7 @@ Quick commands:
- /gsd:plan-phase --research — force research
- /gsd:plan-phase --skip-research — skip research
- /gsd:plan-phase --skip-verify — skip plan check
+- /gsd:settings-advanced — power-user tuning (plan bounce, timeouts, branch templates, cross-AI, context window)
```
diff --git a/tests/gsd-settings-advanced.test.cjs b/tests/gsd-settings-advanced.test.cjs
new file mode 100644
index 000000000..6f6f18c4b
--- /dev/null
+++ b/tests/gsd-settings-advanced.test.cjs
@@ -0,0 +1,377 @@
+'use strict';
+
+/**
+ * Tests for `/gsd-settings-advanced` — power-user configuration command (#2528).
+ *
+ * Covers:
+ * - Command file exists with correct frontmatter
+ * - Workflow file exists with required section structure
+ * - Every field in the issue spec is rendered in the workflow with its default
+ * - Current values are pre-selected in prompts
+ * - Config merge preserves unrelated keys (sibling preservation)
+ * - Confirmation table is rendered after save
+ * - Every field is accepted by VALID_CONFIG_KEYS
+ * - /gsd-settings confirmation output advertises /gsd-settings-advanced
+ * - Negative: non-numeric value rejected for numeric field via config-set
+ */
+
+const { describe, test } = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+
+const { createTempProject, cleanup, runGsdTools } = require('./helpers.cjs');
+const { VALID_CONFIG_KEYS } = require('../get-shit-done/bin/lib/config-schema.cjs');
+
+const ROOT = path.resolve(__dirname, '..');
+const COMMAND_PATH = path.join(ROOT, 'commands', 'gsd', 'settings-advanced.md');
+const WORKFLOW_PATH = path.join(ROOT, 'get-shit-done', 'workflows', 'settings-advanced.md');
+const SETTINGS_WORKFLOW_PATH = path.join(ROOT, 'get-shit-done', 'workflows', 'settings.md');
+
+// ─── Spec — every field the advanced command must expose ──────────────────────
+
+const SPEC_FIELDS = {
+ planning: [
+ { 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.inline_plan_threshold', default: '3' },
+ ],
+ execution: [
+ { key: 'workflow.node_repair', default: 'true' },
+ { key: 'workflow.node_repair_budget', default: '2' },
+ { key: 'workflow.auto_prune_state', default: 'false' },
+ ],
+ discussion: [
+ { key: 'workflow.max_discuss_passes', default: '3' },
+ ],
+ cross_ai: [
+ { key: 'workflow.cross_ai_execution', default: 'false' },
+ { key: 'workflow.cross_ai_command', default: 'null' },
+ { key: 'workflow.cross_ai_timeout', default: '300' },
+ ],
+ git: [
+ { key: 'git.base_branch', default: 'main' },
+ { key: 'git.phase_branch_template', default: 'gsd/phase-{phase}-{slug}' },
+ { key: 'git.milestone_branch_template', default: 'gsd/{milestone}-{slug}' },
+ ],
+ runtime: [
+ { key: 'response_language', default: 'null' },
+ { key: 'context_window', default: '200000' },
+ { key: 'search_gitignored', default: 'false' },
+ { key: 'graphify.build_timeout', default: '300' },
+ ],
+};
+
+const ALL_SPEC_KEYS = Object.values(SPEC_FIELDS).flat().map((f) => f.key);
+
+// ─── File existence + frontmatter ─────────────────────────────────────────────
+
+describe('gsd-settings-advanced — file scaffolding', () => {
+ test('command file exists at commands/gsd/settings-advanced.md', () => {
+ assert.ok(fs.existsSync(COMMAND_PATH), `missing ${COMMAND_PATH}`);
+ });
+
+ test('workflow file exists at get-shit-done/workflows/settings-advanced.md', () => {
+ assert.ok(fs.existsSync(WORKFLOW_PATH), `missing ${WORKFLOW_PATH}`);
+ });
+
+ test('command frontmatter has name, description, allowed-tools', () => {
+ const text = fs.readFileSync(COMMAND_PATH, 'utf-8');
+ const fmMatch = text.match(/^---\n([\s\S]*?)\n---/);
+ assert.ok(fmMatch, 'command file missing frontmatter block');
+ const fm = fmMatch[1];
+ assert.match(fm, /name:\s*gsd:settings-advanced/, 'frontmatter missing name');
+ assert.match(fm, /description:\s*\S/, 'frontmatter missing non-empty description');
+ assert.match(fm, /allowed-tools:/, 'frontmatter missing allowed-tools');
+ });
+
+ test('command routes to the settings-advanced workflow', () => {
+ const text = fs.readFileSync(COMMAND_PATH, 'utf-8');
+ assert.ok(
+ text.includes('workflows/settings-advanced.md'),
+ 'command file must reference workflows/settings-advanced.md'
+ );
+ });
+});
+
+// ─── Workflow content — sections and fields ───────────────────────────────────
+
+describe('gsd-settings-advanced — workflow structure', () => {
+ let workflow;
+ try {
+ workflow = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
+ } catch { workflow = ''; }
+
+ const requiredSteps = [
+ 'ensure_and_load_config',
+ 'read_current',
+ 'present_settings',
+ 'update_config',
+ 'confirm',
+ ];
+ for (const step of requiredSteps) {
+ test(`workflow defines `, () => {
+ assert.ok(
+ workflow.includes(``),
+ `workflow missing step ${step}`
+ );
+ });
+ }
+
+ const requiredSections = [
+ 'Planning Tuning',
+ 'Execution Tuning',
+ 'Discussion Tuning',
+ 'Cross-AI Execution',
+ 'Git Customization',
+ 'Runtime / Output',
+ ];
+ for (const section of requiredSections) {
+ test(`workflow renders section "${section}"`, () => {
+ assert.ok(
+ workflow.includes(section),
+ `workflow missing section heading "${section}"`
+ );
+ });
+ }
+
+ for (const field of Object.values(SPEC_FIELDS).flat()) {
+ test(`workflow mentions key \`${field.key}\``, () => {
+ assert.ok(
+ workflow.includes(field.key),
+ `workflow missing field ${field.key}`
+ );
+ });
+ test(`workflow documents default for \`${field.key}\` (${field.default})`, () => {
+ // Search for the default token in proximity to the key. Keep this
+ // forgiving: same line, or within ~200 chars after the key.
+ const idx = workflow.indexOf(field.key);
+ assert.ok(idx >= 0, `key ${field.key} not found`);
+ const window = workflow.slice(idx, idx + 400);
+ assert.ok(
+ window.includes(field.default),
+ `default "${field.default}" not found near key ${field.key}. Window:\n${window}`
+ );
+ });
+ }
+
+ test('workflow pre-selects current values from loaded config', () => {
+ assert.match(
+ workflow,
+ /pre-selected|current value|Current:/i,
+ 'workflow must document that current values are pre-selected'
+ );
+ });
+
+ test('confirmation step renders a table with saved settings', () => {
+ const confirmStart = workflow.indexOf('');
+ assert.ok(confirmStart >= 0, 'confirm step missing');
+ const confirmBlock = workflow.slice(confirmStart);
+ assert.ok(
+ confirmBlock.includes('|') && /\|[^\n]*Setting[^\n]*\|/.test(confirmBlock),
+ 'confirm step must render a markdown table with a Setting column'
+ );
+ });
+
+ test('update_config step describes merge-preserving-siblings behavior', () => {
+ assert.match(
+ workflow,
+ /(preserv(e|ing) (unrelated|sibling)|do not clobber|merge .*existing|...existing_config)/i,
+ 'update_config step must describe preserving unrelated keys'
+ );
+ });
+});
+
+// ─── VALID_CONFIG_KEYS membership ─────────────────────────────────────────────
+
+describe('gsd-settings-advanced — VALID_CONFIG_KEYS coverage', () => {
+ for (const key of ALL_SPEC_KEYS) {
+ test(`VALID_CONFIG_KEYS contains "${key}"`, () => {
+ assert.ok(
+ VALID_CONFIG_KEYS.has(key),
+ `VALID_CONFIG_KEYS missing ${key} — add it to get-shit-done/bin/lib/config-schema.cjs`
+ );
+ });
+ }
+});
+
+// ─── /gsd-settings mentions /gsd-settings-advanced ────────────────────────────
+
+describe('/gsd-settings advertises /gsd-settings-advanced', () => {
+ test('settings workflow confirmation mentions gsd-settings-advanced', () => {
+ const text = fs.readFileSync(SETTINGS_WORKFLOW_PATH, 'utf-8');
+ assert.ok(
+ text.includes('gsd-settings-advanced') || text.includes('gsd:settings-advanced'),
+ 'get-shit-done/workflows/settings.md must mention /gsd-settings-advanced or /gsd:settings-advanced'
+ );
+ });
+});
+
+// ─── Sibling-preservation via config-set ──────────────────────────────────────
+
+describe('gsd-settings-advanced — config merge preserves unrelated keys', () => {
+ test('setting workflow.plan_bounce_passes does not clobber model_profile or git.branching_strategy', (t) => {
+ const tmpDir = createTempProject();
+ t.after(() => cleanup(tmpDir));
+
+ // Seed config
+ const configPath = path.join(tmpDir, '.planning', 'config.json');
+ const initial = {
+ model_profile: 'quality',
+ git: {
+ branching_strategy: 'phase',
+ phase_branch_template: 'feature/{phase}-{slug}',
+ },
+ workflow: {
+ research: true,
+ plan_check: false,
+ },
+ hooks: {
+ context_warnings: true,
+ },
+ };
+ fs.writeFileSync(configPath, JSON.stringify(initial, null, 2), 'utf-8');
+
+ const result = runGsdTools(
+ ['config-set', 'workflow.plan_bounce_passes', '5'],
+ tmpDir,
+ { HOME: tmpDir }
+ );
+ assert.ok(result.success, `config-set failed: ${result.error || result.output}`);
+
+ const updated = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
+ assert.strictEqual(updated.model_profile, 'quality', 'model_profile clobbered');
+ assert.strictEqual(updated.git.branching_strategy, 'phase', 'git.branching_strategy clobbered');
+ assert.strictEqual(updated.git.phase_branch_template, 'feature/{phase}-{slug}', 'git.phase_branch_template clobbered');
+ assert.strictEqual(updated.workflow.research, true, 'workflow.research clobbered');
+ assert.strictEqual(updated.workflow.plan_check, false, 'workflow.plan_check clobbered');
+ assert.strictEqual(updated.hooks.context_warnings, true, 'hooks.context_warnings clobbered');
+ assert.strictEqual(updated.workflow.plan_bounce_passes, 5, 'new value not written');
+ });
+
+ test('setting context_window preserves existing top-level keys', (t) => {
+ const tmpDir = createTempProject();
+ t.after(() => cleanup(tmpDir));
+
+ const configPath = path.join(tmpDir, '.planning', 'config.json');
+ fs.writeFileSync(configPath, JSON.stringify({
+ model_profile: 'balanced',
+ response_language: 'Japanese',
+ search_gitignored: true,
+ }, null, 2));
+
+ const result = runGsdTools(
+ ['config-set', 'context_window', '1000000'],
+ tmpDir,
+ { HOME: tmpDir }
+ );
+ assert.ok(result.success, `config-set context_window failed: ${result.error || result.output}`);
+
+ const updated = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
+ assert.strictEqual(updated.context_window, 1000000);
+ assert.strictEqual(updated.model_profile, 'balanced');
+ assert.strictEqual(updated.response_language, 'Japanese');
+ assert.strictEqual(updated.search_gitignored, true);
+ });
+});
+
+// ─── Negative: non-numeric for numeric field / unknown key rejected ───────────
+
+describe('gsd-settings-advanced — negative scenarios', () => {
+ test('config-set rejects an unknown key with a helpful error', (t) => {
+ const tmpDir = createTempProject();
+ t.after(() => cleanup(tmpDir));
+
+ const result = runGsdTools(
+ ['config-set', 'workflow.no_such_knob_at_all', 'true'],
+ tmpDir,
+ { HOME: tmpDir }
+ );
+ assert.ok(!result.success, 'config-set should reject unknown keys');
+ const combined = (result.error || '') + (result.output || '');
+ assert.match(combined, /Unknown config key/i);
+ });
+
+ test('workflow.subagent_timeout numeric input is coerced and stored as Number', (t) => {
+ // The config-set parser coerces numeric-looking strings to Number.
+ // This test locks in the coercion so users can't accidentally save
+ // a string for a numeric knob. A non-numeric string would be stored
+ // verbatim — we assert the parser prefers Number for numeric literals.
+ const tmpDir = createTempProject();
+ t.after(() => cleanup(tmpDir));
+
+ const configPath = path.join(tmpDir, '.planning', 'config.json');
+ fs.writeFileSync(configPath, '{}');
+
+ const okNum = runGsdTools(
+ ['config-set', 'workflow.subagent_timeout', '900'],
+ tmpDir,
+ { HOME: tmpDir }
+ );
+ assert.ok(okNum.success);
+ const c1 = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
+ assert.strictEqual(typeof c1.workflow.subagent_timeout, 'number');
+ assert.strictEqual(c1.workflow.subagent_timeout, 900);
+ });
+
+ test('workflow documents numeric-input rejection for non-numeric answers', () => {
+ const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
+ assert.match(
+ workflow,
+ /(non-numeric|must be a number|integer|numeric input|re-?prompt)/i,
+ 'workflow must document how non-numeric input is handled for numeric fields'
+ );
+ });
+
+ // Behavioral coverage for numeric-key inputs at the config-set boundary.
+ // The /gsd-settings-advanced workflow promises non-numeric input is never
+ // silently coerced — that promise is enforced by the AskUserQuestion
+ // re-prompt loop in the workflow runner, not by config-set itself. The
+ // CLI parser passes numeric-looking strings through Number() and stores
+ // anything else verbatim. These tests lock in both behaviors so a future
+ // regression that changes either layer surfaces immediately.
+ test('config-set on a numeric key stores non-numeric input verbatim as string (workflow layer must reject before reaching here)', (t) => {
+ const tmpDir = createTempProject();
+ t.after(() => cleanup(tmpDir));
+
+ const configPath = path.join(tmpDir, '.planning', 'config.json');
+ fs.writeFileSync(configPath, '{}');
+
+ const result = runGsdTools(
+ ['config-set', 'workflow.subagent_timeout', 'not-a-number'],
+ tmpDir,
+ { HOME: tmpDir }
+ );
+ // The CLI layer accepts the write — type validation lives in the
+ // /gsd-settings-advanced workflow. If a future change adds a numeric
+ // type-check at config-set, flip this assertion to !result.success.
+ assert.ok(result.success, `config-set should accept the raw value at the CLI boundary: ${result.error || result.output}`);
+ const stored = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
+ assert.strictEqual(
+ typeof stored.workflow.subagent_timeout,
+ 'string',
+ 'non-numeric input on a numeric key currently lands as a string at the CLI boundary'
+ );
+ assert.strictEqual(stored.workflow.subagent_timeout, 'not-a-number');
+ });
+
+ test('config-set on a numeric key coerces a numeric string to Number (parser invariant)', (t) => {
+ const tmpDir = createTempProject();
+ t.after(() => cleanup(tmpDir));
+
+ const configPath = path.join(tmpDir, '.planning', 'config.json');
+ fs.writeFileSync(configPath, '{}');
+
+ const result = runGsdTools(
+ ['config-set', 'workflow.max_discuss_passes', '7'],
+ tmpDir,
+ { HOME: tmpDir }
+ );
+ assert.ok(result.success, `config-set failed: ${result.error || result.output}`);
+ const stored = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
+ assert.strictEqual(typeof stored.workflow.max_discuss_passes, 'number');
+ assert.strictEqual(stored.workflow.max_discuss_passes, 7);
+ });
+});