@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,645 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gen-prompt-budget-parity-corpus.cjs
4
+ *
5
+ * Captures a characterization corpus of `prompt-budget.applyBudget`'s observable
6
+ * output, for issue #2929 (epic #1671 Phase 2 — the `context-composer` seam).
7
+ *
8
+ * WHY THIS EXISTS
9
+ * ---------------
10
+ * Phase 2 generalizes `applyBudget`'s trim ladder into a shared composer seam.
11
+ * Its success condition is that `prompt-budget`'s output does not change. The only
12
+ * authority on "did not change" is the behavior that shipped BEFORE the refactor,
13
+ * so this corpus is generated against the pre-refactor implementation and committed
14
+ * as a frozen fixture. `tests/prompt-budget-parity.test.cjs` then replays every case
15
+ * and asserts byte-identical `prompt` plus field-identical `metadata`.
16
+ *
17
+ * FIXTURE PROVENANCE (CONTRIBUTING.md "Fixture provenance (#2371)")
18
+ * -----------------------------------------------------------------
19
+ * That rule forbids fixtures derived from the gate's own writer, grammar, or the
20
+ * author's mental model, because such a fixture can only confirm what the author
21
+ * already believed. This corpus is expressly NOT hand-authored: every `expected`
22
+ * value is computed by executing the shipped implementation, which predates the
23
+ * composer and knows nothing about it. What IS author-chosen is the set of INPUTS —
24
+ * so the inputs are derived from the design's parity-critical behavior table rather
25
+ * than from intuition, and `tests/prompt-budget-parity.test.cjs` additionally proves
26
+ * the corpus is non-vacuous by mutating the ladder and requiring the corpus to fail.
27
+ * Disclosed here rather than assumed.
28
+ *
29
+ * USAGE
30
+ * node scripts/gen-prompt-budget-parity-corpus.cjs --write # regenerate
31
+ * node scripts/gen-prompt-budget-parity-corpus.cjs --check # drift-guard (exit 1 on drift)
32
+ *
33
+ * `--check` is the intentional-change gate: after Phase 2 lands, any diff here means
34
+ * review-prompt output moved, which is a user-visible change requiring justification.
35
+ */
36
+
37
+ 'use strict';
38
+
39
+ const fs = require('node:fs');
40
+ const path = require('node:path');
41
+
42
+ const REPO_ROOT = path.resolve(__dirname, '..');
43
+ const OUT_PATH = path.join(REPO_ROOT, 'tests', 'fixtures', 'prompt-budget-parity', 'corpus.json');
44
+ const IMPL_PATH = path.join(REPO_ROOT, 'gsd-core', 'bin', 'lib', 'prompt-budget.cjs');
45
+
46
+ /** Corpus schema version. Bump only on a deliberate shape change. */
47
+ const CORPUS_VERSION = 1;
48
+
49
+ // ─── Deterministic content builders ──────────────────────────────────────────
50
+ // estimateTokens() is Math.ceil(len / 4), so a string of length 4*N measures
51
+ // exactly N tokens. Every size below is therefore exact, not approximate — which
52
+ // is what makes the cap-1 / cap / cap+1 rows land ON the boundary rather than near it.
53
+
54
+ /** A filler string measuring exactly `tokens` tokens under chars/4. */
55
+ function tokensOf(tokens, fill = 'x') {
56
+ return fill.repeat(tokens * 4);
57
+ }
58
+
59
+ /** `lines` newline-separated lines, each `width` chars wide. */
60
+ function linesOf(lines, width = 19) {
61
+ return Array.from({ length: lines }, (_, i) => String(i).padEnd(width, '-')).join('\n');
62
+ }
63
+
64
+ // ─── Case definitions ────────────────────────────────────────────────────────
65
+ // Each case is { name, why, sections, budget, options }.
66
+ // `why` names the design/test-matrix row the case exists to pin.
67
+
68
+ function buildCases() {
69
+ const cases = [];
70
+ const add = (c) => cases.push(c);
71
+
72
+ // Base shape reused by the boundary family. With safetyMarginPct 0 the effective
73
+ // budget equals the budget, so totals can be placed exactly on the cap.
74
+ // staticBase = est(instructions) + est('## Roadmap\n\n') + est(roadmap)
75
+ // + est('## Plans\n\n') + est('### a.md\n\n')
76
+ const boundaryBase = (planTokens) => ({
77
+ instructions: tokensOf(10, 'I'),
78
+ roadmap: tokensOf(10, 'R'),
79
+ plans: [{ file: 'a.md', content: tokensOf(planTokens, 'P') }],
80
+ });
81
+
82
+ // ── A. Budget boundary (RULESET.TESTS.boundary-coverage.fixtures a/b/c) ────
83
+ add({
84
+ name: 'A1-exact-cap',
85
+ why: 'A1/row 3 — total measures exactly effectiveBudget; must NOT be treated as pressure',
86
+ sections: boundaryBase(71),
87
+ budget: 100,
88
+ options: { safetyMarginPct: 0 },
89
+ });
90
+ add({
91
+ name: 'A2-one-under-cap',
92
+ why: 'A2/row 5 — one token under; no pressure, no reserve deducted',
93
+ sections: boundaryBase(70),
94
+ budget: 100,
95
+ options: { safetyMarginPct: 0 },
96
+ });
97
+ add({
98
+ name: 'A3-one-over-cap',
99
+ why: 'A3/row 4 — one token over; pressure engages the ladder',
100
+ sections: boundaryBase(72),
101
+ budget: 100,
102
+ options: { safetyMarginPct: 0 },
103
+ });
104
+
105
+ // A1 pins exact-cap as "no pressure", but with only a floored plan group present that
106
+ // assertion is not load-bearing: the 1024-char floor absorbs any trim, so relaxing the
107
+ // pressure test from `>` to `>=` produces byte-identical output and the corpus cannot
108
+ // see the difference. This case gives the ladder something DROPPABLE at exactly the cap,
109
+ // so `>` vs `>=` becomes observable as context surviving vs being omitted.
110
+ // staticBase 29 + plan 20 = 49, context wrapper 3, so est(context) 148 puts the
111
+ // baseline at exactly 200 = effectiveBudget.
112
+ add({
113
+ name: 'A3b-exact-cap-with-droppable-present',
114
+ why: 'exact-cap discriminator — baseline lands exactly ON effectiveBudget with a droppable available. Pins that `baseline > effectiveBudget` is STRICT: at equality nothing is dropped. Mutating the comparison to >= flips context from kept to omitted, which A1 alone cannot detect.',
115
+ sections: {
116
+ instructions: tokensOf(10, 'I'),
117
+ roadmap: tokensOf(10, 'R'),
118
+ plans: [{ file: 'a.md', content: tokensOf(20, 'P') }],
119
+ context: tokensOf(148, 'C'),
120
+ },
121
+ budget: 200,
122
+ options: { safetyMarginPct: 0 },
123
+ });
124
+ add({
125
+ name: 'A3c-one-under-cap-with-droppable-present',
126
+ why: 'companion to A3b — one token under the cap with the same droppable available. Together they bracket the strict-inequality boundary from below.',
127
+ sections: {
128
+ instructions: tokensOf(10, 'I'),
129
+ roadmap: tokensOf(10, 'R'),
130
+ plans: [{ file: 'a.md', content: tokensOf(20, 'P') }],
131
+ context: tokensOf(147, 'C'),
132
+ },
133
+ budget: 200,
134
+ options: { safetyMarginPct: 0 },
135
+ });
136
+
137
+ // Row (d) of RULESET.TESTS.boundary-coverage.fixtures: baseline within
138
+ // reserve-distance of the limit. NOTE_RESERVE_TOKENS is 80, and this is the exact
139
+ // band (effectiveBudget - 80, effectiveBudget] that PR #3708 shipped two
140
+ // regressions in (LEARNING.prompt-budget.boundary-gap). Swept token by token.
141
+ // Budget 200 (not 100) is required here, not incidental: the band is
142
+ // NOTE_RESERVE_TOKENS (80) wide, so the baseline needs >=81 tokens of headroom
143
+ // beneath the cap to be swept. staticBase is 29 tokens, so planTokens = 171 - delta
144
+ // places the baseline exactly `delta` under effectiveBudget and stays positive
145
+ // across the whole sweep. delta 80 and 81 sit just OUTSIDE the band deliberately —
146
+ // they are the negative controls that prove the band's edge is where it is claimed.
147
+ for (const delta of [0, 1, 2, 39, 40, 79, 80, 81]) {
148
+ add({
149
+ name: `A4-reserve-band-minus-${delta}`,
150
+ why: `boundary-coverage.fixtures row (d) — baseline at effectiveBudget-${delta}. Pins the NEGATIVE control: within NOTE_RESERVE_TOKENS distance of the cap, NOTHING may be trimmed. Early-pressure firing here is the exact PR #3708 regression.`,
151
+ sections: boundaryBase(171 - delta),
152
+ budget: 200,
153
+ options: { safetyMarginPct: 0 },
154
+ });
155
+ }
156
+
157
+ // Mirror of A4 on the OTHER side of the cap. A4 proves the reserve is NOT deducted
158
+ // at or below the cap; this proves it IS deducted above it, and that the ladder
159
+ // relieves pressure by dropping. Without both sides the family pins only half the
160
+ // boundary, and an "always reserve" regression would still pass A4.
161
+ // staticBase 29 + planContent 20 = 49, and the context wrapper costs 3, so
162
+ // est(context) = 148 + delta places the baseline exactly `delta` OVER effectiveBudget.
163
+ for (const delta of [1, 2, 40, 79, 80, 81]) {
164
+ add({
165
+ name: `A10-pressure-band-plus-${delta}`,
166
+ why: `boundary-coverage.fixtures row (d), above-cap mirror — baseline at effectiveBudget+${delta}; pressure fires, NOTE_RESERVE_TOKENS is deducted, and context is dropped`,
167
+ sections: {
168
+ instructions: tokensOf(10, 'I'),
169
+ roadmap: tokensOf(10, 'R'),
170
+ plans: [{ file: 'a.md', content: tokensOf(20, 'P') }],
171
+ context: tokensOf(148 + delta, 'C'),
172
+ },
173
+ budget: 200,
174
+ options: { safetyMarginPct: 0 },
175
+ });
176
+ }
177
+
178
+ add({
179
+ name: 'A5-safety-margin-default',
180
+ why: 'A4 — default 10% margin; effectiveBudget = floor(budget * 0.9)',
181
+ sections: boundaryBase(60),
182
+ budget: 100,
183
+ options: {},
184
+ });
185
+
186
+ // The rounding mode of effectiveBudget was unpinned: every other case in this file
187
+ // uses a (budget, safetyMarginPct) pair whose product is a whole number, so
188
+ // Math.floor and Math.round agree and mutating one to the other changed nothing.
189
+ // These three straddle .5 in both directions so the mode is observable.
190
+ // 95 * 0.90 = 85.5 -> floor 85, round 86 (disagree)
191
+ // 97 * 0.90 = 87.3 -> floor 87, round 87 (agree; guards against a ceil mutation)
192
+ // 93 * 0.85 = 79.05 -> floor 79, round 79 (agree; second ceil guard, different margin)
193
+ add({
194
+ name: 'A11-fractional-effective-budget-half',
195
+ why: 'pins the ROUNDING MODE of effectiveBudget. 95 * (1 - 10/100) = 85.5, where Math.floor (85) and Math.round (86) disagree. Without a fractional case the mode is unpinned and floor->round is a byte-invisible mutation, which an isolated review confirmed against the 47-case corpus.',
196
+ sections: boundaryBase(56),
197
+ budget: 95,
198
+ options: { safetyMarginPct: 10 },
199
+ });
200
+ add({
201
+ name: 'A12-fractional-effective-budget-below-half',
202
+ why: 'companion to A11 below the .5 point: 97 * 0.90 = 87.3, where floor and round agree but Math.ceil (88) would not. Guards the mutation A11 does not cover.',
203
+ sections: boundaryBase(58),
204
+ budget: 97,
205
+ options: { safetyMarginPct: 10 },
206
+ });
207
+ add({
208
+ name: 'A13-fractional-effective-budget-odd-margin',
209
+ why: 'third rounding guard at a non-multiple-of-10 margin: 93 * (1 - 15/100) = 79.05. Exercises the margin arithmetic itself, not just the budget, since safetyMarginPct is caller-supplied.',
210
+ sections: boundaryBase(50),
211
+ budget: 93,
212
+ options: { safetyMarginPct: 15 },
213
+ });
214
+ add({
215
+ name: 'A6-safety-margin-zero',
216
+ why: 'A5 — 0% margin means the full budget is usable',
217
+ sections: boundaryBase(60),
218
+ budget: 100,
219
+ options: { safetyMarginPct: 0 },
220
+ });
221
+ add({
222
+ name: 'A7-safety-margin-hundred',
223
+ why: 'A5 — 100% margin drives effectiveBudget to 0, forcing the min-set hard fail',
224
+ sections: boundaryBase(10),
225
+ budget: 100,
226
+ options: { safetyMarginPct: 100 },
227
+ });
228
+ add({
229
+ name: 'A8-budget-zero',
230
+ why: 'A6 — non-positive budget must hard-fail cleanly, never negative-length slice',
231
+ sections: boundaryBase(10),
232
+ budget: 0,
233
+ options: {},
234
+ });
235
+ add({
236
+ name: 'A9-budget-negative',
237
+ why: 'A6 — negative budget must not crash',
238
+ sections: boundaryBase(10),
239
+ budget: -50,
240
+ options: {},
241
+ });
242
+
243
+ // ── B. Shrink strategies ──────────────────────────────────────────────────
244
+ add({
245
+ name: 'B3-head-shrink-engaged',
246
+ why: 'B3/row 7 — projectMd longer than projectMdHeadLines under pressure; shrunk flag true',
247
+ sections: {
248
+ instructions: tokensOf(10, 'I'),
249
+ roadmap: tokensOf(10, 'R'),
250
+ plans: [{ file: 'a.md', content: tokensOf(40, 'P') }],
251
+ projectMd: linesOf(120),
252
+ },
253
+ budget: 200,
254
+ options: { safetyMarginPct: 0 },
255
+ });
256
+ add({
257
+ name: 'B4-head-shrink-noop',
258
+ why: 'B4/row 7+9 — fewer lines than the cap; text unchanged and shrunk flag MUST stay false',
259
+ sections: {
260
+ instructions: tokensOf(10, 'I'),
261
+ roadmap: tokensOf(10, 'R'),
262
+ plans: [{ file: 'a.md', content: tokensOf(40, 'P') }],
263
+ projectMd: linesOf(5),
264
+ },
265
+ budget: 60,
266
+ options: { safetyMarginPct: 0 },
267
+ });
268
+ add({
269
+ name: 'B5-head-shrink-zero-lines',
270
+ why: 'B5/row 8 — projectMdHeadLines <= 0 yields the empty string',
271
+ sections: {
272
+ instructions: tokensOf(10, 'I'),
273
+ roadmap: tokensOf(10, 'R'),
274
+ plans: [{ file: 'a.md', content: tokensOf(40, 'P') }],
275
+ projectMd: linesOf(120),
276
+ },
277
+ budget: 60,
278
+ options: { safetyMarginPct: 0, projectMdHeadLines: 0 },
279
+ });
280
+ add({
281
+ name: 'B6-plan-truncate-share-above-floor',
282
+ why: 'B6/row 13 — proportional share ABOVE the floor, so the share (not the floor) decides. Budget 700: min-set is 5+5+256+256=522 which fits, so this reaches the truncate step instead of hard-failing the way the original budget-400 version did. groupBudget 598 of 1000 => charsBudget 2392, share 1196 per plan > floor 1024.',
283
+ sections: {
284
+ instructions: tokensOf(5, 'I'),
285
+ roadmap: tokensOf(5, 'R'),
286
+ plans: [
287
+ { file: 'a.md', content: tokensOf(500, 'A') },
288
+ { file: 'b.md', content: tokensOf(500, 'B') },
289
+ ],
290
+ },
291
+ budget: 700,
292
+ options: { safetyMarginPct: 0 },
293
+ });
294
+ add({
295
+ name: 'B7-plan-truncate-floor-binding',
296
+ why: 'B7/row 10 — share falls BELOW the 1024-char floor so the floor wins, and the resulting total deliberately EXCEEDS the group budget. Budget 1100: min-set 5+5+4*256=1034 fits; groupBudget 992 => charsBudget 3968 => share 992 < 1024, so every plan is held at the floor. This is the case that proves a floor is a per-fragment guarantee, not a budget cap.',
297
+ sections: {
298
+ instructions: tokensOf(5, 'I'),
299
+ roadmap: tokensOf(5, 'R'),
300
+ plans: [
301
+ { file: 'a.md', content: tokensOf(500, 'A') },
302
+ { file: 'b.md', content: tokensOf(500, 'B') },
303
+ { file: 'c.md', content: tokensOf(500, 'C') },
304
+ { file: 'd.md', content: tokensOf(500, 'D') },
305
+ ],
306
+ },
307
+ budget: 1100,
308
+ options: { safetyMarginPct: 0 },
309
+ });
310
+ add({
311
+ name: 'B8-zero-sized-plans',
312
+ why: 'B8/row 11 — total original chars 0; no divide-by-zero, pct stays 0',
313
+ sections: {
314
+ instructions: tokensOf(10, 'I'),
315
+ roadmap: tokensOf(10, 'R'),
316
+ plans: [
317
+ { file: 'a.md', content: '' },
318
+ { file: 'b.md', content: '' },
319
+ ],
320
+ context: tokensOf(200, 'C'),
321
+ },
322
+ budget: 60,
323
+ options: { safetyMarginPct: 0 },
324
+ });
325
+ add({
326
+ name: 'B9-nonpositive-plan-budget',
327
+ why: 'B9/row 12 — computed plan budget <= 0 skips the truncate step ENTIRELY rather than clamping',
328
+ sections: boundaryBase(72),
329
+ budget: 100,
330
+ options: { safetyMarginPct: 0 },
331
+ });
332
+ add({
333
+ name: 'B11-drop-order-all-three',
334
+ why: 'B11/row 14 — droppables leave in declared order: context, research, requirements',
335
+ sections: {
336
+ instructions: tokensOf(10, 'I'),
337
+ roadmap: tokensOf(10, 'R'),
338
+ plans: [{ file: 'a.md', content: tokensOf(30, 'P') }],
339
+ context: tokensOf(300, 'C'),
340
+ research: tokensOf(300, 'S'),
341
+ requirements: tokensOf(300, 'Q'),
342
+ },
343
+ budget: 200,
344
+ options: { safetyMarginPct: 0 },
345
+ });
346
+ add({
347
+ name: 'B11b-drop-context-only',
348
+ why: 'B11 — pressure relieved after the first drop; research and requirements survive',
349
+ sections: {
350
+ instructions: tokensOf(10, 'I'),
351
+ roadmap: tokensOf(10, 'R'),
352
+ plans: [{ file: 'a.md', content: tokensOf(30, 'P') }],
353
+ context: tokensOf(300, 'C'),
354
+ research: tokensOf(10, 'S'),
355
+ requirements: tokensOf(10, 'Q'),
356
+ },
357
+ budget: 300,
358
+ options: { safetyMarginPct: 0 },
359
+ });
360
+ add({
361
+ name: 'B12-droppables-absent',
362
+ why: 'B12/row 15 — null droppables are not dropped and never appear in omitted. Budget 400 (not 120) so this actually RENDERS: at 120 the min-set pre-check hard-fails and the case would compare two empty strings.',
363
+ sections: {
364
+ instructions: tokensOf(10, 'I'),
365
+ roadmap: tokensOf(10, 'R'),
366
+ plans: [{ file: 'a.md', content: tokensOf(300, 'P') }],
367
+ context: null,
368
+ research: null,
369
+ requirements: null,
370
+ },
371
+ budget: 400,
372
+ options: { safetyMarginPct: 0 },
373
+ });
374
+ add({
375
+ name: 'B13-droppables-empty-string',
376
+ why: 'B13/row 23 — empty-string droppables. Paired with B12 to pin whether "" and null are distinguished. They are NOT: applyBudget uses truthy checks throughout, so "" behaves as absent. This case records that fact rather than the design doc\'s claim that they differ.',
377
+ sections: {
378
+ instructions: tokensOf(10, 'I'),
379
+ roadmap: tokensOf(10, 'R'),
380
+ plans: [{ file: 'a.md', content: tokensOf(300, 'P') }],
381
+ context: '',
382
+ research: '',
383
+ requirements: '',
384
+ },
385
+ budget: 400,
386
+ options: { safetyMarginPct: 0 },
387
+ });
388
+ add({
389
+ name: 'B13b-empty-string-droppable-under-pressure',
390
+ why: 'B13 companion — an empty-string droppable while the ladder is ACTIVELY running. Pins that "" is never recorded in omitted even under real pressure, which B12/B13 (no pressure) cannot show. research is real and IS dropped, so the ladder is demonstrably engaged.',
391
+ sections: {
392
+ instructions: tokensOf(10, 'I'),
393
+ roadmap: tokensOf(10, 'R'),
394
+ plans: [{ file: 'a.md', content: tokensOf(20, 'P') }],
395
+ context: '',
396
+ research: tokensOf(200, 'S'),
397
+ requirements: '',
398
+ },
399
+ budget: 200,
400
+ options: { safetyMarginPct: 0 },
401
+ });
402
+
403
+ // ── C. Note reserve + hard-fail paths ─────────────────────────────────────
404
+ add({
405
+ name: 'C1-no-trim-no-note',
406
+ why: 'C1/row 1 — everything fits; no note, noteInjected false',
407
+ sections: {
408
+ instructions: tokensOf(10, 'I'),
409
+ roadmap: tokensOf(10, 'R'),
410
+ plans: [{ file: 'a.md', content: tokensOf(10, 'P') }],
411
+ projectMd: linesOf(3),
412
+ context: tokensOf(5, 'C'),
413
+ research: tokensOf(5, 'S'),
414
+ requirements: tokensOf(5, 'Q'),
415
+ },
416
+ budget: 1000,
417
+ options: {},
418
+ });
419
+ add({
420
+ name: 'C4-min-set-hard-fail',
421
+ why: 'C4/row 16 — min-set pre-check exceeds budget; empty output and estimatedTokens 0',
422
+ sections: {
423
+ instructions: tokensOf(500, 'I'),
424
+ roadmap: tokensOf(500, 'R'),
425
+ plans: [{ file: 'a.md', content: tokensOf(500, 'P') }],
426
+ },
427
+ budget: 100,
428
+ options: {},
429
+ });
430
+ add({
431
+ name: 'C5-post-assembly-hard-fail',
432
+ why: 'C5/row 17 — survives the min-set check but the assembled prompt still overflows; real measured size reported',
433
+ sections: {
434
+ instructions: tokensOf(80, 'I'),
435
+ roadmap: tokensOf(80, 'R'),
436
+ plans: [{ file: 'a.md', content: tokensOf(2000, 'P') }],
437
+ },
438
+ budget: 200,
439
+ options: { safetyMarginPct: 0 },
440
+ });
441
+ add({
442
+ name: 'C7-custom-note-template',
443
+ why: 'note template placeholders {budget} {omittedList} {planTruncationPct} all substitute',
444
+ sections: {
445
+ instructions: tokensOf(10, 'I'),
446
+ roadmap: tokensOf(10, 'R'),
447
+ plans: [{ file: 'a.md', content: tokensOf(30, 'P') }],
448
+ context: tokensOf(300, 'C'),
449
+ },
450
+ budget: 200,
451
+ options: {
452
+ safetyMarginPct: 0,
453
+ noteTemplate: 'TRIMMED b={budget} o={omittedList} p={planTruncationPct}',
454
+ },
455
+ });
456
+ add({
457
+ name: 'C8-omitted-list-none',
458
+ why: 'renderNote emits the literal "none" when a trim occurred but nothing was dropped',
459
+ sections: {
460
+ instructions: tokensOf(10, 'I'),
461
+ roadmap: tokensOf(10, 'R'),
462
+ plans: [{ file: 'a.md', content: tokensOf(30, 'P') }],
463
+ projectMd: linesOf(120),
464
+ },
465
+ budget: 120,
466
+ options: { safetyMarginPct: 0 },
467
+ });
468
+
469
+ // ── D. Shape + encoding ───────────────────────────────────────────────────
470
+ add({
471
+ name: 'D1-all-sections-present',
472
+ why: 'full assembly order: instructions, note, roadmap, project, plans, context, research, requirements',
473
+ sections: {
474
+ instructions: tokensOf(10, 'I'),
475
+ roadmap: tokensOf(10, 'R'),
476
+ plans: [
477
+ { file: 'one.md', content: tokensOf(10, '1') },
478
+ { file: 'two.md', content: tokensOf(10, '2') },
479
+ ],
480
+ projectMd: linesOf(4),
481
+ context: tokensOf(10, 'C'),
482
+ research: tokensOf(10, 'S'),
483
+ requirements: tokensOf(10, 'Q'),
484
+ },
485
+ budget: 5000,
486
+ options: {},
487
+ });
488
+ add({
489
+ name: 'D2-no-optional-sections',
490
+ why: 'minimum viable shape — only the two never-droppables plus one plan',
491
+ sections: {
492
+ instructions: tokensOf(10, 'I'),
493
+ roadmap: tokensOf(10, 'R'),
494
+ plans: [{ file: 'a.md', content: tokensOf(10, 'P') }],
495
+ },
496
+ budget: 5000,
497
+ options: {},
498
+ });
499
+ add({
500
+ name: 'D3-zero-plans',
501
+ why: 'empty plans array — headers still assembled, reduce over [] is 0',
502
+ sections: {
503
+ instructions: tokensOf(10, 'I'),
504
+ roadmap: tokensOf(10, 'R'),
505
+ plans: [],
506
+ },
507
+ budget: 5000,
508
+ options: {},
509
+ });
510
+ add({
511
+ name: 'D4-crlf-content',
512
+ why: 'row 25 — CRLF measured and sliced identically to LF under the token unit',
513
+ sections: {
514
+ instructions: 'alpha\r\nbeta\r\ngamma',
515
+ roadmap: 'one\r\ntwo\r\nthree',
516
+ plans: [{ file: 'a.md', content: Array.from({ length: 60 }, (_, i) => `line ${i}`).join('\r\n') }],
517
+ projectMd: Array.from({ length: 80 }, (_, i) => `p${i}`).join('\r\n'),
518
+ },
519
+ budget: 120,
520
+ options: { safetyMarginPct: 0 },
521
+ });
522
+ add({
523
+ name: 'D5-unicode-content',
524
+ why: 'row 26 — multibyte and astral content; chars/4 counts UTF-16 units, and slice may split a surrogate pair (pinned as current behavior)',
525
+ sections: {
526
+ instructions: 'héllo wörld ünicode',
527
+ roadmap: '日本語のロードマップ',
528
+ plans: [{ file: 'ünï.md', content: '🎉'.repeat(400) }],
529
+ context: 'Ω'.repeat(200),
530
+ },
531
+ budget: 150,
532
+ options: { safetyMarginPct: 0 },
533
+ });
534
+ add({
535
+ name: 'D6-plan-filename-affects-budget',
536
+ why: 'S5 — per-plan wrapper cost "### <file>\\n\\n" counts toward staticBaseTokens',
537
+ sections: {
538
+ instructions: tokensOf(10, 'I'),
539
+ roadmap: tokensOf(10, 'R'),
540
+ plans: [{ file: 'a-very-long-plan-filename-that-costs-tokens.md', content: tokensOf(10, 'P') }],
541
+ },
542
+ budget: 5000,
543
+ options: {},
544
+ });
545
+
546
+ return cases;
547
+ }
548
+
549
+ // ─── Capture ─────────────────────────────────────────────────────────────────
550
+
551
+ function capture() {
552
+ if (!fs.existsSync(IMPL_PATH)) {
553
+ throw new Error(
554
+ `prompt-budget.cjs not found at ${IMPL_PATH}. Run "npm run build:lib" first ` +
555
+ '(the .cjs is a gitignored tsc output of src/prompt-budget.cts).'
556
+ );
557
+ }
558
+ const { applyBudget } = require(IMPL_PATH);
559
+
560
+ const cases = buildCases();
561
+
562
+ const seen = new Set();
563
+ for (const c of cases) {
564
+ if (seen.has(c.name)) throw new Error(`duplicate corpus case name: ${c.name}`);
565
+ seen.add(c.name);
566
+ }
567
+
568
+ return {
569
+ version: CORPUS_VERSION,
570
+ description:
571
+ 'Characterization corpus of prompt-budget.applyBudget captured from the pre-refactor ' +
572
+ 'implementation. Oracle for issue #2929 (epic #1671 Phase 2). Regenerate with ' +
573
+ 'node scripts/gen-prompt-budget-parity-corpus.cjs --write',
574
+ cases: cases.map((c) => {
575
+ const result = applyBudget({
576
+ sections: c.sections,
577
+ budget: c.budget,
578
+ options: c.options,
579
+ });
580
+ return {
581
+ name: c.name,
582
+ why: c.why,
583
+ input: { sections: c.sections, budget: c.budget, options: c.options },
584
+ expected: { prompt: result.prompt, metadata: result.metadata },
585
+ };
586
+ }),
587
+ };
588
+ }
589
+
590
+ function serialize(corpus) {
591
+ return `${JSON.stringify(corpus, null, 2)}\n`;
592
+ }
593
+
594
+ // ─── CLI ─────────────────────────────────────────────────────────────────────
595
+
596
+ function main(argv) {
597
+ const write = argv.includes('--write');
598
+ const check = argv.includes('--check');
599
+
600
+ if (write === check) {
601
+ process.stderr.write('usage: gen-prompt-budget-parity-corpus.cjs (--write | --check)\n');
602
+ return 2;
603
+ }
604
+
605
+ const serialized = serialize(capture());
606
+
607
+ if (write) {
608
+ fs.mkdirSync(path.dirname(OUT_PATH), { recursive: true });
609
+ fs.writeFileSync(OUT_PATH, serialized);
610
+ const n = JSON.parse(serialized).cases.length;
611
+ process.stdout.write(`wrote ${path.relative(REPO_ROOT, OUT_PATH)} (${n} cases)\n`);
612
+ return 0;
613
+ }
614
+
615
+ if (!fs.existsSync(OUT_PATH)) {
616
+ process.stderr.write(
617
+ `MISSING ${path.relative(REPO_ROOT, OUT_PATH)}\n` +
618
+ 'Run: node scripts/gen-prompt-budget-parity-corpus.cjs --write\n'
619
+ );
620
+ return 1;
621
+ }
622
+
623
+ const committed = fs.readFileSync(OUT_PATH, 'utf8');
624
+ if (committed !== serialized) {
625
+ process.stderr.write(
626
+ `DRIFT in ${path.relative(REPO_ROOT, OUT_PATH)}\n\n` +
627
+ "prompt-budget's observable output no longer matches the committed corpus.\n" +
628
+ 'This corpus is the parity oracle for #2929: a diff here means review-prompt\n' +
629
+ 'output MOVED. That is user-visible.\n\n' +
630
+ 'If the change was NOT intended, fix the code — do not regenerate.\n' +
631
+ 'If it WAS intended, regenerate and justify the diff in the PR:\n' +
632
+ ' node scripts/gen-prompt-budget-parity-corpus.cjs --write\n'
633
+ );
634
+ return 1;
635
+ }
636
+
637
+ process.stdout.write(`ok ${path.relative(REPO_ROOT, OUT_PATH)}\n`);
638
+ return 0;
639
+ }
640
+
641
+ if (require.main === module) {
642
+ process.exitCode = main(process.argv.slice(2));
643
+ }
644
+
645
+ module.exports = { buildCases, capture, serialize, OUT_PATH, CORPUS_VERSION };
@@ -35,6 +35,7 @@ const path = require('node:path');
35
35
 
36
36
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
37
37
  const { renderMarkdown } = require('./registry-schema.cjs');
38
+ const { normalizeEol } = require('../gsd-core/bin/lib/text-lines.cjs');
38
39
 
39
40
  const SOURCES = [
40
41
  { type: 'capability', jsonFile: 'capabilities.json', mdFile: 'capability-registry.md' },
@@ -42,18 +43,6 @@ const SOURCES = [
42
43
  { type: 'reviewer', jsonFile: 'reviewers.json', mdFile: 'reviewer-registry.md', optional: true },
43
44
  ];
44
45
 
45
- /**
46
- * The generator always writes LF; a Windows checkout (autocrlf) may present
47
- * committed files with CRLF. Normalize before comparing so `--check` only
48
- * fails on real content drift, not checkout-introduced line-ending noise.
49
- *
50
- * @param {string} content
51
- * @returns {string}
52
- */
53
- function normalizeLineEndings(content) {
54
- return content.replace(/\r/g, '');
55
- }
56
-
57
46
  function getRegistriesDir() {
58
47
  return path.join(process.cwd(), 'docs', 'registries');
59
48
  }
@@ -126,7 +115,7 @@ function main() {
126
115
  continue;
127
116
  }
128
117
  const committed = fs.readFileSync(mdPath, 'utf8');
129
- if (normalizeLineEndings(committed) !== normalizeLineEndings(rendered)) {
118
+ if (normalizeEol(committed) !== normalizeEol(rendered)) {
130
119
  process.stderr.write(`${mdFile} is stale. Run:\n node scripts/gen-registry.cjs --write\n`);
131
120
  anyDrift = true;
132
121
  }
@@ -149,4 +138,4 @@ function main() {
149
138
 
150
139
  if (require.main === module) runMain(main);
151
140
 
152
- module.exports = { main, renderFor, SOURCES, normalizeLineEndings };
141
+ module.exports = { main, renderFor, SOURCES, normalizeLineEndings: normalizeEol };