@opengsd/gsd-core 1.19.0 → 1.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.compact.md +5 -5
  4. package/agents/gsd-code-fixer.md +9 -4
  5. package/agents/gsd-code-reviewer.compact.md +49 -8
  6. package/agents/gsd-code-reviewer.md +53 -14
  7. package/agents/gsd-executor.md +8 -4
  8. package/bin/install.js +36 -40
  9. package/commands/gsd/new-milestone.md +2 -2
  10. package/commands/gsd/new-project.md +2 -2
  11. package/commands/gsd/update.md +5 -2
  12. package/gsd-core/bin/gsd-tools.cjs +177 -93
  13. package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
  14. package/gsd-core/bin/lib/capability-registry.cjs +284 -93
  15. package/gsd-core/bin/lib/capability-validator.cjs +15 -0
  16. package/gsd-core/bin/lib/commands.cjs +46 -17
  17. package/gsd-core/bin/lib/config-loader.cjs +3 -3
  18. package/gsd-core/bin/lib/core-utils.cjs +11 -0
  19. package/gsd-core/bin/lib/frontmatter.cjs +21 -3
  20. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
  21. package/gsd-core/bin/lib/gate-context-drift.cjs +1 -1
  22. package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
  23. package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
  24. package/gsd-core/bin/lib/git-info-exclude.cjs +1 -6
  25. package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
  26. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +13 -9
  27. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
  28. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
  29. package/gsd-core/bin/lib/init.cjs +181 -68
  30. package/gsd-core/bin/lib/install-engine.cjs +9 -9
  31. package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
  32. package/gsd-core/bin/lib/install-profiles.cjs +4 -2
  33. package/gsd-core/bin/lib/io.cjs +6 -0
  34. package/gsd-core/bin/lib/markdown-sectionizer.cjs +34 -1
  35. package/gsd-core/bin/lib/markdown-table.cjs +23 -0
  36. package/gsd-core/bin/lib/mcp-server.cjs +8 -5
  37. package/gsd-core/bin/lib/milestone.cjs +18 -50
  38. package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
  39. package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
  40. package/gsd-core/bin/lib/phase-id.cjs +627 -36
  41. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -74
  42. package/gsd-core/bin/lib/phase-locator.cjs +328 -19
  43. package/gsd-core/bin/lib/phase.cjs +3039 -666
  44. package/gsd-core/bin/lib/planning-document.cjs +26 -5
  45. package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
  46. package/gsd-core/bin/lib/planning-snapshot.cjs +12 -7
  47. package/gsd-core/bin/lib/planning-workspace.cjs +3 -3
  48. package/gsd-core/bin/lib/read-only-git-env.cjs +23 -2
  49. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
  50. package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -0
  51. package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
  52. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
  53. package/gsd-core/bin/lib/roadmap-parser.cjs +759 -144
  54. package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
  55. package/gsd-core/bin/lib/roadmap-upgrade.cjs +8 -5
  56. package/gsd-core/bin/lib/roadmap.cjs +358 -106
  57. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -130
  58. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
  59. package/gsd-core/bin/lib/shell-command-projection.cjs +36 -9
  60. package/gsd-core/bin/lib/smart-entry.cjs +22 -11
  61. package/gsd-core/bin/lib/state-contract.cjs +11 -7
  62. package/gsd-core/bin/lib/state-document.cjs +332 -23
  63. package/gsd-core/bin/lib/state-transition.cjs +51 -15
  64. package/gsd-core/bin/lib/state.cjs +499 -180
  65. package/gsd-core/bin/lib/template.cjs +2 -2
  66. package/gsd-core/bin/lib/validate.cjs +3 -2
  67. package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
  68. package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
  69. package/gsd-core/bin/lib/verify.cjs +2 -2
  70. package/gsd-core/bin/lib/workstream-inventory.cjs +27 -21
  71. package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
  72. package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
  73. package/gsd-core/references/cicd-review-patterns.md +45 -0
  74. package/gsd-core/references/necessity-review.md +48 -0
  75. package/gsd-core/workflows/code-review-fix.md +46 -5
  76. package/gsd-core/workflows/code-review.md +3 -0
  77. package/gsd-core/workflows/execute-phase.md +1 -1
  78. package/gsd-core/workflows/execute-plan.md +1 -1
  79. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
  80. package/gsd-core/workflows/plan-phase.md +1 -1
  81. package/gsd-core/workflows/pr-branch.md +2 -0
  82. package/gsd-core/workflows/reapply-patches.md +104 -0
  83. package/gsd-core/workflows/review.md +54 -141
  84. package/gsd-core/workflows/secure-phase.md +1 -1
  85. package/gsd-core/workflows/settings-advanced.md +57 -9
  86. package/gsd-core/workflows/ship.md +12 -0
  87. package/hooks/dist/gsd-agent-isolation-guard.js +1 -1
  88. package/hooks/dist/gsd-cursor-subagent-start.js +1 -1
  89. package/hooks/dist/gsd-graphify-update.sh +6 -0
  90. package/hooks/dist/gsd-statusline.js +3 -2
  91. package/hooks/dist/gsd-windsurf-pre-write.js +4 -4
  92. package/hooks/dist/gsd-workflow-guard.js +2 -1
  93. package/hooks/dist/gsd-worktree-path-guard.js +4 -4
  94. package/hooks/dist/lib/git-probe.js +16 -0
  95. package/hooks/dist/lib/isolation-sentinel.js +77 -2
  96. package/hooks/gsd-agent-isolation-guard.js +1 -1
  97. package/hooks/gsd-cursor-subagent-start.js +1 -1
  98. package/hooks/gsd-graphify-update.sh +6 -0
  99. package/hooks/gsd-statusline.js +3 -2
  100. package/hooks/gsd-windsurf-pre-write.js +4 -4
  101. package/hooks/gsd-workflow-guard.js +2 -1
  102. package/hooks/gsd-worktree-path-guard.js +4 -4
  103. package/hooks/lib/git-probe.js +16 -0
  104. package/hooks/lib/isolation-sentinel.js +77 -2
  105. package/package.json +3 -3
  106. package/pi/package-agents/gsd-code-fixer.md +9 -4
  107. package/pi/package-agents/gsd-code-reviewer.md +53 -14
  108. package/pi/package-agents/gsd-executor.md +8 -4
  109. package/pi/package-agents/manifest.json +3 -3
  110. package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
  111. package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
  112. package/pi/package-skills/gsd-update/SKILL.md +4 -1
  113. package/scripts/changeset/lint.cjs +28 -3
  114. package/scripts/ci-next-health.cjs +246 -23
  115. package/scripts/docs-guard-registry.cjs +2 -0
  116. package/scripts/gen-features.cjs +14 -2
  117. package/scripts/lib/drift-scan.cjs +27 -0
  118. package/scripts/lib/macos-conformance-tier.generated.cjs +10 -0
  119. package/scripts/lib/platform-conformance-tier.generated.cjs +15 -0
  120. package/scripts/lint-phase-enumeration-drift.cjs +8 -4
  121. package/scripts/lint-roadmap-progress-drift.cjs +421 -0
  122. package/scripts/merge-commit-pr.cjs +186 -0
  123. package/scripts/mutation-matrix.cjs +23 -1
  124. package/scripts/release-ancestry.cjs +86 -0
  125. package/scripts/release-pipeline-pr.cjs +92 -0
  126. package/scripts/require-issue-link-policy.cjs +18 -9
  127. package/scripts/sync-next-version.cjs +48 -7
  128. package/skills/gsd-new-milestone/SKILL.md +1 -1
  129. package/skills/gsd-new-project/SKILL.md +1 -1
  130. package/skills/gsd-update/SKILL.md +5 -2
  131. package/vscode/package.json +1 -1
@@ -104,7 +104,7 @@ const planningScopeMod = require("../planning-scope.cjs");
104
104
  const { SCOPE } = planningScopeMod;
105
105
  // eslint-disable-next-line @typescript-eslint/no-require-imports
106
106
  const phaseIdMod = require("../phase-id.cjs");
107
- const { matchPhaseDirs, normalizePhaseName, extractPhaseToken, isSentinelPhaseId, isSentinelPhaseDir } = phaseIdMod;
107
+ const { matchPhaseDirs, phaseLookupQuery, extractPhaseToken, isSentinelPhaseId, isSentinelPhaseDir } = phaseIdMod;
108
108
  // eslint-disable-next-line @typescript-eslint/no-require-imports
109
109
  const validateMod = require("../validate.cjs");
110
110
  const { phaseVariants } = validateMod;
@@ -116,7 +116,7 @@ const { phaseVariants } = validateMod;
116
116
  * does the roadmap claim, for W007) call this — one matcher, reused, per the
117
117
  * file-level comment.
118
118
  */
119
- function dirsForPhase(dirs, phaseId, convention) {
119
+ function dirsForPhase(dirs, phaseId, convention, directoryContext) {
120
120
  // #612: `convention` reaches BOTH operands of this pairing or it pairs
121
121
  // nothing. The phases come from a bracket-aware heading scan and the dirs
122
122
  // from a bracket-aware disk scan, so a convention-less selector here leaves
@@ -125,7 +125,7 @@ function dirsForPhase(dirs, phaseId, convention) {
125
125
  // disk" AND every bracket directory reads as unclaimed. `matchPhaseDirs`
126
126
  // forwards it to its primary `phaseTokenMatches`; handed nothing it is
127
127
  // byte-identical, so no legacy repo's pairing changes.
128
- const canonical = matchPhaseDirs(dirs, normalizePhaseName(phaseId), convention).matches;
128
+ const canonical = matchPhaseDirs(dirs, phaseLookupQuery(phaseId, directoryContext), convention, undefined, directoryContext).matches;
129
129
  if (canonical.length > 0)
130
130
  return canonical;
131
131
  // Bug 2 (#3309 W006/W007 migration cluster, found while fixing the
@@ -187,6 +187,7 @@ function checkW006(snapshot) {
187
187
  return [];
188
188
  const dirs = snapshot.allPhaseDirNames.value;
189
189
  const convention = snapshot.phaseIdConvention;
190
+ const directoryContext = snapshot.directoryContext ?? phaseIdMod.phaseDirectoryContext(snapshot.config.value ?? {}, '', snapshot.roadmapDeclaredPhases.value.map(({ phaseId }) => phaseId));
190
191
  const archivedTokens = new Set(snapshot.archivedPhaseTokens.value);
191
192
  // No separate scope guard: `roadmapSentinelPhaseTokens` and
192
193
  // `roadmapDeclaredPhases` are produced by ONE builder call
@@ -219,7 +220,7 @@ function checkW006(snapshot) {
219
220
  // not change its result.
220
221
  if (sentinelTokens.has(phaseId) || isSentinelPhaseId(phaseId))
221
222
  continue;
222
- if (dirsForPhase(dirs, phaseId, convention).length > 0)
223
+ if (dirsForPhase(dirs, phaseId, convention, directoryContext).length > 0)
223
224
  continue;
224
225
  // Bug 1 (#3309 W006/W007 migration cluster): a phase whose ONLY
225
226
  // directory lives under a milestone archive
@@ -247,10 +248,10 @@ function checkW006(snapshot) {
247
248
  // ─── W007 — an on-disk phase directory has no matching ROADMAP entry ──────
248
249
  // (verify.cts:2086-2101)
249
250
  /** Every directory in `dirs` that ANY declared roadmap phase resolves to. */
250
- function computeClaimedDirs(dirs, declaredPhases, convention) {
251
+ function computeClaimedDirs(dirs, declaredPhases, convention, directoryContext) {
251
252
  const claimed = new Set();
252
253
  for (const { phaseId } of declaredPhases) {
253
- for (const dir of dirsForPhase(dirs, phaseId, convention))
254
+ for (const dir of dirsForPhase(dirs, phaseId, convention, directoryContext))
254
255
  claimed.add(dir);
255
256
  }
256
257
  return claimed;
@@ -261,7 +262,8 @@ function checkW007(snapshot) {
261
262
  return [];
262
263
  const dirs = snapshot.allPhaseDirNames.value;
263
264
  const convention = snapshot.phaseIdConvention;
264
- const claimedDirs = computeClaimedDirs(dirs, snapshot.roadmapDeclaredPhases.value, convention);
265
+ const directoryContext = snapshot.directoryContext ?? phaseIdMod.phaseDirectoryContext(snapshot.config.value ?? {}, '', snapshot.roadmapDeclaredPhases.value.map(({ phaseId }) => phaseId));
266
+ const claimedDirs = computeClaimedDirs(dirs, snapshot.roadmapDeclaredPhases.value, convention, directoryContext);
265
267
  const diagnostics = [];
266
268
  for (const dirName of dirs) {
267
269
  // `extractPhaseToken` is the phase-id.cts owner `PHASE_TOKEN_FROM_DIR_RE`
@@ -46,7 +46,7 @@ const healthDiagnosticMod = require("../health-diagnostic-types.cjs");
46
46
  const { SEVERITY, adviseRemedy } = healthDiagnosticMod;
47
47
  // eslint-disable-next-line @typescript-eslint/no-require-imports
48
48
  const phaseIdMod = require("../phase-id.cjs");
49
- const { getMilestoneFromPhaseId, matchPhaseDirs, normalizePhaseName, extractPhaseToken, PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
49
+ const { getMilestoneFromPhaseId, matchPhaseDirs, phaseLookupQuery, extractPhaseToken, PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
50
50
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- the single owner of the persisted status vocabulary
51
51
  const stateDocumentMod = require("../state-document.cjs");
52
52
  const { normalizeStateStatus } = stateDocumentMod;
@@ -332,8 +332,8 @@ const RULE_W026 = {
332
332
  // field's own doc comment on `PlanningSnapshot` for why
333
333
  // `roadmapDeclaredPhases`'s `milestone` attribution is the wrong fit).
334
334
  const unstarted = [];
335
+ const directoryContext = snapshot.directoryContext ?? phaseIdMod.phaseDirectoryContext(snapshot.config.value ?? {}, '', snapshot.currentMilestoneRoadmapPhaseIds.value);
335
336
  for (const phaseId of snapshot.currentMilestoneRoadmapPhaseIds.value) {
336
- const normalized = normalizePhaseName(phaseId);
337
337
  // `allPhaseDirNames` — every directory under `phases/`, UNWINDOWED by
338
338
  // ROADMAP-declaration membership — mirrors the original's own
339
339
  // unwindowed `phaseDirNames2` (`verify.cts:2372-2382`, a direct
@@ -346,7 +346,7 @@ const RULE_W026 = {
346
346
  // moved the read onto the shared selector; the convention rides along,
347
347
  // since the selector's primary match IS the `phaseTokenMatches` call this
348
348
  // line used to make directly.
349
- const hasDirectory = matchPhaseDirs(snapshot.allPhaseDirNames.value, normalized, snapshot.phaseIdConvention).matches.length > 0;
349
+ const hasDirectory = matchPhaseDirs(snapshot.allPhaseDirNames.value, phaseLookupQuery(phaseId, directoryContext), snapshot.phaseIdConvention, undefined, directoryContext).matches.length > 0;
350
350
  if (!hasDirectory)
351
351
  unstarted.push(phaseId);
352
352
  }
@@ -15,6 +15,7 @@ const node_os_1 = __importDefault(require("node:os"));
15
15
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
16
16
  const clock_cjs_1 = require("./clock.cjs");
17
17
  const pattern_cjs_1 = require("./pattern.cjs");
18
+ const phase_entry_grammar_cjs_1 = require("./phase-entry-grammar.cjs");
18
19
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
19
20
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
20
21
  const io = require("./io.cjs");
@@ -33,6 +34,8 @@ const roadmapParser = require("./roadmap-parser.cjs");
33
34
  const coreUtils = require("./core-utils.cjs");
34
35
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
35
36
  const phaseId = require("./phase-id.cjs");
37
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id-display.cjs is an export= CommonJS module
38
+ const phaseIdDisplayMod = require("./phase-id-display.cjs");
36
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- worktree-safety.cjs is an export= CommonJS module
37
40
  const worktreeSafety = require("./worktree-safety.cjs");
38
41
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
@@ -93,10 +96,11 @@ const { harvestPriorVerifyCommands } = verifyCommandGrounding;
93
96
  const { output, error, ERROR_REASON, formatDiagnosticToken } = io;
94
97
  const { loadConfig, loadConfigResolved } = configLoader;
95
98
  const { resolveModelInternal, resolveGranularityInternal, assertValidGranularityOverride } = modelResolver;
96
- const { findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocator;
99
+ const { phaseDirectoryOptions, findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs, } = phaseLocator;
97
100
  const { getRoadmapPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
98
101
  const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
99
- const { comparePhaseNum, normalizePhaseName, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, PHASE_DEP_REF_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName, parsePhaseId, renderPhaseId, phaseHeadingPrefixSrcFor, PHASE_HEADING_BASELINE, buildPhaseHeadingScanRegex, } = phaseId;
102
+ const { comparePhaseNum, phaseDirectoryName, customPhaseQuery, customDirectoryOwner, phaseScanTokenSource, phaseNumberTokenSource, phaseKeyFromToken, phaseDirectoryContext, normalizePhaseName, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, PHASE_DEP_REF_SOURCE, extractPhaseDependencyTokens, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName, parsePhaseId, renderPhaseId, bracketQualifiedKey, phaseHeadingPrefixSrcFor, parsePhaseChecklistLine, PHASE_HEADING_BASELINE, buildPhaseHeadingScanRegex, } = phaseId;
103
+ const { phaseToken } = phaseIdDisplayMod;
100
104
  const { pruneOrphanedWorktrees } = worktreeSafety;
101
105
  const { planningPaths, planningDir, planningRoot, todosDir, listAvailableWorkstreams, peekActiveWorkstream, resolveEnvWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, findPhaseSpecIn, resolvePhaseIdConvention, worktreesOptedOut, } = planningWorkspace;
102
106
  const { extractFrontmatter, frontmatterBlock } = frontmatterMod;
@@ -134,8 +138,8 @@ function roadmapPhaseMatchesExactPrefix(roadmapPhase, phase) {
134
138
  // #2104: shared helpers that wrap findPhaseInternal / getRoadmapPhaseInternal
135
139
  // with the #2056 foreign-prefix guard, so every init command gets the same
136
140
  // protection without duplicating the guard logic at each call site.
137
- function guardedFindPhase(cwd, phase, projectCode) {
138
- let phaseInfo = findPhaseInternal(cwd, phase);
141
+ function guardedFindPhase(cwd, phase, projectCode, phaseNaming) {
142
+ let phaseInfo = findPhaseInternal(cwd, phase, undefined, phaseDirectoryOptions(cwd, { phase_naming: phaseNaming, project_code: projectCode }));
139
143
  if (isForeignPrefixedPhaseQuery(phase, projectCode) && !phaseInfoMatchesExactPrefix(phaseInfo, phase)) {
140
144
  phaseInfo = null;
141
145
  }
@@ -902,7 +906,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
902
906
  error('phase required for init execute-phase');
903
907
  }
904
908
  const config = loadConfig(cwd);
905
- let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
909
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
906
910
  // #3216: getMilestoneInfo now returns a ScopedResult — `.value` carries the
907
911
  // MilestoneInfo (or null on any non-COMPLETE scope). NOT display-only: when
908
912
  // `branching_strategy === 'milestone'`, `milestone['version']`/`['name']`
@@ -1077,7 +1081,7 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
1077
1081
  }
1078
1082
  const config = loadConfig(cwd);
1079
1083
  // #2056/#2104: foreign-prefixed queries must not collapse to numeric phases.
1080
- let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
1084
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
1081
1085
  const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
1082
1086
  phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
1083
1087
  const phaseName = rp['phase_name'];
@@ -1111,12 +1115,14 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
1111
1115
  const phaseNamePlan = resolvePhaseDisplayName(phaseInfo, roadmapPhase);
1112
1116
  const rawProjectCodePlan = config.project_code || '';
1113
1117
  let expectedPhaseDirPlan = null;
1114
- if (!phaseDirPlan && phaseNumberPlan && phaseNamePlan) {
1118
+ const namingContextPlan = phaseDirectoryContext(config);
1119
+ const customNamingPlan = customPhaseQuery(phase, namingContextPlan) !== null;
1120
+ if ((!phaseDirPlan || customNamingPlan) && phaseNumberPlan && phaseNamePlan) {
1115
1121
  const paddedNum = normalizePhaseName(phaseNumberPlan);
1116
- const slug = (generateSlugInternal(phaseNamePlan) || '').substring(0, 60);
1122
+ const expectedName = roadmapPhase?.['phase_name'] || phaseNamePlan;
1123
+ const slug = (generateSlugInternal(expectedName) || '').substring(0, 60);
1117
1124
  if (slug) {
1118
- const prefix = rawProjectCodePlan ? `${rawProjectCodePlan}-` : '';
1119
- const dirName = `${prefix}${paddedNum}-${slug}`;
1125
+ const dirName = phaseDirectoryName(customPhaseQuery(phaseNumberPlan, namingContextPlan) ? phaseNumberPlan : paddedNum, slug, rawProjectCodePlan);
1120
1126
  // #2376: absolute — see comment on phase_dir below.
1121
1127
  expectedPhaseDirPlan = toPosixPath(node_path_1.default.join(planningPaths(cwd).phases, dirName));
1122
1128
  }
@@ -1707,7 +1713,7 @@ function cmdInitVerifyWork(cwd, phase, raw) {
1707
1713
  }
1708
1714
  const config = loadConfig(cwd);
1709
1715
  const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
1710
- let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
1716
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
1711
1717
  const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
1712
1718
  phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
1713
1719
  const phaseName = rp['phase_name'];
@@ -1814,7 +1820,7 @@ function cmdInitVerifyWork(cwd, phase, raw) {
1814
1820
  */
1815
1821
  function cmdInitCodeReview(cwd, phase, raw, options = {}) {
1816
1822
  const config = loadConfig(cwd);
1817
- let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
1823
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
1818
1824
  const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
1819
1825
  phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
1820
1826
  const rpName = rp['phase_name'];
@@ -1880,7 +1886,7 @@ function cmdInitCodeReview(cwd, phase, raw, options = {}) {
1880
1886
  */
1881
1887
  function cmdInitReview(cwd, phase, raw, options = {}) {
1882
1888
  const config = loadConfig(cwd);
1883
- let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
1889
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
1884
1890
  const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
1885
1891
  phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => ({
1886
1892
  found: true,
@@ -1939,7 +1945,7 @@ function cmdInitReview(cwd, phase, raw, options = {}) {
1939
1945
  */
1940
1946
  function cmdInitDiscussPhaseAssumptions(cwd, phase, raw, options = {}) {
1941
1947
  const config = loadConfig(cwd);
1942
- let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
1948
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
1943
1949
  const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
1944
1950
  phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
1945
1951
  const rpName = rp['phase_name'];
@@ -1995,7 +2001,7 @@ function cmdInitDiscussPhaseAssumptions(cwd, phase, raw, options = {}) {
1995
2001
  }
1996
2002
  function cmdInitPhaseOp(cwd, phase, raw) {
1997
2003
  const config = loadConfig(cwd);
1998
- let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
2004
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
1999
2005
  // #2237: surface ambiguous phase-directory collisions instead of silently
2000
2006
  // taking the first match when unrelated projects share a .planning/phases/ tree.
2001
2007
  if (phaseInfo?.['ambiguous_matches']) {
@@ -2598,37 +2604,37 @@ function cmdInitManager(cwd, raw) {
2598
2604
  }
2599
2605
  const rawContent = node_fs_1.default.readFileSync(paths.roadmap, 'utf-8');
2600
2606
  const content = extractCurrentMilestone(rawContent, cwd);
2607
+ const directoryContext = phaseDirectoryOptions(cwd, config, rawContent);
2601
2608
  // #3185 (ADR-3180 Decision 1): "which phase directories belong to the
2602
2609
  // CURRENT milestone" is the scoped question listMilestonePhaseDirs owns —
2603
2610
  // routed through it instead of a hand-rolled readdirSync + a separate
2604
2611
  // getMilestonePhaseFilter window check (which also never excluded
2605
2612
  // sentinels, unlike the owner).
2606
2613
  const _checkboxStates = new Map();
2607
- // #4982: anchored (`^`, `m`) so the phase label must be the FIRST thing
2608
- // after the checkbox (tolerating only an optional `**` bold marker) —
2609
- // the prior unanchored `\s*.*` let the greedy `.*` bind to the LAST
2610
- // "Phase N" mentioned anywhere in the line's prose instead of the line's
2611
- // own phase. First-match-wins (`if (!_checkboxStates.has(...))` below)
2614
+ // #4982: parsePhaseChecklistLine is line-anchored, so the phase label must
2615
+ // be the FIRST thing after the checkbox (tolerating only an optional `**`
2616
+ // bold marker), and a later "Phase N" in the line's prose cannot bind the
2617
+ // checkbox. First-match-wins (`if (!_checkboxStates.has(...))` below)
2612
2618
  // additionally guards against a later anchored line (e.g. a per-plan
2613
2619
  // sub-entry self-titled "Phase N ...") overwriting an already-recorded
2614
- // phase's own (first) checkbox — anchoring alone does not fully close
2615
- // that "last-match-wins" composition defect.
2616
- const _cbPattern = new RegExp(`^[ \\t]*-[ \\t]*\\[(x| )\\][ \\t]*(?:\\*\\*)?${phaseHeadingPrefix}(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gim');
2617
- let _cbMatch;
2618
- while ((_cbMatch = _cbPattern.exec(content)) !== null) {
2619
- const phaseGroup = capturesBracketId ? 3 : 2;
2620
- const _cbKey = _cbMatch[phaseGroup];
2621
- if (!_checkboxStates.has(_cbKey)) {
2622
- _checkboxStates.set(_cbKey, _cbMatch[1].toLowerCase() === 'x');
2620
+ // phase's own (first) checkbox; anchoring alone does not fully close that
2621
+ // "last-match-wins" composition defect.
2622
+ for (const line of content.split(/\r?\n/)) {
2623
+ const checkbox = parsePhaseChecklistLine(line, phaseIdConvention, directoryContext);
2624
+ if (checkbox && !_checkboxStates.has(checkbox.phaseToken)) {
2625
+ _checkboxStates.set(checkbox.phaseToken, checkbox.checked);
2623
2626
  }
2624
2627
  }
2625
2628
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
2626
- const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefix}(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
2629
+ const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefix}(${phaseScanTokenSource(directoryContext, phaseIdConvention)})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
2627
2630
  const phases = [];
2631
+ const bracketIdsByPhaseNumber = new Map();
2628
2632
  let match;
2629
2633
  while ((match = phasePattern.exec(content)) !== null) {
2630
2634
  const bracketId = capturesBracketId ? match[1] : undefined;
2631
2635
  const phaseNum = capturesBracketId ? match[2] : match[1];
2636
+ if (bracketId)
2637
+ bracketIdsByPhaseNumber.set(phaseNum, bracketId);
2632
2638
  const phaseName = (capturesBracketId ? match[3] : match[2])
2633
2639
  .replace(/\(INSERTED\)/i, '')
2634
2640
  .trim();
@@ -2644,7 +2650,7 @@ function cmdInitManager(cwd, raw) {
2644
2650
  }
2645
2651
  const sectionStart = match.index;
2646
2652
  const restOfContent = content.slice(sectionStart);
2647
- const nextHeader = restOfContent.match(new RegExp(`\\n#{2,4}\\s+${phaseHeadingPrefixNoCapture}\\d[\\d.]*`, 'i'));
2653
+ const nextHeader = restOfContent.match(new RegExp(`\\n#{2,4}\\s+${phaseHeadingPrefixNoCapture}${phaseScanTokenSource(directoryContext, phaseIdConvention)}`, 'i'));
2648
2654
  const sectionEnd = nextHeader
2649
2655
  ? sectionStart + nextHeader.index
2650
2656
  : content.length;
@@ -2674,7 +2680,7 @@ function cmdInitManager(cwd, raw) {
2674
2680
  // of reporting no_directory/phase_complete:false. Five other init
2675
2681
  // commands already route through this same primitive; this was the
2676
2682
  // holdout private copy (#4793-family declared-owner drift).
2677
- const located = findPhaseInternal(cwd, phaseNum, phaseIdConvention);
2683
+ const located = findPhaseInternal(cwd, phaseNum, phaseIdConvention, directoryContext);
2678
2684
  const dirMatch = located && located['found']
2679
2685
  ? node_path_1.default.posix.basename(String(located['directory']))
2680
2686
  : null;
@@ -2785,15 +2791,83 @@ function cmdInitManager(cwd, raw) {
2785
2791
  .filter((p) => p['phase_complete'] === true)
2786
2792
  .map((p) => normalizePhaseNumber(p['number'])));
2787
2793
  const phaseMap = new Map(phases.map((p) => [normalizePhaseNumber(p['number']), p]));
2788
- const _allCompletedPattern = new RegExp(`-\\s*\\[x\\]\\s*.*${phaseHeadingPrefixNoCapture}(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
2789
- let _allMatch;
2790
- while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) {
2791
- const phaseNum = normalizePhaseNumber(_allMatch[1]);
2792
- const phase = phaseMap.get(phaseNum);
2793
- if (!phase || phase['phase_complete'] === true) {
2794
- completedNums.add(phaseNum);
2794
+ const bracketIdentityKey = (value) => {
2795
+ try {
2796
+ const id = parsePhaseId(value);
2797
+ const subphase = id.subphase ? `.${id.subphase}` : '';
2798
+ return bracketQualifiedKey(`${id.project}.${id.milestone}-${id.phase}${subphase}`, 'bracket');
2799
+ }
2800
+ catch {
2801
+ return null;
2802
+ }
2803
+ };
2804
+ const phaseByBracketIdentity = new Map();
2805
+ const bracketIdentityByPhaseNumber = new Map();
2806
+ const completedBracketIdentities = new Set();
2807
+ if (phaseIdConvention === 'bracket') {
2808
+ for (const phase of phases) {
2809
+ const phaseNumber = phase['number'];
2810
+ const bracketId = bracketIdsByPhaseNumber.get(phaseNumber);
2811
+ const canonicalPhaseToken = phaseToken(phaseNumber);
2812
+ // #4304: key each heading on the reader's canonical
2813
+ // token, not on a strict parse of its rendered display. An accepted
2814
+ // unpadded heading (`### [CK.02] 1:`) gets no display_id, because
2815
+ // parsePhaseId rejects `CK.02-1`, so it had no identity at all and a
2816
+ // checked `[CK.02] 01` row completed it while phase_complete was false.
2817
+ // phaseToken pads the number to `01`, and `CK.02-01` is the form
2818
+ // parsePhaseId accepts; canonical headings key exactly as before.
2819
+ const key = bracketId && canonicalPhaseToken
2820
+ ? bracketIdentityKey(`${bracketId}-${canonicalPhaseToken}`)
2821
+ : null;
2822
+ if (!key)
2823
+ continue;
2824
+ bracketIdentityByPhaseNumber.set(normalizePhaseNumber(phaseNumber), key);
2825
+ phaseByBracketIdentity.set(key, phase);
2826
+ if (phase['phase_complete'] === true)
2827
+ completedBracketIdentities.add(key);
2828
+ }
2829
+ }
2830
+ if (phaseIdConvention === 'bracket') {
2831
+ // A checked historical row satisfies only its own qualified identity. In
2832
+ // particular, `[CK.01] 01` must not complete bare/current `[CK.02] 01`.
2833
+ for (const line of rawContent.split(/\r?\n/)) {
2834
+ const checkbox = parsePhaseChecklistLine(line, phaseIdConvention);
2835
+ if (!checkbox?.checked || !checkbox.bracketId)
2836
+ continue;
2837
+ const identity = `[${checkbox.bracketId}] ${checkbox.phaseToken}`;
2838
+ const key = bracketIdentityKey(identity);
2839
+ if (!key)
2840
+ continue;
2841
+ const current = phaseMap.get(normalizePhaseNumber(checkbox.phaseToken));
2842
+ const currentKey = current
2843
+ ? bracketIdentityByPhaseNumber.get(normalizePhaseNumber(current['number'])) ?? null
2844
+ : null;
2845
+ if (!current || currentKey !== key || current['phase_complete'] === true) {
2846
+ completedBracketIdentities.add(key);
2847
+ }
2795
2848
  }
2796
2849
  }
2850
+ else {
2851
+ // Preserve the historical greedy non-bracket fallback byte-for-byte.
2852
+ const _allCompletedPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`.*${phaseHeadingPrefixNoCapture}(${phaseNumberTokenSource(phaseIdConvention)})[:\\s]`, 'gi');
2853
+ let _allMatch;
2854
+ while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) {
2855
+ if (!(0, phase_entry_grammar_cjs_1.isPhaseEntryChecked)(_allMatch[0]))
2856
+ continue;
2857
+ const phaseNum = normalizePhaseNumber(_allMatch[1]);
2858
+ const phase = phaseMap.get(phaseNum);
2859
+ if (!phase || phase['phase_complete'] === true) {
2860
+ completedNums.add(phaseNum);
2861
+ }
2862
+ }
2863
+ }
2864
+ const currentDependencyNumber = (dependency) => {
2865
+ const identity = bracketIdentityKey(dependency);
2866
+ if (!identity)
2867
+ return normalizePhaseNumber(dependency);
2868
+ const current = phaseByBracketIdentity.get(identity);
2869
+ return current ? normalizePhaseNumber(current['number']) : null;
2870
+ };
2797
2871
  function reaches(from, to, visited = new Set()) {
2798
2872
  const normalizedFrom = normalizePhaseNumber(from);
2799
2873
  const normalizedTo = normalizePhaseNumber(to);
@@ -2803,15 +2877,18 @@ function cmdInitManager(cwd, raw) {
2803
2877
  const p = phaseMap.get(normalizedFrom);
2804
2878
  if (!p || !p['dep_phases'] || p['dep_phases'].length === 0)
2805
2879
  return false;
2806
- if (p['dep_phases'].some((dep) => normalizePhaseNumber(dep) === normalizedTo)) {
2880
+ if (p['dep_phases'].some((dep) => currentDependencyNumber(dep) === normalizedTo)) {
2807
2881
  return true;
2808
2882
  }
2809
- return p['dep_phases'].some((dep) => reaches(dep, to, visited));
2883
+ return p['dep_phases'].some((dep) => {
2884
+ const dependencyNumber = currentDependencyNumber(dep);
2885
+ return dependencyNumber === null ? false : reaches(dependencyNumber, to, visited);
2886
+ });
2810
2887
  }
2811
2888
  function hasDepRelationship(numA, numB) {
2812
2889
  return reaches(numA, numB) || reaches(numB, numA);
2813
2890
  }
2814
- // #4764: a phase reference in depends_on prose is a PHASE-SHAPED token in
2891
+ // #4764/#4304: a phase reference in depends_on prose is a PHASE-SHAPED token in
2815
2892
  // context — directly following "Phase"/"Phases" — never a bare digit run.
2816
2893
  // The previous whole-field scrape matched the token grammar against every
2817
2894
  // digit run, so calendar dates ("2026-09-14" → 2026, 09, 14), git shas
@@ -2824,9 +2901,27 @@ function cmdInitManager(cwd, raw) {
2824
2901
  // "Phase 1-3") — silently dropping a REAL dependency would clear
2825
2902
  // deps_satisfied prematurely, the dangerous direction. Negation prose
2826
2903
  // ("dropped the dependency on Phase 654") is NOT detected: the issue's own
2827
- // minimum keeps such tokens.
2828
- const depPhaseRefRe = new RegExp(`${PHASE_DEP_REF_SOURCE}`, 'gi');
2829
- const depTokenRe = new RegExp(`${PHASE_NUMBER_TOKEN_SOURCE}`, 'gi');
2904
+ // minimum keeps such tokens. Bracket repositories route through the same
2905
+ // phase-id owner so `[CK.02] 01` retains its complete qualified identity;
2906
+ // bare `01` still resolves against the active milestone. This prevents a
2907
+ // checked historical `[CK.01] 01` from satisfying incomplete `[CK.02] 01`.
2908
+ // Non-bracket extraction remains this manager surface's exact #4764 code
2909
+ // path below, including its case-insensitive token regex. Planning-inspect's
2910
+ // old token regex was case-sensitive, so forcing both through one widened
2911
+ // helper changed its `Phase 1a` bytes from `1` to `1a`.
2912
+ const legacyManagerDependencyTokens = (prose) => {
2913
+ const depPhaseRefRe = new RegExp(`${PHASE_DEP_REF_SOURCE}`, 'gi');
2914
+ const depTokenRe = new RegExp(`${PHASE_NUMBER_TOKEN_SOURCE}`, 'gi');
2915
+ const tokens = [];
2916
+ let refMatch;
2917
+ while ((refMatch = depPhaseRefRe.exec(prose)) !== null) {
2918
+ let tokenMatch;
2919
+ depTokenRe.lastIndex = 0;
2920
+ while ((tokenMatch = depTokenRe.exec(refMatch[1])) !== null)
2921
+ tokens.push(tokenMatch[0]);
2922
+ }
2923
+ return tokens;
2924
+ };
2830
2925
  for (const phase of phases) {
2831
2926
  if (!phase['depends_on'] ||
2832
2927
  /^none$/i.test(phase['depends_on'].trim())) {
@@ -2835,24 +2930,39 @@ function cmdInitManager(cwd, raw) {
2835
2930
  else {
2836
2931
  const prose = phase['depends_on'];
2837
2932
  const ownNumber = normalizePhaseNumber(phase['number']);
2933
+ const ownIdentity = typeof phase['display_id'] === 'string'
2934
+ ? bracketIdentityKey(phase['display_id'])
2935
+ : null;
2838
2936
  const depNums = [];
2839
2937
  const seen = new Set();
2840
- let refMatch;
2841
- depPhaseRefRe.lastIndex = 0;
2842
- while ((refMatch = depPhaseRefRe.exec(prose)) !== null) {
2843
- let tok;
2844
- depTokenRe.lastIndex = 0;
2845
- while ((tok = depTokenRe.exec(refMatch[1])) !== null) {
2846
- const normalized = normalizePhaseNumber(tok[0]);
2847
- if (normalized === ownNumber)
2848
- continue; // #4764: never the row's own phase
2849
- if (seen.has(normalized))
2938
+ const dependencyTokens = phaseIdConvention === 'bracket'
2939
+ ? extractPhaseDependencyTokens(prose, phaseIdConvention)
2940
+ : legacyManagerDependencyTokens(prose);
2941
+ for (const token of dependencyTokens) {
2942
+ const identity = bracketIdentityKey(token);
2943
+ if (identity) {
2944
+ if (identity === ownIdentity)
2945
+ continue;
2946
+ if (seen.has(identity))
2850
2947
  continue;
2851
- seen.add(normalized);
2852
- depNums.push(tok[0]);
2948
+ seen.add(identity);
2949
+ depNums.push(token);
2950
+ continue;
2853
2951
  }
2952
+ const normalized = normalizePhaseNumber(token);
2953
+ if (normalized === ownNumber)
2954
+ continue; // #4764: never the row's own phase
2955
+ if (seen.has(normalized))
2956
+ continue;
2957
+ seen.add(normalized);
2958
+ depNums.push(token);
2854
2959
  }
2855
- phase['deps_satisfied'] = depNums.every((n) => completedNums.has(normalizePhaseNumber(n)));
2960
+ phase['deps_satisfied'] = depNums.every((dependency) => {
2961
+ const identity = bracketIdentityKey(dependency);
2962
+ return identity
2963
+ ? completedBracketIdentities.has(identity)
2964
+ : completedNums.has(normalizePhaseNumber(dependency));
2965
+ });
2856
2966
  phase['dep_phases'] = depNums;
2857
2967
  }
2858
2968
  }
@@ -3316,19 +3426,20 @@ function cmdInitProgress(cwd, raw, options = {}) {
3316
3426
  `Available workstreams: ${_availableWorkstreams.join(', ')}`, ERROR_REASON.WORKSTREAM_MODE_NONE_ACTIVE);
3317
3427
  }
3318
3428
  const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
3429
+ const directoryContext = phaseDirectoryOptions(cwd, config);
3319
3430
  const phases = [];
3320
3431
  let currentPhase = null;
3321
3432
  let nextPhase = null;
3322
3433
  const roadmapPhaseNums = new Set();
3323
3434
  const roadmapPhaseNames = new Map();
3324
3435
  const roadmapCheckboxStates = new Map();
3436
+ const progressConvention = resolvePhaseIdConvention(cwd);
3325
3437
  try {
3326
3438
  const roadmapContent = extractCurrentMilestone(node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd);
3327
- const progressConvention = resolvePhaseIdConvention(cwd);
3328
3439
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag, owned by
3329
3440
  // buildPhaseHeadingScanRegex (phase-id.cts) so this scan also recognizes
3330
3441
  // bracket-convention headings instead of hand-rolling a literal `Phase\s+`.
3331
- const { regex: headingPattern, phaseNumGroup: hNumGroup, phaseNameGroup: hNameGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, progressConvention);
3442
+ const { regex: headingPattern, phaseNumGroup: hNumGroup, phaseNameGroup: hNameGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, progressConvention, directoryContext);
3332
3443
  let hm;
3333
3444
  while ((hm = headingPattern.exec(roadmapContent)) !== null) {
3334
3445
  roadmapPhaseNums.add(hm[hNumGroup]);
@@ -3361,12 +3472,12 @@ function cmdInitProgress(cwd, raw, options = {}) {
3361
3472
  // matched (by absorbing the bracket text). This does NOT migrate the site
3362
3473
  // onto phaseHeadingPrefixSrcFor / the bracket convention — that stays
3363
3474
  // out of scope per the comment above (#4984 revert, pinned census).
3364
- const cbPattern = new RegExp(`^[ \\t]*-[ \\t]*\\[(x| )\\][ \\t]*(?:\\*\\*)?(?:\\[[^\\]\\n]{0,200}\\]\\s*)?Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gim');
3475
+ const cbPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`(?:\\[[^\\]\\n]{0,200}\\]\\s*)?Phase\\s+(${phaseScanTokenSource(directoryContext, progressConvention)})[:\\s]`, 'gim', { state: 'captured' });
3365
3476
  let cbm;
3366
3477
  while ((cbm = cbPattern.exec(roadmapContent)) !== null) {
3367
3478
  const cbKey = cbm[2];
3368
3479
  if (!roadmapCheckboxStates.has(cbKey)) {
3369
- roadmapCheckboxStates.set(cbKey, cbm[1].toLowerCase() === 'x');
3480
+ roadmapCheckboxStates.set(cbKey, (0, phase_entry_grammar_cjs_1.isBoxCharChecked)(cbm[1]));
3370
3481
  }
3371
3482
  }
3372
3483
  }
@@ -3383,10 +3494,12 @@ function cmdInitProgress(cwd, raw, options = {}) {
3383
3494
  // behavior-preserving).
3384
3495
  const dirs = listMilestonePhaseDirs(phasesDir, { cwd }).value;
3385
3496
  for (const dir of dirs) {
3386
- const dirMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})-?(.*)`, 'i'));
3387
- const phaseNumber = dirMatch ? dirMatch[1] : dir;
3388
- const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
3389
- seenPhaseNums.add(phaseNumber.replace(/^0+/, '') || '0');
3497
+ const dirMatch = dir.match(new RegExp(`^(${phaseScanTokenSource(directoryContext, progressConvention)})-?(.*)`, 'i'));
3498
+ const query = customPhaseQuery(dir, directoryContext);
3499
+ const owner = query && customDirectoryOwner(dir, query);
3500
+ const phaseNumber = owner ? owner.id : dirMatch ? dirMatch[1] : dir;
3501
+ const phaseName = owner ? owner.slug : dirMatch && dirMatch[2] ? dirMatch[2] : null;
3502
+ seenPhaseNums.add((phaseKeyFromToken(phaseNumber, directoryContext) ?? phaseNumber).toLowerCase());
3390
3503
  const phasePath = node_path_1.default.join(phasesDir, dir);
3391
3504
  const phaseFiles = node_fs_1.default.readdirSync(phasePath);
3392
3505
  // #5060: Phase Status Module — the single owner of "what state is
@@ -3431,7 +3544,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
3431
3544
  /* otherwise intentionally empty */
3432
3545
  }
3433
3546
  for (const [num, name] of roadmapPhaseNames) {
3434
- const stripped = num.replace(/^0+/, '') || '0';
3547
+ const stripped = (phaseKeyFromToken(num, directoryContext) ?? num).toLowerCase();
3435
3548
  if (!seenPhaseNums.has(stripped)) {
3436
3549
  const checkboxComplete = roadmapCheckboxStates.get(num) === true ||
3437
3550
  roadmapCheckboxStates.get(stripped) === true;
@@ -3461,7 +3574,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
3461
3574
  }
3462
3575
  }
3463
3576
  }
3464
- phases.sort((a, b) => comparePhaseNum(a['number'], b['number']));
3577
+ phases.sort((a, b) => comparePhaseNum(a['number'], b['number'], directoryContext));
3465
3578
  // #3581: the frontier is ROADMAP ORDER, not artifact presence. The disk loop
3466
3579
  // above could claim nextPhase from a stray out-of-order artifact directory
3467
3580
  // (a phase-9 UAT evidence dir while roadmap phase 8 was pending and
@@ -229,11 +229,8 @@ const USER_OWNED_ARTIFACTS = ['USER-PROFILE.md'];
229
229
  * with the computed pathPrefix for the install.
230
230
  */
231
231
  function applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix) {
232
- content = content.replace(/~\/\.claude\//g, pathPrefix);
233
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
234
- content = content.replace(/\.\/\.claude\//g, `./${getDirName(runtime)}/`);
235
- content = content.replace(/~\/\.opencode\//g, pathPrefix);
236
- content = content.replace(/~\/\.kilo\//g, pathPrefix);
232
+ // #5715: one pass with literal replacement (see rewriteOpencodeFamilyConfigDirForms).
233
+ content = runtimeArtifactConversion._rewriteOpencodeFamilyConfigDirForms(content, pathPrefix, getDirName(runtime));
237
234
  // #5637: the typed-text capture heredoc ends at a terminator the typed text cannot know.
238
235
  return (0, args_terminator_cjs_1.stampInstalledContent)(content);
239
236
  }
@@ -1653,7 +1650,8 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
1653
1650
  let content = installFs().readFileSync(node_path_1.default.join(rawDir, entry.name), 'utf8');
1654
1651
  content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix);
1655
1652
  content = processAttribution(content, resolveAttribution(runtime));
1656
- content = converter(content, skillName);
1653
+ // #5715: the converter runs after the prefix pass; shield the emitted prefix from it.
1654
+ content = runtimeArtifactConversion._shieldEmittedPrefix(content, pathPrefix, (c) => converter(c, skillName));
1657
1655
  const skillDir = node_path_1.default.join(dest, skillName);
1658
1656
  installFs().mkdirSync(skillDir, { recursive: true });
1659
1657
  installFs().writeFileSync(node_path_1.default.join(skillDir, 'SKILL.md'), content);
@@ -1881,9 +1879,11 @@ function installOpencodeFamilyCommands(runtime, destDir, srcDir, pathPrefix, res
1881
1879
  // already satisfies the fold-to-descriptor requirement. Only the SKILLS
1882
1880
  // converter site above (installOpencodeFamilySkills) has a real
1883
1881
  // `converter` string to key on via SKILLS_CONVERTER_REGISTRY.
1884
- content = hostBehaviorsFor(runtime).frontmatterDialect === 'kilo'
1885
- ? runtimeArtifactConversion.convertClaudeToKiloFrontmatter(content)
1886
- : runtimeArtifactConversion.convertClaudeToOpencodeFrontmatter(content);
1882
+ // #5715: the converter runs after the prefix pass; shield the emitted prefix from it.
1883
+ const frontmatterConverter = hostBehaviorsFor(runtime).frontmatterDialect === 'kilo'
1884
+ ? (c) => runtimeArtifactConversion.convertClaudeToKiloFrontmatter(c)
1885
+ : (c) => runtimeArtifactConversion.convertClaudeToOpencodeFrontmatter(c);
1886
+ content = runtimeArtifactConversion._shieldEmittedPrefix(content, pathPrefix, frontmatterConverter);
1887
1887
  installFs().writeFileSync(node_path_1.default.join(destDir, destName), content);
1888
1888
  }
1889
1889
  }