@opengsd/gsd-core 1.15.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 (487) 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/agents/gsd-code-fixer.compact.md +1 -1
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-code-reviewer.compact.md +5 -3
  7. package/agents/gsd-code-reviewer.md +8 -6
  8. package/agents/gsd-debug-session-manager.compact.md +1 -1
  9. package/agents/gsd-debug-session-manager.md +1 -1
  10. package/agents/gsd-debugger.md +2 -2
  11. package/agents/gsd-eval-auditor.compact.md +1 -1
  12. package/agents/gsd-eval-auditor.md +1 -1
  13. package/agents/gsd-executor.md +4 -4
  14. package/agents/gsd-intel-updater.compact.md +1 -1
  15. package/agents/gsd-intel-updater.md +1 -1
  16. package/agents/gsd-mempalace-curator.md +2 -2
  17. package/agents/gsd-phase-researcher.md +1 -1
  18. package/agents/gsd-plan-checker.md +7 -2
  19. package/agents/gsd-planner.md +3 -3
  20. package/agents/gsd-project-researcher.compact.md +1 -1
  21. package/agents/gsd-project-researcher.md +1 -1
  22. package/agents/gsd-research-synthesizer.compact.md +1 -1
  23. package/agents/gsd-research-synthesizer.md +1 -1
  24. package/agents/gsd-ui-auditor.compact.md +21 -30
  25. package/agents/gsd-ui-auditor.md +42 -51
  26. package/agents/gsd-ui-researcher.compact.md +1 -1
  27. package/agents/gsd-ui-researcher.md +1 -1
  28. package/agents/gsd-verifier.md +31 -9
  29. package/bin/install.js +126 -196
  30. package/commands/gsd/add-tests.md +6 -1
  31. package/commands/gsd/ai-integration-phase.md +6 -1
  32. package/commands/gsd/audit-fix.md +5 -0
  33. package/commands/gsd/audit-milestone.md +6 -1
  34. package/commands/gsd/autonomous.md +5 -0
  35. package/commands/gsd/capture.md +8 -4
  36. package/commands/gsd/code-review.md +7 -2
  37. package/commands/gsd/complete-milestone.md +4 -0
  38. package/commands/gsd/config.md +7 -3
  39. package/commands/gsd/debug.md +11 -7
  40. package/commands/gsd/discuss-phase.md +7 -3
  41. package/commands/gsd/docs-update.md +12 -7
  42. package/commands/gsd/eval-review.md +6 -1
  43. package/commands/gsd/execute-phase.md +12 -7
  44. package/commands/gsd/extract-learnings.md +5 -0
  45. package/commands/gsd/fast.md +4 -0
  46. package/commands/gsd/forensics.md +5 -1
  47. package/commands/gsd/graphify.md +10 -6
  48. package/commands/gsd/health.md +5 -0
  49. package/commands/gsd/help.md +7 -2
  50. package/commands/gsd/import.md +7 -3
  51. package/commands/gsd/inbox.md +5 -0
  52. package/commands/gsd/ingest-docs.md +5 -1
  53. package/commands/gsd/manager.md +6 -1
  54. package/commands/gsd/map-codebase.md +7 -3
  55. package/commands/gsd/mempalace-capture.md +5 -1
  56. package/commands/gsd/mempalace-recall.md +5 -1
  57. package/commands/gsd/milestone-summary.md +5 -1
  58. package/commands/gsd/mvp-phase.md +8 -3
  59. package/commands/gsd/new-milestone.md +6 -1
  60. package/commands/gsd/new-project.md +5 -0
  61. package/commands/gsd/next.md +6 -1
  62. package/commands/gsd/ns-context.md +4 -0
  63. package/commands/gsd/ns-ideate.md +4 -0
  64. package/commands/gsd/ns-manage.md +4 -0
  65. package/commands/gsd/ns-project.md +4 -0
  66. package/commands/gsd/ns-review.md +4 -0
  67. package/commands/gsd/ns-workflow.md +4 -0
  68. package/commands/gsd/onboard.md +6 -1
  69. package/commands/gsd/pause-work.md +5 -1
  70. package/commands/gsd/phase.md +8 -4
  71. package/commands/gsd/plan-phase.md +6 -1
  72. package/commands/gsd/plan-review-convergence.md +5 -1
  73. package/commands/gsd/pr-branch.md +4 -0
  74. package/commands/gsd/profile-user.md +5 -1
  75. package/commands/gsd/progress.md +6 -1
  76. package/commands/gsd/quick-batch.md +20 -8
  77. package/commands/gsd/quick.md +12 -7
  78. package/commands/gsd/review.md +5 -1
  79. package/commands/gsd/secure-phase.md +6 -1
  80. package/commands/gsd/ship.md +5 -0
  81. package/commands/gsd/sketch.md +7 -2
  82. package/commands/gsd/spec-phase.md +5 -1
  83. package/commands/gsd/spike.md +8 -3
  84. package/commands/gsd/surface.md +5 -1
  85. package/commands/gsd/thread.md +4 -0
  86. package/commands/gsd/ui-phase.md +6 -1
  87. package/commands/gsd/ui-review.md +6 -1
  88. package/commands/gsd/ultraplan-phase.md +5 -1
  89. package/commands/gsd/undo.md +5 -1
  90. package/commands/gsd/update.md +6 -2
  91. package/commands/gsd/validate-phase.md +6 -1
  92. package/commands/gsd/verify-work.md +6 -1
  93. package/commands/gsd/workspace.md +7 -3
  94. package/gsd-core/bin/gsd-tools.cjs +142 -56
  95. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  96. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  97. package/gsd-core/bin/lib/audit.cjs +78 -44
  98. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  99. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  100. package/gsd-core/bin/lib/capability-registry.cjs +183 -103
  101. package/gsd-core/bin/lib/capability-validator.cjs +16 -0
  102. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  103. package/gsd-core/bin/lib/check-command-router.cjs +164 -1712
  104. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
  105. package/gsd-core/bin/lib/cli-exit.cjs +12 -0
  106. package/gsd-core/bin/lib/codex-agent-toml.cjs +11 -8
  107. package/gsd-core/bin/lib/command-aliases.cjs +7 -0
  108. package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
  109. package/gsd-core/bin/lib/commands.cjs +173 -170
  110. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  111. package/gsd-core/bin/lib/config.cjs +43 -12
  112. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  113. package/gsd-core/bin/lib/coverage.cjs +4 -8
  114. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  115. package/gsd-core/bin/lib/drift.cjs +177 -42
  116. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  117. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  118. package/gsd-core/bin/lib/frontmatter.cjs +413 -234
  119. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  120. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  121. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  122. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  123. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  124. package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
  125. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  126. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  127. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  128. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  129. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  130. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  131. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  132. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  133. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  134. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  135. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  136. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  137. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  138. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  139. package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
  140. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  141. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  142. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  143. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  144. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  145. package/gsd-core/bin/lib/init.cjs +162 -96
  146. package/gsd-core/bin/lib/install-engine.cjs +19 -35
  147. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  148. package/gsd-core/bin/lib/io.cjs +122 -3
  149. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  150. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  151. package/gsd-core/bin/lib/milestone.cjs +19 -1
  152. package/gsd-core/bin/lib/model-resolver.cjs +18 -18
  153. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  154. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  155. package/gsd-core/bin/lib/pattern.cjs +10 -0
  156. package/gsd-core/bin/lib/phase-command-router.cjs +11 -4
  157. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  158. package/gsd-core/bin/lib/phase-id.cjs +1 -1
  159. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  160. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  161. package/gsd-core/bin/lib/phase.cjs +280 -80
  162. package/gsd-core/bin/lib/plan-document.cjs +93 -19
  163. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  164. package/gsd-core/bin/lib/planning-document.cjs +273 -40
  165. package/gsd-core/bin/lib/planning-inspect.cjs +42 -8
  166. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  167. package/gsd-core/bin/lib/planning-workspace.cjs +76 -53
  168. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  169. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  170. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  171. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  172. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
  173. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  174. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  175. package/gsd-core/bin/lib/roadmap-command-router.cjs +13 -15
  176. package/gsd-core/bin/lib/roadmap-parser.cjs +82 -15
  177. package/gsd-core/bin/lib/roadmap-upgrade.cjs +127 -65
  178. package/gsd-core/bin/lib/roadmap.cjs +169 -72
  179. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +91 -157
  180. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -1
  181. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  182. package/gsd-core/bin/lib/runtime-homes.cjs +13 -10
  183. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +307 -591
  184. package/gsd-core/bin/lib/runtime-name-policy.cjs +142 -27
  185. package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
  186. package/gsd-core/bin/lib/shell-command-projection.cjs +37 -2
  187. package/gsd-core/bin/lib/smart-entry.cjs +19 -3
  188. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  189. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  190. package/gsd-core/bin/lib/state-document.cjs +100 -22
  191. package/gsd-core/bin/lib/state.cjs +214 -105
  192. package/gsd-core/bin/lib/surface.cjs +2 -1
  193. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -152
  194. package/gsd-core/bin/lib/uat-predicate.cjs +313 -35
  195. package/gsd-core/bin/lib/uat.cjs +424 -7
  196. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +94 -58
  197. package/gsd-core/bin/lib/vendor/README.md +31 -9
  198. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  199. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  200. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  201. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  202. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  203. package/gsd-core/bin/lib/verification.cjs +1165 -314
  204. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  205. package/gsd-core/bin/lib/verify.cjs +286 -567
  206. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  207. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  208. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  209. package/gsd-core/bin/lib/workstream.cjs +11 -2
  210. package/gsd-core/bin/shared/config-defaults.manifest.json +5 -0
  211. package/gsd-core/bin/shared/config-schema.manifest.json +4 -0
  212. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  213. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  214. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  215. package/gsd-core/references/execute-phase-between-wave-reset.md +3 -0
  216. package/gsd-core/references/execute-phase-response-language.md +1 -1
  217. package/gsd-core/references/gsd-run-resolver.md +1 -1
  218. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  219. package/gsd-core/references/offer-next.md +1 -1
  220. package/gsd-core/references/planning-config.md +1 -1
  221. package/gsd-core/references/spidr-splitting.md +1 -1
  222. package/gsd-core/references/tdd.md +10 -4
  223. package/gsd-core/references/verifier-phase-gates.md +5 -2
  224. package/gsd-core/references/verify-mvp-mode.md +2 -2
  225. package/gsd-core/references/workstream-flag.md +33 -3
  226. package/gsd-core/templates/README.md +1 -1
  227. package/gsd-core/templates/UAT.md +17 -1
  228. package/gsd-core/templates/config.json +2 -11
  229. package/gsd-core/templates/verification-report.md +1 -1
  230. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  231. package/gsd-core/workflows/add-backlog.md +1 -1
  232. package/gsd-core/workflows/add-phase.md +8 -7
  233. package/gsd-core/workflows/add-tests.md +3 -2
  234. package/gsd-core/workflows/add-todo.md +4 -3
  235. package/gsd-core/workflows/ai-integration-phase.md +3 -2
  236. package/gsd-core/workflows/audit-fix.md +1 -1
  237. package/gsd-core/workflows/audit-milestone.md +4 -3
  238. package/gsd-core/workflows/audit-uat.md +1 -1
  239. package/gsd-core/workflows/autonomous.md +28 -18
  240. package/gsd-core/workflows/check-todos.md +6 -5
  241. package/gsd-core/workflows/cleanup.md +1 -1
  242. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  243. package/gsd-core/workflows/code-review-fix.md +5 -4
  244. package/gsd-core/workflows/code-review.md +84 -90
  245. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  246. package/gsd-core/workflows/complete-milestone.md +12 -7
  247. package/gsd-core/workflows/debug.md +30 -5
  248. package/gsd-core/workflows/diagnose-issues.md +3 -2
  249. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  250. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  251. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  252. package/gsd-core/workflows/discuss-phase.md +4 -3
  253. package/gsd-core/workflows/do.md +1 -1
  254. package/gsd-core/workflows/docs-update.md +4 -3
  255. package/gsd-core/workflows/edit-phase.md +4 -3
  256. package/gsd-core/workflows/eval-review.md +5 -3
  257. package/gsd-core/workflows/execute-phase/detail/elaboration.md +1 -1
  258. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +4 -2
  259. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  260. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  261. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
  262. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  263. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  264. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  265. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  266. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  267. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  268. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  269. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  270. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  271. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  272. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  273. package/gsd-core/workflows/execute-phase.md +78 -170
  274. package/gsd-core/workflows/execute-plan.md +13 -19
  275. package/gsd-core/workflows/explore.md +2 -2
  276. package/gsd-core/workflows/extract-learnings.md +3 -2
  277. package/gsd-core/workflows/fast.md +1 -1
  278. package/gsd-core/workflows/forensics.md +1 -1
  279. package/gsd-core/workflows/graduation.md +1 -1
  280. package/gsd-core/workflows/health.md +2 -1
  281. package/gsd-core/workflows/import.md +3 -2
  282. package/gsd-core/workflows/inbox.md +1 -1
  283. package/gsd-core/workflows/ingest-docs.md +1 -1
  284. package/gsd-core/workflows/insert-phase.md +4 -3
  285. package/gsd-core/workflows/list-seeds.md +1 -1
  286. package/gsd-core/workflows/list-workspaces.md +1 -1
  287. package/gsd-core/workflows/manager.md +5 -3
  288. package/gsd-core/workflows/map-codebase.md +4 -3
  289. package/gsd-core/workflows/milestone-summary.md +3 -2
  290. package/gsd-core/workflows/mvp-phase.md +14 -14
  291. package/gsd-core/workflows/new-milestone.md +10 -10
  292. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  293. package/gsd-core/workflows/new-project.md +1 -1
  294. package/gsd-core/workflows/new-workspace.md +1 -1
  295. package/gsd-core/workflows/next.md +1 -1
  296. package/gsd-core/workflows/pause-work.md +2 -2
  297. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  298. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  299. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  300. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  301. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +1 -1
  302. package/gsd-core/workflows/plan-phase.md +27 -18
  303. package/gsd-core/workflows/plan-review-convergence.md +1 -1
  304. package/gsd-core/workflows/plant-seed.md +1 -1
  305. package/gsd-core/workflows/pr-branch.md +1 -1
  306. package/gsd-core/workflows/profile-user.md +1 -1
  307. package/gsd-core/workflows/progress.md +19 -49
  308. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -2
  309. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  310. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  311. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  312. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  313. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  314. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  315. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  316. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  317. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  318. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  319. package/gsd-core/workflows/quick-batch.md +15 -10
  320. package/gsd-core/workflows/quick.md +42 -31
  321. package/gsd-core/workflows/remove-phase.md +3 -2
  322. package/gsd-core/workflows/remove-workspace.md +1 -1
  323. package/gsd-core/workflows/resume-project.md +3 -2
  324. package/gsd-core/workflows/review.md +3 -2
  325. package/gsd-core/workflows/scan.md +3 -2
  326. package/gsd-core/workflows/secure-phase.md +11 -12
  327. package/gsd-core/workflows/settings-advanced.md +1 -1
  328. package/gsd-core/workflows/settings-integrations.md +1 -1
  329. package/gsd-core/workflows/settings.md +1 -1
  330. package/gsd-core/workflows/ship.md +12 -12
  331. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  332. package/gsd-core/workflows/sketch.md +1 -1
  333. package/gsd-core/workflows/smart-entry.md +1 -1
  334. package/gsd-core/workflows/spec-phase.md +1 -1
  335. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  336. package/gsd-core/workflows/spike.md +1 -1
  337. package/gsd-core/workflows/stats.md +1 -1
  338. package/gsd-core/workflows/sync-skills.md +1 -1
  339. package/gsd-core/workflows/thread.md +2 -2
  340. package/gsd-core/workflows/transition.md +13 -23
  341. package/gsd-core/workflows/ui-phase.md +5 -4
  342. package/gsd-core/workflows/ui-review.md +4 -3
  343. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  344. package/gsd-core/workflows/undo.md +1 -1
  345. package/gsd-core/workflows/validate-phase.md +10 -12
  346. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  347. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  348. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  349. package/gsd-core/workflows/verify-work.md +68 -59
  350. package/hooks/dist/gsd-agent-isolation-guard.js +8 -0
  351. package/hooks/dist/gsd-check-update-worker.js +8 -0
  352. package/hooks/dist/gsd-check-update.js +8 -0
  353. package/hooks/dist/gsd-context-monitor.js +31 -8
  354. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  355. package/hooks/dist/gsd-secret-read-guard.js +147 -16
  356. package/hooks/dist/gsd-statusline.js +15 -7
  357. package/hooks/dist/gsd-update-banner.js +8 -0
  358. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  359. package/hooks/dist/gsd-workflow-guard.js +5 -4
  360. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  361. package/hooks/dist/lib/cli-exit.js +12 -0
  362. package/hooks/dist/lib/git-probe.js +17 -1
  363. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  364. package/hooks/gsd-agent-isolation-guard.js +8 -0
  365. package/hooks/gsd-check-update-worker.js +8 -0
  366. package/hooks/gsd-check-update.js +8 -0
  367. package/hooks/gsd-context-monitor.js +31 -8
  368. package/hooks/gsd-cursor-subagent-start.js +8 -0
  369. package/hooks/gsd-secret-read-guard.js +147 -16
  370. package/hooks/gsd-statusline.js +15 -7
  371. package/hooks/gsd-update-banner.js +8 -0
  372. package/hooks/gsd-windsurf-pre-write.js +11 -2
  373. package/hooks/gsd-workflow-guard.js +5 -4
  374. package/hooks/gsd-worktree-path-guard.js +6 -2
  375. package/hooks/hooks.json +5 -5
  376. package/hooks/lib/cli-exit.js +12 -0
  377. package/hooks/lib/git-probe.js +17 -1
  378. package/hooks/lib/isolation-sentinel.js +2 -2
  379. package/package.json +21 -4
  380. package/scripts/changeset/parse.cjs +52 -4
  381. package/scripts/ci-timeout-report.cjs +770 -4
  382. package/scripts/command-contract-helpers.cjs +12 -8
  383. package/scripts/docs-guard-registry.cjs +6 -0
  384. package/scripts/gen-features.cjs +13 -8
  385. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  386. package/scripts/gen-loop-host-contract.cjs +10 -1
  387. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  388. package/scripts/gen-plugin-skills.cjs +87 -1
  389. package/scripts/gen-research-agents.cjs +24 -31
  390. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  391. package/scripts/gen-test-timings.cjs +32 -7
  392. package/scripts/lib/cli-exit.cjs +12 -0
  393. package/scripts/lib/macos-conformance-tier.generated.cjs +20 -2
  394. package/scripts/lib/ndjson-reporter.cjs +28 -3
  395. package/scripts/lib/platform-conformance-tier.generated.cjs +34 -5
  396. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  397. package/scripts/lib/vendor-bundle.cjs +59 -0
  398. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  399. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  400. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  401. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  402. package/scripts/lint-descriptions.cjs +7 -3
  403. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +10 -0
  404. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  405. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  406. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  407. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  408. package/scripts/lint-phase-id-drift.cjs +29 -0
  409. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  410. package/scripts/lint-response-language-coverage.cjs +3 -0
  411. package/scripts/lint-skill-deps.cjs +7 -3
  412. package/scripts/lint-test-file-count.allowlist.json +11 -1
  413. package/scripts/lint-test-file-count.cjs +34 -1
  414. package/scripts/lint-vendored-deps.cjs +41 -5
  415. package/scripts/lint-workflow-shellcheck-baseline.json +10 -10
  416. package/scripts/mutation-matrix.cjs +50 -5
  417. package/scripts/require-issue-link-policy.cjs +6 -2
  418. package/scripts/sync-runtime-launcher.cjs +184 -2
  419. package/scripts/verify-npm-publish.cjs +76 -20
  420. package/skills/gsd-add-tests/SKILL.md +6 -1
  421. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  422. package/skills/gsd-audit-fix/SKILL.md +5 -0
  423. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  424. package/skills/gsd-autonomous/SKILL.md +5 -0
  425. package/skills/gsd-capture/SKILL.md +8 -4
  426. package/skills/gsd-code-review/SKILL.md +7 -2
  427. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  428. package/skills/gsd-config/SKILL.md +8 -4
  429. package/skills/gsd-debug/SKILL.md +11 -7
  430. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  431. package/skills/gsd-docs-update/SKILL.md +12 -7
  432. package/skills/gsd-eval-review/SKILL.md +6 -1
  433. package/skills/gsd-execute-phase/SKILL.md +12 -7
  434. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  435. package/skills/gsd-fast/SKILL.md +4 -0
  436. package/skills/gsd-forensics/SKILL.md +5 -1
  437. package/skills/gsd-graphify/SKILL.md +10 -6
  438. package/skills/gsd-health/SKILL.md +5 -0
  439. package/skills/gsd-help/SKILL.md +7 -2
  440. package/skills/gsd-import/SKILL.md +7 -3
  441. package/skills/gsd-inbox/SKILL.md +5 -0
  442. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  443. package/skills/gsd-manager/SKILL.md +6 -1
  444. package/skills/gsd-map-codebase/SKILL.md +7 -3
  445. package/skills/gsd-mempalace-capture/SKILL.md +5 -1
  446. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  447. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  448. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  449. package/skills/gsd-new-milestone/SKILL.md +6 -1
  450. package/skills/gsd-new-project/SKILL.md +5 -0
  451. package/skills/gsd-next/SKILL.md +6 -1
  452. package/skills/gsd-ns-context/SKILL.md +4 -0
  453. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  454. package/skills/gsd-ns-manage/SKILL.md +4 -0
  455. package/skills/gsd-ns-project/SKILL.md +4 -0
  456. package/skills/gsd-ns-review/SKILL.md +4 -0
  457. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  458. package/skills/gsd-onboard/SKILL.md +6 -1
  459. package/skills/gsd-pause-work/SKILL.md +5 -1
  460. package/skills/gsd-phase/SKILL.md +8 -4
  461. package/skills/gsd-plan-phase/SKILL.md +6 -1
  462. package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
  463. package/skills/gsd-pr-branch/SKILL.md +4 -0
  464. package/skills/gsd-profile-user/SKILL.md +5 -1
  465. package/skills/gsd-progress/SKILL.md +6 -1
  466. package/skills/gsd-quick/SKILL.md +16 -10
  467. package/skills/gsd-quick-batch/SKILL.md +20 -8
  468. package/skills/gsd-review/SKILL.md +5 -1
  469. package/skills/gsd-review-backlog/SKILL.md +3 -2
  470. package/skills/gsd-secure-phase/SKILL.md +6 -1
  471. package/skills/gsd-ship/SKILL.md +5 -0
  472. package/skills/gsd-sketch/SKILL.md +7 -2
  473. package/skills/gsd-spec-phase/SKILL.md +5 -1
  474. package/skills/gsd-spike/SKILL.md +8 -3
  475. package/skills/gsd-surface/SKILL.md +5 -1
  476. package/skills/gsd-thread/SKILL.md +4 -0
  477. package/skills/gsd-ui-phase/SKILL.md +6 -1
  478. package/skills/gsd-ui-review/SKILL.md +6 -1
  479. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  480. package/skills/gsd-undo/SKILL.md +5 -1
  481. package/skills/gsd-update/SKILL.md +6 -2
  482. package/skills/gsd-validate-phase/SKILL.md +6 -1
  483. package/skills/gsd-verify-work/SKILL.md +6 -1
  484. package/skills/gsd-workspace/SKILL.md +7 -3
  485. package/skills/gsd-workstreams/SKILL.md +6 -6
  486. package/vscode/package.json +1 -1
  487. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +0 -24
@@ -12,6 +12,11 @@
12
12
  * enforced by both consumers.
13
13
  */
14
14
 
15
+ // The one frontmatter fence owner (src/frontmatter-fence.cts, built to bin/lib by
16
+ // build:lib). Every consumer of this module runs after the build: lint:ci (CI builds
17
+ // bin/lib first) and the test suite (pretest).
18
+ const { locateFrontmatterFence } = require('../gsd-core/bin/lib/frontmatter-fence.cjs');
19
+
15
20
  const CANONICAL_TOOLS = new Set([
16
21
  'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep',
17
22
  'Task', 'Agent', 'Skill', 'SlashCommand',
@@ -22,16 +27,15 @@ const CANONICAL_TOOLS = new Set([
22
27
  ]);
23
28
 
24
29
  function parseFrontmatter(content) {
25
- // CRLF-tolerant split: Windows checkouts (autocrlf=true) leave a trailing
26
- // \r on every line, making lines.indexOf('---', 1) return -1 (the value
27
- // would be '---\r', not '---') → returns {} → every field appears missing.
28
- const lines = content.split(/\r?\n/);
29
- if (lines[0].trim() !== '---') return {};
30
- const end = lines.indexOf('---', 1);
31
- if (end === -1) return {};
30
+ // The block is the one the one fence owner finds (found while implementing
31
+ // #5105) — BOM, CRLF (a Windows autocrlf checkout), an adjacent empty block
32
+ // and a `----` look-alike all read exactly as every runtime reader sees them.
33
+ // Its lines are split CRLF-tolerantly so a trailing \r never leaks into a value.
34
+ const fence = locateFrontmatterFence(content);
35
+ if (!fence || !fence.closed) return {};
32
36
  const fm = {};
33
37
  let key = null;
34
- for (const line of lines.slice(1, end)) {
38
+ for (const line of content.slice(fence.openEnd, fence.bodyEnd).split(/\r?\n/)) {
35
39
  const kv = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)/);
36
40
  if (kv) { key = kv[1]; fm[key] = kv[2].trim(); }
37
41
  else if (key && line.match(/^\s+-\s+/)) {
@@ -201,6 +201,9 @@ const DOCS_GUARD_TESTS = {
201
201
  'tests/compact-content-4139.test.cjs': ['docs/CONFIGURATION.md'],
202
202
  'tests/config-field-docs.test.cjs': ['docs/CONFIGURATION.md'],
203
203
  'tests/config.test.cjs': ['docs/CONFIGURATION.md'],
204
+ // #4974: doc/workflow content-parity checks read docs/CONFIGURATION.md's
205
+ // Gate Settings section directly (see tests/gates-confirmation-toggle-config.test.cjs).
206
+ 'tests/gates-confirmation-toggle-config.test.cjs': ['docs/CONFIGURATION.md'],
204
207
  'tests/context-index-sync.test.cjs': ['docs/CONTEXT-INDEX.json'],
205
208
  'tests/context-predicates-query.test.cjs': ['docs/contributor-standards.md'],
206
209
  // SCAN_DIRS includes 'docs' and recursively walks every .md file under it
@@ -418,6 +421,9 @@ const DOCS_GUARD_TESTS = {
418
421
  'tests/verifier-behavior-unverified.test.cjs': ['docs/reference/planning-artifacts.md'],
419
422
  'tests/verifier-coincidental-reliance.test.cjs': ['docs/AGENTS.md'],
420
423
  'tests/verify.test.cjs': ['docs/reference/plan-md.md'],
424
+ // #5118 V43: asserts the deleted execute-phase/steps/stale-reverification.md
425
+ // is absent from the generated inventory manifest.
426
+ 'tests/verify-lifecycle-writes-e2e.test.cjs': ['docs/INVENTORY-MANIFEST.json'],
421
427
  'tests/workflow-fragments.test.cjs': ['docs/reference/workflow-fragments.md'],
422
428
  };
423
429
 
@@ -66,6 +66,10 @@ const fs = require('node:fs');
66
66
  const path = require('node:path');
67
67
 
68
68
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
69
+ // The one frontmatter fence owner (src/frontmatter-fence.cts, built by build:lib).
70
+ // Every entry point runs after the build: lint:generated-sync inside lint:ci (CI
71
+ // builds bin/lib first), regen:derived (`npm run build` first), and the tests.
72
+ const { locateFrontmatterFence } = require('../gsd-core/bin/lib/frontmatter-fence.cjs');
69
73
 
70
74
  const ROOT = path.resolve(__dirname, '..');
71
75
  const FEATURES_DIR = path.join(ROOT, 'docs', 'features');
@@ -255,13 +259,12 @@ function renderFrontmatter(data, body) {
255
259
  */
256
260
  function parseFrontmatter(text) {
257
261
  const normalized = String(text).replace(/\r\n/g, '\n');
258
- if (!normalized.startsWith('---\n')) return { data: null, body: normalized };
259
-
260
- const end = normalized.indexOf('\n---\n', 3);
261
- if (end === -1) return { data: null, body: normalized };
262
+ // The block is the one the one fence owner finds (found while implementing #5105).
263
+ const fence = locateFrontmatterFence(normalized);
264
+ if (!fence || !fence.closed) return { data: null, body: normalized };
262
265
 
263
266
  const data = {};
264
- for (const line of normalized.slice(4, end + 1).split('\n')) {
267
+ for (const line of normalized.slice(fence.openEnd, fence.bodyEnd).split('\n')) {
265
268
  if (line.trim() === '') continue;
266
269
  const sep = line.indexOf(':');
267
270
  if (sep === -1) continue;
@@ -270,9 +273,11 @@ function parseFrontmatter(text) {
270
273
  data[key] = parseScalar(line.slice(sep + 1).trim());
271
274
  }
272
275
 
273
- // `+5` clears "\n---\n"; the blank line renderFrontmatter emits after the
274
- // closing fence is consumed here so body text starts at its first real line.
275
- let body = normalized.slice(end + 5);
276
+ // Past the closing fence line and its line ending; the blank line
277
+ // renderFrontmatter emits after the closing fence is consumed here so body
278
+ // text starts at its first real line.
279
+ let body = normalized.slice(fence.closingFenceEnd);
280
+ if (body.startsWith('\n')) body = body.slice(1);
276
281
  if (body.startsWith('\n')) body = body.slice(1);
277
282
  return { data, body };
278
283
  }
@@ -18,6 +18,17 @@
18
18
  * target (`.js`, not `.cjs`, to match the hooks/lib/*.js convention) would
19
19
  * tangle a script that is otherwise simple and single-purpose.
20
20
  *
21
+ * compileToTemp() itself IS shared (required from ./gen-scripts-cli-exit.cjs)
22
+ * rather than duplicated: it compiles only src/cli-exit.cts (via a throwaway
23
+ * tsconfig that extends tsconfig.build.json with files/include scoped to
24
+ * that one module) rather than the whole project, since the emit this
25
+ * script needs depends only on cli-exit.cts's own import closure. A
26
+ * whole-project `-p tsconfig.build.json` type-check of every .cts file under
27
+ * a fixed COMPILE_TIMEOUT_MS ceiling can get killed by that timeout on a
28
+ * CPU-contended shared bench even though the one file this script needs
29
+ * compiles in a fraction of the time — and the compile strategy itself has
30
+ * nothing sibling-specific to fork.
31
+ *
21
32
  * Usage:
22
33
  * node scripts/gen-hooks-cli-exit.cjs # same as --write
23
34
  * node scripts/gen-hooks-cli-exit.cjs --write # write hooks/lib/cli-exit.js
@@ -35,14 +46,12 @@
35
46
 
36
47
  'use strict';
37
48
 
38
- const { execFileSync } = require('node:child_process');
39
49
  const fs = require('node:fs');
40
- const os = require('node:os');
41
50
  const path = require('node:path');
51
+ const { compileToTemp } = require('./gen-scripts-cli-exit.cjs');
42
52
 
43
53
  const REPO_ROOT = path.resolve(__dirname, '..');
44
54
  const OUTPUT_PATH = path.join(REPO_ROOT, 'hooks', 'lib', 'cli-exit.js');
45
- const COMPILE_TIMEOUT_MS = 60_000;
46
55
 
47
56
  /** Frozen reason codes so tests assert on structure, not prose. */
48
57
  const REASON = Object.freeze({
@@ -78,31 +87,6 @@ const BANNER = [
78
87
  '',
79
88
  ].join('\n');
80
89
 
81
- /** Compile the whole project to a throwaway outDir so the work tree is untouched. */
82
- function compileToTemp() {
83
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-hooks-cli-exit-'));
84
- try {
85
- execFileSync(
86
- process.execPath,
87
- [
88
- path.join(REPO_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'),
89
- '-p', path.join(REPO_ROOT, 'tsconfig.build.json'),
90
- '--outDir', tmp,
91
- // A throwaway outDir must not reuse the in-tree incremental state, or
92
- // tsc skips emit for files it believes are already current.
93
- '--incremental', 'false',
94
- '--tsBuildInfoFile', 'null',
95
- ],
96
- { cwd: REPO_ROOT, encoding: 'utf8', stdio: 'pipe', timeout: COMPILE_TIMEOUT_MS },
97
- );
98
- return { ok: true, dir: tmp };
99
- } catch (err) {
100
- fs.rmSync(tmp, { recursive: true, force: true });
101
- const detail = [err.stdout, err.stderr].filter(Boolean).join('\n').trim();
102
- return { ok: false, detail };
103
- }
104
- }
105
-
106
90
  /**
107
91
  * Compile src/cli-exit.cts to a throwaway outDir and return the expected
108
92
  * generated content (banner + compiled bytes, with the sibling registry
@@ -37,7 +37,16 @@ const STEP_WORKFLOWS = [
37
37
  { file: 'quick.md', point: 'plan:pre', kinds: ['contribution'], into: 'planner' },
38
38
  ],
39
39
  },
40
- { file: 'execute-phase.md', step: 'execute' },
40
+ {
41
+ file: 'execute-phase.md',
42
+ step: 'execute',
43
+ // #5118: execute:post's step AND gate dispatch (code_review_gate with
44
+ // its TDD escalation) lives in the ONE shared verification step that
45
+ // execute-phase includes (and verify-work includes too) — no per-caller copy.
46
+ auxiliaryHosts: [
47
+ { file: 'execute-phase/steps/verify-phase-goal.md', point: 'execute:post', kinds: ['step', 'gate'] },
48
+ ],
49
+ },
41
50
  { file: 'verify-work.md', step: 'verify' },
42
51
  { file: 'ship.md', step: 'ship' },
43
52
  ];
@@ -97,6 +97,26 @@
97
97
  * point the CLI at a small temp fixture tree instead of this repo's real,
98
98
  * 900+-file tests/ tree. Production usage (package.json's lint:generated-sync
99
99
  * / regen:derived chains) passes no flags and gets the real repo paths.
100
+ *
101
+ * #5074 — the `.platform` sibling rule. A handful of the heaviest tier files
102
+ * carry their real Windows signal in only a few tests out of hundreds; the
103
+ * fix is to move those tests into a same-directory `X.platform.test.cjs`
104
+ * sibling of `X.test.cjs`, so only the sibling (not the whole base) needs
105
+ * real-OS coverage. That split is enforced here as a FAILING invariant
106
+ * (`findPlatformSplitViolations`, wired into `classifyTree`) rather than as a
107
+ * silent filename-based exclusion rule, for two reasons: epic #4589 Phase 2
108
+ * requires real-OS overrides to be centrally enumerated, not implied by a
109
+ * naming convention (a bare "base excluded whenever a sibling exists" rule
110
+ * would BE such a convention); and a platform-sensitive test later added
111
+ * back into a split base must not silently re-admit the whole base to
112
+ * Linux-only on the Windows tier — it must fail generation loudly instead, at
113
+ * the same `lint:generated-sync` gate this module already backs. This
114
+ * invariant is scoped to the Windows CATEGORIES and wired only into
115
+ * `classifyTree`; `classifyMacosTree` stays pure content selection by design,
116
+ * so a macOS-only signal (e.g. case-sensitivity wording) left in a split base
117
+ * re-admits that base to the macOS tier instead of failing — over-inclusion,
118
+ * the safe direction. See docs/how-to/split-platform-sensitive-tests.md for
119
+ * the authoring rules this invariant enforces.
100
120
  */
101
121
 
102
122
  const fs = require('node:fs');
@@ -316,6 +336,140 @@ function classifyMacosContent(content) {
316
336
  return { needsRealOs: signals.length > 0, signals };
317
337
  }
318
338
 
339
+ /**
340
+ * The `.platform` sibling suffix (#5074). A sibling of `X.test.cjs` is a file
341
+ * whose basename ENDS with this suffix and has a non-empty stem before it —
342
+ * `foo.platform.test.cjs` is a sibling of `foo.test.cjs`, but a bare
343
+ * `platform.test.cjs` (empty stem) is not, and neither is a hyphenated name
344
+ * like `x-platform.test.cjs` or `platform-conformance-tier.test.cjs` (this
345
+ * module's own test file) — the rule keys on the literal dotted suffix, not
346
+ * on the substring "platform" appearing anywhere in the name.
347
+ */
348
+ const PLATFORM_SIBLING_SUFFIX = '.platform.test.cjs';
349
+
350
+ /**
351
+ * Parse `rel` (a POSIX `tests/...`-relative path) as a `.platform` sibling.
352
+ * Returns `{ dir, stem }` when it is one (dir is the POSIX dirname, stem is
353
+ * the basename with `PLATFORM_SIBLING_SUFFIX` stripped), or `null` when it is
354
+ * not — either because the basename does not end with the suffix, or because
355
+ * the stem would be empty (the `platform.test.cjs` negative-space case).
356
+ *
357
+ * @param {string} rel
358
+ * @returns {{ dir: string, stem: string } | null}
359
+ */
360
+ function parsePlatformSibling(rel) {
361
+ const dir = path.posix.dirname(rel);
362
+ const base = path.posix.basename(rel);
363
+ if (!base.endsWith(PLATFORM_SIBLING_SUFFIX)) return null;
364
+ const stem = base.slice(0, -PLATFORM_SIBLING_SUFFIX.length);
365
+ if (stem.length === 0) return null;
366
+ return { dir, stem };
367
+ }
368
+
369
+ /** Resolve `X.test.cjs`'s rel path for a `.platform` sibling's `{ dir, stem }`. */
370
+ function baseRelFor({ dir, stem }) {
371
+ return (dir === '.' ? '' : dir + '/') + stem + '.test.cjs';
372
+ }
373
+
374
+ /** Resolve `X.platform.test.cjs`'s rel path for a base rel path (inverse of baseRelFor). */
375
+ function siblingRelForBase(baseRel) {
376
+ const dir = path.posix.dirname(baseRel);
377
+ const stem = path.posix.basename(baseRel).slice(0, -'.test.cjs'.length);
378
+ return (dir === '.' ? '' : dir + '/') + stem + PLATFORM_SIBLING_SUFFIX;
379
+ }
380
+
381
+ /**
382
+ * The `.platform` sibling invariant (#5074), pure over an explicit `entries`
383
+ * array so it can be driven directly against fixtures (and against the real
384
+ * `ALWAYS_REAL_OS` map, which no temp fixture tree can key into) without
385
+ * touching the filesystem. `entries` is the ELIGIBLE (unit-suite) test-file
386
+ * set — a suite-tagged base is never a member, which is what makes it
387
+ * "never eligible" per the design doc's negative space rather than something
388
+ * this function has to special-case.
389
+ *
390
+ * For every `.platform` sibling present in `entries` (see
391
+ * `parsePlatformSibling`), checks three independent things against its base
392
+ * (`X.test.cjs`, same directory):
393
+ * - `base-has-signal` — the base IS in `entries` and still carries a
394
+ * genuine Windows signal (`classifyContent` on its own content) — the
395
+ * split did not actually remove the signal from the base.
396
+ * - `sibling-without-signal` — the sibling's OWN content carries no
397
+ * Windows signal, regardless of whether its base exists — a `.platform`
398
+ * file whose header claims a platform reason the classifier cannot see
399
+ * in its own text.
400
+ * - `base-always-real-os` — the base is a key of `alwaysRealOs`: the
401
+ * escape hatch says the WHOLE base needs real-OS coverage, which
402
+ * contradicts splitting only some of its tests into a sibling.
403
+ *
404
+ * A base that is absent from `entries` (suite-tagged, deleted, or renamed)
405
+ * never triggers `base-has-signal` — there is nothing to read.
406
+ *
407
+ * @param {{rel: string, content: string}[]} entries
408
+ * @param {Map<string, string>} [alwaysRealOs]
409
+ * @returns {{file: string, kind: 'base-has-signal'|'sibling-without-signal'|'base-always-real-os', signals: string[]}[]}
410
+ * sorted by file, then kind.
411
+ */
412
+ function findPlatformSplitViolations(entries, alwaysRealOs = ALWAYS_REAL_OS) {
413
+ const byRel = new Map(entries.map((entry) => [entry.rel, entry]));
414
+ const violations = [];
415
+
416
+ for (const entry of entries) {
417
+ const parsed = parsePlatformSibling(entry.rel);
418
+ if (!parsed) continue;
419
+ const baseRel = baseRelFor(parsed);
420
+
421
+ const base = byRel.get(baseRel);
422
+ if (base !== undefined) {
423
+ const { signals } = classifyContent(base.content);
424
+ if (signals.length > 0) {
425
+ violations.push({ file: baseRel, kind: 'base-has-signal', signals });
426
+ }
427
+ }
428
+
429
+ const { signals: siblingSignals } = classifyContent(entry.content);
430
+ if (siblingSignals.length === 0) {
431
+ violations.push({ file: entry.rel, kind: 'sibling-without-signal', signals: [] });
432
+ }
433
+
434
+ if (alwaysRealOs.has(baseRel)) {
435
+ violations.push({ file: baseRel, kind: 'base-always-real-os', signals: [] });
436
+ }
437
+ }
438
+
439
+ violations.sort((a, b) => {
440
+ if (a.file !== b.file) return a.file < b.file ? -1 : 1;
441
+ if (a.kind !== b.kind) return a.kind < b.kind ? -1 : 1;
442
+ return 0;
443
+ });
444
+ return violations;
445
+ }
446
+
447
+ /**
448
+ * Render `findPlatformSplitViolations`' result as the Error message
449
+ * `classifyTree` throws: starts with the literal phrase callers/tests grep
450
+ * for, lists one line per violation (file, kind, residual signal names, and
451
+ * — for `base-has-signal` only — the sibling path those tests belong in),
452
+ * and points at the how-to doc for the authoring rules.
453
+ *
454
+ * @param {ReturnType<typeof findPlatformSplitViolations>} violations
455
+ * @returns {string}
456
+ */
457
+ function formatPlatformSplitViolations(violations) {
458
+ const lines = violations.map((v) => {
459
+ let line = ` - ${v.file} [${v.kind}]`;
460
+ if (v.signals.length > 0) line += ` residual signal(s): ${v.signals.join(', ')}`;
461
+ if (v.kind === 'base-has-signal') {
462
+ line += ` — move these tests into ${siblingRelForBase(v.file)}`;
463
+ }
464
+ return line;
465
+ });
466
+ return (
467
+ `platform split invariant violated (#5074) — ${violations.length} file(s):\n` +
468
+ lines.join('\n') +
469
+ '\nSee docs/how-to/split-platform-sensitive-tests.md for the split-invariant rules.\n'
470
+ );
471
+ }
472
+
319
473
  /**
320
474
  * Recursively collect every `*.test.cjs` file beneath `dir`.
321
475
  * @param {string} dir
@@ -355,10 +509,15 @@ function classifyTree(testsDir) {
355
509
  // tier — see the header doc-comment for why suite-tagged files are excluded
356
510
  // entirely rather than merely deprioritized.
357
511
  const unitFiles = absoluteFiles.filter((absPath) => suiteOf(absPath) === null);
512
+ const entries = [];
358
513
  const flagged = [];
359
514
  for (const absPath of unitFiles) {
360
515
  const rel = 'tests/' + path.relative(testsDir, absPath).replace(/\\/g, '/');
361
516
  const content = fs.readFileSync(absPath, 'utf8');
517
+ // Read once, reused below both for this file's own flagging AND (via
518
+ // `entries`) for the #5074 split-invariant check — a second read of the
519
+ // same file for that check would double the I/O for no reason.
520
+ entries.push({ rel, content });
362
521
  const { needsRealOs } = classifyContent(content);
363
522
  // The ALWAYS_REAL_OS escape hatch (Windows tier only — see its doc
364
523
  // comment) is unioned in HERE, keyed off a file that this walk actually
@@ -369,6 +528,19 @@ function classifyTree(testsDir) {
369
528
  flagged.push(rel);
370
529
  }
371
530
  }
531
+
532
+ // #5074: a `.platform` sibling pair that violates the split invariant
533
+ // (residual signal left in the base, a sibling with no signal of its own,
534
+ // or a base that ALWAYS_REAL_OS says needs the whole file on real OS) fails
535
+ // generation loudly rather than being silently included or excluded. This
536
+ // check is Windows-CATEGORIES-only and wired here, not into
537
+ // classifyMacosTree below — a macOS-only signal in a split base is handled
538
+ // there by ordinary content selection (over-inclusion), not this invariant.
539
+ const violations = findPlatformSplitViolations(entries, ALWAYS_REAL_OS);
540
+ if (violations.length > 0) {
541
+ throw new Error(formatPlatformSplitViolations(violations));
542
+ }
543
+
372
544
  const result = [...new Set(flagged)].sort();
373
545
  return { total: absoluteFiles.length, files: result };
374
546
  }
@@ -484,7 +656,19 @@ function main() {
484
656
  const isMacos = target === 'macos';
485
657
  const label = isMacos ? 'gen-platform-conformance-tier --target macos' : 'gen-platform-conformance-tier';
486
658
  const exportKey = isMacos ? 'MACOS_CONFORMANCE_TIER_FILES' : 'CONFORMANCE_TIER_FILES';
487
- const { total, files } = isMacos ? classifyMacosTree(testsDir) : classifyTree(testsDir);
659
+ // classifyTree throws a plain Error on a #5074 split-invariant violation
660
+ // (it has no ExitError/exit-code opinion of its own — it is also called
661
+ // in-process by tests). runMain only special-cases ExitError (stderr gets
662
+ // err.message; anything else gets the full err.stack); wrapping it here
663
+ // keeps the CLI's stderr output to the clean, purpose-built violation
664
+ // report instead of a raw stack trace.
665
+ let total, files;
666
+ try {
667
+ ({ total, files } = isMacos ? classifyMacosTree(testsDir) : classifyTree(testsDir));
668
+ } catch (err) {
669
+ if (err instanceof ExitError) throw err;
670
+ throw new ExitError(1, err && err.message ? err.message : String(err));
671
+ }
488
672
 
489
673
  if (mode === 'write') {
490
674
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
@@ -554,4 +738,6 @@ module.exports = {
554
738
  MACOS_CATEGORIES,
555
739
  classifyMacosTree,
556
740
  renderMacosGeneratedFile,
741
+ PLATFORM_SIBLING_SUFFIX,
742
+ findPlatformSplitViolations,
557
743
  };
@@ -17,6 +17,21 @@
17
17
  * installer uses, producing a build-generated skills/ dir that ships in the
18
18
  * npm package and serves plugin-only installs.
19
19
  *
20
+ * #4995: commands/gsd/*.md still instruct bare command-position `gsd-tools
21
+ * <verb>` calls in 4 files (workstreams.md, quick.md, review-backlog.md,
22
+ * config.md), which fail with "command not found" on a shim-only install.
23
+ * That source can't be normalized to `gsd_run` directly the way #2751 did for
24
+ * agents/*.md and gsd-core/workflows/*.md: tests/gsd-tools-path-refs.test.cjs
25
+ * (#1766) pins commands/gsd/workstreams.md to the literal string
26
+ * `gsd-tools query workstream.list`, and bringing commands/ under the #2751
27
+ * guard was tried and reverted for unrelated load-bearing reasons (11ad7881ed
28
+ * / d286e54163). So this generator rewrites bare gsd-tools -> gsd_run in its
29
+ * OWN output — the same shape of fix #725 already established for the Codex
30
+ * conversion pipeline (rewrite the generated artifact, not the shared
31
+ * source) — reusing the exact `gsd_run` resolver preamble #2751 established
32
+ * (scripts/sync-runtime-launcher.cjs's transformFile) rather than inventing a
33
+ * new resolution mechanism.
34
+ *
20
35
  * Depends on: gsd-core/bin/lib/runtime-artifact-conversion.cjs (compiled from
21
36
  * src/runtime-artifact-conversion.cts by `npm run build:lib`). Must run AFTER
22
37
  * build:lib in the build chain.
@@ -25,6 +40,7 @@
25
40
  const fs = require('node:fs');
26
41
  const path = require('node:path');
27
42
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
43
+ const { transformFile, loadPreamble } = require('./sync-runtime-launcher.cjs');
28
44
 
29
45
  const ROOT = path.resolve(__dirname, '..');
30
46
  const COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd');
@@ -33,6 +49,73 @@ const CONVERSION_MODULE = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-art
33
49
  const PREFIX = 'gsd-';
34
50
  const RUNTIME = 'claude';
35
51
 
52
+ // #4995: the 4 commands/gsd/*.md stems (census taken by the issue) that carry
53
+ // command-position bare `gsd-tools <verb>` calls. Deliberately an explicit
54
+ // allowlist, NOT a blanket regex applied to every generated skill: several
55
+ // other skills (e.g. gsd-graphify) merely NAME `gsd-tools`/`gsd_run`
56
+ // descriptively (prose, "DO NOT use ..." warnings) in ways a command-position
57
+ // regex cannot safely tell apart from a real instruction without per-site
58
+ // human review -- exactly the ambiguity tests/no-bare-gsd-tools-command-
59
+ // position.test.cjs's PROSE_ALLOWLIST exists to resolve by hand for #2751/
60
+ // #3809. gsd-graphify also carries 5 deliberately-separate per-block
61
+ // preambles (tests/graphify-visualization.test.cjs extracts and runs each
62
+ // fenced Step-3 block standalone); running every skill through
63
+ // transformFile's "collapse to one preamble per file" behavior would
64
+ // reproduce the exact regression d286e54163 already found and reverted for
65
+ // commands/gsd/graphify.md. Restricting the rewrite to the 4 files the
66
+ // census in #4995 actually names avoids all of that.
67
+ const BARE_GSD_TOOLS_STEMS = new Set(['workstreams', 'quick', 'review-backlog', 'config']);
68
+
69
+ // #4995: command-position bare `gsd-tools` -> `gsd_run` token swap. Mirrors 3
70
+ // of the 4 command-position shapes rewriteBareGsdToolsCommandsForCodex
71
+ // (src/runtime-artifact-conversion.cts) already covers for the Codex
72
+ // pipeline: start-of-line, inside `$( ... )`, and immediately after a
73
+ // backtick (inline "Run: `gsd-tools verb args`" prose). Deliberately DROPS
74
+ // that function's 4th shape -- a bare, unescaped `|` counted as a shell
75
+ // pipe separator -- because a Markdown table row (`| col | gsd-tools verb
76
+ // arg |`) also starts a cell with `| gsd-tools`, and none of the 11 sites in
77
+ // #4995's census need it: config.md's own routing TABLE names `gsd-tools
78
+ // query config-set-model-profile` descriptively in a `|`-delimited cell one
79
+ // row above the real operative site, and rewriting that cell too would be a
80
+ // change nobody asked for. (`&&`/`;` are kept: two-character or dedicated
81
+ // separators, not also a Markdown table delimiter.) Only applied to
82
+ // BARE_GSD_TOOLS_STEMS files (see above). Within those 4 files this narrower
83
+ // pattern still matches one site beyond the issue's 11-site census --
84
+ // gsd-quick/SKILL.md's <security_notes> line ("Status fields read via
85
+ // `gsd-tools query frontmatter.get`") is a backtick-wrapped DESCRIPTIVE
86
+ // mention, not a `Run:` instruction, but it has the identical command-
87
+ // position shape and gets swapped too. That's harmless (the sentence reads
88
+ // the same with `gsd_run` in it) and left as-is rather than special-cased,
89
+ // but the true count this generator fixes is 12 operative-shaped sites, not
90
+ // 11 -- the 12th just wasn't literal-command "operative" in the issue's own
91
+ // sense.
92
+ function rewriteBareGsdToolsCommandsToGsdRun(content) {
93
+ return content
94
+ .replace(/(^[ \t]*)gsd-tools(?=\s)/gm, '$1gsd_run')
95
+ .replace(/(\$\(\s*)gsd-tools(?=\s)/g, '$1gsd_run')
96
+ .replace(/(`\s*)gsd-tools(?=\s)/g, '$1gsd_run')
97
+ .replace(/((?:&&|;)\s*)gsd-tools(?=\s)/g, '$1gsd_run');
98
+ }
99
+
100
+ // #4995: after the token swap, any fenced bash/sh/shell block that now calls
101
+ // `gsd_run` needs the resolver preamble that defines it -- reuse
102
+ // sync-runtime-launcher.cjs's transformFile verbatim (the same mechanism
103
+ // applied to agents/*.md and gsd-core/workflows/*.md) rather than
104
+ // reimplementing preamble placement here. Inline (non-fenced) "Run: `gsd_run
105
+ // verb args`" prose has no fenced block to anchor a preamble to; per the
106
+ // precedent already set for this exact shape (commands/gsd/workstreams.md
107
+ // and config.md in 11ad7881ed), it is left as a bare `gsd_run` token --
108
+ // `gsd_run` is itself a shipped npm bin name, so it resolves identically to
109
+ // how the pre-fix `gsd-tools` form resolved once PATH carries it (e.g. via
110
+ // the CLAUDE_ENV_FILE export in an earlier-run preamble in the same
111
+ // session), without regressing anything that worked before.
112
+ function rewriteSkillContent(content) {
113
+ const swapped = rewriteBareGsdToolsCommandsToGsdRun(content);
114
+ const preamble = loadPreamble();
115
+ const withPreamble = transformFile(swapped, preamble);
116
+ return withPreamble === null ? swapped : withPreamble;
117
+ }
118
+
36
119
  function generateSkills(conversion) {
37
120
  const cmdNames = conversion.readGsdCommandNames();
38
121
  const files = fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md'));
@@ -41,7 +124,10 @@ function generateSkills(conversion) {
41
124
  const stem = file.slice(0, -3);
42
125
  const skillName = PREFIX + stem;
43
126
  const src = fs.readFileSync(path.join(COMMANDS_DIR, file), 'utf8');
44
- const converted = conversion.convertClaudeCommandToClaudeSkill(src, skillName, RUNTIME, cmdNames, true);
127
+ let converted = conversion.convertClaudeCommandToClaudeSkill(src, skillName, RUNTIME, cmdNames, true);
128
+ if (BARE_GSD_TOOLS_STEMS.has(stem)) {
129
+ converted = rewriteSkillContent(converted);
130
+ }
45
131
  results.push({ skillName, content: converted });
46
132
  }
47
133
  return results;
@@ -25,6 +25,9 @@ const path = require('node:path');
25
25
 
26
26
  const { PROFILES } = require('./research-profiles.cjs');
27
27
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
28
+ // The one frontmatter fence owner (src/frontmatter-fence.cts, built by build:lib).
29
+ // This dev-time script runs after the build (manually, and from the tests).
30
+ const { locateFrontmatterFence } = require('../gsd-core/bin/lib/frontmatter-fence.cjs');
28
31
 
29
32
  const ROOT = path.resolve(__dirname, '..');
30
33
  const AGENTS_DIR = path.join(ROOT, 'agents');
@@ -41,12 +44,13 @@ const AGENTS_DIR = path.join(ROOT, 'agents');
41
44
  * We read the CURRENT commented-hooks block from the agent file and preserve it
42
45
  * byte-for-byte; only name/description/tools/color are regenerated.
43
46
  */
44
- function buildFrontmatter(profile, existingFrontmatter) {
45
- // Extract the commented hooks section from the existing frontmatter, if any.
46
- // The hooks block starts at `# hooks:` and runs to (but not including) the
47
- // closing `---`. In the committed files there is NO blank line between
48
- // `color:` and `# hooks:`, so we append it directly after the color line's `\n`.
49
- const hooksMatch = existingFrontmatter.match(/(# hooks:[\s\S]*?)(?=\n---)/);
47
+ function buildFrontmatter(profile, existingRegion) {
48
+ // Extract the commented hooks section from the existing frontmatter region
49
+ // (the YAML text between the fences), if any. The hooks block starts at
50
+ // `# hooks:` and runs to the end of the region — the closing `---` follows it.
51
+ // In the committed files there is NO blank line between `color:` and
52
+ // `# hooks:`, so we append it directly after the color line's `\n`.
53
+ const hooksMatch = existingRegion.match(/(# hooks:[\s\S]*)$/);
50
54
  // hooksSuffix: if present, the block followed by a newline so `---` is on its own line;
51
55
  // if absent, empty string (the closing `---` follows directly after color's `\n`).
52
56
  const hooksSuffix = hooksMatch ? hooksMatch[1] + '\n' : '';
@@ -67,37 +71,26 @@ function buildFrontmatter(profile, existingFrontmatter) {
67
71
  /**
68
72
  * Parse a .md file and return { frontmatterRaw, body, frontmatterFields }.
69
73
  *
70
- * frontmatterRaw: the raw text between the first and second `---` delimiters (exclusive)
71
- * body: everything after the closing `---\n`
74
+ * frontmatterRaw: the YAML text between the opening and closing fences (exclusive)
75
+ * fullFrontmatter: the block from the opening fence through the closing fence line
76
+ * body: everything after the closing fence line's line ending
72
77
  * frontmatterFields: { name, description, color, tools }
78
+ *
79
+ * The block is the one the one fence owner finds (found while implementing #5105):
80
+ * it opens at byte 0 (after a BOM), exactly as the runtime that loads the agent reads it.
73
81
  */
74
82
  function parseAgentFile(filePath) {
75
83
  const raw = fs.readFileSync(filePath, 'utf8');
76
84
 
77
- // The frontmatter is between the first `---` line and the next `---` line.
78
- const lines = raw.split('\n');
79
- let start = -1;
80
- let end = -1;
81
- for (let i = 0; i < lines.length; i++) {
82
- if (lines[i].trim() === '---') {
83
- if (start === -1) {
84
- start = i;
85
- } else {
86
- end = i;
87
- break;
88
- }
89
- }
90
- }
91
-
92
- if (start === -1 || end === -1) {
85
+ const fence = locateFrontmatterFence(raw);
86
+ if (!fence || !fence.closed) {
93
87
  throw new Error('No valid frontmatter delimiters found in ' + filePath);
94
88
  }
95
89
 
96
- const frontmatterLines = lines.slice(start + 1, end);
97
- const frontmatterRaw = frontmatterLines.join('\n');
98
- // body includes the closing `---` line and everything after
99
- const fullFrontmatter = lines.slice(start, end + 1).join('\n');
100
- const body = lines.slice(end + 1).join('\n');
90
+ const frontmatterRaw = raw.slice(fence.openEnd, fence.bodyEnd);
91
+ const frontmatterLines = frontmatterRaw.split(/\r?\n/);
92
+ const fullFrontmatter = raw.slice(fence.bom.length, fence.closingFenceEnd);
93
+ const body = raw.slice(fence.closingFenceEnd).replace(/^\r?\n/, '');
101
94
 
102
95
  const fields = {};
103
96
  // Parse simple key: value pairs (not nested YAML, no multi-line values here)
@@ -214,9 +207,9 @@ function runCheck() {
214
207
  */
215
208
  function writeAgent(profile) {
216
209
  const agentPath = path.join(AGENTS_DIR, profile.name + '.md');
217
- const { fullFrontmatter, body } = parseAgentFile(agentPath);
210
+ const { frontmatterRaw, body } = parseAgentFile(agentPath);
218
211
 
219
- const newFrontmatter = buildFrontmatter(profile, fullFrontmatter);
212
+ const newFrontmatter = buildFrontmatter(profile, frontmatterRaw);
220
213
  const newContent = newFrontmatter + '\n' + body;
221
214
 
222
215
  fs.writeFileSync(agentPath, newContent, 'utf8');