@opengsd/gsd-core 1.11.0 → 1.12.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 (395) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.md +1 -1
  4. package/agents/gsd-debug-session-manager.md +1 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-dom-verifier.md +169 -0
  7. package/agents/gsd-eval-auditor.md +1 -1
  8. package/agents/gsd-executor.md +17 -9
  9. package/agents/gsd-framework-selector.md +1 -3
  10. package/agents/gsd-intel-updater.md +1 -1
  11. package/agents/gsd-mempalace-curator.md +0 -1
  12. package/agents/gsd-pattern-mapper.md +11 -0
  13. package/agents/gsd-phase-researcher.md +3 -1
  14. package/agents/gsd-plan-checker.md +15 -55
  15. package/agents/gsd-planner.md +6 -4
  16. package/agents/gsd-project-researcher.md +1 -1
  17. package/agents/gsd-research-synthesizer.md +2 -2
  18. package/agents/gsd-roadmapper.md +15 -11
  19. package/agents/gsd-ui-checker.md +63 -4
  20. package/agents/gsd-ui-researcher.md +41 -3
  21. package/agents/gsd-verifier.md +1 -1
  22. package/bin/install.js +609 -134
  23. package/commands/gsd/discuss-phase.md +1 -1
  24. package/commands/gsd/import.md +1 -1
  25. package/commands/gsd/quick.md +8 -4
  26. package/gsd-core/bin/gsd-tools.cjs +567 -51
  27. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  28. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  29. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  30. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  31. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  32. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  33. package/gsd-core/bin/lib/audit.cjs +163 -41
  34. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  35. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  36. package/gsd-core/bin/lib/capability-registry.cjs +336 -95
  37. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  38. package/gsd-core/bin/lib/capability-validator.cjs +205 -18
  39. package/gsd-core/bin/lib/check-command-router.cjs +145 -5
  40. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  41. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  42. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  43. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  44. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  45. package/gsd-core/bin/lib/commands.cjs +543 -44
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  47. package/gsd-core/bin/lib/config-loader.cjs +118 -29
  48. package/gsd-core/bin/lib/config.cjs +92 -2
  49. package/gsd-core/bin/lib/configuration.cjs +129 -37
  50. package/gsd-core/bin/lib/core-utils.cjs +84 -7
  51. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  52. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  53. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  54. package/gsd-core/bin/lib/frontmatter.cjs +840 -305
  55. package/gsd-core/bin/lib/gap-checker.cjs +27 -3
  56. package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
  57. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  58. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
  59. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  60. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  61. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  62. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  63. package/gsd-core/bin/lib/init.cjs +120 -41
  64. package/gsd-core/bin/lib/install-engine.cjs +68 -3
  65. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
  66. package/gsd-core/bin/lib/install-profiles.cjs +78 -4
  67. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  68. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  69. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  70. package/gsd-core/bin/lib/intel.cjs +101 -26
  71. package/gsd-core/bin/lib/io.cjs +160 -15
  72. package/gsd-core/bin/lib/learnings.cjs +85 -14
  73. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  74. package/gsd-core/bin/lib/markdown-table.cjs +52 -4
  75. package/gsd-core/bin/lib/milestone.cjs +90 -5
  76. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  77. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  78. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  79. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  80. package/gsd-core/bin/lib/phase-id.cjs +70 -4
  81. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  82. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  83. package/gsd-core/bin/lib/phase.cjs +405 -84
  84. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  85. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  86. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  87. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  88. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
  89. package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
  90. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  91. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  92. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  93. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  94. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  95. package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
  96. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  97. package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
  98. package/gsd-core/bin/lib/roadmap.cjs +74 -19
  99. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
  100. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
  101. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
  102. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  103. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  104. package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
  105. package/gsd-core/bin/lib/smart-entry.cjs +12 -22
  106. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  107. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  108. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  109. package/gsd-core/bin/lib/state-document.cjs +186 -0
  110. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  111. package/gsd-core/bin/lib/state-transition.cjs +517 -101
  112. package/gsd-core/bin/lib/state.cjs +946 -163
  113. package/gsd-core/bin/lib/surface.cjs +10 -2
  114. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  115. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  116. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  117. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  118. package/gsd-core/bin/lib/uat.cjs +1376 -125
  119. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  120. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  121. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  122. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  123. package/gsd-core/bin/lib/vendor/README.md +43 -5
  124. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  125. package/gsd-core/bin/lib/verification.cjs +14 -1
  126. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  127. package/gsd-core/bin/lib/verify.cjs +95 -40
  128. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  129. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  130. package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
  131. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  132. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  133. package/gsd-core/bin/shared/exit-codes.json +8 -0
  134. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  135. package/gsd-core/bin/shared/model-catalog.json +8 -1
  136. package/gsd-core/references/agent-contracts.md +3 -2
  137. package/gsd-core/references/api-coverage.md +24 -2
  138. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  139. package/gsd-core/references/checkpoints.md +37 -19
  140. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  141. package/gsd-core/references/edge-probe.md +8 -0
  142. package/gsd-core/references/execute-mvp-tdd.md +1 -3
  143. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  144. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  145. package/gsd-core/references/failing-direction.md +78 -0
  146. package/gsd-core/references/gate-prompts.md +1 -1
  147. package/gsd-core/references/git-integration.md +5 -5
  148. package/gsd-core/references/git-planning-commit.md +3 -3
  149. package/gsd-core/references/gsd-run-resolver.md +1 -1
  150. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  151. package/gsd-core/references/model-profiles.md +1 -1
  152. package/gsd-core/references/nyquist-compliance.md +74 -0
  153. package/gsd-core/references/offer-next.md +3 -5
  154. package/gsd-core/references/phase-argument-parsing.md +3 -3
  155. package/gsd-core/references/planner-failing-direction.md +53 -0
  156. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  157. package/gsd-core/references/planner-revision.md +1 -1
  158. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  159. package/gsd-core/references/planning-config.md +37 -8
  160. package/gsd-core/references/reviewer-instances.md +31 -0
  161. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  162. package/gsd-core/references/tdd.md +1 -3
  163. package/gsd-core/references/ui-brand.md +65 -21
  164. package/gsd-core/references/ui-consideration-probe.md +1 -1
  165. package/gsd-core/references/universal-anti-patterns.md +2 -2
  166. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  167. package/gsd-core/references/verify-mvp-mode.md +1 -1
  168. package/gsd-core/references/workstream-flag.md +11 -11
  169. package/gsd-core/templates/README.md +1 -1
  170. package/gsd-core/templates/SECURITY.md +3 -3
  171. package/gsd-core/templates/UI-SPEC.md +25 -3
  172. package/gsd-core/templates/VALIDATION.md +3 -3
  173. package/gsd-core/templates/phase-prompt.md +3 -0
  174. package/gsd-core/templates/state.md +7 -0
  175. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  176. package/gsd-core/workflows/add-backlog.md +1 -1
  177. package/gsd-core/workflows/add-phase.md +3 -3
  178. package/gsd-core/workflows/add-tests.md +3 -8
  179. package/gsd-core/workflows/add-todo.md +1 -1
  180. package/gsd-core/workflows/ai-integration-phase.md +4 -9
  181. package/gsd-core/workflows/audit-fix.md +12 -3
  182. package/gsd-core/workflows/audit-milestone.md +9 -9
  183. package/gsd-core/workflows/audit-uat.md +17 -2
  184. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  185. package/gsd-core/workflows/autonomous.md +10 -26
  186. package/gsd-core/workflows/check-todos.md +1 -1
  187. package/gsd-core/workflows/cleanup.md +2 -2
  188. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
  189. package/gsd-core/workflows/code-review-fix.md +1 -1
  190. package/gsd-core/workflows/code-review.md +121 -40
  191. package/gsd-core/workflows/complete-milestone.md +15 -10
  192. package/gsd-core/workflows/debug.md +5 -3
  193. package/gsd-core/workflows/diagnose-issues.md +12 -6
  194. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  195. package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
  196. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  197. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  198. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
  199. package/gsd-core/workflows/discuss-phase.md +1 -1
  200. package/gsd-core/workflows/do.md +3 -6
  201. package/gsd-core/workflows/docs-update.md +5 -4
  202. package/gsd-core/workflows/edit-phase.md +1 -1
  203. package/gsd-core/workflows/eval-review.md +4 -9
  204. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  205. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
  206. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  207. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  208. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  209. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +22 -4
  210. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  211. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  212. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  213. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  214. package/gsd-core/workflows/execute-phase.md +38 -54
  215. package/gsd-core/workflows/execute-plan.md +17 -12
  216. package/gsd-core/workflows/explore.md +1 -1
  217. package/gsd-core/workflows/extract-learnings.md +1 -1
  218. package/gsd-core/workflows/fast.md +2 -2
  219. package/gsd-core/workflows/forensics.md +1 -1
  220. package/gsd-core/workflows/graduation.md +5 -5
  221. package/gsd-core/workflows/health.md +3 -6
  222. package/gsd-core/workflows/import.md +14 -11
  223. package/gsd-core/workflows/inbox.md +4 -5
  224. package/gsd-core/workflows/ingest-docs.md +44 -11
  225. package/gsd-core/workflows/insert-phase.md +5 -5
  226. package/gsd-core/workflows/list-seeds.md +5 -3
  227. package/gsd-core/workflows/list-workspaces.md +1 -1
  228. package/gsd-core/workflows/manager.md +12 -23
  229. package/gsd-core/workflows/map-codebase.md +1 -1
  230. package/gsd-core/workflows/milestone-summary.md +1 -1
  231. package/gsd-core/workflows/mvp-phase.md +2 -2
  232. package/gsd-core/workflows/new-milestone.md +9 -21
  233. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  234. package/gsd-core/workflows/new-project.md +12 -26
  235. package/gsd-core/workflows/new-workspace.md +1 -1
  236. package/gsd-core/workflows/next.md +2 -2
  237. package/gsd-core/workflows/pause-work.md +1 -1
  238. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  239. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  240. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  241. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  242. package/gsd-core/workflows/plan-phase.md +121 -42
  243. package/gsd-core/workflows/plan-review-convergence.md +46 -9
  244. package/gsd-core/workflows/plant-seed.md +2 -2
  245. package/gsd-core/workflows/pr-branch.md +187 -51
  246. package/gsd-core/workflows/profile-user.md +16 -14
  247. package/gsd-core/workflows/progress.md +27 -12
  248. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  249. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
  250. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  251. package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
  252. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  253. package/gsd-core/workflows/quick.md +20 -29
  254. package/gsd-core/workflows/remove-phase.md +4 -4
  255. package/gsd-core/workflows/remove-workspace.md +2 -2
  256. package/gsd-core/workflows/resume-project.md +8 -12
  257. package/gsd-core/workflows/review.md +193 -15
  258. package/gsd-core/workflows/scan.md +1 -1
  259. package/gsd-core/workflows/secure-phase.md +2 -2
  260. package/gsd-core/workflows/settings-advanced.md +7 -9
  261. package/gsd-core/workflows/settings-integrations.md +64 -31
  262. package/gsd-core/workflows/settings.md +3 -5
  263. package/gsd-core/workflows/ship.md +12 -6
  264. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  265. package/gsd-core/workflows/sketch.md +12 -18
  266. package/gsd-core/workflows/smart-entry.md +3 -5
  267. package/gsd-core/workflows/spec-phase.md +23 -1
  268. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  269. package/gsd-core/workflows/spike.md +20 -31
  270. package/gsd-core/workflows/stats.md +2 -2
  271. package/gsd-core/workflows/sync-skills.md +1 -1
  272. package/gsd-core/workflows/thread.md +11 -7
  273. package/gsd-core/workflows/transition.md +5 -5
  274. package/gsd-core/workflows/ui-phase.md +10 -16
  275. package/gsd-core/workflows/ui-review.md +6 -10
  276. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  277. package/gsd-core/workflows/undo.md +8 -16
  278. package/gsd-core/workflows/update.md +6 -10
  279. package/gsd-core/workflows/validate-phase.md +2 -2
  280. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  281. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  282. package/gsd-core/workflows/verify-work.md +57 -18
  283. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  284. package/hooks/dist/gsd-config-reload.js +18 -12
  285. package/hooks/dist/gsd-context-monitor.js +19 -10
  286. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  287. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  288. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  289. package/hooks/dist/gsd-cursor-stop.js +2 -1
  290. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  291. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  292. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  293. package/hooks/dist/gsd-graphify-update.sh +22 -18
  294. package/hooks/dist/gsd-node-runner.sh +76 -0
  295. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  296. package/hooks/dist/gsd-prompt-guard.js +16 -7
  297. package/hooks/dist/gsd-read-guard.js +16 -7
  298. package/hooks/dist/gsd-read-injection-scanner.js +17 -8
  299. package/hooks/dist/gsd-session-state.sh +1 -0
  300. package/hooks/dist/gsd-statusline.js +215 -26
  301. package/hooks/dist/gsd-validate-commit.sh +80 -6
  302. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  303. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  304. package/hooks/dist/gsd-workflow-guard.js +34 -16
  305. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  306. package/hooks/dist/gsd-write-guard.js +35 -25
  307. package/hooks/dist/lib/cli-exit.js +560 -0
  308. package/hooks/dist/lib/exit-code-registry.js +98 -0
  309. package/hooks/dist/lib/git-probe.js +84 -0
  310. package/hooks/dist/lib/hook-exit.js +81 -0
  311. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  312. package/hooks/gsd-agent-isolation-guard.js +77 -38
  313. package/hooks/gsd-config-reload.js +18 -12
  314. package/hooks/gsd-context-monitor.js +19 -10
  315. package/hooks/gsd-cursor-post-tool.js +3 -1
  316. package/hooks/gsd-cursor-pre-tool.js +3 -1
  317. package/hooks/gsd-cursor-session-start.js +2 -1
  318. package/hooks/gsd-cursor-stop.js +2 -1
  319. package/hooks/gsd-cursor-subagent-start.js +28 -23
  320. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  321. package/hooks/gsd-ensure-canonical-path.js +2 -1
  322. package/hooks/gsd-graphify-update.sh +22 -18
  323. package/hooks/gsd-node-runner.sh +76 -0
  324. package/hooks/gsd-phase-boundary.sh +1 -0
  325. package/hooks/gsd-prompt-guard.js +16 -7
  326. package/hooks/gsd-read-guard.js +16 -7
  327. package/hooks/gsd-read-injection-scanner.js +17 -8
  328. package/hooks/gsd-session-state.sh +1 -0
  329. package/hooks/gsd-statusline.js +215 -26
  330. package/hooks/gsd-validate-commit.sh +80 -6
  331. package/hooks/gsd-windsurf-pre-command.js +16 -11
  332. package/hooks/gsd-windsurf-pre-write.js +22 -13
  333. package/hooks/gsd-workflow-guard.js +34 -16
  334. package/hooks/gsd-worktree-path-guard.js +36 -21
  335. package/hooks/gsd-write-guard.js +35 -25
  336. package/hooks/lib/cli-exit.js +560 -0
  337. package/hooks/lib/exit-code-registry.js +98 -0
  338. package/hooks/lib/git-probe.js +84 -0
  339. package/hooks/lib/hook-exit.js +81 -0
  340. package/hooks/managed-hooks-registry.cjs +3 -0
  341. package/package.json +12 -7
  342. package/scripts/base64-scan.sh +74 -12
  343. package/scripts/build-hooks.js +5 -0
  344. package/scripts/check-glossary-refs.cjs +77 -15
  345. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  346. package/scripts/ci-check-job-near-cap.cjs +49 -0
  347. package/scripts/ci-pr-mergeability.cjs +262 -0
  348. package/scripts/ci-test-scope.cjs +45 -12
  349. package/scripts/ci-timeout-report.cjs +230 -0
  350. package/scripts/docs-guard-registry.cjs +396 -0
  351. package/scripts/gen-capability-registry.cjs +8 -6
  352. package/scripts/gen-exit-code-docs.cjs +318 -0
  353. package/scripts/gen-exit-code-registry.cjs +891 -0
  354. package/scripts/gen-features.cjs +836 -0
  355. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  356. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  357. package/scripts/gen-loop-host-contract.cjs +134 -1
  358. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  359. package/scripts/gen-state-md-docs.cjs +727 -0
  360. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  361. package/scripts/lib/ci-job-timing.cjs +72 -0
  362. package/scripts/lib/cli-exit.cjs +546 -44
  363. package/scripts/lib/drift-scan.cjs +32 -2
  364. package/scripts/lib/exit-code-registry.cjs +98 -0
  365. package/scripts/lib/ndjson-reporter.cjs +119 -0
  366. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  367. package/scripts/lint-docs-guard-registration.cjs +495 -0
  368. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  369. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  370. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  371. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  372. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  373. package/scripts/lint-phase-enumeration-drift.cjs +21 -8
  374. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  375. package/scripts/lint-removed-but-needed.cjs +184 -16
  376. package/scripts/lint-seam-enforcement.cjs +182 -0
  377. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  378. package/scripts/lint-source-test-name-collision.cjs +241 -0
  379. package/scripts/lint-state-write-path-drift.cjs +337 -432
  380. package/scripts/lint-test-file-count.allowlist.json +122 -4
  381. package/scripts/lint-test-file-count.cjs +25 -3
  382. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  383. package/scripts/lint-vendored-deps.cjs +208 -35
  384. package/scripts/mutation-matrix.cjs +599 -50
  385. package/scripts/prompt-injection-scan.sh +75 -14
  386. package/scripts/secret-scan.sh +75 -13
  387. package/scripts/select-docs-guards.cjs +56 -0
  388. package/scripts/sync-runtime-launcher.cjs +22 -3
  389. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  390. package/skills/gsd-import/SKILL.md +1 -1
  391. package/skills/gsd-quick/SKILL.md +8 -4
  392. package/vscode/package.json +1 -1
  393. package/bin/lib/ui-safety-gate.cjs +0 -109
  394. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  395. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -23,6 +23,9 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
23
23
  // eslint-disable-next-line @typescript-eslint/no-require-imports
24
24
  const capabilityStateMod = require("./capability-state.cjs");
25
25
  const { isCapabilityActive } = capabilityStateMod;
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
27
+ const ioMod = require("./io.cjs");
28
+ const { formatDiagnosticToken } = ioMod;
26
29
  // ─── Constants ───────────────────────────────────────────────────────────────
27
30
  const INTEL_DIR = '.planning/intel';
28
31
  const INTEL_FILES = {
@@ -32,6 +35,16 @@ const INTEL_FILES = {
32
35
  arch: 'arch-decisions.json',
33
36
  stack: 'stack.json',
34
37
  };
38
+ /**
39
+ * ADR-3473 §8.5 / #3885: recursion bound for the intel JSON search walk.
40
+ * Restored from the retired SDK lineage (`sdk/src/query/intel.ts` at `11918dcc3^`),
41
+ * lost in the ADR-0174 consolidation. Unlike the original, hitting the ceiling is
42
+ * NOT reported as a silent "no match" — the walk that stops early sets a
43
+ * `truncated` flag threaded back up to `intelQuery`'s result (Decision 4: a
44
+ * routine that discards an input says so). The bound is on DEPTH only; breadth
45
+ * (sibling count at any given depth) is unaffected.
46
+ */
47
+ const MAX_JSON_SEARCH_DEPTH = 48;
35
48
  // ─── Internal helpers ────────────────────────────────────────────────────────
36
49
  /**
37
50
  * Ensure the intel directory exists under the given planning dir.
@@ -71,16 +84,42 @@ function intelFilePath(planningDir, filename) {
71
84
  }
72
85
  /**
73
86
  * Safely read and parse a JSON intel file.
74
- * Returns null if file doesn't exist or can't be parsed.
87
+ *
88
+ * Returns null for THREE distinct on-disk states, only one of which is a
89
+ * "quiet" case (#3885, ADR-3473 §8.5):
90
+ * - ABSENT (ENOENT, via platformReadSync returning null): silent — not
91
+ * every project has every intel file, and callers already loop over
92
+ * the full INTEL_FILES set expecting misses. Never pushed to `errors`.
93
+ * - UNREADABLE (EACCES/EIO/... — platformReadSync rethrows anything that
94
+ * isn't ENOENT): surfaced, naming the file, when `errors` is supplied.
95
+ * - MALFORMED (JSON.parse throws on a file that WAS read successfully):
96
+ * also surfaced, naming the file — a corrupt intel file used to read
97
+ * identically to "no matches", which is the same defect one layer down.
98
+ *
99
+ * `errors` is an optional accumulator so callers that want to thread the
100
+ * outcome into their own result shape can pass an array and read it back
101
+ * after the call; callers that omit it keep the prior fold-to-null shape.
75
102
  */
76
- function safeReadJson(filePath) {
103
+ function safeReadJson(filePath, errors) {
104
+ let raw;
105
+ try {
106
+ raw = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
107
+ }
108
+ catch (err) {
109
+ if (errors) {
110
+ errors.push(`Could not read ${formatDiagnosticToken(filePath)}: ${formatDiagnosticToken(err?.message ?? String(err))}`);
111
+ }
112
+ return null;
113
+ }
114
+ if (raw === null)
115
+ return null; // ENOENT — genuinely absent, not an error.
77
116
  try {
78
- const raw = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
79
- if (raw === null)
80
- return null;
81
117
  return JSON.parse(raw);
82
118
  }
83
- catch {
119
+ catch (err) {
120
+ if (errors) {
121
+ errors.push(`Could not parse ${formatDiagnosticToken(filePath)}: ${formatDiagnosticToken(err?.message ?? String(err))}`);
122
+ }
84
123
  return null;
85
124
  }
86
125
  }
@@ -101,16 +140,17 @@ function hashFile(filePath) {
101
140
  }
102
141
  /**
103
142
  * Search for a term (case-insensitive) in a JSON object's keys and string values.
104
- * Returns an array of matching entries.
143
+ * Returns matching entries plus whether the walk hit MAX_JSON_SEARCH_DEPTH.
105
144
  */
106
145
  function searchJsonEntries(data, term) {
107
146
  if (!data || typeof data !== 'object')
108
- return [];
147
+ return { matches: [], truncated: false };
109
148
  const entries = data.entries || data;
110
149
  if (!entries || typeof entries !== 'object')
111
- return [];
150
+ return { matches: [], truncated: false };
112
151
  const lowerTerm = term.toLowerCase();
113
152
  const matches = [];
153
+ const state = { truncated: false };
114
154
  for (const [key, value] of Object.entries(entries)) {
115
155
  if (key === '_meta')
116
156
  continue;
@@ -119,25 +159,40 @@ function searchJsonEntries(data, term) {
119
159
  matches.push({ key, value });
120
160
  continue;
121
161
  }
122
- // Check string value match (recursive for objects)
123
- if (matchesInValue(value, lowerTerm)) {
162
+ // Check string value match (recursive for objects/arrays, bounded by depth)
163
+ if (matchesInValue(value, lowerTerm, 0, state)) {
124
164
  matches.push({ key, value });
125
165
  }
126
166
  }
127
- return matches;
167
+ return { matches, truncated: state.truncated };
128
168
  }
129
169
  /**
130
170
  * Recursively check if a term appears in any string value.
171
+ *
172
+ * `depth` counts container unwraps already performed (starts at 0 for the
173
+ * entry's own value). Strings never fail the ceiling check themselves — only
174
+ * a container (object/array) refuses to recurse one level deeper once
175
+ * `depth > MAX_JSON_SEARCH_DEPTH`, at which point `state.truncated` is set so
176
+ * the caller can report "I stopped looking" rather than a bare "no match".
177
+ * The bound is on nesting depth only, never on sibling breadth.
131
178
  */
132
- function matchesInValue(value, lowerTerm) {
179
+ function matchesInValue(value, lowerTerm, depth, state) {
133
180
  if (typeof value === 'string') {
134
181
  return value.toLowerCase().includes(lowerTerm);
135
182
  }
136
183
  if (Array.isArray(value)) {
137
- return value.some(v => matchesInValue(v, lowerTerm));
184
+ if (depth > MAX_JSON_SEARCH_DEPTH) {
185
+ state.truncated = true;
186
+ return false;
187
+ }
188
+ return value.some(v => matchesInValue(v, lowerTerm, depth + 1, state));
138
189
  }
139
190
  if (value && typeof value === 'object') {
140
- return Object.values(value).some(v => matchesInValue(v, lowerTerm));
191
+ if (depth > MAX_JSON_SEARCH_DEPTH) {
192
+ state.truncated = true;
193
+ return false;
194
+ }
195
+ return Object.values(value).some(v => matchesInValue(v, lowerTerm, depth + 1, state));
141
196
  }
142
197
  return false;
143
198
  }
@@ -150,19 +205,23 @@ function intelQuery(term, planningDir) {
150
205
  return disabledResponse();
151
206
  const matches = [];
152
207
  let total = 0;
208
+ let truncated = false;
209
+ const readErrors = [];
153
210
  // Search all JSON intel files
154
211
  for (const [_key, filename] of Object.entries(INTEL_FILES)) {
155
212
  const filePath = intelFilePath(planningDir, filename);
156
- const data = safeReadJson(filePath);
213
+ const data = safeReadJson(filePath, readErrors);
157
214
  if (!data)
158
215
  continue;
159
- const found = searchJsonEntries(data, term);
216
+ const { matches: found, truncated: fileTruncated } = searchJsonEntries(data, term);
217
+ if (fileTruncated)
218
+ truncated = true;
160
219
  if (found.length > 0) {
161
220
  matches.push({ source: filename, entries: found });
162
221
  total += found.length;
163
222
  }
164
223
  }
165
- return { matches, term, total };
224
+ return { matches, term, total, truncated, read_errors: readErrors };
166
225
  }
167
226
  /**
168
227
  * Report status and staleness of each intel file.
@@ -179,13 +238,14 @@ function intelStatus(planningDir) {
179
238
  const filePath = intelFilePath(planningDir, filename);
180
239
  const exists = node_fs_1.default.existsSync(filePath);
181
240
  if (!exists) {
182
- files[filename] = { exists: false, updated_at: null, stale: true };
241
+ files[filename] = { exists: false, updated_at: null, stale: true, read_error: null };
183
242
  overallStale = true;
184
243
  continue;
185
244
  }
186
245
  let updatedAt = null;
187
246
  // All intel files are JSON — read _meta.updated_at
188
- const data = safeReadJson(filePath);
247
+ const readErrors = [];
248
+ const data = safeReadJson(filePath, readErrors);
189
249
  if (data && data._meta && data._meta.updated_at) {
190
250
  updatedAt = data._meta.updated_at;
191
251
  }
@@ -196,7 +256,7 @@ function intelStatus(planningDir) {
196
256
  }
197
257
  if (stale)
198
258
  overallStale = true;
199
- files[filename] = { exists: true, updated_at: updatedAt, stale };
259
+ files[filename] = { exists: true, updated_at: updatedAt, stale, read_error: readErrors[0] ?? null };
200
260
  }
201
261
  return { files, overall_stale: overallStale };
202
262
  }
@@ -207,9 +267,15 @@ function intelDiff(planningDir) {
207
267
  if (!isIntelCapabilityActive(planningDir))
208
268
  return disabledResponse();
209
269
  const snapshotPath = intelFilePath(planningDir, '.last-refresh.json');
210
- const snapshot = safeReadJson(snapshotPath);
270
+ const readErrors = [];
271
+ const snapshot = safeReadJson(snapshotPath, readErrors);
211
272
  if (!snapshot) {
212
- return { no_baseline: true };
273
+ // #3885 (ADR-3473 §8.5): `no_baseline: true` stays true for BOTH a
274
+ // genuinely-never-snapshotted project (ENOENT, read_error stays null)
275
+ // AND a present-but-unreadable/malformed snapshot — collapsing those
276
+ // two into a bare `no_baseline: true` would manufacture "you've never
277
+ // run a refresh" out of a read failure. `read_error` names which one.
278
+ return { no_baseline: true, read_error: readErrors[0] ?? null };
213
279
  }
214
280
  const prevHashes = snapshot.hashes || {};
215
281
  const changed = [];
@@ -374,7 +440,9 @@ function intelApiSurface(planningDir) {
374
440
  const intelPath = ensureIntelDir(planningDir);
375
441
  const apiMapPath = node_path_1.default.join(intelPath, INTEL_FILES.apis);
376
442
  const outputPath = node_path_1.default.join(intelPath, 'API-SURFACE.md');
377
- const data = safeReadJson(apiMapPath);
443
+ const readErrors = [];
444
+ const data = safeReadJson(apiMapPath, readErrors);
445
+ const readError = readErrors[0] ?? null;
378
446
  const entries = (data && data.entries && typeof data.entries === 'object')
379
447
  ? Object.entries(data.entries)
380
448
  : [];
@@ -392,7 +460,14 @@ function intelApiSurface(planningDir) {
392
460
  lines.push('> Generated from `.planning/intel/api-map.json`. Do not edit by hand.');
393
461
  lines.push('');
394
462
  if (symbolCount === 0) {
395
- lines.push('> **Incomplete:** api-map.json has no entries (intel extraction is regex/JS-only or not yet populated).');
463
+ if (readError) {
464
+ // #3885: a read/parse failure is NOT "not yet populated" — say so,
465
+ // rather than manufacturing the wrong reason for the empty surface.
466
+ lines.push(`> **Incomplete:** ${readError}`);
467
+ }
468
+ else {
469
+ lines.push('> **Incomplete:** api-map.json has no entries (intel extraction is regex/JS-only or not yet populated).');
470
+ }
396
471
  lines.push('> Treat absence here as "unknown", not "does not exist".');
397
472
  lines.push('');
398
473
  }
@@ -414,7 +489,7 @@ function intelApiSurface(planningDir) {
414
489
  }
415
490
  }
416
491
  (0, shell_command_projection_cjs_1.platformWriteSync)(outputPath, lines.join('\n'));
417
- return { written: outputPath, symbolCount, stale };
492
+ return { written: outputPath, symbolCount, stale, read_error: readError };
418
493
  }
419
494
  /**
420
495
  * Patch _meta.updated_at in a JSON intel file to the current timestamp.
@@ -15,6 +15,9 @@ const node_fs_1 = __importDefault(require("node:fs"));
15
15
  const node_os_1 = __importDefault(require("node:os"));
16
16
  const node_path_1 = __importDefault(require("node:path"));
17
17
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ const cliExitModule = require("./cli-exit.cjs");
20
+ const { setJsonErrorMode, getJsonErrorMode, EXIT_ENVELOPE_REASON, ExitError, setPendingOutcome, projectOutcome, getContractVersion, } = cliExitModule;
18
21
  // ─── Temp-file helpers (needed by output()) ──────────────────────────────────
19
22
  /**
20
23
  * Dedicated GSD temp directory: path.join(os.tmpdir(), 'gsd').
@@ -134,7 +137,48 @@ function writeAllSync(fd, data) {
134
137
  function serializeForOutput(result) {
135
138
  return JSON.stringify(result, null, 2);
136
139
  }
140
+ /**
141
+ * A payload-carried error, per ADR-2980's own definition (#3912, ADR-3889
142
+ * §4): `result` is an object carrying a SERIALIZABLE `error` property, in
143
+ * ANY key order — `{ found: false, error }` counts exactly the same as
144
+ * `{ error, found: false }`. The discriminator is a serializable error
145
+ * value, NOT mere key presence: `result.error`'s own truthiness is
146
+ * irrelevant (falsy `0`/`null`/`''` all count), and neither is `result`'s
147
+ * prototype (a plain object literal is all any call site here ever
148
+ * passes) — but `error: undefined` does NOT count, because
149
+ * `JSON.stringify` (the exact serializer `serializeForOutput` uses to
150
+ * build the payload the caller actually receives) drops an object
151
+ * property whose value is `undefined` entirely. A payload built as
152
+ * `{ found: false, error: undefined }` therefore reaches the wire as
153
+ * `{"found":false}` — no error at all — and recording DEGRADED for it
154
+ * would be a false verdict: exit 80 under v2 for output the user sees as
155
+ * clean. `hasOwnProperty` alone is not enough to answer "does this payload
156
+ * declare an error"; it must also survive `JSON.stringify`.
157
+ */
158
+ function isPayloadCarriedError(result) {
159
+ return typeof result === 'object' && result !== null
160
+ && Object.prototype.hasOwnProperty.call(result, 'error')
161
+ && result.error !== undefined;
162
+ }
137
163
  function output(result, raw, rawValue) {
164
+ // #3912 (ADR-3889 §4): a payload-carried error declares DEGRADED into the
165
+ // pending-outcome cell runMain reads. This is the ONLY new thing output()
166
+ // does — it still just writes fd 1 and returns; the exit code stays
167
+ // whatever it already was under v1 (DEGRADED projects to 0), and nothing
168
+ // here touches process.exitCode directly.
169
+ //
170
+ // LAST-WRITE-WINS (review fix): a clean (non-error-shaped) payload CLEARS
171
+ // the cell rather than leaving a prior degraded declaration in place. A
172
+ // handler that calls output() more than once per invocation — a
173
+ // diagnostic error payload followed by a clean final payload — must have
174
+ // its LATEST declaration win, not its first: the cell reflects "is a
175
+ // degraded outcome pending right now", not "was one ever declared".
176
+ if (isPayloadCarriedError(result)) {
177
+ setPendingOutcome('DEGRADED');
178
+ }
179
+ else {
180
+ setPendingOutcome(undefined);
181
+ }
138
182
  let data;
139
183
  if (raw && rawValue !== undefined) {
140
184
  // eslint-disable-next-line @typescript-eslint/no-base-to-string
@@ -178,7 +222,7 @@ const ERROR_REASON = Object.freeze({
178
222
  CONFIG_PARSE_FAILED: 'config_parse_failed',
179
223
  CONFIG_INVALID_KEY: 'config_invalid_key',
180
224
  // SDK / gsd-tools dispatch
181
- SDK_FAIL_FAST: 'sdk_fail_fast',
225
+ SDK_FAIL_FAST: EXIT_ENVELOPE_REASON,
182
226
  SDK_UNKNOWN_COMMAND: 'sdk_unknown_command',
183
227
  SDK_MISSING_ARG: 'sdk_missing_arg',
184
228
  // workflow / phase
@@ -195,6 +239,10 @@ const ERROR_REASON = Object.freeze({
195
239
  // graphify
196
240
  GRAPHIFY_NO_GRAPH: 'graphify_no_graph',
197
241
  GRAPHIFY_INVALID_QUERY: 'graphify_invalid_query',
242
+ // estimate-calibrate (#3882, ADR-3473 §8.2): the phases directory exists
243
+ // but could not be read — a NON-answer, distinct from a project that
244
+ // genuinely has zero completed phases yet.
245
+ ESTIMATE_PHASES_UNREADABLE: 'estimate_phases_unreadable',
198
246
  // hooks
199
247
  HOOKS_OPT_OUT: 'hooks_opt_out',
200
248
  // commit-docs-guard (#3588)
@@ -203,22 +251,17 @@ const ERROR_REASON = Object.freeze({
203
251
  COMMIT_DOCS_GUARD_HOOKS_PATH_SET: 'commit_docs_guard_hooks_path_set',
204
252
  // security-scan
205
253
  SECURITY_SCAN_FAILED: 'security_scan_failed',
254
+ // --pick (#3365 / #3358, ADR-3473 §8.4): an absent field or non-JSON
255
+ // command output is a failure, never a demotion to an empty answer at
256
+ // exit 0. See .gsd/phase/feat-3884-failure-is-a-value/40-design.md.
257
+ PICK_FIELD_ABSENT: 'pick_field_absent',
258
+ PICK_OUTPUT_NOT_JSON: 'pick_output_not_json',
206
259
  // generic
207
260
  USAGE: 'usage',
208
261
  UNKNOWN: 'unknown',
209
262
  });
210
- /**
211
- * Process-level flag: when true, error() emits structured JSON to stderr
212
- * instead of plain "Error: <message>" text. Set by gsd-tools.cjs when the
213
- * CLI is invoked with `--json-errors`. Tests opt in to typed-IR error
214
- * assertions by passing that flag and parsing the JSON.
215
- *
216
- * Default off so existing callers and human operators keep their plain-text
217
- * diagnostics. The structured form is opt-in for tooling and tests (#2974).
218
- */
219
- let _jsonErrorMode = false;
220
- function setJsonErrorMode(v) { _jsonErrorMode = !!v; }
221
- function getJsonErrorMode() { return _jsonErrorMode; }
263
+ // setJsonErrorMode / getJsonErrorMode now live in cli-exit.cts (imported above)
264
+ // and are re-exported here for the callers that already import them from io.
222
265
  /**
223
266
  * Emit an error and exit. When the second argument is provided it must be
224
267
  * a value from ERROR_REASON; tests can assert on `result.reason`. When the
@@ -233,15 +276,116 @@ function getJsonErrorMode() { return _jsonErrorMode; }
233
276
  * human-readable text. Ignored entirely in plain-text mode — the human
234
277
  * message is the only thing an operator sees there.
235
278
  */
279
+ /**
280
+ * Render an UNTRUSTED string for embedding inside a human-readable,
281
+ * plain-text diagnostic (the `'Error: ' + message` line `error()` writes in
282
+ * non-JSON mode).
283
+ *
284
+ * WHY THIS EXISTS AND WHY `error()` DOES NOT DO IT ITSELF: `error()`
285
+ * deliberately writes its `message` argument verbatim — several callers in
286
+ * this repo intentionally emit multi-line diagnostics (e.g. the phase-gate
287
+ * messages), and `error()` has no way to distinguish a legitimate multi-line
288
+ * message from a hostile one, so it must not mangle newlines generically.
289
+ * That means any UNTRUSTED substring a caller interpolates into `message`
290
+ * (an argv token, a JSON key/value read back from a command's own output,
291
+ * etc.) can smuggle its own `\n` and forge a second `Error: ` line on
292
+ * stderr — a caller that parses stderr line-by-line would then see a second,
293
+ * attacker-authored error. Every call site that interpolates untrusted data
294
+ * into a diagnostic MUST pass that substring through this function first;
295
+ * `error()` itself stays a dumb, faithful writer.
296
+ *
297
+ * `JSON.stringify` is the primitive: it wraps the value in quotes and
298
+ * escapes control characters (`\n`, `\r`, `\t`, and the rest of the C0
299
+ * range, plus the quote character itself), so the result can never span
300
+ * more than one line or introduce an unescaped `"`. Callers embedding the
301
+ * result MUST NOT add their own surrounding quotes — that would
302
+ * double-quote it.
303
+ */
304
+ function formatDiagnosticToken(value) {
305
+ return JSON.stringify(value);
306
+ }
307
+ /**
308
+ * Map an ERROR_REASON wire value onto a declared outcome name (#3912,
309
+ * ADR-3889 §4). Closed over the 25-member enum: every reason gets an
310
+ * explicit entry below, so a 26th member added without a mapping falls
311
+ * through to the `?? 'FAIL'` default rather than silently mis-projecting —
312
+ * and tests/A1 iterates `Object.values(ERROR_REASON)`, so that default is
313
+ * exactly what makes an unmapped addition visible instead of invisible.
314
+ *
315
+ * This function's result is ONLY consulted under v2 (see `error()` below) —
316
+ * it is deliberately never routed through `projectOutcome` under v1, which
317
+ * is what keeps the v1 pin intact (`projectOutcome` treats registered names
318
+ * as version-invariant, so e.g. USAGE would otherwise become 64 today).
319
+ *
320
+ * Each non-FAIL choice below is justified inline; `UNKNOWN` and anything
321
+ * with no clearly better fit stays `FAIL` — the honest default the design
322
+ * calls for, not a guess dressed up as a specific outcome.
323
+ */
324
+ const REASON_TO_OUTCOME = Object.freeze({
325
+ // Bad argv/subcommand/argument — the caller, not the run, is at fault.
326
+ [ERROR_REASON.CONFIG_INVALID_KEY]: 'USAGE',
327
+ [ERROR_REASON.SDK_UNKNOWN_COMMAND]: 'USAGE',
328
+ [ERROR_REASON.SDK_MISSING_ARG]: 'USAGE',
329
+ [ERROR_REASON.GRAPHIFY_INVALID_QUERY]: 'USAGE',
330
+ [ERROR_REASON.USAGE]: 'USAGE',
331
+ // A specific, named thing does not exist / nothing was there to find —
332
+ // genuine, known emptiness rather than a broken prerequisite.
333
+ [ERROR_REASON.CONFIG_KEY_NOT_FOUND]: 'NO_INPUT',
334
+ [ERROR_REASON.SUMMARY_NO_PLANNING]: 'NO_INPUT',
335
+ [ERROR_REASON.WORKSTREAM_MODE_NONE_ACTIVE]: 'NO_INPUT',
336
+ // A prerequisite is absent, unreadable, or otherwise not in a state the
337
+ // run could proceed from — distinct from NO_INPUT's genuine emptiness.
338
+ [ERROR_REASON.CONFIG_NO_FILE]: 'UNAVAILABLE',
339
+ [ERROR_REASON.CONFIG_PARSE_FAILED]: 'UNAVAILABLE',
340
+ [ERROR_REASON.PHASE_NOT_FOUND]: 'UNAVAILABLE',
341
+ [ERROR_REASON.PHASE_VERIFICATION_INCOMPLETE]: 'UNAVAILABLE',
342
+ [ERROR_REASON.PHASE_PLAN_COVERAGE_INCOMPLETE]: 'UNAVAILABLE',
343
+ // Its own docstring: "a marker exists but didn't resolve" — a broken
344
+ // prerequisite, not the "no marker anywhere" emptiness NONE_ACTIVE covers.
345
+ [ERROR_REASON.WORKSTREAM_MODE_MARKER_UNRESOLVED]: 'UNAVAILABLE',
346
+ [ERROR_REASON.GRAPHIFY_NO_GRAPH]: 'UNAVAILABLE',
347
+ // Its own docstring: "a NON-answer, distinct from a project that
348
+ // genuinely has zero completed phases yet" — UNAVAILABLE, not NO_INPUT.
349
+ [ERROR_REASON.ESTIMATE_PHASES_UNREADABLE]: 'UNAVAILABLE',
350
+ [ERROR_REASON.COMMIT_DOCS_GUARD_NOT_A_REPO]: 'UNAVAILABLE',
351
+ [ERROR_REASON.COMMIT_DOCS_GUARD_FOREIGN_HOOK]: 'UNAVAILABLE',
352
+ [ERROR_REASON.COMMIT_DOCS_GUARD_HOOKS_PATH_SET]: 'UNAVAILABLE',
353
+ // Its own docstring: "an absent field or non-JSON command output is a
354
+ // failure, never a demotion to an empty answer" — the field/output was
355
+ // supposed to be there and was not; a prerequisite of the query failed.
356
+ [ERROR_REASON.PICK_FIELD_ABSENT]: 'UNAVAILABLE',
357
+ [ERROR_REASON.PICK_OUTPUT_NOT_JSON]: 'UNAVAILABLE',
358
+ // Self-failure: the run itself broke, not its inputs.
359
+ [ERROR_REASON.SDK_FAIL_FAST]: 'INTERNAL',
360
+ [ERROR_REASON.SECURITY_SCAN_FAILED]: 'INTERNAL',
361
+ // No clearly better fit — the honest default, per design.
362
+ [ERROR_REASON.HOOKS_OPT_OUT]: 'FAIL',
363
+ [ERROR_REASON.UNKNOWN]: 'FAIL',
364
+ });
365
+ function outcomeForReason(reason) {
366
+ return REASON_TO_OUTCOME[reason] ?? 'FAIL';
367
+ }
236
368
  function error(message, reason = ERROR_REASON.UNKNOWN, extra) {
237
- if (_jsonErrorMode) {
369
+ if (getJsonErrorMode()) {
238
370
  const payload = JSON.stringify({ ok: false, reason, message, ...(extra || {}) }) + '\n';
239
371
  writeAllSync(2, payload);
240
372
  }
241
373
  else {
242
374
  writeAllSync(2, 'Error: ' + message + '\n');
243
375
  }
244
- process.exit(1);
376
+ // #3912 (ADR-3889 §4): the declaration is version-gated HERE, not inside
377
+ // projectOutcome — registered names are version-invariant there, so
378
+ // routing every reason through it unconditionally would change v1 exit
379
+ // codes today (e.g. USAGE -> 64) and break the pin. Under v1 the exit
380
+ // stays ExitError(1) unconditionally, byte-identical to every prior
381
+ // release; only v2 projects the declared outcome through the registry.
382
+ if (getContractVersion() === 'v2') {
383
+ throw new ExitError(projectOutcome(outcomeForReason(reason), 'v2'));
384
+ }
385
+ // No message passed to ExitError: the stderr write above is already done,
386
+ // byte-identical to the prior process.exit(1) behavior, and ExitError with
387
+ // no message means runMain's catch adds nothing further to stderr.
388
+ throw new ExitError(1);
245
389
  }
246
390
  module.exports = {
247
391
  GSD_TEMP_DIR,
@@ -253,4 +397,5 @@ module.exports = {
253
397
  setJsonErrorMode,
254
398
  getJsonErrorMode,
255
399
  error,
400
+ formatDiagnosticToken,
256
401
  };
@@ -21,6 +21,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
21
21
  const node_fs_1 = __importDefault(require("node:fs"));
22
22
  const node_path_1 = __importDefault(require("node:path"));
23
23
  const node_crypto_1 = __importDefault(require("node:crypto"));
24
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
25
+ const phaseLocator = require("./phase-locator.cjs");
24
26
  const node_os_1 = __importDefault(require("node:os"));
25
27
  // eslint-disable-next-line @typescript-eslint/no-require-imports
26
28
  const ioMod = require("./io.cjs");
@@ -145,9 +147,55 @@ function learningsDelete(id, opts) {
145
147
  node_fs_1.default.unlinkSync(filePath);
146
148
  return true;
147
149
  }
150
+ /**
151
+ * #3683 — resolve which learnings artifact a copy reads. The extractor writes
152
+ * phase-scoped `{PHASE_DIR}/{PADDED}-LEARNINGS.md`; the legacy shape is a
153
+ * project-root `LEARNINGS.md`. The MOST RECENT phase-scoped artifact wins (a
154
+ * gated completion copies the phase it just wrote); the project-root file is
155
+ * the fallback when no phase artifact exists. Returns null when neither shape
156
+ * is present.
157
+ */
158
+ function resolveLearningsSource(planningDir) {
159
+ let best = null;
160
+ const phasesDir = node_path_1.default.join(planningDir, 'phases');
161
+ // Phase enumeration routes through the sanctioned seam (the #3185 drift
162
+ // guard rejects ad-hoc re-derivations): no cwd → every phase dir in the
163
+ // tree, sentinel dirs excluded, unreadable phases dir already degraded to
164
+ // an empty list by the locator itself.
165
+ const { value: phaseDirNames } = phaseLocator.listMilestonePhaseDirs(phasesDir);
166
+ for (const entry of phaseDirNames) {
167
+ const phaseDir = node_path_1.default.join(phasesDir, entry);
168
+ let files;
169
+ try {
170
+ files = node_fs_1.default.readdirSync(phaseDir);
171
+ }
172
+ catch {
173
+ // An unreadable phase dir (ACL, removal race) must not crash the copy —
174
+ // skip it and keep scanning.
175
+ continue;
176
+ }
177
+ for (const f of files) {
178
+ if (!/-LEARNINGS\.md$/i.test(f))
179
+ continue;
180
+ const p = node_path_1.default.join(phaseDir, f);
181
+ try {
182
+ const m = node_fs_1.default.statSync(p).mtimeMs;
183
+ if (best === null || m > best.m)
184
+ best = { p, m };
185
+ }
186
+ catch {
187
+ continue;
188
+ }
189
+ }
190
+ }
191
+ if (best !== null)
192
+ return best.p;
193
+ const rootPath = node_path_1.default.join(planningDir, 'LEARNINGS.md');
194
+ return node_fs_1.default.existsSync(rootPath) ? rootPath : null;
195
+ }
148
196
  function learningsCopyFromProject(planningDir, opts) {
149
- const learningsPath = node_path_1.default.join(planningDir, 'LEARNINGS.md');
150
- if (!node_fs_1.default.existsSync(learningsPath)) {
197
+ const learningsPath = resolveLearningsSource(planningDir);
198
+ if (learningsPath === null) {
151
199
  return { total: 0, created: 0, skipped: 0 };
152
200
  }
153
201
  const content = node_fs_1.default.readFileSync(learningsPath, 'utf-8');
@@ -167,29 +215,52 @@ function learningsCopyFromProject(planningDir, opts) {
167
215
  dedupeIndex.set(existing.content_hash, existing.id);
168
216
  }
169
217
  }
170
- // Parse markdown: split on ## headings
218
+ // Parse markdown: split on ## category headings.
219
+ // #3683: the real producer (extract-learnings.md write_learnings) writes ##
220
+ // categories containing ### item headings — each item is ONE learning (the
221
+ // store's relevance contract caps injection by count, so category-sized
222
+ // blobs defeat it). A bare ## section with no ### items keeps the legacy
223
+ // single-learning shape.
171
224
  const sections = content.split(/^## /m).slice(1); // skip preamble before first ##
172
225
  let created = 0;
173
226
  let skipped = 0;
174
- for (const section of sections) {
175
- const lines = section.trim().split('\n');
176
- const title = lines[0].trim();
177
- const body = lines.slice(1).join('\n').trim();
178
- if (!body)
179
- continue;
180
- // Extract tags from title (simple: use words as tags)
181
- const tags = title.toLowerCase().split(/\s+/).filter(w => w.length > 2);
227
+ const writeOne = (title, body, extraTags) => {
228
+ const tags = Array.from(new Set([
229
+ ...extraTags,
230
+ ...title.toLowerCase().split(/\s+/).filter(w => w.length > 2),
231
+ ]));
182
232
  const result = learningsWrite({
183
233
  source_project: sourceProject,
184
234
  learning: body,
185
235
  context: title,
186
236
  tags,
187
237
  }, { ...opts, dedupeIndex });
188
- if (result.created) {
238
+ if (result.created)
189
239
  created++;
190
- }
191
- else {
240
+ else
192
241
  skipped++;
242
+ };
243
+ for (const section of sections) {
244
+ const lines = section.trim().split('\n');
245
+ const category = lines[0].trim();
246
+ const body = lines.slice(1).join('\n').trim();
247
+ if (!body)
248
+ continue;
249
+ const categoryTag = category.toLowerCase().split(/\s+/)[0] || '';
250
+ // Split into ### items when present; otherwise one learning per section.
251
+ const items = body.split(/^### /m).map(s => s.trim()).filter(s => s.length > 0);
252
+ const hasItemHeadings = /^### /m.test(body);
253
+ if (!hasItemHeadings) {
254
+ writeOne(category, body, categoryTag ? [categoryTag] : []);
255
+ continue;
256
+ }
257
+ for (const item of items) {
258
+ const itemLines = item.split('\n');
259
+ const title = itemLines[0].trim();
260
+ const itemBody = itemLines.slice(1).join('\n').trim();
261
+ if (!itemBody && !title)
262
+ continue;
263
+ writeOne(title, itemBody || title, categoryTag ? [categoryTag] : []);
193
264
  }
194
265
  }
195
266
  return { total: created + skipped, created, skipped };
@@ -262,8 +262,14 @@ function planLegacyCleanup(configDirs, opts = {}) {
262
262
  }
263
263
  }
264
264
 
265
- // Legacy shared cache (fixed name from the old package)
266
- const legacyCachePath = path.join(homeDir, '.cache', 'gsd', 'gsd-update-check.json');
265
+ // Legacy shared cache (fixed name from the old package). #3799: under an
266
+ // explicit configDirs override the cache lives under the SCOPE root(s), not
267
+ // the default home — the override means "clean only what this redirected
268
+ // install owns", and the default home's cache belongs to the live install.
269
+ const cacheRoot = (Array.isArray(opts.configDirs) && opts.configDirs.length > 0)
270
+ ? opts.configDirs[0]
271
+ : homeDir;
272
+ const legacyCachePath = path.join(cacheRoot, '.cache', 'gsd', 'gsd-update-check.json');
267
273
  try {
268
274
  const stat = fsMod.statSync(legacyCachePath);
269
275
  if (stat.isFile()) {