From 9c0a153a5fa3382e9795e3d80961897490f8ea88 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 22 Apr 2026 20:50:15 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20/gsd-settings-advanced=20=E2=80=94=20po?= =?UTF-8?q?wer-user=20config=20tuning=20command=20(closes=20#2528)=20(#260?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: /gsd-settings-advanced — power-user config tuning command (closes #2528) Adds a second-tier interactive configuration command covering the power-user knobs that don't belong in the common-case /gsd-settings prompt. Six sectioned AskUserQuestion batches cover planning, execution, discussion, cross-AI, git, and runtime settings (19 config keys total). Current values are pre-selected; numeric fields reject non-numeric input; writes route through gsd-sdk query config-set so unrelated keys are preserved. - commands/gsd/settings-advanced.md — command entry - get-shit-done/workflows/settings-advanced.md — six-section workflow - get-shit-done/workflows/settings.md — advertise advanced command - get-shit-done/bin/lib/config-schema.cjs — add context_window to VALID_CONFIG_KEYS - docs/COMMANDS.md, docs/CONFIGURATION.md, docs/INVENTORY.md — docs + inventory - tests/gsd-settings-advanced.test.cjs — 81 tests (files, frontmatter, field coverage, pre-selection, merge-preserves-siblings, VALID_CONFIG_KEYS membership, confirmation table, /gsd-settings cross-link, negative scenarios) All 5073 tests pass; coverage 88.66% (>= 70% threshold). * docs(settings-advanced): clarify per-field numeric bounds and label fenced blocks Addresses CodeRabbit review on PR #2603: - Numeric-input rule now states min is field-specific: plan_bounce_passes and max_discuss_passes require >= 1; other numeric fields accept >= 0. Resolves the inconsistency between the global rule and the field-level prompts (CodeRabbit comment 3127136557). - Adds 'text' fence language to seven previously unlabeled code blocks in the workflow (six AskUserQuestion sections plus the confirmation banner) to satisfy markdownlint MD040 (CodeRabbit comment 3127136561). * test(settings-advanced): tighten section assertion, fix misleading test name, add executable numeric-input coverage Addresses CodeRabbit review on PR #2603: - Required section list now asserts the full 'Runtime / Output' heading rather than the looser 'Runtime' substring (comment 3127136564). - Renames the subagent_timeout coercion test to match the actual key under test (was titled 'context_window' but exercised workflow.subagent_timeout — comment 3127136573). - Adds two executable behavioral tests at the config-set boundary (comment 3127136579): * Non-numeric input on a numeric key currently lands as a string — locks in that the workflow's AskUserQuestion re-prompt loop is the layer responsible for type rejection. If a future change adds CLI-side numeric validation, the assertion flips and the test surfaces it. * Numeric string on workflow.max_discuss_passes is coerced to Number — locks in the parser invariant for a second numeric key. --- commands/gsd/settings-advanced.md | 39 ++ docs/COMMANDS.md | 23 + docs/CONFIGURATION.md | 1 + docs/INVENTORY-MANIFEST.json | 6 +- docs/INVENTORY.md | 6 +- get-shit-done/bin/lib/config-schema.cjs | 1 + get-shit-done/workflows/settings-advanced.md | 435 +++++++++++++++++++ get-shit-done/workflows/settings.md | 1 + tests/gsd-settings-advanced.test.cjs | 377 ++++++++++++++++ 9 files changed, 885 insertions(+), 4 deletions(-) create mode 100644 commands/gsd/settings-advanced.md create mode 100644 get-shit-done/workflows/settings-advanced.md create mode 100644 tests/gsd-settings-advanced.test.cjs 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); + }); +});