* fix(#105): skip strategy branch auto-switch when use_worktrees is false
When workflow.use_worktrees is false, the primary checkout is shared or
pinned to a base branch. Auto-switching HEAD in that mode silently moves
the shared checkout and allows concurrent commits to land on the wrong
branch. ensureStrategyBranch now returns ok:true with an explicit skip
reason instead of calling git checkout in this configuration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#105): replace source-grep tests with behavioral coverage
Remove the root tests/bug-105-*.test.cjs file which used source-text
structural assertions (reading commit.ts as a string) — tests that pass
even when the runtime behavior is broken. Replace with a Vitest test at
sdk/src/query/commit.bug-105.test.ts that invokes ensureStrategyBranch
directly with use_worktrees:false and asserts the guard fires before any
git invocation, and that the guard does NOT fire when use_worktrees is
true or absent.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore(#105): add changeset fragment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(#105): address review — type WorkflowConfig.use_worktrees, accept "false" string, stub git in tests
- Add `use_worktrees?: boolean | string` to WorkflowConfig interface with doc comment
- Remove `(config.workflow as unknown as Record<string, unknown>)` double cast; use typed `config.workflow?.use_worktrees`
- Accept string `"false"` alongside boolean `false` via `isExplicitlyFalse` guard (YAML/JSON parser resilience)
- Add `vi.mock('node:child_process')` stub in commit.bug-105.test.ts; assert no-call on skip-path tests
- Add new test case for string `"false"` coercion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(5): add failing test for decision IDs inside <objective>/<tasks>/<task>/<action> XML bodies
Regression test for issue #5 — the translation gate (check.decision-coverage-plan)
is blind to D-NN citations placed inside XML tag bodies by gsd-planner.
Maintainer acceptance criteria (verbatim, issue #5):
"Gate parses <action> tag bodies for decision ID citations; regression test
with XML-tag plan body covers all decision IDs."
Five new test cases added to the 'XML tag body citation parsing (issue #5)' suite:
1. RED: five decisions cited only in <objective>/<action> bodies → gate fails (before fix)
2. Non-canonical tag <comment> → must NOT count (negative control, passes)
3. Plain prose under undesignated heading → must NOT count (negative control, passes)
4. Self-closing <action/> → no crash, D-NN not covered (passes)
5. D-NN in <objective> body → should count (also fails before fix, GREEN after)
Tests 1 and 5 are the load-bearing RED cases. All others are negative controls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(5): extend extractPlanSections to scan <objective>/<tasks>/<task>/<action> for D-NN citations
Closes#5.
Root cause: extractPlanSections() in check-decision-coverage.ts collected only
front-matter (must_haves/truths/objective) and body lines under designated
markdown headings. The gsd-planner spec (agents/gsd-planner.md line 66) says
'Task actions reference the decision ID they implement (e.g., "per D-03")' and
emits citations inside <action> tag bodies — a location the gate could not see.
Fix: add extractXmlTagBodies() helper that matches the four canonical planner
XML tags (<objective>, <tasks>, <task>, <action>) via a deliberately narrow
regex (no XML parser library — D2 design decision). The helper output is
appended to the designated string inside extractPlanSections(), making any
D-NN citation inside those tag bodies count toward coverage.
Maintainer acceptance criteria (verbatim, issue #5):
"Gate parses <action> tag bodies for decision ID citations; regression test
with XML-tag plan body covers all decision IDs."
Self-closing tags (<action/>) are safely ignored — the capturing group does
not match. Non-canonical tags (<comment>, <note>, etc.) are not in the
alternation and are ignored by design.
The CJS surface for check-decision-coverage.ts does NOT have a generator
(gen-decisions.mjs covers decisions.ts, not this gate). No CJS artifact
exists for this module. Per the generator framework established in PR #154
(ADR-3524), a CJS migration is a follow-up; this PR focuses on the TS fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(5): clarify in gsd-planner.md that decision-coverage gate reads XML tag bodies
Adds a parenthetical note to the existing self-check bullet (line 66) explaining
which locations the gate scans so the planner's own guidance and the gate's
behavior are explicitly aligned.
Refs #5. No behavior change — documentation truthing only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(5): add changeset fragment for decision-coverage XML body fix
Refs #5.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(5): extract Interface Context for Executors into reference file to pass planner-decomposition gate
gsd-planner.md was 49446 chars after the XML-tag clarification added in
this PR, exceeding the 48K threshold enforced by
tests/planner-decomposition.test.cjs. Extracted the "Interface Context
for Executors" section (~2137 chars) into
get-shit-done/references/planner-interface-context.md, leaving a one-line
pointer in gsd-planner.md. New normalized size: 47310 chars (1842 chars
under threshold).
Refs #5
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(5): register planner-interface-context.md in INVENTORY.md and manifest
- Bump References headline from 61 to 62 to match filesystem count
- Add planner-interface-context.md row in Modular Planner Decomposition table
- Update footnote from 61 to 62 top-level references
- Regenerate docs/INVENTORY-MANIFEST.json via gen-inventory-manifest.cjs --write
Fixes inventory-counts and inventory-manifest-sync CI failures caused by the
extraction commit (32e8950f) adding a new reference file without updating the
inventory artefacts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(26): reproduce W005/W006-archived/I001 false positives in CJS validate path
Issue #26 (open-gsd/get-shit-done-redux): three validation drift items from
PR #3479 were hand-ported to verify.cjs via PR #3806 but never routed through
the generator pattern. This means they can drift again whenever validate.ts
changes.
RED tests assert that validate.generated.cjs exports four new items:
- phaseDirNameRe (W005 regex — /^\d{2,}(?:\.\d+)*-[\w-]+$/)
- MILESTONE_ARCHIVE_DIR_RE (W006-archived — /^v\d+.*-phases$/i)
- PHASE_TOKEN_FROM_DIR_RE (W006-archived — phase dir token extractor)
- canonicalPlanStem (I001 — plan/summary stem canonicalization)
Three of four new export tests are RED (exports missing from generated artifact).
Behavioral tests (W005 no-false-positive, W006-archived no-false-positive,
I001 no-false-positive) are GREEN because #3806's hand-ported fixes are present.
References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6),
PR #3479 (original fix), PR #3806 (hand-port).
* chore(26): extend gen-validate.mjs to export W005/W006-archived/I001 helpers
Issue #26 (open-gsd/get-shit-done-redux): extend gen-validate.mjs (introduced
in PR #156 / issue #6) to also extract the three drift items that PR #3806
hand-ported to verify.cjs but were never routed through the generator.
New helpers added to gen-validate.mjs:
phaseDirNameRe (PHASE_DIR_NAME_RE) — W005 phase directory naming regex.
/^\d{2,}(?:\.\d+)*-[\w-]+$/ accepts multi-digit prefixes (999.1-foo valid).
Requires adding PHASE_DIR_NAME_RE as a named constant to validate.ts so
it appears as an extractable identifier in the compiled output.
PHASE_TOKEN_FROM_DIR_RE — W006-archived regex; extracts phase token from
directory names like "64-auth-service" → "64". Used by
forEachArchivedPhaseToken() and collectDiskPhases() in verify.cjs.
MILESTONE_ARCHIVE_DIR_RE — W006-archived regex; matches milestone archive
directory names like "v1.0-phases". Used by listMilestoneArchiveDirs().
canonicalPlanStem() — I001 PLAN/SUMMARY stem canonicalization.
'68-01-scaffolding' → '68-01'. Top-level named function in compiled output.
Extraction approach: PHASE_TOKEN_FROM_DIR_RE and MILESTONE_ARCHIVE_DIR_RE are
module-level const assignments, extracted via extractConstRegExp() (handles both
`const` and `export const` prefixes). PHASE_DIR_NAME_RE is the new named export
added to validate.ts in this commit. canonicalPlanStem is a top-level function,
extracted via extractTopLevelFunction() (brace-balanced).
validate.ts change: inline regex in Check 6 extracted to named constant
PHASE_DIR_NAME_RE (exported) and Check 6 updated to reference it.
References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6).
* chore(26): regenerate validate.generated.cjs with W005/W006-archived/I001 helpers
Re-run of sdk/scripts/gen-validate.mjs after extending it in the preceding
commit. The artifact now exports seven items (was three):
New (issue #26):
phaseDirNameRe — /^\d{2,}(?:\.\d+)*-[\w-]+$/ (W005 check)
PHASE_TOKEN_FROM_DIR_RE — phase token extractor regex (W006-archived)
MILESTONE_ARCHIVE_DIR_RE — archive dir name matcher (W006-archived)
canonicalPlanStem() — PLAN/SUMMARY stem canonicalization (I001)
Existing (issue #6):
phaseVariants()
buildRoadmapPhaseVariants()
buildNotStartedPhaseVariants()
Freshness check: node sdk/scripts/check-validate-fresh.mjs → "fresh".
References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6).
* fix(26): migrate verify.cjs W005/W006-archived/I001 call sites to generated helpers
Issue #26 (open-gsd/get-shit-done-redux): three hand-maintained items in
verify.cjs now consumed from validate.generated.cjs (ADR-3524 §4 adapter pattern).
Changes:
- Top-of-file require(): extend to also destructure phaseDirNameRe,
PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem
from validate.generated.cjs (issue #26 exports).
- Remove inline PHASE_TOKEN_FROM_DIR_RE and MILESTONE_ARCHIVE_DIR_RE constants
(lines ~403-404). Now sourced from generated artifact. listMilestoneArchiveDirs
and forEachArchivedPhaseToken pick them up via the require() at top of file.
- Check 6 (W005): replace inline regex /^\d{2,}(?:\.\d+)*-[\w-]+$/ with
phaseDirNameRe from validate.generated.cjs. No behavior change.
- Remove inline canonicalPlanStem() function (~8 lines). Now sourced from
validate.generated.cjs. Check 7 (I001) continues to call it as before.
Public API of verify.cjs unchanged. Same migration shape as PR #156's Check 8.
References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6),
PR #3479 (original fix), PR #3806 (hand-port that #26 supersedes).
* docs(26): extend ADR-3524 2026-05-23 amendment with #26 scope
Extends the existing 2026-05-23 amendment (not a new dated section) to document
the W005/W006-archived/I001 generator migration introduced by issue #26.
Key points documented:
- Four new exports added to validate.generated.cjs (phaseDirNameRe,
PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem)
- W006-archived coverage note: both fixes were already in verify.cjs from
#3806; the gap was generator coverage of the regex constants
- Extraction methods: extractConstRegExp() and extractTopLevelFunction()
- Parity tests: tests/26-w005-w006-i001-cjs-drift-regression.test.cjs (7 tests)
- Cross-reference: issue #26 completes the validate.ts ↔ verify.cjs migration
scope started by issue #6
References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6),
PR #3479 (original fix), PR #3806 (hand-port).
* chore(26): add changeset fragment for W005/W006-archived/I001 generator migration
Touches get-shit-done/bin/lib/validate.generated.cjs and verify.cjs which
match USER_FACING_PREFIXES. Required by the fix-template checklist + the
changeset-lint CI workflow.
References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6).
* test(3): add failing tests for milestone.complete header-dup, bullet-leakage, one-liner-noise
Three RED tests for confirmed-bug #3 in sdk/src/query/phase-lifecycle.test.ts:
Defect 1 (header-dup): milestoneComplete produces "## v1.0 v1.0 (Shipped: ...)"
when no --name is given because `milestoneName = nameOpt || version` is always
truthy, so the template `## ${version} ${milestoneName}` duplicates the version.
Two sub-cases: no --name, and --name "Foundation Release".
Defect 2 (bullet-leakage): getMilestonePhaseFilter falls back to passAll
(milestonePhaseNums.size === 0) when the milestone section declares phases via
GFM task-list bullets only (https://github.github.com/gfm/#task-list-items-extension-)
with no ### Phase N: headings, admitting unrelated phase 99.
Defect 3 (one-liner-noise): extractOneLinerFromBody matches the first bold in the
entire document body after the heading, regardless of section boundaries.
Per GFM § ATX headings (https://github.github.com/gfm/#atx-headings), the scope
must be bounded to "first heading until next heading of ANY level".
CJS bundle `bin/lib/*.cjs` intentionally NOT updated; bundle sync covered by #4
(first generator-introducing PR) and #26.
* fix(3-1): preserve version in milestone header, append name only when provided
Bug: `milestoneName = nameOpt || version` was always truthy (falling back to
version), so the template `## ${version} ${milestoneName}` rendered as
`## v1.0 v1.0 (Shipped: ...)` when no --name was given.
Fix: separate `milestoneName = nameOpt ?? undefined` from `version`, then
build the title with `milestoneName ? \`${version} ${milestoneName}\` : version`.
Canonical template (GFM § ATX headings: https://github.github.com/gfm/#atx-headings):
No --name: `## v1.0 (Shipped: 2026-05-23)`
With --name "Foo": `## v1.0 Foo (Shipped: 2026-05-23)`
Historical evidence: `## v1.8.0 Quick Mode (Shipped: 2026-01-19)`
Also applies the same fix to the Requirements Archive header.
Files changed: sdk/src/query/phase-lifecycle.ts
#3
CJS bundle `bin/lib/*.cjs` intentionally NOT updated; bundle sync covered by #4
(first generator-introducing PR) and #26.
* fix(3-2): recognize checkbox-bullet phase declarations in milestone phase filter
Bug: getMilestonePhaseFilter in sdk/src/query/state.ts used the regex
`/#{2,4}\s*Phase\s+([\w][\w.-]*)\s*:/gi` which only matched heading-style
phase declarations (### Phase N: title). When a ROADMAP declares phases via
GFM task-list bullets only:
- [ ] **Phase 1: Foundation**
- [ ] **Phase 2: API**
the Set remained empty → passAll fired → all phase directories (including
unrelated 99-*) were admitted into the milestone accomplishments.
Fix: extend the regex to also match bullet-style declarations:
/(?:#{2,4}\s*|-\s*(?:\[[x ]\]\s*)?\*{0,2}\s*)Phase\s+([\w][\w.-]*)\s*:/gi
GFM § ATX headings: https://github.github.com/gfm/#atx-headings
GFM § Task list items: https://github.github.com/gfm/#task-list-items-extension-
Files changed: sdk/src/query/state.ts
#3
CJS bundle `bin/lib/*.cjs` intentionally NOT updated; bundle sync covered by #4
(first generator-introducing PR) and #26.
* fix(3-3): bound extractOneLinerFromBody to first heading until next heading of any level
Bug: extractOneLinerFromBody in sdk/src/query/phase-lifecycle-policy.ts
used the regex /^#[^\n]*\n+\*\*([^*]+)\*\*/m which matched the first bold
in the entire document body after any heading. This leaked bold text from
later sub-sections (e.g. "### Deviation 1 -- bar") into the one-liner.
Fix: bound the search scope to the first section only.
Implementation follows D3 spec:
1. Find the first heading of any level (GFM ATX headings:
https://github.github.com/gfm/#atx-headings).
2. Extract content from after that heading to the next heading of ANY level
(not "same or higher") so ### Deviation terminates scope even when title
is H1.
3. Match the first **bold** within that bounded scope only.
Note: TypeScript 5.x rejects backtick-containing regex patterns in JSDoc
comments (TS1443: template literal parse error); comments use plain text instead.
Files changed: sdk/src/query/phase-lifecycle-policy.ts
#3
CJS bundle `bin/lib/*.cjs` intentionally NOT updated; bundle sync covered by #4
(first generator-introducing PR) and #26.
* fix(3-3): apply bounded one-liner extraction to summary.ts duplicate
Sibling copy of extractOneLinerFromBody in sdk/src/query/summary.ts had
the same pre-fix logic flagged in commit d1eab690. Same bug, same fix —
"Fix Everything You Find" applies. DRY-extracting to a shared module is
left as a follow-up refactor; out of scope for this bug fix.
Refs #3
* chore(3): add changeset fragment for milestone.complete noise fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(3): update changeset fragment with PR number 146
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3785): case-insensitive depends_on resolution in phase resolver
planMap, canonicalToId, and shortFormToId in phasePlanIndex used strict
Map.has() with no case normalization. A depends_on ref in mixed/lowercase
against an uppercase-suffix plan ID (e.g. '20-01-auth' → '20-01-Auth')
dropped the DAG edge, assigning the dependent plan to wave 1 instead of
wave 2. Fix: normalize all keys and lookup values to lowercase so the
three-tier resolution is case-insensitive. Adds regression test (#3785).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(changeset): add PR 3798 changelog fragment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3785): detect case-fold collisions; apply lowercase-both to CJS path
- Add collision guard in both sdk/src/query/phase.ts (phasePlanIndex)
and get-shit-done/bin/lib/phase.cjs (cmdPhasePlanIndex): when two plan
IDs in the same phase are identical after toLowerCase(), throw/error
immediately with a clear message naming both files instead of silently
overwriting one in planMap and misrouting depends_on edges.
- Apply the same lowercase-both normalization (#3785) to cmdPhasePlanIndex
in phase.cjs, which was missing from the original PR — planMap and
canonicalToId keys are now lowercased on write; dep strings are
lowercased before lookup.
- Add regression tests to tests/phase.test.cjs: case-insensitive
resolution test (runs on all platforms) and collision-detection test
(skipped on macOS/Windows where FS is case-insensitive).
- Update changeset to describe both the SDK and CJS fixes.
Identified via Codex adversarial review of PR #3798.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3785): address review — KNOWN GAP comment for CJS shortFormToId, canonical-casing tests, depends_on output normalization
- F1: Reword changeset for accuracy (plannerID drift trigger; two-tier CJS gap honest).
Add KNOWN GAP comment in phase.cjs before Kahn's loop noting CJS lacks shortFormToId
(tracked as follow-up parity gap, out of scope for #3785).
- F2: Add strict planA.id === '20-01-Auth' assertions in both SDK (vitest) and CJS (node --test)
tests — a future regression that silently lowercases stored IDs would now fail the test.
- F3: Normalize depends_on output to canonical plan IDs in both SDK phase.ts and CJS phase.cjs.
User-typed '20-01-auth' in depends_on resolves to '20-01-Auth' in output via planMap lookup.
Add planB.depends_on === ['20-01-Auth'] assertions in both test suites.
- F5: Add seenLower guard-scope comment (full-ID collisions only; shared-prefix collisions
handled by first-write-wins from sorted planFiles).
- F6: Add ASCII-safe toLowerCase comment at first call site in both SDK and CJS.
- F7: Add intentional-separation comment on seenLower vs planMap in both SDK and CJS.
Reviewers: gsd-code-reviewer (MN-01/NT-1) + sonnet adversarial.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(3785): cover case-insensitive depends_on resolution branches
Add 4 focused test cases exercising branches introduced by #3785:
- All-uppercase depends_on ref resolving to lowercase plan ID via planMap
- External cross-phase dep preserved as-is in Pass 3 output (planMap miss)
- Mixed-case short canonical prefix resolving via canonicalToId
- Plans with undefined/empty depends_on emit empty array correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(npm): rebrand packages to @opengsd scope
Rename:
- get-shit-done-redux → @opengsd/get-shit-done-redux
- @gsd-redux/sdk → @opengsd/gsd-sdk
Add publishConfig.access=public for first-time scoped publish.
CLI binary names (get-shit-done-redux, gsd-sdk, gsd-tools) unchanged.
Sweeps install commands, npx invocations, CI publish/version-check
workflows, tests, docs, READMEs (all translations), and the
PACKAGE_NAME constant in check-latest-version.
Bumps qs 6.15.1 → 6.15.2 to clear a moderate advisory surfaced by
the audit-clean test (GHSA-q8mj-m7cp-5q26).
Closes#126
* chore: pin 2.0.0 release + remove canary workflow
- Bump both packages 1.50.0-canary.0 → 2.0.0 for first @opengsd publish
- Remove .github/workflows/canary.yml and canary dist-tag handling in
release.yml / release-sdk.yml
- Drop canary section from VERSIONING.md
Refs #126
* chore: address review findings + harden tarball-smoke timeout
- .changeset/opengsd-org-rename.md: match project's custom
parse.cjs frontmatter (type: Changed / pr: 127); the scoped
@changesets/cli keys were silently rejected.
- CONTEXT.md: drop two canary-stream policy lines and a dangling
DEFECT.CANARY-VERSION-LEAK.cross-ref now that canary.yml is gone.
- tests/release-tarball-smoke.install.test.cjs: pass
timeout: 600_000 for npm pack + global install; the 3-minute
runNpm default was timing out on slower Docker hosts (cartographer).
Refs #126
* fix(sdk): add missing type/runtime devDependencies for build
prepublishOnly invokes tsc which couldn't resolve @types/node,
@types/ws, or synckit. They had been hoisted from root but were
not declared in sdk/'s own package.json — first publish from a
clean SDK tree failed.
Refs #126
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ci): use npm pack stdout instead of glob to find tarball
`npm pack --silent` for a scoped package (@opengsd/get-shit-done-redux)
produces `opengsd-get-shit-done-redux-*.tgz`, not `get-shit-done-redux-*.tgz`.
Capture the filename from stdout instead of a hardcoded glob so the step
works regardless of package name format.
Fixes smoke (ubuntu-latest, 22, false) CI failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ci: treat workflow-file changes as test-skip eligible
`.github/workflows/install-smoke.yml` (and other workflow files)
were in neither `test.yml` paths nor `test-skip.yml` paths-ignore,
so neither workflow ran on a workflow-only commit — leaving the
required test-skip check perpetually missing.
Refs #126
* chore: reset version to 1.0.0 for first @opengsd publish
Nothing has been published yet under the @opengsd scope, so the
inaugural release uses 1.0.0 rather than 2.0.0. The "major bump"
in the changeset reflects the breaking install-command change for
users migrating from the prior unscoped `get-shit-done-redux`, not
a numeric continuation from a 1.x line under the new identity.
Refs #126
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3774): treat 999 as exact sentinel, not lower bound, in phase-lifecycle-policy
scanSequentialMaxPhaseFromMilestone and scanSequentialMaxPhaseFromDirs used
`num >= 999` to skip the backlog lane, but this incorrectly excluded every
phase ≥ 1000, causing computeNextSequentialPhaseId to return 1 for projects
using canonical phase IDs in the 1000+ range. Change both guards to
`num === 999` so only the backlog sentinel is skipped.
Adds regression test: project with phases 1000–1500 must produce 1501, not 1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add changeset for fix#3792 (phase.add returns 1 on 1000+ projects)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3774): address review — fix 4 CJS scanner twins + tighten regression test
Addresses gsd-code-reviewer BLOCKER (4 CJS scanner twins in phase.cjs:610,624,688,698 still carried >= 999, reachable via GSD_WORKSTREAM / absent SDK build) and MAJOR (regression test couldn't distinguish === 999 from === 1000 — added [999, 1000] fixture asserting result === 1001). Decrement helpers at :893, :922, :930, :936 left unchanged — intentional 999-lane protection per dual-review analysis.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#3816): handle milestone-scoped phase dirs in roadmap parser, phase.add, findPhase
- phase.ts: replace hardcoded /^v[\d.]+-phases$/ regex with sortedMilestoneArchiveDirs()
helper that matches any *-phases suffix and sorts the current-milestone dir first
- roadmap.ts: add fallback for plain-bullet phases (- [ ] Phase N:) in searchPhaseInContent
and roadmapAnalyze; truncate content at ## Backlog boundary in roadmapAnalyze to prevent
backlog phases from leaking into the active-milestone phase list
- phase-lifecycle.ts: add resolveWritePhasesDir() and findPhaseInsertionPoint() helpers;
wire both into phaseAdd and phaseAddBatch so new phase dirs land in the milestone-scoped
path and roadmap entries are inserted before ## Backlog rather than after the last ---
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3816): sort milestone archives by version, not lexically
`sortedMilestoneArchiveDirs` was sorting in descending order (b before a),
causing v1.10-phases to be searched before v1.2-phases. Flip to ascending
so the earliest archive is searched first, matching the deterministic sort
the test at milestone-archive.test.cjs:383 asserts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3815): phase.insert parser handles checked-bullet ROADMAP format
phaseInsert (TS) and cmdPhaseInsert (CJS) previously used a heading-only
regex (#{2,4}\s*Phase\s+N:) to locate the target phase. On projects whose
ROADMAP uses the checked-bullet format (- [ ] **Phase N: name** or
- [ ] Phase N: name), the lookup always failed with "Phase N not found".
Extend the locator to also accept the bullet form — mirroring the patterns
already used by phaseRemove and phaseComplete. When bullet-style is
detected, insert a new bullet entry after the matched line (preserving
bold/plain style to match surrounding entries). The heading-style code
path is unchanged.
Also fix a pre-existing test timeout: the first registry-integration test in
phase-lifecycle.test.ts was failing with STACK_TRACE_ERROR (masked timeout)
because the cold import of index.js takes >5 s. Added { timeout: 30_000 }.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3815): refine hybrid-ROADMAP detection, preserve #3098 parity
Tighten the bullet-style branch guard: only treat a ROADMAP as
bullet-style (and apply the bullet-insert path) when it contains
ZERO heading-style phase entries (anyHeadingPattern test). A
mixed (hybrid) ROADMAP — headings for some phases, bullet summaries
for others — is the #3098 case where the detail section is absent;
that path must still error with "missing a detail section".
Adds a regression test (#3098 preserved) in both TS and CJS to
confirm that a heading-style ROADMAP with a bullet-only entry for
the target phase still fires the "missing a detail section" error,
not the bullet-insert path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add changeset for #3815 phase.insert bullet-roadmap fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3799): detect project-local agents in init.* (local-first resolution)
Reverses resolution order in resolveAgentsDir() so project-local
<projectDir>/.claude/agents is checked BEFORE the global runtime dir.
Claude Code auto-creates ~/.claude/agents at startup (empty); the old
global-first check returned that empty dir over a populated local dir.
Adds 5 tests to tests/bug-3751-init-local-agents.test.cjs:
- Structural: local-first ordering in function body (#3799 RED gate)
- Runtime: local+empty-global → agents_dir = local
- Runtime: global-only (no local) → agents_dir = global (no regression)
- Runtime: both present → local wins (Claude Code parity)
- Updated Contract 3 assertion to reflect new local-first ordering
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add changeset for fix(3799)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirror of code, issues, and PRs from the upstream gsd-build/get-shit-done,
which appears compromised or abandoned (maintainer unreachable since
2026-04-01; $GSD token linked to rug-pull).
- Adds rebrand notice block at top of English README
- Removes $GSD token badge and @gsd_foundation X badge (keeps Discord)
- Renames npm packages: get-shit-done-cc -> get-shit-done-redux,
@gsd-build/sdk -> @gsd-redux/sdk
- Updates all repo URLs across docs, workflows, package.json, bin/
- Updates ci@gsd-build -> ci@gsd-redux in workflow git identities
- Leaves CHANGELOG and .changeset/* alone (historical, time-stamped)
* test(3749): add RED test — SDK commit handler missing strategy-branch port
Structural assertions on sdk/src/query/commit.ts verify the branching-strategy
block (phase/milestone) is present. 9 tests fail today; pass after the fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3749): port PR #1279 strategy-branch logic from CJS to SDK commit handler
sdk/src/query/commit.ts lacked the branching-strategy block that PR #1279
added to commands.cjs:285-320. Pre-execution workflow commits (discuss-phase,
plan-phase) with branching_strategy:"phase" or "milestone" were landing on
whatever branch was active rather than the configured strategy branch.
Changes:
- sdk/src/query/phase.ts: export findPhaseByNumber() so commit.ts can look up
a phase without going through the QueryHandler dispatch stack
- sdk/src/query/commit.ts: import loadConfig, findPhaseByNumber, getMilestoneInfo;
add ensureStrategyBranch() helper (extracts the logic, preserving SRP);
call it between the commit_docs check and the staging step
Semantics match the CJS path exactly: best-effort (errors are swallowed so a
misconfigured branching_strategy never aborts a commit), same phase-number
extraction from file paths, same checkout -b / checkout fallback sequence,
same current-branch guard to avoid repeated checkout calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(3749): update changeset to reference PR #3763
* fix(3749): validate phase_branch_template before substitution
Before this commit, a missing or empty `phase_branch_template` (or
`milestone_branch_template`) would cause `.replace()` to throw inside
the try-block, which was swallowed by the surrounding catch → silent
strategy-skip with no observable signal.
Exports `validateBranchTemplate(template)` as a pure helper that
checks the template is a non-empty string BEFORE any `.replace()` call
is attempted. Also exports `resolveStrategyBranchName(template,
phaseNumber, phaseSlug)` which validates that no `{placeholder}` tokens
survive substitution.
Both an invalid template and an unresolved-placeholder result now return
`{ ok: false, reason: '...' }` from `ensureStrategyBranch`, surfaced
distinctly — satisfying the no-silent-skip contract (codex finding 3).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(3749): replace source-includes with typed-IR assertions on extracted helpers
The original test file used `source.includes(...)` throughout — grep
theater that verifies text presence, not behavior (codex finding 4 /
test-rigor Contract 1 violation).
This commit upgrades the test suite:
1. Typed-IR unit tests (ts-node, skipped gracefully when unavailable):
- `parsePhasesFromFiles`: empty, single-phase, mixed-phase, dotted
phase numbers, root numeric tokens
- `validateBranchTemplate`: undefined, empty, whitespace, valid
- `resolveStrategyBranchName`: well-formed, unresolved placeholder,
slug fallback
2. Structural assertions (retained and tightened) now verify:
- Named exports of the three helpers exist (enabling typed-IR tests)
- Caller halts on `strategyResult.ok === false` (Finding 2)
- Mixed-phase rejection message contains "single phase" (Finding 1)
- Template validation precedes resolveStrategyBranchName in source
(Finding 3, using lastIndexOf to skip helper definitions)
- `alreadyExists` guard and `branch_switch_failed` reason present
(Finding 2)
Old structural tests that verified implementation tokens
(loadConfig, phase_branch_template, --abbrev-ref, etc.) are
retained as they verify observable contracts, not code style.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3749): bug-2767 tests skip cleanly when sdk/dist is absent (was hard-fail)
The 4 behavioral tests in bug-2767-gsd-sdk-commit-files-flag.test.cjs
invoke the built SDK CLI (sdk/dist/cli.js) end-to-end. When that dist is
absent, they threw MODULE_NOT_FOUND and were reported as 4 hard failures
rather than observable skips.
Apply the same `if (!existsSync(SDK_CLI)) { t.skip(...); return; }` guard
already used in bug-3019-help-passthrough.test.cjs. When dist is present,
all 4 tests run and pass. When dist is absent, all 4 emit a ﹣ skip line
with an actionable reason ('run `cd sdk && npm run build`'), leaving 0
hard failures and maintaining full observability.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3749): address pr-review-toolkit + codex review findings
- Add allow-test-rule annotation to satisfy lint-no-source-grep
- Remove dead identifiers (registerScript, tsConfigPath, os import)
- Standardize issue #1278 / PR #1279 references in JSDoc
- Document root-level phase-number false-positive in parsePhasesFromFiles
- Generalize resolveStrategyBranchName parameter naming (phase + milestone reuse)
- Add behavioral integration test for commit handler with branching_strategy: phase
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(3749): normalize extracted phase token in phaseTokenMatches
parsePhasesFromFiles extracts "1" from a path like ".planning/phases/1-setup/PLAN.md".
normalizePhaseName pads this to "01" before passing it to searchPhaseInDir, which calls
phaseTokenMatches("1-setup", "01"). extractPhaseToken("1-setup") returned "1" (raw,
unpadded), so "1" !== "01" and the phase directory was not found — ensureStrategyBranch
fell through with "phase not found" and never switched the branch.
Fix: normalize the extracted token via normalizePhaseName before comparing so that "1"
and "01" both resolve to "01" and match correctly. Applied to both the primary comparison
and the project-code-prefix-stripping retry path.
Verified by bug-3749-sdk-commit-strategy-branch-integration.test.cjs passing:
✔ commit with --files in a phase dir switches to the strategy branch
✔ commit with --files outside a phase dir skips branch switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#3751): add RED test for resolveAgentsDir missing repo-local .claude/agents
Drive resolveAgentsDir() with a repo-local .claude/agents present and
~/.claude/agents absent. Structural contracts assert the function body
must reference projectDir when constructing the fallback path, and that
init-complex.ts must pass projectDir to the call site. Both fail
deterministically before the fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#3751): resolveAgentsDir() falls back to repo-local .claude/agents for --local installs
resolveAgentsDir() was probing only GSD_AGENTS_DIR or the runtime-global
config dir (~/.claude/agents for Claude). For Claude Code --local installs
where agents land in ./.claude/agents, the SDK reported agents_installed:
false even when the agent files were present.
Precedence (post-fix):
1. GSD_AGENTS_DIR (explicit override, unchanged)
2. <getRuntimeConfigDir(runtime)>/agents (when the dir exists)
3. <projectDir>/.claude/agents (repo-local fallback for claude runtime)
Both init.ts:checkAgentsInstalled and init-complex.ts:initNewProject now
receive projectDir and thread it to resolveAgentsDir().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(3751): update changeset to reference PR #3762
* fix(3751): add allow-test-rule annotation to satisfy lint-no-source-grep
The structural-contract test reads TypeScript SDK source files to verify
resolveAgentsDir() signature shape, fallback ordering, and call-site
wiring. These are legitimate SDK-seam contracts (the TS source IS the
product artifact). The allow-test-rule annotation bypasses the
lint-no-source-grep check that flags readFileSync-bound variable usage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3751): repair Windows config-set concurrent-write race
withPlanningLock threw on EPERM instead of retrying, causing the losing
process to crash silently and its config write to never land.
On Windows, when two processes race to create the same lock file via
O_EXCL (writeFileSync with flag:'wx'), the OS can return EPERM instead of
EEXIST while NTFS holds an internal exclusive handle on the newly-created
file during the write. The previous catch block only recognized EEXIST as
a contention signal — any other code (including EPERM) fell through to
`throw err`. The calling test used .catch(()=>{}) to suppress process
errors, so the losing process silently exited without writing its value;
the winning process then wrote from the stale pre-race config, producing
the observed 'balanced' instead of 'quality'. Fix: port the
PLANNING_LOCK_RETRY_ERRNOS Set from main (landed in #3777) into this
branch. The set treats EPERM, EBUSY, and six other transient filesystem
codes as retry signals rather than fatal errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Between `open(lockPath, O_CREAT | O_EXCL)` and `fd.writeFile(pid)` there
is an async await gap. If a second process reads the lock file during that
window it sees empty content, which parseInt returns as NaN. The previous
code returned `true` (dead) for non-finite PID values, causing the second
process to unlink the live lock and steal it — both processes then entered
readModifyWriteStateMd simultaneously, producing a lost-update TOCTOU.
Fix: return `null` (unknown) instead of `true` when the lock file
contains no parseable PID. The caller already treats `null` as "not
confirmed dead" and falls through to the normal retry + timeout path,
giving the first process time to finish writing its PID.
The CJS acquireStateLock in state.cjs never had this race because it uses
synchronous fs.openSync / fs.writeSync / fs.closeSync with no async gap.
Fixes intermittent failure of:
#1925 TOCTOU: state commands use readModifyWriteStateMd
→ state add-blocker: both concurrent calls append different blockers
(observed: macos-latest / Node 22 and windows-latest / Node 22 on main)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Export isPhaseUatPassed and supporting types/enums from SDK public surface
- Remove /m flag from frontmatter regex to prevent mid-body strip
- Implement reasonsHuman population with per-ReasonCode humanizer
- Add test for multiple UAT files in same phase
- Add test for INVALID_PHASE_NUM error path
- Add test for workstream routing
- Eliminate redundant stripMarkdownInjection call
- Update stale cycle-number comments
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(tests): lint rule — cap test files per production module at 2
Adds scripts/lint-test-file-count.cjs with a ratcheted allowlist
(scripts/lint-test-file-count.allowlist.json) capturing today's
30 violating clusters as a ceiling. New entries blocked at PR time;
reductions ratchet automatically.
Wires into .github/workflows/test.yml as a new step in lint-tests.
Refs #3737
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(tests): consolidate Phase Lifecycle Module — 20 files → 4
- Merge 10 CJS bug-fix test files into tests/phase.test.cjs
- Merge sdk/src/phase-runner-types.test.ts + sdk/src/phase-prompt.test.ts
into sdk/src/phase-runner.test.ts (97 → 152 tests, 0 failures)
- Rename 4 mis-attributed test files out of phase cluster:
phase-researcher-app-aware → gsd-researcher-app-aware
phase-researcher-flow-diagram → gsd-researcher-flow-diagram
feat-3023-phase-type-models → feat-3023-model-phase-types
phase-6-cjs-sdk-seam-contracts → cjs-sdk-bridge-seam-contracts
- Fix phasePlanIndex (#3430): non-canonical plan filename warning now
surfaces in data.warnings[] as "Ignored noncanonical plan files: ..."
instead of a separate singular data.warning field
- Update allowlist: phase ceiling 20 → 4 (closes#3740)
- Add Phase Lifecycle Module glossary entry to CONTEXT.md
Closes#3740
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(phase-roadmap-mutation): replaceInCurrentMilestone handles active milestone inside <details> block
When the active milestone is wrapped in a <details> block (e.g. user
collapsed it, or milestone transition), the after-</details> slice is
empty or contains only footer text — the pattern never matches and the
replacement is silently dropped.
Fix: when after.replace() produces no change, fall back to replacing
inside the last <details>...</details> block. Shipped-milestone blocks
are untouched because only the last <details> block is targeted.
Closes#2641
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(changeset): add required type/pr frontmatter to 3740 fragment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3687): update insert-phase docs and roadmapper to use --insert flag
Updates stale references in insert-phase.md workflow and
gsd-roadmapper.md agent to use the consolidated /gsd:phase --insert
command syntax instead of the retired /gsd-insert-phase and
/gsd:phase insert forms.
Closes#3687
* refactor(tests): consolidate Init Command Module — 7 files → 5
Closes#3755
Merges `tests/init-manager-deps.test.cjs` (#2267 regression) into
`tests/init-manager.test.cjs` (718 LOC), and
`sdk/src/query/init-progress-precedence.test.ts` (#2674 regression)
into `sdk/src/query/init-complex.test.ts` (788 LOC).
The 800 LOC ceiling prevents further consolidation:
- `tests/init.test.cjs` is pre-existing at 1630 LOC
- `sdk/src/query/init.test.ts` is at 791 LOC
- `sdk/src/query/init-workstream-milestone-op.test.ts` is a distinct
seam testing initMilestoneOp, roadmapAnalyze, and
resolveQueryRuntimeContext workstream resolution.
Also adds Init Command Module Glossary entry to CONTEXT.md.
Allowlist update deferred to rebase after #3738 merges (allowlist
file does not exist on origin/main).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(init): initExecutePhase preserves same-milestone archived phase dir (#3469)
When `phases clear` archives current-milestone phases into
`.planning/milestones/<version>-phases/` but the workflow is still on
that same milestone, `shouldDropArchivedPhaseMatch` was unconditionally
dropping the archived dir match. This caused `phase_dir: null` when the
phase was still executing in the current milestone.
Fix: detect when `phaseInfo.archived === currentMilestone` (read from
STATE.md) and skip the drop. The #2391 regression guard is safe because
that scenario involves archived.version != current milestone.
Also corrects two tests in `initRemoveWorkspace` to expect thrown
GSDError instead of `{ data: { error } }` — the production code was
intentionally changed to throw for CLI non-zero exit propagation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: arya rizky <aryarizkyardhipratama@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(worktree): unlock-retry on locked cleanup + startup orphan sweep (#3707)
Two root causes fixed:
1. **In-session cleanup blocked**: `executeWorktreeWaveCleanupPlan` now attempts
`git worktree unlock <path>` then retries `git worktree remove --force` when the
initial single-force remove fails on a locked worktree. Previously every cleanup
after a successful merge was silently blocked.
2. **Cross-session orphan accumulation**: new `reapOrphanWorktrees` helper sweeps
`.git/worktrees/*/locked` at startup. It reaps entries where the pid is dead,
the branch tip is an ancestor of the default branch (ancestry guard prevents data
loss on squash-merge repos), and the lock mtime is older than 5 minutes (race
guard). Wired into `quick.md` and `execute-phase.md` startup blocks guarded by
`USE_WORKTREES != false`.
SDK: adds `worktree.reap-orphans` query command (routes through gsd-tools.cjs).
Tests: 11 real-fs tests covering unlock-retry, dead-pid reap, live-pid skip,
unmerged skip, fresh-mtime skip, idempotent double-call, and structural wiring.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(changeset): add Fixed fragment for PR #3707 (worktree orphan cleanup)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(worktree): fix test portability on Windows + macOS for bug-3707 reap tests
- worktreeMeta helper: replace /\/\.git$/ with /[/\\]\.git$/ so the
gitdir path suffix is stripped on both Windows (backslash) and Unix.
- worktreeMeta helper: normalize CRLF→LF before splitting porcelain
blocks, fixing block parsing when git emits CRLF on Windows.
- reapOrphanWorktrees: replace single 'main' rev-parse with a
[defaultBranch, 'main', 'master'] candidate loop so test fixtures
without a remote origin (where branch may be 'master') don't bail
early. Intentionally excludes 'HEAD' to prevent false reaping when
HEAD is detached or on a feature branch (Codex adversarial finding).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(worktree): CI green — macOS symlink path, Windows test helper, pid portability, EPERM liveness
Four fixes to get macOS + Windows CI from red to green:
1. **macOS symlink mismatch** (worktree-safety.cjs): `reapOrphanWorktrees` now
builds a canonical→listed path map from `git worktree list --porcelain` using
`fs.realpathSync.native`. Uses the listed path (as git knows it) for
`git worktree unlock/remove`, not the gitdir-derived path. Fixes the
`/var/folders` vs `/private/var/folders` discrepancy on GitHub macOS runners
where `git worktree unlock <realpath>` was silently failing because git's
list stored the unresolved symlink path.
2. **Windows path separator in test helper** (test file): `worktreeMeta`
`.replace(/\/\.git$/, '')` → `.replace(/[/\\]\.git$/, '')`. On Windows,
git writes backslash separators in the gitdir file; the Unix-only regex was
causing `Cannot find .git/worktrees/<name>` for all Suite 2 tests.
3. **Non-portable PID in tests** (test file): All `'999999'` dead-PID literals
replaced with `deadPid()` helper that spawns a real short-lived child, captures
its PID, and returns it after exit. Eliminates flakiness on Linux systems where
`pid_max` can reach 4194304, making 999999 a live PID.
4. **EPERM fail-closed in isPidAlive** (worktree-safety.cjs): `catch { return false }`
→ checks `err.code === 'EPERM'` and returns `true` (alive). On Windows and
cross-user scenarios, `process.kill(pid, 0)` throws EPERM for live but
inaccessible processes; treating that as dead would reap a live worktree.
Adversarial review via codex confirmed:
- Squash-merge repos: fail-closed (CONCERN, not BUG — by design, not data-loss)
- canonicalToListed map: SAFE (fail-closed on realpathSync error)
- Concurrent reapers: SAFE (both prune; second gets skipped: remove_failed)
- Startup blocking: CONCERN (no global cap, 10s/call × N worktrees) — tracked,
not fixed here (requires separate perf work)
- gsd-sdk missing: SAFE (quick.md checks and fails fast with guidance)
All 27 local tests + Docker (holodeck) green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(worktree): address codex adversarial findings — fail-closed default branch + CRLF map
Two fixes from codex adversarial review of PR 3718:
1. **Default branch resolution (data-loss risk)**: `reapOrphanWorktrees` now
uses `refs/remotes/origin/<branch>` exclusively when a remote is configured.
If `origin/HEAD` is absent but a remote exists, we bail out (fail-closed)
rather than falling back to a local `main`/`master` that may not be the
real integration branch. The `main`/`master` fallback is only used when
there is provably no remote (local-only test fixtures).
2. **CRLF normalization in canonical-path mapper**: The `worktree list
--porcelain` output was split on '\n\n' without normalizing CRLF first.
On Windows, git emits CRLF, which caused block-splitting to fail and
left the canonicalToListed map only partially populated, weakening the
symlink/path-mismatch fix introduced earlier.
3. **Windows 8.3 short-path fix (test helper)**: Both `beforeEach` blocks
now call `resolvedTmpDir()` which pre-resolves `os.tmpdir()` via
`fs.realpathSync.native` so temp paths avoid RUNNER~1-style short names
that git stores in long form, causing worktreeMeta path comparisons to
fail on Windows CI.
All 11 real-fs + 16 unit tests green locally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(worktree): adversarial findings + macOS CI path-mismatch fix
## Root cause (macOS CI fail)
`reapOrphanWorktrees` stored `worktreePath` (gitdir-derived, real path via
git's symlink resolution, e.g. `/private/var/folders/…`) in results, while
the test's `wtDir` used the unresolved symlink form (`/var/folders/…`). After
reaping, `canonicalPath(wtDir)` can no longer call `realpathSync.native`
(directory gone), so it falls back to `path.resolve` — which returns the
symlink form — causing the `result.find()` comparison to miss.
## Fixes applied
### Source — worktree-safety.cjs
1. **Finding 1 (fail-closed PID check)**: Non-parseable lock content (e.g.
`"Locked by claude-code agent-xxx"`) is now treated as ALIVE with reason
`lock_owner_unknown`, not as dead. Previously it fell through as dead.
2. **Finding 1b (EPERM safe)**: `isPidAlive` call wrapped in try/catch; any
thrown error (EPERM = process exists but cross-user on Windows) → ALIVE.
3. **Finding 2 (startup warning)**: `cmdWorktreeReapOrphans` now writes a
one-line stderr warning when ≥1 entry is skipped or when reaper throws,
while keeping exit-zero so workflows don't break.
4. **Finding 3 (default-branch discovery)**: Local-only fallback now tries
`init.defaultBranch` config and HEAD symref before `main`/`master`, so
repos configured with `trunk`, `dev`, etc. get correct orphan detection.
5. **macOS path fix**: Result entry for reaped worktrees now uses `gitKnownPath`
(from `git worktree list`) instead of `worktreePath` (from gitdir file),
ensuring the caller always sees the path git uses for the worktree.
### Test — bug-3707-locked-worktree-cleanup.test.cjs
6. **macOS CI fix**: Pre-compute `wtDirCanonical = canonicalPath(wtDir)` before
calling `reapOrphanWorktrees` so the comparison works after removal.
7. **Gap 1**: New test — Claude Code lock format (`"Locked by claude-code …"`)
must not be reaped; asserts `status=skipped, reason=lock_owner_unknown`.
8. **Gap 2**: New test — `isPidAlive` throwing EPERM → must not reap.
9. **Gap 3**: New test — repo with `init.defaultBranch=trunk`; merged worktree
must be reaped (verifies trunk is discovered as the integration branch).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(test): raise waitForStoppedAt timeout 2 s → 5 s for Windows/Node22 CI load
Subprocess write latency exceeds 2 s on loaded windows-latest/Node22 runners
(test duration was 6181 ms); 5 s gives sufficient headroom without changing
any production behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Consolidates 7 sibling test files for sdk/src/query/query-dispatch.ts and
its stage handlers into a single query-dispatch.test.ts with 8 describe
blocks (699 LOC, 53 tests). Deletes 3 clean shim sources with zero non-test
importers. Leaves query-dispatch-formatting.ts and query-dispatch-error-mapper.ts
in place (non-test importer: query-fallback-executor.ts — deferred to #3732).
- query-dispatch-input-validation.ts deleted (clean shim)
- query-dispatch-plan.ts deleted (clean shim)
- query-dispatch-result-builder.ts deleted (clean shim)
- 6 per-stage test files deleted (consolidated into query-dispatch.test.ts)
- counter-tests added per Contract 6 for each stage field
- CONTEXT.md Glossary updated with Dispatch Pipeline Module entry
Closes#3731
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The 16 TDD cycles passed vitest but tsc --noEmit flagged TS2322:
UatItem lacked a string index signature, so the consumer at
phase-uat-passed.ts:249 (returning Record<string, unknown>[])
failed to typecheck. Index signature added; behaviour unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cycle 16 of 16: wires isPhaseUatPassed into the SDK query
registry as 'phase.uat-passed' (alias 'phase uat-passed').
Read-only, JSON output. Handler validates the phase argument
and adapts isPhaseUatPassed to the QueryHandler signature.
Adds INVALID_PHASE_NUM to ERROR_CODE for argument validation.
Updates command-aliases.generated.ts to keep seam coverage parity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cycle 15 of ~15: introduces PhaseUatPassedError (extends
GSDError, classification Validation) with typed ERROR_CODE
enum. Missing-projectDir now fails fast with a typed error
rather than letting downstream code crash on a vague fs error.
Cycle 14 of ~15: a UAT file present but empty of headings,
orphans, and placeholders now emits NO_ITEMS_EXTRACTED rather
than reporting passed=false with reasons=[] (which was
indistinguishable from missing-files).
Cycle 13 of ~15: 'result: [pending]' (template placeholder
copy-pasted without being filled) was previously dropped by
the \w+ regex. Now flagged as BRACKETED_PLACEHOLDER.
De-conflicted against ORPHAN_ITEM_MISSING_RESULT so an item
with a bracketed result doesn't double-report.
Cycle 12 of ~15: heading-without-result-line was previously
silently dropped by the regex — meaning a phase with an
unfilled UAT item could falsely pass the predicate. We now
scan for orphan headings and emit ORPHAN_ITEM_MISSING_RESULT
so the operator's typo / unfilled-template is surfaced.
Cycle 11 of ~15: reuses parseVerificationFrontmatterItems from
uat.ts. Frontmatter-declared manual-verification items roll
into the same items[] roster and emit HUMAN_VERIFICATION_NEEDED
reasons so passed=false is justified, not silent.
Cycle 10 of ~15: a captured 'result:' value that lowercases to
'pass' but isn't literally 'pass' now produces a CASE_MISMATCH
reason rather than a generic NON_PASS_RESULT — so operators can
distinguish a real non-pass from a likely typo.
Cycle 9 of ~15: regex now matches both bare 'result:' and
bold-prefixed '**result:**' forms. Requester used the bold
form in the issue text; canonical template uses bare. Both
are equally valid; predicate is form-agnostic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cycle 8 of ~15: adds blockquote-line stripping pass. Quoted
documentation snippets no longer pollute the item roster.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cycle 6 of ~15: adds fenced-block stripping pass to
stripMarkdownInjection. Docs-and-examples inside code fences
no longer pollute the item roster.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cycle 5 of ~15: introduces stripMarkdownInjection helper.
First pass strips the YAML frontmatter region so injected
'### N. item' patterns inside frontmatter literal blocks
cannot be mistaken for real UAT items.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cycle 4 of ~15: phase dir present but no *-HUMAN-UAT.md files
now returns a typed NO_UAT_FILES reason instead of an empty
reasons array (which prior cycles used as a 'shouldn't happen'
fallback).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cycle 3 of ~15: when resolvePhaseDir returns null, predicate
returns a typed NO_PHASE_DIR reason. Previously the empty
reasons array was indistinguishable from other failure modes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>