@opengsd/gsd-core 1.20.0 → 1.21.0

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 (154) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +33 -2
  4. package/agents/gsd-executor.md +1 -1
  5. package/agents/gsd-plan-checker.md +1 -1
  6. package/agents/gsd-planner.md +3 -3
  7. package/agents/gsd-verifier.md +2 -2
  8. package/bin/install.js +265 -161
  9. package/commands/gsd/autonomous.md +1 -1
  10. package/commands/gsd/new-milestone.md +2 -2
  11. package/commands/gsd/new-project.md +2 -2
  12. package/gsd-core/bin/gsd-tools.cjs +554 -823
  13. package/gsd-core/bin/lib/capability-activation.cjs +8 -2
  14. package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
  15. package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
  16. package/gsd-core/bin/lib/capability-lock.cjs +19 -27
  17. package/gsd-core/bin/lib/capability-registry.cjs +74 -71
  18. package/gsd-core/bin/lib/capability-validator.cjs +1 -0
  19. package/gsd-core/bin/lib/capability-writer.cjs +7 -4
  20. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
  21. package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
  22. package/gsd-core/bin/lib/cli-surface.cjs +545 -0
  23. package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
  24. package/gsd-core/bin/lib/commands.cjs +99 -2
  25. package/gsd-core/bin/lib/config-file-read.cjs +85 -0
  26. package/gsd-core/bin/lib/config-loader.cjs +23 -15
  27. package/gsd-core/bin/lib/config.cjs +139 -101
  28. package/gsd-core/bin/lib/configuration.cjs +41 -2
  29. package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
  30. package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
  31. package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
  32. package/gsd-core/bin/lib/gap-checker.cjs +12 -1
  33. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
  34. package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
  35. package/gsd-core/bin/lib/graphify.cjs +19 -2
  36. package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
  37. package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
  38. package/gsd-core/bin/lib/init.cjs +20 -18
  39. package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
  40. package/gsd-core/bin/lib/install-engine.cjs +20 -28
  41. package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
  42. package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
  43. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
  44. package/gsd-core/bin/lib/install-profiles.cjs +4 -2
  45. package/gsd-core/bin/lib/io.cjs +6 -0
  46. package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
  47. package/gsd-core/bin/lib/markdown-table.cjs +39 -1
  48. package/gsd-core/bin/lib/milestone.cjs +8 -6
  49. package/gsd-core/bin/lib/model-resolver.cjs +8 -4
  50. package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
  51. package/gsd-core/bin/lib/phase-id.cjs +26 -9
  52. package/gsd-core/bin/lib/phase-locator.cjs +8 -3
  53. package/gsd-core/bin/lib/phase.cjs +170 -45
  54. package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
  55. package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
  56. package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
  57. package/gsd-core/bin/lib/profile-output.cjs +9 -6
  58. package/gsd-core/bin/lib/project-context.cjs +439 -0
  59. package/gsd-core/bin/lib/project-root.cjs +9 -230
  60. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
  61. package/gsd-core/bin/lib/report-parser.cjs +16 -0
  62. package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
  63. package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
  64. package/gsd-core/bin/lib/roadmap.cjs +2 -2
  65. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
  66. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
  67. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
  68. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
  69. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  70. package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
  71. package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
  72. package/gsd-core/bin/lib/state-transition.cjs +103 -46
  73. package/gsd-core/bin/lib/state.cjs +448 -63
  74. package/gsd-core/bin/lib/surface.cjs +4 -7
  75. package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
  76. package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
  77. package/gsd-core/bin/lib/verify.cjs +49 -6
  78. package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
  79. package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
  80. package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
  81. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  82. package/gsd-core/references/autonomous-smart-discuss.md +99 -18
  83. package/gsd-core/references/checkpoints.md +1 -1
  84. package/gsd-core/references/planner-guidance.md +11 -0
  85. package/gsd-core/references/planner-human-verify-mode.md +1 -1
  86. package/gsd-core/references/planner-reversibility.md +2 -0
  87. package/gsd-core/references/planner-verify-command-grounding.md +22 -6
  88. package/gsd-core/workflows/autonomous.md +4 -4
  89. package/gsd-core/workflows/code-review-fix.md +5 -7
  90. package/gsd-core/workflows/execute-phase.md +21 -7
  91. package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
  92. package/gsd-core/workflows/plan-phase.md +18 -19
  93. package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
  94. package/hooks/dist/gsd-config-reload.js +2 -2
  95. package/hooks/dist/gsd-context-monitor.js +2 -1
  96. package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
  97. package/hooks/dist/gsd-read-injection-scanner.js +2 -2
  98. package/hooks/dist/gsd-secret-read-guard.js +141 -6
  99. package/hooks/dist/gsd-statusline.js +3 -2
  100. package/hooks/dist/gsd-workflow-guard.js +2 -1
  101. package/hooks/dist/lib/config-read.js +49 -0
  102. package/hooks/dist/lib/git-cmd.js +3 -1
  103. package/hooks/dist/lib/git-head-tree.js +474 -0
  104. package/hooks/dist/lib/isolation-sentinel.js +168 -13
  105. package/hooks/dist/lib/secret-read-exemptions.js +161 -0
  106. package/hooks/gsd-agent-isolation-guard.js +4 -3
  107. package/hooks/gsd-config-reload.js +2 -2
  108. package/hooks/gsd-context-monitor.js +2 -1
  109. package/hooks/gsd-cursor-subagent-start.js +9 -6
  110. package/hooks/gsd-read-injection-scanner.js +2 -2
  111. package/hooks/gsd-secret-read-guard.js +141 -6
  112. package/hooks/gsd-statusline.js +3 -2
  113. package/hooks/gsd-workflow-guard.js +2 -1
  114. package/hooks/lib/config-read.js +49 -0
  115. package/hooks/lib/git-cmd.js +3 -1
  116. package/hooks/lib/git-head-tree.js +474 -0
  117. package/hooks/lib/isolation-sentinel.js +168 -13
  118. package/hooks/lib/secret-read-exemptions.js +161 -0
  119. package/package.json +7 -2
  120. package/pi/package-agents/gsd-executor.md +1 -1
  121. package/pi/package-agents/gsd-plan-checker.md +1 -1
  122. package/pi/package-agents/gsd-planner.md +3 -3
  123. package/pi/package-agents/gsd-verifier.md +2 -2
  124. package/pi/package-agents/manifest.json +4 -4
  125. package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
  126. package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
  127. package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
  128. package/scripts/changeset/cli.cjs +2 -2
  129. package/scripts/changeset/lint.cjs +28 -3
  130. package/scripts/changeset/serialize.cjs +12 -2
  131. package/scripts/ci-compact-v8-coverage.cjs +204 -0
  132. package/scripts/ci-fix-next-rerun.cjs +149 -0
  133. package/scripts/ci-next-health.cjs +52 -9
  134. package/scripts/ci-test-scope.cjs +77 -71
  135. package/scripts/command-contract-helpers.cjs +56 -7
  136. package/scripts/docs-guard-registry.cjs +3 -0
  137. package/scripts/gen-cli-surface-manifest.cjs +287 -0
  138. package/scripts/gen-platform-conformance-tier.cjs +258 -19
  139. package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
  140. package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
  141. package/scripts/lib/shellcheck-fetch.cjs +71 -4
  142. package/scripts/lint-removed-but-needed.cjs +562 -50
  143. package/scripts/merge-commit-pr.cjs +186 -0
  144. package/scripts/release-ancestry.cjs +86 -0
  145. package/scripts/release-notes/changelog-release-notes.cjs +345 -0
  146. package/scripts/release-notes/discord-release-summary.cjs +82 -1
  147. package/scripts/release-pipeline-pr.cjs +92 -0
  148. package/scripts/require-issue-link-policy.cjs +18 -9
  149. package/scripts/sync-next-version.cjs +11 -2
  150. package/scripts/timeout-evidence-from-ci.cjs +964 -0
  151. package/skills/gsd-autonomous/SKILL.md +1 -1
  152. package/skills/gsd-new-milestone/SKILL.md +1 -1
  153. package/skills/gsd-new-project/SKILL.md +1 -1
  154. package/vscode/package.json +1 -1
@@ -25,6 +25,7 @@ const capabilityRegistry = require('./capability-registry.cjs');
25
25
  const { hostBehaviorsFor } = require('./runtime-name-policy.cjs');
26
26
  const { resolveConfigHomeFromDescriptor } = require('./runtime-homes.cjs');
27
27
  const { resolveActiveRuntime } = require('./runtime-slash.cjs');
28
+ const { readConfigFileBounded } = require('./config-file-read.cjs');
28
29
 
29
30
  /**
30
31
  * Runtimes whose agent config is static frontmatter/TOML baked at install time,
@@ -226,7 +227,9 @@ function _readRuntimeConfig(cwd, { fsStatSync = fs.statSync } = {}) {
226
227
  const candidate = path.join(probe, '.planning', 'config.json');
227
228
  try {
228
229
  fsStatSync(candidate);
229
- const raw = fs.readFileSync(candidate, 'utf8');
230
+ // Bounded read (#5759): a FIFO/device/oversized config throws and is walked past like a
231
+ // missing one; null (raced away after the stat) makes JSON.parse('') throw into the same catch.
232
+ const raw = readConfigFileBounded(candidate) ?? '';
230
233
  const parsed = JSON.parse(raw);
231
234
  if (parsed && typeof parsed === 'object') return parsed;
232
235
  return {};
@@ -31,6 +31,8 @@ exports.applyPreserveWhenUnchanged = applyPreserveWhenUnchanged;
31
31
  exports.applyStatePreservation = applyStatePreservation;
32
32
  exports.transitionCore = transitionCore;
33
33
  exports.sliceCurrentPositionSection = sliceCurrentPositionSection;
34
+ exports.readPlanPosition = readPlanPosition;
35
+ exports.readRecordedPlanTotal = readRecordedPlanTotal;
34
36
  exports.isStatePastPhase = isStatePastPhase;
35
37
  // eslint-disable-next-line @typescript-eslint/no-require-imports
36
38
  const frontmatter = require("./frontmatter.cjs");
@@ -42,7 +44,7 @@ const roadmap_progress_cjs_1 = require("./roadmap-progress.cjs");
42
44
  const pattern_cjs_1 = require("./pattern.cjs");
43
45
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
44
46
  const phaseIdMod = require("./phase-id.cjs");
45
- const { comparePhaseNum } = phaseIdMod;
47
+ const { comparePhaseNum, parsePhaseFromProse } = phaseIdMod;
46
48
  // #4129: the completion-ratio kernel for the resync-arm ratchet's percent
47
49
  // (planning-scope's SCOPE — state-document's own dependency, no cycle here:
48
50
  // state-document never imports this module).
@@ -1261,6 +1263,94 @@ function planNumberFrom(digits) {
1261
1263
  const n = Number(digits);
1262
1264
  return Number.isSafeInteger(n) ? n : null;
1263
1265
  }
1266
+ /**
1267
+ * The one plan-position parse: legacy pair first, then the hybrid, then
1268
+ * compound. `advancePlanCore` and `readPlanPosition` both consume it, so the
1269
+ * precedence is decided in exactly one place (#5786).
1270
+ *
1271
+ * These branches decide ONE thing: which numbers the position is read from.
1272
+ * They deliberately do not record which FIELD supplied them, because the
1273
+ * advance write path writes every spelling back from its own raw text (#3791
1274
+ * review round 6, B1/M1).
1275
+ *
1276
+ * Every branch reads its numbers out of an ANCHORED match's capture groups.
1277
+ * Nothing here calls parseInt on a raw field value, so a value the grammar
1278
+ * does not fully describe cannot half-parse into a plausible number.
1279
+ */
1280
+ function parsePlanFields(content) {
1281
+ const legacyPlan = (0, state_document_cjs_1.stateExtractField)(content, 'Current Plan');
1282
+ const legacyTotal = (0, state_document_cjs_1.stateExtractField)(content, 'Total Plans in Phase');
1283
+ const planField = (0, state_document_cjs_1.stateExtractField)(content, 'Plan');
1284
+ const legacyNMatch = legacyPlan ? PLAN_SHAPE_N.exec(legacyPlan) : null;
1285
+ const legacyNofMMatch = legacyPlan ? PLAN_SHAPE_N_OF_M.exec(legacyPlan) : null;
1286
+ const totalNMatch = legacyTotal ? PLAN_SHAPE_N.exec(legacyTotal) : null;
1287
+ const planNofMMatch = planField ? PLAN_SHAPE_N_OF_M.exec(planField) : null;
1288
+ const planNMatch = planField ? PLAN_SHAPE_N.exec(planField) : null;
1289
+ let current = null;
1290
+ let total = null;
1291
+ if (legacyPlan && legacyTotal && (legacyNMatch || legacyNofMMatch) && totalNMatch) {
1292
+ // Legacy pair wins whenever both fields are present and both are readable,
1293
+ // even if the Current Plan value also carries an "of M" — the explicit
1294
+ // sibling field is the stated intent, so it supplies the total.
1295
+ current = planNumberFrom((legacyNMatch ?? legacyNofMMatch)[1]);
1296
+ total = planNumberFrom(totalNMatch[1]);
1297
+ }
1298
+ else if (legacyNofMMatch) {
1299
+ // Hybrid: legacy field name, compound value, no readable Total Plans
1300
+ // sibling. Written by hand (and by agents) often enough to be worth
1301
+ // reading — #3784.
1302
+ current = planNumberFrom(legacyNofMMatch[1]);
1303
+ total = planNumberFrom(legacyNofMMatch[2]);
1304
+ }
1305
+ else if (planNofMMatch) {
1306
+ current = planNumberFrom(planNofMMatch[1]);
1307
+ total = planNumberFrom(planNofMMatch[2]);
1308
+ }
1309
+ // No branch for a bare `Plan: N` paired with a `Total Plans in Phase: M`
1310
+ // sibling and no `Current Plan` at all (#3791 review round 6, M2). A revision
1311
+ // of this PR accepted it; base did not (its `else if (planField)` arm had no
1312
+ // `of M` match and errored via NaN), and it is out of #3784's scope, which is
1313
+ // the hybrid `Current Plan: N of M`. It cannot be given the schema-row +
1314
+ // forcing-test coupling the other shapes have, either: `Plan` is body-only,
1315
+ // `buildStateFrontmatter` never reads it into frontmatter, so there is no
1316
+ // `current_*` key to hang a row on. An accepted shape with no schema row and
1317
+ // no forcing test is exactly the drift this diff is otherwise built to
1318
+ // prevent, so the shape is refused and named in the error instead.
1319
+ return { legacyPlan, planField, legacyNMatch, legacyNofMMatch, planNofMMatch, planNMatch, current, total };
1320
+ }
1321
+ /**
1322
+ * Read the recorded plan position out of a STATE.md document through the one
1323
+ * plan-position grammar, or `null` when no readable position is recorded
1324
+ * (`Plan: 1 of ?`, `Total Plans in Phase: TBD`, no field at all). #5786.
1325
+ */
1326
+ function readPlanPosition(content) {
1327
+ const { current, total } = parsePlanFields(content);
1328
+ return current === null || total === null ? null : { current, total };
1329
+ }
1330
+ /**
1331
+ * The plan total a STATE.md document records, for the `init --validate`
1332
+ * plan-count check: a readable `Total Plans in Phase` first, else the
1333
+ * of-M total read straight from the plan fields: `Current Plan`'s of-M is
1334
+ * tried first, then `Plan`'s, and convertibility is checked per field (an
1335
+ * unconvertible `Current Plan` M falls through to `Plan`'s M), unlike
1336
+ * `parsePlanFields`, which commits to the first matching field. The fallback
1337
+ * does not go through the legacy pair, so an unconvertible `Total Plans in
1338
+ * Phase` beside `Current Plan: N of M` still yields M. `null` when neither
1339
+ * yields a convertible M. #5786.
1340
+ */
1341
+ function readRecordedPlanTotal(content) {
1342
+ // An explicit `Total Plans in Phase` wins over an `of M` in either plan field.
1343
+ const raw = (0, state_document_cjs_1.stateExtractField)(content, 'Total Plans in Phase');
1344
+ const match = raw ? PLAN_SHAPE_N.exec(raw) : null;
1345
+ const t = match ? planNumberFrom(match[1]) : null;
1346
+ if (t !== null)
1347
+ return t;
1348
+ const { legacyNofMMatch, planNofMMatch } = parsePlanFields(content);
1349
+ const fromCurrentPlan = legacyNofMMatch ? planNumberFrom(legacyNofMMatch[2]) : null;
1350
+ if (fromCurrentPlan !== null)
1351
+ return fromCurrentPlan;
1352
+ return planNofMMatch ? planNumberFrom(planNofMMatch[2]) : null;
1353
+ }
1264
1354
  /**
1265
1355
  * Advance the leading integer of a written plan value, preserving everything
1266
1356
  * the author wrote around it: the zero-padding width ("04" -> "05") and any
@@ -1483,47 +1573,7 @@ function advancePlanCore(content, deps) {
1483
1573
  // `planSourceField`/`planRawValue` pair here and then wrote the OTHER
1484
1574
  // spelling from this one's numbers, which is precisely how a field ended up
1485
1575
  // holding a value nothing had derived for it.
1486
- const legacyPlan = (0, state_document_cjs_1.stateExtractField)(content, 'Current Plan');
1487
- const legacyTotal = (0, state_document_cjs_1.stateExtractField)(content, 'Total Plans in Phase');
1488
- const planField = (0, state_document_cjs_1.stateExtractField)(content, 'Plan');
1489
- // Every branch below reads its numbers out of an ANCHORED match's capture
1490
- // groups. Nothing here calls parseInt on a raw field value, so a value the
1491
- // grammar does not fully describe cannot half-parse into a plausible number.
1492
- const legacyNMatch = legacyPlan ? PLAN_SHAPE_N.exec(legacyPlan) : null;
1493
- const legacyNofMMatch = legacyPlan ? PLAN_SHAPE_N_OF_M.exec(legacyPlan) : null;
1494
- const totalNMatch = legacyTotal ? PLAN_SHAPE_N.exec(legacyTotal) : null;
1495
- const planNofMMatch = planField ? PLAN_SHAPE_N_OF_M.exec(planField) : null;
1496
- const planNMatch = planField ? PLAN_SHAPE_N.exec(planField) : null;
1497
- let parsedCurrent = null;
1498
- let parsedTotal = null;
1499
- if (legacyPlan && legacyTotal && (legacyNMatch || legacyNofMMatch) && totalNMatch) {
1500
- // Legacy pair wins whenever both fields are present and both are readable,
1501
- // even if the Current Plan value also carries an "of M" — the explicit
1502
- // sibling field is the stated intent, so it supplies the total.
1503
- parsedCurrent = planNumberFrom((legacyNMatch ?? legacyNofMMatch)[1]);
1504
- parsedTotal = planNumberFrom(totalNMatch[1]);
1505
- }
1506
- else if (legacyNofMMatch) {
1507
- // Hybrid: legacy field name, compound value, no readable Total Plans
1508
- // sibling. Written by hand (and by agents) often enough to be worth
1509
- // reading — #3784.
1510
- parsedCurrent = planNumberFrom(legacyNofMMatch[1]);
1511
- parsedTotal = planNumberFrom(legacyNofMMatch[2]);
1512
- }
1513
- else if (planNofMMatch) {
1514
- parsedCurrent = planNumberFrom(planNofMMatch[1]);
1515
- parsedTotal = planNumberFrom(planNofMMatch[2]);
1516
- }
1517
- // No branch for a bare `Plan: N` paired with a `Total Plans in Phase: M`
1518
- // sibling and no `Current Plan` at all (#3791 review round 6, M2). A revision
1519
- // of this PR accepted it; base did not (its `else if (planField)` arm had no
1520
- // `of M` match and errored via NaN), and it is out of #3784's scope, which is
1521
- // the hybrid `Current Plan: N of M`. It cannot be given the schema-row +
1522
- // forcing-test coupling the other shapes have, either: `Plan` is body-only,
1523
- // `buildStateFrontmatter` never reads it into frontmatter, so there is no
1524
- // `current_*` key to hang a row on. An accepted shape with no schema row and
1525
- // no forcing test is exactly the drift this diff is otherwise built to
1526
- // prevent, so the shape is refused and named in the error instead.
1576
+ const { legacyPlan, planField, legacyNMatch, legacyNofMMatch, planNofMMatch, planNMatch, current: parsedCurrent, total: parsedTotal, } = parsePlanFields(content);
1527
1577
  if (parsedCurrent === null || parsedTotal === null) {
1528
1578
  return { content: reassemble(body), updated: [], data: { error: true } };
1529
1579
  }
@@ -1724,8 +1774,10 @@ function nextPhaseWording(disk) {
1724
1774
  * position; the caller combines this with "the phase is already complete in
1725
1775
  * ROADMAP.md" so a first-time out-of-order completion (#2028) still
1726
1776
  * transitions. An unparseable or absent current phase is "not past".
1777
+ * #5676: under `milestone-prefixed` the id is the whole `M-NN` (`3-02`, not `3`); that read goes
1778
+ * through `parsePhaseFromProse` (the #5626 grammar owner), so the two cannot drift.
1727
1779
  */
1728
- function isStatePastPhase(content, phaseNum, sourcePath) {
1780
+ function isStatePastPhase(content, phaseNum, sourcePath, convention) {
1729
1781
  const fm = extractFrontmatter(content, sourcePath);
1730
1782
  const fromFm = fm['current_phase'];
1731
1783
  let raw = typeof fromFm === 'string' || typeof fromFm === 'number' ? String(fromFm) : null;
@@ -1734,10 +1786,15 @@ function isStatePastPhase(content, phaseNum, sourcePath) {
1734
1786
  raw = (0, state_document_cjs_1.stateExtractField)(body, 'Current Phase') || (0, state_document_cjs_1.stateExtractField)(body, 'Phase') || null;
1735
1787
  }
1736
1788
  // `Phase 4` / `phase 4 of 9` name phase 4 as surely as `4 of 9` does.
1737
- const m = raw === null ? null : /^\s*(?:phase\s+)?(\d+(?:\.\d+)*)/i.exec(raw);
1738
- if (!m)
1789
+ let id = null;
1790
+ if (raw !== null) {
1791
+ id = convention === 'milestone-prefixed'
1792
+ ? parsePhaseFromProse(raw, 'milestone-prefixed').phase
1793
+ : (/^\s*(?:phase\s+)?(\d+(?:\.\d+)*)/i.exec(raw)?.[1] ?? null);
1794
+ }
1795
+ if (id === null)
1739
1796
  return false;
1740
- return comparePhaseNum(m[1], String(phaseNum)) > 0;
1797
+ return comparePhaseNum(id, String(phaseNum)) > 0;
1741
1798
  }
1742
1799
  /**
1743
1800
  * Apply a `completePhase` transition to STATE.md content.