resolveWorktreeRoot() unconditionally resolved linked worktrees to the main
repo root. When a linked worktree has its own independent .planning/ directory
(e.g., Conductor workspaces), all GSD commands read/wrote the wrong planning
state. Add an early return that checks for a local .planning/ before falling
through to main repo resolution.
The caller in gsd-tools.cjs already had this guard (added in #1283), but the
function itself should be correct regardless of call site. This is defense-in-
depth for any future callers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three non-blocking findings from the adversarial re-review of the
workstream namespacing PR, addressed as a follow-up:
1. setActiveWorkstream now validates names with the same regex used
at CLI entry and cmdWorkstreamSet — defense-in-depth so future
callers can't poison the active-workstream file
2. Replaced tautological test assertion (result.success || !result.success
was always true) with actual validation that cmdWorkstreamSet returns
invalid_name error for path traversal attempts. Added 8 new tests
for setActiveWorkstream's own validation.
3. Updated stale comment in copilot-install.test.cjs (said 31, actual 56)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- core.cjs: add filterPlanFiles, filterSummaryFiles, getPhaseFileStats,
readSubdirectories helpers; use them in searchPhaseInDir and getArchivedPhaseDirs
- gsd-tools.cjs: add parseNamedArgs() helper; replace ~50 repetitive indexOf/ternary
patterns in state record-metric, add-decision, add-blocker, record-session,
begin-phase, signal-waiting, template fill, and frontmatter subcommands
- phase.cjs: decompose 250-line cmdPhaseRemove into renameDecimalPhases(),
renameIntegerPhases(), and updateRoadmapAfterPhaseRemoval(); import readSubdirectories
- workstream.cjs: import stateExtractField from state.cjs and shared helpers from
core.cjs; replace all inline regex state parsing and readdirSync+filter+map patterns
All 1062 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Validates workstream name at all entry points — CLI --ws flag,
GSD_WORKSTREAM env var, active-workstream file, and cmdWorkstreamSet —
blocking names that don't match [a-zA-Z0-9_-]+. Also fixes
getActiveWorkstream to use planningRoot() consistently and validates
names read from the active-workstream file before using them in path
joins.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Enable multiple Claude Code instances to work on the same codebase
simultaneously by scoping .planning/ state into workstreams.
Core changes:
- planningDir(cwd, ws?) and planningPaths(cwd, ws?) are now workstream-aware
via GSD_WORKSTREAM env var (auto-detected from --ws flag or active-workstream file)
- All bin/lib modules use planningDir(cwd) for scoped paths (STATE.md, ROADMAP.md,
phases/, REQUIREMENTS.md) and planningRoot(cwd) for shared paths (milestones/,
PROJECT.md, config.json, codebase/)
- New workstream.cjs module: create, list, status, complete, set, get, progress
- gsd-tools.cjs: --ws flag parsing with priority chain
(--ws > GSD_WORKSTREAM env > active-workstream file > flat mode)
- Collision detection: transition.md checks for other active workstreams before
suggesting next-milestone continuation (prevents WS A from trampling WS B)
- ${GSD_WS} routing propagation across all 9 workflow files ensures workstream
scope chains automatically through the workflow lifecycle
New files:
- get-shit-done/bin/lib/workstream.cjs (CRUD + collision detection)
- get-shit-done/commands/gsd/workstreams.md (slash command)
- get-shit-done/references/workstream-flag.md (documentation)
- tests/workstream.test.cjs (20 tests covering CRUD, env var routing, --ws flag)
All 1062 tests passing (1042 existing + 20 new workstream tests).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extend resolve_model_ids to accept "omit" value: returns empty string
so non-Claude runtimes (OpenCode, Codex, Gemini, etc.) use their
configured default model instead of unresolvable Claude aliases.
- resolve_model_ids: "omit" short-circuits before alias resolution
- model_overrides still respected (checked first) for explicit IDs
- Installer sets resolve_model_ids: "omit" in ~/.gsd/defaults.json
for non-Claude runtimes during install
- 4 new tests covering omit behavior and override passthrough
- Fix websearch test mocks for fs.writeSync output change (#1276)
Closes#1156
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
process.stdout.write() is async when stdout is a pipe. The
immediate process.exit(0) tears down the process before the
downstream reader (jq, python3, etc) consumes the buffer,
producing truncated JSON.
Replace with fs.writeSync(1, data) which blocks until the
kernel pipe buffer accepts the bytes, and drop process.exit(0)
on the success path so the event loop drains naturally.
Fixes#1275
Related: #493 (addressed >50KB case, this fixes <50KB)
Wire the --reviews flag through the full stack so plan-phase can
replan incorporating cross-AI review feedback from REVIEWS.md:
- core.cjs: add has_reviews detection in searchPhaseInDir
- init.cjs: wire has_reviews and reviews_path through all init functions
- plan-phase.md command: add --reviews to argument-hint and flags
- plan-phase.md workflow: add step 2.5 validation, skip research,
skip existing plans prompt, pass reviews_path to planner
- gsd-planner.md: add reviews_mode section for consuming review feedback
- COMMANDS.md: add --reviews and missing flags to docs
Closes the gap where --reviews was referenced in 6 places (review
workflow, review command, help workflow, COMMANDS.md, FEATURES.md)
but never implemented.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
core.cjs output() created gsd-*.json files in /tmp for large payloads
but never cleaned them up, causing unbounded disk usage (800+ GB
reported in #1251). Similarly, profile-pipeline.cjs created
gsd-pipeline-* and gsd-profile-* temp directories without cleanup.
Adds reapStaleTempFiles() that removes gsd-prefixed temp files/dirs
older than 5 minutes. Called opportunistically before each new temp
file/dir creation. Non-critical — cleanup failures never break output.
Closes#1251
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
loadConfig() defaulted commit_docs to true regardless of whether
.planning/ was gitignored. The documented auto-detection only existed
inside cmdCommit, so init commands returned commit_docs: true even
when .planning/ was in .gitignore. This caused LLM executors to
bypass the cmdCommit gate and re-commit planning files with raw git.
Now loadConfig() checks isGitIgnored(cwd, '.planning/') when no
explicit commit_docs value is set in config.json. If .planning/ is
gitignored, commit_docs defaults to false. An explicit commit_docs
value in config.json is always respected.
Added 5 regression tests covering auto-detection, explicit overrides,
and the no-config-file edge case.
Integrate Exa (semantic search) and Firecrawl (deep web scraping) as
MCP-based research tools, following the existing Brave Search pattern.
- Add tool declarations to all 3 researcher agents
- Add tool strategy sections with usage guidance and priority
- Add config detection for FIRECRAWL_API_KEY and EXA_API_KEY env vars
- Add firecrawl/exa_search config keys to core defaults and init output
- Update source priority hierarchy across all researchers
- findProjectRoot: use isInsideGitRepo() to walk up and find .git in
ancestor dirs, fixing nested paths like backend/src/modules/
- Add sub_repos to cmdInitExecutePhase output so execute-plan.md and
gsd-executor.md can route commits correctly
- Align new-project.md sub-repo detection to maxdepth 1 matching
detectSubRepos() behavior
- Add 3 nested path tests for .git heuristic, sub_repos, and multiRepo
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add support for workspaces with multiple independent git repositories.
When configured, GSD routes commits to the correct sub-repo and ensures
.planning/ stays at the project root.
Core features:
- detectSubRepos(): scans child directories for .git to discover repos
- findProjectRoot(): walks up from CWD to find the project root that
owns .planning/, preventing orphaned .planning/ in sub-repos
- loadConfig auto-syncs sub_repos when repos are added or removed
- Migrates legacy "multiRepo: true" to sub_repos array automatically
- All init commands include project_root in output
- cmdCommitToSubrepo: groups files by sub-repo prefix, commits independently
Zero impact on single-repo workflows — sub_repos defaults to empty array.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refactoring:
- Extract stateReplaceFieldWithFallback() to state.cjs as single source of truth
for the try-primary-then-fallback pattern that was duplicated inline across
phase.cjs, milestone.cjs, and state.cjs
- Replace all inline bold-only regex patterns in cmdPhaseComplete with shared
stateReplaceField/stateExtractField helpers — now supports both **Bold:**
and plain Field: STATE.md formats (fixes the same bug as #924)
- Replace inline bold-only regex patterns in cmdMilestoneComplete with shared helpers
- Replace inline bold-only regex for Completed Phases/Total Phases/Progress
counters in cmdPhaseComplete with stateExtractField/stateReplaceField
- Replace inline bold-only Total Phases regex in cmdPhaseRemove with shared helpers
Security:
- Fix command injection surface in isGitIgnored (core.cjs): replace execSync with
string concatenation with execFileSync using array arguments — prevents shell
interpretation of special characters in file paths
Tests (7 new):
- 5 tests for stateReplaceFieldWithFallback: primary field, fallback, neither,
preference, and plain format
- 1 regression test: phase complete with plain-format STATE.md fields
- 1 regression test: milestone complete with plain-format STATE.md fields
854 tests pass (was 847). No behavioral regressions.
- Add resolveWorktreeRoot() that detects linked worktrees via
git rev-parse --git-common-dir and resolves to the main worktree
where .planning/ lives
- Add withPlanningLock() file-based locking mechanism to prevent
concurrent worktrees from corrupting shared planning files
- Wire worktree root resolution into gsd-tools.cjs main entry point
- Add regression tests for resolveWorktreeRoot (non-git, normal repo)
and withPlanningLock (normal execution, error cleanup, stale lock
recovery)
The summary template puts the one-liner as a `**bold**` line after the
`# Phase N` heading, but `cmdSummaryExtract` and `cmdMilestoneComplete`
only checked frontmatter `one-liner` field — which is often empty.
Adds `extractOneLinerFromBody()` to core.cjs as a fallback that parses
the first `**...**` line after the heading.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add workflow.text_mode config option (default: false) that replaces
AskUserQuestion TUI menus with plain-text numbered lists
- Document --text flag and config-set workflow.text_mode true as the
fix for /rc remote sessions where the Claude App cannot forward TUI
menu selections
- Update discuss-phase.md with text mode parsing and answer_validation
fallback documentation
- Add text_mode to loadConfig defaults and VALID_CONFIG_KEYS
- Add regression tests for config-set and loadConfig
Claude Code's Task tool sometimes doesn't resolve short aliases (opus,
sonnet, haiku) and passes them directly to the API, causing 404s. Tasks
then inherit the parent session's model instead of the configured one.
Added:
- MODEL_ALIAS_MAP in core.cjs mapping aliases to full model IDs
- resolve_model_ids config option (default: false for backward compat)
- resolveModelInternal() maps aliases when resolve_model_ids is true
Usage:
{ "resolve_model_ids": true }
This causes gsd-tools resolve-model to return 'claude-sonnet-4-5' instead
of 'sonnet', which the Task tool passes to the API without needing alias
resolution on Claude Code's side.
The alias map is maintained per release. Users can also use model_overrides
for full control.
All 755 tests pass.
Fixes#991
stripShippedMilestones() uses a negative heuristic: strip all <details>
blocks, assume what remains is the current milestone. This breaks when
agents accidentally wrap the current milestone in <details> for
collapsibility — all downstream consumers then see an empty milestone.
Observed failure: cmdPhaseComplete() returns is_last_phase: true and
next_phase: null for non-final phases because the current milestone's
phases were stripped along with shipped ones.
Added extractCurrentMilestone(content, cwd) — a positive lookup that:
1. Reads the current milestone version from STATE.md frontmatter
2. Falls back to 🚧 in-progress marker in ROADMAP.md
3. Finds the section heading matching that version
4. Returns only that section's content
5. Falls back to stripShippedMilestones() if version can't be determined
Updated 12 call sites across 6 files to use extractCurrentMilestone:
- core.cjs: getRoadmapPhaseInternal(), getMilestonePhaseFilter()
- phase.cjs: cmdPhaseAdd(), cmdPhaseInsert(), cmdPhaseComplete() (2 sites)
- roadmap.cjs: cmdRoadmapGetPhase(), cmdRoadmapAnalyze()
- commands.cjs: stats/progress display
- verify.cjs: phase verification (2 sites)
- init.cjs: project initialization
Kept stripShippedMilestones() for:
- getMilestoneInfo() — determines the version itself, can't use positive lookup
- replaceInCurrentMilestone() — write operations, conservative boundary
- extractCurrentMilestone() fallback — when no version available
All 755 tests pass.
Fixes#1145
- fix(core): getMilestoneInfo() version regex `\d+\.\d+` only matched
2-segment versions (v1.2). Changed to `\d+(?:\.\d+)+` to support
3+ segments (v1.2.1, v2.0.1). Same fix in roadmap.cjs milestone
extraction pattern.
- fix(state): stripFrontmatter() used `^---\n` (LF-only) which failed
to strip CRLF frontmatter blocks. When STATE.md had dual frontmatter
blocks from prior CRLF corruption, each writeStateMd() call preserved
the stale block and prepended a new wrong one. Now handles CRLF and
strips all stacked frontmatter blocks.
- fix(frontmatter): extractFrontmatter() always used the first ---
block. When dual blocks exist from corruption, the first is stale.
Now uses the last block (most recent sync).
resolveModelInternal() was converting 'opus' to 'inherit', assuming
the parent process runs on Opus. When the orchestrator runs on Sonnet
(the default), 'inherit' resolves to Sonnet — silently downgrading
quality profile subagents.
Remove the opus→inherit conversion so the resolved model name is
passed through directly. Claude Code's Task tool now supports model
aliases like 'opus', 'sonnet', 'haiku'.
Fixes#695
* perf: make `/gsd:set-profile`'s implementation more programmatic
* perf: run the set-profile script without Claude needing to invoke it
- inject the script's output as dynamic context into the set-profile skill, so that Claude doesn't
need to invoke it and can simply read + print the output to the user
- reference: https://code.claude.com/docs/en/skills#inject-dynamic-context
* feat: improve output message for case where model profile wasn't changed
* feat: specify haiku model for set-profile command since it's so simple
* fix: remove ' (default)' from previousProfile to avoid false negative for didChange
* chore: add docstring to MODEL_PROFILES with note about the analogous markdown reference table
* chore: delete set-profile workflow file that's no longer needed
Phase numbers reset per milestone (v1.0 Phase 1-26, v1.1 Phase 1-6,
v1.2 Phase 1-6). Functions that searched ROADMAP.md for phase headings
or checkboxes would match archived milestone entries inside <details>
blocks before reaching the current milestone's entries.
This caused:
- roadmap_complete: true for phases that aren't complete (checkbox
regex matched the archived [x] Phase N from a previous milestone)
- phase-complete marking the wrong checkbox (archived instead of current)
- phase-add computing wrong maxPhase from archived headings
- progress not showing phases defined in ROADMAP but not yet on disk
Add pre-planning design contract generation and post-execution visual audit
for frontend phases. Closes the gap where execute-phase runs without a visual
contract, producing inconsistent spacing, color, and typography across components.
New agents: gsd-ui-researcher (UI-SPEC.md), gsd-ui-checker (6-dimension
validation), gsd-ui-auditor (6-pillar scored audit + registry re-vetting).
Third-party shadcn registry blocks are machine-vetted at contract time,
verification time, and audit time — three enforcement points, not a checkbox.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: change nyquist_validation default to true and harden absent-key skip conditions
new-project.md never wrote the key, so agents reading config directly
treated absent as falsy. Changed all agent skip conditions from "is false"
to "explicitly set to false; absent = enabled". Default changed from false
to true in core.cjs, config.cjs, and templates/config.json.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: enforce VALIDATION.md creation with verification gate and Check 8e
Step 5.5 was narrative markdown that Claude skipped under context pressure.
Now MANDATORY with Write tool requirement and file-existence verification.
Step 7.5 gates planner spawn on VALIDATION.md presence. Check 8e blocks
Dimension 8 if file missing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add W008/W009 health checks and addNyquistKey repair for Nyquist drift detection
W008 warns when workflow.nyquist_validation key is absent from config.json
(agents may skip validation). W009 warns when RESEARCH.md has Validation
Architecture section but no VALIDATION.md file exists. addNyquistKey repair
adds the missing key with default true value.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add /gsd:validate-phase command and gsd-nyquist-auditor agent
Retroactively applies Nyquist validation to already-executed phases.
Works mid-milestone and post-milestone. Detects existing test coverage,
maps gaps to phase requirements, writes missing tests, debugs failing
ones, and produces {phase}-VALIDATION.md from existing artifacts.
Handles three states: VALIDATION.md exists (audit + update), no
VALIDATION.md (reconstruct from PLAN.md + SUMMARY.md), phase not yet
executed (exit cleanly with guidance).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: audit-milestone reports Nyquist compliance gaps across phases
Adds Nyquist coverage table to audit-milestone output when
workflow.nyquist_validation is true. Identifies phases missing
VALIDATION.md or with nyquist_compliant: false/partial.
Routes to /gsd:validate-phase for resolution. Updates USER-GUIDE
with retroactive validation documentation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: compress Nyquist prompts to match GSD meta-prompt density conventions
Auditor agent: deleted philosophy section (35 lines), compressed execution
flow 60%, removed redundant constraints. Workflow: cut purpose bloat,
collapsed state narrative, compressed auditor spawn template. Command:
removed redundant process section. Plan-phase Steps 5.5/7.5: replaced
hedging language with directives. Audit-milestone Step 5.5: collapsed
sub-steps into inline instructions. Net: -376 lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The "depth" setting with values quick/standard/comprehensive implied
investigation thoroughness, but it only controls phase count. Renamed to
"granularity" with values coarse/standard/fine to accurately reflect what
it controls: how finely scope is sliced into phases.
Includes backward-compatible migration in loadConfig and config-ensure
that auto-renames depth→granularity with value mapping in both
.planning/config.json and ~/.gsd/defaults.json.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phases listed in ROADMAP.md but not yet planned (no directory on disk)
were excluded from total_phases, causing premature milestone completion.
Now uses Math.max(diskDirs, roadmapCount) via filter.phaseCount.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract getMilestonePhaseFilter() from milestone.cjs closure into core.cjs
as a shared helper. Apply it in buildStateFrontmatter and cmdPhaseComplete
so multi-milestone projects count only current milestone's phases instead
of all directories on disk.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
getMilestoneInfo matches the first version string in ROADMAP.md, which
is typically the oldest shipped milestone. For list-format roadmaps
that use emoji markers (e.g. "🚧 **v2.1 Belgium**"), the function now
checks for the in-progress marker first before falling back to the
heading-based and bare version matching.
Closes#700
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without --no-index, git check-ignore only reports files as ignored if
they are untracked. Once .planning/ files enter git's index (e.g., from
an initial commit before .gitignore was set up), check-ignore returns
"not ignored" even when .gitignore explicitly lists .planning/.
This means the documented safety net — "if .planning/ is gitignored,
commit_docs is automatically false" — silently fails for any repo where
.planning/ was ever committed. The --no-index flag checks .gitignore
rules regardless of tracking state, matching user expectations.
Closes#703
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
loadConfig() didn't include nyquist_validation in its return object, so
cmdInitPlanPhase always set nyquist_validation_enabled to undefined. The
plan-phase workflow could never detect whether Nyquist validation was
enabled or disabled via config.
Co-authored-by: Ethan Hurst <ethan.hurst@outlook.com.au>
- Add toPosixPath() helper to normalize output paths to forward slashes
- Use string concatenation for relative base paths instead of path.join()
- Apply toPosixPath() to all user-facing file paths in init.cjs output
- Use array-based execFileSync in test helpers to bypass shell quoting
issues with JSON args and dollar signs on Windows cmd.exe
Fixes 7 test failures on Windows: frontmatter set/merge (3), init
path assertions (2), and state dollar-amount corruption (2).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
loadConfig() never returned model_overrides, so resolveModelInternal()
could never find per-agent overrides — they were silently ignored.
Additionally, cmdResolveModel duplicated model resolution logic but
skipped the override check entirely. Now delegates to resolveModelInternal
so both code paths behave consistently.
Phase number parsing only matched single-decimal (e.g. 03.2) but crashed
on multi-level decimals (e.g. 03.2.1). Requirement IDs with regex
metacharacters (parentheses, dots) were interpolated raw into RegExp
constructors, causing SyntaxError crashes.
- Add escapeRegex() utility for safe regex interpolation
- Update normalizePhaseName/comparePhaseNum for multi-level decimals
- Replace all .replace('.', '\\.') with escapeRegex() across modules
- Escape reqId before regex interpolation in cmdPhaseComplete
- Update all phase dir matching regexes from (?:\.\d+)? to (?:\.\d+)*
- Add regression test for phase complete 03.2.1
Closes#621
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>