* refactor: remove stale sdk/src generated-file banners from bin/lib/*.cjs (#506)
Drop the GENERATED FILE / Source: sdk/src / Regenerate: cd sdk banners from
13 hand-maintained CJS modules and delete the orphaned
generator-freshness-contract script + test. Post-ADR-0174 cleanup; the
referenced sdk/ generator pipeline (dir, gen:* scripts, *.generated.cjs) no
longer exists. No runtime behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: add changeset for #510 (sdk/src banner cleanup)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The Pass-2 topological level assignment in cmdPhasePlanIndex dequeued its
Kahn's-algorithm queue with Array.shift(), which is O(n) per call in V8, so the
BFS was O(V^2) and slowed superlinearly on deep queues (wide fan-in plan
graphs). Extract the traversal into a pure, exported computeDependencyLevels
(rawPlans, planMap, canonicalToId) and dequeue via a head index (queue[head++])
-> O(V+E). Behavior is identical: same FIFO order, same longest-path levels,
same visited-count cycle detection. A complexity-contract comment above the loop
documents why shift() must not be reintroduced.
Adds tests/phase-dependency-levels.test.cjs with deterministic behavior and
edge-case coverage (linear chain, diamond longest-path, independent set, cycle,
canonical-prefix resolution, empty, self-loop, duplicate edge, external dep). A
timing-based complexity guard was intentionally omitted: the O(V+E) Map-build
constant dilutes the O(V^2) signal until impractical N (~1e6), so an empirical
guard is inherently flaky on contended CI — the contract is enforced by the
inline comment and correctness tests instead.
Fixes#307
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(4): reproduce non-idempotent phase complete + unclamped percent in CJS CLI
RED regression tests for issue #4:
- T1: double invocation of cmdPhaseComplete double-increments **Completed Phases:**
in STATE.md body (blind parseInt+1 instead of deriving from ROADMAP)
- T2: progress percent can exceed 100% when Completed Phases > Total Phases
The CJS path (bin/lib/phase.cjs:cmdPhaseComplete) has the bug; the SDK path
(phase-lifecycle.ts:phaseComplete, fixed in ~PR#3520) already derives
completed_phases from ROADMAP Complete-row count, making it idempotent.
References:
- Issue #4 (open-gsd/get-shit-done-redux)
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- /tmp/adr-3524-review-findings.md (architectural justification)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(4): add sdk/scripts/gen-phase-lifecycle-policy.mjs generator + freshness check placeholder
Generates phase-lifecycle-policy.generated.cjs from sdk/src/query/phase-lifecycle-policy.ts.
All functions in phase-lifecycle-policy.ts are pure transforms (no I/O), directly
serializable via Function.prototype.toString(). The GSDError dependency is replaced
with a lightweight stub that throws plain Error objects — CJS callers that need
process.exit(1) behavior catch these and delegate to error().
This is the "I/O adapter pattern" from ADR-3524 Section 4 applied to pure helpers.
References:
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- /tmp/adr-3524-review-findings.md (architectural justification)
- Issue #4 (open-gsd/get-shit-done-redux)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(4): add sdk/scripts/gen-phase.mjs generator
Generates phase.generated.cjs from sdk/src/query/phase.ts.
Only pure helpers (isCanonicalPlanFile, describeNonCanonicalPlans) are generated;
async query handlers (findPhase, phasePlanIndex) are I/O-bound and remain per-side
per ADR-3524 Section 4.
References:
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- /tmp/adr-3524-review-findings.md (architectural justification)
- Issue #4 (open-gsd/get-shit-done-redux)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(4): add sdk/scripts/gen-phase-lifecycle.mjs + core idempotency fix logic
Generates phase-lifecycle.generated.cjs providing two pure functions that are the
root-cause fix for issue #4:
1. deriveProgressFromRoadmap(roadmapContent): counts Complete rows in ROADMAP
progress table — makes completed_phases idempotent (derived from ground truth
instead of blind +1). Direct transcription of the SDK's "Root cause 1 fix"
block in phase-lifecycle.ts (~line 1644).
2. clampPercent(completed, total): percent capped at 100 — prevents >100% progress
when Completed Phases exceeds Total Phases.
Design note: the full phase lifecycle mutations (add, insert, remove, complete) are
inherently async and I/O-bound. Per ADR-3524 Section 4 ("I/O stays per-side"), those
are NOT generated. Only the pure-computation kernel is extracted, following the
I/O adapter pattern: pure logic shared; each side (CJS sync, SDK async) supplies
its own I/O adapter.
The pure functions are defined in the generator as real JS functions and serialized
via Function.prototype.toString() — same technique as gen-project-root.mjs — rather
than embedded in template literals (which would require double-escaping all regex
backslashes).
References:
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- /tmp/adr-3524-review-findings.md (architectural justification)
- Issue #4 (open-gsd/get-shit-done-redux)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(4): emit phase.generated.cjs, phase-lifecycle.generated.cjs, phase-lifecycle-policy.generated.cjs
Three generated CJS artifacts from their respective generator scripts:
- phase.generated.cjs (1.7K): isCanonicalPlanFile + describeNonCanonicalPlans
from sdk/src/query/phase.ts
- phase-lifecycle.generated.cjs (3.6K): deriveProgressFromRoadmap + clampPercent
— the idempotency+clamp fix for issue #4
- phase-lifecycle-policy.generated.cjs (7.0K): 14 pure phase naming/directory
helpers from sdk/src/query/phase-lifecycle-policy.ts
Run to regenerate:
node sdk/scripts/gen-phase.mjs
node sdk/scripts/gen-phase-lifecycle.mjs
node sdk/scripts/gen-phase-lifecycle-policy.mjs
References:
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- Issue #4 (open-gsd/get-shit-done-redux)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(4): migrate bin/lib/phase.cjs cmdPhaseComplete to use generated helpers
Replace the blind-increment + unclamped percent bug in cmdPhaseComplete with
the idempotent ROADMAP-derived approach: read freshly-updated ROADMAP, call
deriveProgressFromRoadmap() from phase-lifecycle.generated.cjs, fall back to
existing value when ROADMAP is unavailable. clampPercent() prevents >100%.
Root cause fix for issue #4: the original parseInt(completedRaw) + 1 on every
call made phase complete non-idempotent; the missing Math.min(100, ...) clamp
allowed Progress to exceed 100%.
I/O adapter pattern (ADR-3524 §4): pure computation in generated module;
CJS supplies sync readFileSync; SDK supplies async readFile. Same logic, two adapters.
Closes: Tests in 4-phase-complete-cjs-regression.test.cjs go GREEN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(4): add freshness checks + npm scripts for phase generated artifacts (D4/D6)
Add check-phase-fresh.mjs, check-phase-lifecycle-fresh.mjs, and
check-phase-lifecycle-policy-fresh.mjs (same pattern as check-project-root-fresh.mjs:
import buildXCjs() from the generator, regenerate in-memory, byte-compare to committed
file, exit 1 if stale).
Add gen:phase, check:phase-fresh, gen:phase-lifecycle, check:phase-lifecycle-fresh,
gen:phase-lifecycle-policy, check:phase-lifecycle-policy-fresh to sdk/package.json.
Note: gen:phase-lifecycle / check:phase-lifecycle-fresh do not require 'npm run build'
because the generator defines pure functions directly rather than importing dist.
Update shared-module-handsync-allowlist.json: reclassify phase.cjs justification to
reflect that it now consumes phase-lifecycle.generated.cjs for cmdPhaseComplete. The
*.generated.cjs files are excluded by the lint scanner (excludes *.generated.cjs) so
no new allowlist entries are required for the generated artifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(4): add freshness-check CI steps for phase, phase-lifecycle, phase-lifecycle-policy
Add three drift-check steps to .github/workflows/test.yml following the same
pattern as the existing freshness checks (ubuntu-latest + node 24 only):
- SDK generated phase artifact drift check
- SDK generated phase-lifecycle artifact drift check
- SDK generated phase-lifecycle-policy artifact drift check
These guard against editors modifying the generated *.cjs files directly.
They run check-phase-fresh.mjs, check-phase-lifecycle-fresh.mjs, and
check-phase-lifecycle-policy-fresh.mjs respectively (added in D4).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(4): amend ADR-3524 — phase * I/O adapter pattern for issue #4 (D8)
Append a 2026-05-23 amendment to docs/adr/3524-cjs-sdk-hard-seam.md documenting
the Phase * cooperating-sibling retirement: three new generator scripts extract
pure-computation helpers from phase.ts / phase-lifecycle.ts / phase-lifecycle-policy.ts,
cmdPhaseComplete migrates to deriveProgressFromRoadmap + clampPercent for idempotency,
freshness checks + CI steps added.
Clarifies what is NOT generated (async I/O-bound mutation handlers stay per-side per
Section 4) and notes open drift bugs #6 and #26 for traceability.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(4): add changeset fragment for cmdPhaseComplete fix
Refs #4
* fix(154): use canonical /gsd:plan-phase form in phase-lifecycle-policy.ts
Replaces the retired /gsd-plan-phase slash command reference with the
canonical colon-namespaced /gsd:plan-phase in the TS source template
string that feeds the generated CJS roadmap entry helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(154): regenerate phase-lifecycle-policy.generated.cjs after slash-namespace fix
Regenerated via node sdk/scripts/gen-phase-lifecycle-policy.mjs after
fixing /gsd-plan-phase → /gsd:plan-phase in the TS source. Generated
file now contains the canonical colon form.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(154): cross-platform frontmatter regex anchor in 4-phase-complete-cjs-regression.test.cjs
Replaces /^---\n/ with /^---\r?\n/ so the frontmatter extraction helper
in the regression test tolerates Windows CRLF line endings (autocrlf=true
checkout leaves \r before \n, causing /^---\n/ to never match).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(154): add new generated CJS modules to INVENTORY.md and regenerate manifest
Adds three missing rows to the CLI Modules table:
- phase-lifecycle-policy.generated.cjs
- phase-lifecycle.generated.cjs
- phase.generated.cjs
Bumps the headline count from 74 to 77 to match the filesystem.
Also regenerates docs/INVENTORY-MANIFEST.json via
node scripts/gen-inventory-manifest.cjs --write.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(154): regenerate phase-lifecycle-policy.generated.cjs with hyphen form
Root cause: commit 6cd701f4 regenerated the CJS artifact but at that point
sdk/dist/query/phase-lifecycle-policy.js already had the correct /gsd-plan-phase
(hyphen) form while sdk/src/query/phase-lifecycle-policy.ts still had /gsd:plan-phase
(colon). The generator uses Function.prototype.toString() on the compiled dist, so
the CJS picked up the wrong string from the stale TS source that was compiled into
dist at some earlier point.
Fix: correct the TS source to /gsd-plan-phase and re-run gen-phase-lifecycle-policy.mjs
so that buildPhaseRoadmapEntry in the CJS emits the hyphen form, satisfying the
bug-3584-runtime-slash-emitters.test.cjs assertion at line 179.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(154): restore /gsd:plan-phase canonical form in phase-lifecycle-policy
Commit 0c3a9c75 incorrectly reverted the slash-namespace fix by misreading
sdk/dist/ (a build artifact in hyphen form for non-Claude runtimes) as the
authoritative source. The canonical form for Claude-facing source is
/gsd:plan-phase (colon-namespaced).
Fix: revert TS source back to /gsd:plan-phase, rebuild dist, regenerate
phase-lifecycle-policy.generated.cjs.
Fixes bug-2543-gsd-slash-namespace test failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(154): update INVENTORY.md CLI Modules count to 79 after rebase onto main
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(154): use hyphen form /gsd-plan-phase in persisted phase section template
- sdk/src/query/phase-lifecycle-policy.ts: use /gsd-plan-phase (routable
hyphen form) in the phase scaffold template that gets persisted to
ROADMAP.md; bug-3584 requires persisted artifacts use the hyphen form
- docs/INVENTORY.md: add missing runtime-name-policy.cjs row in CLI
Modules table
- tests/4-phase-complete-cjs-regression.test.cjs: add maxRetries/retryDelay
to rmSync calls to satisfy Windows parity ratchet (baseline was 95)
- Regenerate phase-lifecycle-policy.generated.cjs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <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>
* 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>
Three regex defects in cmdRoadmapAnnotateDependencies (roadmap.cjs):
1. Plans-block detection (line ~553): `Plans:\s*\n` required no text after
the colon, silently skipping `Plans: 3 plans\n` and `**Plans:** N\n`.
Fixed: `\*{0,2}Plans\*{0,2}:[^\n]*\n` + require `+` checklist lines so
a bold summary line above a bare `Plans:` block doesn't consume the match.
2. Phase-section boundary (line ~542): `\d` matched only one digit, so
`### Phase 02.3:` was not recognised as a section terminator, allowing
plan-list content from adjacent decimal phases to bleed in. Fixed with
`\d[\d.]*`. Same one-digit boundary also patched in roadmap.cjs (analyze
path), phase.cjs (insert-after path), and init.cjs (section-slice path).
3. Plan-ID extraction (line ~566): `[\w-]+?` excluded `.`, capturing `02`
from `02.3-01-PLAN.md` instead of `02.3-01`, so planData.find never
resolved and every plan defaulted to wave 1. Fixed: `[\w.-]+?`.
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>
phase.cjs:updateRoadmapAfterPhaseRemoval renumbers plan references in
ROADMAP.md via a regex that captured `NN-NN` followed by an optional
suffix:
/(?<![0-9-])(\d{2})-(\d{2})(?=(?:-(?:PLAN|SUMMARY)\.md)?(?![0-9-]))/g
The suffix branch was strict: it only accepted `-(PLAN|SUMMARY).md`
directly after the plan number. A slug like
`07-01-cherry-pick-foundation-PLAN.md` placed `-cherry-…` between the
number and the canonical suffix, so both the suffix branch AND the
"bare token" branch (`(?![0-9-])` — fails because the next char is `-`)
failed. Result: the on-disk file got renamed to
`06-01-cherry-pick-foundation-PLAN.md` by the directory-rename pass,
but the ROADMAP entry kept pointing at the stale `07-01-…` prefix —
disk/ROADMAP inconsistency.
Fix: extend the suffix branch to allow an optional kebab-case slug
between the plan number and the PLAN/SUMMARY suffix:
(?:(?:-[A-Za-z][A-Za-z0-9-]*)*-(?:PLAN|SUMMARY)\.md)|(?![0-9-])
Each slug token must start with a letter so `07-01-02-PLAN.md` is not
silently consumed as one slugged token (the `-02` is correctly
unreachable from the slug branch because it starts with a digit).
Regression test exercises three cases via the typed `roadmap get-phase
--json` query (no raw text matching on ROADMAP.md content):
1. Slugged PLAN + SUMMARY filenames get renumbered (#3602 fix).
2. Compact `NN-NN-PLAN.md` filenames still renumber correctly
(#3601 / earlier contracts preserved).
3. Counter-test: ISO dates (`2026-01-01`) and version tags (`v1-2-3`)
in ROADMAP prose are NOT modified — the existing `(?<![0-9-])` /
`(?![0-9-])` boundaries hold against false positives.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`phase remove N` for an integer phase silently deleted the adjacent
`### Phase N.1:` decimal section when the decimal was a peer-depth
heading. The bug was in the section-removal regex inside
get-shit-done/bin/lib/phase.cjs:updateRoadmapAfterPhaseRemoval:
(?=\n#{2,4}\s+Phase\s+\d+\s*:|$)
The lookahead required the next header's digits to be followed by
`\s*:` — true for `### Phase 3:` but false for `### Phase 2.1:` because
the `.1` breaks the match. The non-greedy `[\s\S]*?` body then consumed
`Phase 2.1` along with `Phase 2` until it found the next integer
header. The on-disk phase directory `.planning/phases/02.1-*` survived
but its ROADMAP entry was gone — disk/ROADMAP inconsistency.
The fix uses a depth-aware lookahead: capture the hash count of the
header being removed with a named group `(?<h>#{2,4})` and require the
end-of-section lookahead to match the SAME depth via `\k<h>(?!#)`. The
`(?!#)` guards against `###` accidentally matching a deeper `####`
header by anchoring on the captured hash count.
This preserves two contracts simultaneously:
- #3601: removing `### Phase 2:` (depth 3) stops at the next depth-3
header, including `### Phase 2.1:` — the peer-level decimal is
preserved.
- #3355: removing `### Phase 27:` (depth 3) continues past
`#### Phase 27.1:` (depth 4, a child of the integer phase) until it
reaches the next depth-3 header. The child decimal is part of the
integer phase being removed.
The regression test exercises the public CLI via runGsdTools and
asserts on typed JSON output from `roadmap get-phase --json` — no raw
text matching on ROADMAP.md content (per CONTRIBUTING.md
"Prohibited: Raw Text Matching on Test Outputs").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce `runtime-slash.cjs` as the single source of truth for emitting
GSD slash-command references in user-facing runtime output and persisted
artifacts. `formatGsdSlash(commandName, runtime)` produces `/gsd-<cmd>`
for skills-based runtimes (Claude/Cursor/OpenCode/Kilo/etc.) and
`$gsd-<cmd>` for Codex. The deprecated `/gsd:<cmd>` colon form is never
emitted — pasting a recommended-action command into Claude Code now
routes correctly instead of failing with `Unknown command`.
Wired into the high-impact emitters identified in #3584:
- `init.cjs` `cmdInitManager` recommended_actions[].command (the
original failure path in the bug report) plus the no-ROADMAP /
no-STATE error hints.
- `phase.cjs` `cmdPhaseAdd`, `cmdPhaseAddBatch`, `cmdPhaseInsert` —
ROADMAP.md `Plans:` references now persist the routable form
instead of the legacy colon form.
- `verify.cjs` `cmdValidateHealth` — every fix-hint addIssue() call
(E001/E002/E003/E004/E005, W002/W003/W008/W009/W011/W016/W018) and
the persisted STATE.md regenerate / MILESTONES.md backfill notes.
- `milestone.cjs` `cmdMilestoneComplete` — Operator Next Steps tail
rewrite.
- `validate-command-router.cjs` — `validate context` recommendation
strings for WARNING/CRITICAL utilization bands.
- `workstream.cjs` — missing .planning hint.
- `profile-output.cjs` — `generate-claude-md` workflow enforcement
block, project/skills fallbacks, profile placeholder, and the
dev-preferences refresh hints.
- `drift.cjs`, `gsd2-import.cjs`, `commands.cjs scaffold context` —
remaining one-off persisted references.
Runtime detection: `resolveRuntime(projectDir)` reads
`process.env.GSD_RUNTIME` first, then a side-effect-free direct read of
`.planning/config.json` (NOT `loadConfig`, which would normalize legacy
keys and re-write the file just to read the runtime name).
Tests:
- `tests/bug-3584-runtime-slash-formatter.test.cjs` — 22 unit tests
covering the pure formatter and resolver (hyphen vs codex, prefix
normalization, defensive returns, env/config/default chain).
- `tests/bug-3584-runtime-slash-emitters.test.cjs` — 6 integration
tests exercising `init manager`, `phase add` (via the structured
`roadmap get-phase` payload to avoid raw-text matching on the
on-disk artifact), `validate health`, `validate context`, and the
codex variant.
- Existing tests updated to assert the new contract: validate-context
recommendations, claude-md workflow block, milestone complete
Operator Next Steps. Copilot-install engine-conversion test now
asserts against a synthetic input since bin/lib/*.cjs no longer
contains literal `/gsd:` references for the install-time converter
to rewrite.
INVENTORY.md and INVENTORY-MANIFEST.json updated for the new module
(64 CLI modules shipped, +1).
Fixes#3584
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(3537): route every phase-number ROADMAP regex through phaseMarkdownRegexSource
v1.42.1 added the padding-tolerant `phaseMarkdownRegexSource()` helper but
wired it into only 1 of 8 call sites that build phase-number regexes against
ROADMAP/STATE prose. The other 7 used raw `escapeRegex(phaseNum)` or partial
`0*${escapeRegex(...)}` (tolerated extra padding, not missing), so when
skills passed the resolved padded form (`02.7`) against un-padded ROADMAP
prose (`### Phase 2.7:`, `- [ ] **Phase 2.7:**`), the verbs silently no-op'd
while reporting success.
This consolidates every phase-number ROADMAP/STATE regex through the
canonical helper:
- Promote `phaseMarkdownRegexSource` from `roadmap.cjs` to `core.cjs` so
`phase.cjs` and `core.cjs` itself can consume it (no circular dep —
both already import `core.cjs`).
- Wire the helper into the 7 remaining sites:
- `core.cjs:getRoadmapPhaseInternal` (replaces hand-rolled `isNumeric`
branch that only padded integers, not decimals).
- `roadmap.cjs:cmdRoadmapGetPhase` (searchPhaseInContent escapedPhase).
- `roadmap.cjs:cmdRoadmapAnalyze` checkbox lookup.
- `roadmap.cjs:cmdRoadmapAnnotateDependencies` phase header lookup.
- `phase.cjs:cmdPhaseNextDecimal` ROADMAP prose scan.
- `phase.cjs:cmdPhaseInsert` target anchor + decimal scan + header.
- `phase.cjs:cmdPhaseComplete` (3 regexes: checkbox, plan-count,
REQUIREMENTS extraction).
Adds `tests/bug-3537-padded-id-against-unpadded-roadmap.test.cjs` — a
parity-style regression matching CONTEXT.md DEFECT.GENERATIVE-FIX: for
each user-facing verb, asserts that the padded form (`02.7`) and the
un-padded form (`2.7`) produce identical ROADMAP.md against an identical
fixture. Includes one control case (`update-plan-progress`, already wired
in 1.42.1) to prove the parity assertion is non-vacuous.
Closes#3537
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(3537): add changeset fragment (pr: placeholder, amended post-create)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(3537): pin changeset pr: field to #3538
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(shell-projection): migrate roadmap.cjs writes to platformWriteSync (#3467)
2 atomicWriteFileSync calls → platformWriteSync. The seam owns markdown
normalization, so the explicit utf-8 encoding arg is no longer needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate config.cjs writes to platformWriteSync (#3467)
- 3 atomicWriteFileSync calls → platformWriteSync
- 1 raw fs.writeFileSync (depth→granularity migration) → platformWriteSync
- 2 fs.mkdirSync(planningBase, { recursive: true }) → platformEnsureDir
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate docs.cjs reads to platformReadSync (#3467)
6 try { fs.readFileSync } catch {} patterns → platformReadSync(path) with
explicit null guards. detectProjectType now reads package.json once and
shares it across has_cli_bin/is_monorepo/has_tests checks. JSON.parse is
still wrapped in a try (parsing is a separate failure mode from missing file).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate audit.cjs reads to platformReadSync (#3467)
8 try { fs.readFileSync(safeFilePath, 'utf-8') } catch { continue } patterns
→ const content = platformReadSync(safeFilePath); if (content === null) continue;
The single safeSum case (where catch set status='unreadable' rather than
continue) maps to an if/else that preserves the same semantics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate planning-workspace.cjs to platform* seam (#3467)
- 2 try { fs.readFileSync } catch {} → platformReadSync (null on missing)
- 2 fs.writeFileSync (workstream pointer writes) → platformWriteSync
- 3 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
The .lock file write at withPlanningLock is intentionally NOT migrated.
That call uses { flag: 'wx' } for atomic exclusive-create, which is the
correct lock-acquisition primitive. platformWriteSync's atomic-rename
pattern would silently overwrite an existing lock file and break the
locking guarantee.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate milestone.cjs writes to platform* seam (#3467)
- 5 atomicWriteFileSync calls → platformWriteSync (4 dropped normalizeMd
wrapper; seam handles .md normalization automatically)
- 2 raw fs.writeFileSync (archive ROADMAP.md / REQUIREMENTS.md) → platformWriteSync
- 2 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
- Dropped normalizeMd import (only used as write pre-call here)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate intel.cjs to platform* seam (#3467)
- 7 fs.readFileSync (existsSync+readFileSync patterns and try/catch) → platformReadSync
- 2 fs.writeFileSync → platformWriteSync
- 1 fs.mkdirSync(intelPath, { recursive: true }) → platformEnsureDir
- Consolidated dual-check (existsSync + readFileSync) into single platformReadSync
call returning null on missing file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate workstream.cjs to platform* seam (#3467)
- 5 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
- 1 fs.writeFileSync (STATE.md initial scaffold) → platformWriteSync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate init.cjs reads/writes to platform* seam (#3467)
- 11 try/readFileSync and existsSync+readFileSync patterns → platformReadSync
- 1 fs.writeFileSync (skill-manifest.json) → platformWriteSync
Three bare fs.readFileSync calls remain (ROADMAP/STATE reads in code paths
where the file is required to exist) — these are not "Done when" violations
(no try/catch wrapping, no inline existsSync guard).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate commands.cjs reads/writes to platform* seam (#3467)
- 6 try/readFileSync and existsSync+readFileSync patterns → platformReadSync
- 2 fs.writeFileSync → platformWriteSync
- 3 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
- Removed unused safeReadFile import (zero call sites in this file)
Three bare fs.readFileSync calls remain (sourcePath at line 752, fullPath at
443, roadmapPath in cmdAuditOpen) — preceded by existsSync guards or in code
paths where file presence is required; not "Done when" violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate profile-output.cjs to platform* seam (#3467)
- 6 safeReadFile (from core.cjs) calls preserved by aliasing platformReadSync
as safeReadFile in the import — same semantics, zero call-site changes
- 3 try/JSON.parse(readFileSync) patterns → platformReadSync + try/JSON.parse
- 1 existsSync+readFileSync pattern (claude.md update) → platformReadSync
- 5 fs.writeFileSync → platformWriteSync
- 4 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
Two bare fs.readFileSync calls remain (template reads where file must exist
or fail loudly) — not "Done when" violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate state.cjs to platform* seam (#3467)
- 4 atomicWriteFileSync calls → platformWriteSync (3 dropped normalizeMd
wrapper; seam handles .md normalization)
- 4 try/readFileSync and existsSync+readFileSync patterns → platformReadSync
- 1 fs.writeFileSync (WAITING.json) → platformWriteSync
- 1 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
- Dropped normalizeMd and atomicWriteFileSync imports (only used as write
pre-calls here)
Bare fs.readFileSync calls remain in code paths where STATE.md is required
to exist (statePath reads in cmd handlers, dry-run prune) — not "Done when"
violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate core.cjs to platform* seam (#3467)
- 7 try/readFileSync and existsSync+readFileSync patterns → platformReadSync
- 3 fs.writeFileSync (config writes + large-payload temp file) → platformWriteSync
- 1 fs.mkdirSync (GSD_TEMP_DIR) → platformEnsureDir
Three fs calls remain — they are the internal implementations of the
safeReadFile and atomicWriteFileSync wrappers that core.cjs exports for
backward compatibility. The wrappers are scheduled for removal in Phase 4
(#3468) and will not be migrated here.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate phase.cjs writes to platform* seam (#3467)
- 6 atomicWriteFileSync calls → platformWriteSync
- 3 fs.writeFileSync(path.join(dirPath, '.gitkeep'), '') → platformWriteSync
- 3 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir
Bare fs.readFileSync calls remain for roadmapPath/planPath reads where the
file is required to exist; these are not "Done when" violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate verify.cjs to platform* seam (#3467)
- 8 safeReadFile (from core.cjs) calls preserved by aliasing platformReadSync
as safeReadFile in the import — same semantics, zero call-site changes
- 1 existsSync+readFileSync inline ternary → safeReadFile (returns null)
- 5 fs.writeFileSync (config writes + milestones writes) → platformWriteSync
Bare fs.readFileSync calls remain for code paths where the file is required
to exist (roadmap/state/config full reads); these are not "Done when"
violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(shell-projection): migrate frontmatter.cjs + update atomic-write test (#3467)
- frontmatter.cjs: 2 atomicWriteFileSync calls → platformWriteSync. The
legacy normalizeMd wrapper is dropped because the seam handles markdown
normalization. safeReadFile preserved by aliasing platformReadSync.
- atomic-write-coverage.test.cjs: update the #1972 structural invariant
to assert on platformWriteSync. platformWriteSync uses the same
tmp-file + atomic-rename primitive that atomicWriteFileSync did — the
no-partial-write guarantee is preserved across the migration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(changeset): add entry for shell-projection Phase 3 migration (#3467)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(coderabbit): disable ESLint tool (repo uses custom lint scripts)
CodeRabbit's review surface emits a "skipped: no ESLint configuration"
warning because the repo doesn't ship ESLint config. The repo
intentionally does not use ESLint — it ships its own targeted lint
scripts (scripts/lint-no-source-grep.cjs, npm run lint:tests) that
enforce repo-specific test-quality invariants. Adding ESLint config
purely to satisfy CR would add an external dependency
(CONTRIBUTING.md: "No external dependencies in core") and overlap
with the existing custom lint surface.
Disable the ESLint tool in CR's tools config so the skip warning
stops appearing on every PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3266): preserve wave 0 and bucket plans by depends_on DAG in phase-plan-index
Fixes two cooperating bugs in the phase-plan-index builder:
1. Wave 0 collapse: `parseInt(...) || 1` coerced parsed value `0` to `1` due to
JS falsy default. Fixed with `Number.isNaN` guard.
2. depends_on ignored: wave-bucketing used only the `wave:` frontmatter field.
Now replaced with Kahn's topological-level algorithm over `depends_on`:
source nodes (no in-phase deps) → lowest level; each plan's level = max(deps'
levels) + 1. Declared `wave:` that disagrees with computed level emits a
non-fatal warning on the result. Cycle detection throws GSDError.
`PlanInfo` gains `depends_on: string[]`. `PhasePlanIndex` gains `warnings?: string[]`.
Both TS (`sdk/src/query/phase.ts`) and CJS twin (`get-shit-done/bin/lib/phase.cjs`)
fixed identically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add changeset for #3276
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(phase): resolve depends_on against canonical plan id (#3276 CR)
Build a secondary `canonicalToId` index alongside `planMap` so that a
dependency declared as '03-01' resolves to a descriptive plan stored
under '03-01-auth-hardening', preventing silent wave-ordering failures.
Applied at both DAG construction sites in phase.cjs and the SDK's
phase.ts (k014 parity). Regression test added.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes#3164
Validators and find-phase hardcoded phasesDir = .planning/phases/, so projects
using the milestone-archive layout (.planning/milestones/v*-phases/) had an empty
diskPhases set, triggering W006 for every active phase and find-phase returning
found:false.
Add collectDiskPhases(planBase) helper that scans both flat layout and all
.planning/milestones/v*-phases/ subdirs. Wire it into cmdValidateConsistency,
cmdValidateHealth (both the Check 4 validPhases set and Check 8 diskPhases),
and refactor cmdFindPhase to iterate candidate search dirs so it also searches
milestone-archive dirs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2893): surface non-canonical plan filenames instead of silently returning zero plans
Reporter saw `plan_count: 0` from `/gsd:execute-phase` even though five
plan files existed on disk. Investigation showed the planner had written
files like `01-PLAN-01-foundation.md`, while `phase-plan-index`'s strict
filter (`f.endsWith('-PLAN.md') || f === 'PLAN.md'`) rejected them
silently — collapsing two distinct states into the same `plans: []`
return:
- directory truly has no plans (legit empty)
- directory has plans but the filter rejected them (user/agent error)
The canonical contract is documented in three places:
- `agents/gsd-planner.md` write_phase_prompt step (lines 1063-1080)
- `commands/gsd/plan-phase.md`
- `references/universal-anti-patterns.md` (rule 26)
It mandates `{padded_phase}-{NN}-PLAN.md` and explicitly forbids
`PLAN-NN.md` / `01-PLAN-01.md` / `plan-NN.md` etc. The strict filter is
correct per that contract. The bug is that the executor never tells the
user when the contract was violated — they just see `plan_count: 0`
with no signal.
Fix: add a diagnostic helper `describeNonCanonicalPlans()` that scans
the phase directory for files matching `*PLAN*.md` (the diagnostic net)
that the canonical filter rejected, excluding legit derivatives like
`*-PLAN-OUTLINE.md` and `*-PLAN.pre-bounce.md`. When offenders exist,
return a `warning` field naming each one and citing the canonical
pattern so the user knows what to rename to.
Wired into the three filter sites:
- `phase-plan-index` (the executor's main entry point)
- `phases list --type plans`
- `find-phase`
The strict filter itself is unchanged — existing canonical plans behave
identically. This is purely a diagnostic that converts silent-empty
into loud-with-actionable-error.
Tests:
- `phase-plan-index returns warning for reporter's exact filename
pattern (`01-PLAN-01-foundation.md`)`
- `truly empty dir does not emit a warning`
- `canonical plans + outline + pre-bounce files do not emit a warning`
Closes#2893
* test(#2893): add parity tests for find-phase and phases list --type plans warnings
CodeRabbit's only finding on the prior commit: I wired the warning into
three filter sites (`phase-plan-index`, `find-phase`,
`phases list --type plans`) but only `phase-plan-index` had test
coverage for the warning shape. The other two paths could silently
diverge during future refactors — exactly the silent-drift class of bug
this fix exists to prevent.
Add four parity tests mirroring the existing two:
- find-phase: non-canonical filenames produce a warning naming each
offender + citing the canonical pattern.
- find-phase: canonical plan + derivative files (PLAN-OUTLINE,
pre-bounce) produce no warning.
- phases list --type plans: same non-canonical case, but assert the
warning is prefixed with `${dir}: ` (this path aggregates across
phase directories so each offender is tagged with its dir).
- phases list --type plans: canonical case, no warning.
`node --test tests/phase.test.cjs`: 98/98 pass (was 94, +4 new).
* fix(#2769): tolerate Requirements header with colon inside bold delimiters
extractReqIds in sdk/src/query/init.ts and the legacy init.cjs port only
matched `**Requirements**:` (colon outside bold), so phases declared with
the equally-valid markdown form `**Requirements:**` (colon inside bold,
which is what the project's own templates emit) returned phase_req_ids:
null for both `init plan-phase` and `init execute-phase`.
The mirror-image bug in `phase complete`'s REQUIREMENTS.md traceability
sweep at get-shit-done/bin/lib/phase.cjs:871 only matched the inside-bold
form, silently skipping the REQ-ID checkbox flips for any roadmap that
used the outside-bold form. Both parsers now share the same canonical
regex that accepts all three rendered-identical variants:
**Requirements:** (colon inside bold)
**Requirements**: (colon outside bold)
**Requirements** : (space before outside colon)
Tests:
- tests/init.test.cjs — parameterized over the three header variants for
both init plan-phase and init execute-phase (6 new behavioral cases).
- sdk/src/query/init.test.ts — describe.each over the same variants
exercising initPlanPhase through the SDK.
- tests/bug-2769-requirements-header-variants.test.cjs — phase complete
flips REQ-001 in REQUIREMENTS.md across all three header variants.
Closes#2769
* refactor(#2769): centralize REQUIREMENTS_HEADER_RE constant per CodeRabbit
All workflow, command, reference, template, and tool-output files that
surfaced /gsd:<cmd> as a user-typed slash command have been updated to
use /gsd-<cmd>, matching the Claude Code skill directory name.
Closes#2697
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Commands are now installed as commands/gsd/<name>.md and invoked as
/gsd:<name> in Claude Code. The old hyphen form /gsd-<name> was still
hardcoded in hundreds of places across workflows, references, templates,
lib modules, and command files — causing "Unknown command" errors
whenever GSD suggested a command to the user.
Replace all /gsd-<cmd> occurrences where <cmd> is a known command name
(derived at runtime from commands/gsd/*.md) using a targeted Node.js
script. Agent names, tool names (gsd-sdk, gsd-tools), directory names,
and path fragments are not touched.
Adds regression test tests/bug-2543-gsd-slash-namespace.test.cjs that
enforces zero legacy occurrences going forward. Removes inverted
tests/stale-colon-refs.test.cjs (bug #1748) which enforced the now-obsolete
hyphen form; the new bug-2543 test supersedes it. Updates 5 assertion
tests that hardcoded the old hyphen form to accept the new colon form.
Closes#2543
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scan REQUIREMENTS.md body for all **REQ-ID** patterns during phase
complete and emit a warning for any IDs absent from the Traceability
table, regardless of whether the roadmap has a Requirements: line.
Closes#2526
* fix(phase): guard backlog dirs and YYYY-MM dates in integer phase removal
Closes#2435Closes#2434
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(phase): extend date-collision guard to hyphen-adjacent context
The lookbehind `(?<!\d)` in renameIntegerPhases only excluded
digit-prefixed matches; a YYYY-MM-DD date like 2026-05-14 has a hyphen
before the month digits, which passed the original guard and caused
date corruption when renumbering a phase whose zero-padded number
matched the month. Replace with `(?<![0-9-])` lookbehind and
`(?![0-9-])` lookahead to exclude both digit- and hyphen-adjacent
contexts. Adds a regression test for the hyphen-adjacent case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(handoffs): include project identity in all Next Up blocks
Adds project_code and project_title to withProjectRoot() and updates
all 30 Next Up headings across 18 workflow files to include
[PROJECT_CODE] PROJECT_TITLE suffix for multi-project clarity.
Closes#1948
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(review): add withProjectRoot tests and fix placeholder syntax (#1951)
Address code review feedback:
- Add 4 tests for project_code/project_title injection in withProjectRoot()
- Fix inconsistent placeholder syntax in continuation-format.md canonical
template (bare-brace → shell-dollar to match variant examples)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(phase): preserve zero-padded prefix in renameDecimalPhases
Captures the zero-padded prefix (e.g. "06" from "06.3-slug") with
(0*${baseInt}) so renamed directories keep their original format
(06.2-slug) instead of stripping padding (6.2-slug).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Higgins <brandonscotthiggins@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parallel `phase add` invocations each read disk state before any write
completes, causing all processes to calculate the same next phase number
and produce duplicate directories and ROADMAP entries.
The new `add-batch` subcommand accepts a JSON array of phase descriptions
and performs all directory creation and ROADMAP appends within a single
`withPlanningLock()` call, incrementing `maxPhase` within the lock for
each entry. This guarantees sequential numbering regardless of call
concurrency patterns.
Closes#2165
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(2129): add failing tests for 999.x backlog phase exclusion
Bug A: phase complete reports 999.1 as next phase instead of 3
Bug B: init manager returns all_complete:false when only 999.x is incomplete
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(2129): exclude 999.x backlog phases from next-phase scan and all_complete check
In cmdPhaseComplete, backlog phases (999.x) on disk were picked as the
next phase when intervening milestone phases had no directory yet. Now
the filesystem scan skips any directory whose phase number starts with 999.
In cmdInitManager, all_complete compared completed count against the full
phase list including 999.x stubs, making it impossible to reach true when
backlog items existed. Now the check uses only non-backlog phases.
Closes#2129
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Extend cmdStatePrune to prune Performance Metrics table rows older than cutoff
- Add workflow.auto_prune_state config key (default: false)
- Call cmdStatePrune automatically in cmdPhaseComplete when enabled
- Document workflow.auto_prune_state in planning-config.md reference
- Add silent option to cmdStatePrune for programmatic use without stdout
Closes#2087
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace 11 fs.writeFileSync calls with atomicWriteFileSync in three
files that write to .planning/ artifacts (ROADMAP.md, REQUIREMENTS.md,
MILESTONES.md, and frontmatter updates). This prevents partial writes
from corrupting planning files on crash or power loss.
Skipped low-risk writes: .gitkeep (empty files) and archive directory
writes (new files, not read-modify-write).
Files changed:
- milestone.cjs: 5 sites (REQUIREMENTS.md, MILESTONES.md)
- phase.cjs: 5 sites (ROADMAP.md, REQUIREMENTS.md)
- frontmatter.cjs: 2 sites (arbitrary .planning/ files)
Closes#1972
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cmdPhaseAdd computed maxPhase from ROADMAP.md only, allowing orphan
directories on disk (untracked in roadmap) to silently collide with
newly added phases. The new phase's mkdirSync succeeded against the
existing directory, contaminating it with fresh content.
Fix: take max(roadmapMax, diskMax) where diskMax scans
.planning/phases/ and strips optional project_code prefix before
parsing the leading integer. Backlog orphans (>=999) are skipped.
Adds 3 regression tests covering:
- orphan dir with number higher than roadmap max
- prefixed orphan dirs (project_code-NN-slug)
- no collision when orphan number is lower than roadmap max
Fixes#2026
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
replaceInCurrentMilestone() locates content by finding the last </details>
in the ROADMAP and only operates on text after that boundary. When the
current (in-progress) milestone section is itself wrapped in a <details>
block (the standard /gsd-new-project layout), the phase section's
**Plans:** counter lives INSIDE that block. The replacement target ends up
in the empty space after the block's closing </details>, so the regex never
matches and the plan count stays at 0/N permanently.
Fix: switch the plan count update to use direct .replace() on the full
roadmapContent, consistent with the checkbox and progress table updates
that already use this pattern. The phase-scoped heading regex
(### Phase N: ...) is specific enough to avoid matching archived phases.
Adds two regression tests covering: (1) plan count updates inside a
<details>-wrapped current milestone, and (2) phase 2 plan count is not
corrupted when completing phase 1.
cmdPhaseComplete used replaceInCurrentMilestone() to update the overview
bullet checkbox (- [ ] → - [x]), but that function scopes replacements
to content after the last </details> tag. The current milestone's
overview bullets appear before any <details> blocks, so the replacement
never matched.
Switch to direct .replace() which correctly finds and updates the first
matching unchecked checkbox. This is safe because unchecked checkboxes
([ ]) only exist in the current milestone — archived phases have [x].
Closes#1998
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Backlog phases use 999.x numbering and should not be counted when
calculating the next sequential phase ID. Without this fix, having
backlog phases causes the next phase to be numbered 1000+.
Co-authored-by: gg <grgbrasil@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cmdPhaseComplete and cmdPhasesRemove read STATE.md outside the lock
then wrote inside. A crash between the ROADMAP update (locked) and
the STATE write left them inconsistent. Wrap both STATE.md updates in
readModifyWriteStateMd to hold the lock across read-modify-write.
Also exports readModifyWriteStateMd from state.cjs for cross-module use.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
next-decimal and insert-phase only scanned directory names in
.planning/phases/ when calculating the next available decimal number.
When agents added backlog items by writing ROADMAP.md entries and
creating directories without calling next-decimal, the function would
not see those entries and return a number that was already in use.
Both functions now union directory names AND ROADMAP.md phase headers
(e.g. ### Phase 999.3: ...) before computing max + 1. This follows the
same pattern already used by cmdPhaseComplete (lines 791-834) which
scans ROADMAP.md as a fallback for phases defined but not yet
scaffolded to disk.
Additional hardening:
- Use escapeRegex() on normalized phase names in regex construction
- Support optional project-code prefix in directory pattern matching
- Handle edge cases: missing ROADMAP.md, empty/missing phases dir,
leading-zero padded phase numbers in ROADMAP.md
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(state): add programmatic gates for STATE.md consistency
Adds four enforcement gates to prevent STATE.md drift:
- `state validate`: detects drift between STATE.md and filesystem
- `state sync`: reconstructs STATE.md from actual project state
- `state planned-phase`: records state after plan-phase completes
- Performance Metrics update in `phase complete`
Also fixes ghost `state update-position` command reference in
execute-phase.md (command didn't exist in CLI dispatcher).
Closes#1627
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(state): By Phase table regex ate next section when table body was empty
The lazy [\s\S]*? with a $ lookahead in byPhaseTablePattern would
match past blank lines and capture the next ## section header as table
body when no data rows existed. Replaced with a precise row-matching
pattern ((?:[ \t]*\|[^\n]*\n)*) that only captures pipe-delimited
lines. Added regression assertion to verify row placement.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>