@opengsd/gsd-core 1.12.0 → 1.14.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 (455) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +12 -0
  4. package/agents/gsd-advisor-researcher.compact.md +85 -0
  5. package/agents/gsd-ai-researcher.compact.md +96 -0
  6. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  7. package/agents/gsd-code-fixer.compact.md +458 -0
  8. package/agents/gsd-code-fixer.md +5 -5
  9. package/agents/gsd-code-reviewer.compact.md +269 -0
  10. package/agents/gsd-code-reviewer.md +15 -3
  11. package/agents/gsd-codebase-mapper.compact.md +760 -0
  12. package/agents/gsd-debug-session-manager.compact.md +345 -0
  13. package/agents/gsd-doc-classifier.compact.md +192 -0
  14. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  15. package/agents/gsd-doc-verifier.compact.md +143 -0
  16. package/agents/gsd-doc-writer.compact.md +440 -0
  17. package/agents/gsd-dom-verifier.compact.md +138 -0
  18. package/agents/gsd-domain-researcher.compact.md +141 -0
  19. package/agents/gsd-eval-auditor.compact.md +160 -0
  20. package/agents/gsd-eval-planner.compact.md +137 -0
  21. package/agents/gsd-executor.md +63 -35
  22. package/agents/gsd-framework-selector.compact.md +82 -0
  23. package/agents/gsd-integration-checker.compact.md +245 -0
  24. package/agents/gsd-intel-updater.compact.md +226 -0
  25. package/agents/gsd-mempalace-curator.compact.md +45 -0
  26. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  27. package/agents/gsd-pattern-mapper.compact.md +275 -0
  28. package/agents/gsd-plan-checker.md +76 -57
  29. package/agents/gsd-planner.md +14 -0
  30. package/agents/gsd-project-researcher.compact.md +587 -0
  31. package/agents/gsd-research-synthesizer.compact.md +212 -0
  32. package/agents/gsd-roadmapper.compact.md +454 -0
  33. package/agents/gsd-roadmapper.md +13 -0
  34. package/agents/gsd-security-auditor.compact.md +162 -0
  35. package/agents/gsd-ui-auditor.compact.md +404 -0
  36. package/agents/gsd-ui-checker.compact.md +277 -0
  37. package/agents/gsd-ui-checker.md +19 -3
  38. package/agents/gsd-ui-researcher.compact.md +282 -0
  39. package/agents/gsd-ui-researcher.md +29 -0
  40. package/agents/gsd-user-profiler.compact.md +108 -0
  41. package/agents/gsd-verifier.md +23 -1
  42. package/bin/install.js +444 -134
  43. package/commands/gsd/cleanup.md +1 -0
  44. package/commands/gsd/code-review.md +2 -1
  45. package/commands/gsd/complete-milestone.md +1 -0
  46. package/commands/gsd/config.md +1 -0
  47. package/commands/gsd/debug.md +1 -0
  48. package/commands/gsd/execute-phase.md +1 -1
  49. package/commands/gsd/graphify.md +1 -0
  50. package/commands/gsd/health.md +1 -0
  51. package/commands/gsd/mempalace-capture.md +1 -0
  52. package/commands/gsd/mempalace-recall.md +1 -0
  53. package/commands/gsd/new-milestone.md +1 -0
  54. package/commands/gsd/new-project.md +1 -0
  55. package/commands/gsd/next.md +1 -0
  56. package/commands/gsd/ns-workflow.md +2 -1
  57. package/commands/gsd/pause-work.md +1 -0
  58. package/commands/gsd/phase.md +2 -1
  59. package/commands/gsd/pr-branch.md +1 -0
  60. package/commands/gsd/quick-batch.md +105 -0
  61. package/commands/gsd/resume-work.md +1 -0
  62. package/commands/gsd/review-backlog.md +1 -0
  63. package/commands/gsd/settings.md +2 -1
  64. package/commands/gsd/stats.md +1 -0
  65. package/commands/gsd/surface.md +18 -8
  66. package/commands/gsd/thread.md +1 -0
  67. package/commands/gsd/workspace.md +1 -0
  68. package/commands/gsd/workstreams.md +1 -0
  69. package/gsd-core/bin/check-latest-version.cjs +8 -3
  70. package/gsd-core/bin/gsd-tools.cjs +532 -174
  71. package/gsd-core/bin/lib/adr-parser.cjs +1 -1
  72. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  73. package/gsd-core/bin/lib/audit.cjs +39 -22
  74. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  75. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  76. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  77. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  78. package/gsd-core/bin/lib/capability-registry.cjs +528 -116
  79. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  80. package/gsd-core/bin/lib/capability-state.cjs +7 -1
  81. package/gsd-core/bin/lib/capability-validator.cjs +134 -5
  82. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  83. package/gsd-core/bin/lib/check-command-router.cjs +198 -38
  84. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  85. package/gsd-core/bin/lib/clusters.cjs +1 -0
  86. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  87. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  88. package/gsd-core/bin/lib/commands.cjs +981 -79
  89. package/gsd-core/bin/lib/config-loader.cjs +4 -0
  90. package/gsd-core/bin/lib/config.cjs +153 -38
  91. package/gsd-core/bin/lib/core-utils.cjs +34 -7
  92. package/gsd-core/bin/lib/coverage.cjs +1 -1
  93. package/gsd-core/bin/lib/decisions.cjs +343 -28
  94. package/gsd-core/bin/lib/edge-probe.cjs +14 -1
  95. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  96. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  97. package/gsd-core/bin/lib/frontmatter.cjs +137 -23
  98. package/gsd-core/bin/lib/gap-checker.cjs +22 -13
  99. package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
  100. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  101. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  102. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
  103. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +87 -23
  104. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  105. package/gsd-core/bin/lib/host-integration.cjs +57 -5
  106. package/gsd-core/bin/lib/init-command-router.cjs +14 -0
  107. package/gsd-core/bin/lib/init.cjs +539 -60
  108. package/gsd-core/bin/lib/install-engine.cjs +199 -14
  109. package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
  110. package/gsd-core/bin/lib/install-profiles.cjs +36 -14
  111. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  112. package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
  113. package/gsd-core/bin/lib/io.cjs +35 -0
  114. package/gsd-core/bin/lib/loop-resolver.cjs +64 -39
  115. package/gsd-core/bin/lib/markdown-table.cjs +123 -0
  116. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  117. package/gsd-core/bin/lib/milestone.cjs +41 -10
  118. package/gsd-core/bin/lib/model-resolver.cjs +101 -10
  119. package/gsd-core/bin/lib/phase-command-router.cjs +20 -7
  120. package/gsd-core/bin/lib/phase-id.cjs +412 -31
  121. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  122. package/gsd-core/bin/lib/phase.cjs +941 -98
  123. package/gsd-core/bin/lib/plan-document.cjs +10 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +206 -30
  126. package/gsd-core/bin/lib/planning-workspace.cjs +153 -29
  127. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  128. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  129. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  130. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  131. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  132. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  133. package/gsd-core/bin/lib/research-store.cjs +11 -12
  134. package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
  135. package/gsd-core/bin/lib/review-lane-invocation.cjs +96 -1
  136. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  137. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  138. package/gsd-core/bin/lib/roadmap-parser.cjs +555 -41
  139. package/gsd-core/bin/lib/roadmap.cjs +292 -69
  140. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +260 -43
  141. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +28 -20
  142. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +294 -108
  143. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -47
  144. package/gsd-core/bin/lib/security.cjs +126 -7
  145. package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
  146. package/gsd-core/bin/lib/smart-entry.cjs +7 -9
  147. package/gsd-core/bin/lib/state-document.cjs +159 -32
  148. package/gsd-core/bin/lib/state-md-schema.cjs +44 -27
  149. package/gsd-core/bin/lib/state-transition.cjs +465 -62
  150. package/gsd-core/bin/lib/state.cjs +906 -151
  151. package/gsd-core/bin/lib/surface.cjs +83 -10
  152. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  153. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  154. package/gsd-core/bin/lib/uat.cjs +1420 -516
  155. package/gsd-core/bin/lib/update-context.cjs +36 -26
  156. package/gsd-core/bin/lib/validate.cjs +230 -12
  157. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  158. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  159. package/gsd-core/bin/lib/verification.cjs +316 -23
  160. package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
  161. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  162. package/gsd-core/bin/lib/verify.cjs +531 -36
  163. package/gsd-core/bin/lib/workstream-inventory.cjs +21 -2
  164. package/gsd-core/bin/lib/worktree-safety.cjs +21 -7
  165. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  166. package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
  167. package/gsd-core/bin/verify-reapply-patches.cjs +507 -81
  168. package/gsd-core/references/agent-contracts.md +3 -3
  169. package/gsd-core/references/compact-content-gate.md +66 -0
  170. package/gsd-core/references/edge-probe.md +17 -13
  171. package/gsd-core/references/execute-mvp-tdd.md +18 -16
  172. package/gsd-core/references/execute-phase-response-language.md +6 -0
  173. package/gsd-core/references/executor-examples.md +42 -0
  174. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  175. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  176. package/gsd-core/references/model-profiles.md +12 -3
  177. package/gsd-core/references/mvp-concepts.md +2 -2
  178. package/gsd-core/references/plan-checker-examples.md +41 -0
  179. package/gsd-core/references/planner-antipatterns.md +25 -0
  180. package/gsd-core/references/planner-chunked.md +5 -1
  181. package/gsd-core/references/planner-coupling.md +42 -0
  182. package/gsd-core/references/planner-quick-batch.md +71 -0
  183. package/gsd-core/references/planner-reviews.md +47 -0
  184. package/gsd-core/references/planner-revision.md +75 -2
  185. package/gsd-core/references/planning-config.md +5 -1
  186. package/gsd-core/references/response-language-directive.md +9 -0
  187. package/gsd-core/references/revision-loop.md +118 -11
  188. package/gsd-core/references/tdd.md +17 -9
  189. package/gsd-core/references/thinking-models-planning.md +18 -2
  190. package/gsd-core/references/verification-patterns.md +17 -4
  191. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  192. package/gsd-core/references/worktree-path-safety.md +112 -2
  193. package/gsd-core/templates/README.md +7 -1
  194. package/gsd-core/templates/phase-prompt.md +4 -0
  195. package/gsd-core/templates/state.md +6 -3
  196. package/gsd-core/templates/summary.compact.md +212 -0
  197. package/gsd-core/templates/user-setup.compact.md +199 -0
  198. package/gsd-core/templates/user-setup.md +0 -9
  199. package/gsd-core/templates/verification-report.md +5 -0
  200. package/gsd-core/workflows/add-backlog.md +2 -0
  201. package/gsd-core/workflows/add-phase.md +2 -0
  202. package/gsd-core/workflows/add-tests.md +1 -1
  203. package/gsd-core/workflows/add-todo.md +4 -3
  204. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  205. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  206. package/gsd-core/workflows/audit-fix.md +2 -0
  207. package/gsd-core/workflows/audit-milestone.md +2 -0
  208. package/gsd-core/workflows/audit-uat.md +2 -0
  209. package/gsd-core/workflows/autonomous.md +15 -10
  210. package/gsd-core/workflows/check-todos.md +5 -3
  211. package/gsd-core/workflows/cleanup.md +4 -2
  212. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +22 -13
  213. package/gsd-core/workflows/code-review-fix.md +5 -3
  214. package/gsd-core/workflows/code-review.md +211 -43
  215. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  216. package/gsd-core/workflows/complete-milestone.md +40 -254
  217. package/gsd-core/workflows/debug.md +1 -1
  218. package/gsd-core/workflows/diagnose-issues.md +5 -1
  219. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
  220. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  221. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  222. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  223. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  224. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
  225. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  226. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  227. package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
  228. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  229. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  230. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  231. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  232. package/gsd-core/workflows/discuss-phase.md +1 -1
  233. package/gsd-core/workflows/do.md +43 -13
  234. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  235. package/gsd-core/workflows/docs-update.md +15 -156
  236. package/gsd-core/workflows/edit-phase.md +2 -0
  237. package/gsd-core/workflows/eval-review.md +1 -1
  238. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  239. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +20 -3
  240. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  241. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +24 -3
  242. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  243. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
  245. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  246. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  247. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  248. package/gsd-core/workflows/execute-phase.md +78 -159
  249. package/gsd-core/workflows/execute-plan.md +28 -15
  250. package/gsd-core/workflows/explore.md +2 -0
  251. package/gsd-core/workflows/extract-learnings.md +2 -0
  252. package/gsd-core/workflows/fast.md +6 -0
  253. package/gsd-core/workflows/forensics.md +2 -0
  254. package/gsd-core/workflows/graduation.md +1 -1
  255. package/gsd-core/workflows/health.md +1 -1
  256. package/gsd-core/workflows/help/modes/brief.md +2 -0
  257. package/gsd-core/workflows/help/modes/default.md +2 -0
  258. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  259. package/gsd-core/workflows/help/modes/full.md +12 -0
  260. package/gsd-core/workflows/help/modes/topic.md +2 -0
  261. package/gsd-core/workflows/help.md +3 -1
  262. package/gsd-core/workflows/import.md +3 -3
  263. package/gsd-core/workflows/inbox.md +1 -1
  264. package/gsd-core/workflows/ingest-docs.md +1 -1
  265. package/gsd-core/workflows/insert-phase.md +2 -0
  266. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  267. package/gsd-core/workflows/list-seeds.md +2 -0
  268. package/gsd-core/workflows/list-workspaces.md +2 -0
  269. package/gsd-core/workflows/manager.md +3 -3
  270. package/gsd-core/workflows/map-codebase.md +52 -3
  271. package/gsd-core/workflows/milestone-summary.md +2 -0
  272. package/gsd-core/workflows/mvp-phase.md +1 -1
  273. package/gsd-core/workflows/new-milestone.md +55 -13
  274. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  275. package/gsd-core/workflows/new-project.md +37 -205
  276. package/gsd-core/workflows/new-workspace.md +1 -1
  277. package/gsd-core/workflows/next.md +2 -0
  278. package/gsd-core/workflows/node-repair.md +2 -0
  279. package/gsd-core/workflows/note.md +2 -0
  280. package/gsd-core/workflows/onboard.md +1 -1
  281. package/gsd-core/workflows/pause-work.md +19 -4
  282. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  283. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  284. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
  285. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
  286. package/gsd-core/workflows/plan-phase.md +144 -185
  287. package/gsd-core/workflows/plan-review-convergence.md +102 -10
  288. package/gsd-core/workflows/plant-seed.md +1 -1
  289. package/gsd-core/workflows/pr-branch.md +30 -10
  290. package/gsd-core/workflows/profile-user.md +1 -1
  291. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  292. package/gsd-core/workflows/progress.md +25 -3
  293. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
  294. package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
  295. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  296. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  297. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  298. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  299. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  300. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  301. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  302. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  303. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  304. package/gsd-core/workflows/quick-batch.md +203 -0
  305. package/gsd-core/workflows/quick.md +21 -4
  306. package/gsd-core/workflows/reapply-patches.md +79 -3
  307. package/gsd-core/workflows/remove-phase.md +2 -0
  308. package/gsd-core/workflows/remove-workspace.md +1 -1
  309. package/gsd-core/workflows/resume-project.md +6 -2
  310. package/gsd-core/workflows/review.md +215 -10
  311. package/gsd-core/workflows/scan.md +2 -0
  312. package/gsd-core/workflows/section-manifest.json +12 -0
  313. package/gsd-core/workflows/secure-phase.md +1 -1
  314. package/gsd-core/workflows/session-report.md +2 -0
  315. package/gsd-core/workflows/settings-advanced.md +2 -0
  316. package/gsd-core/workflows/settings-integrations.md +9 -8
  317. package/gsd-core/workflows/settings.md +19 -6
  318. package/gsd-core/workflows/ship.md +10 -10
  319. package/gsd-core/workflows/sketch-wrap-up.md +2 -0
  320. package/gsd-core/workflows/sketch.md +1 -1
  321. package/gsd-core/workflows/smart-entry.md +1 -1
  322. package/gsd-core/workflows/spec-phase.md +24 -19
  323. package/gsd-core/workflows/spike-wrap-up.md +2 -0
  324. package/gsd-core/workflows/spike.md +1 -1
  325. package/gsd-core/workflows/stats.md +2 -0
  326. package/gsd-core/workflows/sync-skills.md +12 -4
  327. package/gsd-core/workflows/thread.md +2 -0
  328. package/gsd-core/workflows/transition.md +2 -0
  329. package/gsd-core/workflows/ui-phase.md +26 -5
  330. package/gsd-core/workflows/ui-review.md +1 -1
  331. package/gsd-core/workflows/ultraplan-phase.md +2 -0
  332. package/gsd-core/workflows/undo.md +1 -1
  333. package/gsd-core/workflows/update.md +48 -43
  334. package/gsd-core/workflows/validate-phase.md +1 -1
  335. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  336. package/gsd-core/workflows/verify-work.md +68 -182
  337. package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
  338. package/hooks/dist/gsd-check-update-worker.js +19 -2
  339. package/hooks/dist/gsd-context-monitor.js +371 -27
  340. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  341. package/hooks/dist/gsd-node-runner.sh +1 -0
  342. package/hooks/dist/gsd-prompt-guard.js +30 -5
  343. package/hooks/dist/gsd-read-guard.js +2 -0
  344. package/hooks/dist/gsd-read-injection-scanner.js +5 -5
  345. package/hooks/dist/gsd-secret-read-guard.js +1105 -0
  346. package/hooks/dist/gsd-statusline.js +18 -10
  347. package/hooks/dist/gsd-validate-commit.sh +474 -7
  348. package/hooks/dist/gsd-workflow-guard.js +2 -1
  349. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  350. package/hooks/dist/gsd-write-guard.js +46 -1
  351. package/hooks/dist/lib/dispatch-identity.js +187 -0
  352. package/hooks/dist/lib/filename-classification.js +64 -0
  353. package/hooks/dist/lib/git-cmd.js +210 -1
  354. package/hooks/dist/lib/injection-patterns.js +36 -6
  355. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  356. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  357. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  358. package/hooks/gsd-agent-isolation-guard.js +42 -16
  359. package/hooks/gsd-check-update-worker.js +19 -2
  360. package/hooks/gsd-context-monitor.js +371 -27
  361. package/hooks/gsd-cursor-subagent-start.js +34 -14
  362. package/hooks/gsd-node-runner.sh +1 -0
  363. package/hooks/gsd-prompt-guard.js +30 -5
  364. package/hooks/gsd-read-guard.js +2 -0
  365. package/hooks/gsd-read-injection-scanner.js +5 -5
  366. package/hooks/gsd-secret-read-guard.js +1105 -0
  367. package/hooks/gsd-statusline.js +18 -10
  368. package/hooks/gsd-validate-commit.sh +474 -7
  369. package/hooks/gsd-workflow-guard.js +2 -1
  370. package/hooks/gsd-worktree-path-guard.js +25 -14
  371. package/hooks/gsd-write-guard.js +46 -1
  372. package/hooks/hooks.json +6 -0
  373. package/hooks/lib/dispatch-identity.js +187 -0
  374. package/hooks/lib/filename-classification.js +64 -0
  375. package/hooks/lib/git-cmd.js +210 -1
  376. package/hooks/lib/injection-patterns.js +36 -6
  377. package/hooks/lib/isolation-deny-reason.js +53 -1
  378. package/hooks/lib/isolation-sentinel.js +58 -19
  379. package/hooks/managed-hooks-registry.cjs +1 -0
  380. package/package.json +13 -9
  381. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  382. package/scripts/benchmark-compact-content.cjs +368 -0
  383. package/scripts/build-hooks.js +11 -4
  384. package/scripts/check-contract-drift.cjs +4 -1
  385. package/scripts/check-env.cjs +36 -8
  386. package/scripts/check-glossary-refs.cjs +25 -21
  387. package/scripts/ci-next-health.cjs +271 -0
  388. package/scripts/ci-prepare-test-scope.cjs +7 -7
  389. package/scripts/ci-test-scope.cjs +133 -20
  390. package/scripts/ci-timeout-report.cjs +1 -1
  391. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  392. package/scripts/docs-guard-registry.cjs +17 -2
  393. package/scripts/gen-adr-index.cjs +8 -2
  394. package/scripts/gen-inventory-manifest.cjs +12 -0
  395. package/scripts/gen-loop-host-contract.cjs +67 -15
  396. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  397. package/scripts/lib/drift-scan.cjs +1 -1
  398. package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
  399. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  400. package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
  401. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  402. package/scripts/lib/suite-detection.cjs +32 -0
  403. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  404. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  405. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  406. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  407. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +24 -2
  408. package/scripts/lint-phase-enumeration-drift.cjs +24 -6
  409. package/scripts/lint-phase-id-drift.cjs +465 -15
  410. package/scripts/lint-portable-grep.cjs +176 -0
  411. package/scripts/lint-response-language-coverage.cjs +530 -0
  412. package/scripts/lint-source-test-name-collision.cjs +1 -1
  413. package/scripts/lint-test-file-count.allowlist.json +4 -1
  414. package/scripts/lint-vendored-deps.cjs +128 -17
  415. package/scripts/lint-workflow-shellcheck-baseline.json +1112 -0
  416. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  417. package/scripts/npm-audit-baseline.cjs +376 -0
  418. package/scripts/prompt-injection-scan.sh +22 -0
  419. package/scripts/require-issue-link-policy.cjs +16 -1
  420. package/scripts/workflow-size.cjs +139 -0
  421. package/skills/gsd-cleanup/SKILL.md +1 -0
  422. package/skills/gsd-code-review/SKILL.md +2 -1
  423. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  424. package/skills/gsd-config/SKILL.md +1 -0
  425. package/skills/gsd-debug/SKILL.md +1 -0
  426. package/skills/gsd-execute-phase/SKILL.md +1 -1
  427. package/skills/gsd-graphify/SKILL.md +1 -0
  428. package/skills/gsd-health/SKILL.md +1 -0
  429. package/skills/gsd-mempalace-capture/SKILL.md +1 -0
  430. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  431. package/skills/gsd-new-milestone/SKILL.md +1 -0
  432. package/skills/gsd-new-project/SKILL.md +1 -0
  433. package/skills/gsd-next/SKILL.md +1 -0
  434. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  435. package/skills/gsd-pause-work/SKILL.md +1 -0
  436. package/skills/gsd-phase/SKILL.md +2 -1
  437. package/skills/gsd-pr-branch/SKILL.md +1 -0
  438. package/skills/gsd-quick-batch/SKILL.md +105 -0
  439. package/skills/gsd-resume-work/SKILL.md +1 -0
  440. package/skills/gsd-review-backlog/SKILL.md +1 -0
  441. package/skills/gsd-settings/SKILL.md +2 -1
  442. package/skills/gsd-stats/SKILL.md +1 -0
  443. package/skills/gsd-surface/SKILL.md +18 -8
  444. package/skills/gsd-thread/SKILL.md +1 -0
  445. package/skills/gsd-workspace/SKILL.md +1 -0
  446. package/skills/gsd-workstreams/SKILL.md +1 -0
  447. package/vscode/package.json +1 -1
  448. package/gsd-core/templates/claude-md.md +0 -145
  449. package/gsd-core/templates/codebase/concerns.md +0 -310
  450. package/gsd-core/templates/codebase/conventions.md +0 -307
  451. package/gsd-core/templates/codebase/integrations.md +0 -280
  452. package/gsd-core/templates/codebase/structure.md +0 -285
  453. package/gsd-core/templates/codebase/testing.md +0 -480
  454. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  455. package/gsd-core/templates/discovery.md +0 -146
@@ -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,15 +216,262 @@ 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
 
187
- function verifyFile({ relPath, patchesDir, configDir, pristineDir, pristineHashes }) {
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
+
368
+ /**
369
+ * #4086: resolve where a backed-up file's INSTALLED counterpart lives.
370
+ * Primary is the config-dir-relative join (the manifest key's native form).
371
+ * For skills/ keys of a runtime whose skills kind declares a `home` override
372
+ * (codex global → $HOME/.agents/skills), the file lives OUTSIDE configDir, so
373
+ * when the primary path is absent we fall back to the runtime's ACTUAL skills
374
+ * root — derived from the same `resolveRuntimeArtifactLayout` seam the
375
+ * installer writes through. Returns the primary path unchanged whenever the
376
+ * fallback is impossible (no manifest, no runtime, no override, no file).
377
+ */
378
+ function resolveInstalledPath(configDir, relPath, skillsRedirect) {
379
+ const primary = path.join(configDir, relPath);
380
+ if (!skillsRedirect) return primary;
381
+ const hashKey = relPath.replace(/\\/g, '/');
382
+ if (!hashKey.startsWith(skillsRedirect.prefix)) return primary;
383
+ if (fs.existsSync(primary)) return primary;
384
+ const alt = path.join(skillsRedirect.root, hashKey.slice(skillsRedirect.prefix.length));
385
+ return fs.existsSync(alt) ? alt : primary;
386
+ }
387
+
388
+ /**
389
+ * #4086: build the skills-root fallback descriptor for a config dir from its
390
+ * own gsd-file-manifest.json (runtime + scope) and the runtime-artifact
391
+ * layout seam. Returns null whenever any step is unavailable — the verifier
392
+ * then behaves exactly as before (config-dir-relative only). Lazy + guarded
393
+ * require: runtime-artifact-layout.cjs is a built lib; a layout-unaware
394
+ * invocation must never crash the gate.
395
+ */
396
+ function resolveSkillsRedirect(configDir) {
397
+ try {
398
+ const manifest = JSON.parse(
399
+ fs.readFileSync(path.join(configDir, 'gsd-file-manifest.json'), 'utf8'),
400
+ );
401
+ if (!manifest || typeof manifest.runtime !== 'string' || !manifest.runtime) return null;
402
+ const scope = manifest.scope === 'local' ? 'local' : 'global';
403
+ const { resolveRuntimeArtifactLayout } = require('./lib/runtime-artifact-layout.cjs');
404
+ const layout = resolveRuntimeArtifactLayout(manifest.runtime, configDir, scope);
405
+ const kind = layout.kinds.find((k) => k.kind === 'skills');
406
+ if (!kind) return null;
407
+ const root = path.resolve(path.join(kind.home || configDir, kind.destSubpath));
408
+ const resolvedConfig = path.resolve(configDir);
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
410
+ // Same descriptor-driven manifest prefix writeManifest uses for skills
411
+ // keys (hermes nests under 'skills/gsd/', everyone else 'skills/'), read
412
+ // from the same shipped registry the installer's _resolveHostBehaviors
413
+ // consults — never a re-derived literal (generative-fix-divergence guard).
414
+ let prefix = 'skills/';
415
+ try {
416
+ const { runtimes: registryRuntimes } = require('./lib/capability-registry.cjs');
417
+ const declared = registryRuntimes
418
+ && registryRuntimes[manifest.runtime]
419
+ && registryRuntimes[manifest.runtime].runtime
420
+ && registryRuntimes[manifest.runtime].runtime.hostBehaviors;
421
+ if (declared && typeof declared.skillsManifestPrefix === 'string') {
422
+ prefix = declared.skillsManifestPrefix;
423
+ }
424
+ } catch { /* keep default prefix */ }
425
+ return { root, prefix };
426
+ } catch {
427
+ return null;
428
+ }
429
+ }
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
+
472
+ function verifyFile({ relPath, patchesDir, configDir, pristineDir, pristineHashes, skillsRedirect }) {
188
473
  const backupPath = path.join(patchesDir, relPath);
189
- const installedPath = path.join(configDir, relPath);
474
+ const installedPath = resolveInstalledPath(configDir, relPath, skillsRedirect);
190
475
  const result = { file: relPath, status: 'ok', missing: [], reason: null };
191
476
 
192
477
  if (!fs.existsSync(backupPath) || !fs.statSync(backupPath).isFile()) {
@@ -224,80 +509,32 @@ function verifyFile({ relPath, patchesDir, configDir, pristineDir, pristineHashe
224
509
  // Normalize to forward slashes so the key lookup matches on Windows
225
510
  // where path.join produces backslash-separated relPath values but
226
511
  // backup-meta.json stores keys written with forward slashes.
227
- const hashKey = relPath.replace(/\\/g, '/');
228
- 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 });
229
517
 
230
- let pristineContent = null;
231
- if (pristineDir) {
232
- const pristinePath = path.join(pristineDir, relPath);
233
- // Bug #934: track whether the pristine path EXISTS on disk (stat did not
234
- // throw ENOENT). A regular file that fails to read, or a non-file path
235
- // (e.g. a directory accidentally placed at the pristine path), is treated
236
- // as "present but unusable" — we fall to over-broad mode (safe side).
237
- // OK_NO_BASELINE is reserved for the strictly absent case: stat throws,
238
- // meaning the file was never written (the gap the bug describes).
239
- let pristinePathExists = false;
240
- try {
241
- const stat = fs.statSync(pristinePath);
242
- pristinePathExists = true; // path exists (any type)
243
- if (stat.isFile()) {
244
- const candidate = fs.readFileSync(pristinePath, 'utf8');
245
- // Bug #3657: if backup-meta.json recorded a pristine_hash for this
246
- // file, validate that the on-disk pristine matches it. A mismatch
247
- // means the installer refreshed gsd-pristine/ to a newer GSD version
248
- // after the backup was captured. Using the wrong-version pristine as
249
- // the diff baseline inverts the delta: upstream removals appear as
250
- // "user-added lines that must survive", causing FAIL_USER_LINES_MISSING
251
- // false positives. When the hash is stale, skip the pristine and fall
252
- // through to over-broad mode (every significant backup line is checked).
253
- // Over-broad mode never false-fails for a different reason because all
254
- // backup lines that are genuinely user-added will still be present in a
255
- // correctly merged install.
256
- if (recordedHash) {
257
- if (sha256(candidate) === recordedHash) {
258
- // Hash matches: the on-disk pristine is the correct baseline.
259
- pristineContent = candidate;
260
- } else {
261
- // Hash mismatch: the on-disk gsd-pristine/ was refreshed to a newer
262
- // GSD version after the backup was captured. Using it as the diff
263
- // baseline would invert the delta and produce false FAIL_USER_LINES_MISSING
264
- // reports (Bug #3657). Report the file as ok with a diagnostic code
265
- // so the gate does not false-fail; a re-anchor or git-aware baseline
266
- // step is required to verify this file correctly.
267
- result.reason = REASON.OK_PRISTINE_DRIFT_DETECTED;
268
- return result;
269
- }
270
- } else {
271
- // No recorded hash for this file (older installer or absent
272
- // backup-meta) — use the on-disk pristine as-is (pre-fix behaviour).
273
- pristineContent = candidate;
274
- }
275
- }
276
- // Non-file at pristinePath (e.g. a directory): stat succeeded so
277
- // pristinePathExists is true; we fall through to over-broad mode below,
278
- // which is safe and conservative.
279
- } catch {
280
- // Pristine stat threw — path is absent (ENOENT) or inaccessible.
281
- // pristinePathExists stays false.
282
- }
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
+ }
283
525
 
284
- // Bug #934: recordedHash is present (modern installer) but the pristine
285
- // path does not exist on disk at all (stat threw above). This means
286
- // saveLocalPatches recorded a hash but could not write the corresponding
287
- // gsd-pristine/ file (the only candidate was discarded because it was from
288
- // a newer release). Falling to over-broad mode here would treat every
289
- // upstream-changed line as a "user-added line that must survive", producing
290
- // false FAIL_USER_LINES_MISSING for each upstream removal. Since we
291
- // cannot reason correctly without a baseline, the safe answer is advisory/
292
- // non-blocking: return OK_NO_BASELINE and let the caller decide.
293
- // NOTE: this guard fires ONLY when stat threw (path absent), not when the
294
- // path is present but non-file — in that case over-broad mode is safer.
295
- if (!pristinePathExists && recordedHash) {
296
- result.reason = REASON.OK_NO_BASELINE;
297
- return result;
298
- }
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;
299
532
  }
300
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
+
301
538
  const userAdded = computeUserAddedLines(backupContent, pristineContent);
302
539
  if (userAdded.length === 0) {
303
540
  // Backup and pristine match exactly (or no significant content) — nothing
@@ -320,6 +557,101 @@ function verifyFile({ relPath, patchesDir, configDir, pristineDir, pristineHashe
320
557
  return result;
321
558
  }
322
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
+
323
655
  function main() {
324
656
  const opts = parseArgs(process.argv.slice(2));
325
657
  if (!opts.patchesDir || !opts.configDir) {
@@ -336,6 +668,58 @@ function main() {
336
668
  // Bug #3657: read pristine_hashes from backup-meta.json once and share
337
669
  // across all per-file verifications so each can detect drift independently.
338
670
  const pristineHashes = readPristineHashes(opts.patchesDir);
671
+ // #4086: skills-root fallback for runtimes whose skills kind declares a
672
+ // `home` override outside configDir (codex global → $HOME/.agents/skills).
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
+
339
723
  const results = files.map((relPath) =>
340
724
  verifyFile({
341
725
  relPath,
@@ -343,6 +727,7 @@ function main() {
343
727
  configDir: opts.configDir,
344
728
  pristineDir: opts.pristineDir,
345
729
  pristineHashes,
730
+ skillsRedirect,
346
731
  }),
347
732
  );
348
733
 
@@ -366,14 +751,52 @@ function main() {
366
751
  const no_baseline = noBaselineResults.length;
367
752
  const no_baseline_files = noBaselineResults.map((r) => r.file);
368
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
+
369
772
  if (opts.json) {
370
773
  process.stdout.write(
371
- 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',
372
775
  );
373
776
  } else {
374
777
  process.stdout.write(`# Hunk Verification Gate (#2969)\n\n`);
375
778
  process.stdout.write(`Checked: ${results.length} file(s)\n`);
376
- 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
+ }
377
800
  if (failures.length > 0) {
378
801
  process.stdout.write(`## Files with missing user-added content\n\n`);
379
802
  for (const r of failures) {
@@ -389,11 +812,14 @@ function main() {
389
812
  }
390
813
  }
391
814
 
392
- return failures.length > 0 ? 1 : 0;
815
+ if (failures.length > 0) return 1;
816
+ if (coverageGateFailed) return 3;
817
+ return 0;
393
818
  }
394
819
 
395
820
  if (require.main === module) {
396
821
  runMain(main);
397
822
  }
398
823
 
399
- module.exports = { computeUserAddedLines, isSignificantLine, verifyFile, walk, REASON, readPristineHashes, sha256 };
824
+ module.exports = { computeUserAddedLines, isSignificantLine, verifyFile, classifyFile, walk, REASON, CLASSIFICATION, PRISTINE_RESOLUTION, resolvePristineBaseline, readPristineHashes, sha256, resolveInstalledPath, resolveSkillsRedirect, coverageHeadline, classifyBaselineCoverage };
825
+