@opengsd/gsd-core 1.9.1 → 1.11.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 (426) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debug-session-manager.md +11 -0
  6. package/agents/gsd-debugger.md +12 -246
  7. package/agents/gsd-doc-synthesizer.md +2 -4
  8. package/agents/gsd-executor.md +12 -10
  9. package/agents/gsd-integration-checker.md +3 -0
  10. package/agents/gsd-mempalace-curator.md +5 -2
  11. package/agents/gsd-phase-researcher.md +20 -1
  12. package/agents/gsd-plan-checker.md +46 -0
  13. package/agents/gsd-planner.md +49 -54
  14. package/agents/gsd-roadmapper.md +21 -3
  15. package/agents/gsd-user-profiler.md +3 -0
  16. package/agents/gsd-verifier.md +26 -73
  17. package/bin/install.js +1272 -1238
  18. package/bin/lib/ui-safety-gate.cjs +2 -0
  19. package/commands/gsd/code-review.md +1 -1
  20. package/commands/gsd/execute-phase.md +1 -1
  21. package/commands/gsd/map-codebase.md +1 -1
  22. package/commands/gsd/mempalace-capture.md +2 -2
  23. package/commands/gsd/mempalace-recall.md +1 -1
  24. package/commands/gsd/new-milestone.md +2 -2
  25. package/commands/gsd/plan-phase.md +1 -1
  26. package/commands/gsd/quick.md +1 -1
  27. package/commands/gsd/review-backlog.md +2 -1
  28. package/commands/gsd/verify-work.md +1 -1
  29. package/gsd-core/bin/gsd-tools.cjs +1009 -115
  30. package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
  31. package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
  32. package/gsd-core/bin/lib/api-coverage.cjs +123 -5
  33. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  35. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  36. package/gsd-core/bin/lib/audit.cjs +926 -202
  37. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  38. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  39. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  40. package/gsd-core/bin/lib/capability-registry.cjs +608 -148
  41. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  42. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  43. package/gsd-core/bin/lib/capability-validator.cjs +507 -24
  44. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  45. package/gsd-core/bin/lib/check-command-router.cjs +114 -38
  46. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  47. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  48. package/gsd-core/bin/lib/command-aliases.cjs +94 -0
  49. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  50. package/gsd-core/bin/lib/commands.cjs +665 -99
  51. package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
  52. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  53. package/gsd-core/bin/lib/config-loader.cjs +76 -0
  54. package/gsd-core/bin/lib/config.cjs +22 -2
  55. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  56. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  57. package/gsd-core/bin/lib/core-utils.cjs +217 -40
  58. package/gsd-core/bin/lib/decisions.cjs +23 -0
  59. package/gsd-core/bin/lib/docs.cjs +3 -2
  60. package/gsd-core/bin/lib/external-job.cjs +19 -4
  61. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  62. package/gsd-core/bin/lib/frontmatter.cjs +239 -32
  63. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  64. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  65. package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
  66. package/gsd-core/bin/lib/graphify.cjs +142 -27
  67. package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
  68. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  69. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  72. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  73. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  74. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  75. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  76. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  77. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  78. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  79. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  80. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  81. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  82. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  83. package/gsd-core/bin/lib/init.cjs +1325 -169
  84. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  85. package/gsd-core/bin/lib/install-engine.cjs +805 -264
  86. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  87. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  88. package/gsd-core/bin/lib/install-profiles.cjs +160 -57
  89. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  90. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  91. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  92. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  93. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  94. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  95. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  96. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  97. package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
  98. package/gsd-core/bin/lib/io.cjs +38 -3
  99. package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
  100. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  101. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  102. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  103. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  104. package/gsd-core/bin/lib/milestone.cjs +821 -109
  105. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  106. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  107. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  108. package/gsd-core/bin/lib/pattern.cjs +122 -0
  109. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  110. package/gsd-core/bin/lib/phase-id.cjs +507 -36
  111. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  112. package/gsd-core/bin/lib/phase-locator.cjs +258 -58
  113. package/gsd-core/bin/lib/phase.cjs +891 -156
  114. package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
  115. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  116. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  117. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  118. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  119. package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
  120. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  121. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  122. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  123. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  124. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
  125. package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
  126. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  127. package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
  128. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  129. package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
  130. package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
  131. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  132. package/gsd-core/bin/lib/roadmap.cjs +405 -84
  133. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
  134. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  135. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
  136. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  137. package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
  138. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
  139. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  140. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  141. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  142. package/gsd-core/bin/lib/security.cjs +104 -5
  143. package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
  144. package/gsd-core/bin/lib/smart-entry.cjs +154 -22
  145. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  146. package/gsd-core/bin/lib/state-document.cjs +152 -8
  147. package/gsd-core/bin/lib/state-transition.cjs +424 -105
  148. package/gsd-core/bin/lib/state.cjs +1927 -401
  149. package/gsd-core/bin/lib/surface.cjs +35 -10
  150. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  151. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  152. package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
  153. package/gsd-core/bin/lib/uat.cjs +706 -64
  154. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  155. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  156. package/gsd-core/bin/lib/unusable-input.cjs +33 -0
  157. package/gsd-core/bin/lib/update-context.cjs +8 -2
  158. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  159. package/gsd-core/bin/lib/validate.cjs +20 -6
  160. package/gsd-core/bin/lib/vendor/README.md +37 -0
  161. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  162. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  163. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  164. package/gsd-core/bin/lib/verification.cjs +287 -20
  165. package/gsd-core/bin/lib/verify.cjs +368 -880
  166. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  167. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
  169. package/gsd-core/bin/lib/workstream.cjs +8 -2
  170. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  171. package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
  172. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  173. package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
  174. package/gsd-core/references/agent-contracts.md +43 -26
  175. package/gsd-core/references/artifact-types.md +10 -3
  176. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  177. package/gsd-core/references/checkpoints.md +2 -2
  178. package/gsd-core/references/context-budget.md +1 -1
  179. package/gsd-core/references/debugger-techniques.md +255 -0
  180. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  181. package/gsd-core/references/doc-conflict-engine.md +1 -1
  182. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  184. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  185. package/gsd-core/references/execute-phase-response-language.md +1 -1
  186. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  187. package/gsd-core/references/gate-prompts.md +1 -1
  188. package/gsd-core/references/git-planning-commit.md +2 -1
  189. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  190. package/gsd-core/references/model-profiles.md +12 -4
  191. package/gsd-core/references/mvp-concepts.md +9 -9
  192. package/gsd-core/references/planner-guidance.md +3 -9
  193. package/gsd-core/references/planner-preconditions.md +1 -1
  194. package/gsd-core/references/planner-reviews.md +1 -1
  195. package/gsd-core/references/planning-config.md +8 -6
  196. package/gsd-core/references/research-documentation-lookup.md +5 -3
  197. package/gsd-core/references/revision-loop.md +1 -1
  198. package/gsd-core/references/specless-probe-fallback.md +8 -7
  199. package/gsd-core/references/universal-anti-patterns.md +3 -3
  200. package/gsd-core/references/verifier-phase-gates.md +192 -0
  201. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  202. package/gsd-core/references/verify-mvp-mode.md +1 -1
  203. package/gsd-core/references/workstream-flag.md +22 -6
  204. package/gsd-core/references/worktree-branch-check.md +2 -2
  205. package/gsd-core/templates/discussion-log.md +1 -1
  206. package/gsd-core/templates/phase-prompt.md +2 -4
  207. package/gsd-core/templates/state.md +4 -4
  208. package/gsd-core/templates/summary-complex.md +2 -0
  209. package/gsd-core/templates/summary-minimal.md +2 -0
  210. package/gsd-core/templates/summary-standard.md +2 -0
  211. package/gsd-core/templates/summary.md +2 -0
  212. package/gsd-core/templates/verification-report.md +9 -1
  213. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  214. package/gsd-core/workflows/audit-milestone.md +3 -0
  215. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  216. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  217. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  218. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  219. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  220. package/gsd-core/workflows/autonomous.md +33 -70
  221. package/gsd-core/workflows/cleanup.md +62 -3
  222. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  223. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
  224. package/gsd-core/workflows/code-review-fix.md +37 -10
  225. package/gsd-core/workflows/code-review.md +74 -166
  226. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  227. package/gsd-core/workflows/complete-milestone.md +160 -95
  228. package/gsd-core/workflows/debug.md +16 -17
  229. package/gsd-core/workflows/diagnose-issues.md +56 -8
  230. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  231. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  232. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  233. package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
  234. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  235. package/gsd-core/workflows/docs-update.md +8 -51
  236. package/gsd-core/workflows/edit-phase.md +26 -1
  237. package/gsd-core/workflows/eval-review.md +3 -5
  238. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
  239. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  240. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  241. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  242. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
  243. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  245. package/gsd-core/workflows/execute-phase.md +103 -187
  246. package/gsd-core/workflows/execute-plan.md +36 -4
  247. package/gsd-core/workflows/explore.md +131 -4
  248. package/gsd-core/workflows/fast.md +10 -2
  249. package/gsd-core/workflows/health.md +73 -4
  250. package/gsd-core/workflows/help/modes/full.md +6 -1
  251. package/gsd-core/workflows/import.md +4 -4
  252. package/gsd-core/workflows/ingest-docs.md +7 -6
  253. package/gsd-core/workflows/mvp-phase.md +6 -3
  254. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  255. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  256. package/gsd-core/workflows/new-milestone.md +35 -47
  257. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  258. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  259. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  260. package/gsd-core/workflows/new-project.md +27 -240
  261. package/gsd-core/workflows/next.md +12 -0
  262. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  263. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  264. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  265. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  266. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  267. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  268. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  269. package/gsd-core/workflows/plan-phase.md +89 -209
  270. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  271. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  272. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  273. package/gsd-core/workflows/progress.md +45 -159
  274. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  275. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  276. package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
  277. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  278. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  279. package/gsd-core/workflows/quick.md +55 -405
  280. package/gsd-core/workflows/resume-project.md +3 -0
  281. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  282. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  283. package/gsd-core/workflows/review.md +41 -13
  284. package/gsd-core/workflows/section-manifest.json +219 -0
  285. package/gsd-core/workflows/secure-phase.md +1 -1
  286. package/gsd-core/workflows/session-report.md +2 -1
  287. package/gsd-core/workflows/settings.md +66 -2
  288. package/gsd-core/workflows/ship.md +104 -44
  289. package/gsd-core/workflows/sketch.md +1 -1
  290. package/gsd-core/workflows/spec-phase.md +41 -20
  291. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  292. package/gsd-core/workflows/spike.md +50 -16
  293. package/gsd-core/workflows/sync-skills.md +106 -13
  294. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  295. package/gsd-core/workflows/transition.md +53 -31
  296. package/gsd-core/workflows/ui-phase.md +13 -12
  297. package/gsd-core/workflows/ui-review.md +2 -2
  298. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  299. package/gsd-core/workflows/update.md +19 -8
  300. package/gsd-core/workflows/validate-phase.md +1 -1
  301. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  302. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  303. package/gsd-core/workflows/verify-work.md +17 -65
  304. package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
  305. package/hooks/dist/gsd-check-update-worker.js +64 -12
  306. package/hooks/dist/gsd-check-update.js +19 -1
  307. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  308. package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
  309. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  310. package/hooks/dist/gsd-prompt-guard.js +21 -20
  311. package/hooks/dist/gsd-read-injection-scanner.js +45 -24
  312. package/hooks/dist/gsd-statusline.js +90 -6
  313. package/hooks/dist/gsd-update-banner.js +22 -1
  314. package/hooks/dist/gsd-workflow-guard.js +134 -36
  315. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  316. package/hooks/dist/gsd-write-guard.js +359 -0
  317. package/hooks/dist/lib/git-cmd.js +92 -59
  318. package/hooks/dist/lib/injection-patterns.js +45 -0
  319. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  320. package/hooks/dist/lib/isolation-sentinel.js +277 -0
  321. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  322. package/hooks/gsd-agent-isolation-guard.js +517 -0
  323. package/hooks/gsd-check-update-worker.js +64 -12
  324. package/hooks/gsd-check-update.js +19 -1
  325. package/hooks/gsd-cursor-pre-tool.js +0 -3
  326. package/hooks/gsd-cursor-subagent-start.js +607 -26
  327. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  328. package/hooks/gsd-prompt-guard.js +21 -20
  329. package/hooks/gsd-read-injection-scanner.js +45 -24
  330. package/hooks/gsd-statusline.js +90 -6
  331. package/hooks/gsd-update-banner.js +22 -1
  332. package/hooks/gsd-workflow-guard.js +134 -36
  333. package/hooks/gsd-worktree-path-guard.js +2 -1
  334. package/hooks/gsd-write-guard.js +359 -0
  335. package/hooks/hooks.json +12 -0
  336. package/hooks/lib/git-cmd.js +92 -59
  337. package/hooks/lib/injection-patterns.js +45 -0
  338. package/hooks/lib/isolation-deny-reason.js +39 -0
  339. package/hooks/lib/isolation-sentinel.js +277 -0
  340. package/hooks/managed-hooks-registry.cjs +2 -0
  341. package/package.json +31 -10
  342. package/pi/gsd.cjs +71 -12
  343. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  344. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  345. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  346. package/scripts/build-hooks.js +9 -0
  347. package/scripts/changeset/lint.cjs +68 -6
  348. package/scripts/changeset/serialize.cjs +5 -1
  349. package/scripts/check-alias-drift.cjs +7 -43
  350. package/scripts/check-contract-drift.cjs +297 -0
  351. package/scripts/ci-test-scope.cjs +19 -2
  352. package/scripts/command-contract-helpers.cjs +903 -1
  353. package/scripts/gen-adr-index.cjs +728 -38
  354. package/scripts/gen-capability-matrix.cjs +1 -1
  355. package/scripts/gen-capability-registry.cjs +3 -15
  356. package/scripts/gen-context-index.cjs +439 -0
  357. package/scripts/gen-health-docs.cjs +390 -0
  358. package/scripts/gen-inventory-manifest.cjs +150 -4
  359. package/scripts/gen-loop-host-contract.cjs +4 -24
  360. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  361. package/scripts/gen-registry.cjs +3 -14
  362. package/scripts/gen-section-manifest.cjs +638 -0
  363. package/scripts/generate-package-identity.cjs +4 -2
  364. package/scripts/lib/alias-drift-families.cjs +46 -0
  365. package/scripts/lib/drift-scan.cjs +278 -0
  366. package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
  367. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  368. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  369. package/scripts/lint-canary-version-leak.cjs +73 -0
  370. package/scripts/lint-command-contract.cjs +96 -13
  371. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  372. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  373. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  374. package/scripts/lint-default-flip-documentation.cjs +193 -0
  375. package/scripts/lint-docs-command-form.cjs +195 -0
  376. package/scripts/lint-docs-required.cjs +9 -1
  377. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  378. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  379. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  380. package/scripts/lint-example-parser-parity.cjs +395 -0
  381. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  382. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  383. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  384. package/scripts/lint-milestone-window-drift.cjs +468 -0
  385. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  386. package/scripts/lint-plan-count-drift.cjs +318 -0
  387. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  388. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  389. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  390. package/scripts/lint-regression-test-names.cjs +15 -13
  391. package/scripts/lint-removed-but-needed.cjs +320 -0
  392. package/scripts/lint-state-field-drift.cjs +805 -0
  393. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  394. package/scripts/lint-test-file-count.allowlist.json +40 -3
  395. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  396. package/scripts/lint-vendored-deps.cjs +124 -0
  397. package/scripts/mutation-matrix.cjs +13 -0
  398. package/scripts/pr-changed-files.cjs +63 -0
  399. package/scripts/pr-template-policy.cjs +14 -4
  400. package/scripts/prompt-injection-scan.sh +52 -6
  401. package/scripts/require-issue-link-policy.cjs +192 -0
  402. package/scripts/state-write-path-drift-baseline.json +19 -0
  403. package/scripts/sync-runtime-launcher.cjs +2 -4
  404. package/skills/gsd-autonomous/SKILL.md +0 -1
  405. package/skills/gsd-code-review/SKILL.md +1 -1
  406. package/skills/gsd-execute-phase/SKILL.md +1 -2
  407. package/skills/gsd-map-codebase/SKILL.md +1 -1
  408. package/skills/gsd-mempalace-capture/SKILL.md +2 -2
  409. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  410. package/skills/gsd-new-milestone/SKILL.md +2 -2
  411. package/skills/gsd-next/SKILL.md +0 -1
  412. package/skills/gsd-plan-phase/SKILL.md +1 -2
  413. package/skills/gsd-progress/SKILL.md +0 -1
  414. package/skills/gsd-quick/SKILL.md +1 -1
  415. package/skills/gsd-review-backlog/SKILL.md +2 -1
  416. package/skills/gsd-stats/SKILL.md +0 -1
  417. package/skills/gsd-verify-work/SKILL.md +1 -1
  418. package/vscode/package.json +1 -1
  419. package/gsd-core/workflows/discovery-phase.md +0 -298
  420. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  421. package/gsd-core/workflows/verify-phase.md +0 -577
  422. package/scripts/affected-tests-lib.cjs +0 -554
  423. package/scripts/gen-emitted-baseline.cjs +0 -145
  424. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  425. package/scripts/run-affected-tests.cjs +0 -7
  426. package/scripts/run-tests.cjs +0 -1050
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Anti-divergence drift guard for the completion-RATIO seam
6
+ * (epic #3180, ADR-3180 "Planning Semantic Model Single Owner").
7
+ *
8
+ * `src/phase-lifecycle.cts`'s `clampPercent(completed, total)` /
9
+ * `clampPercentFromFraction(fraction)` are the SINGLE canonical owner of
10
+ * "turn a completed/total pair into an integer completion percentage,
11
+ * clamped to 100". Until just before this guard was added, the identical
12
+ * expression `total > 0 ? Math.min(100, Math.round((completed / total) * 100)) : 0`
13
+ * was hand-inlined at six call sites across five modules while the owner sat
14
+ * exported and unused by them — the exact ADR-3180 divergence class, in a
15
+ * derivation the epic had not previously named. Those six sites have been
16
+ * migrated onto the owner; this guard is what stops a seventh copy.
17
+ *
18
+ * Per ADR-3180 Decision 4(a) this guard discovers call sites by SCANNING THE
19
+ * WHOLE `src/` TREE, not by consulting an allowlist of known files — an
20
+ * allowlist only measures re-derivations in files someone remembered to
21
+ * list, and a new call site added anywhere else would sail through silently.
22
+ *
23
+ * DETECTION. A line is a re-derivation when ALL THREE hold, on that ONE
24
+ * source line:
25
+ * (a) it calls one of the `Math.round(`/`Math.floor(`/`Math.trunc(`/
26
+ * `Math.ceil(` rounding family — MATH_ROUND_FAMILY_RE;
27
+ * (b) it SCALES by 100 — a `*` followed by optional whitespace then `100`
28
+ * at a word boundary — SCALE_100_RE;
29
+ * (c) it contains a DIVISION — an identifier/closing-bracket, optional
30
+ * whitespace, `/`, optional whitespace, an identifier/opening-paren —
31
+ * DIVISION_RE — AND that division's index in the line is EARLIER than
32
+ * the index of the `* 100` scale from (b).
33
+ *
34
+ * Clause (c)'s ORDERING requirement is the whole precision of this guard.
35
+ * `(a / b) * 100` — divide FIRST, scale SECOND — is a percentage: the
36
+ * completed/total-derived shape this guard exists to catch. `Math.round(n *
37
+ * 100) / 100` — scale FIRST, divide SECOND — is a completely unrelated
38
+ * idiom (2-decimal-place rounding of an already-fractional value) that
39
+ * happens to share both a rounding call and a `* 100` token; it appears in
40
+ * this repo at `src/eval.cts` and `src/commands.cts` and MUST stay
41
+ * unflagged. Comparing leftmost-match indices (rather than merely testing
42
+ * "does a division exist anywhere on the line") is what tells the two
43
+ * idioms apart: this guard finds the EARLIEST division and the EARLIEST
44
+ * `* 100` scale on the line and requires divIdx < scaleIdx, so a line with a
45
+ * scale-then-divide shape (divIdx > scaleIdx, or no division at all) never
46
+ * matches, regardless of what else is on the line.
47
+ *
48
+ * `Math.floor(Math.random() * 100)` carries (a) and (b) but no division
49
+ * anywhere on the line (DIVISION_RE finds nothing, divIdx === -1) and is
50
+ * correctly excluded by clause (c) alone.
51
+ *
52
+ * `src/context-utilization.cts`'s `Math.min(Math.round(ratio * 100), 100)`
53
+ * is OUT OF SCOPE BY DOMAIN, not by exemption: it scales an
54
+ * ALREADY-COMPUTED fraction (`ratio`, a context-window utilization figure —
55
+ * unrelated to `.planning/` phase/plan completion) and there is no division
56
+ * anywhere on that line either, so clause (c) excludes it the same way as
57
+ * the `Math.random()` case above; it needs no FUNCTION_SCOPED_EXEMPTIONS
58
+ * entry because it was never going to match.
59
+ *
60
+ * Every regex below is small, bounded, and has no nested/overlapping
61
+ * quantifiers — each character class is followed by a fixed literal or a
62
+ * single `\s*` run bounded by the next required literal, so there is
63
+ * nothing for a backtracking engine to explore more than linearly.
64
+ * `npm run lint:ci` runs CodeQL js/redos over this repo; mirrors the
65
+ * ReDoS discipline of `lint-plan-count-drift.cjs` / `lint-milestone-window-drift.cjs`.
66
+ *
67
+ * The tree-walk / root-confinement / sanitizer machinery is SHARED with the
68
+ * sibling drift guards via `scripts/lib/drift-scan.cjs` (ADR-3180 Decision 4)
69
+ * — see that module for the `isInsideRoot` case-sensitivity note and the
70
+ * `walk` symlink-confinement rationale. This guard's detection shape needs
71
+ * no regex-LITERAL extraction (unlike the milestone-window guard), so it
72
+ * does not use `readRegexLiteralAt`; the reported fragment is simply the
73
+ * trimmed source line, bounded to MAX_REGEX_LITERAL_LEN characters.
74
+ *
75
+ * KNOWN, ACCEPTED limits of a per-line textual scan (same tradeoff the
76
+ * sibling drift guards document): a re-derivation whose division and
77
+ * `Math.round`/scale are split across two DIFFERENT lines with no single
78
+ * line carrying all three tokens is not caught by this narrow shape, nor is
79
+ * one routed through a helper that itself performs the division one call
80
+ * away from the rounding. That is left to code review, not this regex.
81
+ */
82
+
83
+ const path = require('node:path');
84
+ const driftScan = require('./lib/drift-scan.cjs');
85
+ const { MAX_REGEX_LITERAL_LEN, sanitizeForReport, scanTree } = driftScan;
86
+
87
+ // (a) The `Math.round`/`Math.floor`/`Math.trunc`/`Math.ceil` rounding family,
88
+ // called with an open paren. `\b` before `Math` keeps this from matching
89
+ // inside a longer identifier (e.g. `fooMath.round(` never occurs in this
90
+ // codebase, but the boundary costs nothing and documents intent).
91
+ const MATH_ROUND_FAMILY_RE = /\bMath\.(?:round|floor|trunc|ceil)\(/;
92
+
93
+ // (b) A `* 100` scale — a `*` operator, optional whitespace, then the
94
+ // literal digits `100` at a word boundary (so `*1000` or `*100.5` do not
95
+ // match a bare `100` inside a longer number).
96
+ const SCALE_100_RE = /\*\s*100\b/;
97
+
98
+ // (c) A division: an identifier character/closing-bracket (the end of the
99
+ // numerator expression), optional whitespace, `/`, optional whitespace, an
100
+ // identifier character/opening-paren (the start of the denominator
101
+ // expression). Deliberately does not try to distinguish this from a regex
102
+ // literal or a `//` comment — the detection window is a Math.round-family
103
+ // call on the same line, which neither idiom co-occurs with in practice, and
104
+ // keeping the class small is what keeps the regex non-backtracking.
105
+ const DIVISION_RE = /[A-Za-z0-9_$)\]]\s*\/\s*[A-Za-z0-9_$(]/;
106
+
107
+ // Authored TypeScript source only (the generated bin/lib/*.cjs mirror it).
108
+ const SCAN_DIRS = ['src'];
109
+ const SCAN_EXT = new Set(['.cts', '.ts', '.mts']);
110
+
111
+ // The canonical owner defines the ratio-to-percent grammar. It is NOT
112
+ // exempt as a whole file (ADR-3180 Decision 4(a) forbids bare file
113
+ // allowlists) — it is scanned like every other file in SCAN_DIRS, and only
114
+ // the two named functions below are exempt, each for a documented reason.
115
+ // An unrelated re-derivation added elsewhere in this same file (including a
116
+ // future one) is still caught.
117
+ const OWNER_FILE = path.join('src', 'phase-lifecycle.cts');
118
+
119
+ // Per ADR-3180 Decision 4(a): function-scoped, not a bare file allowlist.
120
+ // - clampPercentFromFraction: `Math.min(100, Math.round(fraction * 100))`
121
+ // IS the canonical fraction-to-percent kernel this guard exists to
122
+ // protect, not a copy of it — every other caller in the tree is
123
+ // expected to CALL this function rather than re-express its body.
124
+ // - clampPercent: the canonical count-shaped entry point; it delegates to
125
+ // `clampPercentFromFraction(completed / total)` rather than computing
126
+ // `Math.round(...)` itself, so it is exempted for the same reason even
127
+ // though its own line does not currently carry a Math.round-family call.
128
+ const FUNCTION_SCOPED_EXEMPTIONS = new Map([[OWNER_FILE, new Set(['clampPercent', 'clampPercentFromFraction'])]]);
129
+
130
+ // Optional `export ` modifier, matching the sibling guards' convention —
131
+ // only a column-0 top-level `function` declaration updates the
132
+ // current-function tracker.
133
+ const TOP_LEVEL_FUNCTION_RE = /^(?:export\s+)?function\s+([A-Za-z0-9_]+)\s*\(/;
134
+
135
+ /**
136
+ * Pure: find every unsanctioned completion-ratio re-derivation in `text`.
137
+ * `relPath` is the repo-relative path, used both to report file:line and to
138
+ * apply the narrow, function-scoped owner exemptions above.
139
+ * Returns [{ line, found }].
140
+ */
141
+ function findCompletionRatioDrift(text, relPath) {
142
+ const out = [];
143
+ const lines = text.split('\n');
144
+ const exemptFunctions = FUNCTION_SCOPED_EXEMPTIONS.get(relPath) || null;
145
+ let currentFunction = null;
146
+ for (let i = 0; i < lines.length; i++) {
147
+ const line = lines[i];
148
+ const fnMatch = TOP_LEVEL_FUNCTION_RE.exec(line);
149
+ if (fnMatch) currentFunction = fnMatch[1];
150
+
151
+ if (!MATH_ROUND_FAMILY_RE.test(line)) continue;
152
+ const scaleIdx = line.search(SCALE_100_RE);
153
+ if (scaleIdx === -1) continue;
154
+ const divIdx = line.search(DIVISION_RE);
155
+ if (divIdx === -1 || divIdx >= scaleIdx) continue;
156
+
157
+ if (exemptFunctions && exemptFunctions.has(currentFunction)) continue;
158
+
159
+ out.push({ line: i + 1, found: line.trim().slice(0, MAX_REGEX_LITERAL_LEN) });
160
+ }
161
+ return out;
162
+ }
163
+
164
+ /**
165
+ * Scan the authored source tree and return every unsanctioned re-derivation,
166
+ * each annotated with the repo-relative file path.
167
+ */
168
+ function scanRepo(root) {
169
+ return scanTree({
170
+ root,
171
+ scanDirs: SCAN_DIRS,
172
+ scanExt: SCAN_EXT,
173
+ onFile(rel, text) {
174
+ // `rel` is already the REAL (canonical) path (scanTree resolves
175
+ // symlinks before calling onFile), so this — and
176
+ // FUNCTION_SCOPED_EXEMPTIONS above, also keyed on `rel` — match
177
+ // consistently regardless of which symlink reached the file.
178
+ return findCompletionRatioDrift(text, rel).map((d) => ({ file: rel, ...d }));
179
+ },
180
+ });
181
+ }
182
+
183
+ function main() {
184
+ const root = path.join(__dirname, '..');
185
+ const violations = scanRepo(root);
186
+ if (violations.length === 0) {
187
+ process.stdout.write('ok completion-ratio-drift: no unsanctioned completed/total percent re-derivations outside phase-lifecycle.cts\n');
188
+ return;
189
+ }
190
+ process.stderr.write('completion-ratio-drift: independent re-derivation(s) of completed/total percent found.\n');
191
+ process.stderr.write('Use src/phase-lifecycle.cjs `clampPercent(completed, total)` (or `clampPercentFromFraction(fraction)`\n');
192
+ process.stderr.write('when you already hold a fraction) instead of re-deriving Math.round((completed / total) * 100):\n');
193
+ for (const d of violations) {
194
+ // `d.file` is exactly as attacker-controlled as `d.found`: a repo can
195
+ // legally track a filename containing control bytes / bidi overrides,
196
+ // and it is a fork-PR-authored value reaching a CI log the same way the
197
+ // matched line text does — sanitize it at the same reporting boundary.
198
+ process.stderr.write(` ${sanitizeForReport(d.file)}:${d.line} ${sanitizeForReport(d.found)}\n`);
199
+ }
200
+ process.exitCode = 1;
201
+ }
202
+
203
+ if (require.main === module) main();
204
+
205
+ module.exports = {
206
+ findCompletionRatioDrift,
207
+ scanRepo,
208
+ MATH_ROUND_FAMILY_RE,
209
+ SCALE_100_RE,
210
+ DIVISION_RE,
211
+ OWNER_FILE,
212
+ FUNCTION_SCOPED_EXEMPTIONS,
213
+ MAX_REGEX_LITERAL_LEN,
214
+ };
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * lint-default-flip-documentation.cjs — DEFECT.DEFAULT-FLIP-DOCUMENTATION
6
+ * (CONTEXT.md).
7
+ *
8
+ * ## Why
9
+ *
10
+ * A PR flips a config default but doesn't call out the migration semantics
11
+ * (when the new default takes effect; existing configs vs new configs; what
12
+ * the opt-back-in looks like — #3309, the v2 default flip from mid-flight to
13
+ * end-of-phase).
14
+ *
15
+ * ## Scope (deliberately narrower than the full DEFECT.detect clause)
16
+ *
17
+ * The DEFECT text names two surfaces: `CONFIG_DEFAULTS` and
18
+ * `buildNewProjectConfig`. This check covers ONLY the single-source-of-truth
19
+ * defaults manifest, `gsd-core/bin/shared/config-defaults.manifest.json`
20
+ * (what `CONFIG_DEFAULTS` in `src/configuration.cts` / `src/config.cts`
21
+ * actually loads at runtime) — because it is pure JSON, a resolved
22
+ * key→value-map diff between base and head is trivially reliable: no line
23
+ * movement, reordering, or refactor can ever produce a false "value changed"
24
+ * verdict, only an actual value change can.
25
+ *
26
+ * `buildNewProjectConfig`'s `hardcoded` object literal in `src/config.cts`
27
+ * is DELIBERATELY OUT OF SCOPE here. It mixes literal values with
28
+ * environment-derived branches (`hasBraveSearch`, etc.) and spreads of
29
+ * `CONFIG_DEFAULTS.*` — there is no reliable way to compute its *resolved*
30
+ * value map from source text alone without executing the compiled module at
31
+ * both refs, and a line/AST-level diff of that literal would inherit exactly
32
+ * the false-positive risk (a harmless refactor that moves or restructures
33
+ * the literal reads as a "flip") this check exists to avoid. Per the audit's
34
+ * own risk callout, a noisy check here is worse than no check — the
35
+ * `buildNewProjectConfig` half of the DEFECT stays prose-only.
36
+ *
37
+ * ## What this checks
38
+ *
39
+ * If any *value* differs between the base and head resolved manifest
40
+ * key→value maps (additions/removals alone don't count as a "flip" — the
41
+ * symptom is specifically about an EXISTING default changing), fail unless
42
+ * the PR body contains a `## Breaking Changes` (or `# Breaking Changes`)
43
+ * heading.
44
+ *
45
+ * Needs a PR event payload (`GITHUB_EVENT_PATH`) to read the PR body — this
46
+ * is a dedicated-workflow check (like `lint-canary-version-leak.cjs`), not a
47
+ * `lint:ci` member, since a local/push run has no PR body to check against.
48
+ */
49
+
50
+ const fs = require('node:fs');
51
+ const path = require('node:path');
52
+ const cp = require('node:child_process');
53
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
54
+
55
+ const ROOT = path.join(__dirname, '..');
56
+ const MANIFEST_PATH = path.join('gsd-core', 'bin', 'shared', 'config-defaults.manifest.json');
57
+ const BREAKING_CHANGES_RE = /^#{1,6}\s*Breaking Changes\b/im;
58
+
59
+ /**
60
+ * Pure: flatten a nested plain-object JSON value into dot-path
61
+ * `{ "a.b.c": value }` leaves. Arrays and primitives are leaves (compared by
62
+ * JSON.stringify equality, never recursed into) so array reordering reads as
63
+ * one value change, not N.
64
+ * @param {unknown} value
65
+ * @param {string} prefix
66
+ * @param {Record<string, unknown>} out
67
+ * @returns {Record<string, unknown>}
68
+ */
69
+ function flatten(value, prefix = '', out = {}) {
70
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
71
+ for (const [key, v] of Object.entries(value)) {
72
+ flatten(v, prefix ? `${prefix}.${key}` : key, out);
73
+ }
74
+ } else {
75
+ out[prefix] = value;
76
+ }
77
+ return out;
78
+ }
79
+
80
+ /**
81
+ * Pure: given two resolved (already-flattened) key→value maps, return the
82
+ * keys present in BOTH whose value differs. Additions/removals are NOT
83
+ * "flips" — a brand-new default has no prior behavior to contradict.
84
+ * @param {Record<string, unknown>} baseMap
85
+ * @param {Record<string, unknown>} headMap
86
+ * @returns {{ key: string, from: unknown, to: unknown }[]}
87
+ */
88
+ function findDefaultValueChanges(baseMap, headMap) {
89
+ const changes = [];
90
+ for (const key of Object.keys(baseMap)) {
91
+ if (!Object.prototype.hasOwnProperty.call(headMap, key)) continue;
92
+ if (JSON.stringify(baseMap[key]) !== JSON.stringify(headMap[key])) {
93
+ changes.push({ key, from: baseMap[key], to: headMap[key] });
94
+ }
95
+ }
96
+ return changes;
97
+ }
98
+
99
+ /**
100
+ * Pure verdict: given the detected default-value changes and the PR body,
101
+ * decide pass/fail.
102
+ * @param {{ key: string, from: unknown, to: unknown }[]} changes
103
+ * @param {string} prBody
104
+ * @returns {{ ok: boolean, changes: object[] }}
105
+ */
106
+ function evaluateDefaultFlipDoc(changes, prBody) {
107
+ if (changes.length === 0) return { ok: true, changes: [] };
108
+ if (BREAKING_CHANGES_RE.test(prBody || '')) return { ok: true, changes };
109
+ return { ok: false, changes };
110
+ }
111
+
112
+ /**
113
+ * Read and JSON.parse the manifest at a given git ref. Returns `{}` when the
114
+ * file doesn't exist at that ref (new file, or ref predates it) — that is
115
+ * not a "flip", it's an addition, and is silently excluded by
116
+ * findDefaultValueChanges's both-sides-present requirement anyway.
117
+ * @param {string} root
118
+ * @param {string} ref
119
+ * @returns {Record<string, unknown>}
120
+ */
121
+ function readManifestAtRef(root, ref) {
122
+ let raw;
123
+ try {
124
+ raw = cp.execFileSync('git', ['show', `${ref}:${MANIFEST_PATH.split(path.sep).join('/')}`], {
125
+ cwd: root,
126
+ encoding: 'utf8',
127
+ timeout: 15000,
128
+ });
129
+ } catch {
130
+ return {};
131
+ }
132
+ try {
133
+ return JSON.parse(raw);
134
+ } catch (e) {
135
+ throw new ExitError(2, `lint-default-flip-documentation: ${ref}:${MANIFEST_PATH} is not valid JSON: ${e.message}`);
136
+ }
137
+ }
138
+
139
+ function readPrBody() {
140
+ const eventPath = process.env.GITHUB_EVENT_PATH;
141
+ if (!eventPath || !fs.existsSync(eventPath)) return null;
142
+ try {
143
+ const event = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
144
+ return typeof event.pull_request?.body === 'string' ? event.pull_request.body : '';
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ function main() {
151
+ const prBody = readPrBody();
152
+ if (prBody === null) {
153
+ console.log('lint-default-flip-documentation: no PR event payload (not a pull_request run), skipping');
154
+ return;
155
+ }
156
+
157
+ const baseRef = `origin/${process.env.GITHUB_BASE_REF || 'next'}`; // #2988
158
+ const baseMap = flatten(readManifestAtRef(ROOT, baseRef));
159
+ const headMap = flatten(readManifestAtRef(ROOT, 'HEAD'));
160
+ const changes = findDefaultValueChanges(baseMap, headMap);
161
+ const verdict = evaluateDefaultFlipDoc(changes, prBody);
162
+
163
+ if (!verdict.ok) {
164
+ const detail = verdict.changes
165
+ .map((c) => ` ${c.key}: ${JSON.stringify(c.from)} → ${JSON.stringify(c.to)}`)
166
+ .join('\n');
167
+ throw new ExitError(
168
+ 1,
169
+ 'lint-default-flip-documentation: this PR changes an existing default value in\n'
170
+ + 'config-defaults.manifest.json (DEFECT.DEFAULT-FLIP-DOCUMENTATION) but the PR body has no\n'
171
+ + '`## Breaking Changes` section. Add one covering: (a) when the new default takes effect\n'
172
+ + '(config-set, fresh project, regenerated config), (b) the opt-back-in command\n'
173
+ + '(`gsd config-set <key> <old-value>`), (c) effect on in-flight artifacts. Changed default(s):\n'
174
+ + detail,
175
+ );
176
+ }
177
+ console.log(
178
+ changes.length === 0
179
+ ? 'ok lint-default-flip-documentation: no default value changed'
180
+ : `ok lint-default-flip-documentation: ${changes.length} default value change(s), PR body documents Breaking Changes`,
181
+ );
182
+ }
183
+
184
+ module.exports = {
185
+ flatten,
186
+ findDefaultValueChanges,
187
+ evaluateDefaultFlipDoc,
188
+ readManifestAtRef,
189
+ MANIFEST_PATH,
190
+ BREAKING_CHANGES_RE,
191
+ };
192
+
193
+ if (require.main === module) runMain(main);
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lint-docs-command-form.cjs
4
+ *
5
+ * Enforces the human-facing command form in docs/ (#2903).
6
+ *
7
+ * `/gsd:<cmd>` (and bare `gsd:<cmd>`) is a SOURCE-AUTHORING token: install-time
8
+ * converters (`transformContentToHyphen`, `convertSlashCommandsTo<Runtime>SkillMentions`)
9
+ * rewrite it to `/gsd-<cmd>` per-runtime. It is correct in `commands/gsd/**`,
10
+ * `gsd-core/workflows/**`, and `agents/**` — but docs are never passed through a
11
+ * converter, so a doc telling a reader to type `/gsd:<cmd>` names a command no
12
+ * runtime registers. The real user-facing form is `/gsd-<cmd>`.
13
+ *
14
+ * This guard fails any `docs/**\/*.md` file that still contains `/gsd:<cmd>` or
15
+ * bare `gsd:<cmd>` where `<cmd>` is a real command name (drawn from the
16
+ * `commands/gsd/*.md` roster). It explicitly permits `/gsd-core:<cmd>` (the
17
+ * Claude Code plugin namespace) and any `gsd:<token>` whose `<token>` is not a
18
+ * real command (e.g. the `gsd:section` / `gsd:loop-host` workflow-fragment
19
+ * marker syntax documented in docs/reference/workflow-fragments.md, or
20
+ * `gsd:command-name` used as a placeholder while explaining the Gemini CLI
21
+ * colon-form convention).
22
+ *
23
+ * Exclusions (never checked):
24
+ * - docs/adr/** (historical record)
25
+ * - docs/RELEASE-NOTES-LEGACY.md (maintainer question still open)
26
+ * - commands/gsd/**, gsd-core/workflows/**, agents/** — never touched by this
27
+ * guard at all; the colon form is correct there.
28
+ *
29
+ * Exemption — `name:` frontmatter key citations: source command files
30
+ * (`commands/gsd/*.md`) carry the colon form in their `name:` YAML frontmatter
31
+ * key (e.g. `name: gsd:next`). A doc that quotes that key verbatim — e.g.
32
+ * `` `name: gsd:next` `` or `name: gsd:next` — is citing the real source file,
33
+ * not telling a reader what to type. Rewriting that citation to `gsd-next`
34
+ * would make the doc lie about the source it's quoting, so a `gsd:<cmd>` token
35
+ * immediately preceded by `name:` (optionally with a backtick/whitespace in
36
+ * between) is permitted. This is narrow: it does not exempt `gsd:<cmd>`
37
+ * anywhere else on the line or file, including the reader-facing `/gsd-<cmd>`
38
+ * form that may appear later in the same sentence.
39
+ *
40
+ * Detection is case-insensitive (`/GSD:next`, `Gsd:Next`, etc. are all
41
+ * flagged) since the install-time converters and runtimes treat command names
42
+ * case-insensitively in practice, and a doc typo in casing is still a lie
43
+ * about the real command form.
44
+ *
45
+ * Exit 0 if no violations; exit 1 if any are found (with stderr diagnostics).
46
+ */
47
+
48
+ 'use strict';
49
+
50
+ const { execFileSync } = require('child_process');
51
+ const fs = require('fs');
52
+ const path = require('path');
53
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
54
+
55
+ const SELF_PATH = path.resolve(__filename);
56
+ // GSD_LINT_DOCS_COMMAND_FORM_REPO_ROOT is used by tests to redirect the guard to
57
+ // a temporary fixture git repo without touching the real working tree.
58
+ const REPO_ROOT = process.env.GSD_LINT_DOCS_COMMAND_FORM_REPO_ROOT
59
+ ? path.resolve(process.env.GSD_LINT_DOCS_COMMAND_FORM_REPO_ROOT)
60
+ : path.resolve(__dirname, '..');
61
+
62
+ const DOCS_PREFIX = 'docs/';
63
+ const ADR_PREFIX = 'docs/adr/';
64
+ const RELEASE_NOTES_LEGACY = 'docs/RELEASE-NOTES-LEGACY.md';
65
+ const COMMANDS_DIR = path.join(REPO_ROOT, 'commands/gsd');
66
+
67
+ // Matches `/gsd:<cmd>` and bare `gsd:<cmd>` (not part of `/gsd-core:<cmd>`,
68
+ // which does not contain the substring `gsd:` — the hyphen breaks it).
69
+ // Case-insensitive so `/GSD:next` / `Gsd:Next` are also caught.
70
+ const COMMAND_FORM_RE = /(^|[^A-Za-z0-9_-])(\/)?gsd:([A-Za-z0-9_-]+)/gi;
71
+
72
+ // A `gsd:<cmd>` token is exempt when it is a citation of a source file's
73
+ // YAML `name:` frontmatter key — i.e. the text immediately before the match
74
+ // (ending exactly where the match begins) is `name:` followed by optional
75
+ // whitespace and/or a backtick. Only applies to the bare (non-`/`) form,
76
+ // since the real frontmatter key never carries a leading slash.
77
+ const NAME_KEY_CITATION_RE = /name:\s*`?\s*$/i;
78
+
79
+ function loadRoster() {
80
+ let entries;
81
+ try {
82
+ entries = fs.readdirSync(COMMANDS_DIR);
83
+ } catch (err) {
84
+ throw new ExitError(1, 'ERROR lint-docs-command-form: could not read commands/gsd: ' + err.message);
85
+ }
86
+ return new Set(
87
+ entries.filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, '')),
88
+ );
89
+ }
90
+
91
+ function isCheckedDocsFile(relPath) {
92
+ if (!relPath.startsWith(DOCS_PREFIX)) return false;
93
+ if (relPath.startsWith(ADR_PREFIX)) return false;
94
+ if (relPath === RELEASE_NOTES_LEGACY) return false;
95
+ return relPath.endsWith('.md');
96
+ }
97
+
98
+ /**
99
+ * Pure scan — no fs, no git. Returns violations for a single file's content.
100
+ *
101
+ * @param {string} relPath file path (used only in violation records)
102
+ * @param {string} content file contents
103
+ * @param {Set<string>} roster valid command names
104
+ * @returns {Array<{file:string, line:number, col:number, text:string}>}
105
+ */
106
+ function scanContent(relPath, content, roster) {
107
+ const violations = [];
108
+ const lines = content.split('\n');
109
+ for (let i = 0; i < lines.length; i++) {
110
+ const line = lines[i];
111
+ COMMAND_FORM_RE.lastIndex = 0;
112
+ let match;
113
+ while ((match = COMMAND_FORM_RE.exec(line)) !== null) {
114
+ const [, pre, slash, cmd] = match;
115
+ if (!roster.has(cmd.toLowerCase())) continue;
116
+ if (!slash) {
117
+ const preContext = line.slice(0, match.index + pre.length);
118
+ if (NAME_KEY_CITATION_RE.test(preContext)) continue;
119
+ }
120
+ const matchedToken = (slash || '') + 'gsd:' + cmd;
121
+ violations.push({
122
+ file: relPath,
123
+ line: i + 1,
124
+ col: match.index + pre.length + 1,
125
+ text: matchedToken,
126
+ });
127
+ }
128
+ }
129
+ return violations;
130
+ }
131
+
132
+ function main() {
133
+ const roster = loadRoster();
134
+
135
+ let trackedFiles;
136
+ try {
137
+ trackedFiles = execFileSync('git', ['ls-files'], { cwd: REPO_ROOT, encoding: 'utf8' })
138
+ .split('\n')
139
+ .map((f) => f.trim())
140
+ .filter(Boolean);
141
+ } catch (err) {
142
+ throw new ExitError(1, 'ERROR lint-docs-command-form: git ls-files failed: ' + err.message);
143
+ }
144
+
145
+ const docsFiles = trackedFiles.filter(isCheckedDocsFile);
146
+ const violations = [];
147
+
148
+ for (const relPath of docsFiles) {
149
+ const fullPath = path.join(REPO_ROOT, relPath);
150
+ if (path.resolve(fullPath) === SELF_PATH) continue;
151
+
152
+ let content;
153
+ try {
154
+ content = fs.readFileSync(fullPath, 'utf8');
155
+ } catch {
156
+ // Unreadable/deleted files — skip silently.
157
+ continue;
158
+ }
159
+
160
+ violations.push(...scanContent(relPath, content, roster));
161
+ }
162
+
163
+ if (violations.length === 0) {
164
+ process.stdout.write(
165
+ 'ok lint-docs-command-form: ' + docsFiles.length + ' file(s) checked, 0 violations\n',
166
+ );
167
+ return 0;
168
+ }
169
+
170
+ process.stderr.write('\nERROR lint-docs-command-form: ' + violations.length + ' violation(s) found\n\n');
171
+ for (const v of violations) {
172
+ process.stderr.write(' ' + v.file + ':' + v.line + ':' + v.col + ' — ' + JSON.stringify(v.text) + '\n');
173
+ }
174
+ process.stderr.write('\n');
175
+ process.stderr.write(
176
+ 'Fix: docs are never passed through the install-time slash-form converters, so the\n',
177
+ );
178
+ process.stderr.write(
179
+ ' colon form names a command no runtime registers. Rewrite to the hyphen form\n',
180
+ );
181
+ process.stderr.write(
182
+ ' (`/gsd-<cmd>`), or `/gsd-core:<cmd>` if this is genuinely the Claude Code\n',
183
+ );
184
+ process.stderr.write(' plugin namespace.\n\n');
185
+ return 1;
186
+ }
187
+
188
+ if (require.main === module) runMain(main);
189
+
190
+ module.exports = {
191
+ scanContent,
192
+ isCheckedDocsFile,
193
+ loadRoster,
194
+ COMMAND_FORM_RE,
195
+ };
@@ -16,6 +16,10 @@
16
16
  const { parseFragment, FRAGMENT_ERROR } = require('./changeset/parse.cjs');
17
17
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
18
18
 
19
+ // #2988: the repo's integration/default branch — the base every PR targets.
20
+ // Used as the local fallback when GITHUB_BASE_REF is unset (CI sets it).
21
+ const DEFAULT_BASE = 'next';
22
+
19
23
  const LINT_REASON = Object.freeze({
20
24
  OK_NO_TRIGGERING_FRAGMENTS: 'ok_no_triggering_fragments',
21
25
  OK_DOCS_UPDATED: 'ok_docs_updated',
@@ -149,7 +153,10 @@ function main() {
149
153
  } catch { /* fall through */ }
150
154
  }
151
155
 
152
- const base = process.env.GITHUB_BASE_REF || 'main';
156
+ // #2988: local fallback must match the repo's integration branch (`next`),
157
+ // not the release branch (`main`). CI sets GITHUB_BASE_REF explicitly; the
158
+ // fallback only fires locally, where `next` is the base every PR targets.
159
+ const base = process.env.GITHUB_BASE_REF || DEFAULT_BASE;
153
160
  let changedFiles = [];
154
161
  try {
155
162
  // execFileSync with argv — no shell, so a malicious GITHUB_BASE_REF
@@ -216,6 +223,7 @@ module.exports = {
216
223
  OPT_OUT_LABEL,
217
224
  TRIGGERING_TYPES,
218
225
  FRAGMENT_ERROR,
226
+ DEFAULT_BASE,
219
227
  isFragmentPath,
220
228
  isDocsFile,
221
229
  isExemptFragment,