@opengsd/gsd-core 1.14.0 → 1.16.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 (551) 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 +85 -5
  4. package/README.ja-JP.md +3 -3
  5. package/README.ko-KR.md +3 -3
  6. package/README.pt-BR.md +3 -3
  7. package/README.zh-CN.md +3 -3
  8. package/agents/gsd-code-fixer.compact.md +7 -6
  9. package/agents/gsd-code-fixer.md +9 -8
  10. package/agents/gsd-code-reviewer.compact.md +5 -3
  11. package/agents/gsd-code-reviewer.md +8 -6
  12. package/agents/gsd-debug-session-manager.compact.md +17 -2
  13. package/agents/gsd-debug-session-manager.md +17 -2
  14. package/agents/gsd-debugger.md +3 -3
  15. package/agents/gsd-eval-auditor.compact.md +1 -1
  16. package/agents/gsd-eval-auditor.md +1 -1
  17. package/agents/gsd-executor.md +17 -12
  18. package/agents/gsd-intel-updater.compact.md +1 -1
  19. package/agents/gsd-intel-updater.md +1 -1
  20. package/agents/gsd-mempalace-curator.md +2 -2
  21. package/agents/gsd-phase-researcher.md +19 -11
  22. package/agents/gsd-plan-checker.md +15 -9
  23. package/agents/gsd-planner.md +15 -11
  24. package/agents/gsd-project-researcher.compact.md +1 -1
  25. package/agents/gsd-project-researcher.md +1 -1
  26. package/agents/gsd-research-synthesizer.compact.md +1 -1
  27. package/agents/gsd-research-synthesizer.md +1 -1
  28. package/agents/gsd-ui-auditor.compact.md +21 -30
  29. package/agents/gsd-ui-auditor.md +166 -37
  30. package/agents/gsd-ui-researcher.compact.md +1 -1
  31. package/agents/gsd-ui-researcher.md +1 -1
  32. package/agents/gsd-verifier.md +37 -14
  33. package/bin/install.js +764 -287
  34. package/commands/gsd/add-tests.md +6 -1
  35. package/commands/gsd/ai-integration-phase.md +6 -1
  36. package/commands/gsd/audit-fix.md +5 -0
  37. package/commands/gsd/audit-milestone.md +6 -1
  38. package/commands/gsd/autonomous.md +7 -2
  39. package/commands/gsd/capture.md +9 -5
  40. package/commands/gsd/code-review.md +7 -2
  41. package/commands/gsd/complete-milestone.md +4 -0
  42. package/commands/gsd/config.md +7 -3
  43. package/commands/gsd/debug.md +11 -7
  44. package/commands/gsd/discuss-phase.md +7 -3
  45. package/commands/gsd/docs-update.md +12 -7
  46. package/commands/gsd/eval-review.md +6 -1
  47. package/commands/gsd/execute-phase.md +12 -7
  48. package/commands/gsd/extract-learnings.md +5 -0
  49. package/commands/gsd/fast.md +4 -0
  50. package/commands/gsd/forensics.md +5 -1
  51. package/commands/gsd/graphify.md +10 -6
  52. package/commands/gsd/health.md +5 -0
  53. package/commands/gsd/help.md +7 -2
  54. package/commands/gsd/import.md +7 -3
  55. package/commands/gsd/inbox.md +5 -0
  56. package/commands/gsd/ingest-docs.md +5 -1
  57. package/commands/gsd/manager.md +6 -1
  58. package/commands/gsd/map-codebase.md +7 -3
  59. package/commands/gsd/mempalace-capture.md +12 -4
  60. package/commands/gsd/mempalace-recall.md +5 -1
  61. package/commands/gsd/milestone-summary.md +5 -1
  62. package/commands/gsd/mvp-phase.md +8 -3
  63. package/commands/gsd/new-milestone.md +6 -1
  64. package/commands/gsd/new-project.md +5 -0
  65. package/commands/gsd/next.md +6 -1
  66. package/commands/gsd/ns-context.md +4 -0
  67. package/commands/gsd/ns-ideate.md +4 -0
  68. package/commands/gsd/ns-manage.md +4 -0
  69. package/commands/gsd/ns-project.md +4 -0
  70. package/commands/gsd/ns-review.md +4 -0
  71. package/commands/gsd/ns-workflow.md +4 -0
  72. package/commands/gsd/onboard.md +6 -1
  73. package/commands/gsd/pause-work.md +5 -1
  74. package/commands/gsd/phase.md +8 -4
  75. package/commands/gsd/plan-phase.md +6 -1
  76. package/commands/gsd/plan-review-convergence.md +11 -7
  77. package/commands/gsd/pr-branch.md +4 -0
  78. package/commands/gsd/profile-user.md +5 -1
  79. package/commands/gsd/progress.md +7 -2
  80. package/commands/gsd/quick-batch.md +21 -9
  81. package/commands/gsd/quick.md +12 -7
  82. package/commands/gsd/review.md +7 -4
  83. package/commands/gsd/secure-phase.md +6 -1
  84. package/commands/gsd/ship.md +5 -0
  85. package/commands/gsd/sketch.md +7 -2
  86. package/commands/gsd/spec-phase.md +5 -1
  87. package/commands/gsd/spike.md +8 -3
  88. package/commands/gsd/surface.md +5 -1
  89. package/commands/gsd/thread.md +4 -0
  90. package/commands/gsd/ui-phase.md +6 -1
  91. package/commands/gsd/ui-review.md +6 -1
  92. package/commands/gsd/ultraplan-phase.md +5 -1
  93. package/commands/gsd/undo.md +5 -1
  94. package/commands/gsd/update.md +6 -2
  95. package/commands/gsd/validate-phase.md +6 -1
  96. package/commands/gsd/verify-work.md +6 -1
  97. package/commands/gsd/workspace.md +7 -3
  98. package/gsd-core/bin/gsd-tools.cjs +477 -78
  99. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  100. package/gsd-core/bin/lib/adr-parser.cjs +3 -1
  101. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  102. package/gsd-core/bin/lib/audit.cjs +144 -42
  103. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  104. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  105. package/gsd-core/bin/lib/capability-registry.cjs +197 -222
  106. package/gsd-core/bin/lib/capability-validator.cjs +16 -1
  107. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  108. package/gsd-core/bin/lib/check-command-router.cjs +164 -1625
  109. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
  110. package/gsd-core/bin/lib/cli-exit.cjs +12 -0
  111. package/gsd-core/bin/lib/codex-agent-toml.cjs +32 -33
  112. package/gsd-core/bin/lib/command-aliases.cjs +7 -0
  113. package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
  114. package/gsd-core/bin/lib/commands.cjs +343 -207
  115. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  116. package/gsd-core/bin/lib/config-loader.cjs +65 -4
  117. package/gsd-core/bin/lib/config.cjs +76 -19
  118. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  119. package/gsd-core/bin/lib/coverage.cjs +4 -8
  120. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  121. package/gsd-core/bin/lib/decisions.cjs +30 -14
  122. package/gsd-core/bin/lib/drift.cjs +177 -42
  123. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  124. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  125. package/gsd-core/bin/lib/frontmatter.cjs +426 -234
  126. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  127. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  128. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  129. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  130. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  131. package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
  132. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  133. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  134. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  135. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  136. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  137. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  138. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  139. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  140. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  141. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  142. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  143. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  144. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  145. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  146. package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
  147. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  148. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  149. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  150. package/gsd-core/bin/lib/graphify.cjs +10 -2
  151. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  152. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  153. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  154. package/gsd-core/bin/lib/init.cjs +364 -132
  155. package/gsd-core/bin/lib/install-engine.cjs +30 -33
  156. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  157. package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
  158. package/gsd-core/bin/lib/io.cjs +122 -3
  159. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  160. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  161. package/gsd-core/bin/lib/milestone.cjs +37 -6
  162. package/gsd-core/bin/lib/model-resolver.cjs +171 -62
  163. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  164. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  165. package/gsd-core/bin/lib/pattern.cjs +10 -0
  166. package/gsd-core/bin/lib/phase-command-router.cjs +20 -5
  167. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  168. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  169. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  170. package/gsd-core/bin/lib/phase-id.cjs +110 -8
  171. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  172. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  173. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  174. package/gsd-core/bin/lib/phase.cjs +489 -88
  175. package/gsd-core/bin/lib/plan-document.cjs +142 -20
  176. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  177. package/gsd-core/bin/lib/planning-document.cjs +692 -0
  178. package/gsd-core/bin/lib/planning-inspect.cjs +60 -9
  179. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  180. package/gsd-core/bin/lib/planning-workspace.cjs +83 -55
  181. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  182. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  183. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  184. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  185. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  186. package/gsd-core/bin/lib/project-root.cjs +41 -2
  187. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  188. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
  189. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  190. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  191. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  192. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  193. package/gsd-core/bin/lib/roadmap-command-router.cjs +25 -19
  194. package/gsd-core/bin/lib/roadmap-parser.cjs +242 -15
  195. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1653 -65
  196. package/gsd-core/bin/lib/roadmap.cjs +405 -88
  197. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +373 -187
  198. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +5 -2
  199. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  200. package/gsd-core/bin/lib/runtime-homes.cjs +14 -7
  201. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +522 -624
  202. package/gsd-core/bin/lib/runtime-name-policy.cjs +246 -21
  203. package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
  204. package/gsd-core/bin/lib/shell-command-projection.cjs +47 -8
  205. package/gsd-core/bin/lib/smart-entry.cjs +19 -3
  206. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  207. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  208. package/gsd-core/bin/lib/state-document.cjs +100 -22
  209. package/gsd-core/bin/lib/state-transition.cjs +39 -2
  210. package/gsd-core/bin/lib/state.cjs +256 -105
  211. package/gsd-core/bin/lib/surface.cjs +19 -2
  212. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -79
  213. package/gsd-core/bin/lib/uat-predicate.cjs +359 -38
  214. package/gsd-core/bin/lib/uat.cjs +432 -7
  215. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  216. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +167 -40
  217. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  218. package/gsd-core/bin/lib/vendor/README.md +31 -9
  219. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  220. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  221. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  222. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  223. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  224. package/gsd-core/bin/lib/verification.cjs +1378 -274
  225. package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
  226. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  227. package/gsd-core/bin/lib/verify.cjs +357 -531
  228. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  229. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  230. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  231. package/gsd-core/bin/lib/workstream.cjs +11 -2
  232. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  233. package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
  234. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -0
  235. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  236. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  237. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  238. package/gsd-core/references/checkpoints.md +5 -3
  239. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  240. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  241. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  242. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  243. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  244. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  245. package/gsd-core/references/edge-probe.md +195 -21
  246. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  247. package/gsd-core/references/execute-phase-between-wave-reset.md +10 -6
  248. package/gsd-core/references/execute-phase-response-language.md +1 -1
  249. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  250. package/gsd-core/references/gsd-run-resolver.md +1 -1
  251. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  252. package/gsd-core/references/model-profiles.md +1 -1
  253. package/gsd-core/references/offer-next.md +1 -1
  254. package/gsd-core/references/phase-argument-parsing.md +9 -7
  255. package/gsd-core/references/phase-id-convention.md +28 -0
  256. package/gsd-core/references/planner-gap-closure.md +2 -0
  257. package/gsd-core/references/planner-load-graph-context.md +24 -13
  258. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  259. package/gsd-core/references/planning-config.md +12 -3
  260. package/gsd-core/references/spidr-splitting.md +1 -1
  261. package/gsd-core/references/tdd.md +37 -8
  262. package/gsd-core/references/ui-consideration-probe.md +10 -5
  263. package/gsd-core/references/verifier-phase-gates.md +5 -2
  264. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  265. package/gsd-core/references/verify-mvp-mode.md +2 -2
  266. package/gsd-core/references/workstream-flag.md +33 -3
  267. package/gsd-core/references/worktree-path-safety.md +321 -0
  268. package/gsd-core/templates/README.md +1 -1
  269. package/gsd-core/templates/UAT.md +17 -1
  270. package/gsd-core/templates/config.json +2 -11
  271. package/gsd-core/templates/verification-report.md +1 -1
  272. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  273. package/gsd-core/workflows/add-backlog.md +1 -1
  274. package/gsd-core/workflows/add-phase.md +8 -7
  275. package/gsd-core/workflows/add-tests.md +4 -3
  276. package/gsd-core/workflows/add-todo.md +6 -5
  277. package/gsd-core/workflows/ai-integration-phase.md +13 -4
  278. package/gsd-core/workflows/audit-fix.md +1 -1
  279. package/gsd-core/workflows/audit-milestone.md +4 -3
  280. package/gsd-core/workflows/audit-uat.md +1 -1
  281. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  282. package/gsd-core/workflows/autonomous.md +43 -23
  283. package/gsd-core/workflows/check-todos.md +7 -6
  284. package/gsd-core/workflows/cleanup.md +2 -2
  285. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  286. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  287. package/gsd-core/workflows/code-review-fix.md +112 -25
  288. package/gsd-core/workflows/code-review.md +146 -135
  289. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  290. package/gsd-core/workflows/complete-milestone.md +13 -8
  291. package/gsd-core/workflows/debug.md +32 -7
  292. package/gsd-core/workflows/diagnose-issues.md +3 -2
  293. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  294. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  295. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  296. package/gsd-core/workflows/discuss-phase.md +4 -3
  297. package/gsd-core/workflows/do.md +2 -2
  298. package/gsd-core/workflows/docs-update.md +6 -5
  299. package/gsd-core/workflows/edit-phase.md +4 -3
  300. package/gsd-core/workflows/eval-review.md +14 -5
  301. package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
  302. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1019 -0
  303. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  304. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  305. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
  306. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  307. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  308. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  309. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  310. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  311. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  312. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  313. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  314. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  315. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  316. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  317. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  318. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  319. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  320. package/gsd-core/workflows/execute-phase.md +96 -178
  321. package/gsd-core/workflows/execute-plan.md +18 -23
  322. package/gsd-core/workflows/explore.md +4 -4
  323. package/gsd-core/workflows/extract-learnings.md +4 -2
  324. package/gsd-core/workflows/fast.md +1 -1
  325. package/gsd-core/workflows/forensics.md +1 -1
  326. package/gsd-core/workflows/graduation.md +1 -1
  327. package/gsd-core/workflows/health.md +3 -2
  328. package/gsd-core/workflows/help/modes/full.compact.md +3 -3
  329. package/gsd-core/workflows/help/modes/full.md +5 -5
  330. package/gsd-core/workflows/help/modes/topic.md +15 -5
  331. package/gsd-core/workflows/import.md +4 -3
  332. package/gsd-core/workflows/inbox.md +2 -2
  333. package/gsd-core/workflows/ingest-docs.md +3 -3
  334. package/gsd-core/workflows/insert-phase.md +4 -3
  335. package/gsd-core/workflows/list-seeds.md +1 -1
  336. package/gsd-core/workflows/list-workspaces.md +1 -1
  337. package/gsd-core/workflows/manager.md +6 -4
  338. package/gsd-core/workflows/map-codebase.md +5 -4
  339. package/gsd-core/workflows/milestone-summary.md +3 -2
  340. package/gsd-core/workflows/mvp-phase.md +14 -14
  341. package/gsd-core/workflows/new-milestone.md +11 -11
  342. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  343. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  344. package/gsd-core/workflows/new-project.md +7 -7
  345. package/gsd-core/workflows/new-workspace.md +2 -2
  346. package/gsd-core/workflows/next.md +1 -1
  347. package/gsd-core/workflows/note.md +1 -1
  348. package/gsd-core/workflows/onboard.md +1 -1
  349. package/gsd-core/workflows/pause-work.md +2 -2
  350. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  351. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  352. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  353. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  354. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  355. package/gsd-core/workflows/plan-phase.md +50 -24
  356. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  357. package/gsd-core/workflows/plant-seed.md +62 -20
  358. package/gsd-core/workflows/pr-branch.md +113 -13
  359. package/gsd-core/workflows/profile-user.md +2 -2
  360. package/gsd-core/workflows/progress.md +19 -49
  361. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +27 -0
  362. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  363. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  364. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  365. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  366. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  367. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  368. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  369. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  370. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  371. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  372. package/gsd-core/workflows/quick-batch.md +15 -10
  373. package/gsd-core/workflows/quick.md +62 -39
  374. package/gsd-core/workflows/reapply-patches.md +9 -3
  375. package/gsd-core/workflows/remove-phase.md +3 -2
  376. package/gsd-core/workflows/remove-workspace.md +2 -2
  377. package/gsd-core/workflows/resume-project.md +3 -2
  378. package/gsd-core/workflows/review.md +33 -17
  379. package/gsd-core/workflows/scan.md +3 -2
  380. package/gsd-core/workflows/secure-phase.md +13 -13
  381. package/gsd-core/workflows/settings-advanced.md +30 -10
  382. package/gsd-core/workflows/settings-integrations.md +2 -3
  383. package/gsd-core/workflows/settings.md +4 -4
  384. package/gsd-core/workflows/ship.md +14 -13
  385. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  386. package/gsd-core/workflows/sketch.md +1 -1
  387. package/gsd-core/workflows/smart-entry.md +2 -2
  388. package/gsd-core/workflows/spec-phase.md +15 -5
  389. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  390. package/gsd-core/workflows/spike.md +1 -1
  391. package/gsd-core/workflows/stats.md +1 -1
  392. package/gsd-core/workflows/sync-skills.md +5 -5
  393. package/gsd-core/workflows/thread.md +2 -2
  394. package/gsd-core/workflows/transition.md +13 -23
  395. package/gsd-core/workflows/ui-phase.md +48 -11
  396. package/gsd-core/workflows/ui-review.md +21 -6
  397. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  398. package/gsd-core/workflows/undo.md +339 -20
  399. package/gsd-core/workflows/update.md +7 -7
  400. package/gsd-core/workflows/validate-phase.md +12 -13
  401. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  402. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  403. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  404. package/gsd-core/workflows/verify-work.md +129 -55
  405. package/hooks/dist/gsd-agent-isolation-guard.js +32 -0
  406. package/hooks/dist/gsd-check-update-worker.js +8 -0
  407. package/hooks/dist/gsd-check-update.js +8 -0
  408. package/hooks/dist/gsd-context-monitor.js +31 -8
  409. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  410. package/hooks/dist/gsd-secret-read-guard.js +161 -4
  411. package/hooks/dist/gsd-statusline.js +85 -20
  412. package/hooks/dist/gsd-update-banner.js +8 -0
  413. package/hooks/dist/gsd-validate-commit.sh +63 -4
  414. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  415. package/hooks/dist/gsd-workflow-guard.js +5 -4
  416. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  417. package/hooks/dist/lib/cli-exit.js +12 -0
  418. package/hooks/dist/lib/git-probe.js +17 -1
  419. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  420. package/hooks/gsd-agent-isolation-guard.js +32 -0
  421. package/hooks/gsd-check-update-worker.js +8 -0
  422. package/hooks/gsd-check-update.js +8 -0
  423. package/hooks/gsd-context-monitor.js +31 -8
  424. package/hooks/gsd-cursor-subagent-start.js +8 -0
  425. package/hooks/gsd-secret-read-guard.js +161 -4
  426. package/hooks/gsd-statusline.js +85 -20
  427. package/hooks/gsd-update-banner.js +8 -0
  428. package/hooks/gsd-validate-commit.sh +63 -4
  429. package/hooks/gsd-windsurf-pre-write.js +11 -2
  430. package/hooks/gsd-workflow-guard.js +5 -4
  431. package/hooks/gsd-worktree-path-guard.js +6 -2
  432. package/hooks/hooks.json +5 -5
  433. package/hooks/lib/cli-exit.js +12 -0
  434. package/hooks/lib/git-probe.js +17 -1
  435. package/hooks/lib/isolation-sentinel.js +2 -2
  436. package/package.json +22 -4
  437. package/scripts/build-hooks.js +15 -6
  438. package/scripts/changeset/parse.cjs +52 -4
  439. package/scripts/check-contract-drift.cjs +127 -11
  440. package/scripts/ci-timeout-report.cjs +770 -4
  441. package/scripts/command-contract-helpers.cjs +15 -8
  442. package/scripts/docs-guard-registry.cjs +34 -0
  443. package/scripts/gen-features.cjs +13 -8
  444. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  445. package/scripts/gen-loop-host-contract.cjs +79 -1
  446. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  447. package/scripts/gen-plugin-skills.cjs +87 -1
  448. package/scripts/gen-research-agents.cjs +24 -31
  449. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  450. package/scripts/gen-test-timings.cjs +32 -7
  451. package/scripts/lib/cli-exit.cjs +12 -0
  452. package/scripts/lib/macos-conformance-tier.generated.cjs +34 -2
  453. package/scripts/lib/ndjson-reporter.cjs +31 -5
  454. package/scripts/lib/platform-conformance-tier.generated.cjs +45 -5
  455. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  456. package/scripts/lib/vendor-bundle.cjs +59 -0
  457. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  458. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  459. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  460. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  461. package/scripts/lint-descriptions.cjs +7 -3
  462. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +38 -1
  463. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  464. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  465. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  466. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  467. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  468. package/scripts/lint-phase-id-drift.cjs +319 -5
  469. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  470. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  471. package/scripts/lint-response-language-coverage.cjs +3 -0
  472. package/scripts/lint-retired-runtime-name.cjs +619 -0
  473. package/scripts/lint-skill-deps.cjs +7 -3
  474. package/scripts/lint-state-write-path-drift.cjs +93 -0
  475. package/scripts/lint-test-file-count.allowlist.json +38 -9
  476. package/scripts/lint-test-file-count.cjs +34 -1
  477. package/scripts/lint-vendored-deps.cjs +41 -5
  478. package/scripts/lint-workflow-shellcheck-baseline.json +25 -10
  479. package/scripts/mutation-matrix.cjs +50 -5
  480. package/scripts/prompt-injection-scan.sh +4 -0
  481. package/scripts/release-tarball-smoke.cjs +194 -1
  482. package/scripts/require-issue-link-policy.cjs +6 -2
  483. package/scripts/sync-runtime-launcher.cjs +184 -2
  484. package/scripts/verify-npm-publish.cjs +76 -20
  485. package/skills/gsd-add-tests/SKILL.md +6 -1
  486. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  487. package/skills/gsd-audit-fix/SKILL.md +5 -0
  488. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  489. package/skills/gsd-autonomous/SKILL.md +7 -2
  490. package/skills/gsd-capture/SKILL.md +9 -5
  491. package/skills/gsd-code-review/SKILL.md +7 -2
  492. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  493. package/skills/gsd-config/SKILL.md +8 -4
  494. package/skills/gsd-debug/SKILL.md +11 -7
  495. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  496. package/skills/gsd-docs-update/SKILL.md +12 -7
  497. package/skills/gsd-eval-review/SKILL.md +6 -1
  498. package/skills/gsd-execute-phase/SKILL.md +12 -7
  499. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  500. package/skills/gsd-fast/SKILL.md +4 -0
  501. package/skills/gsd-forensics/SKILL.md +5 -1
  502. package/skills/gsd-graphify/SKILL.md +10 -6
  503. package/skills/gsd-health/SKILL.md +5 -0
  504. package/skills/gsd-help/SKILL.md +7 -2
  505. package/skills/gsd-import/SKILL.md +7 -3
  506. package/skills/gsd-inbox/SKILL.md +5 -0
  507. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  508. package/skills/gsd-manager/SKILL.md +6 -1
  509. package/skills/gsd-map-codebase/SKILL.md +7 -3
  510. package/skills/gsd-mempalace-capture/SKILL.md +12 -4
  511. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  512. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  513. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  514. package/skills/gsd-new-milestone/SKILL.md +6 -1
  515. package/skills/gsd-new-project/SKILL.md +5 -0
  516. package/skills/gsd-next/SKILL.md +6 -1
  517. package/skills/gsd-ns-context/SKILL.md +4 -0
  518. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  519. package/skills/gsd-ns-manage/SKILL.md +4 -0
  520. package/skills/gsd-ns-project/SKILL.md +4 -0
  521. package/skills/gsd-ns-review/SKILL.md +4 -0
  522. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  523. package/skills/gsd-onboard/SKILL.md +6 -1
  524. package/skills/gsd-pause-work/SKILL.md +5 -1
  525. package/skills/gsd-phase/SKILL.md +8 -4
  526. package/skills/gsd-plan-phase/SKILL.md +6 -1
  527. package/skills/gsd-plan-review-convergence/SKILL.md +10 -6
  528. package/skills/gsd-pr-branch/SKILL.md +4 -0
  529. package/skills/gsd-profile-user/SKILL.md +5 -1
  530. package/skills/gsd-progress/SKILL.md +7 -2
  531. package/skills/gsd-quick/SKILL.md +16 -10
  532. package/skills/gsd-quick-batch/SKILL.md +21 -9
  533. package/skills/gsd-review/SKILL.md +7 -4
  534. package/skills/gsd-review-backlog/SKILL.md +3 -2
  535. package/skills/gsd-secure-phase/SKILL.md +6 -1
  536. package/skills/gsd-ship/SKILL.md +5 -0
  537. package/skills/gsd-sketch/SKILL.md +7 -2
  538. package/skills/gsd-spec-phase/SKILL.md +5 -1
  539. package/skills/gsd-spike/SKILL.md +8 -3
  540. package/skills/gsd-surface/SKILL.md +5 -1
  541. package/skills/gsd-thread/SKILL.md +4 -0
  542. package/skills/gsd-ui-phase/SKILL.md +6 -1
  543. package/skills/gsd-ui-review/SKILL.md +6 -1
  544. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  545. package/skills/gsd-undo/SKILL.md +5 -1
  546. package/skills/gsd-update/SKILL.md +6 -2
  547. package/skills/gsd-validate-phase/SKILL.md +6 -1
  548. package/skills/gsd-verify-work/SKILL.md +6 -1
  549. package/skills/gsd-workspace/SKILL.md +7 -3
  550. package/skills/gsd-workstreams/SKILL.md +6 -6
  551. package/vscode/package.json +1 -1
@@ -16,10 +16,10 @@ const node_fs_1 = __importDefault(require("node:fs"));
16
16
  const node_path_1 = __importDefault(require("node:path"));
17
17
  // eslint-disable-next-line @typescript-eslint/no-require-imports
18
18
  const io = require("./io.cjs");
19
- const { output, error } = io;
19
+ const { output, error, captureStdoutSyncWrites, resolveAtFileOutput } = io;
20
20
  // eslint-disable-next-line @typescript-eslint/no-require-imports
21
21
  const markdownSectionizer = require("./markdown-sectionizer.cjs");
22
- const { collectSection, tokenizeHeadings, stripFencedCode, scanFencedBlocks } = markdownSectionizer;
22
+ const { collectSection, withSection, tokenizeHeadings, stripFencedCode, scanFencedBlocks } = markdownSectionizer;
23
23
  // eslint-disable-next-line @typescript-eslint/no-require-imports
24
24
  const markdownTable = require("./markdown-table.cjs");
25
25
  const { splitTableRow, isDelimiterRow } = markdownTable;
@@ -31,7 +31,8 @@ const planningWorkspace = require("./planning-workspace.cjs");
31
31
  const { planningDir } = planningWorkspace;
32
32
  // eslint-disable-next-line @typescript-eslint/no-require-imports
33
33
  const frontmatter = require("./frontmatter.cjs");
34
- const { extractFrontmatter, frontmatterListEntries, flattenObjectListItem } = frontmatter;
34
+ const { extractFrontmatter, spliceFrontmatter, frontmatterListEntries, flattenObjectListItem, isFrontmatterWriteRefusal } = frontmatter;
35
+ const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
35
36
  // eslint-disable-next-line @typescript-eslint/no-require-imports
36
37
  const phaseIdMod = require("./phase-id.cjs");
37
38
  const { PHASE_NUMBER_TOKEN_SOURCE, scopeToPhase } = phaseIdMod;
@@ -41,6 +42,13 @@ const { listMilestonePhaseDirs, getAllArchivedPhaseDirs } = phaseLocator;
41
42
  // eslint-disable-next-line @typescript-eslint/no-require-imports
42
43
  const auditMod = require("./audit.cjs");
43
44
  const { isAuditItemAcknowledged, deriveUatGapSnapshotValue } = auditMod;
45
+ // #5118: the verification-status owner's report reader and closed enum.
46
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
47
+ const verificationMod = require("./verification.cjs");
48
+ const { reportStatusOf, isReportContained, VERIFICATION_STATUS } = verificationMod;
49
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
50
+ const pristineBaseline = require("./pristine-baseline.cjs");
51
+ const { gitExec } = pristineBaseline;
44
52
  const security_cjs_1 = require("./security.cjs");
45
53
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
46
54
  const configLoader = require("./config-loader.cjs");
@@ -133,6 +141,13 @@ function cmdAuditUat(cwd, raw) {
133
141
  // the reason scopeToPhase has no unfiltered fallback.
134
142
  for (const file of selectPhaseUatFiles(files, dir)) {
135
143
  const uatFilePath = node_path_1.default.join(phaseDir, file);
144
+ // #5118 security review (SEC-1): containment BEFORE the read, exactly as
145
+ // for the VERIFICATION loop below — a `*-UAT.md` / `*-HUMAN-UAT.md`
146
+ // symlinked outside the phase directory is treated as absent, so the
147
+ // `### N. <name>` and `expected:` text it points at never reaches the
148
+ // audit output.
149
+ if (!isReportContained(phaseDir, uatFilePath))
150
+ continue;
136
151
  const content = readNormalizedDocument(uatFilePath);
137
152
  const { items, headingsSeen } = parseUatItemsWithStats(content);
138
153
  const uatFm = extractFrontmatter(content, uatFilePath);
@@ -202,14 +217,20 @@ function cmdAuditUat(cwd, raw) {
202
217
  // for the same reason as the UAT loop above.
203
218
  for (const file of scopeToPhase(files.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md')), dir)) {
204
219
  const verificationFilePath = node_path_1.default.join(phaseDir, file);
220
+ // #5118 security review (S1): containment BEFORE the read — an escaping
221
+ // report reads `missing` and never reaches `reportStatusOf`'s message.
222
+ if (!isReportContained(phaseDir, verificationFilePath))
223
+ continue;
205
224
  const content = readNormalizedDocument(verificationFilePath);
206
225
  const verFm = extractFrontmatter(content, verificationFilePath);
207
- const status = (verFm.status || 'unknown').toLowerCase();
226
+ // #5118: the owner's report reader judges `status` (exact match, no case
227
+ // folding); an out-of-set value throws VerificationStatusError.
228
+ const status = reportStatusOf(verFm, verificationFilePath);
208
229
  // #3805: same marker, same 'status' snapshot key as scanVerificationGaps,
209
230
  // and the same ORDERING — the open-status gate runs FIRST (a marker on
210
231
  // a file that would never surface is not a suppressed item), then the
211
232
  // acknowledgement suppresses what the gate surfaced.
212
- if (status === 'human_needed' || status === 'gaps_found') {
233
+ if (status === VERIFICATION_STATUS.HUMAN_NEEDED || status === VERIFICATION_STATUS.GAPS_FOUND) {
213
234
  if (isAuditItemAcknowledged(verFm, { snapshotKey: 'status', currentValue: status })) {
214
235
  acknowledgedFiles++;
215
236
  continue;
@@ -341,6 +362,360 @@ function cmdRenderCheckpoint(cwd, options = {}, raw) {
341
362
  checkpoint,
342
363
  }, raw, checkpoint);
343
364
  }
365
+ // ─── completeUatSession ─────────────────────────────────────────────────────
366
+ // #5105 R1: the ONE owner for "does completing this UAT session change
367
+ // anything material". Pure core; the CLI handler below writes/commits only
368
+ // when `changed` is true (ADR-5057 §3, #4981 UAT self-stale offender C1/C2).
369
+ //
370
+ // #5105 review S7/S8: section edits go through the shared markdown-sectionizer
371
+ // primitives (`withSection`/`collectSection`) rather than a hand-rolled `^## `
372
+ // scanner — a `## ` occurrence inside a fenced code block must not end the
373
+ // section, and `tokenizeHeadings` (which both are built on) is fence-aware.
374
+ // Frontmatter reads/writes are scoped to the `---`…`---` block only, never a
375
+ // whole-document regex — a body line that happens to start with `status:` or
376
+ // `updated:` must never be touched. `status` goes through
377
+ // `extractFrontmatter`/`spliceFrontmatter` (safe: `complete`/`partial` never
378
+ // contain a `:`); `updated` does NOT (see `setFrontmatterUpdated`'s doc
379
+ // comment for why an ISO-8601 timestamp can't round-trip through the same
380
+ // re-serializer without picking up a spurious quoting change).
381
+ /**
382
+ * Replace the `## Current Test` section body with `[testing complete]`,
383
+ * leaving every other line of the document untouched. Mirrors the section
384
+ * boundary convention `parseCurrentTest` reads (a level-2 `## Current Test`
385
+ * heading; the section body runs to the next `## ` heading or EOF) via the
386
+ * shared `withSection` primitive, which is fence-aware (a `## `-looking line
387
+ * inside a fenced code block is not a heading and cannot end the section).
388
+ *
389
+ * A document missing the heading altogether is returned unchanged (bounded
390
+ * no-op, `withSection`'s own contract on a heading miss) — that is a
391
+ * malformed-input case `completeUatSession`'s caller already never produces,
392
+ * and this function fails soft rather than throwing so the pure core stays
393
+ * total.
394
+ */
395
+ function setCurrentTestComplete(content) {
396
+ // `'\n[testing complete]'` (no trailing newline) mirrors the trimmed shape
397
+ // `collectSection` itself produces for an ALREADY-canonical section body —
398
+ // required for idempotence: when the section already reads exactly this,
399
+ // `withSection`'s own `newBody === section.body` no-op guard must fire, or
400
+ // every already-complete document would gain a spurious extra blank line on
401
+ // each pass (a real, if invisible-looking, material byte change). The line
402
+ // break is the document's own (its first line's terminator), so a CRLF
403
+ // document's already-canonical section is recognized as unchanged and a
404
+ // rewritten one never gains a bare-LF line.
405
+ const eol = /^[^\n]*\r\n/.test(content) ? '\r\n' : '\n';
406
+ return withSection(content, (h) => /^current\s+test$/i.test(h.text) && h.level === 2, () => `${eol}[testing complete]`, { levelBounded: true });
407
+ }
408
+ /**
409
+ * Set the frontmatter `status:` value, preserving every other key's raw text
410
+ * verbatim (`spliceFrontmatter`'s per-key fidelity) — scoped strictly to the
411
+ * `---`…`---` block, so a body line that happens to start with `status:` is
412
+ * never touched (#5105 review S8). `status` values here (`complete`/`partial`)
413
+ * never contain a `:`, so `reconstructFrontmatter`'s double-quoting rule for
414
+ * colon-bearing scalars never fires — see `withFrontmatterUpdated`'s doc
415
+ * comment for why `updated:` (an ISO-8601 timestamp, which ALWAYS contains a
416
+ * `:`) is deliberately NOT routed through this same round-trip.
417
+ *
418
+ * Throws `spliceFrontmatter`'s write refusal (`isFrontmatterWriteRefusal`)
419
+ * when the frontmatter block is not parseable YAML — a block like
420
+ * `status:complete` (no space) is never "repaired" by regeneration;
421
+ * `cmdUatCompleteSession` surfaces the refusal and writes/commits nothing.
422
+ */
423
+ function setFrontmatterStatus(content, status) {
424
+ const fm = extractFrontmatter(content);
425
+ return spliceFrontmatter(content, { ...fm, status });
426
+ }
427
+ /**
428
+ * The `---`…`---` frontmatter block's `[start, end)` character bounds in
429
+ * `content`, and where its closing fence line starts — located by
430
+ * `locateFrontmatterFence`, the one fence owner `spliceFrontmatter` and every
431
+ * reader use — or `null` if there is none.
432
+ */
433
+ function frontmatterBlockBounds(content) {
434
+ // A leading BOM (#2977) is not content before the fence — it sits before
435
+ // `start`, carried through unchanged.
436
+ const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(content);
437
+ return fence?.closed ? { start: fence.bom.length, end: fence.closingFenceEnd, closingStart: fence.closingStart } : null;
438
+ }
439
+ /**
440
+ * `line` inserted into `block` (the bounds' slice) as a new last line, just
441
+ * before its closing fence line — whatever that line is (`---`, `--- `, the
442
+ * lenient `----`) — with the line ending of the line before it.
443
+ */
444
+ function insertBeforeClosingFence(block, bounds, line) {
445
+ const closingOffset = bounds.closingStart - bounds.start;
446
+ const eol = block[closingOffset - 2] === '\r' ? '\r\n' : '\n';
447
+ return `${block.slice(0, closingOffset)}${line}${eol}${block.slice(closingOffset)}`;
448
+ }
449
+ /**
450
+ * Write the frontmatter `updated:` line, SCOPED to the matched
451
+ * `---`…`---` block only (#5105 review S8) — deliberately NOT routed through
452
+ * `extractFrontmatter`/`spliceFrontmatter`'s generic re-serializer the way
453
+ * `setFrontmatterStatus` is: `reconstructFrontmatter` double-quotes any
454
+ * scalar containing a `:`, and an ISO-8601 timestamp always contains one —
455
+ * round-tripping `updated` through it would silently reformat every
456
+ * `updated:`/`started:`-shaped timestamp the first material change touches,
457
+ * from `updated: 2026-01-01T00:00:00Z` to `updated: "2026-01-01T00:00:00Z"`,
458
+ * breaking every OTHER reader in the ecosystem that pattern-matches the
459
+ * historically-unquoted form. A key-line replace confined to the matched
460
+ * frontmatter block gets the same "body text is never touched" guarantee
461
+ * without that regression.
462
+ *
463
+ * Returns the whole document with `updated:` set to `value` — appending the
464
+ * key just before the closing `---` (with the block's own line ending) when
465
+ * the block exists but the key does not (S8: "a frontmatter lacking
466
+ * `updated:` gains one when changed"), or returning `content` unchanged when
467
+ * there is no frontmatter block at all (never manufacturing one here).
468
+ */
469
+ function setFrontmatterUpdated(content, value) {
470
+ const bounds = frontmatterBlockBounds(content);
471
+ if (!bounds)
472
+ return content;
473
+ const block = content.slice(bounds.start, bounds.end);
474
+ const keyLineRe = /^updated:.*$/m;
475
+ const line = `updated: ${value}`;
476
+ // A function replacer: the line is inserted literally, never read as a `$&`/`$'`/`` $` ``
477
+ // replacement pattern.
478
+ const newBlock = keyLineRe.test(block)
479
+ ? block.replace(keyLineRe, () => line)
480
+ : insertBeforeClosingFence(block, bounds, line);
481
+ return content.slice(0, bounds.start) + newBlock + content.slice(bounds.end);
482
+ }
483
+ /**
484
+ * The MATERIAL projection of a document for #5105 R1's comparison: every byte
485
+ * except the frontmatter `updated:` line (deny-by-default — everything else
486
+ * counts). The whole line, terminator included, is dropped, so a document
487
+ * LACKING an `updated:` line projects the same as one carrying any `updated:`
488
+ * value — presence is not material either, and "differs only in `updated`"
489
+ * holds when one side has no such line. A document with no frontmatter block,
490
+ * or frontmatter with no `updated` key, is returned unchanged.
491
+ */
492
+ function stripUpdatedForCompare(content) {
493
+ const bounds = frontmatterBlockBounds(content);
494
+ if (!bounds)
495
+ return content;
496
+ const block = content.slice(bounds.start, bounds.end);
497
+ // The block always ends with `<eol>---`, so an `updated:` line always has
498
+ // its own terminator to drop with it.
499
+ const newBlock = block.replace(/^updated:.*\r?\n/m, '');
500
+ return content.slice(0, bounds.start) + newBlock + content.slice(bounds.end);
501
+ }
502
+ /**
503
+ * #5105 R1 — pure core of `uat.complete-session`.
504
+ *
505
+ * Computes the resulting `status` using verify-work.md's `complete_session`
506
+ * criterion (the workflow step this command replaces, pinned at
507
+ * 518ccb0061:gsd-core/workflows/verify-work.md:522-545): `partial` when ANY
508
+ * row is `result: [pending]` (including a genuine parse gap — a row missing
509
+ * its `result:` line entirely counts the same as an explicit pending token),
510
+ * `result: blocked`, or a `result: skipped` row with no `reason:` field;
511
+ * `complete` otherwise. An `issue` row — resolved or not — is a DEFINITIVE
512
+ * result and never blocks completion on its own ("All tests have a
513
+ * definitive result (pass, issue, or skipped-with-reason)"); this is a
514
+ * deliberately looser criterion than `uat-predicate.cjs`'s `evaluateUatPassed`
515
+ * (the `phase uat-passed` gate), which does still block on an unresolved
516
+ * issue. Sets `## Current Test` to `[testing complete]`.
517
+ *
518
+ * #5105 R1 redesign (grilling-pass DESIGN DEFECT): the prior contract compared
519
+ * the candidate against the LIVE file, which never sees the UAT rows a
520
+ * verify-work session writes to the file (with the Write tool, uncommitted)
521
+ * DURING the session — `complete_session` is where those rows were meant to
522
+ * be committed, so a live-vs-result compare could report `changed: false` and
523
+ * never commit them. `baseline` is now an explicit second input — the file's
524
+ * content at git HEAD (`null` when untracked/absent; the caller resolves it,
525
+ * this core stays pure) — and the comparison is candidate-vs-baseline, not
526
+ * candidate-vs-live: `changed` means "does the candidate MATERIALLY differ
527
+ * from what is already committed", where MATERIAL excludes only the
528
+ * frontmatter `updated:` line — its value and its presence
529
+ * (`stripUpdatedForCompare`, deny-by-default,
530
+ * #5105 R1 "F10"). No material change → `{ changed: false, content }` returns
531
+ * the ORIGINAL `content` UNTOUCHED (not the candidate) — even a live document
532
+ * that differs from baseline ONLY in `updated:` is never written, and the
533
+ * caller performs zero writes and zero commits (the stated #5105 R1 decision:
534
+ * a live UAT that differs from the committed baseline ONLY in the frontmatter
535
+ * `updated:` value is never written). A material change → this function ALSO
536
+ * stamps `updated` from `clock()` on the candidate; the caller writes + commits.
537
+ *
538
+ * #5105 review finding 2 / M1: when the candidate is NOT materially changed
539
+ * from `baseline`, but the LIVE `content` differs from that candidate in
540
+ * something OTHER than `updated:` (e.g. a re-opened session left
541
+ * `status: testing` plus a pending `## Current Test` with no row changes),
542
+ * the byte-exact `baseline` form is restored (`restored: true`) — `content`
543
+ * IS `baseline` itself: `changed === false` means candidate and baseline differ
544
+ * at most in the `updated:` line, so the exact committed bytes (whatever the
545
+ * `updated:` line's spacing, position, or absence) are the restore target and
546
+ * no splice is performed. This restore path is DENIED (no write, `content`
547
+ * is the untouched live bytes, no `restored` flag) when live differs from the
548
+ * candidate ONLY in `updated:` — that case is covered by the decision above.
549
+ */
550
+ function completeUatSession(content, options = {}) {
551
+ const clock = options.clock || (() => new Date());
552
+ const baseline = options.baseline ?? null;
553
+ // Scoped to the `## Tests` section body ONLY — the `## Current Test`
554
+ // section can itself contain a `### N. Name` heading with no `result:`
555
+ // line (a still-pending test's own description), which would otherwise be
556
+ // mis-parsed as a 'missing' (blocking) item and poison the status
557
+ // computation for a session that is, in fact, fully complete. Reuses the
558
+ // module's own row parser (`parseUatItemsWithStats`) rather than
559
+ // `uat-predicate.cjs`'s independent one — `uat-predicate.cjs` encodes a
560
+ // DIFFERENT (stricter) passing criterion for a different gate, so re-using
561
+ // its parser without its criterion would still be re-deriving.
562
+ const testsSection = collectSection(content, (h) => /^tests$/i.test(h.text) && h.level === 2, { levelBounded: true });
563
+ const { items, headingsSeen } = parseUatItemsWithStats(testsSection ? testsSection.body : '');
564
+ const hasBlockingRow = items.some((item) => {
565
+ const result = item.result.toLowerCase();
566
+ if (result === 'pending' || result === 'blocked')
567
+ return true;
568
+ if (result === 'skipped')
569
+ return !item.reason;
570
+ return false;
571
+ });
572
+ const status = (headingsSeen > 0 || hasBlockingRow) ? 'partial' : 'complete';
573
+ let candidate = setFrontmatterStatus(content, status);
574
+ candidate = setCurrentTestComplete(candidate);
575
+ const changed = stripUpdatedForCompare(candidate) !== stripUpdatedForCompare(baseline ?? '');
576
+ if (!changed) {
577
+ // #5105 review finding 2 / M1: a material match against `baseline` does
578
+ // not mean `content` (the LIVE file) is already clean — a re-opened
579
+ // session can leave `status: testing` plus a pending `## Current Test`
580
+ // with no row changes, which normalizes to the SAME material candidate
581
+ // but is not byte-identical to what is already committed. Restore the
582
+ // byte-exact committed form ONLY when the live document differs from the
583
+ // candidate in something OTHER than `updated:` — a live document that
584
+ // differs from baseline/candidate ONLY in `updated:` must never be
585
+ // written (the stated decision above), so that case falls through to the
586
+ // untouched-`content` return below instead.
587
+ if (baseline !== null && stripUpdatedForCompare(content) !== stripUpdatedForCompare(candidate)) {
588
+ // `changed === false` means candidate and baseline differ at most in the `updated:` line,
589
+ // so the byte-exact restore target is `baseline` itself — no splice needed.
590
+ const restoredContent = baseline;
591
+ if (restoredContent !== content) {
592
+ return { changed: false, content: restoredContent, status, restored: true };
593
+ }
594
+ }
595
+ return { changed: false, content, status };
596
+ }
597
+ const updatedIso = clock().toISOString();
598
+ candidate = setFrontmatterUpdated(candidate, updatedIso);
599
+ return { changed: true, content: candidate, status };
600
+ }
601
+ /**
602
+ * Bounded, byte-exact read of `absPath` at git HEAD — the #5105 R1 `baseline`
603
+ * input. Routes through `pristine-baseline.cts`'s shared `gitExec` (not the
604
+ * trimming `execGit` shell projection, and not a second private
605
+ * `execFileSync` wrapper — #5105 review finding 4): the comparison this
606
+ * feeds is byte-sensitive (a trailing newline IS material), so the read must
607
+ * not trim anything.
608
+ *
609
+ * The pathspec is git-native: `git show HEAD:./<basename>` run from the file's
610
+ * own directory. A `./`-prefixed tree path is resolved by git relative to the
611
+ * command's cwd (the form `verify.cts` uses via `rev-parse --show-prefix`), so
612
+ * git — not JS — canonicalizes it: a symlinked `--cwd`, a project root in a
613
+ * subdirectory of the repository, and Windows 8.3 / drive-case / forward-slash
614
+ * toplevel shapes all resolve with no realpath or `path.relative` derivation,
615
+ * and git itself contains the path inside the repository.
616
+ *
617
+ * Returns `null` on any failure (git absent, not a repository, unborn HEAD,
618
+ * the path untracked/absent at HEAD) — never throws, matching the "no
619
+ * baseline" posture every other git-history reader in this codebase uses.
620
+ */
621
+ function readBaselineAtHead(absPath) {
622
+ try {
623
+ return gitExec(node_path_1.default.dirname(absPath), ['show', `HEAD:./${node_path_1.default.basename(absPath)}`]);
624
+ }
625
+ catch {
626
+ return null;
627
+ }
628
+ }
629
+ /**
630
+ * CLI command handler (#5105 R1): `uat.complete-session <uatPath> [--message <m>]`.
631
+ * Deny-by-default no-op on an unchanged session — zero writes, zero commits
632
+ * (closes C1 in the #5105 census); a live document differing from the
633
+ * committed baseline ONLY in the frontmatter `updated:` value is one such
634
+ * no-op and is never written. On a material change, writes the file and
635
+ * commits through the existing `cmdCommit` internals (honoring `commit_docs`),
636
+ * reading `cmdCommit`'s own result back via the shared `captureStdoutSyncWrites`
637
+ * helper (#5105 review S9 — the ONE such helper, also used by gsd-tools.cjs)
638
+ * instead of a hand-rolled `fs.writeSync` monkeypatch, so this command can
639
+ * report the real `committed`/`reason` outcome rather than assuming success.
640
+ * A third `changed:false, restored:true` outcome (M1) writes the file — a
641
+ * byte-exact restore to the committed baseline — but never commits: this is
642
+ * still a no-op from git's perspective (see `completeUatSession`'s own doc).
643
+ */
644
+ async function cmdUatCompleteSession(cwd, uatPathArg, options = {}, raw) {
645
+ if (!uatPathArg) {
646
+ error('UAT file required: use uat.complete-session <uatPath>');
647
+ return;
648
+ }
649
+ const resolvedPath = (0, security_cjs_1.requireSafePath)(uatPathArg, cwd, 'UAT file', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
650
+ if (!node_fs_1.default.existsSync(resolvedPath)) {
651
+ error(`UAT file not found: ${uatPathArg}`);
652
+ return;
653
+ }
654
+ const content = node_fs_1.default.readFileSync(resolvedPath, 'utf-8');
655
+ const relPath = toPosixPath(node_path_1.default.relative(cwd, resolvedPath));
656
+ const baseline = readBaselineAtHead(resolvedPath);
657
+ let result;
658
+ try {
659
+ result = completeUatSession(content, { baseline });
660
+ }
661
+ catch (err) {
662
+ // An unparseable (or key-unreconcilable) frontmatter block: fail closed —
663
+ // nothing is written and nothing is committed (`spliceFrontmatter` owns
664
+ // the refusal decision; this surfaces it).
665
+ if (!isFrontmatterWriteRefusal(err))
666
+ throw err;
667
+ error(`uat.complete-session: ${uatPathArg} — ${err.message}`);
668
+ return;
669
+ }
670
+ if (!result.changed) {
671
+ // #5105 review finding 2: a `restored` result still writes (byte-exact
672
+ // restore to HEAD's own committed form) but never commits — this is a
673
+ // no-op from git's perspective, not a new change.
674
+ if (result.restored) {
675
+ node_fs_1.default.writeFileSync(resolvedPath, result.content);
676
+ }
677
+ output({ changed: false, status: result.status, ...(result.restored ? { restored: true } : {}) }, raw);
678
+ return;
679
+ }
680
+ node_fs_1.default.writeFileSync(resolvedPath, result.content);
681
+ const message = options.message || `test: complete UAT session (${relPath})`;
682
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
683
+ const commandsMod = require('./commands.cjs');
684
+ // `raw: false` — cmdCommit's `raw: true` shape emits only a bare hash/reason
685
+ // STRING (its own `output(result, raw, hash || 'committed')` rawValue arm),
686
+ // not the structured `{ committed, reason }` object this command needs to
687
+ // read back; the full JSON envelope is captured here and never reaches the
688
+ // real stdout (captureStdoutSyncWrites), so suppressing it via `raw` would
689
+ // only destroy the information this command needs.
690
+ const captured = await captureStdoutSyncWrites(() => {
691
+ commandsMod.cmdCommit(cwd, message, [relPath], false, false, false);
692
+ });
693
+ let committed = false;
694
+ let reason;
695
+ try {
696
+ // io.cjs's output() redirects a >50KB JSON payload to a tmpfile and emits
697
+ // `@file:<path>` instead (never the case for a commit result in practice,
698
+ // but resolved defensively rather than assumed) — via the shared
699
+ // `resolveAtFileOutput` helper (#5105 review finding 7), the same one
700
+ // gsd-tools.cjs uses for its own `@file:` resolution.
701
+ const capturedJson = resolveAtFileOutput(captured);
702
+ const commitResult = JSON.parse(capturedJson);
703
+ committed = commitResult.committed === true;
704
+ if (!committed && typeof commitResult.reason === 'string')
705
+ reason = commitResult.reason;
706
+ }
707
+ catch {
708
+ reason = 'commit_output_unparseable';
709
+ }
710
+ const envelope = {
711
+ changed: true,
712
+ status: result.status,
713
+ committed,
714
+ };
715
+ if (reason)
716
+ envelope.reason = reason;
717
+ output(envelope, raw);
718
+ }
344
719
  // ─── parseCurrentTest ─────────────────────────────────────────────────────────
345
720
  function parseCurrentTest(content) {
346
721
  // #3707-CR: this is the render-checkpoint path's own independent ingress
@@ -1418,6 +1793,19 @@ function parseUatItemsWithStats(content) {
1418
1793
  const headingParts = parseTestRowHeadingText(current.text);
1419
1794
  const testNumber = headingParts.number;
1420
1795
  const testName = headingParts.name;
1796
+ // #4983: an `issue` test whose `## Gaps` entry has been reconciled to
1797
+ // `status: resolved` (verify-work.md's reconcile_gaps step, #1921) is a
1798
+ // fixed-and-verified issue — skip it exactly like a PASS token above so
1799
+ // the read-side/audit view stops surfacing it as an open row forever.
1800
+ // Scoped to `result === 'issue'` only, mirroring the #4983 gate fix's own
1801
+ // scope (src/uat-predicate.cts's isTestGapResolved). Trusts the `status:`
1802
+ // text the same way `parseGapsItems` below already trusts it for a Gaps
1803
+ // entry viewed on its own (this is the informational read layer, not the
1804
+ // gate — the filesystem-verified "resolved_by names an executed plan"
1805
+ // check lives only in the GATE, which is what actually blocks
1806
+ // `phase uat-passed`).
1807
+ if (result === 'issue' && isTestIssueResolvedInGaps(content, testNumber))
1808
+ continue;
1421
1809
  // Reuse the existing block-scalar/inline `expected:` grammar rather than
1422
1810
  // re-deriving a second one (#3707 defect 2). #3078 blocker: the block is
1423
1811
  // CLIPPED at its first top-level fence opener first — still raw text (a
@@ -1541,6 +1929,33 @@ function parseGapsItems(content) {
1541
1929
  items.push(...parseGapsTableItems(gapsSection.body));
1542
1930
  return items;
1543
1931
  }
1932
+ /**
1933
+ * #4983: true when the UAT file's `## Gaps` section records `testNum` as
1934
+ * `status: resolved` (case-insensitive). Reuses the same `collectSection` /
1935
+ * `splitGapsEntries` / `extractGapEntryFields` seam `parseGapsItems` above
1936
+ * already uses to find and read a Gaps entry, so this reader stays in
1937
+ * lockstep with that entry's own fence/nesting/quoting hardening rather than
1938
+ * re-deriving it.
1939
+ *
1940
+ * This is the READ-SIDE / audit view: it trusts the entry's `status:` text
1941
+ * exactly the way `parseGapsItems` already trusts it to skip a resolved Gaps
1942
+ * entry on its own — no filesystem check that `resolved_by` names an
1943
+ * executed plan. That stricter, filesystem-verified check belongs to the
1944
+ * GATE that actually blocks `phase uat-passed` (src/uat-predicate.cts's
1945
+ * `isTestGapResolved`), not to this informational listing.
1946
+ */
1947
+ function isTestIssueResolvedInGaps(content, testNum) {
1948
+ const gapsSection = collectSection(content, (h) => /^gaps$/i.test(h.text) && h.level === 2, { levelBounded: true });
1949
+ if (!gapsSection)
1950
+ return false;
1951
+ return splitGapsEntries(gapsSection.body).some((entryLines) => {
1952
+ const fields = extractGapEntryFields(entryLines);
1953
+ if (!fields.status || fields.status.toLowerCase() !== 'resolved')
1954
+ return false;
1955
+ const entryTest = fields.test;
1956
+ return !!entryTest && /^\d+$/.test(entryTest) && parseInt(entryTest, 10) === testNum;
1957
+ });
1958
+ }
1544
1959
  /**
1545
1960
  * Split a section body into its GFM pipe tables, one entry per table (#2766).
1546
1961
  *
@@ -3333,12 +3748,12 @@ function parseVerificationGapsItems(content) {
3333
3748
  */
3334
3749
  function parseVerificationItems(content, status, sourcePath) {
3335
3750
  const items = [];
3336
- if (status === 'gaps_found') {
3751
+ if (status === VERIFICATION_STATUS.GAPS_FOUND) {
3337
3752
  items.push(...parseHumanVerificationItems(content, sourcePath));
3338
3753
  items.push(...parseVerificationGapsItems(content));
3339
3754
  return items;
3340
3755
  }
3341
- if (status === 'human_needed') {
3756
+ if (status === VERIFICATION_STATUS.HUMAN_NEEDED) {
3342
3757
  return parseHumanVerificationItems(content, sourcePath);
3343
3758
  }
3344
3759
  return items;
@@ -3579,6 +3994,14 @@ function categorizeItem(rawResult, reason, blockedBy) {
3579
3994
  return 'blocked';
3580
3995
  }
3581
3996
  if (result === 'skipped') {
3997
+ // #4546 — a deliberately deferred follow-up (the verify-work writer's
3998
+ // "Deferred follow-up:" template, #1921) is its own category, checked
3999
+ // BEFORE the keyword families so e.g. "…on the release build next
4000
+ // version" is not misfiled as build_needed. Must agree with
4001
+ // uat-predicate.cts's DEFERRED_REASON_RE (gate/audit agreement,
4002
+ // #3078-CR), pinned by tests/uat-predicate.test.cjs.
4003
+ if (reason && /^["']?deferred follow-up\b/i.test(reason))
4004
+ return 'deferred';
3582
4005
  if (reason) {
3583
4006
  if (/server|not running|not available/i.test(reason))
3584
4007
  return 'server_blocked';
@@ -3604,6 +4027,8 @@ function categorizeItem(rawResult, reason, blockedBy) {
3604
4027
  module.exports = {
3605
4028
  cmdAuditUat,
3606
4029
  cmdRenderCheckpoint,
4030
+ completeUatSession,
4031
+ cmdUatCompleteSession,
3607
4032
  parseCurrentTest,
3608
4033
  parseUatItems,
3609
4034
  parseUatItemsWithStats,
@@ -117,6 +117,15 @@ function validateRequirement(element) {
117
117
  if (r.elements == null && !(typeof r.text === 'string' && r.text.trim())) {
118
118
  throw new Error(`element ${element.id} text must be a non-empty string when no elements override is provided`);
119
119
  }
120
+ // text_en (#4657) is optional, but when present it must be a non-empty string. An empty
121
+ // string is NOT caught by `??` (only null/undefined are), so an unvalidated `text_en: ''`
122
+ // would silently win `text_en ?? text` and classify against '' — the same fail-open shape
123
+ // #1110/#2773 already exist to eliminate, just moved one field over. Validated
124
+ // unconditionally (not gated on whether `elements` will make it unused) so bad data fails
125
+ // closed even when it happens to be dead for this particular call.
126
+ if (r.text_en != null && !(typeof r.text_en === 'string' && r.text_en.trim())) {
127
+ throw new Error(`element ${element.id} text_en must be a non-empty string when present`);
128
+ }
120
129
  }
121
130
  /** Validate a UI-consideration resolution against the UI verification vocabulary (delegated, D-06). */
122
131
  function validateResolution(resolution) {
@@ -143,7 +152,11 @@ function proposeConsiderations(element) {
143
152
  kinds = element.elements;
144
153
  }
145
154
  else {
146
- kinds = classifyElement(element.text);
155
+ // #4657: prefer the English translation when present — UI_CUES are English-only
156
+ // word-boundary patterns, so a non-English `text` (e.g. response_language projects)
157
+ // would otherwise classify to zero kinds. validateRequirement (called above) has
158
+ // already guaranteed text_en, if present, is a non-empty string.
159
+ kinds = classifyElement((element.text_en ?? element.text));
147
160
  if (kinds.length === 0) {
148
161
  // Prose present but no element cue matched. Do NOT silently drop it (#1110): a UI element
149
162
  // whose phrasing missed every cue would otherwise vanish from coverage with no signal — the
@@ -210,7 +223,7 @@ function proposeElements(elements) {
210
223
  const considerations = proposeConsiderations(el);
211
224
  const kinds = Array.isArray(el.elements)
212
225
  ? el.elements // already validated inside proposeConsiderations
213
- : classifyElement(el.text);
226
+ : classifyElement((el.text_en ?? el.text)); // #4657: text_en ?? text
214
227
  const unclassified = !Array.isArray(el.elements) && kinds.length === 0;
215
228
  const categories = unclassified ? [] : applicableCategories(kinds);
216
229
  return { id: el.id, kinds, categories, considerations, unclassified };