* enhance(#4285): resolve context-monitor fire-points from .planning/config.json
The monitor's WARNING (35%) and CRITICAL (25%) fire-points were module
constants, so the only way to tune them was editing gsd-context-monitor.js —
a file in the MANAGED hooks registry, whose body the next install re-stages,
silently discarding the edit. The alternative was turning the safety net off.
Both are now readable from the config block the hook already opens:
hooks.context_warning_threshold and hooks.context_critical_threshold. Absent
keys resolve to today's 35/25, so every existing project is byte-identical.
Resolution is total and never throws — this hook must not block the tool call
it rides in on. A value is usable only if Number.isFinite (type-strict, so the
string "30" and true are rejected) and inside the 0-100 domain of the
remaining_percentage it is compared against; anything else falls back to the
default. The PAIR falls back together: critical >= warning has no coherent
reading, and honouring one side silently picks which of the operator's two
numbers to discard. That also covers a single override contradicting the other
key's default.
config-set validates the domain per key so accept and honour agree, but
deliberately does not enforce the pair — it writes one key per call, so a
two-step retune is transiently inconsistent on disk and refusing it there
would block a legitimate configuration.
Registration follows the statusline.show_git precedent: schema manifest plus
src/config.cts validation, not config-defaults.manifest.json and not
buildNewProjectConfig — emitting 35/25 into every new project would pin the
defaults at creation time for a setting nobody has tuned.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsUAawHKUy9pCpnye1Jzd2
* enhance(#4285): address Codex review — per-key fallback docs, discriminating tests
Codex full-PR review (gpt-6-astra, read-only) returned five findings. Each was
verified against source before acting; all five are real.
1. docs/CONFIGURATION.md described the wrong fallback. An out-of-domain value
falls back PER KEY; both defaults apply only when the RESOLVED pair violates
critical < warning. warning 150 with critical 30 resolves to 35/30, not
35/25 — at remaining 28 that difference changes the severity emitted. The
table now states the two rules in the order they compose, and
docs/context-monitor.md gains the same worked example.
2. The inconsistent-pair test could not prove the CRITICAL side reverts: its
pair was 20/25, and 25 is already the default, so an implementation that
reset only `warning` passed it. A 45/50 pair — both halves away from their
defaults — now pins each side with its own reading, and an equal 45/45 pair
pins that the rule is strict (`<`, not `<=`).
3. The rejection table's rows could not tell rejection from acceptance: an
accepted -5 pairs with the default critical 25, trips the pair check, and
produces the same silence. Two rows now separate those: a below-domain
critical must escalate remaining 20 to CRITICAL (proving -5 was rejected,
not honoured), and an unusable critical beside a usable warning 45 must
still fire WARNING at remaining 40 (proving per-key fallback rather than
reset-both). The over-claiming comments are narrowed to what each row
actually shows.
4. Scope, reproduced rather than assumed: config-set writes through
planningDir(), so under GSD_WORKSTREAM it lands in
.planning/workstreams/<name>/config.json while this hook reads only
<cwd>/.planning/config.json. That is the pre-existing root-only scope
hooks.context_warnings has always had, but this PR advertises the setter
route, so both docs now say the keys are root-project settings.
5. Four other English docs still stated 35/25 as fixed: the REQ-CTX-02/03
requirements fragment, ARCHITECTURE.md's hook table and threshold table,
and INVENTORY.md's hook row. All now name them as defaults and point at the
config keys; docs/FEATURES.md is regenerated from its fragment via
scripts/gen-features.cjs --write, not hand-edited.
Four new mutations, each reverted after: resetting only the warning half on an
inconsistent pair (1 red), resetting both on any unusable key (1), dropping the
>= 0 bound (1), and accepting critical == warning (1). perf-317 is 116/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsUAawHKUy9pCpnye1Jzd2
* enhance(#4285): tighten claims after Codex round 2 — scoped paths, one more discriminator
Confirmation round found no runtime defect and confirmed the five round-1 fixes
landed. Four precision items, all real, all fixed here.
1. The scoped-write note named the wrong path for GSD_PROJECT. planningDir()
composes three distinct shapes, confirmed by running config-set under each:
.planning/<project>/config.json, .planning/workstreams/<ws>/config.json, and
.planning/<project>/workstreams/<ws>/config.json. docs/context-monitor.md
now tabulates all four cases instead of collapsing them into one.
2. The 45/50 silence row asserted empty stdout without pinning the exit code.
runMonitorRaw turns a spawn failure, a non-zero exit or a timeout into empty
stdout as well, so the row could have passed on a dead child. It asserts
exitCode === 0 first now, like the equal-pair row already did.
3. The sibling row's message claimed it proved critical fell back to 25. It
does not: coercing '30' to 30 yields WARNING at remaining 40 too, so the row
pins the WARNING side surviving and nothing more. Message narrowed, and a
new row reads the same config at remaining 28, where the two candidate
resolutions diverge — rejected gives (45, 25) and WARNING, coerced gives
(45, 30) and CRITICAL. Mutation-verified: swapping Number.isFinite for the
coercing global reds it.
4. "Accept and honour must agree" was too absolute in the src/config.cts and
tests/config.test.cjs comments. The agreement holds on the DOMAIN and per
key: an accepted value can still lose to the hook's pair check at read time,
and a scoped write never reaches the hook at all. Likewise a two-step retune
only CAN be transiently inconsistent — 35/25 to 20/10 is valid throughout if
critical moves first — so the docs now say what a setter-side pair check
would actually cost: rejecting that intermediate write and forcing an order.
The same over-absolute phrasing is in b7d179c89's message, which is left as
written rather than rewriting history; this commit and the PR body carry the
precise claim.
perf-317 117/0, config 192/0, config-field-docs 47/0, features-index-gate 84/0,
lint:ci clean cold.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsUAawHKUy9pCpnye1Jzd2
* chore(#4285): add changeset
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsUAawHKUy9pCpnye1Jzd2
* enhance(#4285): address review — planning-config rows, resolveThresholds properties
Two Minor findings from the maintainer review, no behaviour change.
Minor 1: gsd-core/references/planning-config.md's "Hook Fields" table gains
rows for hooks.context_warning_threshold and hooks.context_critical_threshold,
in that table's 5-column form, carrying the same per-key-fallback,
pair-reversion and root-config-scope claims docs/CONFIGURATION.md already
makes. hooks.workflow_guard's absence from that table is pre-existing and
out of scope here.
Minor 2: resolveThresholds() gets fast-check property coverage, which ADR 456
requires of a threshold/limit contract. Reaching it needed a require-time
seam: the resolver was previously observable only by spawning the hook, and a
subprocess per case cannot drive 200 runs — the same conclusion CONTEXT-INDEX
records for the ROADMAP Requirements parser. The stdin adapter therefore moves
into main() behind `require.main === module`, mirroring
gsd-cursor-subagent-start.js and gsd-statusline.js, and module.exports exposes
the resolver plus both default constants so a test asserts fallback against
the source of truth rather than a second copy of 35/25. Spawned behaviour is
unchanged: the 10s stdin timeout still arms per invocation (stdinTimeout is
now a module-scope let assigned in main(), still cleared by the end handler),
and the try/catch crash(ON_CRASH) path is untouched.
Seven properties: totality, ordering, exactness, togetherness, non-vacuity,
per-key fallback, non-object argument. Exactness is stated PER KEY — a mixed
result (one key honoured, one fallen back) is legal and is the documented
contract; the property falsified a per-pair phrasing of it in 4 runs.
Verified: cold lint:ci 0; perf-317 file 125/0; seven mutations killed and
restored, one of which (upper bound widened to 120) is invisible to the 17
hand-written cases and caught only by a property.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZw5UhR474YLyE4knjHrte
* enhance(#4285): close the Codex-found gap in the property coverage
Codex whole-PR review of round 3 returned no Blocker and no Major. Two items,
both in the tests added this round, both verified against source before acting.
Minor — the per-key fallback property was asymmetric: it required a usable
warning to survive an unusable critical, but never the reverse. A resolver
that reverted BOTH keys the moment warning was unusable passed all seven
properties. Reproduced exactly: that mutant answers 35/25 for
{warning: 150, critical: 30} where the resolver answers 35/30, and the file
stayed green at 125/0. The mirrored property closes it — with the mutant
re-applied it is now the single failing row, and it is the only row that
fails, so it is load-bearing rather than incidental.
Nit — the ordering property's comment credited it with catching a
half-honoured pair, which it does not: 45/50 "repaired" by resetting only
critical yields 45/25, perfectly ordered. That case belongs to togetherness.
The same comment claimed the behavioural rows sample an inconsistent pair at
exactly one point; stale — they cover 20/25, 45/50 and the 45/45 equality
boundary. Both claims corrected in place.
Verified: cold lint:ci 0; perf-317 file 126/0; the mutant above killed by the
new property alone and the hook restored byte-identical afterwards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZw5UhR474YLyE4knjHrte
* enhance(#4285): name the installed-monitor prerequisite; close the negative-critical gap
Second Codex whole-PR pass, run because the base moved: the author's three
"Update branch" merges pulled ~26 upstream commits in, so the previously
reviewed diff sat on a base that no longer exists. No Blocker, no Major, two
Minor — both verified against source before acting.
Minor 1, and only reachable because of what the merge brought in: #2586
(03738824d) landed in that window and stops staging
hooks/gsd-context-monitor.js for Codex, since the metrics bridge it reads is
written only by hooks/gsd-statusline.js, which Codex never installs
(bin/install.js: "gsd-context-monitor.js is deliberately NOT copied for
Codex"). These two keys are read by that hook and nothing else, so on such a
runtime config-set stores and validates them and nothing consumes them — a
claim the docs this PR adds did not make. docs/context-monitor.md now carries
the explanation and both key tables carry a clause pointing at it; the FEATURES
and INVENTORY entries already link through to those two files, so they are not
edited again. The changeset says it too, because it is user-facing.
Accepting the keys on every runtime is kept deliberately: config is shared
across runtimes, so validation stays runtime-independent and the runtime
caveat lives in documentation rather than in the setter.
Minor 2: the per-key fallback property's junk generator had no negative arm,
though its mirror did — and that asymmetry hid a gap. A resolver reverting
BOTH keys whenever critical is negative answers 35/25 for {45, -5} where the
resolver answers 45/25, and it passed all 126 tests. With the negative arm
added it is the single failing row.
Verified: cold lint:ci 0; perf-317 126/0; both mutants above killed and the
hook restored byte-identical; 538/0 across the config, changeset, doc-parity
and emitted-attribution gates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZw5UhR474YLyE4knjHrte
* enhance(#4285): refuse the two dead threshold endpoints; resolve absent keys
Maintainer review round 2 raised two Minors and a nit.
Minor 1 — `hooks.context_warning_threshold: 0` was accepted and stored but can
never take effect: `critical < warning` must hold and both sides are clamped to
0-100, so nothing can sit below a warning of 0. Verifying it surfaced the MIRROR
case the review did not name: `critical: 100` is equally dead, since nothing can
sit above it. Both confirmed against the real resolver for partners {absent, 0,
50, 100}, with 0.001 and 99.999 honoured as controls.
`config-set` now refuses both, because storing a value the reader always
discards is the accept-then-discard shape this codebase refuses elsewhere. The
hook is unchanged and still total — it degrades to defaults rather than
throwing, so a project that already carries one of these on disk still loads.
The old "accepts the domain bounds 0 and 100" row asserted the misleading half
and is replaced by tables that make the asymmetry the point (0 is legal for
critical and illegal for warning; 100 is the reverse), plus a control row so
"refuse both endpoints outright" would not pass in its place.
Minor 2 — the keys are absent from config-defaults.manifest.json /
buildNewProjectConfig where the sibling `hooks.context_warnings` lives. Kept
that way: buildNewProjectConfig writes a hooks object into every NEW project's
config.json, which would freeze today's fire-points as an explicit per-project
override everywhere — the opposite of this PR's premise. But the underlying
complaint was real, so the actual symptom is fixed: `config-get` on an absent
key returned "Key not found" while the hook silently used 35/25. It now resolves
through SCHEMA_DEFAULTS. Restated rather than derived because CONFIG_DEFAULTS is
re-exported flattened and has no `hooks` member at runtime; the one resulting
copy of 35/25 outside the hook is pinned against the hook's exported constants
by a drift test (red-checked: moving the literal to 40 reds it).
Nit — PR-body counts unverifiable from the diff. Noted, no code change.
Codex round 3 then found a broken doc link (`context-monitor.md` resolved
inside gsd-core/references/, where it does not exist; the emitted tree's own
convention is `../../docs/...`) and a stale comment still describing the
manifest-derived approach I had backed out. Both fixed. It also corrected my
rationale on a point of fact: manifest entries alone would NOT have reached new
project configs, since buildNewProjectConfig builds its own literal — the
freezing argument applies to that function, not to the manifest. The comment now
says so rather than running the two together.
Verified: cold lint:ci 0; full suite 36,082 / 0 fail before these two fixes,
config + perf-317 321/0 after; drift pin red-checked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZw5UhR474YLyE4knjHrte
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
37 KiB
<planning_config>
Configuration options for .planning/ directory behavior.
<config_schema>
"planning": {
"commit_docs": true,
"pr_strict": false,
"search_gitignored": false
},
"git": {
"branching_strategy": "none",
"base_branch": null,
"protected_branches": ["develop", "staging"],
"phase_branch_template": "gsd/phase-{phase}-{slug}",
"milestone_branch_template": "gsd/{milestone}-{slug}",
"quick_branch_template": null
},
"manager": {
"flags": {
"discuss": "",
"plan": "",
"execute": ""
}
}
| Option | Default | Description |
|---|---|---|
commit_docs |
true |
Whether to commit planning artifacts to git |
pr_strict |
false |
Filter mode for /gsd:pr-branch. false keeps structural planning state (STATE.md, ROADMAP.md, MILESTONES.md, PROJECT.md, REQUIREMENTS.md, milestones/) in the PR branch; true drops every .planning/ path |
search_gitignored |
false |
Add --no-ignore to broad rg searches |
git.branching_strategy |
"none" |
Git branching approach: "none", "phase", or "milestone" |
git.base_branch |
null (auto-detect) |
Target branch for PRs and merges (e.g. "master", "develop"). When null, auto-detects from git symbolic-ref refs/remotes/origin/HEAD, falling back to "main". |
git.protected_branches |
(none) | Optional array of non-empty strings naming additional shared branches that should trigger protected-branch warnings |
git.create_tag |
true |
Create git tags on milestone completion |
git.phase_branch_template |
"gsd/phase-{phase}-{slug}" |
Branch template for phase strategy |
git.milestone_branch_template |
"gsd/{milestone}-{slug}" |
Branch template for milestone strategy |
git.quick_branch_template |
null |
Optional branch template for quick-task runs |
workflow.use_worktrees |
true |
Whether executor agents run in isolated git worktrees. Set to false to disable worktrees — agents execute sequentially on the main working tree instead. Recommended for solo developers or when worktree merges cause issues. Note: if your branch is ahead of origin/HEAD (a diverged milestone or feature branch), GSD auto-degrades to sequential and prints a warning; set worktree.baseRef:"head" in .claude/settings.local.json to restore parallel execution. See the branch-divergence note below. |
workflow.subagent_timeout |
300000 |
Timeout in milliseconds for parallel subagent tasks (e.g. codebase mapping). Increase for large codebases or slower models. Default: 300000 (5 minutes). |
workflow.inline_plan_threshold |
2 |
Plans with this many tasks or fewer execute inline (Pattern C) instead of spawning a subagent. Avoids ~14K token spawn overhead for small plans. Set to 0 to always spawn subagents. |
workflow.test_command |
null |
Custom shell command run as the regression/test gate by execute-phase, audit-fix, and post-merge-gate. When unset, GSD auto-detects (Makefile / package.json / Cargo.toml / go.mod / pyproject.toml). Example: npm test. |
workflow.build_command |
null |
Custom shell command run as the build gate by the post-merge gate. When unset, the build step is skipped/auto-detected. Example: npm run build. |
workflow.inline_plan_threshold |
2 |
Plans with this many tasks or fewer execute inline (Pattern C) instead of spawning a subagent. Avoids ~14K token spawn overhead for small plans. Set to 0 to always spawn subagents. |
manager.flags.discuss |
"" |
Flags passed to /gsd:discuss-phase when dispatched from manager (e.g. "--auto --analyze") |
manager.flags.plan |
"" |
Flags passed to plan workflow when dispatched from manager |
manager.flags.execute |
"" |
Flags passed to execute workflow when dispatched from manager |
response_language |
null |
Language for user-facing questions and prompts across all phases/subagents (e.g. "Portuguese", "Japanese", "Spanish"). When set, all spawned agents include a directive to respond in this language. |
| </config_schema> |
git.protected_branches has no persisted default. When it is absent, only the resolved base branch
is protected, preserving existing project behavior. Every configured item must be a non-empty
string. The configured list extends the resolved base branch; it never replaces the base or changes
the resolution ladder. A match produces an advisory warning at execute-phase and ship and does not
change git.branching_strategy: "none".
Matching is by exact branch name — there is no glob or prefix support, so a git-flow
layout must name each release/* or hotfix/* branch it wants protected. An entry that
is not a non-empty string is ignored with a warning naming it, and the remaining names
still apply.
{
"git": {
"branching_strategy": "none",
"protected_branches": ["develop", "staging"]
}
}
<commit_docs_behavior>
When commit_docs: true (default):
- Planning files committed normally
- SUMMARY.md, STATE.md, ROADMAP.md tracked in git
- Full history of planning decisions preserved
When commit_docs: false:
- Skip all
git add/git commitfor.planning/files - User must add
.planning/to.gitignore - Useful for: OSS contributions, client projects, keeping planning private
Using gsd-tools query (preferred):
# Commit with automatic commit_docs + gitignore checks:
gsd_run query commit "docs: update state" --files .planning/STATE.md
# Load config via state load (returns JSON):
INIT=$(gsd_run query state.load)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
# commit_docs is available in the JSON output
# Or use init commands which include commit_docs:
INIT=$(gsd_run query init.execute-phase "1")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
# commit_docs is included in all init command outputs
Auto-detection: If .planning/ is gitignored, commit_docs is automatically false regardless of config.json. This prevents git errors when users have .planning/ in .gitignore.
Per-phase override: phase_commit_docs.<phase-id> (e.g. phase_commit_docs.03) overrides commit_docs for one phase only, and wins over both the explicit config value and gitignore auto-detection — see docs/CONFIGURATION.md#per-phase-override-phase_commit_docs for the full precedence chain and examples.
Commit via CLI (handles checks automatically):
gsd_run query commit "docs: update state" --files .planning/STATE.md
The CLI checks commit_docs config and gitignore status internally — no manual conditionals needed.
</commit_docs_behavior>
<search_behavior>
When search_gitignored: false (default):
- Standard rg behavior (respects .gitignore)
- Direct path searches work:
rg "pattern" .planning/finds files - Broad searches skip gitignored:
rg "pattern"skips.planning/
When search_gitignored: true:
- Add
--no-ignoreto broad rg searches that should include.planning/ - Only needed when searching entire repo and expecting
.planning/matches
Note: Most GSD operations use direct file reads or explicit paths, which work regardless of gitignore status.
</search_behavior>
<setup_uncommitted_mode>
To use uncommitted mode:
-
Set config:
"planning": { "commit_docs": false, "search_gitignored": true } -
Add to .gitignore:
.planning/ -
Existing tracked files: If
.planning/was previously tracked:git rm -r --cached .planning/ git commit -m "chore: stop tracking planning docs" -
Branch merges: When using
branching_strategy: phaseormilestone, thecomplete-milestoneworkflow automatically strips.planning/files from staging before merge commits whencommit_docs: false.
</setup_uncommitted_mode>
<branching_strategy_behavior>
Branching Strategies:
| Strategy | When branch created | Branch scope | Merge point |
|---|---|---|---|
none |
Never | N/A | N/A |
phase |
At execute-phase start |
Single phase | User merges after phase |
milestone |
At first execute-phase of milestone |
Entire milestone | At complete-milestone |
When git.branching_strategy: "none" (default):
- All work commits to current branch
- Standard GSD behavior
When git.branching_strategy: "phase":
execute-phasecreates/switches to a branch before execution- Branch name from
phase_branch_template(e.g.,gsd/phase-03-authentication) - All plan commits go to that branch
- User merges branches manually after phase completion
complete-milestoneoffers to merge all phase branches
When git.branching_strategy: "milestone":
- First
execute-phaseof milestone creates the milestone branch - Branch name from
milestone_branch_template(e.g.,gsd/v1.0-mvp) - All phases in milestone commit to same branch
complete-milestoneoffers to merge milestone branch to main
Template variables:
| Variable | Available in | Description |
|---|---|---|
{phase} |
phase_branch_template | Zero-padded phase number (e.g., "03") |
{slug} |
Both | Lowercase, hyphenated name |
{milestone} |
milestone_branch_template | Milestone version (e.g., "v1.0") |
Checking the config:
Use init execute-phase which returns all config as JSON:
INIT=$(gsd_run query init.execute-phase "1")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
# JSON output includes: branching_strategy, phase_branch_template, milestone_branch_template
Or use state load for the config values:
INIT=$(gsd_run query state.load)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
# Parse branching_strategy, phase_branch_template, milestone_branch_template from JSON
Branch creation:
# For phase strategy
if [ "$BRANCHING_STRATEGY" = "phase" ]; then
PHASE_SLUG=$(echo "$PHASE_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
BRANCH_NAME=$(echo "$PHASE_BRANCH_TEMPLATE" | sed "s/{phase}/$PADDED_PHASE/g" | sed "s/{slug}/$PHASE_SLUG/g")
git checkout -b "$BRANCH_NAME" 2>/dev/null || git checkout "$BRANCH_NAME"
fi
# For milestone strategy
if [ "$BRANCHING_STRATEGY" = "milestone" ]; then
MILESTONE_SLUG=$(echo "$MILESTONE_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
BRANCH_NAME=$(echo "$MILESTONE_BRANCH_TEMPLATE" | sed "s/{milestone}/$MILESTONE_VERSION/g" | sed "s/{slug}/$MILESTONE_SLUG/g")
git checkout -b "$BRANCH_NAME" 2>/dev/null || git checkout "$BRANCH_NAME"
fi
Merge options at complete-milestone:
| Option | Git command | Result |
|---|---|---|
| Squash merge (recommended) | git merge --squash |
Single clean commit per branch |
| Merge with history | git merge --no-ff |
Preserves all individual commits |
| Delete without merging | git branch -D |
Discard branch work |
| Keep branches | (none) | Manual handling later |
Squash merge is recommended — keeps main branch history clean while preserving the full development history in the branch (until deleted).
Use cases:
| Strategy | Best for |
|---|---|
none |
Solo development, simple projects |
phase |
Code review per phase, granular rollback, team collaboration |
milestone |
Release branches, staging environments, PR per version |
</branching_strategy_behavior>
<complete_field_reference>
Complete Field Reference
Generated from CONFIG_DEFAULTS (configuration.cjs) and VALID_CONFIG_KEYS (config-schema.cjs).
Core Fields
| Key | Type | Default | Allowed Values | Description |
|---|---|---|---|---|
model_profile |
string | "balanced" |
"quality", "balanced", "budget", "adaptive", "inherit" |
Model selection preset for subagents |
mode |
string | "interactive" |
"interactive", "yolo" |
Operation mode: "interactive" shows gates and confirmations; "yolo" runs autonomously without prompts |
granularity |
string | (none) | "coarse", "standard", "fine" |
Planning depth for phase plans (migrated from deprecated depth) |
commit_docs |
boolean | true |
true, false |
Commit .planning/ artifacts to git (auto-false if .planning/ is gitignored) |
search_gitignored |
boolean | false |
true, false |
Include gitignored paths in broad rg searches via --no-ignore |
phase_naming |
string | "sequential" |
"sequential", "custom" |
Phase numbering: auto-increment or arbitrary string IDs |
project_code |
string|null | null |
Any short string | Prefix for phase dirs (e.g., "CK" produces CK-01-foundation) |
response_language |
string|null | null |
Any language name | Language for user-facing prompts (e.g., "Portuguese", "Japanese") |
context_window |
number | 200000 |
200000, 1000000 |
Context window size; set 1000000 for 1M-context models |
resolve_model_ids |
boolean|string | false |
false, true, "omit" |
Map model aliases to full Claude IDs; "omit" returns empty string |
context |
string|null | null |
"dev", "research", "review" |
Execution context profile that adjusts agent behavior: "dev" for development tasks, "research" for investigation/exploration, "review" for code review workflows |
review.models.<cli> |
string|null | null |
Any model ID string | Per-CLI model override for /gsd:review (e.g., review.models.gemini). Falls back to CLI default when null. |
review.max_prompt_tokens |
number|null | null |
Any positive integer, or null |
Central, cross-lane default cap (in estimated tokens) on the assembled review prompt; null means no trim. A per-lane review.max_prompt_tokens_per_reviewer.<slug> value overrides it for that lane: -1 means unset (inherits this global default), 0 means "do not trim that lane" (not unset — it is an explicit, standing opt-out). Alias: max_prompt_tokens is the flat-key form used in CONFIG_DEFAULTS; review.max_prompt_tokens is the canonical namespaced form. |
Workflow Fields
Set via workflow.* namespace in config.json (e.g., "workflow": { "research": true }).
| Key | Type | Default | Allowed Values | Description |
|---|---|---|---|---|
workflow.research |
boolean | true |
true, false |
Run research agent before planning |
workflow.plan_check |
boolean | true |
true, false |
Run plan-checker agent to validate plans. Alias: plan_checker is the flat-key form used in CONFIG_DEFAULTS; workflow.plan_check is the canonical namespaced form. |
workflow.verifier |
boolean | true |
true, false |
Run verifier agent after execution |
workflow.nyquist_validation |
boolean | true |
true, false |
Enable Nyquist-inspired validation gates |
workflow.auto_prune_state |
boolean | false |
true, false |
Automatically prune old STATE.md entries on phase completion (keeps 3 most recent phases) |
workflow.auto_advance |
boolean | false |
true, false |
Auto-advance to next phase after completion |
workflow.node_repair |
boolean | true |
true, false |
Attempt automatic repair of failed plan nodes |
workflow.node_repair_budget |
number | 2 |
Any positive integer | Max repair retries per failed node |
workflow.smart_zone_tokens |
number | 100000 |
Any positive integer | Smart-zone token budget for phase-effort estimation (#2630, ADR-2629). A phase whose estimate exceeds this is flagged with a split recommendation — advisory only, never a block. A policy default, not a benchmark constant: degradation begins before the advertised context window is full, but the effective ceiling is model- and task-dependent, so the calibration loop corrects it per project. Alias: smart_zone_tokens is the flat-key form used in CONFIG_DEFAULTS; workflow.smart_zone_tokens is the canonical namespaced form. |
workflow.ai_integration_phase |
boolean | true |
true, false |
Run /gsd:ai-integration-phase before planning AI system phases |
workflow.api_coverage_gate |
boolean | true |
true, false |
Require an explicit API-coverage decision (full-by-default, opt-out-not-opt-in) before a phase that integrates an external API/SDK/service can seal. At plan:pre prompts a COVERAGE.md matrix; at verify:pre a blocking gate fails the seal unless the matrix exists with every non-integrated capability an explicit, reasoned opt-out (#1562) |
workflow.ui_phase |
boolean | true |
true, false |
Generate UI-SPEC.md for frontend phases |
workflow.ui_safety_gate |
boolean | true |
true, false |
Require safety gate approval for UI changes |
workflow.text_mode |
boolean | false |
true, false |
Use plain-text numbered lists instead of AskUserQuestion menus |
workflow.compact_content |
boolean | false |
true, false |
Compact content mode (#4139, ADR-4139) — per-project boolean selecting terser payloads. Six workflows branch on it via spine+detail: plan-phase (#4402, pilot), execute-phase, docs-update, new-project, verify-work, complete-milestone (#4405). The rest of the eager-window corpus was reviewed and recorded as not worth splitting (docs/PARTITION-RULES.md). Lazily-Read workflow fragments and gsd-core/templates/** templates use a .compact.md sibling instead (#4406, gsd-core/references/compact-content-gate.md § "Streams 1b and 4") — wired today for help --full and the sequential-execution SUMMARY.md/USER-SETUP.md reads |
workflow.research_before_questions |
boolean | false |
true, false |
Run research before interactive questions in discuss phase (also honored on the /gsd:quick path, #3894). Alias: research_before_questions is the flat-key form used in CONFIG_DEFAULTS; workflow.research_before_questions is the canonical namespaced form. |
workflow.discuss_mode |
string | "discuss" |
"discuss", "assumptions" |
Default mode for discuss-phase: "discuss" runs interactive questioning; "assumptions" analyzes codebase and surfaces assumptions instead |
workflow.skip_discuss |
boolean | false |
true, false |
Skip discuss phase entirely |
workflow.use_worktrees |
boolean | true |
true, false |
Run executor agents in isolated git worktrees |
workflow.subagent_timeout |
number | 300000 |
Any positive integer (ms) | Timeout for parallel subagent tasks (default: 5 minutes) |
workflow.inline_plan_threshold |
number | 2 |
0–10 |
Plans with ≤N tasks execute inline instead of spawning a subagent |
workflow.test_command |
string|null | null |
Any shell command | Regression/test gate command run by execute-phase, audit-fix, and post-merge-gate. Unset → GSD auto-detects (Makefile / package.json / Cargo.toml / go.mod / pyproject.toml). |
workflow.build_command |
string|null | null |
Any shell command | Build gate command run by the post-merge gate. Unset → build step auto-detected/skipped. |
workflow.mvp_mode |
boolean | false |
true, false |
Persist the MVP-mode flag in config so every phase defaults to MVP framing without requiring --mvp on the CLI. Resolved via the chain: --mvp CLI flag → ROADMAP.md **Mode:** mvp field → this config value → false. When true, the planner, executor, verifier, and discovery surfaces (progress, stats, graphify) all treat the phase as an MVP vertical slice (UI → API → DB) of one user-visible capability. |
workflow.context_guard_mode |
string | "warn" |
"auto", "warn", "off" |
Context exhaustion guard mode for execute-phase. Before each wave, the orchestrator self-assesses context pressure using degradation signals from context-budget.md. "warn" (default): emit a warning and recommend /gsd:pause-work when POOR tier is detected. "auto": automatically invoke /gsd:pause-work before the next wave when POOR tier is detected. "off": disable the guard. The guard is heuristic — no programmatic context-% API exists. |
workflow.plan_chunked |
boolean | false |
true, false |
Enable chunked planning mode. When true, the plan-phase orchestrator splits the single long-lived planner Task into a short outline Task followed by N short per-plan Tasks (~3–5 min each). Each plan is committed individually for crash resilience. Particularly useful on Windows where long-lived Tasks may hang on stdio. Also activated by the --chunked flag. See planning.chunked_parallel below for concurrent per-plan dispatch. |
workflow.specless_probe_fallback |
boolean | true |
true, false |
Gate the SPEC-less probe fallback in plan-phase. When true (default), a phase that did not supply a ## Edge Coverage / ## Prohibitions SPEC section (header absent or present-but-empty) runs the existing probe protocol — the deterministic edge-probe.cjs for edges and an in-planner LLM recall pass for prohibitions — and authors the resulting predicates into PLAN.md must_haves (section-level precedence: a SPEC-supplied section is never re-run or overwritten). When false, the fallback is skipped but the skip is recorded: plan-phase emits a visible "probe fallback disabled" marker, never a silent skip. |
workflow.code_review_command |
string|null | null |
Any shell command | External code-review command integrated into /gsd:ship. The diff is piped to the command via stdin; the command must output JSON with a verdict field ("APPROVED" or "REVISE"). Non-zero exit or "REVISE" verdict blocks the ship workflow. When unset, the built-in review flow runs. Example: my-review-tool --review. |
workflow.inline_plan_threshold |
number | 2 |
0–10 |
Plans with ≤N tasks execute inline instead of spawning a subagent |
workflow.code_review |
boolean | true |
true, false |
Enable built-in code review step in the ship workflow |
workflow.code_review_depth |
string | "standard" |
"quick", "standard", "deep" |
Depth level for code review analysis in the ship workflow |
workflow.code_review_depth_overrides |
array | [] |
Array of {paths, depth} rule objects |
Ordered path-scoped depth rules for /gsd:code-review (#2554). Each rule's paths are matched against the review's changed-file set by whole-segment directory-path prefix (src/auth matches src/auth/token.ts, never src/authfoo/x.ts); matching is case-sensitive. Glob syntax (*, ?) is a configuration error. One matched file escalates the entire review — depth is not applied per file. Resolution order: --depth= flag → strongest matching rule → workflow.code_review_depth → standard. A malformed rule halts the review with a typed error rather than falling back silently. |
workflow._auto_chain_active |
boolean | false |
true, false |
Internal: tracks whether autonomous chaining is active |
workflow.security_enforcement |
boolean | true |
true, false |
Enable threat-model-anchored security verification via /gsd:secure-phase. When false, security checks are skipped entirely |
workflow.security_asvs_level |
number | 1 |
1, 2, 3 |
OWASP ASVS verification level. Level 1 = opportunistic, Level 2 = standard, Level 3 = comprehensive. Scales both planner threat-disposition rigor (which threats must be mitigated vs. accepted) and auditor verification depth (grep-level → boundary-placement check → full data-flow trace). See gsd-core/references/security-asvs-levels.md. |
workflow.security_block_on |
string | "high" |
"critical", "high", "medium", "low", "none" |
Minimum threat severity that blocks phase advancement. The auditor counts only open threats at or above this severity toward the blocking gate (SECURITY.md threats_open); none disables severity blocking. |
workflow.post_planning_gaps |
boolean | true |
true, false |
Post-planning gap report (#2493). After plans are generated, scans REQUIREMENTS.md and CONTEXT.md <decisions> against all PLAN.md files and emits a unified Source | Item | Status table. Non-blocking. Set to false to skip Step 13e of plan-phase. Alias: post_planning_gaps is the flat-key form used in CONFIG_DEFAULTS; workflow.post_planning_gaps is the canonical namespaced form. |
Ship Fields
Set via ship.* namespace in config.json. These fields affect /gsd:ship PRD-style pull request body composition only.
| Key | Type | Default | Allowed Values | Description |
|---|---|---|---|---|
ship.pr_body_sections |
array | [] |
Array of section objects | Append-only project-specific PR body sections. Each entry has heading, optional enabled, and one or more of source, template, or fallback. Disabled entries remain in onboarding config but do not render. Core sections remain required and cannot be removed or replaced. |
Git Fields
Set via git.* namespace (e.g., "git": { "branching_strategy": "phase" }).
| Key | Type | Default | Allowed Values | Description |
|---|---|---|---|---|
git.branching_strategy |
string | "none" |
"none", "phase", "milestone" |
Git branching approach for phase/milestone isolation |
git.base_branch |
string|null | null (auto-detect) |
Any branch name | Target branch for PRs and merges; auto-detects from origin/HEAD when null |
git.protected_branches |
array of non-empty strings | (none) | Non-empty branch names | Optional protected names added to the resolved base branch for execute-phase and ship warnings |
git.create_tag |
boolean | true |
true, false |
Create git tags on milestone completion |
git.phase_branch_template |
string | "gsd/phase-{phase}-{slug}" |
Template with {phase}, {slug} |
Branch naming template for phase strategy |
git.milestone_branch_template |
string | "gsd/{milestone}-{slug}" |
Template with {milestone}, {slug} |
Branch naming template for milestone strategy |
git.quick_branch_template |
string|null | null |
Template with {slug} |
Optional branch template for quick-task runs |
Search & API Fields
These toggle external search integrations. Auto-detected at project creation when API keys are present.
| Key | Type | Default | Allowed Values | Description |
|---|---|---|---|---|
brave_search |
boolean | false |
true, false |
Enable Brave web search for research agent (requires BRAVE_API_KEY) |
firecrawl |
boolean | false |
true, false |
Enable Firecrawl page scraping (requires FIRECRAWL_API_KEY) |
exa_search |
boolean | false |
true, false |
Enable Exa semantic search (requires EXA_API_KEY) |
Features Fields
Set via features.* namespace (e.g., "features": { "thinking_partner": true }).
| Key | Type | Default | Allowed Values | Description |
|---|---|---|---|---|
features.thinking_partner |
boolean | false |
true, false |
Enable conditional extended thinking at workflow decision points (used by discuss-phase and plan-phase for architectural tradeoff analysis) |
features.global_learnings |
boolean | false |
true, false |
Enable injection of global learnings from ~/.gsd/knowledge/ into agent prompts |
Hook Fields
Set via hooks.* namespace (e.g., "hooks": { "context_warnings": true }).
| Key | Type | Default | Allowed Values | Description |
|---|---|---|---|---|
hooks.context_warnings |
boolean | true |
true, false |
Show warnings when context budget is exceeded |
hooks.context_warning_threshold |
number | 35 |
Greater than 0 and at most 100, and strictly greater than hooks.context_critical_threshold. config-set refuses 0: nothing is below it, so no critical value could satisfy the pair |
Percent of context window REMAINING at or below which the monitor emits CONTEXT WARNING. An out-of-domain value falls back per key; both keys revert to their defaults only when the RESOLVED pair violates critical < warning. Read from the root project config — a workstream-scoped config-set does not reach this hook. Inert on a runtime with no context-monitor hook installed, Codex among them (#2586); see context-monitor.md (#4285) |
hooks.context_critical_threshold |
number | 25 |
At least 0 and less than 100, and strictly less than hooks.context_warning_threshold. config-set refuses 100: nothing is above it, so no warning value could satisfy the pair |
Percent of context window REMAINING at or below which the monitor escalates to CONTEXT CRITICAL. Setting only one of the pair is checked against the other's default, so tune both when moving either past the other. Same root-config scope, and the same installed-monitor prerequisite, as the key above (#4285) |
Learnings Fields
Set via learnings.* namespace (e.g., "learnings": { "max_inject": 5 }). Used together with features.global_learnings.
| Key | Type | Default | Allowed Values | Description |
|---|---|---|---|---|
learnings.max_inject |
number | 10 |
Any positive integer | Maximum number of global learning entries to inject into agent prompts per session |
Intel Fields
Set via intel.* namespace (e.g., "intel": { "enabled": true }). Controls the queryable codebase intelligence system consumed by /gsd:map-codebase --query.
| Key | Type | Default | Allowed Values | Description |
|---|---|---|---|---|
intel.enabled |
boolean | false |
true, false |
Enable queryable codebase intelligence system. When true, /gsd:map-codebase --query builds and queries a JSON index in .planning/intel/. |
Manager Fields
Set via manager.* namespace (e.g., "manager": { "flags": { "discuss": "--auto" } }).
| Key | Type | Default | Allowed Values | Description |
|---|---|---|---|---|
manager.flags.discuss |
string | "" |
Any CLI flags string | Flags passed to /gsd:discuss-phase from manager (e.g., "--auto --analyze") |
manager.flags.plan |
string | "" |
Any CLI flags string | Flags passed to plan workflow from manager |
manager.flags.execute |
string | "" |
Any CLI flags string | Flags passed to execute workflow from manager |
Advanced Fields
| Key | Type | Default | Allowed Values | Description |
|---|---|---|---|---|
parallelization |
boolean|object | true |
true, false, { "enabled": true } |
Enable parallel wave execution; object form allows additional sub-keys |
model_overrides |
object|null | null |
{ "<agent-type>": "<model-id>" } |
Override model selection per agent type |
agent_skills |
object | {} |
{ "<agent-type>": "<skill-set>" } or { "<agent-type>": ["<skill-set>", "<skill-set>", ...] } |
Assign skill sets to specific agent types. Each value is a single skill-set path (string) or an array of skill-set paths — the array form assigns multiple skill sets to one agent type. Paths cannot be comma-joined into one string; each path must be its own array element |
sub_repos |
array | [] |
Array of relative path strings | Child directories with independent .git repos (auto-detected) |
Planning Fields
These can be set at top level or nested under planning.* (e.g., "planning": { "commit_docs": false }). Both forms are equivalent; top-level takes precedence if both exist.
| Key | Type | Default | Allowed Values | Description |
|---|---|---|---|---|
planning.commit_docs |
boolean | true |
true, false |
Alias for top-level commit_docs |
planning.search_gitignored |
boolean | false |
true, false |
Alias for top-level search_gitignored |
planning.chunked_parallel |
boolean | false |
true, false |
Opt-in for workflow.plan_chunked's per-plan loop (§8.5.2 of chunked-planning-mode.md, #3777). When true, the runnable per-plan planners within one outline Wave are dispatched concurrently (one message, run_in_background=true each) instead of one at a time, honoring the outline's Wave column as the schedule (Depends On is expected to name only an earlier Wave and is not separately parsed — batching strictly by Wave already respects it). Gated on the negotiated dispatch-capacity query (#3673): a host that declares no maxConcurrency (capacity resolves to 1) stays serial regardless of this setting. Default false is byte-identical to the pre-#3777 serial loop. Trade-off: per-plan commits interleave within a batch instead of strictly one-at-a-time, and a stalled plan's retry no longer blocks sibling plans in the same batch from having already committed. |
Field Interactions
Several config fields affect each other or trigger special behavior:
-
commit_docsresolution chain -- Four tiers, highest wins: (1)phase_commit_docs.<phase-id>for the phase being committed, (2) an explicitcommit_docs(orplanning.commit_docs) value in config.json, (3).gitignoreauto-detection (.planning/in.gitignoreresolves tofalse), (4) the manifest default (true). Precedence: per-phase → explicit config → gitignore auto-detect → default. -
branching_strategycontrols branch templates -- Thephase_branch_templateandmilestone_branch_templatefields are only used whenbranching_strategyis set to"phase"or"milestone"respectively. Whenbranching_strategyis"none", all template fields are ignored. -
context_windowthreshold triggers -- Whencontext_window >= 500000, workflows enable adaptive context enrichment: full-body reads of prior phase SUMMARYs, cross-phase context injection in plan-phase, and deeper read depth for anti-pattern references. Below 500000, only frontmatter and summaries are read. -
parallelizationpolymorphism -- Accepts both a simple boolean and an object with anenabledfield.loadConfig()normalizes either form to a boolean.{ "enabled": true }is equivalent totrue. -
Search API keys and flags --
brave_search,firecrawl, andexa_searchare auto-set totrueduring project creation if the corresponding API key is detected (environment variable or~/.gsd/<name>_api_keyfile). Setting them totruewithout the API key has no effect. -
planning.*and top-level equivalence --planning.commit_docsandcommit_docsare equivalent;planning.search_gitignoredandsearch_gitignoredare equivalent. If both are set, the top-level value takes precedence. -
depthtogranularitymigration -- The deprecateddepthkey (quick/standard/comprehensive) is automatically migrated togranularity(coarse/standard/fine) on config load and persisted back to disk. -
sub_reposauto-sync -- On every config load, GSD scans for child directories with.gitand updates thesub_reposarray if the filesystem has changed. LegacymultiRepo: trueis automatically migrated to a detectedsub_reposarray. -
workflow.use_worktreesand branch divergence -- Whenuse_worktreesistrue(default), executor worktrees are forked fromorigin/HEAD-- by the host's own harness ondispatch.isolation: harness-worktreeruntimes (Claude Code, Cursor), or by GSD itself onorchestrator-worktreeruntimes (Codex, OpenCode, Kimi, Kimi Code). The divergence behavior below is identical either way, because the fork base is a property of the repository rather than of whoever creates the worktree. If your current branch has commits thatorigin/HEADdoes not (for example an unmerged milestone or feature branch), GSD automatically degrades to sequential execution for that run and prints a one-line⚠ Worktree base mismatchwarning. To restore parallel execution permanently, setworktree.baseRef:"head"in.claude/settings.local.json(rungsd_run worktree set-baseref). This makes the harness fork worktrees from the live HEAD instead oforigin/HEAD. Both fresh installs and upgrades of GSD Core set this automatically (no-clobber) whenuse_worktreesis enabled; you can also run the command manually at any time. Settingworkflow.use_worktrees: falseis the alternative if worktrees are not needed at all. On a runtime whose declareddispatch.isolationisnone, an explicittrueis a config the execution workflows fail closed on;/gsd:healthreports it as warningW025and/gsd:settingsoffers to repair it (#2486).
Example Configurations
Minimal -- Solo Developer
{
"model_profile": "balanced",
"commit_docs": true,
"workflow": {
"research": true,
"plan_check": true,
"verifier": true,
"use_worktrees": false
}
}
Team Project with Branching
{
"model_profile": "quality",
"commit_docs": true,
"project_code": "APP",
"git": {
"branching_strategy": "phase",
"base_branch": "develop",
"phase_branch_template": "gsd/phase-{phase}-{slug}"
},
"workflow": {
"research": true,
"plan_check": true,
"verifier": true,
"nyquist_validation": true,
"use_worktrees": true,
"discuss_mode": "discuss"
},
"manager": {
"flags": {
"discuss": "",
"plan": "",
"execute": ""
}
},
"response_language": "English"
}
Large Codebase -- 1M Context with Extended Timeouts
{
"model_profile": "quality",
"context_window": 1000000,
"commit_docs": true,
"project_code": "MEGA",
"phase_naming": "sequential",
"git": {
"branching_strategy": "milestone",
"milestone_branch_template": "gsd/{milestone}-{slug}"
},
"workflow": {
"research": true,
"plan_check": true,
"verifier": true,
"nyquist_validation": true,
"subagent_timeout": 600000,
"use_worktrees": true,
"node_repair": true,
"node_repair_budget": 3,
"auto_advance": true
},
"brave_search": true,
"hooks": {
"context_warnings": true
}
}
</complete_field_reference>
</planning_config>