@opengsd/gsd-core 1.9.1 → 1.10.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 (219) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debugger.md +12 -246
  6. package/agents/gsd-executor.md +7 -5
  7. package/agents/gsd-integration-checker.md +3 -0
  8. package/agents/gsd-plan-checker.md +9 -0
  9. package/agents/gsd-planner.md +5 -8
  10. package/agents/gsd-roadmapper.md +21 -3
  11. package/agents/gsd-verifier.md +14 -70
  12. package/bin/install.js +453 -289
  13. package/commands/gsd/mempalace-capture.md +1 -1
  14. package/commands/gsd/new-milestone.md +1 -1
  15. package/commands/gsd/plan-phase.md +1 -1
  16. package/gsd-core/bin/gsd-tools.cjs +579 -66
  17. package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
  18. package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
  19. package/gsd-core/bin/lib/api-coverage.cjs +120 -0
  20. package/gsd-core/bin/lib/audit.cjs +89 -1
  21. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  22. package/gsd-core/bin/lib/capability-registry.cjs +96 -110
  23. package/gsd-core/bin/lib/capability-validator.cjs +12 -2
  24. package/gsd-core/bin/lib/check-command-router.cjs +43 -1
  25. package/gsd-core/bin/lib/command-aliases.cjs +72 -0
  26. package/gsd-core/bin/lib/commands.cjs +26 -25
  27. package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
  28. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  29. package/gsd-core/bin/lib/config.cjs +12 -1
  30. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  31. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  32. package/gsd-core/bin/lib/core-utils.cjs +91 -12
  33. package/gsd-core/bin/lib/docs.cjs +3 -2
  34. package/gsd-core/bin/lib/external-job.cjs +19 -4
  35. package/gsd-core/bin/lib/frontmatter.cjs +84 -12
  36. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  37. package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
  38. package/gsd-core/bin/lib/graphify.cjs +142 -27
  39. package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
  40. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  41. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  42. package/gsd-core/bin/lib/init.cjs +1021 -57
  43. package/gsd-core/bin/lib/install-engine.cjs +64 -10
  44. package/gsd-core/bin/lib/install-profiles.cjs +27 -1
  45. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  46. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  47. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  48. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  49. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  50. package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
  51. package/gsd-core/bin/lib/io.cjs +28 -3
  52. package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
  53. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  54. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  55. package/gsd-core/bin/lib/milestone.cjs +75 -47
  56. package/gsd-core/bin/lib/phase-id.cjs +63 -0
  57. package/gsd-core/bin/lib/phase-locator.cjs +138 -45
  58. package/gsd-core/bin/lib/phase.cjs +260 -28
  59. package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
  60. package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
  61. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  62. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
  63. package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
  64. package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
  65. package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
  66. package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
  67. package/gsd-core/bin/lib/roadmap.cjs +37 -7
  68. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
  69. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
  70. package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
  71. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
  72. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  73. package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
  74. package/gsd-core/bin/lib/smart-entry.cjs +12 -0
  75. package/gsd-core/bin/lib/state-transition.cjs +73 -8
  76. package/gsd-core/bin/lib/state.cjs +151 -62
  77. package/gsd-core/bin/lib/surface.cjs +12 -1
  78. package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
  79. package/gsd-core/bin/lib/uat.cjs +320 -21
  80. package/gsd-core/bin/lib/unusable-input.cjs +9 -0
  81. package/gsd-core/bin/lib/verification.cjs +29 -12
  82. package/gsd-core/bin/lib/verify.cjs +10 -2
  83. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  84. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
  85. package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
  86. package/gsd-core/bin/lib/workstream.cjs +6 -0
  87. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  88. package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
  89. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  90. package/gsd-core/references/artifact-types.md +10 -3
  91. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  92. package/gsd-core/references/debugger-techniques.md +255 -0
  93. package/gsd-core/references/research-documentation-lookup.md +5 -3
  94. package/gsd-core/references/specless-probe-fallback.md +7 -6
  95. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  96. package/gsd-core/references/worktree-branch-check.md +2 -2
  97. package/gsd-core/templates/summary-complex.md +2 -0
  98. package/gsd-core/templates/summary-minimal.md +2 -0
  99. package/gsd-core/templates/summary-standard.md +2 -0
  100. package/gsd-core/templates/summary.md +2 -0
  101. package/gsd-core/workflows/audit-milestone.md +3 -0
  102. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  103. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  104. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  105. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  106. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  107. package/gsd-core/workflows/autonomous.md +32 -69
  108. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  109. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
  110. package/gsd-core/workflows/code-review.md +42 -160
  111. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  112. package/gsd-core/workflows/complete-milestone.md +23 -81
  113. package/gsd-core/workflows/debug.md +9 -12
  114. package/gsd-core/workflows/diagnose-issues.md +22 -0
  115. package/gsd-core/workflows/discovery-phase.md +4 -4
  116. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  117. package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
  118. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  119. package/gsd-core/workflows/docs-update.md +8 -51
  120. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
  121. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  122. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  123. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
  124. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  126. package/gsd-core/workflows/execute-phase.md +65 -137
  127. package/gsd-core/workflows/execute-plan.md +1 -1
  128. package/gsd-core/workflows/help/modes/full.md +6 -1
  129. package/gsd-core/workflows/ingest-docs.md +2 -1
  130. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  131. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  132. package/gsd-core/workflows/new-milestone.md +21 -38
  133. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  134. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  135. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  136. package/gsd-core/workflows/new-project.md +13 -226
  137. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  138. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  139. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  140. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  141. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  142. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  143. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  144. package/gsd-core/workflows/plan-phase.md +49 -193
  145. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  146. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  147. package/gsd-core/workflows/progress.md +11 -153
  148. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  149. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  150. package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
  151. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  152. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  153. package/gsd-core/workflows/quick.md +20 -390
  154. package/gsd-core/workflows/resume-project.md +3 -0
  155. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  156. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  157. package/gsd-core/workflows/review.md +15 -8
  158. package/gsd-core/workflows/section-manifest.json +219 -0
  159. package/gsd-core/workflows/sketch.md +1 -1
  160. package/gsd-core/workflows/spec-phase.md +17 -14
  161. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  162. package/gsd-core/workflows/spike.md +50 -16
  163. package/gsd-core/workflows/sync-skills.md +49 -11
  164. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  165. package/gsd-core/workflows/transition.md +8 -21
  166. package/gsd-core/workflows/ui-phase.md +8 -7
  167. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  168. package/gsd-core/workflows/update.md +18 -7
  169. package/gsd-core/workflows/verify-phase.md +4 -7
  170. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  171. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  172. package/gsd-core/workflows/verify-work.md +8 -58
  173. package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
  174. package/hooks/dist/gsd-check-update-worker.js +14 -5
  175. package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
  176. package/hooks/dist/gsd-read-injection-scanner.js +7 -0
  177. package/hooks/dist/gsd-statusline.js +72 -6
  178. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  179. package/hooks/dist/gsd-write-guard.js +359 -0
  180. package/hooks/dist/lib/isolation-sentinel.js +268 -0
  181. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  182. package/hooks/gsd-agent-isolation-guard.js +428 -0
  183. package/hooks/gsd-check-update-worker.js +14 -5
  184. package/hooks/gsd-cursor-subagent-start.js +532 -26
  185. package/hooks/gsd-read-injection-scanner.js +7 -0
  186. package/hooks/gsd-statusline.js +72 -6
  187. package/hooks/gsd-worktree-path-guard.js +2 -1
  188. package/hooks/gsd-write-guard.js +359 -0
  189. package/hooks/hooks.json +12 -0
  190. package/hooks/lib/isolation-sentinel.js +268 -0
  191. package/hooks/managed-hooks-registry.cjs +2 -0
  192. package/package.json +14 -5
  193. package/pi/gsd.cjs +57 -12
  194. package/scripts/build-hooks.js +9 -0
  195. package/scripts/changeset/lint.cjs +9 -2
  196. package/scripts/changeset/serialize.cjs +5 -1
  197. package/scripts/gen-capability-matrix.cjs +1 -1
  198. package/scripts/gen-context-index.cjs +448 -0
  199. package/scripts/gen-inventory-manifest.cjs +101 -1
  200. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  201. package/scripts/gen-section-manifest.cjs +638 -0
  202. package/scripts/generate-package-identity.cjs +4 -2
  203. package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
  204. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  205. package/scripts/lint-docs-command-form.cjs +195 -0
  206. package/scripts/lint-docs-required.cjs +9 -1
  207. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  208. package/scripts/lint-example-parser-parity.cjs +395 -0
  209. package/scripts/lint-test-file-count.allowlist.json +27 -1
  210. package/scripts/mutation-matrix.cjs +13 -0
  211. package/scripts/prompt-injection-scan.sh +27 -6
  212. package/scripts/run-tests.cjs +3 -2
  213. package/skills/gsd-autonomous/SKILL.md +1 -1
  214. package/skills/gsd-execute-phase/SKILL.md +1 -1
  215. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  216. package/skills/gsd-new-milestone/SKILL.md +1 -1
  217. package/skills/gsd-plan-phase/SKILL.md +2 -2
  218. package/vscode/package.json +1 -1
  219. package/scripts/gen-emitted-baseline.cjs +0 -145
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.pickRollupWinners = pickRollupWinners;
15
16
  exports.isCompletedInventory = isCompletedInventory;
16
17
  exports.buildWorkstreamInventory = buildWorkstreamInventory;
17
18
  const node_path_1 = __importDefault(require("node:path"));
@@ -19,6 +20,61 @@ const node_path_1 = __importDefault(require("node:path"));
19
20
  function toPosixPath(p) {
20
21
  return p.split('\\').join('/');
21
22
  }
23
+ /**
24
+ * #2562: verification verdicts that DISQUALIFY a phase from `complete`, even
25
+ * when its SUMMARY count meets its PLAN count. Deliberately scoped to the two
26
+ * EXPLICIT failing verdicts the verifier emits — `missing`/`unknown` (verifier
27
+ * off / not yet run) and `stale` (mtime-derived, #2348) are intentionally left
28
+ * untouched so verifier-disabled projects do not regress to never-complete.
29
+ *
30
+ * #2645: `'unrecorded'` is NOT a verdict the verifier itself ever emits — it
31
+ * is an internal sentinel `workstream-inventory.cts`'s verification-deletion
32
+ * ledger substitutes for `'missing'` once that workstream has ADOPTED the
33
+ * ledger (a `.verification-ledger.json` file exists for it) but has no
34
+ * remembered entry for this specific phase. Pre-adoption (no ledger file at
35
+ * all) still resolves to plain `'missing'`, which stays OUTSIDE this set —
36
+ * that is what keeps a project untouched by this fix until it actually uses
37
+ * the verifier at least once. Post-adoption, an unrecorded phase fails
38
+ * CLOSED (counted here) rather than open, so a corrupt or evidence-absent
39
+ * ledger entry can no longer be read as "safe to complete" the way a bare
40
+ * `'missing'` sentinel is.
41
+ */
42
+ const FAILING_VERIFICATION_STATUSES = new Set(['gaps_found', 'human_needed', 'unrecorded']);
43
+ /**
44
+ * #2562 / Bug #2445 / #2645 review: pick ONE winning item per key from a
45
+ * PRE-SORTED list — newest `mtimeMs` wins; on an exact tie the incumbent
46
+ * (first-in-sort-order) wins, since only a STRICTLY greater mtime replaces
47
+ * it. `includeItem` lets a caller exclude items before comparison (e.g.
48
+ * out-of-milestone directories) — critically, the filter runs BEFORE the
49
+ * mtime comparison, so an excluded item can never win a tie or a comparison
50
+ * against an included one.
51
+ *
52
+ * Extracted as the SINGLE shared implementation after a #2645 review found
53
+ * two independently-written copies of this exact rule had silently
54
+ * diverged: `workstream-inventory.cts`'s ledger-winner selection compared
55
+ * raw mtimes with no scoping filter, while this module's own `rollupDirByKey`
56
+ * (below) filtered out-of-milestone directories first. A stale out-of-
57
+ * milestone directory with a newer mtime than the live in-milestone one
58
+ * (plausible after a checkout/rebase resets mtimes) could then win the
59
+ * LEDGER's selection while losing the BUILDER's — reopening #2645's own
60
+ * hole for the phase that actually counts toward `completed_phases`,
61
+ * reachable with a plain `rm` and no ledger tampering. A comment asserting
62
+ * two hand-written copies "use the same rule" is not a guarantee they do;
63
+ * one shared function is.
64
+ */
65
+ function pickRollupWinners(sortedItems, keyOf, mtimeOf, includeItem = () => true) {
66
+ const winners = new Map();
67
+ for (const item of sortedItems) {
68
+ if (!includeItem(item))
69
+ continue;
70
+ const key = keyOf(item);
71
+ const incumbent = winners.get(key);
72
+ if (incumbent === undefined || mtimeOf(item) > mtimeOf(incumbent)) {
73
+ winners.set(key, item);
74
+ }
75
+ }
76
+ return winners;
77
+ }
22
78
  function isCompletedInventory(status) {
23
79
  const s = (typeof status === 'string'
24
80
  ? status
@@ -28,42 +84,144 @@ function isCompletedInventory(status) {
28
84
  return /\bmilestone\s+complete\b/.test(s) || /\barchived\b/.test(s);
29
85
  }
30
86
  function buildWorkstreamInventory(inputs) {
31
- const { name, projectDir, workstreamDir, phaseDirNames, activeWorkstreamName, phaseFilesCounts, roadmapPhaseCount, stateProjection, filesExist, milestoneShipped, } = inputs;
87
+ const { name, projectDir, workstreamDir, phaseDirNames, activeWorkstreamName, phaseFilesCounts, roadmapPhaseCount, stateProjection, filesExist, milestoneShipped, milestoneShippedSignal, currentMilestonePhaseCount = 0, milestoneScoped, } = inputs;
88
+ // A caller that passes only the legacy boolean states THAT a signal fired but
89
+ // not which one. Treat it as `legacy` — the ungated strength — so pre-review
90
+ // callers keep their exact behavior rather than silently acquiring a new gate.
91
+ const shippedSignal = milestoneShippedSignal !== undefined ? milestoneShippedSignal : (milestoneShipped ? 'legacy' : null);
92
+ const milestoneShippedResolved = shippedSignal !== null;
93
+ // #2562: when scoping is active, prior-milestone phase directories are
94
+ // excluded from the completion rollup and the denominator. The caller states
95
+ // this; the count-derived default is the pre-#2562 fallback for callers that
96
+ // do not, and cannot represent a scoped-but-empty current milestone.
97
+ const scoped = milestoneScoped ?? currentMilestonePhaseCount > 0;
32
98
  // Index counts by directory for O(1) lookup during sort/iteration
33
99
  const countsMap = new Map();
34
100
  for (const entry of phaseFilesCounts) {
35
- countsMap.set(entry.directory, { planCount: entry.planCount, summaryCount: entry.summaryCount });
101
+ countsMap.set(entry.directory, entry);
36
102
  }
103
+ // #2562 / Bug #2445: pick ONE directory per phase key for the rollup. Stale
104
+ // same-numbered directories left over from a prior milestone would otherwise
105
+ // each add to the numerator while the denominator counts distinct phases —
106
+ // pushing completed_phases past it, where the old `Math.min` cap silently
107
+ // rounded the result up to 100% and hid an unstarted phase. Newest-on-disk
108
+ // wins, mirroring state.cts's #2445 de-duplication. `pickRollupWinners` is
109
+ // the SHARED implementation `workstream-inventory.cts`'s ledger-winner
110
+ // selection also calls, so the two can never independently diverge again
111
+ // (#2645 review).
112
+ const rollupDirByKey = pickRollupWinners([...phaseDirNames].sort(), (dir) => countsMap.get(dir)?.phaseKey ?? dir, (dir) => countsMap.get(dir)?.mtimeMs ?? 0, (dir) => !(scoped && countsMap.get(dir)?.inMilestone === false));
113
+ const rollupDirs = new Set(rollupDirByKey.values());
37
114
  const phases = [];
38
115
  let completedPhases = 0;
39
116
  let totalPlans = 0;
40
117
  let completedPlans = 0;
118
+ // #2562 review: in-milestone phase directories still present under `phases/`,
119
+ // whatever their status. A CLEAN archive has none — `milestone complete` moves
120
+ // them all out — so this counts exactly the phases that outlived the archive,
121
+ // which is what distinguishes "archived" from "archived, then reopened".
122
+ // Deliberately NOT "…and unfinished": a complete live dir beside a declared
123
+ // but never-scaffolded phase is a dirty archive too, and the dirless phase has
124
+ // no directory to inspect.
125
+ let liveInMilestonePhases = 0;
41
126
  for (const dir of [...phaseDirNames].sort()) {
42
- const counts = countsMap.get(dir) ?? { planCount: 0, summaryCount: 0 };
43
- const status = counts.summaryCount >= counts.planCount && counts.planCount > 0
127
+ const counts = countsMap.get(dir);
128
+ const planCount = counts?.planCount ?? 0;
129
+ const summaryCount = counts?.summaryCount ?? 0;
130
+ // #2562: SUMMARY≥PLAN parity is necessary but not sufficient — a phase whose
131
+ // verification verdict is an explicit failing one is still in progress.
132
+ const verificationStatus = counts?.verificationStatus ?? 'missing';
133
+ const summariesMeetPlans = summaryCount >= planCount && planCount > 0;
134
+ const status = summariesMeetPlans && !FAILING_VERIFICATION_STATUSES.has(verificationStatus)
44
135
  ? 'complete'
45
- : counts.planCount > 0
136
+ : planCount > 0
46
137
  ? 'in_progress'
47
138
  : 'pending';
48
- totalPlans += counts.planCount;
49
- completedPlans += Math.min(counts.summaryCount, counts.planCount);
50
- if (status === 'complete')
51
- completedPhases++;
139
+ // #2562: only current-milestone phases feed the rollup when scoping is on,
140
+ // and only one directory per phase key (see rollupDirs above).
141
+ const countsTowardMilestone = (!scoped || counts?.inMilestone !== false) && rollupDirs.has(dir);
142
+ if (countsTowardMilestone) {
143
+ totalPlans += planCount;
144
+ completedPlans += Math.min(summaryCount, planCount);
145
+ if (status === 'complete')
146
+ completedPhases++;
147
+ liveInMilestonePhases++;
148
+ }
52
149
  phases.push({
53
150
  directory: dir,
54
151
  status,
55
- plan_count: counts.planCount,
56
- summary_count: counts.summaryCount,
152
+ plan_count: planCount,
153
+ summary_count: summaryCount,
57
154
  });
58
155
  }
156
+ // #2562: the denominator is the current milestone's declared phase count when
157
+ // scoping is active (catches phases declared but never scaffolded), else the
158
+ // legacy whole-roadmap heading count.
159
+ const effectivePhaseCount = scoped ? currentMilestonePhaseCount : roadmapPhaseCount;
160
+ // #2562 invariant: the numerator counts de-duplicated in-milestone phase keys
161
+ // and the denominator counts the union of those keys with the roadmap's own
162
+ // declarations, so the numerator can never exceed it. Raising the numerator
163
+ // above the denominator means the two sides were derived in different key
164
+ // spaces — the defect class this issue is about. The old `Math.min(100, …)`
165
+ // capped that away and reported 100%; this makes it fail loudly instead.
166
+ if (scoped && completedPhases > effectivePhaseCount) {
167
+ throw new Error(`workstream inventory invariant violated for "${name}": completed_phases (${completedPhases}) ` +
168
+ `exceeds the current-milestone denominator (${effectivePhaseCount}). The completion numerator and ` +
169
+ `denominator were derived in different phase-key spaces.`);
170
+ }
59
171
  // #1913: derive status from authoritative shipped signals rather than trusting
60
172
  // the mutable STATE.md `Status` field. When a shipped signal is present, the
61
173
  // workstream is "milestone complete" regardless of a stale field value.
174
+ //
175
+ // #2562 review: a shipped signal is a CLAIM, and a claim its own milestone's
176
+ // artifacts contradict must not be echoed as fact — the defect class this issue
177
+ // is about reaches `status`, not just `progress_percent`. The two signals need
178
+ // DIFFERENT cross-checks; one check for both regresses the commonest shape:
179
+ //
180
+ // - `heading` — operator-typed marker in the LIVE roadmap. Nothing has been
181
+ // archived, so every phase the milestone declares should be on disk and
182
+ // complete. Gate on the full ratio, which also catches the
183
+ // declared-but-never-scaffolded phases that have no directory to inspect.
184
+ // - `snapshot` — `milestones/<version>-ROADMAP.md`. The `milestone complete`
185
+ // run that writes it also MOVES the milestone's phase directories into
186
+ // `milestones/<version>-phases/` (milestone.cts:783-790) while COPYING —
187
+ // never truncating — the live ROADMAP (:700-702), so its Progress rows
188
+ // survive. A CLEAN archive therefore reads 0/N by construction, and gating
189
+ // it on the ratio alone would strip `milestone complete` from every
190
+ // archived milestone. But a live in-milestone directory means the archive
191
+ // is NOT clean — a phase was added or reopened after it, reachable because
192
+ // `milestone complete` does not advance STATE's `milestone:` field
193
+ // (state-transition.cts:83, :1335); only `/gsd-new-milestone` does (:1224).
194
+ // Once any in-milestone directory is live the ratio IS meaningful again, so
195
+ // the check is the conjunction. Requiring the live directory to itself be
196
+ // unfinished was too narrow: it let a complete live dir alongside a
197
+ // declared-but-unscaffolded phase reproduce the reported symptom, since a
198
+ // dirless phase has nothing to inspect.
199
+ //
200
+ // The whole cross-check is scoped-only, and NOT because of the signal: when
201
+ // scoping is off, `effectivePhaseCount` is the whole-roadmap count and
202
+ // membership is everything, so there is no current-milestone artifact set to
203
+ // check a current-milestone claim against. `legacy` is additionally ungated by
204
+ // signal — it is the fallback for an unknown milestone version, which is
205
+ // exactly when scoping cannot engage either.
62
206
  const fieldStatus = stateProjection.status;
63
- const useDerived = milestoneShipped;
64
- const status = useDerived ? 'milestone complete' : fieldStatus;
65
- const status_source = useDerived ? 'derived' : 'field';
66
- const status_conflict = useDerived && !isCompletedInventory(fieldStatus);
207
+ const shippedContradicted = scoped && (shippedSignal === 'heading'
208
+ ? completedPhases < effectivePhaseCount
209
+ : shippedSignal === 'snapshot'
210
+ ? liveInMilestonePhases > 0 && completedPhases < effectivePhaseCount
211
+ : false);
212
+ const useDerived = milestoneShippedResolved && !shippedContradicted;
213
+ // Refusing the claim does not make the STATE field a safe fallback: it is
214
+ // operator-written and in this window it commonly ALSO reads "milestone
215
+ // complete", which would re-report the refused claim through the other door.
216
+ // Against contradicting artifacts, NEITHER source may assert completion.
217
+ const artifactOverride = shippedContradicted && isCompletedInventory(fieldStatus);
218
+ const status = useDerived
219
+ ? 'milestone complete'
220
+ : artifactOverride
221
+ ? 'in_progress'
222
+ : fieldStatus;
223
+ const status_source = useDerived || artifactOverride ? 'derived' : 'field';
224
+ const status_conflict = (useDerived && !isCompletedInventory(fieldStatus)) || artifactOverride;
67
225
  return {
68
226
  name,
69
227
  path: toPosixPath(node_path_1.default.relative(projectDir, workstreamDir)),
@@ -76,16 +234,21 @@ function buildWorkstreamInventory(inputs) {
76
234
  status,
77
235
  status_source,
78
236
  status_conflict,
237
+ milestone_shipped_unverified: shippedContradicted,
79
238
  current_phase: stateProjection.current_phase,
80
239
  last_activity: stateProjection.last_activity,
81
240
  phases,
82
241
  phase_count: phases.length,
83
242
  completed_phases: completedPhases,
84
- roadmap_phase_count: roadmapPhaseCount,
243
+ roadmap_phase_count: effectivePhaseCount,
85
244
  total_plans: totalPlans,
86
245
  completed_plans: completedPlans,
87
- progress_percent: roadmapPhaseCount > 0
88
- ? Math.min(100, Math.round((completedPhases / roadmapPhaseCount) * 100))
246
+ // The `Math.min` cap is unreachable under milestone scoping (the invariant
247
+ // above throws first) and survives only for the legacy unscoped path, where
248
+ // the denominator is a roadmap heading count that a caller cannot guarantee
249
+ // bounds the numerator.
250
+ progress_percent: effectivePhaseCount > 0
251
+ ? Math.min(100, Math.round((completedPhases / effectivePhaseCount) * 100))
89
252
  : 0,
90
253
  };
91
254
  }