fix(phase-dir): apply project_code prefix in plan-milestone-gaps, import, and add-backlog workflows (PRED.k015) (#3306)

* test(phase-dir): add red test for k015 prefix-drift in plan-milestone-gaps and import workflows (#3298)

Asserts that:
- plan-milestone-gaps.md step 8 does not use bare {NN}-{name} mkdir pattern
- plan-milestone-gaps.md step 8 uses phase.add or expected_phase_dir
- import.md plan_convert does not use bare {NN}-{slug} mkdir pattern
- import.md plan_convert uses expected_phase_dir from init.phase-op

These 5 tests are RED until the fix lands.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(phase-dir): add projectCode prefix to phase-dir construction in plan-milestone-gaps and import workflows (#3298)

Both plan-milestone-gaps.md step 8 and import.md plan_convert step were
constructing phase directories using raw {NN}-{name}/{NN}-{slug} template
patterns, bypassing the project_code prefix from .planning/config.json.

Fix: both steps now call `gsd-sdk query init.phase-op <N>` and consume the
`expected_phase_dir` field (which includes the `<CODE>-<NN>-<slug>` prefix
when project_code is set), matching the pattern already established by
PR #3292 for /gsd-discuss-phase and /gsd-plan-phase.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(phase-dir): apply project_code prefix to backlog phase dir in add-backlog workflow (k015 sibling, #3298)

Sibling k015 audit found a third drift site: add-backlog.md step 4 was
constructing the 999.x backlog phase directory using raw ${NEXT}-${SLUG}
without applying the project_code prefix from .planning/config.json.

Fix: read project_code via `gsd-sdk query config-get project_code --raw`
and prepend `${CODE}-` when set, matching the pattern used by phase.insert
(which already applies project_code to decimal phases in phase.cjs line 736).

Also extends bug-3298 regression test to cover this third site.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(changeset): add changeset for #3298 phase-dir prefix drift fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(changeset): set pr: 3306 in changeset for #3298; resolve stash conflict in live-command-registry.cjs

The conflict was cosmetic (string concat → template literals) introduced by
accidental git stash during test verification. Taking the newer template-literal
form throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-09 11:47:15 -04:00
committed by GitHub
parent 04031244b8
commit 370033e907
6 changed files with 217 additions and 24 deletions

View File

@@ -46,16 +46,16 @@ let _cache = null;
* introducing a YAML parser dependency would be disproportionate.
*
* Supported name forms:
* name: gsd:slug -> slug = "slug"
* name: gsd-slug -> slug = "slug"
* name: "gsd:slug" -> slug = "slug" (quoted)
* name: "gsd-slug" -> slug = "slug" (quoted)
* name: gsd:slug → slug = "slug"
* name: gsd-slug → slug = "slug"
* name: "gsd:slug" → slug = "slug" (quoted)
* name: "gsd-slug" → slug = "slug" (quoted)
*/
function parseSlug(content, filePath) {
// Frontmatter must start with '---' on the very first line.
if (!content.startsWith('---')) {
throw new Error(
'[live-command-registry] ' + filePath + ': missing YAML frontmatter — file must start with \'---\''
`[live-command-registry] ${filePath}: missing YAML frontmatter — file must start with '---'`
);
}
@@ -63,7 +63,7 @@ function parseSlug(content, filePath) {
const closingIdx = content.indexOf('\n---', 3);
if (closingIdx < 0) {
throw new Error(
'[live-command-registry] ' + filePath + ': unclosed YAML frontmatter — no closing \'---\' found'
`[live-command-registry] ${filePath}: unclosed YAML frontmatter — no closing '---' found`
);
}
@@ -72,11 +72,11 @@ function parseSlug(content, filePath) {
// Match `name:` line, allowing optional quotes around the value.
// The value must be one of: gsd:<slug> or gsd-<slug>
// where slug = [a-z0-9][a-z0-9-]*
const nameMatch = frontmatter.match(/^name:\s*"?(gsd[:-])([a-z0-9][a-z0-9-]*)"?\s*$/m);
const nameMatch = frontmatter.match(/^name:\s*"?(gsd[:‑-])([a-z0-9][a-z0-9-]*)"?\s*$/m);
if (!nameMatch) {
throw new Error(
'[live-command-registry] ' + filePath + ': could not extract slug from frontmatter ' +
'(expected "name: gsd:<slug>" or "name: gsd-<slug>")'
`[live-command-registry] ${filePath}: could not extract slug from frontmatter ` +
`(expected "name: gsd:<slug>" or "name: gsd-<slug>")`
);
}
@@ -96,12 +96,12 @@ function getLiveCommandTokens() {
if (!fs.existsSync(COMMANDS_DIR)) {
throw new Error(
'[live-command-registry] commands directory not found: ' + COMMANDS_DIR
`[live-command-registry] commands directory not found: ${COMMANDS_DIR}`
);
}
const entries = fs.readdirSync(COMMANDS_DIR)
.filter(function(f) { return f.endsWith('.md'); })
.filter(f => f.endsWith('.md'))
.sort(); // deterministic order for reproducible error messages
const tokens = new Set();
@@ -113,16 +113,16 @@ function getLiveCommandTokens() {
content = fs.readFileSync(filePath, 'utf-8');
} catch (err) {
throw new Error(
'[live-command-registry] failed to read ' + filePath + ': ' + err.message
`[live-command-registry] failed to read ${filePath}: ${err.message}`
);
}
const slug = parseSlug(content, filePath);
// Emit all three canonical token forms per slug.
tokens.add('/gsd-' + slug); // Claude / non-Gemini
tokens.add('/gsd:' + slug); // Gemini
tokens.add('$gsd-' + slug); // Codex
tokens.add(`/gsd-${slug}`); // Claude / non-Gemini
tokens.add(`/gsd:${slug}`); // Gemini
tokens.add(`$gsd-${slug}`); // Codex
}
_cache = tokens;