@opengsd/gsd-core 1.13.0 → 1.15.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 (441) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +3 -3
  4. package/README.ko-KR.md +3 -3
  5. package/README.pt-BR.md +3 -3
  6. package/README.zh-CN.md +3 -3
  7. package/agents/gsd-advisor-researcher.compact.md +85 -0
  8. package/agents/gsd-ai-researcher.compact.md +96 -0
  9. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  10. package/agents/gsd-code-fixer.compact.md +459 -0
  11. package/agents/gsd-code-fixer.md +9 -8
  12. package/agents/gsd-code-reviewer.compact.md +269 -0
  13. package/agents/gsd-code-reviewer.md +15 -3
  14. package/agents/gsd-codebase-mapper.compact.md +760 -0
  15. package/agents/gsd-debug-session-manager.compact.md +360 -0
  16. package/agents/gsd-debug-session-manager.md +17 -2
  17. package/agents/gsd-debugger.md +2 -2
  18. package/agents/gsd-doc-classifier.compact.md +192 -0
  19. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  20. package/agents/gsd-doc-verifier.compact.md +143 -0
  21. package/agents/gsd-doc-writer.compact.md +440 -0
  22. package/agents/gsd-dom-verifier.compact.md +138 -0
  23. package/agents/gsd-domain-researcher.compact.md +141 -0
  24. package/agents/gsd-eval-auditor.compact.md +160 -0
  25. package/agents/gsd-eval-auditor.md +1 -1
  26. package/agents/gsd-eval-planner.compact.md +137 -0
  27. package/agents/gsd-executor.md +13 -8
  28. package/agents/gsd-framework-selector.compact.md +82 -0
  29. package/agents/gsd-integration-checker.compact.md +245 -0
  30. package/agents/gsd-intel-updater.compact.md +226 -0
  31. package/agents/gsd-intel-updater.md +1 -1
  32. package/agents/gsd-mempalace-curator.compact.md +45 -0
  33. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  34. package/agents/gsd-pattern-mapper.compact.md +275 -0
  35. package/agents/gsd-phase-researcher.md +19 -11
  36. package/agents/gsd-plan-checker.md +8 -7
  37. package/agents/gsd-planner.md +12 -8
  38. package/agents/gsd-project-researcher.compact.md +587 -0
  39. package/agents/gsd-project-researcher.md +1 -1
  40. package/agents/gsd-research-synthesizer.compact.md +212 -0
  41. package/agents/gsd-research-synthesizer.md +1 -1
  42. package/agents/gsd-roadmapper.compact.md +454 -0
  43. package/agents/gsd-roadmapper.md +13 -0
  44. package/agents/gsd-security-auditor.compact.md +162 -0
  45. package/agents/gsd-ui-auditor.compact.md +404 -0
  46. package/agents/gsd-ui-auditor.md +155 -17
  47. package/agents/gsd-ui-checker.compact.md +277 -0
  48. package/agents/gsd-ui-researcher.compact.md +282 -0
  49. package/agents/gsd-ui-researcher.md +1 -1
  50. package/agents/gsd-user-profiler.compact.md +108 -0
  51. package/agents/gsd-verifier.md +10 -9
  52. package/bin/install.js +848 -163
  53. package/commands/gsd/autonomous.md +2 -2
  54. package/commands/gsd/capture.md +1 -1
  55. package/commands/gsd/cleanup.md +1 -0
  56. package/commands/gsd/code-review.md +2 -1
  57. package/commands/gsd/complete-milestone.md +1 -0
  58. package/commands/gsd/config.md +1 -0
  59. package/commands/gsd/debug.md +1 -0
  60. package/commands/gsd/graphify.md +1 -0
  61. package/commands/gsd/health.md +1 -0
  62. package/commands/gsd/mempalace-capture.md +8 -3
  63. package/commands/gsd/mempalace-recall.md +1 -0
  64. package/commands/gsd/new-milestone.md +1 -0
  65. package/commands/gsd/new-project.md +1 -0
  66. package/commands/gsd/next.md +1 -0
  67. package/commands/gsd/pause-work.md +1 -0
  68. package/commands/gsd/phase.md +1 -0
  69. package/commands/gsd/plan-review-convergence.md +6 -6
  70. package/commands/gsd/pr-branch.md +1 -0
  71. package/commands/gsd/progress.md +1 -1
  72. package/commands/gsd/quick-batch.md +1 -1
  73. package/commands/gsd/resume-work.md +1 -0
  74. package/commands/gsd/review-backlog.md +1 -0
  75. package/commands/gsd/review.md +2 -3
  76. package/commands/gsd/settings.md +2 -1
  77. package/commands/gsd/stats.md +1 -0
  78. package/commands/gsd/thread.md +1 -0
  79. package/commands/gsd/workspace.md +1 -0
  80. package/commands/gsd/workstreams.md +1 -0
  81. package/gsd-core/bin/check-latest-version.cjs +8 -3
  82. package/gsd-core/bin/gsd-tools.cjs +672 -146
  83. package/gsd-core/bin/lib/adr-parser.cjs +4 -2
  84. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  85. package/gsd-core/bin/lib/audit.cjs +119 -34
  86. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  87. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  88. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  89. package/gsd-core/bin/lib/capability-registry.cjs +96 -189
  90. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  91. package/gsd-core/bin/lib/capability-validator.cjs +14 -2
  92. package/gsd-core/bin/lib/check-command-router.cjs +213 -49
  93. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  94. package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
  95. package/gsd-core/bin/lib/commands.cjs +823 -112
  96. package/gsd-core/bin/lib/config-loader.cjs +66 -4
  97. package/gsd-core/bin/lib/config.cjs +186 -45
  98. package/gsd-core/bin/lib/coverage.cjs +1 -1
  99. package/gsd-core/bin/lib/decisions.cjs +164 -45
  100. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  101. package/gsd-core/bin/lib/frontmatter.cjs +13 -0
  102. package/gsd-core/bin/lib/graphify.cjs +10 -2
  103. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  104. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
  105. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  106. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  107. package/gsd-core/bin/lib/init.cjs +614 -86
  108. package/gsd-core/bin/lib/install-engine.cjs +29 -3
  109. package/gsd-core/bin/lib/install-profiles.cjs +14 -0
  110. package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
  111. package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
  112. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  113. package/gsd-core/bin/lib/milestone.cjs +37 -13
  114. package/gsd-core/bin/lib/model-resolver.cjs +253 -53
  115. package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
  116. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  117. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  118. package/gsd-core/bin/lib/phase-id.cjs +268 -27
  119. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  120. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  121. package/gsd-core/bin/lib/phase.cjs +393 -88
  122. package/gsd-core/bin/lib/plan-document.cjs +49 -1
  123. package/gsd-core/bin/lib/planning-document.cjs +459 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
  126. package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
  127. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  128. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  129. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  130. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  131. package/gsd-core/bin/lib/project-root.cjs +41 -2
  132. package/gsd-core/bin/lib/quick-batch.cjs +1 -1
  133. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  134. package/gsd-core/bin/lib/research-store.cjs +11 -12
  135. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  136. package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
  137. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  138. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  139. package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
  140. package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
  141. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
  142. package/gsd-core/bin/lib/roadmap.cjs +356 -42
  143. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
  144. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
  145. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
  146. package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
  147. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
  148. package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
  149. package/gsd-core/bin/lib/security.cjs +126 -7
  150. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
  151. package/gsd-core/bin/lib/state-document.cjs +130 -28
  152. package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
  153. package/gsd-core/bin/lib/state-transition.cjs +181 -30
  154. package/gsd-core/bin/lib/state.cjs +265 -27
  155. package/gsd-core/bin/lib/surface.cjs +77 -3
  156. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  157. package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
  158. package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
  159. package/gsd-core/bin/lib/uat.cjs +9 -1
  160. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  161. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
  162. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  163. package/gsd-core/bin/lib/update-context.cjs +30 -24
  164. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  165. package/gsd-core/bin/lib/verification.cjs +315 -30
  166. package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
  167. package/gsd-core/bin/lib/verify.cjs +320 -48
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
  169. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  170. package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
  171. package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
  172. package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
  173. package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
  174. package/gsd-core/references/checkpoints.md +5 -3
  175. package/gsd-core/references/compact-content-gate.md +66 -0
  176. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  177. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  178. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  179. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  180. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  181. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  182. package/gsd-core/references/edge-probe.md +195 -21
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
  184. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  185. package/gsd-core/references/gsd-run-resolver.md +1 -1
  186. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  187. package/gsd-core/references/model-profiles.md +13 -4
  188. package/gsd-core/references/phase-argument-parsing.md +9 -7
  189. package/gsd-core/references/phase-id-convention.md +28 -0
  190. package/gsd-core/references/planner-gap-closure.md +2 -0
  191. package/gsd-core/references/planner-load-graph-context.md +24 -13
  192. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  193. package/gsd-core/references/planning-config.md +14 -2
  194. package/gsd-core/references/tdd.md +30 -4
  195. package/gsd-core/references/thinking-models-planning.md +18 -2
  196. package/gsd-core/references/ui-consideration-probe.md +10 -5
  197. package/gsd-core/references/verification-patterns.md +17 -4
  198. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  199. package/gsd-core/references/worktree-path-safety.md +433 -2
  200. package/gsd-core/templates/README.md +7 -1
  201. package/gsd-core/templates/state.md +6 -3
  202. package/gsd-core/templates/summary.compact.md +212 -0
  203. package/gsd-core/templates/user-setup.compact.md +199 -0
  204. package/gsd-core/templates/user-setup.md +0 -9
  205. package/gsd-core/templates/verification-report.md +1 -1
  206. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  207. package/gsd-core/workflows/add-backlog.md +1 -1
  208. package/gsd-core/workflows/add-phase.md +1 -1
  209. package/gsd-core/workflows/add-tests.md +2 -2
  210. package/gsd-core/workflows/add-todo.md +6 -5
  211. package/gsd-core/workflows/ai-integration-phase.md +11 -3
  212. package/gsd-core/workflows/audit-fix.md +1 -1
  213. package/gsd-core/workflows/audit-milestone.md +1 -1
  214. package/gsd-core/workflows/audit-uat.md +1 -1
  215. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  216. package/gsd-core/workflows/autonomous.md +29 -16
  217. package/gsd-core/workflows/check-todos.md +6 -4
  218. package/gsd-core/workflows/cleanup.md +5 -3
  219. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  220. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
  221. package/gsd-core/workflows/code-review-fix.md +108 -22
  222. package/gsd-core/workflows/code-review.md +216 -73
  223. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  224. package/gsd-core/workflows/complete-milestone.md +41 -264
  225. package/gsd-core/workflows/debug.md +3 -3
  226. package/gsd-core/workflows/diagnose-issues.md +1 -1
  227. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  228. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  229. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  230. package/gsd-core/workflows/discuss-phase.md +1 -1
  231. package/gsd-core/workflows/do.md +2 -2
  232. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  233. package/gsd-core/workflows/docs-update.md +17 -158
  234. package/gsd-core/workflows/edit-phase.md +1 -1
  235. package/gsd-core/workflows/eval-review.md +10 -3
  236. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  237. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
  238. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
  239. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  240. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
  241. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  242. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  243. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  244. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  245. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  246. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  247. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  248. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  249. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  250. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  251. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
  252. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  253. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  254. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
  255. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  256. package/gsd-core/workflows/execute-phase.md +83 -172
  257. package/gsd-core/workflows/execute-plan.md +24 -10
  258. package/gsd-core/workflows/explore.md +3 -3
  259. package/gsd-core/workflows/extract-learnings.md +2 -1
  260. package/gsd-core/workflows/fast.md +1 -1
  261. package/gsd-core/workflows/forensics.md +1 -1
  262. package/gsd-core/workflows/graduation.md +1 -1
  263. package/gsd-core/workflows/health.md +2 -2
  264. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  265. package/gsd-core/workflows/help/modes/full.md +5 -5
  266. package/gsd-core/workflows/help/modes/topic.md +15 -5
  267. package/gsd-core/workflows/help.md +1 -1
  268. package/gsd-core/workflows/import.md +2 -2
  269. package/gsd-core/workflows/inbox.md +2 -2
  270. package/gsd-core/workflows/ingest-docs.md +3 -3
  271. package/gsd-core/workflows/insert-phase.md +1 -1
  272. package/gsd-core/workflows/list-seeds.md +1 -1
  273. package/gsd-core/workflows/list-workspaces.md +1 -1
  274. package/gsd-core/workflows/manager.md +2 -2
  275. package/gsd-core/workflows/map-codebase.md +52 -5
  276. package/gsd-core/workflows/milestone-summary.md +1 -1
  277. package/gsd-core/workflows/mvp-phase.md +1 -1
  278. package/gsd-core/workflows/new-milestone.md +56 -14
  279. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  280. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  281. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  282. package/gsd-core/workflows/new-project.md +39 -209
  283. package/gsd-core/workflows/new-workspace.md +2 -2
  284. package/gsd-core/workflows/next.md +1 -1
  285. package/gsd-core/workflows/note.md +1 -1
  286. package/gsd-core/workflows/onboard.md +1 -1
  287. package/gsd-core/workflows/pause-work.md +1 -1
  288. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  289. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  290. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  291. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  292. package/gsd-core/workflows/plan-phase.md +45 -187
  293. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  294. package/gsd-core/workflows/plant-seed.md +62 -20
  295. package/gsd-core/workflows/pr-branch.md +132 -20
  296. package/gsd-core/workflows/profile-user.md +2 -2
  297. package/gsd-core/workflows/progress.md +1 -1
  298. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
  299. package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
  300. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
  301. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  302. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  303. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  304. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  305. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  306. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  307. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
  308. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  309. package/gsd-core/workflows/quick-batch.md +1 -1
  310. package/gsd-core/workflows/quick.md +29 -10
  311. package/gsd-core/workflows/reapply-patches.md +86 -6
  312. package/gsd-core/workflows/remove-phase.md +1 -1
  313. package/gsd-core/workflows/remove-workspace.md +2 -2
  314. package/gsd-core/workflows/resume-project.md +1 -1
  315. package/gsd-core/workflows/review.md +31 -16
  316. package/gsd-core/workflows/scan.md +1 -1
  317. package/gsd-core/workflows/secure-phase.md +3 -2
  318. package/gsd-core/workflows/settings-advanced.md +30 -10
  319. package/gsd-core/workflows/settings-integrations.md +2 -3
  320. package/gsd-core/workflows/settings.md +22 -9
  321. package/gsd-core/workflows/ship.md +3 -2
  322. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  323. package/gsd-core/workflows/sketch.md +1 -1
  324. package/gsd-core/workflows/smart-entry.md +2 -2
  325. package/gsd-core/workflows/spec-phase.md +15 -5
  326. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  327. package/gsd-core/workflows/spike.md +1 -1
  328. package/gsd-core/workflows/stats.md +1 -1
  329. package/gsd-core/workflows/sync-skills.md +5 -5
  330. package/gsd-core/workflows/thread.md +1 -1
  331. package/gsd-core/workflows/transition.md +1 -1
  332. package/gsd-core/workflows/ui-phase.md +44 -8
  333. package/gsd-core/workflows/ui-review.md +18 -4
  334. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  335. package/gsd-core/workflows/undo.md +339 -20
  336. package/gsd-core/workflows/update.md +14 -12
  337. package/gsd-core/workflows/validate-phase.md +3 -2
  338. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  339. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  340. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  341. package/gsd-core/workflows/verify-work.md +101 -196
  342. package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
  343. package/hooks/dist/gsd-context-monitor.js +88 -15
  344. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  345. package/hooks/dist/gsd-secret-read-guard.js +71 -19
  346. package/hooks/dist/gsd-statusline.js +81 -20
  347. package/hooks/dist/gsd-validate-commit.sh +97 -8
  348. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  349. package/hooks/dist/gsd-write-guard.js +46 -1
  350. package/hooks/dist/lib/dispatch-identity.js +187 -0
  351. package/hooks/dist/lib/filename-classification.js +64 -0
  352. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  353. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  354. package/hooks/gsd-agent-isolation-guard.js +66 -16
  355. package/hooks/gsd-context-monitor.js +88 -15
  356. package/hooks/gsd-cursor-subagent-start.js +34 -14
  357. package/hooks/gsd-secret-read-guard.js +71 -19
  358. package/hooks/gsd-statusline.js +81 -20
  359. package/hooks/gsd-validate-commit.sh +97 -8
  360. package/hooks/gsd-worktree-path-guard.js +25 -14
  361. package/hooks/gsd-write-guard.js +46 -1
  362. package/hooks/lib/dispatch-identity.js +187 -0
  363. package/hooks/lib/filename-classification.js +64 -0
  364. package/hooks/lib/isolation-deny-reason.js +53 -1
  365. package/hooks/lib/isolation-sentinel.js +58 -19
  366. package/package.json +11 -6
  367. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  368. package/scripts/benchmark-compact-content.cjs +368 -0
  369. package/scripts/build-hooks.js +15 -6
  370. package/scripts/check-contract-drift.cjs +131 -12
  371. package/scripts/check-env.cjs +36 -8
  372. package/scripts/check-glossary-refs.cjs +25 -21
  373. package/scripts/ci-next-health.cjs +271 -0
  374. package/scripts/ci-prepare-test-scope.cjs +7 -7
  375. package/scripts/ci-test-scope.cjs +126 -20
  376. package/scripts/ci-timeout-report.cjs +1 -1
  377. package/scripts/command-contract-helpers.cjs +3 -0
  378. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  379. package/scripts/docs-guard-registry.cjs +35 -2
  380. package/scripts/gen-adr-index.cjs +8 -2
  381. package/scripts/gen-inventory-manifest.cjs +12 -0
  382. package/scripts/gen-loop-host-contract.cjs +69 -0
  383. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  384. package/scripts/lib/drift-scan.cjs +1 -1
  385. package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
  386. package/scripts/lib/ndjson-reporter.cjs +3 -2
  387. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  388. package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
  389. package/scripts/lib/suite-detection.cjs +32 -0
  390. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  391. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
  392. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  393. package/scripts/lint-phase-id-drift.cjs +623 -13
  394. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  395. package/scripts/lint-response-language-coverage.cjs +9 -3
  396. package/scripts/lint-retired-runtime-name.cjs +619 -0
  397. package/scripts/lint-source-test-name-collision.cjs +1 -1
  398. package/scripts/lint-state-write-path-drift.cjs +93 -0
  399. package/scripts/lint-test-file-count.allowlist.json +29 -9
  400. package/scripts/lint-vendored-deps.cjs +128 -17
  401. package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
  402. package/scripts/prompt-injection-scan.sh +18 -0
  403. package/scripts/release-tarball-smoke.cjs +194 -1
  404. package/scripts/workflow-size.cjs +139 -0
  405. package/skills/gsd-autonomous/SKILL.md +2 -2
  406. package/skills/gsd-capture/SKILL.md +1 -1
  407. package/skills/gsd-cleanup/SKILL.md +1 -0
  408. package/skills/gsd-code-review/SKILL.md +2 -1
  409. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  410. package/skills/gsd-config/SKILL.md +1 -0
  411. package/skills/gsd-debug/SKILL.md +1 -0
  412. package/skills/gsd-graphify/SKILL.md +1 -0
  413. package/skills/gsd-health/SKILL.md +1 -0
  414. package/skills/gsd-mempalace-capture/SKILL.md +8 -3
  415. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  416. package/skills/gsd-new-milestone/SKILL.md +1 -0
  417. package/skills/gsd-new-project/SKILL.md +1 -0
  418. package/skills/gsd-next/SKILL.md +1 -0
  419. package/skills/gsd-pause-work/SKILL.md +1 -0
  420. package/skills/gsd-phase/SKILL.md +1 -0
  421. package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
  422. package/skills/gsd-pr-branch/SKILL.md +1 -0
  423. package/skills/gsd-progress/SKILL.md +1 -1
  424. package/skills/gsd-quick-batch/SKILL.md +1 -1
  425. package/skills/gsd-resume-work/SKILL.md +1 -0
  426. package/skills/gsd-review/SKILL.md +2 -3
  427. package/skills/gsd-review-backlog/SKILL.md +1 -0
  428. package/skills/gsd-settings/SKILL.md +2 -1
  429. package/skills/gsd-stats/SKILL.md +1 -0
  430. package/skills/gsd-thread/SKILL.md +1 -0
  431. package/skills/gsd-workspace/SKILL.md +1 -0
  432. package/skills/gsd-workstreams/SKILL.md +1 -0
  433. package/vscode/package.json +1 -1
  434. package/gsd-core/templates/claude-md.md +0 -145
  435. package/gsd-core/templates/codebase/concerns.md +0 -310
  436. package/gsd-core/templates/codebase/conventions.md +0 -307
  437. package/gsd-core/templates/codebase/integrations.md +0 -280
  438. package/gsd-core/templates/codebase/structure.md +0 -285
  439. package/gsd-core/templates/codebase/testing.md +0 -480
  440. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  441. package/gsd-core/templates/discovery.md +0 -146
@@ -38,6 +38,7 @@
38
38
  "ui_phase": true,
39
39
  "ui_safety_gate": true,
40
40
  "text_mode": false,
41
+ "compact_content": false,
41
42
  "research_before_questions": false,
42
43
  "discuss_mode": "discuss",
43
44
  "skip_discuss": false,
@@ -67,6 +68,9 @@
67
68
  "sub_repos": [],
68
69
  "granularity": "standard"
69
70
  },
71
+ "planner": {
72
+ "stall_detection_enabled": true
73
+ },
70
74
  "hooks": {
71
75
  "context_warnings": true,
72
76
  "workflow_guard": false
@@ -22,6 +22,7 @@
22
22
  "workflow.smart_zone_tokens",
23
23
  "workflow.human_verify_mode",
24
24
  "workflow.text_mode",
25
+ "workflow.compact_content",
25
26
  "workflow.research_before_questions",
26
27
  "workflow.discuss_mode",
27
28
  "workflow.skip_discuss",
@@ -71,11 +72,16 @@
71
72
  "workflow.context_guard_mode",
72
73
  "executor.stall_detect_interval_minutes",
73
74
  "executor.stall_threshold_minutes",
75
+ "planner.stall_detection_enabled",
74
76
  "planner.stall_detect_interval_minutes",
75
77
  "planner.stall_threshold_minutes",
76
78
  "workflow.inline_plan_threshold",
77
79
  "hooks.context_warnings",
80
+ "hooks.context_warning_threshold",
81
+ "hooks.context_critical_threshold",
78
82
  "hooks.workflow_guard",
83
+ "hooks.commit_types",
84
+ "hooks.community",
79
85
  "workflow.context_coverage_gate",
80
86
  "statusline.show_last_command",
81
87
  "statusline.context_position",
@@ -17,37 +17,75 @@
17
17
  * # false-positive halts beat silent successes
18
18
  * # on lost content)
19
19
  * [--json] # emit JSON report instead of human text
20
+ * [--classify] # pre-merge mode: classify each backed-up
21
+ * # file as incorporated / needs_merge /
22
+ * # unknown (#4136); always exits 0
23
+ * [--min-baseline-coverage <0..1>] # OPT-IN strict gate (#4135): exit 3
24
+ * # when the fraction of files verified
25
+ * # against a resolved pristine baseline
26
+ * # (baseline_covered / checked) falls below
27
+ * # the threshold. Default: off — a
28
+ * # low-coverage run stays green per the
29
+ * # documented #934 advisory posture, but
30
+ * # its coverage is ALWAYS headline-reported.
20
31
  *
21
- * Exit codes:
32
+ * Exit codes (default gate mode):
22
33
  * 0 — every user-added line is present in the merged file (gate passes)
23
- * 1 — at least one missing line in at least one file (gate fails)
34
+ * 1 — at least one missing line in at least one file (gate fails); outranks
35
+ * a coverage-gate failure when both apply
24
36
  * 2 — usage / structural error (e.g. patches dir missing)
37
+ * 3 — opt-in coverage gate failed (--min-baseline-coverage not met; #4135)
25
38
  *
26
39
  * Bug #2969: the Step 5 gate previously trusted Claude's free-text "verified:
27
40
  * yes/no" reporting per hunk. The LLM was filling in `yes` even when content
28
41
  * had been silently dropped. Moving the check to a deterministic script is the
29
42
  * durability fix.
43
+ *
44
+ * Bug #4136 adds --classify (pre-merge mode, always exit 0 — informational;
45
+ * the binding gate remains the post-merge default run): per backed-up file,
46
+ * decides whether the user's modification was already adopted upstream
47
+ * ("Incorporated", reapply-patches.md Step 4 item 6). The classification is
48
+ * ONLY produced from a hash-validated pristine baseline with every
49
+ * significant user-added line present verbatim in the freshly installed
50
+ * version — a false Incorporated silently retires a live customization,
51
+ * which is worse than no Incorporated at all.
30
52
  */
31
53
 
32
54
  const fs = require('node:fs');
33
55
  const path = require('node:path');
34
56
  const crypto = require('node:crypto');
35
57
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
58
+ // #4145: shared hash-first recovery for baselines stored at an unexpected
59
+ // path under gsd-pristine/ (e.g. without the gsd-core/ prefix an earlier
60
+ // release's writer dropped). Same module the installer's preserve-check uses,
61
+ // so the two readers cannot drift apart again.
62
+ const { findPristineByHash, findPristineInGit } = require('./lib/pristine-baseline.cjs');
36
63
 
37
64
  const SIGNIFICANT_MIN_CHARS = 12;
38
65
  const GSD_HOOK_VERSION_LINE_RE = /^(?:\/\/|#)\s*gsd-hook-version:\s*\S+\s*$/i;
39
66
 
40
67
  function parseArgs(argv) {
41
- const opts = { patchesDir: null, configDir: null, pristineDir: null, json: false };
68
+ const opts = { patchesDir: null, configDir: null, pristineDir: null, json: false, classify: false, minBaselineCoverage: null };
42
69
  for (let i = 0; i < argv.length; i++) {
43
70
  const arg = argv[i];
44
71
  if (arg === '--patches-dir') opts.patchesDir = argv[++i];
45
72
  else if (arg === '--config-dir') opts.configDir = argv[++i];
46
73
  else if (arg === '--pristine-dir') opts.pristineDir = argv[++i];
47
74
  else if (arg === '--json') opts.json = true;
48
- else if (arg === '--help' || arg === '-h') {
75
+ else if (arg === '--classify') opts.classify = true;
76
+ else if (arg === '--min-baseline-coverage') {
77
+ const raw = argv[++i];
78
+ if (raw === undefined) {
79
+ throw new ExitError(2, '--min-baseline-coverage requires a value between 0 and 1');
80
+ }
81
+ const value = Number(raw);
82
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
83
+ throw new ExitError(2, `--min-baseline-coverage must be a number between 0 and 1, got: ${raw}`);
84
+ }
85
+ opts.minBaselineCoverage = value;
86
+ } else if (arg === '--help' || arg === '-h') {
49
87
  process.stdout.write(
50
- 'usage: verify-reapply-patches.cjs --patches-dir <path> --config-dir <path> [--pristine-dir <path>] [--json]\n',
88
+ 'usage: verify-reapply-patches.cjs --patches-dir <path> --config-dir <path> [--pristine-dir <path>] [--json] [--classify] [--min-baseline-coverage <0..1>]\n',
51
89
  );
52
90
  throw new ExitError(0);
53
91
  } else {
@@ -178,12 +216,155 @@ const REASON = Object.freeze({
178
216
  // block" rather than "ignore everything" — it only applies when the hash was
179
217
  // recorded (modern installer) but the file is absent (specific gap).
180
218
  OK_NO_BASELINE: 'ok_no_baseline',
219
+ // Bug #4136: an on-disk gsd-pristine/ snapshot exists for this file but
220
+ // backup-meta.json records no pristine_hashes entry for it (older
221
+ // installer), so nothing confirms the snapshot is the baseline the backup
222
+ // was captured against — it could be a drifted newer-version snapshot the
223
+ // #3657 guard cannot see. The default gate still uses it as the diff
224
+ // baseline (pre-#3657 behaviour, unchanged); --classify refuses to confirm
225
+ // adoption on an unvalidated baseline, so a file is never classified
226
+ // Incorporated on the snapshot's say-so alone.
227
+ OK_UNVALIDATED_BASELINE: 'ok_unvalidated_baseline',
181
228
  FAIL_INSTALLED_MISSING: 'fail_installed_missing',
182
229
  FAIL_INSTALLED_NOT_REGULAR_FILE: 'fail_installed_not_regular_file',
183
230
  FAIL_READ_ERROR: 'fail_read_error',
184
231
  FAIL_USER_LINES_MISSING: 'fail_user_lines_missing',
185
232
  });
186
233
 
234
+ /**
235
+ * Bug #4136: stable per-file classification codes for --classify (pre-merge)
236
+ * mode. Tests assert via `assert.equal(result.classification, CLASSIFICATION.X)`
237
+ * rather than regex-matching prose, mirroring the REASON enum contract.
238
+ *
239
+ * INCORPORATED — hash-validated pristine baseline confirms that every
240
+ * significant user-added line is already present verbatim
241
+ * in the freshly installed version: upstream adopted the
242
+ * customization. The workflow must NOT re-apply the diff.
243
+ * NEEDS_MERGE — validated baseline, but at least one significant
244
+ * user-added line is absent from the fresh install; the
245
+ * missing lines are listed for the merge step.
246
+ * UNKNOWN — no confirmable baseline (drift #3657, absent #934,
247
+ * unvalidated, or no --pristine-dir), zero significant
248
+ * user-added delta, or a structural failure. Never
249
+ * Incorporated.
250
+ */
251
+ const CLASSIFICATION = Object.freeze({
252
+ INCORPORATED: 'incorporated',
253
+ NEEDS_MERGE: 'needs_merge',
254
+ UNKNOWN: 'unknown',
255
+ });
256
+
257
+ /**
258
+ * Typed outcome of resolving one file's pristine baseline. Shared by the
259
+ * post-merge gate (verifyFile) and the pre-merge classifier (classifyFile)
260
+ * so the two can never drift on what counts as a usable baseline (#4136).
261
+ */
262
+ const PRISTINE_RESOLUTION = Object.freeze({
263
+ VALIDATED: 'validated', // recorded hash matches the resolved snapshot
264
+ UNVALIDATED: 'unvalidated', // snapshot present, no recorded hash to confirm it
265
+ DRIFTED: 'drifted', // recorded hash mismatches the snapshot (#3657)
266
+ ABSENT_RECORDED: 'absent_recorded', // recorded hash, no snapshot anywhere (#934/#4145)
267
+ OVERBROAD: 'overbroad', // no pristine dir, or a non-file at the path
268
+ });
269
+
270
+ /**
271
+ * Resolve the pristine baseline for one backed-up file, applying the #3657
272
+ * drift guard, the #4145 hash-first recovery, and the #934 absent-baseline
273
+ * guard. Extracted from verifyFile's inline block so --classify reasons over
274
+ * the exact same baseline semantics the post-merge gate enforces.
275
+ */
276
+ function resolvePristineBaseline({ relPath, configDir, pristineDir, pristineHashes }) {
277
+ const hashKey = relPath.replace(/\\/g, '/');
278
+ const recordedHash = pristineHashes && pristineHashes[hashKey];
279
+ if (pristineDir) {
280
+ const pristinePath = path.join(pristineDir, relPath);
281
+ let pristinePathExists = false;
282
+ try {
283
+ const stat = fs.statSync(pristinePath);
284
+ pristinePathExists = true; // path exists (any type)
285
+ if (stat.isFile()) {
286
+ const candidate = fs.readFileSync(pristinePath, 'utf8');
287
+ if (recordedHash) {
288
+ if (sha256(candidate) === recordedHash) {
289
+ // Hash matches: the on-disk pristine is the correct baseline.
290
+ return { resolution: PRISTINE_RESOLUTION.VALIDATED, content: candidate };
291
+ }
292
+ // Hash mismatch: the on-disk gsd-pristine/ was refreshed to a newer
293
+ // GSD version after the backup was captured (Bug #3657). Using it as
294
+ // the diff baseline would invert the delta and produce false
295
+ // FAIL_USER_LINES_MISSING reports.
296
+ return { resolution: PRISTINE_RESOLUTION.DRIFTED, content: null };
297
+ }
298
+ // No recorded hash for this file (older installer or absent
299
+ // backup-meta) — the default gate uses the on-disk pristine as-is
300
+ // (pre-fix behaviour); --classify treats it as unvalidated.
301
+ return { resolution: PRISTINE_RESOLUTION.UNVALIDATED, content: candidate };
302
+ }
303
+ // Non-file at pristinePath (e.g. a directory): fall through to
304
+ // over-broad mode below, which is safe and conservative.
305
+ } catch {
306
+ // Pristine stat threw — path is absent (ENOENT) or inaccessible.
307
+ // pristinePathExists stays false.
308
+ }
309
+
310
+ // Bug #4145: the canonical join missed, but the recorded hash is the
311
+ // baseline authority the #3657 drift guard already trusts. Before
312
+ // reporting ABSENT_RECORDED, scan gsd-pristine/ for byte-identical content
313
+ // (an earlier release may have stored the snapshot without the gsd-core/
314
+ // prefix). An exact sha-256 match cannot be the wrong baseline, and
315
+ // gsd-pristine/ holds only backed-up files, so the scan is small. The
316
+ // canonical path itself is excluded — a mismatching file at the joined
317
+ // path is drift (#3657), never re-adopted through the scan. A recovered
318
+ // baseline is hash-confirmed by construction, so it VALIDATES.
319
+ if (!pristinePathExists && recordedHash) {
320
+ try {
321
+ const recoveredRel = findPristineByHash(pristineDir, recordedHash, hashKey);
322
+ if (recoveredRel) {
323
+ return {
324
+ resolution: PRISTINE_RESOLUTION.VALIDATED,
325
+ content: fs.readFileSync(path.join(pristineDir, recoveredRel), 'utf8'),
326
+ };
327
+ }
328
+ } catch {
329
+ // scan or read failure — fall through to the ABSENT_RECORDED posture
330
+ }
331
+ }
332
+
333
+ if (pristinePathExists) {
334
+ // Present but not a regular file — over-broad mode is the safe side.
335
+ return { resolution: PRISTINE_RESOLUTION.OVERBROAD, content: null };
336
+ }
337
+
338
+ // Bug #4135: still nothing under gsd-pristine/ — the multi-version
339
+ // regeneration collapse (the #3407 promotion rule only keeps files
340
+ // byte-identical across the WHOLE version span, so the surviving set is
341
+ // precisely the files upstream did not change). When the config dir is
342
+ // itself a git repository, its history may hold the outgoing bytes: this
343
+ // is the workflow's documented Option A (reapply-patches.md Step 2),
344
+ // anchored by the SAME authority every other tier trusts — exact
345
+ // pristine_hashes equality. Read-only (git log / git show); any failure
346
+ // (no git, not a repo, no matching blob) degrades to ABSENT_RECORDED.
347
+ // A recovered baseline is hash-confirmed by construction, so it VALIDATES.
348
+ if (!pristinePathExists && recordedHash) {
349
+ try {
350
+ const fromGit = findPristineInGit(configDir, hashKey, recordedHash);
351
+ if (fromGit !== null) {
352
+ return { resolution: PRISTINE_RESOLUTION.VALIDATED, content: fromGit };
353
+ }
354
+ } catch {
355
+ // git unavailable or history walk failed — ABSENT_RECORDED posture
356
+ }
357
+ }
358
+ // Bug #934: recordedHash is present (modern installer) but no
359
+ // hash-matching pristine exists anywhere under gsd-pristine/ (the stat
360
+ // missed and the #4145 recovery found nothing).
361
+ if (recordedHash) {
362
+ return { resolution: PRISTINE_RESOLUTION.ABSENT_RECORDED, content: null };
363
+ }
364
+ }
365
+ return { resolution: PRISTINE_RESOLUTION.OVERBROAD, content: null };
366
+ }
367
+
187
368
  /**
188
369
  * #4086: resolve where a backed-up file's INSTALLED counterpart lives.
189
370
  * Primary is the config-dir-relative join (the manifest key's native form).
@@ -225,7 +406,7 @@ function resolveSkillsRedirect(configDir) {
225
406
  if (!kind) return null;
226
407
  const root = path.resolve(path.join(kind.home || configDir, kind.destSubpath));
227
408
  const resolvedConfig = path.resolve(configDir);
228
- if (root === resolvedConfig || root.startsWith(resolvedConfig + path.sep)) return null;
409
+ if (root === resolvedConfig || root.startsWith(resolvedConfig + path.sep)) return null; // allow-handrolled-containment: answers whether the skills root is a separate location, not a containment/admission decision
229
410
  // Same descriptor-driven manifest prefix writeManifest uses for skills
230
411
  // keys (hermes nests under 'skills/gsd/', everyone else 'skills/'), read
231
412
  // from the same shipped registry the installer's _resolveHostBehaviors
@@ -247,6 +428,47 @@ function resolveSkillsRedirect(configDir) {
247
428
  }
248
429
  }
249
430
 
431
+ /**
432
+ * #4135: baseline-coverage accounting. A file counts as baseline-covered
433
+ * when its diff was actually computed against a resolved pristine baseline,
434
+ * i.e. its reason is one of the baseline-diff outcomes (null = lines
435
+ * verified present, OK_NO_USER_LINES_VS_PRISTINE = no user lines versus
436
+ * the baseline, FAIL_USER_LINES_MISSING = lines missing). Everything else
437
+ * is uncovered: the silent skips (no baseline anywhere per #934/#4135,
438
+ * drift per #3657, no-pristine over-broad with nothing significant) AND
439
+ * the structural failures (installed missing / not a file / read error),
440
+ * which never reached a baseline — they are loud blocking failures, but
441
+ * they were not baseline-verified either, and this aggregate exists to
442
+ * state exactly how much of the run the gate could reason about. When
443
+ * --pristine-dir is absent the whole run is over-broad (#2998 fallback):
444
+ * no file was verified against a baseline, covered is 0 by construction.
445
+ */
446
+ const BASELINE_VERIFIED_REASONS = new Set([
447
+ REASON.OK_NO_USER_LINES_VS_PRISTINE,
448
+ REASON.FAIL_USER_LINES_MISSING,
449
+ ]);
450
+
451
+ function classifyBaselineCoverage(results, pristineDirProvided) {
452
+ if (!pristineDirProvided) return 0;
453
+ let covered = 0;
454
+ for (const r of results) {
455
+ if (r.reason === null || BASELINE_VERIFIED_REASONS.has(r.reason)) covered++;
456
+ }
457
+ return covered;
458
+ }
459
+
460
+ /**
461
+ * #4135: the headline string for the human summary. A run with 12 of 13
462
+ * files unbaselined must not present the same way as a fully-verified one —
463
+ * the N-of-M form is the issue's own framing, and the unverified tail is
464
+ * only rendered when it is non-zero.
465
+ */
466
+ function coverageHeadline(covered, total) {
467
+ const unverified = Math.max(0, total - covered);
468
+ const base = `Baseline coverage: ${covered} of ${total} file(s) verified against a pristine baseline`;
469
+ return unverified > 0 ? `${base} (${unverified} unverified)` : base;
470
+ }
471
+
250
472
  function verifyFile({ relPath, patchesDir, configDir, pristineDir, pristineHashes, skillsRedirect }) {
251
473
  const backupPath = path.join(patchesDir, relPath);
252
474
  const installedPath = resolveInstalledPath(configDir, relPath, skillsRedirect);
@@ -287,80 +509,32 @@ function verifyFile({ relPath, patchesDir, configDir, pristineDir, pristineHashe
287
509
  // Normalize to forward slashes so the key lookup matches on Windows
288
510
  // where path.join produces backslash-separated relPath values but
289
511
  // backup-meta.json stores keys written with forward slashes.
290
- const hashKey = relPath.replace(/\\/g, '/');
291
- const recordedHash = pristineHashes && pristineHashes[hashKey];
512
+ // #4136: the inline baseline block (stat/read + #3657 drift guard + #4145
513
+ // hash-first recovery + #934 absent guard) moved into resolvePristineBaseline
514
+ // so the pre-merge classifier reasons over the exact same semantics.
515
+ const { resolution, content: pristineContent } =
516
+ resolvePristineBaseline({ relPath, configDir, pristineDir, pristineHashes });
292
517
 
293
- let pristineContent = null;
294
- if (pristineDir) {
295
- const pristinePath = path.join(pristineDir, relPath);
296
- // Bug #934: track whether the pristine path EXISTS on disk (stat did not
297
- // throw ENOENT). A regular file that fails to read, or a non-file path
298
- // (e.g. a directory accidentally placed at the pristine path), is treated
299
- // as "present but unusable" — we fall to over-broad mode (safe side).
300
- // OK_NO_BASELINE is reserved for the strictly absent case: stat throws,
301
- // meaning the file was never written (the gap the bug describes).
302
- let pristinePathExists = false;
303
- try {
304
- const stat = fs.statSync(pristinePath);
305
- pristinePathExists = true; // path exists (any type)
306
- if (stat.isFile()) {
307
- const candidate = fs.readFileSync(pristinePath, 'utf8');
308
- // Bug #3657: if backup-meta.json recorded a pristine_hash for this
309
- // file, validate that the on-disk pristine matches it. A mismatch
310
- // means the installer refreshed gsd-pristine/ to a newer GSD version
311
- // after the backup was captured. Using the wrong-version pristine as
312
- // the diff baseline inverts the delta: upstream removals appear as
313
- // "user-added lines that must survive", causing FAIL_USER_LINES_MISSING
314
- // false positives. When the hash is stale, skip the pristine and fall
315
- // through to over-broad mode (every significant backup line is checked).
316
- // Over-broad mode never false-fails for a different reason because all
317
- // backup lines that are genuinely user-added will still be present in a
318
- // correctly merged install.
319
- if (recordedHash) {
320
- if (sha256(candidate) === recordedHash) {
321
- // Hash matches: the on-disk pristine is the correct baseline.
322
- pristineContent = candidate;
323
- } else {
324
- // Hash mismatch: the on-disk gsd-pristine/ was refreshed to a newer
325
- // GSD version after the backup was captured. Using it as the diff
326
- // baseline would invert the delta and produce false FAIL_USER_LINES_MISSING
327
- // reports (Bug #3657). Report the file as ok with a diagnostic code
328
- // so the gate does not false-fail; a re-anchor or git-aware baseline
329
- // step is required to verify this file correctly.
330
- result.reason = REASON.OK_PRISTINE_DRIFT_DETECTED;
331
- return result;
332
- }
333
- } else {
334
- // No recorded hash for this file (older installer or absent
335
- // backup-meta) — use the on-disk pristine as-is (pre-fix behaviour).
336
- pristineContent = candidate;
337
- }
338
- }
339
- // Non-file at pristinePath (e.g. a directory): stat succeeded so
340
- // pristinePathExists is true; we fall through to over-broad mode below,
341
- // which is safe and conservative.
342
- } catch {
343
- // Pristine stat threw — path is absent (ENOENT) or inaccessible.
344
- // pristinePathExists stays false.
345
- }
518
+ // Bug #3657: the resolved snapshot hash-mismatches the recorded baseline.
519
+ // Skip the file with a diagnostic code rather than diffing against the
520
+ // wrong baseline (a re-anchor or git-aware baseline step is required).
521
+ if (resolution === PRISTINE_RESOLUTION.DRIFTED) {
522
+ result.reason = REASON.OK_PRISTINE_DRIFT_DETECTED;
523
+ return result;
524
+ }
346
525
 
347
- // Bug #934: recordedHash is present (modern installer) but the pristine
348
- // path does not exist on disk at all (stat threw above). This means
349
- // saveLocalPatches recorded a hash but could not write the corresponding
350
- // gsd-pristine/ file (the only candidate was discarded because it was from
351
- // a newer release). Falling to over-broad mode here would treat every
352
- // upstream-changed line as a "user-added line that must survive", producing
353
- // false FAIL_USER_LINES_MISSING for each upstream removal. Since we
354
- // cannot reason correctly without a baseline, the safe answer is advisory/
355
- // non-blocking: return OK_NO_BASELINE and let the caller decide.
356
- // NOTE: this guard fires ONLY when stat threw (path absent), not when the
357
- // path is present but non-file — in that case over-broad mode is safer.
358
- if (!pristinePathExists && recordedHash) {
359
- result.reason = REASON.OK_NO_BASELINE;
360
- return result;
361
- }
526
+ // Bug #934 / #4145: a hash was recorded (modern installer) but no
527
+ // hash-matching pristine exists anywhere under gsd-pristine/ —
528
+ // advisory/non-blocking, the caller logs a warning.
529
+ if (resolution === PRISTINE_RESOLUTION.ABSENT_RECORDED) {
530
+ result.reason = REASON.OK_NO_BASELINE;
531
+ return result;
362
532
  }
363
533
 
534
+ // VALIDATED / UNVALIDATED keep the gate's pre-#4136 semantics: the resolved
535
+ // content (null for OVERBROAD) feeds computeUserAddedLines, whose no-pristine
536
+ // branch is the over-broad fallback.
537
+
364
538
  const userAdded = computeUserAddedLines(backupContent, pristineContent);
365
539
  if (userAdded.length === 0) {
366
540
  // Backup and pristine match exactly (or no significant content) — nothing
@@ -383,6 +557,101 @@ function verifyFile({ relPath, patchesDir, configDir, pristineDir, pristineHashe
383
557
  return result;
384
558
  }
385
559
 
560
+ /**
561
+ * Bug #4136: pre-merge classification for reapply-patches Step 4. Runs over
562
+ * the SAME fixture state the gate sees (patches dir + freshly installed
563
+ * config dir + optional pristine dir) but BEFORE any merge, answering the
564
+ * question the gate cannot: is this file's user modification already adopted
565
+ * upstream, such that the workflow should leave the installed file untouched
566
+ * (status `Incorporated`) instead of re-grafting the diff?
567
+ *
568
+ * The classification is deliberately conservative in the direction the issue
569
+ * demands ("a false Incorporated is worse than no Incorporated"):
570
+ * - only a hash-VALIDATED pristine baseline can confirm adoption — drift
571
+ * (#3657), absent (#934/#4145), unvalidated (no recorded hash), and a
572
+ * missing --pristine-dir all classify UNKNOWN;
573
+ * - every significant user-added line (structural/trivial lines excluded by
574
+ * isSignificantLine) must be present verbatim in the fresh install — a
575
+ * signature-looking short line or a code fence matching anywhere in the
576
+ * new file proves nothing;
577
+ * - a backup with zero significant delta vs the validated pristine is
578
+ * UNKNOWN (the workflow's Critical invariant: a backed-up file is never
579
+ * concluded to have "no custom content", and Incorporated is a positive
580
+ * adoption finding, not a skip);
581
+ * - structural failures are reported with their FAIL_* reason but never
582
+ * gate the run — the binding enforcement point stays the post-merge gate.
583
+ */
584
+ function classifyFile({ relPath, patchesDir, configDir, pristineDir, pristineHashes, skillsRedirect }) {
585
+ const backupPath = path.join(patchesDir, relPath);
586
+ const installedPath = resolveInstalledPath(configDir, relPath, skillsRedirect);
587
+ const result = { file: relPath, classification: CLASSIFICATION.UNKNOWN, reason: null, missing: [] };
588
+
589
+ if (!fs.existsSync(backupPath) || !fs.statSync(backupPath).isFile()) {
590
+ return result; // walked entry no longer exists — non-fatal
591
+ }
592
+
593
+ let installedStat;
594
+ try {
595
+ installedStat = fs.statSync(installedPath);
596
+ } catch {
597
+ result.reason = REASON.FAIL_INSTALLED_MISSING;
598
+ return result;
599
+ }
600
+ if (!installedStat.isFile()) {
601
+ result.reason = REASON.FAIL_INSTALLED_NOT_REGULAR_FILE;
602
+ return result;
603
+ }
604
+
605
+ let backupContent;
606
+ let installedContent;
607
+ try {
608
+ backupContent = fs.readFileSync(backupPath, 'utf8');
609
+ installedContent = fs.readFileSync(installedPath, 'utf8');
610
+ } catch {
611
+ result.reason = REASON.FAIL_READ_ERROR;
612
+ return result;
613
+ }
614
+
615
+ const { resolution, content: pristineContent } =
616
+ resolvePristineBaseline({ relPath, configDir, pristineDir, pristineHashes });
617
+
618
+ if (resolution === PRISTINE_RESOLUTION.DRIFTED) {
619
+ result.reason = REASON.OK_PRISTINE_DRIFT_DETECTED;
620
+ return result;
621
+ }
622
+ if (resolution === PRISTINE_RESOLUTION.ABSENT_RECORDED) {
623
+ result.reason = REASON.OK_NO_BASELINE;
624
+ return result;
625
+ }
626
+ if (resolution === PRISTINE_RESOLUTION.UNVALIDATED) {
627
+ result.reason = REASON.OK_UNVALIDATED_BASELINE;
628
+ return result;
629
+ }
630
+ if (resolution !== PRISTINE_RESOLUTION.VALIDATED) {
631
+ // OVERBROAD: no --pristine-dir (two-way fallback) or a non-file at the
632
+ // pristine path. Without a baseline there is no adoption evidence.
633
+ return result;
634
+ }
635
+
636
+ const userAdded = computeUserAddedLines(backupContent, pristineContent);
637
+ if (userAdded.length === 0) {
638
+ result.reason = REASON.OK_NO_USER_LINES_VS_PRISTINE;
639
+ return result;
640
+ }
641
+
642
+ for (const line of userAdded) {
643
+ if (!installedContent.includes(line)) {
644
+ result.missing.push(line.trim());
645
+ }
646
+ }
647
+ if (result.missing.length === 0) {
648
+ result.classification = CLASSIFICATION.INCORPORATED;
649
+ } else {
650
+ result.classification = CLASSIFICATION.NEEDS_MERGE;
651
+ }
652
+ return result;
653
+ }
654
+
386
655
  function main() {
387
656
  const opts = parseArgs(process.argv.slice(2));
388
657
  if (!opts.patchesDir || !opts.configDir) {
@@ -402,6 +671,55 @@ function main() {
402
671
  // #4086: skills-root fallback for runtimes whose skills kind declares a
403
672
  // `home` override outside configDir (codex global → $HOME/.agents/skills).
404
673
  const skillsRedirect = resolveSkillsRedirect(opts.configDir);
674
+
675
+ // Bug #4136: --classify is the pre-merge mode the workflow runs in Step 4
676
+ // to decide which files NOT to merge. It is informational — always exit 0 —
677
+ // because the binding enforcement stays with the post-merge gate below;
678
+ // a needs_merge or unknown outcome is direction for the merge step, not a
679
+ // failure, and halting here would block the very merge that resolves it.
680
+ if (opts.classify) {
681
+ const classifyResults = files.map((relPath) =>
682
+ classifyFile({
683
+ relPath,
684
+ patchesDir: opts.patchesDir,
685
+ configDir: opts.configDir,
686
+ pristineDir: opts.pristineDir,
687
+ pristineHashes,
688
+ skillsRedirect,
689
+ }),
690
+ );
691
+ const incorporatedResults = classifyResults.filter(
692
+ (r) => r.classification === CLASSIFICATION.INCORPORATED,
693
+ );
694
+ const incorporated = incorporatedResults.length;
695
+ const incorporated_files = incorporatedResults.map((r) => r.file);
696
+
697
+ if (opts.json) {
698
+ process.stdout.write(
699
+ JSON.stringify({ checked: classifyResults.length, incorporated, incorporated_files, results: classifyResults }, null, 2) + '\n',
700
+ );
701
+ } else {
702
+ process.stdout.write(`# Reapply Patch Classification (#4136)\n\n`);
703
+ process.stdout.write(`Checked: ${classifyResults.length} file(s)\n`);
704
+ process.stdout.write(`Incorporated: ${incorporated} file(s)\n`);
705
+ for (const f of incorporated_files) {
706
+ process.stdout.write(` incorporated: ${f}\n`);
707
+ }
708
+ for (const r of classifyResults) {
709
+ if (r.classification === CLASSIFICATION.NEEDS_MERGE) {
710
+ process.stdout.write(` needs merge: ${r.file}\n`);
711
+ for (const line of r.missing.slice(0, 5)) {
712
+ process.stdout.write(` missing: ${line}\n`);
713
+ }
714
+ if (r.missing.length > 5) {
715
+ process.stdout.write(` …and ${r.missing.length - 5} more line(s)\n`);
716
+ }
717
+ }
718
+ }
719
+ }
720
+ return 0;
721
+ }
722
+
405
723
  const results = files.map((relPath) =>
406
724
  verifyFile({
407
725
  relPath,
@@ -433,14 +751,52 @@ function main() {
433
751
  const no_baseline = noBaselineResults.length;
434
752
  const no_baseline_files = noBaselineResults.map((r) => r.file);
435
753
 
754
+ // Bug #4135: baseline coverage is a first-class aggregate. The collapse
755
+ // this reports is silent by design in every other surface (no_baseline is
756
+ // advisory, exit stays 0), which is exactly why it must be counted here:
757
+ // "A clean verifier exit reads as 'hunks survived'. On this run it meant
758
+ // '12 of 13 files were not checked at all.'"
759
+ const baseline_covered = classifyBaselineCoverage(results, Boolean(opts.pristineDir));
760
+
761
+ // Bug #4135: the opt-in strict coverage gate. Threshold semantics are >=
762
+ // (a run exactly at the threshold passes); an empty run is vacuously
763
+ // covered (nothing checked cannot be under-covered). A content failure
764
+ // (exit 1) outranks a coverage failure — it is the louder, more specific
765
+ // signal.
766
+ let coverageGateFailed = false;
767
+ if (opts.minBaselineCoverage !== null) {
768
+ const ratio = results.length === 0 ? 1 : baseline_covered / results.length;
769
+ coverageGateFailed = ratio < opts.minBaselineCoverage;
770
+ }
771
+
436
772
  if (opts.json) {
437
773
  process.stdout.write(
438
- JSON.stringify({ checked: results.length, failures: failures.length, drifted, drifted_files, no_baseline, no_baseline_files, results }, null, 2) + '\n',
774
+ JSON.stringify({ checked: results.length, failures: failures.length, drifted, drifted_files, no_baseline, no_baseline_files, baseline_covered, results }, null, 2) + '\n',
439
775
  );
440
776
  } else {
441
777
  process.stdout.write(`# Hunk Verification Gate (#2969)\n\n`);
442
778
  process.stdout.write(`Checked: ${results.length} file(s)\n`);
443
- process.stdout.write(`Failures: ${failures.length}\n\n`);
779
+ process.stdout.write(`Failures: ${failures.length}\n`);
780
+ // #4135: the coverage headline is printed on EVERY run, before any
781
+ // per-file detail, so a near-zero-coverage green run can never render
782
+ // identically to a fully-verified one.
783
+ process.stdout.write(`${coverageHeadline(baseline_covered, results.length)}\n\n`);
784
+ if (baseline_covered < results.length) {
785
+ if (!opts.pristineDir) {
786
+ process.stdout.write(`No --pristine-dir: over-broad fallback — nothing was verified against a pristine baseline (#2998).\n\n`);
787
+ } else {
788
+ const skipped = results.filter((r) => r.reason !== null && !BASELINE_VERIFIED_REASONS.has(r.reason));
789
+ process.stdout.write(`## Files not verified against a pristine baseline\n\n`);
790
+ process.stdout.write(`Advisory (non-blocking): their user customizations may or may not have survived the merge.\n\n`);
791
+ for (const r of skipped) {
792
+ process.stdout.write(`- ${r.file} (${r.reason})\n`);
793
+ }
794
+ process.stdout.write('\n');
795
+ }
796
+ }
797
+ if (coverageGateFailed) {
798
+ process.stdout.write(`COVERAGE GATE FAILED: baseline coverage ${(opts.minBaselineCoverage * 100).toFixed(1)}% required, ${results.length === 0 ? 100 : ((baseline_covered / results.length) * 100).toFixed(1)}% achieved (#4135 strict mode).\n\n`);
799
+ }
444
800
  if (failures.length > 0) {
445
801
  process.stdout.write(`## Files with missing user-added content\n\n`);
446
802
  for (const r of failures) {
@@ -456,11 +812,14 @@ function main() {
456
812
  }
457
813
  }
458
814
 
459
- return failures.length > 0 ? 1 : 0;
815
+ if (failures.length > 0) return 1;
816
+ if (coverageGateFailed) return 3;
817
+ return 0;
460
818
  }
461
819
 
462
820
  if (require.main === module) {
463
821
  runMain(main);
464
822
  }
465
823
 
466
- module.exports = { computeUserAddedLines, isSignificantLine, verifyFile, walk, REASON, readPristineHashes, sha256, resolveInstalledPath, resolveSkillsRedirect };
824
+ module.exports = { computeUserAddedLines, isSignificantLine, verifyFile, classifyFile, walk, REASON, CLASSIFICATION, PRISTINE_RESOLUTION, resolvePristineBaseline, readPristineHashes, sha256, resolveInstalledPath, resolveSkillsRedirect, coverageHeadline, classifyBaselineCoverage };
825
+