Files
msd-core/.changeset
Tom Boucher 55e50cf392 fix(3600): count project-code-prefixed phase dirs in milestone filter
`init.new-milestone` reported `phase_dir_count: 0` for projects whose
phase directories carry a project_code prefix (`.planning/phases/CK-01-name`)
when the ROADMAP used numeric `### Phase N:` headings. Verified via a
temp-project repro that mirrors the reporter's setup.

Root cause: `getMilestonePhaseFilter` builds an `isDirInMilestone(dirName)`
predicate that tries two paths:

  1) Numeric — requires the dir name to START with a digit. `CK-01-name`
     starts with `C`, so this skips.
  2) Custom-ID — captures the leading kebab token (`CK-01-name` as a
     whole) and compares it to the normalised milestone phase IDs
     (`{"1"}`). No match.

There was no path that stripped the project_code prefix before retrying
the numeric match. Added a third path that strips the same shape
`normalizePhaseName` already recognises (`^[A-Z]{1,6}-(?=\d)`) and retries
the numeric match. This runs AFTER the custom-ID path so a ROADMAP that
uses `### Phase PROJ-42:` continues to win via the custom-ID match for
a `PROJ-42` directory; the new branch only fires when the milestone is
keyed on the bare numeric form.

The fix lands in both:

  - get-shit-done/bin/lib/core.cjs:isDirInMilestone (active CJS runtime)
  - sdk/src/query/state.ts:isDirInMilestone (SDK twin)

`getMilestonePhaseFilter` is shared by multiple callers — init.new-milestone,
phase complete, verify-work, validate-health — so the fix benefits every
caller that walks `.planning/phases/` against a numeric ROADMAP.

Regression test
(tests/bug-3600-milestone-phase-filter-project-code-prefix.test.cjs):

  1. Reporter's case: CK-01-name + CK-02-build dirs against Phase 1 / 2
     headings → phase_dir_count === 2.
  2. Existing contract: 01-first dir against Phase 1 heading still counts.
  3. Custom-ID contract: PROJ-42 dir against `### Phase PROJ-42:` still
     counts via the existing custom-ID match (no regression).
  4. Counter-test: CK-99-backlog and CK-100-future dirs MUST NOT count
     against a milestone with only Phase 1 — the strip-and-retry must
     still respect the milestone's actual phase set.

All assertions go through `init new-milestone --json` (typed payload —
`phase_dir_count`). No raw text matching.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:28:49 -04:00
..

Changeset Fragments

This directory holds per-PR CHANGELOG fragments. Every PR with user-facing changes drops one (or more) <random-name>.md files here describing its CHANGELOG entry. Fragments are consolidated into the top-level CHANGELOG.md at release time.

Why

Two PRs that both edit the ### Fixed block of CHANGELOG.md always conflict on merge — git can't pick a serialization order without human input. Two PRs that each add a fresh .changeset/<unique-name>.md never conflict because they don't share lines.

See #2975 for the full rationale.

Adding a fragment

node scripts/changeset/new.cjs \
  --type Fixed \
  --pr 1234 \
  --body "fix the thing — explain the user-visible change in one sentence"

This writes .changeset/<adjective>-<noun>-<noun>.md with frontmatter and a body. Three random words → concurrent PRs don't collide.

Format

---
type: Fixed
pr: 1234
---
**`/gsd-foo` no longer drops trailing slashes** — explain the user-visible change.

Allowed type: values follow Keep a Changelog: Added, Changed, Deprecated, Removed, Fixed, Security.

Opting out

PRs that legitimately have no user-facing impact can add the no-changelog label. CI honors it. When unsure, add the fragment.

At release time

node scripts/changeset/cli.cjs render --version vX.Y.Z --date YYYY-MM-DD

Reads every fragment, groups bullets by type:, replaces ## [Unreleased] with a new ## [vX.Y.Z] - YYYY-MM-DD block, opens a fresh ## [Unreleased] above, deletes consumed fragments. Idempotent.