@opengsd/gsd-core 1.8.0 → 1.9.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 (177) 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 +31 -1
  4. package/agents/gsd-code-fixer.md +107 -34
  5. package/agents/gsd-codebase-mapper.md +1 -1
  6. package/agents/gsd-debug-session-manager.md +36 -0
  7. package/agents/gsd-executor.md +20 -7
  8. package/agents/gsd-intel-updater.md +3 -3
  9. package/agents/gsd-phase-researcher.md +4 -2
  10. package/agents/gsd-plan-checker.md +20 -0
  11. package/agents/gsd-planner.md +15 -23
  12. package/agents/gsd-project-researcher.md +2 -2
  13. package/agents/gsd-ui-auditor.md +0 -40
  14. package/bin/install.js +236 -107
  15. package/commands/gsd/plan-review-convergence.md +5 -1
  16. package/gsd-core/bin/gsd-tools.cjs +882 -4
  17. package/gsd-core/bin/lib/api-coverage.cjs +22 -8
  18. package/gsd-core/bin/lib/audit.cjs +8 -8
  19. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  20. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  21. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  22. package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
  23. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  24. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  25. package/gsd-core/bin/lib/check-command-router.cjs +12 -2
  26. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  27. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
  28. package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
  29. package/gsd-core/bin/lib/commands.cjs +246 -18
  30. package/gsd-core/bin/lib/config-loader.cjs +200 -28
  31. package/gsd-core/bin/lib/config.cjs +90 -5
  32. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  33. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  34. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  35. package/gsd-core/bin/lib/init.cjs +44 -19
  36. package/gsd-core/bin/lib/install-engine.cjs +1 -0
  37. package/gsd-core/bin/lib/milestone.cjs +36 -9
  38. package/gsd-core/bin/lib/model-catalog.cjs +51 -1
  39. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  40. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  41. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  42. package/gsd-core/bin/lib/phase-id.cjs +278 -5
  43. package/gsd-core/bin/lib/phase.cjs +61 -6
  44. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  45. package/gsd-core/bin/lib/plan-scan.cjs +1 -1
  46. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  47. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  48. package/gsd-core/bin/lib/project-root.cjs +48 -0
  49. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  50. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  51. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  52. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  53. package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
  54. package/gsd-core/bin/lib/roadmap.cjs +10 -4
  55. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
  56. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
  57. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
  58. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  59. package/gsd-core/bin/lib/smart-entry.cjs +1 -1
  60. package/gsd-core/bin/lib/state-document.cjs +164 -20
  61. package/gsd-core/bin/lib/state-transition.cjs +28 -10
  62. package/gsd-core/bin/lib/state.cjs +141 -21
  63. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  64. package/gsd-core/bin/lib/uat.cjs +9 -7
  65. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  66. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  67. package/gsd-core/bin/lib/validate.cjs +32 -0
  68. package/gsd-core/bin/lib/verification.cjs +51 -14
  69. package/gsd-core/bin/lib/verify.cjs +146 -22
  70. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  71. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  72. package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
  73. package/gsd-core/bin/shared/model-catalog.json +5 -0
  74. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  75. package/gsd-core/references/context-budget.md +40 -0
  76. package/gsd-core/references/gate-prompts.md +6 -3
  77. package/gsd-core/references/model-profile-resolution.md +64 -13
  78. package/gsd-core/references/offer-next.md +88 -0
  79. package/gsd-core/references/planning-config.md +2 -1
  80. package/gsd-core/references/reviewer-instances.md +28 -21
  81. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  82. package/gsd-core/references/ui-consideration-probe.md +2 -2
  83. package/gsd-core/references/worktree-branch-check.md +4 -4
  84. package/gsd-core/templates/summary-minimal.md +4 -0
  85. package/gsd-core/templates/summary-standard.md +4 -0
  86. package/gsd-core/templates/summary.md +7 -0
  87. package/gsd-core/workflows/ai-integration-phase.md +4 -4
  88. package/gsd-core/workflows/audit-fix.md +4 -0
  89. package/gsd-core/workflows/audit-milestone.md +8 -0
  90. package/gsd-core/workflows/autonomous.md +19 -15
  91. package/gsd-core/workflows/check-todos.md +2 -2
  92. package/gsd-core/workflows/code-review-fix.md +14 -6
  93. package/gsd-core/workflows/code-review.md +93 -21
  94. package/gsd-core/workflows/debug.md +10 -2
  95. package/gsd-core/workflows/diagnose-issues.md +4 -0
  96. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  97. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  98. package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
  99. package/gsd-core/workflows/discuss-phase.md +2 -2
  100. package/gsd-core/workflows/docs-update.md +8 -0
  101. package/gsd-core/workflows/eval-review.md +1 -1
  102. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  103. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  104. package/gsd-core/workflows/execute-phase.md +85 -115
  105. package/gsd-core/workflows/execute-plan.md +5 -4
  106. package/gsd-core/workflows/explore.md +4 -0
  107. package/gsd-core/workflows/extract-learnings.md +21 -0
  108. package/gsd-core/workflows/help/modes/full.md +3 -3
  109. package/gsd-core/workflows/import.md +4 -1
  110. package/gsd-core/workflows/ingest-docs.md +4 -0
  111. package/gsd-core/workflows/map-codebase.md +13 -6
  112. package/gsd-core/workflows/new-milestone.md +10 -2
  113. package/gsd-core/workflows/new-project.md +11 -4
  114. package/gsd-core/workflows/next.md +5 -2
  115. package/gsd-core/workflows/plan-phase.md +42 -46
  116. package/gsd-core/workflows/plan-review-convergence.md +18 -14
  117. package/gsd-core/workflows/progress.md +1 -1
  118. package/gsd-core/workflows/quick.md +14 -3
  119. package/gsd-core/workflows/review.md +146 -575
  120. package/gsd-core/workflows/scan.md +9 -1
  121. package/gsd-core/workflows/secure-phase.md +10 -2
  122. package/gsd-core/workflows/ship.md +41 -11
  123. package/gsd-core/workflows/smart-entry.md +1 -1
  124. package/gsd-core/workflows/ui-phase.md +8 -1
  125. package/gsd-core/workflows/ui-review.md +8 -1
  126. package/gsd-core/workflows/update.md +104 -5
  127. package/gsd-core/workflows/validate-phase.md +10 -2
  128. package/gsd-core/workflows/verify-work.md +8 -1
  129. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  130. package/hooks/dist/gsd-cursor-stop.js +6 -2
  131. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  132. package/hooks/dist/gsd-graphify-update.sh +9 -0
  133. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  134. package/hooks/dist/gsd-prompt-guard.js +101 -2
  135. package/hooks/dist/gsd-read-guard.js +100 -2
  136. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  137. package/hooks/dist/gsd-statusline.js +9 -6
  138. package/hooks/dist/gsd-workflow-guard.js +110 -6
  139. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  140. package/hooks/dist/lib/cursor-workspace.js +74 -0
  141. package/hooks/gsd-cursor-session-start.js +6 -2
  142. package/hooks/gsd-cursor-stop.js +6 -2
  143. package/hooks/gsd-cursor-subagent-start.js +6 -2
  144. package/hooks/gsd-graphify-update.sh +9 -0
  145. package/hooks/gsd-phase-boundary.sh +14 -2
  146. package/hooks/gsd-prompt-guard.js +101 -2
  147. package/hooks/gsd-read-guard.js +100 -2
  148. package/hooks/gsd-read-injection-scanner.js +109 -2
  149. package/hooks/gsd-statusline.js +9 -6
  150. package/hooks/gsd-workflow-guard.js +110 -6
  151. package/hooks/gsd-worktree-path-guard.js +132 -8
  152. package/hooks/lib/cursor-workspace.js +74 -0
  153. package/package.json +7 -7
  154. package/pi/gsd.cjs +26 -1
  155. package/scripts/check-coverage-gate.cjs +51 -0
  156. package/scripts/check-glossary-refs.cjs +24 -0
  157. package/scripts/ci-test-scope.cjs +67 -17
  158. package/scripts/gen-adr-index.cjs +6 -4
  159. package/scripts/gen-capability-matrix.cjs +26 -2
  160. package/scripts/gen-capability-registry.cjs +132 -34
  161. package/scripts/gen-emitted-baseline.cjs +145 -0
  162. package/scripts/gen-registry.cjs +39 -15
  163. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  164. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  165. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  166. package/scripts/lint-resolution-provenance.cjs +9 -0
  167. package/scripts/mutation-matrix.cjs +4 -0
  168. package/scripts/prompt-injection-scan.sh +6 -0
  169. package/scripts/registry-schema.cjs +372 -94
  170. package/scripts/release-notes/conventional-title.cjs +19 -1
  171. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  172. package/scripts/validate-registry.cjs +10 -6
  173. package/scripts/workflow-size.cjs +16 -8
  174. package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
  175. package/vscode/package.json +1 -1
  176. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  177. package/scripts/update-size-baseline.cjs +0 -68
@@ -63,14 +63,35 @@ const { MODEL_PROFILES } = modelProfilesMod;
63
63
  // Unused but imported for structural parity
64
64
  void stripShippedMilestones;
65
65
  void schema_detect_cjs_1.detectSchemaFiles;
66
- function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
67
- if (!summaryPath) {
68
- error('summary-path required');
69
- }
66
+ /**
67
+ * Pure core of `verify-summary` (#2572).
68
+ *
69
+ * Same artifact↔git checks the CLI verb has always run, lifted out of the
70
+ * `output()` wrapper so other verbs can consume the structured
71
+ * `{ passed, checks, errors }` contract directly instead of shelling out and
72
+ * re-parsing JSON. `cmdVerifySummary` is now a thin adapter over this.
73
+ *
74
+ * Never throws and never writes to stdout: a missing SUMMARY, a non-repo, or an
75
+ * unresolvable commit all come back as structured `false`/`missing` values.
76
+ *
77
+ * Caveat for callers surfacing `commits_exist`: the hash pattern is a loose
78
+ * `\b[0-9a-f]{7,40}\b`, so any hex-shaped token in the prose counts as a
79
+ * candidate. That is cheap as an advisory signal and unacceptable as a gate.
80
+ *
81
+ * @param checkFileCount How many extracted candidates to probe. Defaults to 2 —
82
+ * the value the CLI verb has always used. Pass `Infinity` to probe every
83
+ * candidate (see `cmdPhaseComplete`, which reports on all of them).
84
+ * @param opts.checkCommits When `false`, the `git cat-file` probes are skipped
85
+ * entirely and `commits_exist` comes back `false` meaning *not checked*.
86
+ * Callers that do not surface `commits_exist` should pass `false` so this
87
+ * stays a pure-filesystem check with no subprocess cost.
88
+ */
89
+ function verifySummaryCore(cwd, summaryPath, checkFileCount, opts) {
70
90
  const fullPath = node_path_1.default.join(cwd, summaryPath);
71
91
  const checkCount = checkFileCount || 2;
92
+ const checkCommits = opts?.checkCommits !== false;
72
93
  if (!node_fs_1.default.existsSync(fullPath)) {
73
- const result = {
94
+ return {
74
95
  passed: false,
75
96
  checks: {
76
97
  summary_exists: false,
@@ -80,34 +101,104 @@ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
80
101
  },
81
102
  errors: ['SUMMARY.md not found'],
82
103
  };
83
- output(result, raw, 'failed');
84
- return;
85
104
  }
86
105
  const content = node_fs_1.default.readFileSync(fullPath, 'utf-8');
87
106
  const errors = [];
107
+ const projectRoot = node_path_1.default.resolve(cwd);
108
+ /**
109
+ * Is `candidate` plausibly a repo-relative file this check should probe?
110
+ *
111
+ * Deliberately narrowing. This is an ADVISORY, so the two error directions are
112
+ * not symmetric: a false positive tells a user their healthy project is
113
+ * missing a file that was never claimed, while a false negative just means one
114
+ * reference goes unprobed. Every rejection below is a noise class confirmed on
115
+ * #2685; when in doubt, skip rather than warn.
116
+ */
117
+ const isProbableProjectFile = (candidate) => {
118
+ // Only repo-relative paths — a bare filename is too ambiguous to locate.
119
+ if (!candidate.includes('/'))
120
+ return false;
121
+ // URLs, protocol-relative links, and any other scheme.
122
+ if (candidate.startsWith('http') || candidate.startsWith('//'))
123
+ return false;
124
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(candidate))
125
+ return false;
126
+ // Globs name a set, not a file: `src/**/*.cts` is never "missing".
127
+ if (/[*?]/.test(candidate))
128
+ return false;
129
+ // Bare hostnames (`docs.example.com/guide.html`). A repo-relative path's
130
+ // first segment is a directory name, which in practice contains a dot only
131
+ // when it is a dotfile directory (`.github/`, `.changeset/`, `.planning/`)
132
+ // — i.e. the dot is at index 0. A dot anywhere later marks a hostname.
133
+ const firstSegment = candidate.split('/')[0] || '';
134
+ if (firstSegment.indexOf('.') > 0)
135
+ return false;
136
+ // Containment guard: a `../`-bearing reference must not turn this advisory
137
+ // into a filesystem existence probe outside the project.
138
+ const resolved = node_path_1.default.resolve(projectRoot, candidate);
139
+ if (resolved !== projectRoot && !resolved.startsWith(projectRoot + node_path_1.default.sep))
140
+ return false;
141
+ return true;
142
+ };
143
+ // Pattern 2 excludes `[` and `]` from its path class (#2685 Blocker 1). All
144
+ // three SUMMARY templates prescribe a YAML flow sequence for `key-files`:
145
+ //
146
+ // key-files:
147
+ // created: [src/auth/login.ts, src/auth/session.ts]
148
+ //
149
+ // and the label matches `(?:Created|Modified|…):` case-insensitively. Without
150
+ // the bracket exclusion the class captures the literal `[` as part of the
151
+ // first path, yielding `[src/auth/login.ts` — a candidate that can never exist
152
+ // on disk. That fired on healthy projects built from GSD's own shipped
153
+ // template. The exclusion also stops a markdown list in the body from
154
+ // reintroducing the same artifact.
155
+ //
156
+ // Stripping frontmatter first was the other remedy offered on #2685. It is a
157
+ // verified no-op on top of this exclusion — measured identical extraction
158
+ // across all three shipped templates — because the exclusion already makes a
159
+ // flow-sequence line contribute nothing. Consequence worth naming: the
160
+ // `key-files` block, the most authoritative statement of what a phase created,
161
+ // is still not read. Recovering it needs a real frontmatter parse, which is
162
+ // deliberately left as a follow-up rather than smuggled in here.
88
163
  const mentionedFiles = new Set();
164
+ // #2844: Pattern 1 matches any backticked path-like token. A SUMMARY body is
165
+ // predominantly about what the phase DID, so a backticked path in prose ("Built
166
+ // `src/kept.ts`", a `- \`src/x.ts\`` list item) is a legitimate claim (#2685
167
+ // pins this). The false-positive class #2844 fixes is a path mentioned as a
168
+ // FUTURE/CONDITIONAL deliverable — "next phase will add `shared/types.ts`",
169
+ // "planned", "would", "to be created" — which is NOT a claim about this phase.
170
+ // Exclude those lines rather than requiring an explicit claim verb (which would
171
+ // drop the legitimate "Built …" / list-item forms #2685 protects).
172
+ const isFutureMention = (line) => /\b(?:will(?:\s+(?:add|create|build|land))?(?:[^.])?|(?:next|later|future)\s+phase|planned?|would\s+(?:be|add|create|build)|to\s+be\s+(?:added|created|built)|eventually|not\s+yet)\b/i.test(line);
89
173
  const patterns = [
90
174
  /`([^`]+\.[a-zA-Z]+)`/g,
91
- /(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`]+\.[a-zA-Z]+)`?/gi,
175
+ /(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`[\]]+\.[a-zA-Z]+)`?/gi,
92
176
  ];
93
177
  for (const pattern of patterns) {
94
178
  let m;
95
179
  while ((m = pattern.exec(content)) !== null) {
96
180
  const filePath = m[1];
97
- if (filePath && !filePath.startsWith('http') && filePath.includes('/')) {
98
- mentionedFiles.add(filePath);
99
- }
181
+ if (!filePath || !isProbableProjectFile(filePath))
182
+ continue;
183
+ // #2844: skip a backticked path on a future/conditional line — it names a
184
+ // deliverable this phase did NOT produce, so probing it is a false positive.
185
+ const lineStart = content.lastIndexOf('\n', m.index) + 1;
186
+ const lineEnd = content.indexOf('\n', m.index);
187
+ const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
188
+ if (isFutureMention(line))
189
+ continue;
190
+ mentionedFiles.add(filePath);
100
191
  }
101
192
  }
102
193
  const filesToCheck = Array.from(mentionedFiles).slice(0, checkCount);
103
194
  const missing = [];
104
195
  for (const file of filesToCheck) {
105
- if (!node_fs_1.default.existsSync(node_path_1.default.join(cwd, file))) {
196
+ if (!node_fs_1.default.existsSync(node_path_1.default.resolve(projectRoot, file))) {
106
197
  missing.push(file);
107
198
  }
108
199
  }
109
200
  const commitHashPattern = /\b[0-9a-f]{7,40}\b/g;
110
- const hashes = content.match(commitHashPattern) || [];
201
+ const hashes = checkCommits ? content.match(commitHashPattern) || [] : [];
111
202
  let commitsExist = false;
112
203
  if (hashes.length > 0) {
113
204
  for (const hash of hashes.slice(0, 3)) {
@@ -144,8 +235,15 @@ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
144
235
  self_check: selfCheck,
145
236
  };
146
237
  const passed = missing.length === 0 && selfCheck !== 'failed';
147
- const result = { passed, checks, errors };
148
- output(result, raw, passed ? 'passed' : 'failed');
238
+ return { passed, checks, errors };
239
+ }
240
+ /** CLI adapter over verifySummaryCore — arg guard + output shaping only. */
241
+ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
242
+ if (!summaryPath) {
243
+ error('summary-path required');
244
+ }
245
+ const result = verifySummaryCore(cwd, summaryPath, checkFileCount);
246
+ output(result, raw, result.passed ? 'passed' : 'failed');
149
247
  }
150
248
  /**
151
249
  * Issue #429 — negative-grep comment-text echo gate.
@@ -649,13 +747,24 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
649
747
  if (!filePath) {
650
748
  error('file path required');
651
749
  }
750
+ if (filePath.includes('\0')) {
751
+ error('file path contains null bytes');
752
+ }
652
753
  const fullPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.join(cwd, filePath);
653
754
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
654
755
  if (!content) {
655
756
  output({ error: 'File not found', path: filePath }, raw);
656
757
  return;
657
758
  }
658
- const fm = extractFrontmatter(content);
759
+ // #2701: fail loud on NUL/binary corruption before structure checks. A
760
+ // structurally intact-but-NUL-corrupted plan otherwise passes as valid and is
761
+ // silently skipped by recursive/binary-skipping searchers downstream.
762
+ const encErr = (0, validate_cjs_2.textEncodingError)(content, filePath);
763
+ if (encErr) {
764
+ output({ valid: false, errors: [encErr] }, raw);
765
+ return;
766
+ }
767
+ const fm = extractFrontmatter(content, fullPath);
659
768
  const errors = [];
660
769
  const warnings = [];
661
770
  const required = ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'];
@@ -909,7 +1018,7 @@ function collectPromisedFilesAtOrAfterWave(phaseDir, minWave) {
909
1018
  const planContent = (0, shell_command_projection_cjs_1.platformReadSync)(planFullPath);
910
1019
  if (!planContent)
911
1020
  continue;
912
- const fm = extractFrontmatter(planContent);
1021
+ const fm = extractFrontmatter(planContent, planFullPath);
913
1022
  const waveRaw = fm['wave'];
914
1023
  const wave = typeof waveRaw === 'string' ? parseInt(waveRaw, 10) : (typeof waveRaw === 'number' ? waveRaw : NaN);
915
1024
  if (isNaN(wave) || wave < minWave)
@@ -944,7 +1053,7 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
944
1053
  }
945
1054
  // Derive the current plan's wave number and phase directory for wave-aware
946
1055
  // missing-file handling (fix #1202).
947
- const currentFm = extractFrontmatter(content);
1056
+ const currentFm = extractFrontmatter(content, fullPath);
948
1057
  const currentWaveRaw = currentFm['wave'];
949
1058
  const currentWave = typeof currentWaveRaw === 'string'
950
1059
  ? parseInt(currentWaveRaw, 10)
@@ -1041,8 +1150,16 @@ function listMilestoneArchiveDirs(planBase) {
1041
1150
  .map((e) => node_path_1.default.join(milestonesDir, e.name))
1042
1151
  .sort((a, b) => node_path_1.default.basename(a).localeCompare(node_path_1.default.basename(b), undefined, { numeric: true }));
1043
1152
  }
1044
- catch {
1045
- return [];
1153
+ catch (err) {
1154
+ // #1883: distinguish genuine absence from a permission/I-O failure. ENOENT
1155
+ // (no milestones/ dir yet) keeps the long-standing [] contract that
1156
+ // collectPhaseRoots / forEachArchivedPhaseToken depend on for "no archives";
1157
+ // every other error (EACCES, EIO, …) must propagate — otherwise an unreadable
1158
+ // milestones/ dir is silently reported as "no archives" and active-milestone
1159
+ // resolution / archived-phase filtering misbehaves.
1160
+ if (err.code === 'ENOENT')
1161
+ return [];
1162
+ throw err;
1046
1163
  }
1047
1164
  }
1048
1165
  function forEachArchivedPhaseToken(planBase, onPhase) {
@@ -1215,8 +1332,9 @@ function cmdValidateConsistency(cwd, raw) {
1215
1332
  }
1216
1333
  }
1217
1334
  for (const plan of plans) {
1218
- const content = node_fs_1.default.readFileSync(node_path_1.default.join(phasePath, plan), 'utf-8');
1219
- const fmData = extractFrontmatter(content);
1335
+ const planFilePath = node_path_1.default.join(phasePath, plan);
1336
+ const content = node_fs_1.default.readFileSync(planFilePath, 'utf-8');
1337
+ const fmData = extractFrontmatter(content, planFilePath);
1220
1338
  if (!fmData['wave']) {
1221
1339
  warnings.push(`${phaseLabel}/${plan}: missing 'wave' in frontmatter`);
1222
1340
  }
@@ -2147,6 +2265,7 @@ module.exports = {
2147
2265
  scanNegativeGrepCommentEcho,
2148
2266
  scanFileWideNegativeGateConflict,
2149
2267
  cmdVerifySummary,
2268
+ verifySummaryCore,
2150
2269
  cmdVerifyPlanStructure,
2151
2270
  cmdVerifyPhaseCompleteness,
2152
2271
  cmdVerifyReferences,
@@ -2158,4 +2277,9 @@ module.exports = {
2158
2277
  cmdValidateAgents,
2159
2278
  cmdVerifySchemaDrift,
2160
2279
  cmdVerifyCodebaseDrift,
2280
+ // Test seam (#1883): listMilestoneArchiveDirs is private and exercised through
2281
+ // the validate command, which runs in a subprocess — an fs monkeypatch in the
2282
+ // test process cannot reach it. Exposed under a leading underscore so the
2283
+ // permission-error path can be unit-tested directly (no chmod 0o000).
2284
+ _listMilestoneArchiveDirs: listMilestoneArchiveDirs,
2161
2285
  };