@opengsd/gsd-core 1.9.0 → 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 (223) 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 +131 -34
  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 +503 -341
  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 +607 -63
  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 +106 -51
  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 -25
  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/project-root.cjs +48 -0
  62. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  63. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
  64. package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
  65. package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
  66. package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
  67. package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
  68. package/gsd-core/bin/lib/roadmap.cjs +37 -7
  69. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
  70. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
  71. package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
  72. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
  73. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  74. package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
  75. package/gsd-core/bin/lib/smart-entry.cjs +12 -0
  76. package/gsd-core/bin/lib/state-transition.cjs +73 -8
  77. package/gsd-core/bin/lib/state.cjs +151 -62
  78. package/gsd-core/bin/lib/surface.cjs +12 -1
  79. package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
  80. package/gsd-core/bin/lib/uat.cjs +320 -21
  81. package/gsd-core/bin/lib/unusable-input.cjs +9 -0
  82. package/gsd-core/bin/lib/verification.cjs +29 -12
  83. package/gsd-core/bin/lib/verify.cjs +29 -5
  84. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  85. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
  86. package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
  87. package/gsd-core/bin/lib/workstream.cjs +6 -0
  88. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  89. package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
  90. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  91. package/gsd-core/references/artifact-types.md +10 -3
  92. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  93. package/gsd-core/references/debugger-techniques.md +255 -0
  94. package/gsd-core/references/research-documentation-lookup.md +5 -3
  95. package/gsd-core/references/specless-probe-fallback.md +7 -6
  96. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  97. package/gsd-core/references/worktree-branch-check.md +2 -2
  98. package/gsd-core/templates/summary-complex.md +2 -0
  99. package/gsd-core/templates/summary-minimal.md +2 -0
  100. package/gsd-core/templates/summary-standard.md +2 -0
  101. package/gsd-core/templates/summary.md +2 -0
  102. package/gsd-core/workflows/audit-milestone.md +3 -0
  103. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  104. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  105. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  106. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  107. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  108. package/gsd-core/workflows/autonomous.md +32 -69
  109. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  110. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
  111. package/gsd-core/workflows/code-review.md +42 -145
  112. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  113. package/gsd-core/workflows/complete-milestone.md +23 -81
  114. package/gsd-core/workflows/debug.md +9 -12
  115. package/gsd-core/workflows/diagnose-issues.md +22 -0
  116. package/gsd-core/workflows/discovery-phase.md +4 -4
  117. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  118. package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
  119. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  120. package/gsd-core/workflows/docs-update.md +8 -51
  121. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
  122. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  123. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  124. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  126. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  127. package/gsd-core/workflows/execute-phase.md +65 -137
  128. package/gsd-core/workflows/execute-plan.md +1 -1
  129. package/gsd-core/workflows/help/modes/full.md +6 -1
  130. package/gsd-core/workflows/ingest-docs.md +2 -1
  131. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  132. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  133. package/gsd-core/workflows/new-milestone.md +21 -38
  134. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  135. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  136. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  137. package/gsd-core/workflows/new-project.md +13 -226
  138. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  139. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  140. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  141. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  142. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  143. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  144. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  145. package/gsd-core/workflows/plan-phase.md +49 -193
  146. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  147. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  148. package/gsd-core/workflows/progress.md +11 -153
  149. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  150. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  151. package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
  152. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  153. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  154. package/gsd-core/workflows/quick.md +20 -390
  155. package/gsd-core/workflows/resume-project.md +3 -0
  156. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  157. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  158. package/gsd-core/workflows/review.md +15 -8
  159. package/gsd-core/workflows/section-manifest.json +219 -0
  160. package/gsd-core/workflows/sketch.md +1 -1
  161. package/gsd-core/workflows/spec-phase.md +17 -14
  162. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  163. package/gsd-core/workflows/spike.md +50 -16
  164. package/gsd-core/workflows/sync-skills.md +49 -11
  165. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  166. package/gsd-core/workflows/transition.md +8 -21
  167. package/gsd-core/workflows/ui-phase.md +8 -7
  168. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  169. package/gsd-core/workflows/update.md +18 -7
  170. package/gsd-core/workflows/verify-phase.md +4 -7
  171. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  172. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  173. package/gsd-core/workflows/verify-work.md +8 -58
  174. package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
  175. package/hooks/dist/gsd-check-update-worker.js +14 -5
  176. package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
  177. package/hooks/dist/gsd-read-injection-scanner.js +7 -0
  178. package/hooks/dist/gsd-statusline.js +72 -6
  179. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  180. package/hooks/dist/gsd-write-guard.js +359 -0
  181. package/hooks/dist/lib/isolation-sentinel.js +268 -0
  182. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  183. package/hooks/gsd-agent-isolation-guard.js +428 -0
  184. package/hooks/gsd-check-update-worker.js +14 -5
  185. package/hooks/gsd-cursor-subagent-start.js +532 -26
  186. package/hooks/gsd-read-injection-scanner.js +7 -0
  187. package/hooks/gsd-statusline.js +72 -6
  188. package/hooks/gsd-worktree-path-guard.js +2 -1
  189. package/hooks/gsd-write-guard.js +359 -0
  190. package/hooks/hooks.json +12 -0
  191. package/hooks/lib/isolation-sentinel.js +268 -0
  192. package/hooks/managed-hooks-registry.cjs +2 -0
  193. package/package.json +14 -5
  194. package/pi/gsd.cjs +57 -12
  195. package/scripts/build-hooks.js +9 -0
  196. package/scripts/changeset/lint.cjs +9 -2
  197. package/scripts/changeset/serialize.cjs +5 -1
  198. package/scripts/gen-capability-matrix.cjs +1 -1
  199. package/scripts/gen-context-index.cjs +448 -0
  200. package/scripts/gen-inventory-manifest.cjs +101 -1
  201. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  202. package/scripts/gen-registry.cjs +39 -15
  203. package/scripts/gen-section-manifest.cjs +638 -0
  204. package/scripts/generate-package-identity.cjs +4 -2
  205. package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
  206. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  207. package/scripts/lint-docs-command-form.cjs +195 -0
  208. package/scripts/lint-docs-required.cjs +9 -1
  209. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  210. package/scripts/lint-example-parser-parity.cjs +395 -0
  211. package/scripts/lint-test-file-count.allowlist.json +27 -1
  212. package/scripts/mutation-matrix.cjs +13 -0
  213. package/scripts/prompt-injection-scan.sh +27 -6
  214. package/scripts/registry-schema.cjs +323 -94
  215. package/scripts/run-tests.cjs +3 -2
  216. package/scripts/validate-registry.cjs +10 -6
  217. package/skills/gsd-autonomous/SKILL.md +1 -1
  218. package/skills/gsd-execute-phase/SKILL.md +1 -1
  219. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  220. package/skills/gsd-new-milestone/SKILL.md +1 -1
  221. package/skills/gsd-plan-phase/SKILL.md +2 -2
  222. package/vscode/package.json +1 -1
  223. package/scripts/gen-emitted-baseline.cjs +0 -145
@@ -22,7 +22,7 @@ const markdownSectionizer = require("./markdown-sectionizer.cjs");
22
22
  const { collectSection, tokenizeHeadings } = markdownSectionizer;
23
23
  // eslint-disable-next-line @typescript-eslint/no-require-imports
24
24
  const markdownTable = require("./markdown-table.cjs");
25
- const { splitTableRow } = markdownTable;
25
+ const { splitTableRow, isDelimiterRow } = markdownTable;
26
26
  // eslint-disable-next-line @typescript-eslint/no-require-imports
27
27
  const roadmapParser = require("./roadmap-parser.cjs");
28
28
  const { getMilestonePhaseFilter } = roadmapParser;
@@ -38,6 +38,9 @@ const { extractFrontmatter } = frontmatter;
38
38
  // eslint-disable-next-line @typescript-eslint/no-require-imports
39
39
  const phaseIdMod = require("./phase-id.cjs");
40
40
  const { PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
41
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
42
+ const phaseLocator = require("./phase-locator.cjs");
43
+ const { getArchivedPhaseDirs } = phaseLocator;
41
44
  const security_cjs_1 = require("./security.cjs");
42
45
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
43
46
  const configLoader = require("./config-loader.cjs");
@@ -45,21 +48,50 @@ const { loadConfig } = configLoader;
45
48
  // ─── cmdAuditUat ─────────────────────────────────────────────────────────────
46
49
  function cmdAuditUat(cwd, raw) {
47
50
  const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
48
- if (!node_fs_1.default.existsSync(phasesDir)) {
51
+ const hasActivePhases = node_fs_1.default.existsSync(phasesDir);
52
+ // #2766: on milestone completion `milestone.cts` MOVES each phase dir into
53
+ // `.planning/milestones/<version>-phases/` (archive-by-default since #1871),
54
+ // leaving `.planning/phases/` empty or absent. Scanning only the active tree
55
+ // meant a partly-archived project silently omitted the archived phases, and a
56
+ // fully-archived one hard-errored with "No phases directory found" —
57
+ // indistinguishable from a broken install. Outstanding UAT items do not stop
58
+ // mattering when a milestone closes: a deferred human-UAT scenario or a
59
+ // `skipped` live-stack test is exactly what gets archived still-open.
60
+ //
61
+ // Reuses the canonical `getArchivedPhaseDirs` seam (phase-locator.cts), which
62
+ // `findPhaseInternal` already uses for this same fallback, so the archive
63
+ // layout convention stays owned by one module.
64
+ const archivedDirs = getArchivedPhaseDirs(cwd);
65
+ if (!hasActivePhases && archivedDirs.length === 0) {
49
66
  error('No phases directory found in planning directory');
50
67
  }
51
68
  const isDirInMilestone = getMilestonePhaseFilter(cwd);
52
69
  const results = [];
53
- // Scan all phase directories
54
- const dirs = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true })
55
- .filter(e => e.isDirectory())
56
- .map(e => e.name)
57
- .filter(isDirInMilestone)
58
- .sort();
59
- for (const dir of dirs) {
70
+ // Active dirs are milestone-filtered; archived dirs deliberately are NOT.
71
+ // getMilestonePhaseFilter derives the CURRENT milestone's phase numbers from
72
+ // ROADMAP.md, and archived phases belong to past milestones by definition — so
73
+ // applying it to them discards every one and silently reinstates the bug.
74
+ const scanTargets = [];
75
+ if (hasActivePhases) {
76
+ const dirs = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true })
77
+ .filter(e => e.isDirectory())
78
+ .map(e => e.name)
79
+ .filter(isDirInMilestone)
80
+ .sort();
81
+ for (const dir of dirs) {
82
+ scanTargets.push({ dir, phaseDir: node_path_1.default.join(phasesDir, dir) });
83
+ }
84
+ }
85
+ for (const archived of archivedDirs) {
86
+ scanTargets.push({
87
+ dir: archived.name,
88
+ phaseDir: archived.fullPath,
89
+ milestone: archived.milestone,
90
+ });
91
+ }
92
+ for (const { dir, phaseDir, milestone } of scanTargets) {
60
93
  const phaseMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
61
94
  const phaseNum = phaseMatch ? phaseMatch[1] : dir;
62
- const phaseDir = node_path_1.default.join(phasesDir, dir);
63
95
  const files = node_fs_1.default.readdirSync(phaseDir);
64
96
  // Process UAT files
65
97
  for (const file of files.filter(f => f.includes('-UAT') && f.endsWith('.md'))) {
@@ -74,6 +106,7 @@ function cmdAuditUat(cwd, raw) {
74
106
  file_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(phaseDir, file))),
75
107
  type: 'uat',
76
108
  status: (extractFrontmatter(content, uatFilePath).status || 'unknown'),
109
+ archived_milestone: milestone,
77
110
  items,
78
111
  });
79
112
  }
@@ -93,6 +126,7 @@ function cmdAuditUat(cwd, raw) {
93
126
  file_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(phaseDir, file))),
94
127
  type: 'verification',
95
128
  status,
129
+ archived_milestone: milestone,
96
130
  items,
97
131
  });
98
132
  }
@@ -117,6 +151,7 @@ function cmdAuditUat(cwd, raw) {
117
151
  file_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(phaseDir, deferredFile))),
118
152
  type: 'deferred',
119
153
  status: 'unresolved',
154
+ archived_milestone: milestone,
120
155
  items,
121
156
  });
122
157
  }
@@ -308,6 +343,43 @@ const CHECKPOINT_FRAMES = {
308
343
  banner: 'PUNTO DI CONTROLLO: Verifica richiesta',
309
344
  instruction: 'Digita `pass` o descrivi cosa non va.',
310
345
  },
346
+ dutch: {
347
+ banner: 'CONTROLEPUNT: Verificatie vereist',
348
+ instruction: 'Typ `pass` of beschrijf wat er mis is.',
349
+ },
350
+ polish: {
351
+ banner: 'PUNKT KONTROLNY: Wymagana weryfikacja',
352
+ instruction: 'Wpisz `pass` lub opisz, co jest nie tak.',
353
+ },
354
+ russian: {
355
+ banner: 'КОНТРОЛЬНАЯ ТОЧКА: требуется проверка',
356
+ instruction: 'Введите `pass` или опишите, что не так.',
357
+ },
358
+ ukrainian: {
359
+ banner: 'КОНТРОЛЬНА ТОЧКА: потрібна перевірка',
360
+ instruction: 'Введіть `pass` або опишіть, що не так.',
361
+ },
362
+ turkish: {
363
+ banner: 'KONTROL NOKTASI: Doğrulama gerekli',
364
+ instruction: '`pass` yazın veya sorunu açıklayın.',
365
+ },
366
+ hindi: {
367
+ banner: 'चेकपॉइंट: सत्यापन आवश्यक',
368
+ instruction: '`pass` लिखें या बताएं कि क्या गलत है।',
369
+ },
370
+ arabic: {
371
+ banner: 'نقطة تحقق: المراجعة مطلوبة',
372
+ instruction: 'اكتب `pass` أو صف المشكلة.',
373
+ direction: 'rtl',
374
+ },
375
+ vietnamese: {
376
+ banner: 'ĐIỂM KIỂM TRA: Cần xác minh',
377
+ instruction: 'Nhập `pass` hoặc mô tả vấn đề.',
378
+ },
379
+ indonesian: {
380
+ banner: 'TITIK PEMERIKSAAN: Verifikasi diperlukan',
381
+ instruction: 'Ketik `pass` atau jelaskan apa yang salah.',
382
+ },
311
383
  };
312
384
  // Free-form response_language aliases → canonical CHECKPOINT_FRAMES key.
313
385
  const CHECKPOINT_LANGUAGE_ALIASES = {
@@ -320,21 +392,27 @@ const CHECKPOINT_LANGUAGE_ALIASES = {
320
392
  chinese: 'chinese', zh: 'chinese', 'zh-cn': 'chinese', 'zh-tw': 'chinese', mandarin: 'chinese', 'simplified chinese': 'chinese', 'traditional chinese': 'chinese', '中文': 'chinese',
321
393
  korean: 'korean', ko: 'korean', '한국어': 'korean',
322
394
  italian: 'italian', it: 'italian', italiano: 'italian',
395
+ dutch: 'dutch', nl: 'dutch', nederlands: 'dutch', flemish: 'dutch', vlaams: 'dutch',
396
+ polish: 'polish', pl: 'polish', polski: 'polish',
397
+ russian: 'russian', ru: 'russian', 'ru-ru': 'russian', 'русский': 'russian',
398
+ ukrainian: 'ukrainian', uk: 'ukrainian', ua: 'ukrainian', 'українська': 'ukrainian',
399
+ turkish: 'turkish', tr: 'turkish', 'türkçe': 'turkish', turkce: 'turkish',
400
+ hindi: 'hindi', hi: 'hindi', 'हिन्दी': 'hindi', 'हिंदी': 'hindi',
401
+ arabic: 'arabic', ar: 'arabic', 'العربية': 'arabic',
402
+ vietnamese: 'vietnamese', vi: 'vietnamese', 'tiếng việt': 'vietnamese', 'tieng viet': 'vietnamese',
403
+ indonesian: 'indonesian', id: 'indonesian', 'bahasa indonesia': 'indonesian',
323
404
  };
324
405
  function resolveCheckpointFrame(responseLanguage) {
325
406
  if (!responseLanguage)
326
407
  return CHECKPOINT_FRAMES.english;
327
- const key = CHECKPOINT_LANGUAGE_ALIASES[responseLanguage.trim().toLowerCase()];
408
+ const key = CHECKPOINT_LANGUAGE_ALIASES[responseLanguage.trim().normalize('NFC').toLowerCase()];
328
409
  return (key && CHECKPOINT_FRAMES[key]) || CHECKPOINT_FRAMES.english;
329
410
  }
330
- // Approximate East Asian Width ranges (Unicode property values W and F) — the
331
- // CJK scripts CHECKPOINT_FRAMES ships (Japanese/Chinese/Korean) render each
332
- // matching code point at 2 terminal/display columns, not 1. Padding computed
333
- // from `.length` (UTF-16 code units) undercounts these by one column per
334
- // wide character, visually misaligning the box's right border (#2402 review
335
- // medium finding). Latin-script frames (English/Spanish/French/German/
336
- // Portuguese/Italian) contain no wide code points, so displayWidth === length
337
- // for them — no behavior change there.
411
+ // Approximate terminal-cell width. East Asian Width W/F code points occupy two
412
+ // cells, while Unicode combining marks occupy no additional cell beyond their
413
+ // base character. Counting only W/F ranges is insufficient for scripts such as
414
+ // Devanagari: Hindi vowel signs and viramas are combining marks, and treating
415
+ // each as a full cell visibly shifts the checkpoint box's right border.
338
416
  function isWideCodePoint(codePoint) {
339
417
  return ((codePoint >= 0x1100 && codePoint <= 0x115f) || // Hangul Jamo
340
418
  codePoint === 0x2329 || codePoint === 0x232a ||
@@ -351,11 +429,17 @@ function isWideCodePoint(codePoint) {
351
429
  (codePoint >= 0x20000 && codePoint <= 0x3fffd) // CJK Unified Ideographs Extension B+ / supplementary
352
430
  );
353
431
  }
432
+ // Non-spacing/enclosing marks and format controls occupy zero terminal cells.
433
+ // Spacing combining marks (General_Category=Mc), such as Devanagari vowel
434
+ // signs, still advance the cursor and must contribute one column.
435
+ const ZERO_WIDTH_MARK_RE = /\p{gc=Mn}|\p{gc=Me}|\p{gc=Cf}/u;
354
436
  // Iterates by Unicode code point (not UTF-16 code unit) so astral characters
355
437
  // are measured once, not as two surrogate units.
356
438
  function displayWidth(text) {
357
439
  let width = 0;
358
440
  for (const ch of text) {
441
+ if (ZERO_WIDTH_MARK_RE.test(ch))
442
+ continue;
359
443
  width += isWideCodePoint(ch.codePointAt(0)) ? 2 : 1;
360
444
  }
361
445
  return width;
@@ -370,11 +454,20 @@ function checkpointBoxLine(text) {
370
454
  const padded = padLength > 0 ? content + ' '.repeat(padLength) : content;
371
455
  return `║${padded}║`;
372
456
  }
457
+ const RTL_ISOLATE = '\u2067';
458
+ const POP_DIRECTIONAL_ISOLATE = '\u2069';
459
+ function isolateCheckpointFrameText(text, frame) {
460
+ return frame.direction === 'rtl'
461
+ ? `${RTL_ISOLATE}${text}${POP_DIRECTIONAL_ISOLATE}`
462
+ : text;
463
+ }
373
464
  function buildCheckpoint(currentTest, responseLanguage) {
374
465
  const frame = resolveCheckpointFrame(responseLanguage);
466
+ const banner = isolateCheckpointFrameText(frame.banner, frame);
467
+ const instruction = isolateCheckpointFrameText(frame.instruction, frame);
375
468
  return [
376
469
  '╔══════════════════════════════════════════════════════════════╗',
377
- checkpointBoxLine(frame.banner),
470
+ checkpointBoxLine(banner),
378
471
  '╚══════════════════════════════════════════════════════════════╝',
379
472
  '',
380
473
  `**Test ${currentTest.number}: ${currentTest.name}**`,
@@ -382,7 +475,7 @@ function buildCheckpoint(currentTest, responseLanguage) {
382
475
  currentTest.expected,
383
476
  '',
384
477
  '──────────────────────────────────────────────────────────────',
385
- frame.instruction,
478
+ instruction,
386
479
  '──────────────────────────────────────────────────────────────',
387
480
  ].join('\n');
388
481
  }
@@ -486,6 +579,165 @@ function parseGapsItems(content) {
486
579
  item.reason = reason;
487
580
  items.push(item);
488
581
  }
582
+ // #2766: union with the table form. A `|`-leading line is never a `- ` bullet
583
+ // opener, so a section mixing bullet entries and a table surfaces both with no
584
+ // double-counting.
585
+ items.push(...parseGapsTableItems(gapsSection.body));
586
+ return items;
587
+ }
588
+ /**
589
+ * Split a section body into its GFM pipe tables, one entry per table (#2766).
590
+ *
591
+ * Shared by `parseGapsTableItems` and `parseDeferredTableItems` so the
592
+ * header/delimiter/table-boundary handling — the fiddly part — lives in exactly
593
+ * one place, and the two consumers only decide what a data row MEANS.
594
+ *
595
+ * Header detection is lookahead-free: the last data-shaped row is held in
596
+ * `pending` until the NEXT line decides its fate — a delimiter row
597
+ * (`|---|---|`) proves the held row was a header, anything else promotes it to a
598
+ * data row. So a conventional table drops exactly its header, a HEADERLESS table
599
+ * keeps every row (hand-authored planning tables often omit the delimiter), and
600
+ * a header with no data rows yields nothing. A prose or blank line ends the
601
+ * current table, so two tables separated by text are read independently and each
602
+ * drops its own header.
603
+ *
604
+ * Reuses the canonical `isDelimiterRow` shape check from markdown-table.cts
605
+ * rather than re-deriving it. Deliberately NOT routed through
606
+ * `parseMarkdownTable`, which reads only the FIRST table in a body and treats
607
+ * ragged/headerless shapes as errors (ADR-2143 §3) — correct for the mandated
608
+ * tables in STATE.md/ROADMAP.md, but the wrong contract here, where a malformed
609
+ * hand-written table must still surface its rows rather than be dropped.
610
+ */
611
+ function collectTableRows(sectionBody) {
612
+ const tables = [];
613
+ let current = null;
614
+ let pending = null;
615
+ const ensure = () => {
616
+ if (!current)
617
+ current = { header: null, rows: [] };
618
+ };
619
+ const flushPending = () => {
620
+ if (pending) {
621
+ ensure();
622
+ current.rows.push(pending);
623
+ pending = null;
624
+ }
625
+ };
626
+ const endTable = () => {
627
+ flushPending();
628
+ if (current) {
629
+ tables.push(current);
630
+ current = null;
631
+ }
632
+ };
633
+ for (const rawLine of sectionBody.split('\n')) {
634
+ const line = rawLine.replace(/\r$/, '').trim();
635
+ if (!line.startsWith('|')) {
636
+ endTable();
637
+ continue;
638
+ }
639
+ const cells = splitTableRow(line);
640
+ if (cells.length === 0)
641
+ continue;
642
+ if (isDelimiterRow(cells)) {
643
+ ensure();
644
+ current.header = pending; // may be null for a delimiter-first table
645
+ pending = null;
646
+ continue;
647
+ }
648
+ flushPending();
649
+ pending = cells;
650
+ }
651
+ endTable();
652
+ return tables;
653
+ }
654
+ /**
655
+ * Header-name → canonical Gaps field (#2766).
656
+ *
657
+ * Anchored on the `## Gaps` field vocabulary `templates/UAT.md` mandates for the
658
+ * YAML-lite bullet form (truth/status/reason/severity/test), plus the obvious
659
+ * synonyms a human writing the same information as a table reaches for instead.
660
+ */
661
+ const GAPS_COLUMN_ALIASES = {
662
+ truth: 'truth', gap: 'truth', finding: 'truth', item: 'truth',
663
+ description: 'truth', issue: 'truth', name: 'truth',
664
+ status: 'status', result: 'status', state: 'status',
665
+ reason: 'reason', note: 'reason', notes: 'reason',
666
+ detail: 'reason', details: 'reason', evidence: 'reason',
667
+ severity: 'severity',
668
+ test: 'test', '#': 'test', 'test #': 'test', 'test number': 'test',
669
+ };
670
+ function mapGapsHeader(header) {
671
+ if (!header)
672
+ return null;
673
+ const columns = {};
674
+ header.forEach((cell, idx) => {
675
+ const key = GAPS_COLUMN_ALIASES[cell.trim().toLowerCase().replace(/\*+/g, '')];
676
+ if (key && !(key in columns))
677
+ columns[key] = idx;
678
+ });
679
+ return Object.keys(columns).length > 0 ? columns : null;
680
+ }
681
+ /**
682
+ * Extract gap entries from GFM pipe tables in a `## Gaps` section (#2766) — a
683
+ * UNION with the YAML-lite bullet scan in `parseGapsItems`, for the same reason
684
+ * `parseDeferredTableItems` exists: `splitGapsEntries` keys entirely on `- `
685
+ * bullet openers, so a table-shaped `## Gaps` section yielded ZERO items and
686
+ * every finding in it was silently invisible.
687
+ *
688
+ * Neither `templates/UAT.md` nor `templates/verification-report.md` documents a
689
+ * table for this section (both mandate the bullet/numbered form), so a table
690
+ * here is off-template hand-authoring — which is precisely why it must not fail
691
+ * silently. Note `parseVerificationItems` in this same file already reads table
692
+ * rows AND numbered AND bullet items as a union because the live sections mix
693
+ * shapes; the Gaps and deferred parsers never got the same treatment.
694
+ *
695
+ * When a header row is present its columns are mapped by name against the
696
+ * template's own field vocabulary (see GAPS_COLUMN_ALIASES) so a tabled gap
697
+ * carries the same status/reason/test fields as its bullet equivalent and
698
+ * `categorizeItem` classifies it identically. With no recognizable header, the
699
+ * row degrades to a joined-cells name with status `unknown` — surfaced, not
700
+ * dropped, matching this module's established fail-safe stance.
701
+ *
702
+ * Resolution follows the bullet path exactly: an entry is skipped ONLY on an
703
+ * explicit resolved marker — the mapped `status` column reading `resolved`, or,
704
+ * absent a status column, any cell reading exactly `resolved`. A gap with no
705
+ * parseable status is NEVER treated as resolved.
706
+ */
707
+ function parseGapsTableItems(sectionBody) {
708
+ const items = [];
709
+ for (const { header, rows } of collectTableRows(sectionBody)) {
710
+ const columns = mapGapsHeader(header);
711
+ for (const cells of rows) {
712
+ const at = (key) => (columns && key in columns ? (cells[columns[key]] ?? '').trim() : '');
713
+ const rawStatus = at('status');
714
+ if (rawStatus && rawStatus.toLowerCase() === 'resolved')
715
+ continue;
716
+ // No status column: fall back to an explicit resolved marker in any cell
717
+ // (the headerless-table equivalent of `status: resolved`).
718
+ if (!columns || !('status' in columns)) {
719
+ if (cells.some(c => /^resolved$/i.test(c.trim())))
720
+ continue;
721
+ }
722
+ const truth = at('truth');
723
+ const reason = at('reason');
724
+ const testNum = at('test');
725
+ const name = truth || cells.filter(c => c !== '').join(' — ');
726
+ if (!name)
727
+ continue;
728
+ const status = rawStatus || 'unknown';
729
+ const item = {
730
+ name,
731
+ result: status,
732
+ category: categorizeItem(status, reason || undefined, undefined),
733
+ };
734
+ if (testNum && /^\d+$/.test(testNum))
735
+ item.test = parseInt(testNum, 10);
736
+ if (reason)
737
+ item.reason = reason;
738
+ items.push(item);
739
+ }
740
+ }
489
741
  return items;
490
742
  }
491
743
  // ─── parseDeferredItems ────────────────────────────────────────────────────────
@@ -533,6 +785,49 @@ function parseDeferredItems(content) {
533
785
  category: 'deferred',
534
786
  });
535
787
  }
788
+ // #2766: union with the table form — see parseDeferredTableItems. Executors
789
+ // write this file by hand with no mandated shape, and a GFM table is a natural
790
+ // choice for the common "test → failing seeds" case, which produced ZERO items.
791
+ items.push(...parseDeferredTableItems(sectionBody));
792
+ return items;
793
+ }
794
+ /**
795
+ * Extract deferred entries from GFM pipe tables in a deferred-items.md body
796
+ * (#2766) — a UNION with the bullet scan in `parseDeferredItems`.
797
+ *
798
+ * Cells are joined with ` — ` rather than taking only the first: these tables
799
+ * carry the useful detail in the later columns (the failing seeds, the reason,
800
+ * the owner), and dropping them would surface a name with no context.
801
+ *
802
+ * A row is skipped when any cell reads exactly `resolved`/`done`/`pass`
803
+ * (case-insensitive), mirroring the "explicit resolution only" convention
804
+ * `parseGapsItems` uses for `status: resolved` and `parseVerificationItems` uses
805
+ * for its `hasPassResult` cell scan — so a human can close a tabled deferred
806
+ * item in place and keep deferred-items.md the single source of truth.
807
+ *
808
+ * Deliberately permissive: an unrelated table in a deferred-items.md (say a
809
+ * table of environment notes) will surface as deferred entries. That is the
810
+ * correct fail-safe direction for a false-NEGATIVE bug — the whole file exists to
811
+ * record outstanding work, and this module's established stance (see
812
+ * parseGapsItems' 'unknown'-status fallback) is to surface a questionable entry
813
+ * rather than silently drop a real one.
814
+ */
815
+ function parseDeferredTableItems(sectionBody) {
816
+ const items = [];
817
+ for (const { rows } of collectTableRows(sectionBody)) {
818
+ for (const cells of rows) {
819
+ if (cells.some(c => /^(resolved|done|pass)$/i.test(c)))
820
+ continue;
821
+ const name = cells.filter(c => c !== '').join(' — ');
822
+ if (!name)
823
+ continue;
824
+ items.push({
825
+ name,
826
+ result: 'unresolved',
827
+ category: 'deferred',
828
+ });
829
+ }
830
+ }
536
831
  return items;
537
832
  }
538
833
  /**
@@ -834,5 +1129,9 @@ module.exports = {
834
1129
  cmdRenderCheckpoint,
835
1130
  parseCurrentTest,
836
1131
  buildCheckpoint,
1132
+ CHECKPOINT_FRAMES,
1133
+ CHECKPOINT_LANGUAGE_ALIASES,
1134
+ resolveCheckpointFrame,
1135
+ checkpointBoxLine,
837
1136
  parseDeferredItems,
838
1137
  };
@@ -50,11 +50,20 @@ const UNUSABLE_REASON = Object.freeze({
50
50
  * simply has no ROADMAP yet: absence returns the same sentinel, silently. (#1881)
51
51
  */
52
52
  ROADMAP_UNREADABLE: 'roadmap_unreadable',
53
+ /**
54
+ * A `last_activity` value in STATE.md frontmatter is present but unparseable as a date.
55
+ * Distinct from absent: absence means the field was never set, while an unparseable value
56
+ * means it was set to something Date.parse rejects. Per ADR-1411 amendment: corrupt is not
57
+ * absent — the fallback (stale_activity: false) stays, but a diagnostic is emitted so the
58
+ * silent degradation is visible. (#3099, sixth #1879 site)
59
+ */
60
+ LAST_ACTIVITY_UNPARSEABLE: 'last_activity_unparseable',
53
61
  });
54
62
  /** One human-readable clause per reason. Prose lives here, never in a test assertion. */
55
63
  const REASON_PROSE = Object.freeze({
56
64
  [UNUSABLE_REASON.FRONTMATTER_UNTERMINATED]: 'frontmatter opens with "---" but never closes; metadata was NOT applied',
57
65
  [UNUSABLE_REASON.ROADMAP_UNREADABLE]: 'ROADMAP.md exists but could not be read; phase and milestone lookups fell back to defaults',
66
+ [UNUSABLE_REASON.LAST_ACTIVITY_UNPARSEABLE]: 'last_activity in STATE.md is present but unparseable as a date; stale_activity fell back to false (idle-stranded suppressed)',
58
67
  });
59
68
  // ─── Dedup state ──────────────────────────────────────────────────────────────
60
69
  /**
@@ -213,23 +213,26 @@ function missingResult(runtime, phaseArg) {
213
213
  }
214
214
  function findStaleVerificationSummary(phaseDir, fsImpl = node_fs_1.default, phaseCleanCommitTimesMs = defaultPhaseCleanCommitTimesMs) {
215
215
  // FS errors (TOCTOU: a SUMMARY listed by scanPhasePlans then removed before statSync;
216
- // unreadable dir; broken symlink; file->dir swap) must degrade to "not stale" rather
217
- // than throw uncaught into callers that are NOT under the planning lock
218
- // (init.manager / init.progress / uat-predicate). Mirrors readVerificationStatus's
219
- // no-throw contract; `fsImpl` threads the same injectable-fs seam for parity/testing.
220
- // (Review B1 on #1548.)
216
+ // unreadable dir; broken symlink; file->dir swap) must degrade rather than throw
217
+ // uncaught into callers that are NOT under the planning lock (init.manager /
218
+ // init.progress / uat-predicate). Mirrors readVerificationStatus's no-throw
219
+ // contract; `fsImpl` threads the same injectable-fs seam for parity/testing.
220
+ // (Review B1 on #1548.) The degraded result is `{determined:false}`, NOT the
221
+ // same value as a completed "nothing is stale" check — see StaleCheckResult
222
+ // doc and #3057 B3. The caller decides how to route an indeterminate result;
223
+ // this function only reports what it actually knows.
221
224
  try {
222
225
  const phaseFiles = fsImpl.readdirSync(phaseDir);
223
226
  const verificationFile = phaseFiles.filter((f) => f.endsWith('-VERIFICATION.md')).sort()[0];
224
227
  if (!verificationFile)
225
- return null;
228
+ return { determined: true, stale: false };
226
229
  const summaryFiles = scanPhasePlans(phaseDir).summaryFiles
227
230
  .slice()
228
231
  .sort();
229
232
  // No summary can be newer than the verification → never stale. Return before
230
233
  // touching git so a phase with no summaries costs zero subprocesses. (#2348)
231
234
  if (summaryFiles.length === 0)
232
- return null;
235
+ return { determined: true, stale: false };
233
236
  // Each file's effective "last changed" time = its commit time when committed
234
237
  // AND clean (content-tied and clone-stable), else its filesystem mtime (the
235
238
  // uncommitted working-tree edit). Both are real wall-clock change times, so
@@ -244,13 +247,13 @@ function findStaleVerificationSummary(phaseDir, fsImpl = node_fs_1.default, phas
244
247
  // The caller only needs whether the phase is stale, not which summary —
245
248
  // the first stale summary (in sorted order) is enough. Short-circuit.
246
249
  if (effectiveTimeMs(summaryFile) > verificationTimeMs) {
247
- return { verificationFile, summaryFile };
250
+ return { determined: true, stale: true, verificationFile, summaryFile };
248
251
  }
249
252
  }
250
- return null;
253
+ return { determined: true, stale: false };
251
254
  }
252
255
  catch {
253
- return null;
256
+ return { determined: false };
254
257
  }
255
258
  }
256
259
  /**
@@ -265,6 +268,12 @@ function findStaleVerificationSummary(phaseDir, fsImpl = node_fs_1.default, phas
265
268
  * If no frontmatter block or no `status` key → status 'missing'.
266
269
  * 3. Map to routing table. Unknown non-empty value → status 'unknown'.
267
270
  *
271
+ * The internal staleness check can itself fail (fs / scanPhasePlans / clock
272
+ * error); when it does, `status` is routed as if nothing were stale (the
273
+ * pre-existing no-throw fail-open contract — unchanged), but the returned
274
+ * result carries `staleCheckIndeterminate: true` so a caller can distinguish
275
+ * "checked; nothing is stale" from "could not check" (#3057 B3).
276
+ *
268
277
  * @param phaseDir - Absolute path to the phase directory.
269
278
  * @param opts - Options. `opts.fs` allows test injection (defaults to node:fs).
270
279
  * `opts.runtime` selects the command surface `next_command` is
@@ -331,8 +340,8 @@ function readVerificationStatus(phaseDir, opts = {}) {
331
340
  next_command: projectNextCommand('plan-phase', runtime, `${phaseArg} --gaps`),
332
341
  };
333
342
  }
334
- const staleVerification = findStaleVerificationSummary(phaseDir, fsImpl, phaseCleanCommitTimesMs);
335
- if (staleVerification) {
343
+ const staleCheck = findStaleVerificationSummary(phaseDir, fsImpl, phaseCleanCommitTimesMs);
344
+ if (staleCheck.determined && staleCheck.stale) {
336
345
  const entry = VERIFICATION_ROUTING_TABLE['stale'];
337
346
  return {
338
347
  status: entry.status,
@@ -340,6 +349,12 @@ function readVerificationStatus(phaseDir, opts = {}) {
340
349
  next_command: projectNextCommand('verify-work', runtime, phaseArg),
341
350
  };
342
351
  }
352
+ // staleCheck is either {determined:true, stale:false} (checked; nothing
353
+ // stale) or {determined:false} (could not check — fs/scan/clock failure).
354
+ // Both fall through to normal routing below (the pre-existing no-throw
355
+ // fail-open contract is unchanged), but the indeterminate case is flagged
356
+ // on the returned result so a caller can tell the two apart (#3057 B3).
357
+ const staleCheckIndeterminate = !staleCheck.determined;
343
358
  // 3. Route — exclude internal sentinels from raw-file lookup (they are
344
359
  // constructed internally above, never written by the verifier).
345
360
  if (rawStatus in VERIFICATION_ROUTING_TABLE &&
@@ -352,6 +367,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
352
367
  status: entry.status,
353
368
  next_action: entry.next_action,
354
369
  next_command: projectNextCommand(entry.next_command, runtime, phaseArg),
370
+ ...(staleCheckIndeterminate ? { staleCheckIndeterminate: true } : {}),
355
371
  };
356
372
  }
357
373
  // Unknown value
@@ -360,6 +376,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
360
376
  status: unknownRoute.status,
361
377
  next_action: `Unexpected verification status '${rawStatus}'. Re-run execute-phase verification.`,
362
378
  next_command: projectNextCommand(unknownRoute.next_command, runtime, phaseArg),
379
+ ...(staleCheckIndeterminate ? { staleCheckIndeterminate: true } : {}),
363
380
  };
364
381
  }
365
382
  /**
@@ -161,6 +161,15 @@ function verifySummaryCore(cwd, summaryPath, checkFileCount, opts) {
161
161
  // is still not read. Recovering it needs a real frontmatter parse, which is
162
162
  // deliberately left as a follow-up rather than smuggled in here.
163
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);
164
173
  const patterns = [
165
174
  /`([^`]+\.[a-zA-Z]+)`/g,
166
175
  /(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`[\]]+\.[a-zA-Z]+)`?/gi,
@@ -169,9 +178,16 @@ function verifySummaryCore(cwd, summaryPath, checkFileCount, opts) {
169
178
  let m;
170
179
  while ((m = pattern.exec(content)) !== null) {
171
180
  const filePath = m[1];
172
- if (filePath && isProbableProjectFile(filePath)) {
173
- mentionedFiles.add(filePath);
174
- }
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);
175
191
  }
176
192
  }
177
193
  const filesToCheck = Array.from(mentionedFiles).slice(0, checkCount);
@@ -1596,7 +1612,7 @@ function cmdValidateHealth(cwd, options, raw) {
1596
1612
  }
1597
1613
  }
1598
1614
  try {
1599
- const agentStatus = checkAgentsInstalled();
1615
+ const agentStatus = checkAgentsInstalled(_slashRuntime, cwd);
1600
1616
  if (!agentStatus.agents_installed) {
1601
1617
  if ((agentStatus.installed_agents).length === 0) {
1602
1618
  addIssue('warning', 'W010', `No GSD agents found in ${agentStatus.agents_dir} — Task(subagent_type="gsd-*") will fall back to general-purpose`, `Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
@@ -1719,6 +1735,14 @@ function cmdValidateHealth(cwd, options, raw) {
1719
1735
  if (isActiveWorktree)
1720
1736
  continue;
1721
1737
  addIssue('warning', 'W017', `Stale git worktree: ${worktreePath} (last modified ${finding['ageMinutes']} minutes ago)`, `Run: git worktree remove ${worktreePath} --force`);
1738
+ continue;
1739
+ }
1740
+ // #3050/#3057 (B5): a 'unverified' finding means existsSync confirmed
1741
+ // the worktree is present but statSync threw, so orphan/stale status
1742
+ // could not be determined for THIS entry — it must not be silently
1743
+ // dropped (that would be the exact fail-open the row exists to close).
1744
+ if (finding['kind'] === 'unverified') {
1745
+ addIssue('warning', 'W020', `Worktree health check degraded: could not stat ${finding['path']} — presence/staleness could not be verified`, 'Check filesystem permissions on the worktree path, or investigate why statSync failed for it');
1722
1746
  }
1723
1747
  }
1724
1748
  }
@@ -2023,7 +2047,7 @@ function cmdValidateHealth(cwd, options, raw) {
2023
2047
  return result;
2024
2048
  }
2025
2049
  function cmdValidateAgents(cwd, raw) {
2026
- const agentStatus = checkAgentsInstalled();
2050
+ const agentStatus = checkAgentsInstalled((0, runtime_slash_cjs_1.resolveRuntime)(cwd), cwd);
2027
2051
  const expected = Object.keys(MODEL_PROFILES);
2028
2052
  output({
2029
2053
  agents_dir: agentStatus.agents_dir,