@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
@@ -15,7 +15,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
15
15
  return (mod && mod.__esModule) ? mod : { "default": mod };
16
16
  };
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.PATH_ACTION_REASON = void 0;
18
+ exports.scrubGitLocationEnv = exports.GIT_LOCATION_ENV_VARS = exports.PATH_ACTION_REASON = void 0;
19
19
  exports.toPosixPath = toPosixPath;
20
20
  exports.toNativePath = toNativePath;
21
21
  exports.posixNormalize = posixNormalize;
@@ -37,6 +37,7 @@ exports.projectCodexHookTomlCommand = projectCodexHookTomlCommand;
37
37
  exports.escapePowerShellSingleQuoted = escapePowerShellSingleQuoted;
38
38
  exports.escapePosixDoubleQuoted = escapePosixDoubleQuoted;
39
39
  exports.escapeSingleQuotedShellLiteral = escapeSingleQuotedShellLiteral;
40
+ exports.shellSingleQuote = shellSingleQuote;
40
41
  exports.projectPathExportLine = projectPathExportLine;
41
42
  exports.renderShellActionLines = renderShellActionLines;
42
43
  exports.projectPathActionProjection = projectPathActionProjection;
@@ -64,6 +65,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
64
65
  // at load time and become un-mockable.
65
66
  const node_child_process_1 = __importDefault(require("node:child_process"));
66
67
  const pattern_cjs_1 = require("./pattern.cjs");
68
+ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
67
69
  const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
68
70
  const markdown_line_diff_cjs_1 = require("./markdown-line-diff.cjs");
69
71
  const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
@@ -458,6 +460,18 @@ function escapePosixDoubleQuoted(value) {
458
460
  function escapeSingleQuotedShellLiteral(value) {
459
461
  return String(value).replace(/'/g, "'\\''");
460
462
  }
463
+ /**
464
+ * #1460 / #4039: POSIX single-quote an arbitrary string as ONE shell token. A path such as a
465
+ * hook script's absolute install location commonly sits under a home directory containing
466
+ * spaces or other metacharacters (e.g. "/Users/Bob Smith/.claude/..."); written unquoted it
467
+ * would word-split and, with a hostile prefix, could inject. Wrapping in single quotes — with
468
+ * each embedded `'` escaped as `'\''` by escapeSingleQuotedShellLiteral — makes the whole value a
469
+ * single token no metacharacter inside it can break. This is the one implementation of that rule;
470
+ * do not re-implement it in a caller.
471
+ */
472
+ function shellSingleQuote(value) {
473
+ return "'" + escapeSingleQuotedShellLiteral(value) + "'";
474
+ }
461
475
  /**
462
476
  * The `export PATH="<dir>:$PATH"` line every persistence lane appends, plus the escaped directory
463
477
  * token it embeds. One builder because three lanes emit this line: a lane that re-escapes it
@@ -615,6 +629,10 @@ function _spawnResult(result, program) {
615
629
  timedOut: isSpawnTimeout({ error }),
616
630
  };
617
631
  }
632
+ // #5548: owned by read-only-git-env.cts (its default base is scrubbed); re-exported for callers.
633
+ var read_only_git_env_cjs_2 = require("./read-only-git-env.cjs");
634
+ Object.defineProperty(exports, "GIT_LOCATION_ENV_VARS", { enumerable: true, get: function () { return read_only_git_env_cjs_2.GIT_LOCATION_ENV_VARS; } });
635
+ Object.defineProperty(exports, "scrubGitLocationEnv", { enumerable: true, get: function () { return read_only_git_env_cjs_2.scrubGitLocationEnv; } });
618
636
  function execGit(args, opts = {}) {
619
637
  // Non-interactive defaults: a hung credential prompt or terminal-input
620
638
  // probe must surface as a timeout, not block the tool forever. Callers
@@ -1055,7 +1073,6 @@ function probeTty(opts = {}) {
1055
1073
  return null;
1056
1074
  }
1057
1075
  }
1058
- // ─── Platform file I/O ────────────────────────────────────────────────────────
1059
1076
  /**
1060
1077
  * How many leading lines of LF-only `text` are its closed YAML frontmatter block (opening
1061
1078
  * fence through closing fence), or 0 when there is none (an unterminated block is not
@@ -1075,7 +1092,7 @@ function leadingFrontmatterLineCount(text) {
1075
1092
  function withoutBom(line) {
1076
1093
  return line !== undefined && line.charCodeAt(0) === 0xFEFF ? line.slice(1) : (line ?? '');
1077
1094
  }
1078
- function _normalizeMd(content, prior) {
1095
+ function _normalizeMd(content, prior, preserveFencedStructure = false) {
1079
1096
  if (!content || typeof content !== 'string')
1080
1097
  return content;
1081
1098
  let text = content.replace(/\r\n/g, '\n');
@@ -1107,6 +1124,7 @@ function _normalizeMd(content, prior) {
1107
1124
  };
1108
1125
  const fenceRegex = /^```/;
1109
1126
  const insideFence = new Array(lines.length);
1127
+ const insideSharedFence = new Array(lines.length).fill(false);
1110
1128
  let fenceOpen = false;
1111
1129
  for (let i = 0; i < lines.length; i++) {
1112
1130
  if (i < frontmatterLines) {
@@ -1127,6 +1145,13 @@ function _normalizeMd(content, prior) {
1127
1145
  insideFence[i] = fenceOpen;
1128
1146
  }
1129
1147
  }
1148
+ if (preserveFencedStructure) {
1149
+ for (const block of (0, markdown_sectionizer_cjs_1.scanFencedBlocks)(lines)) {
1150
+ const end = block.closeLineIdx === -1 ? lines.length : block.closeLineIdx;
1151
+ for (let i = block.openLineIdx + 1; i < end; i++)
1152
+ insideSharedFence[i] = true;
1153
+ }
1154
+ }
1130
1155
  for (let i = 0; i < lines.length; i++) {
1131
1156
  const line = lines[i];
1132
1157
  if (i < frontmatterLines) {
@@ -1137,6 +1162,8 @@ function _normalizeMd(content, prior) {
1137
1162
  const prevTrimmed = prev.trimEnd();
1138
1163
  const trimmed = line.trimEnd();
1139
1164
  const isFenceLine = fenceRegex.test(trimmed);
1165
+ // Bracket writers preserve literal headings and lists inside fenced examples.
1166
+ const normalizeStructuralLine = !preserveFencedStructure || !insideSharedFence[i];
1140
1167
  // A heading or fence line the write authored brings its own separation, even
1141
1168
  // where it replaced a prose line in place (#5165). One that replaced a heading
1142
1169
  // or fence (a rename, a renumber) inherits the spacing around it instead.
@@ -1149,7 +1176,7 @@ function _normalizeMd(content, prior) {
1149
1176
  // A blank-line run is collapsed only where the write made it (#5165).
1150
1177
  if (scope !== null && line === '' && result.length > 0 && result[result.length - 1] === '' && !keptBefore)
1151
1178
  continue;
1152
- if (/^#{1,6}\s/.test(trimmed) && i > 0 && prevTrimmed !== '' && prevTrimmed !== '---' && !keptBefore)
1179
+ if (normalizeStructuralLine && /^#{1,6}\s/.test(trimmed) && i > 0 && prevTrimmed !== '' && prevTrimmed !== '---' && !keptBefore)
1153
1180
  blank();
1154
1181
  if (isFenceLine && i > 0 && prevTrimmed !== '' && !insideFence[i] && (i === 0 || !insideFence[i - 1] || isFenceLine) && !keptBefore) {
1155
1182
  if (i === 0 || !insideFence[i - 1])
@@ -1165,11 +1192,11 @@ function _normalizeMd(content, prior) {
1165
1192
  // from the after-heading rule below. The after-a-bullet rule at the end
1166
1193
  // of this loop is a different transition (list→prose) and is unaffected.
1167
1194
  result.push(line);
1168
- if (/^#{1,6}\s/.test(trimmed) && i < lines.length - 1 && (lines[i + 1] ?? '').trimEnd() !== '' && !keptAfter)
1195
+ if (normalizeStructuralLine && /^#{1,6}\s/.test(trimmed) && i < lines.length - 1 && (lines[i + 1] ?? '').trimEnd() !== '' && !keptAfter)
1169
1196
  blank();
1170
1197
  if (/^```\s*$/.test(trimmed) && i > 0 && insideFence[i - 1] && i < lines.length - 1 && (lines[i + 1] ?? '').trimEnd() !== '' && !keptAfter)
1171
1198
  blank();
1172
- if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(line) && i < lines.length - 1 && !keptAfter) {
1199
+ if (normalizeStructuralLine && /^(\s*[-*+]\s|\s*\d+\.\s)/.test(line) && i < lines.length - 1 && !keptAfter) {
1173
1200
  const next = lines[i + 1];
1174
1201
  if (next !== undefined && next.trimEnd() !== '' && !/^(\s*[-*+]\s|\s*\d+\.\s)/.test(next) && !/^\s/.test(next))
1175
1202
  blank();
@@ -1190,7 +1217,7 @@ function normalizeContent(filePath, content, opts = {}) {
1190
1217
  const isMd = node_path_1.default.extname(filePath).toLowerCase() === '.md';
1191
1218
  let normalized;
1192
1219
  if (isMd) {
1193
- normalized = _normalizeMd(content, opts.prior);
1220
+ normalized = _normalizeMd(content, opts.prior, opts.preserveFencedMarkdownStructure === true);
1194
1221
  }
1195
1222
  else {
1196
1223
  normalized = (content ?? '').replace(/\r\n/g, '\n').replace(/\n*$/, '\n');
@@ -1211,8 +1238,8 @@ function normalizeContent(filePath, content, opts = {}) {
1211
1238
  * post-write re-read of the actual on-disk bytes) instead of a raw `!==` —
1212
1239
  * do not simplify this back to a direct string comparison.
1213
1240
  */
1214
- function contentChangedAfterNormalize(filePath, before, after) {
1215
- return normalizeContent(filePath, after, { prior: before }).content !== normalizeContent(filePath, before, { prior: before }).content;
1241
+ function contentChangedAfterNormalize(filePath, before, after, opts = {}) {
1242
+ return normalizeContent(filePath, after, { ...opts, prior: before }).content !== normalizeContent(filePath, before, { ...opts, prior: before }).content;
1216
1243
  }
1217
1244
  // Rename errnos that are transient on Windows: a concurrent reader (or an AV
1218
1245
  // scanner / indexer) holding the target open makes renameSync fail briefly.
@@ -69,11 +69,14 @@ const stateDocument = require("./state-document.cjs");
69
69
  const { stateFieldValue } = stateDocument;
70
70
  // eslint-disable-next-line @typescript-eslint/no-require-imports
71
71
  const phaseId = require("./phase-id.cjs");
72
- const { comparePhaseNum, extractPhaseToken, matchPhaseDirs, normalizePhaseName, parsePhaseFromProse, stripProjectCodePrefix } = phaseId;
72
+ const { comparePhaseNum, extractPhaseToken, customPhaseQuery, customDirectoryOwner, parsePhaseFromProse, stripProjectCodePrefix } = phaseId;
73
73
  // eslint-disable-next-line @typescript-eslint/no-require-imports
74
74
  const stateMod = require("./state.cjs");
75
75
  const { readStateHeadFreshness } = stateMod;
76
76
  // eslint-disable-next-line @typescript-eslint/no-require-imports
77
+ const phaseLocatorMod = require("./phase-locator.cjs");
78
+ const { phaseDirectoryOptions, resolvePhaseDirectoryLookup, matchPhaseDirsForLookup } = phaseLocatorMod;
79
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
77
80
  const unusableInput = require("./unusable-input.cjs");
78
81
  const { warnUnusableInput, UNUSABLE_REASON } = unusableInput;
79
82
  // #5118: the verification-status owner's frontmatter-only report check.
@@ -120,8 +123,8 @@ function parseIntOrNull(s) {
120
123
  const n = Number.parseInt(cleaned, 10);
121
124
  return Number.isFinite(n) ? n : null;
122
125
  }
123
- function phaseTokenFromDirName(name) {
124
- const token = extractPhaseToken(name);
126
+ function phaseTokenFromDirName(name, convention) {
127
+ const token = extractPhaseToken(name, convention);
125
128
  // #2528: the shape probe runs on the PROJECT-CODE-STRIPPED token. A prefixed
126
129
  // directory tokenizes to `MEM-05-80-20`, which does not start with a digit,
127
130
  // so the unstripped probe rejected it and the entry was dropped before any
@@ -252,9 +255,9 @@ function readGitSignals(cwd) {
252
255
  const unpushed = run(['log', '@{u}..HEAD', '--oneline']).trim().length > 0;
253
256
  return { has_git: true, dirty, unpushed };
254
257
  }
255
- /** Leading numeric phase token from a STATE.md scalar or body `Phase:` value. */
256
- function phaseTokenFromState(raw) {
257
- return parsePhaseFromProse(raw).phase;
258
+ /** Phase identity from a STATE.md scalar or body `Phase:` value, honoring custom naming when a context is supplied. */
259
+ function phaseTokenFromState(raw, context) {
260
+ return parsePhaseFromProse(raw, undefined, context).phase;
258
261
  }
259
262
  /**
260
263
  * Detect whether the current phase's verify report indicates failure. The
@@ -264,12 +267,20 @@ function phaseTokenFromState(raw) {
264
267
  */
265
268
  function detectVerifyFailed(cwd, currentPhaseRaw) {
266
269
  const phasesDir = node_path_1.default.join(planningPaths(cwd).phases);
270
+ // #4304: both the directory admission scan and the later selector must read
271
+ // the same current-checkout grammar. Bracket is the only opt-in widening.
272
+ const convention = resolvePhaseIdConvention(cwd) === 'bracket' ? 'bracket' : undefined;
273
+ const directoryContext = phaseDirectoryOptions(cwd);
267
274
  let entries = [];
268
275
  try {
269
276
  entries = node_fs_1.default.readdirSync(phasesDir)
270
- .map((name) => ({ name, phaseToken: phaseTokenFromDirName(name) }))
277
+ .map((name) => {
278
+ const custom = customPhaseQuery(name, directoryContext);
279
+ const parts = custom && customDirectoryOwner(name, custom);
280
+ return { name, phaseToken: parts ? parts.id : phaseTokenFromDirName(name, convention) };
281
+ })
271
282
  .filter((entry) => entry.phaseToken !== null)
272
- .sort((a, b) => comparePhaseNum(a.phaseToken, b.phaseToken) || a.name.localeCompare(b.name))
283
+ .sort((a, b) => comparePhaseNum(a.phaseToken, b.phaseToken, directoryContext) || a.name.localeCompare(b.name))
273
284
  .map((entry) => entry.name);
274
285
  }
275
286
  catch {
@@ -277,10 +288,10 @@ function detectVerifyFailed(cwd, currentPhaseRaw) {
277
288
  }
278
289
  if (entries.length === 0)
279
290
  return false;
280
- const phaseToken = phaseTokenFromState(currentPhaseRaw);
291
+ const phaseToken = phaseTokenFromState(currentPhaseRaw, directoryContext);
281
292
  let targetDir;
282
293
  if (phaseToken) {
283
- const normalized = normalizePhaseName(phaseToken);
294
+ const lookup = resolvePhaseDirectoryLookup(cwd, phaseToken, undefined, directoryContext);
284
295
  // #2528: the fourth directory-resolution site, and the one where a miss is
285
296
  // silent — a phase whose directory cannot be found reports "not failed",
286
297
  // which reads identically to a healthy phase. It must therefore apply the
@@ -289,7 +300,7 @@ function detectVerifyFailed(cwd, currentPhaseRaw) {
289
300
  // surfaces its own failed verification. `entries` is already sorted, and
290
301
  // `matchPhaseDirs` filters without reordering, so taking the first match
291
302
  // preserves the previous `.find()` selection exactly.
292
- const { matches } = matchPhaseDirs(entries, normalized);
303
+ const { matches } = matchPhaseDirsForLookup(entries, lookup, undefined, directoryContext);
293
304
  targetDir = matches[0];
294
305
  if (!targetDir)
295
306
  return false;
@@ -8,7 +8,8 @@
8
8
  * already answers:
9
9
  * - `planning-workspace.cjs`'s `planningPaths` for every `.planning/` path
10
10
  * (workstream-aware — never re-derived here).
11
- * - `phase-lifecycle.cjs`'s `locateProgressTable` (this issue's Edit 1) for
11
+ * - `roadmap-progress.cjs`'s `locateRoadmapProgress` (#5761; was
12
+ * `phase-lifecycle`'s `locateProgressTable`) for
12
13
  * "where is the Progress table" — the SAME table
13
14
  * `deriveProgressFromRoadmap` counts from, so `phases[]` and
14
15
  * `deriveProgressFromRoadmap`'s `totalPhases` can never disagree about
@@ -79,6 +80,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
79
80
  // documented `state.cjs -> state-contract.cjs -> smart-entry.cjs -> state.cjs`
80
81
  // require cycle the file header warns about.
81
82
  const phase_status_cjs_1 = require("./phase-status.cjs");
83
+ const phase_entry_grammar_cjs_1 = require("./phase-entry-grammar.cjs");
82
84
  // ─── Public wire vocabulary ────────────────────────────────────────────────
83
85
  exports.STATE_CONTRACT_VERSION = '1.0.0';
84
86
  exports.STATE_CONTRACT_FLAVOR = 'core';
@@ -114,7 +116,7 @@ function owners() {
114
116
  /* eslint-disable @typescript-eslint/no-require-imports */
115
117
  const planningWorkspaceMod = require('./planning-workspace.cjs');
116
118
  const shellCommandProjectionMod = require('./shell-command-projection.cjs');
117
- const phaseLifecycleMod = require('./phase-lifecycle.cjs');
119
+ const roadmapProgressMod = require('./roadmap-progress.cjs');
118
120
  const phaseIdMod = require('./phase-id.cjs');
119
121
  const roadmapParserMod = require('./roadmap-parser.cjs');
120
122
  const markdownSectionizerMod = require('./markdown-sectionizer.cjs');
@@ -126,7 +128,7 @@ function owners() {
126
128
  planningPaths: planningWorkspaceMod.planningPaths,
127
129
  platformReadSync: shellCommandProjectionMod.platformReadSync,
128
130
  platformWriteSync: shellCommandProjectionMod.platformWriteSync,
129
- locateProgressTable: phaseLifecycleMod.locateProgressTable,
131
+ locateRoadmapProgress: roadmapProgressMod.locateRoadmapProgress,
130
132
  isSentinelPhaseId: phaseIdMod.isSentinelPhaseId,
131
133
  parsePhaseFromProse: phaseIdMod.parsePhaseFromProse,
132
134
  resolvePhaseIdConvention: planningWorkspaceMod.resolvePhaseIdConvention,
@@ -188,9 +190,11 @@ function phasesFromProgressTable(table, convention) {
188
190
  }
189
191
  // ─── phases: `## Phases` checkbox fallback ─────────────────────────────────
190
192
  // `- [x] **Phase 1: Foundation**` / `- [ ] **Phase 2.1: Hardening**`.
191
- const PHASE_BULLET_RE = /^[ \t]*[-*][ \t]+\[([ xX])\][ \t]+\*\*Phase[ \t]+(\d+(?:\.\d+)*)[ \t]*:?[ \t]*([^*\n]*)\*\*/gm;
193
+ // The entry prefix (marker, box, bold token) is the phase-entry grammar's (#5764): capture
194
+ // group 1 is the box character, 2 the number, 3 the name.
195
+ const PHASE_BULLET_RE = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`Phase[ \\t]+(\\d+(?:\\.\\d+)*)[ \\t]*:?[ \\t]*([^*\\n]*)${phase_entry_grammar_cjs_1.PHASE_ENTRY_BOLD_CLOSE_SRC}`, 'g', { state: 'captured', bold: 'required' });
192
196
  // #5626: the same bullet under `milestone-prefixed`: the number capture takes one optional `-NN` segment.
193
- const PHASE_BULLET_MILESTONE_RE = /^[ \t]*[-*][ \t]+\[([ xX])\][ \t]+\*\*Phase[ \t]+(\d+(?:-\d+)?(?:\.\d+)*)[ \t]*:?[ \t]*([^*\n]*)\*\*/gm;
197
+ const PHASE_BULLET_MILESTONE_RE = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`Phase[ \\t]+(\\d+(?:-\\d+)?(?:\\.\\d+)*)[ \\t]*:?[ \\t]*([^*\\n]*)${phase_entry_grammar_cjs_1.PHASE_ENTRY_BOLD_CLOSE_SRC}`, 'g', { state: 'captured', bold: 'required' });
194
198
  /**
195
199
  * STATE.md's current phase, via the same frontmatter-then-body chain
196
200
  * `cmdStateCompletePhase` uses (`stateFieldValue` owns the #1760 fallback
@@ -249,7 +253,7 @@ function phasesFromChecklist(roadmapContent, cwd, statePath, convention) {
249
253
  return phases;
250
254
  }
251
255
  function buildPhases(cwd, paths) {
252
- const { platformReadSync, locateProgressTable } = owners();
256
+ const { platformReadSync, locateRoadmapProgress } = owners();
253
257
  try {
254
258
  let content = platformReadSync(paths.roadmap);
255
259
  if (content === null)
@@ -257,7 +261,7 @@ function buildPhases(cwd, paths) {
257
261
  if (content.charCodeAt(0) === 0xfeff)
258
262
  content = content.slice(1); // strip leading BOM
259
263
  const convention = phaseIdConventionOf(cwd);
260
- const table = locateProgressTable(content);
264
+ const { table } = locateRoadmapProgress(content);
261
265
  if (table)
262
266
  return phasesFromProgressTable(table, convention);
263
267
  return phasesFromChecklist(content, cwd, paths.state, convention);