@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,434 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Anti-divergence drift guard for the PROMPT-LAYER plan/summary-COUNTING seam
6
+ * (epic #3180, ADR-3180 "Planning Semantic Model Single Owner", Decision 4(e)).
7
+ *
8
+ * `scripts/lint-plan-count-drift.cjs` and `scripts/lint-milestone-window-drift.cjs`
9
+ * scan `src/` only — but the `.planning/` semantic derivations they own are ALSO
10
+ * re-derived a second time, in the PROMPT layer: the workflow markdown that
11
+ * ships to every runtime, authored as raw shell rather than TypeScript. Issue
12
+ * #1762's second reproduction traced a wrong `30 plans, 24 summaries` figure to
13
+ * a `ls -1 ... *-PLAN.md | wc -l` snippet in `gsd-core/workflows/progress.md` —
14
+ * a re-derivation no `.cts`-scoped guard can see, because it is markdown, not
15
+ * source. ADR-3180 Decision 4(a) requires whole-repo discovery; this guard
16
+ * extends that requirement from "the whole `src/` tree" to "every authored
17
+ * surface that can carry a derivation", covering the prompt layer the two
18
+ * sibling guards structurally cannot reach.
19
+ *
20
+ * Detection is intentionally NARROW, mirroring the sibling guards' precedent:
21
+ * a line is a re-derivation when it carries BOTH, in ONE source line:
22
+ * (a) a plan/summary SET GLOB — a `*` followed by a run of
23
+ * `[-A-Za-z0-9_.{}$]` characters and then the literal `PLAN.md` or
24
+ * `SUMMARY.md`. The leading `*` is load-bearing: it is what makes the
25
+ * line enumerate a SET of files rather than name one specific plan.
26
+ * `gsd-core/workflows/execute-plan.md`'s
27
+ * `grep -cE '^\s*<task[[:space:]>]' .../{phase}-{plan}-PLAN.md` counts
28
+ * TASKS *inside* one already-named plan file — it has no glob token
29
+ * (no `*` anywhere near `PLAN.md`), so it is not a plan-count
30
+ * re-derivation and correctly never matches (a).
31
+ * (b) a COUNTING operation on that same line — `wc -l`, or `grep -c`
32
+ * (optionally with bundled short flags, e.g. `grep -cE`). Reading,
33
+ * globbing, or merely LISTING plan/summary files (`ls *-PLAN.md`,
34
+ * `cat *-PLAN.md`, `--files ".../*-PLAN.md"`) without counting them is
35
+ * not this derivation and must not be flagged — every non-counting
36
+ * `*-PLAN.md`/`*-SUMMARY.md` glob in `gsd-core/workflows/plan-phase.md`
37
+ * (backup, `--files`, `cat`, cross-reference prose) is exactly this
38
+ * shape and is deliberately left alone.
39
+ * `*-UAT.md` never matches (a) — UAT artifacts are a different derivation
40
+ * this guard does not own — so `gsd-core/workflows/progress.md`'s
41
+ * `... *-UAT.md ... | wc -l` line correctly never fires even though it sits
42
+ * one line below two lines that DO.
43
+ *
44
+ * Both regexes are small, bounded, and non-backtracking by construction (a
45
+ * single fixed character class with no nested quantifiers) — `npm run
46
+ * lint:ci` runs CodeQL js/redos over this repo, the same discipline the
47
+ * sibling guards document in their own headers.
48
+ *
49
+ * Surfaces scanned (SCAN_DIRS): `gsd-core/workflows`, `commands`, `agents`,
50
+ * `skills` — the prompt-layer markdown that ships to runtimes. SCAN_EXT:
51
+ * `.md` only. The tree-walk / root-confinement / symlink / sanitizer
52
+ * machinery is SHARED with the two sibling guards via `scripts/lib/drift-scan.cjs`
53
+ * (ADR-3180 Decision 4's own "Rejected: let the new drift guard copy Phase 1's
54
+ * tree-walk / root-confinement / sanitizer") — see that module for the
55
+ * `isInsideRoot` case-sensitivity note, the `walk` symlink-confinement
56
+ * rationale, and the ReDoS-avoidance rationale for its regex-literal reader
57
+ * (unused by this guard's own regexes, which need no literal tokenizer, but
58
+ * shared for the tree walk and report sanitization).
59
+ *
60
+ * RATCHET, not an allowlist. Per ADR-3180 Decision 4(e) this guard's baseline
61
+ * (`scripts/baselines/planning-prompt-drift-baseline.json`) mirrors
62
+ * `scripts/qa-smell-ratchet.cjs`'s precedent exactly: a violation whose
63
+ * `(file, text)` pair is already RECORDED in the baseline is KNOWN and never
64
+ * fails; a violation whose pair is NOT recorded is NEW and fails, telling the
65
+ * author to route the count through the `gsd-core` CLI instead of re-deriving
66
+ * it in shell; a recorded pair that no longer fires in this run is STALE and
67
+ * ALSO fails, forcing `--update` (run by a maintainer after a migration) to
68
+ * prune it — this is what makes the baseline SHRINK-ONLY as call sites
69
+ * migrate off the shell re-derivation, rather than a list that only ever
70
+ * grows. Matching is keyed on the pair (`file`, TRIMMED source `text`), never
71
+ * the line number: a workflow markdown file's line numbers churn on every
72
+ * unrelated edit (a new paragraph, a reworded step) and a number-keyed
73
+ * baseline would need hand-maintenance on changes that have nothing to do
74
+ * with this derivation at all.
75
+ *
76
+ * COUNT, not duplicate rows. Two DIFFERENT source lines can carry the exact
77
+ * same (file, TRIMMED text) pair — `gsd-core/workflows/plan-phase.md` has two
78
+ * byte-identical `DISK_PLANS=$(ls "${PHASE_DIR}"/*-PLAN.md 2>/dev/null | wc -l
79
+ * | tr -d ' ')` sites. Keying on (file, text) alone with one baseline row per
80
+ * OCCURRENCE made a partial migration invisible: migrating ONE of the two
81
+ * sites still leaves a violation matching the row, so nothing goes fresh and
82
+ * nothing goes stale — the remaining, unmigrated copy is silently covered by
83
+ * the row meant to acknowledge the pair NO LONGER MIGRATING. Each baseline
84
+ * entry therefore carries a `count` — the number of byte-identical
85
+ * occurrences of that (file, text) pair acknowledged at this site, not a
86
+ * duplicated row per occurrence:
87
+ * - actual occurrences this run < entry.count -> STALE as a PARTIAL
88
+ * migration: some but not all acknowledged copies are gone, so the entry
89
+ * no longer describes reality and must be re-recorded via `--update`;
90
+ * - actual occurrences this run > entry.count -> the occurrences beyond
91
+ * the acknowledged count are FRESH: a new copy landed next to one that
92
+ * was already acknowledged;
93
+ * - actual occurrences this run === 0 -> fully STALE, the
94
+ * existing "site was migrated, delete the row" case;
95
+ * - actual occurrences this run === entry.count -> fully acknowledged, no
96
+ * failure.
97
+ * Line numbers stay OUT of the key even with counting — that is still what
98
+ * keeps the baseline immune to unrelated churn; `count` answers "how many",
99
+ * never "which lines".
100
+ *
101
+ * KNOWN, ACCEPTED limits of a per-line textual scan (same tradeoff the
102
+ * sibling guards document): a re-derivation whose glob and counting operator
103
+ * are split across two DIFFERENT lines (e.g. a variable holding the glob,
104
+ * counted via `wc -l` on the next line) is not caught by this narrow shape.
105
+ * That is left to code review, not this regex.
106
+ */
107
+
108
+ const fs = require('node:fs');
109
+ const path = require('node:path');
110
+ const driftScan = require('./lib/drift-scan.cjs');
111
+ const { sanitizeForReport, scanTree } = driftScan;
112
+
113
+ // (a) A plan/summary SET GLOB: a `*` followed by a bounded run of path/brace/
114
+ // var-interpolation characters and then the literal `PLAN.md` or
115
+ // `SUMMARY.md`. The character class is fixed and the quantifier is a single
116
+ // `*` (regex "zero or more", not the shell glob character being matched) over
117
+ // that one class — no nesting, no alternation inside a repeated group, so
118
+ // there is nothing here for a backtracking engine to explore more than once.
119
+ const PLAN_SUMMARY_GLOB_RE = /\*[-A-Za-z0-9_.{}$]*(?:PLAN|SUMMARY)\.md/;
120
+
121
+ // `scanTree` (scripts/lib/drift-scan.cjs) builds its repo-relative path via
122
+ // `path.relative()`, which uses NATIVE separators: on Windows that is
123
+ // `gsd-core\workflows\execute-plan.md`, while the committed baseline
124
+ // (`scripts/baselines/planning-prompt-drift-baseline.json`) stores POSIX
125
+ // paths (`gsd-core/workflows/execute-plan.md`). Every baseline lookup in this
126
+ // guard is keyed on that path, so an un-normalized Windows path silently
127
+ // fails to match ANY baseline entry — every real violation reports as FRESH
128
+ // and every baseline entry reports as STALE (100% failure rate on Windows,
129
+ // caught by GitHub Actions' Windows CI lane on PR #3223; the remote runner
130
+ // this repo otherwise gates on is Linux-only and cannot see this class).
131
+ // Normalized UNCONDITIONALLY — never gated on `process.platform` — because a
132
+ // platform-conditional normalizer is itself the bug: it makes the POSIX path
133
+ // the tested case and leaves the Windows branch exercised only on Windows.
134
+ // Applied at the single seam `findPromptDrift` owns (the only place a
135
+ // repo-relative path enters this guard's violation objects), so ONE
136
+ // normalized value flows into all four consumers: the baseline key
137
+ // (`diffAgainstBaseline`), the `--update` writer (`writeBaseline` via
138
+ // `dedupeViolationsForBaseline`), the violation report (`main`), and the
139
+ // tests.
140
+ function toPosixRel(relPath) {
141
+ return relPath.replace(/\\/g, '/');
142
+ }
143
+
144
+ // (b) A counting operation: `wc -l`, or `grep -c` optionally followed by
145
+ // bundled short flags before the next space (e.g. `grep -cE`, `grep -cE`).
146
+ // `[A-Za-z]{0,4}` bounds the bundled-flag run so the alternative branch is
147
+ // exactly as fixed-width-bounded as `wc -l` — no unbounded quantifier chained
148
+ // to another, so nothing to backtrack.
149
+ const COUNTING_OP_RE = /wc -l|grep -c[A-Za-z]{0,4}\b/;
150
+
151
+ // Prompt-layer markdown that ships to every runtime.
152
+ const SCAN_DIRS = ['gsd-core/workflows', 'commands', 'agents', 'skills'];
153
+ const SCAN_EXT = new Set(['.md']);
154
+
155
+ const BASELINE_REL_PATH = path.join('scripts', 'baselines', 'planning-prompt-drift-baseline.json');
156
+
157
+ // ADR-3180 Decision 4(e): a baseline entry is "acknowledged, in writing, with
158
+ // the issue that owns its removal" — that is Phase 8 (#3218, "the prompt
159
+ // layer": give the workflow layer a CLI surface to ask for plan and phase
160
+ // counts, and burn this ratchet baseline to zero), NOT the epic (#3180)
161
+ // itself. #3180 is the scope authority for the whole consolidation; #3218 is
162
+ // the phase that actually deletes these shell re-derivations.
163
+ const RATCHET_OWNER_ISSUE = '#3218';
164
+
165
+ /**
166
+ * Pure: find every plan/summary-count re-derivation line in `text`.
167
+ * `relPath` is the repo-relative path (native separators or POSIX, either
168
+ * is accepted) — normalized via `toPosixRel` and attached as `file` on every
169
+ * result; this function applies no per-file exemption, so `relPath` is not
170
+ * otherwise consulted for detection.
171
+ * Returns [{ file, line, found, text }] — `file` is always POSIX-separated,
172
+ * `text` is the TRIMMED source line, the same value the baseline keys on.
173
+ */
174
+ function findPromptDrift(text, relPath) {
175
+ const file = toPosixRel(relPath);
176
+ const out = [];
177
+ const lines = text.split('\n');
178
+ for (let i = 0; i < lines.length; i++) {
179
+ const line = lines[i];
180
+ const globMatch = PLAN_SUMMARY_GLOB_RE.exec(line);
181
+ if (!globMatch) continue;
182
+ if (!COUNTING_OP_RE.test(line)) continue;
183
+ out.push({ file, line: i + 1, found: globMatch[0], text: line.trim() });
184
+ }
185
+ return out;
186
+ }
187
+
188
+ /**
189
+ * Scan the prompt-layer markdown tree and return every re-derivation, each
190
+ * annotated with the repo-relative file path (POSIX-normalized — see
191
+ * `toPosixRel`).
192
+ */
193
+ function scanRepo(root) {
194
+ return scanTree({
195
+ root,
196
+ scanDirs: SCAN_DIRS,
197
+ scanExt: SCAN_EXT,
198
+ onFile(rel, text) {
199
+ return findPromptDrift(text, rel);
200
+ },
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Read and parse the ratchet baseline. Returns `{ entries, errors }` —
206
+ * `entries` is `[]` and `errors` names the problem when the file is missing,
207
+ * empty, invalid JSON, or malformed; callers in check mode treat a non-empty
208
+ * `errors` as a hard failure (mirrors `qa-smell-ratchet.cjs`'s `readBaseline`).
209
+ */
210
+ function loadBaseline(root) {
211
+ const baselinePath = path.join(root, BASELINE_REL_PATH);
212
+ if (!fs.existsSync(baselinePath)) {
213
+ return { entries: [], errors: [`${BASELINE_REL_PATH} is missing — run \`node scripts/lint-planning-prompt-drift.cjs --update\` to generate it`] };
214
+ }
215
+ const raw = fs.readFileSync(baselinePath, 'utf8');
216
+ if (raw.trim() === '') {
217
+ return { entries: [], errors: [`${BASELINE_REL_PATH} is present but empty`] };
218
+ }
219
+ let doc;
220
+ try {
221
+ doc = JSON.parse(raw);
222
+ } catch (err) {
223
+ return { entries: [], errors: [`${BASELINE_REL_PATH} is not valid JSON: ${err.message}`] };
224
+ }
225
+ if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) {
226
+ return { entries: [], errors: [`${BASELINE_REL_PATH} must be a JSON object, got ${Array.isArray(doc) ? 'array' : typeof doc}`] };
227
+ }
228
+ if (!Array.isArray(doc.entries)) {
229
+ return { entries: [], errors: [`${BASELINE_REL_PATH}: "entries" must be an array, got ${JSON.stringify(doc.entries)}`] };
230
+ }
231
+ const errors = [];
232
+ const entries = [];
233
+ doc.entries.forEach((entry, i) => {
234
+ const where = `${BASELINE_REL_PATH}.entries[${i}]`;
235
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
236
+ errors.push(`${where} must be an object, got ${JSON.stringify(entry)}`);
237
+ return;
238
+ }
239
+ if (typeof entry.file !== 'string' || entry.file === '') {
240
+ errors.push(`${where}.file must be a non-empty string, got ${JSON.stringify(entry.file)}`);
241
+ return;
242
+ }
243
+ if (typeof entry.text !== 'string' || entry.text === '') {
244
+ errors.push(`${where}.text must be a non-empty string, got ${JSON.stringify(entry.text)}`);
245
+ return;
246
+ }
247
+ // `count` is optional on read (diffAgainstBaseline defaults an absent
248
+ // count to 1) but when present must be a positive integer — the number
249
+ // of byte-identical (file, text) occurrences this entry acknowledges.
250
+ if (entry.count !== undefined && !(Number.isInteger(entry.count) && entry.count >= 1)) {
251
+ errors.push(`${where}.count must be a positive integer when present, got ${JSON.stringify(entry.count)}`);
252
+ return;
253
+ }
254
+ entries.push(entry);
255
+ });
256
+ return { entries, errors };
257
+ }
258
+
259
+ /**
260
+ * Diff scanned `violations` (from `scanRepo`) against baseline `entries`,
261
+ * matched by the pair (`file`, TRIMMED `text`) — never the line
262
+ * number — and COUNT-aware: an entry acknowledges `entry.count`
263
+ * (default 1 when absent) byte-identical occurrences of that pair, not
264
+ * merely its presence. Returns `{ fresh, stale }`:
265
+ * - `fresh`: violations whose (file, text) pair is NOT in the baseline at
266
+ * all (a brand new site), PLUS any occurrences of a KNOWN pair beyond
267
+ * its acknowledged `count` (a new copy landed next to an
268
+ * already-acknowledged one) — both fail the build as NEW.
269
+ * - `stale`: baseline entries whose actual occurrence count this run is
270
+ * LESS than their acknowledged `count` — zero actual
271
+ * occurrences is the fully-migrated case ("site was migrated, delete
272
+ * the row"); a positive but short count is a PARTIAL migration (some
273
+ * but not all acknowledged copies are gone). Both fail the build,
274
+ * forcing `--update` to re-record the pair (this is what keeps the
275
+ * baseline shrink-only and what makes a partial migration visible
276
+ * instead of silently covered by the still-present sibling
277
+ * occurrence).
278
+ */
279
+ function diffAgainstBaseline(violations, baseline) {
280
+ const key = (file, text) => `${file}${text}`;
281
+
282
+ // Group this run's violations by (file, text) so a duplicated pair's
283
+ // occurrence COUNT — not merely its presence — can be
284
+ // compared against what the baseline entry acknowledges.
285
+ const actualByKey = new Map();
286
+ for (const v of violations) {
287
+ const k = key(v.file, v.text);
288
+ let vs = actualByKey.get(k);
289
+ if (!vs) { vs = []; actualByKey.set(k, vs); }
290
+ vs.push(v);
291
+ }
292
+
293
+ const knownKeys = new Set(baseline.map((e) => key(e.file, e.text)));
294
+
295
+ const fresh = [];
296
+ const stale = [];
297
+
298
+ // Every occurrence of a pair the baseline has never recorded at all is NEW.
299
+ for (const [k, vs] of actualByKey) {
300
+ if (!knownKeys.has(k)) fresh.push(...vs);
301
+ }
302
+
303
+ // For every RECORDED pair, compare its acknowledged count against how
304
+ // many occurrences this run actually found.
305
+ for (const entry of baseline) {
306
+ const k = key(entry.file, entry.text);
307
+ const expected = entry.count ?? 1;
308
+ const vs = actualByKey.get(k) || [];
309
+ const actual = vs.length;
310
+ if (actual < expected) {
311
+ // Zero actual occurrences is the fully-stale case; 0 < actual <
312
+ // expected is a partial migration — both are STALE, and
313
+ // both carry the expected/actual counts so the caller can name the
314
+ // mismatch.
315
+ stale.push({ ...entry, count: expected, actualCount: actual });
316
+ } else if (actual > expected) {
317
+ // Occurrences beyond the acknowledged count are a NEW copy landing
318
+ // next to one that was already acknowledged.
319
+ fresh.push(...vs.slice(expected));
320
+ }
321
+ // actual === expected: fully acknowledged, no failure.
322
+ }
323
+
324
+ return { fresh, stale };
325
+ }
326
+
327
+ /** Stable sort: by `file`, then by `text`. */
328
+ function sortEntries(entries) {
329
+ return [...entries].sort((a, b) => {
330
+ if (a.file !== b.file) return a.file < b.file ? -1 : 1;
331
+ if (a.text !== b.text) return a.text < b.text ? -1 : 1;
332
+ return 0;
333
+ });
334
+ }
335
+
336
+ /**
337
+ * Collapse `violations` into one baseline row per distinct (file, text) pair,
338
+ * carrying a `count` of how many occurrences that pair has in THIS run — see
339
+ * the module header's "COUNT, not duplicate rows" note. Pure; no I/O.
340
+ */
341
+ function dedupeViolationsForBaseline(violations) {
342
+ const order = [];
343
+ const byKey = new Map();
344
+ for (const v of violations) {
345
+ const k = `${v.file}${v.text}`;
346
+ let entry = byKey.get(k);
347
+ if (!entry) {
348
+ entry = { file: v.file, text: v.text, derivation: 'plan-count', owner_issue: RATCHET_OWNER_ISSUE, count: 0 };
349
+ byKey.set(k, entry);
350
+ order.push(entry);
351
+ }
352
+ entry.count += 1;
353
+ }
354
+ return order;
355
+ }
356
+
357
+ function writeBaseline(root, violations) {
358
+ const entries = sortEntries(dedupeViolationsForBaseline(violations));
359
+ const doc = {
360
+ $comment:
361
+ 'ADR-3180 Decision 4(e) ratchet, owned by Phase 8 (#3218). See scripts/lint-planning-prompt-drift.cjs. '
362
+ + 'SHRINK-ONLY: entries are removed as sites migrate to the gsd-core CLI; new or changed entries fail '
363
+ + 'lint:ci. `count` is the number of byte-identical (file, text) occurrences acknowledged at this site '
364
+ + '— a run producing fewer fails as a partial migration, more fails as an unacknowledged new copy.',
365
+ entries,
366
+ };
367
+ const baselinePath = path.join(root, BASELINE_REL_PATH);
368
+ fs.mkdirSync(path.dirname(baselinePath), { recursive: true });
369
+ fs.writeFileSync(baselinePath, `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
370
+ return entries;
371
+ }
372
+
373
+ function main() {
374
+ const root = path.join(__dirname, '..');
375
+ const update = process.argv.includes('--update');
376
+ const violations = scanRepo(root);
377
+
378
+ if (update) {
379
+ const entries = writeBaseline(root, violations);
380
+ process.stdout.write(`ok planning-prompt-drift: baseline regenerated with ${entries.length} entr${entries.length === 1 ? 'y' : 'ies'}\n`);
381
+ return;
382
+ }
383
+
384
+ const { entries: baseline, errors } = loadBaseline(root);
385
+ if (errors.length > 0) {
386
+ process.stderr.write('planning-prompt-drift: baseline load error(s):\n');
387
+ for (const e of errors) process.stderr.write(` ${e}\n`);
388
+ process.exitCode = 1;
389
+ return;
390
+ }
391
+
392
+ const { fresh, stale } = diffAgainstBaseline(violations, baseline);
393
+
394
+ if (fresh.length === 0 && stale.length === 0) {
395
+ process.stdout.write(`ok planning-prompt-drift: no unacknowledged plan/summary count re-derivations in the prompt layer (${baseline.length} known)\n`);
396
+ return;
397
+ }
398
+
399
+ if (fresh.length > 0) {
400
+ process.stderr.write('planning-prompt-drift: NEW plan/summary count re-derivation(s) found in the prompt layer.\n');
401
+ process.stderr.write('Route the count through the gsd-core CLI instead of re-deriving it in shell (ls .../*-PLAN.md | wc -l\n');
402
+ process.stderr.write('or grep -c on a *-PLAN.md/*-SUMMARY.md glob), or add an acknowledged entry to\n');
403
+ process.stderr.write(`${BASELINE_REL_PATH} via --update:\n`);
404
+ for (const v of fresh) {
405
+ process.stderr.write(` ${sanitizeForReport(v.file)}:${v.line} ${sanitizeForReport(v.found)} ${sanitizeForReport(v.text)}\n`);
406
+ }
407
+ }
408
+
409
+ if (stale.length > 0) {
410
+ process.stderr.write('\nplanning-prompt-drift: STALE baseline entr' + (stale.length === 1 ? 'y' : 'ies') + " (fully migrated, or a PARTIAL migration — fewer occurrences found than acknowledged; delete or re-record the row):\n");
411
+ for (const e of stale) {
412
+ process.stderr.write(` ${sanitizeForReport(e.file)} ${sanitizeForReport(e.text)} (found ${e.actualCount}/${e.count} acknowledged occurrence${e.count === 1 ? '' : 's'})\n`);
413
+ }
414
+ process.stderr.write(`\n remedy: node scripts/lint-planning-prompt-drift.cjs --update\n`);
415
+ }
416
+
417
+ process.exitCode = 1;
418
+ }
419
+
420
+ if (require.main === module) main();
421
+
422
+ module.exports = {
423
+ findPromptDrift,
424
+ scanRepo,
425
+ toPosixRel,
426
+ loadBaseline,
427
+ diffAgainstBaseline,
428
+ writeBaseline,
429
+ PLAN_SUMMARY_GLOB_RE,
430
+ COUNTING_OP_RE,
431
+ SCAN_DIRS,
432
+ SCAN_EXT,
433
+ BASELINE_REL_PATH,
434
+ };