* 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.
This commit is contained in:
39
commands/gsd/settings-advanced.md
Normal file
39
commands/gsd/settings-advanced.md
Normal file
@@ -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
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/get-shit-done/workflows/settings-advanced.md
|
||||
</execution_context>
|
||||
|
||||
<process>
|
||||
**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
|
||||
</process>
|
||||
@@ -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.
|
||||
|
||||
@@ -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/<source-path>` 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.<section>` (e.g. `claude_md_assembly.blocks.architecture: link`). Added in v1.38 |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 `<purpose>` 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` |
|
||||
|
||||
@@ -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',
|
||||
|
||||
435
get-shit-done/workflows/settings-advanced.md
Normal file
435
get-shit-done/workflows/settings-advanced.md
Normal file
@@ -0,0 +1,435 @@
|
||||
<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, 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.
|
||||
</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-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.
|
||||
</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`)
|
||||
|
||||
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" }
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
</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-sdk 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-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": <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>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
</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} |
|
||||
|
||||
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`
|
||||
- [ ] 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
|
||||
</success_criteria>
|
||||
@@ -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)
|
||||
```
|
||||
</step>
|
||||
|
||||
|
||||
377
tests/gsd-settings-advanced.test.cjs
Normal file
377
tests/gsd-settings-advanced.test.cjs
Normal file
@@ -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 <step name="${step}">`, () => {
|
||||
assert.ok(
|
||||
workflow.includes(`<step name="${step}">`),
|
||||
`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('<step name="confirm">');
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user