@opengsd/gsd-core 1.9.1 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (426) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debug-session-manager.md +11 -0
  6. package/agents/gsd-debugger.md +12 -246
  7. package/agents/gsd-doc-synthesizer.md +2 -4
  8. package/agents/gsd-executor.md +12 -10
  9. package/agents/gsd-integration-checker.md +3 -0
  10. package/agents/gsd-mempalace-curator.md +5 -2
  11. package/agents/gsd-phase-researcher.md +20 -1
  12. package/agents/gsd-plan-checker.md +46 -0
  13. package/agents/gsd-planner.md +49 -54
  14. package/agents/gsd-roadmapper.md +21 -3
  15. package/agents/gsd-user-profiler.md +3 -0
  16. package/agents/gsd-verifier.md +26 -73
  17. package/bin/install.js +1272 -1238
  18. package/bin/lib/ui-safety-gate.cjs +2 -0
  19. package/commands/gsd/code-review.md +1 -1
  20. package/commands/gsd/execute-phase.md +1 -1
  21. package/commands/gsd/map-codebase.md +1 -1
  22. package/commands/gsd/mempalace-capture.md +2 -2
  23. package/commands/gsd/mempalace-recall.md +1 -1
  24. package/commands/gsd/new-milestone.md +2 -2
  25. package/commands/gsd/plan-phase.md +1 -1
  26. package/commands/gsd/quick.md +1 -1
  27. package/commands/gsd/review-backlog.md +2 -1
  28. package/commands/gsd/verify-work.md +1 -1
  29. package/gsd-core/bin/gsd-tools.cjs +1009 -115
  30. package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
  31. package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
  32. package/gsd-core/bin/lib/api-coverage.cjs +123 -5
  33. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  35. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  36. package/gsd-core/bin/lib/audit.cjs +926 -202
  37. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  38. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  39. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  40. package/gsd-core/bin/lib/capability-registry.cjs +608 -148
  41. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  42. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  43. package/gsd-core/bin/lib/capability-validator.cjs +507 -24
  44. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  45. package/gsd-core/bin/lib/check-command-router.cjs +114 -38
  46. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  47. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  48. package/gsd-core/bin/lib/command-aliases.cjs +94 -0
  49. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  50. package/gsd-core/bin/lib/commands.cjs +665 -99
  51. package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
  52. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  53. package/gsd-core/bin/lib/config-loader.cjs +76 -0
  54. package/gsd-core/bin/lib/config.cjs +22 -2
  55. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  56. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  57. package/gsd-core/bin/lib/core-utils.cjs +217 -40
  58. package/gsd-core/bin/lib/decisions.cjs +23 -0
  59. package/gsd-core/bin/lib/docs.cjs +3 -2
  60. package/gsd-core/bin/lib/external-job.cjs +19 -4
  61. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  62. package/gsd-core/bin/lib/frontmatter.cjs +239 -32
  63. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  64. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  65. package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
  66. package/gsd-core/bin/lib/graphify.cjs +142 -27
  67. package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
  68. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  69. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  72. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  73. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  74. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  75. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  76. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  77. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  78. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  79. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  80. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  81. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  82. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  83. package/gsd-core/bin/lib/init.cjs +1325 -169
  84. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  85. package/gsd-core/bin/lib/install-engine.cjs +805 -264
  86. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  87. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  88. package/gsd-core/bin/lib/install-profiles.cjs +160 -57
  89. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  90. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  91. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  92. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  93. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  94. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  95. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  96. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  97. package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
  98. package/gsd-core/bin/lib/io.cjs +38 -3
  99. package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
  100. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  101. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  102. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  103. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  104. package/gsd-core/bin/lib/milestone.cjs +821 -109
  105. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  106. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  107. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  108. package/gsd-core/bin/lib/pattern.cjs +122 -0
  109. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  110. package/gsd-core/bin/lib/phase-id.cjs +507 -36
  111. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  112. package/gsd-core/bin/lib/phase-locator.cjs +258 -58
  113. package/gsd-core/bin/lib/phase.cjs +891 -156
  114. package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
  115. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  116. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  117. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  118. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  119. package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
  120. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  121. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  122. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  123. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  124. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
  125. package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
  126. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  127. package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
  128. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  129. package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
  130. package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
  131. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  132. package/gsd-core/bin/lib/roadmap.cjs +405 -84
  133. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
  134. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  135. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
  136. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  137. package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
  138. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
  139. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  140. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  141. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  142. package/gsd-core/bin/lib/security.cjs +104 -5
  143. package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
  144. package/gsd-core/bin/lib/smart-entry.cjs +154 -22
  145. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  146. package/gsd-core/bin/lib/state-document.cjs +152 -8
  147. package/gsd-core/bin/lib/state-transition.cjs +424 -105
  148. package/gsd-core/bin/lib/state.cjs +1927 -401
  149. package/gsd-core/bin/lib/surface.cjs +35 -10
  150. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  151. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  152. package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
  153. package/gsd-core/bin/lib/uat.cjs +706 -64
  154. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  155. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  156. package/gsd-core/bin/lib/unusable-input.cjs +33 -0
  157. package/gsd-core/bin/lib/update-context.cjs +8 -2
  158. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  159. package/gsd-core/bin/lib/validate.cjs +20 -6
  160. package/gsd-core/bin/lib/vendor/README.md +37 -0
  161. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  162. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  163. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  164. package/gsd-core/bin/lib/verification.cjs +287 -20
  165. package/gsd-core/bin/lib/verify.cjs +368 -880
  166. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  167. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
  169. package/gsd-core/bin/lib/workstream.cjs +8 -2
  170. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  171. package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
  172. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  173. package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
  174. package/gsd-core/references/agent-contracts.md +43 -26
  175. package/gsd-core/references/artifact-types.md +10 -3
  176. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  177. package/gsd-core/references/checkpoints.md +2 -2
  178. package/gsd-core/references/context-budget.md +1 -1
  179. package/gsd-core/references/debugger-techniques.md +255 -0
  180. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  181. package/gsd-core/references/doc-conflict-engine.md +1 -1
  182. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  184. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  185. package/gsd-core/references/execute-phase-response-language.md +1 -1
  186. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  187. package/gsd-core/references/gate-prompts.md +1 -1
  188. package/gsd-core/references/git-planning-commit.md +2 -1
  189. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  190. package/gsd-core/references/model-profiles.md +12 -4
  191. package/gsd-core/references/mvp-concepts.md +9 -9
  192. package/gsd-core/references/planner-guidance.md +3 -9
  193. package/gsd-core/references/planner-preconditions.md +1 -1
  194. package/gsd-core/references/planner-reviews.md +1 -1
  195. package/gsd-core/references/planning-config.md +8 -6
  196. package/gsd-core/references/research-documentation-lookup.md +5 -3
  197. package/gsd-core/references/revision-loop.md +1 -1
  198. package/gsd-core/references/specless-probe-fallback.md +8 -7
  199. package/gsd-core/references/universal-anti-patterns.md +3 -3
  200. package/gsd-core/references/verifier-phase-gates.md +192 -0
  201. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  202. package/gsd-core/references/verify-mvp-mode.md +1 -1
  203. package/gsd-core/references/workstream-flag.md +22 -6
  204. package/gsd-core/references/worktree-branch-check.md +2 -2
  205. package/gsd-core/templates/discussion-log.md +1 -1
  206. package/gsd-core/templates/phase-prompt.md +2 -4
  207. package/gsd-core/templates/state.md +4 -4
  208. package/gsd-core/templates/summary-complex.md +2 -0
  209. package/gsd-core/templates/summary-minimal.md +2 -0
  210. package/gsd-core/templates/summary-standard.md +2 -0
  211. package/gsd-core/templates/summary.md +2 -0
  212. package/gsd-core/templates/verification-report.md +9 -1
  213. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  214. package/gsd-core/workflows/audit-milestone.md +3 -0
  215. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  216. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  217. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  218. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  219. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  220. package/gsd-core/workflows/autonomous.md +33 -70
  221. package/gsd-core/workflows/cleanup.md +62 -3
  222. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  223. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
  224. package/gsd-core/workflows/code-review-fix.md +37 -10
  225. package/gsd-core/workflows/code-review.md +74 -166
  226. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  227. package/gsd-core/workflows/complete-milestone.md +160 -95
  228. package/gsd-core/workflows/debug.md +16 -17
  229. package/gsd-core/workflows/diagnose-issues.md +56 -8
  230. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  231. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  232. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  233. package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
  234. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  235. package/gsd-core/workflows/docs-update.md +8 -51
  236. package/gsd-core/workflows/edit-phase.md +26 -1
  237. package/gsd-core/workflows/eval-review.md +3 -5
  238. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
  239. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  240. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  241. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  242. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
  243. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  245. package/gsd-core/workflows/execute-phase.md +103 -187
  246. package/gsd-core/workflows/execute-plan.md +36 -4
  247. package/gsd-core/workflows/explore.md +131 -4
  248. package/gsd-core/workflows/fast.md +10 -2
  249. package/gsd-core/workflows/health.md +73 -4
  250. package/gsd-core/workflows/help/modes/full.md +6 -1
  251. package/gsd-core/workflows/import.md +4 -4
  252. package/gsd-core/workflows/ingest-docs.md +7 -6
  253. package/gsd-core/workflows/mvp-phase.md +6 -3
  254. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  255. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  256. package/gsd-core/workflows/new-milestone.md +35 -47
  257. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  258. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  259. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  260. package/gsd-core/workflows/new-project.md +27 -240
  261. package/gsd-core/workflows/next.md +12 -0
  262. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  263. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  264. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  265. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  266. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  267. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  268. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  269. package/gsd-core/workflows/plan-phase.md +89 -209
  270. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  271. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  272. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  273. package/gsd-core/workflows/progress.md +45 -159
  274. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  275. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  276. package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
  277. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  278. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  279. package/gsd-core/workflows/quick.md +55 -405
  280. package/gsd-core/workflows/resume-project.md +3 -0
  281. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  282. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  283. package/gsd-core/workflows/review.md +41 -13
  284. package/gsd-core/workflows/section-manifest.json +219 -0
  285. package/gsd-core/workflows/secure-phase.md +1 -1
  286. package/gsd-core/workflows/session-report.md +2 -1
  287. package/gsd-core/workflows/settings.md +66 -2
  288. package/gsd-core/workflows/ship.md +104 -44
  289. package/gsd-core/workflows/sketch.md +1 -1
  290. package/gsd-core/workflows/spec-phase.md +41 -20
  291. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  292. package/gsd-core/workflows/spike.md +50 -16
  293. package/gsd-core/workflows/sync-skills.md +106 -13
  294. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  295. package/gsd-core/workflows/transition.md +53 -31
  296. package/gsd-core/workflows/ui-phase.md +13 -12
  297. package/gsd-core/workflows/ui-review.md +2 -2
  298. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  299. package/gsd-core/workflows/update.md +19 -8
  300. package/gsd-core/workflows/validate-phase.md +1 -1
  301. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  302. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  303. package/gsd-core/workflows/verify-work.md +17 -65
  304. package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
  305. package/hooks/dist/gsd-check-update-worker.js +64 -12
  306. package/hooks/dist/gsd-check-update.js +19 -1
  307. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  308. package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
  309. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  310. package/hooks/dist/gsd-prompt-guard.js +21 -20
  311. package/hooks/dist/gsd-read-injection-scanner.js +45 -24
  312. package/hooks/dist/gsd-statusline.js +90 -6
  313. package/hooks/dist/gsd-update-banner.js +22 -1
  314. package/hooks/dist/gsd-workflow-guard.js +134 -36
  315. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  316. package/hooks/dist/gsd-write-guard.js +359 -0
  317. package/hooks/dist/lib/git-cmd.js +92 -59
  318. package/hooks/dist/lib/injection-patterns.js +45 -0
  319. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  320. package/hooks/dist/lib/isolation-sentinel.js +277 -0
  321. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  322. package/hooks/gsd-agent-isolation-guard.js +517 -0
  323. package/hooks/gsd-check-update-worker.js +64 -12
  324. package/hooks/gsd-check-update.js +19 -1
  325. package/hooks/gsd-cursor-pre-tool.js +0 -3
  326. package/hooks/gsd-cursor-subagent-start.js +607 -26
  327. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  328. package/hooks/gsd-prompt-guard.js +21 -20
  329. package/hooks/gsd-read-injection-scanner.js +45 -24
  330. package/hooks/gsd-statusline.js +90 -6
  331. package/hooks/gsd-update-banner.js +22 -1
  332. package/hooks/gsd-workflow-guard.js +134 -36
  333. package/hooks/gsd-worktree-path-guard.js +2 -1
  334. package/hooks/gsd-write-guard.js +359 -0
  335. package/hooks/hooks.json +12 -0
  336. package/hooks/lib/git-cmd.js +92 -59
  337. package/hooks/lib/injection-patterns.js +45 -0
  338. package/hooks/lib/isolation-deny-reason.js +39 -0
  339. package/hooks/lib/isolation-sentinel.js +277 -0
  340. package/hooks/managed-hooks-registry.cjs +2 -0
  341. package/package.json +31 -10
  342. package/pi/gsd.cjs +71 -12
  343. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  344. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  345. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  346. package/scripts/build-hooks.js +9 -0
  347. package/scripts/changeset/lint.cjs +68 -6
  348. package/scripts/changeset/serialize.cjs +5 -1
  349. package/scripts/check-alias-drift.cjs +7 -43
  350. package/scripts/check-contract-drift.cjs +297 -0
  351. package/scripts/ci-test-scope.cjs +19 -2
  352. package/scripts/command-contract-helpers.cjs +903 -1
  353. package/scripts/gen-adr-index.cjs +728 -38
  354. package/scripts/gen-capability-matrix.cjs +1 -1
  355. package/scripts/gen-capability-registry.cjs +3 -15
  356. package/scripts/gen-context-index.cjs +439 -0
  357. package/scripts/gen-health-docs.cjs +390 -0
  358. package/scripts/gen-inventory-manifest.cjs +150 -4
  359. package/scripts/gen-loop-host-contract.cjs +4 -24
  360. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  361. package/scripts/gen-registry.cjs +3 -14
  362. package/scripts/gen-section-manifest.cjs +638 -0
  363. package/scripts/generate-package-identity.cjs +4 -2
  364. package/scripts/lib/alias-drift-families.cjs +46 -0
  365. package/scripts/lib/drift-scan.cjs +278 -0
  366. package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
  367. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  368. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  369. package/scripts/lint-canary-version-leak.cjs +73 -0
  370. package/scripts/lint-command-contract.cjs +96 -13
  371. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  372. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  373. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  374. package/scripts/lint-default-flip-documentation.cjs +193 -0
  375. package/scripts/lint-docs-command-form.cjs +195 -0
  376. package/scripts/lint-docs-required.cjs +9 -1
  377. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  378. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  379. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  380. package/scripts/lint-example-parser-parity.cjs +395 -0
  381. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  382. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  383. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  384. package/scripts/lint-milestone-window-drift.cjs +468 -0
  385. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  386. package/scripts/lint-plan-count-drift.cjs +318 -0
  387. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  388. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  389. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  390. package/scripts/lint-regression-test-names.cjs +15 -13
  391. package/scripts/lint-removed-but-needed.cjs +320 -0
  392. package/scripts/lint-state-field-drift.cjs +805 -0
  393. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  394. package/scripts/lint-test-file-count.allowlist.json +40 -3
  395. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  396. package/scripts/lint-vendored-deps.cjs +124 -0
  397. package/scripts/mutation-matrix.cjs +13 -0
  398. package/scripts/pr-changed-files.cjs +63 -0
  399. package/scripts/pr-template-policy.cjs +14 -4
  400. package/scripts/prompt-injection-scan.sh +52 -6
  401. package/scripts/require-issue-link-policy.cjs +192 -0
  402. package/scripts/state-write-path-drift-baseline.json +19 -0
  403. package/scripts/sync-runtime-launcher.cjs +2 -4
  404. package/skills/gsd-autonomous/SKILL.md +0 -1
  405. package/skills/gsd-code-review/SKILL.md +1 -1
  406. package/skills/gsd-execute-phase/SKILL.md +1 -2
  407. package/skills/gsd-map-codebase/SKILL.md +1 -1
  408. package/skills/gsd-mempalace-capture/SKILL.md +2 -2
  409. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  410. package/skills/gsd-new-milestone/SKILL.md +2 -2
  411. package/skills/gsd-next/SKILL.md +0 -1
  412. package/skills/gsd-plan-phase/SKILL.md +1 -2
  413. package/skills/gsd-progress/SKILL.md +0 -1
  414. package/skills/gsd-quick/SKILL.md +1 -1
  415. package/skills/gsd-review-backlog/SKILL.md +2 -1
  416. package/skills/gsd-stats/SKILL.md +0 -1
  417. package/skills/gsd-verify-work/SKILL.md +1 -1
  418. package/vscode/package.json +1 -1
  419. package/gsd-core/workflows/discovery-phase.md +0 -298
  420. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  421. package/gsd-core/workflows/verify-phase.md +0 -577
  422. package/scripts/affected-tests-lib.cjs +0 -554
  423. package/scripts/gen-emitted-baseline.cjs +0 -145
  424. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  425. package/scripts/run-affected-tests.cjs +0 -7
  426. package/scripts/run-tests.cjs +0 -1050
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ /**
3
+ * Installer migration: retire pi's legacy `<piConfigDir>/hooks/` directory
4
+ * after GSD's shared hook bundle moved to `<piConfigDir>/gsd-hooks/` (#3023).
5
+ *
6
+ * What old artifact is being retired?
7
+ * `hooks/` (and its `hooks/lib/` subdirectory) at the pi config root. pi
8
+ * reserves that exact name as its own deprecated extension directory and
9
+ * warns on every startup whenever it exists — pi's
10
+ * `checkDeprecatedExtensionDirs()` fires on mere PATH EXISTENCE, not on the
11
+ * directory having contents (unlike the sibling `tools/` check, which does
12
+ * `readdir` first). GSD used to install its shared hook bundle at exactly
13
+ * that reserved path, so every pi install carried the warning permanently.
14
+ * The fix moved the install target to `gsd-hooks/`
15
+ * (`hostBehaviors.sharedHooksDirName`), but an EXISTING install that
16
+ * upgrades still has the old `hooks/` tree sitting on disk — nothing
17
+ * removes it on its own, so the warning would persist forever without this
18
+ * migration.
19
+ *
20
+ * How do we prove it is GSD-owned?
21
+ * Per file, by manifest membership — the same `classifyArtifact()` check
22
+ * every other migration in this directory uses. Pre-#3023 pi installs
23
+ * record the shared hook bundle under `hooks/…` keys in
24
+ * `gsd-file-manifest.json`; the new install target writes `gsd-hooks/…`
25
+ * keys instead, which this migration structurally never sees because it
26
+ * only ever walks the `hooks/` subtree.
27
+ *
28
+ * What happens if the user modified it?
29
+ * `backup-and-remove` instead of `remove-managed`, so a locally patched
30
+ * hook script is recoverable from the backup rather than silently
31
+ * destroyed — mirrors migration 006.
32
+ *
33
+ * What happens to files the manifest never recorded?
34
+ * Nothing. An unmanifested file under `hooks/` (classification `unknown`)
35
+ * is left exactly where it is, and — because its presence keeps the
36
+ * directory non-empty — it also keeps the directory itself from being
37
+ * retired. That is a deliberate consequence of directory removal being
38
+ * gated on emptiness, not a special case.
39
+ *
40
+ * What happens to the directory itself?
41
+ * `hooks/lib/` and then `hooks/` each get a `remove-empty-dir` action (see
42
+ * `evaluateRemoveEmptyDir` in `../installer-migrations.cts`). That action
43
+ * only ever calls `fs.rmdirSync` — never a recursive removal — and
44
+ * re-checks emptiness immediately before doing so, so a directory that
45
+ * still holds anything (an unmanifested file, or a file-level action that
46
+ * failed to apply) is left in place rather than assumed empty. Actions are
47
+ * emitted deepest-first (`hooks/lib` before `hooks`) so the parent has a
48
+ * chance to become empty in the same pass.
49
+ *
50
+ * What happens if it is missing?
51
+ * No actions. A fresh post-#3023 pi install never creates `hooks/` at all,
52
+ * and an already-migrated install has nothing left to retire — both plan
53
+ * empty, so the migration is idempotent.
54
+ *
55
+ * What runtime and scope does it affect?
56
+ * pi only, global and local. No other runtime's install is affected:
57
+ * `hostBehaviors.sharedHooksDirName` defaults to `'hooks'` for every other
58
+ * runtime, and none of them reserve that name the way pi does, so a
59
+ * claude/kimi/opencode/etc. `hooks/` directory is a live, in-use install
60
+ * surface that must never be touched here. The runtime check is the FIRST
61
+ * thing `plan()` does, ahead of even checking whether the directory exists,
62
+ * as defense in depth beyond the `runtimes: ['pi']` record-level filter the
63
+ * framework itself already enforces.
64
+ *
65
+ * Is the action safe in non-interactive install?
66
+ * Yes. Every emitted action type (`remove-managed`, `backup-and-remove`,
67
+ * `remove-empty-dir`) is non-interactive and journaled; none requires a
68
+ * user choice, and unknown files never produce an action.
69
+ *
70
+ * See docs/installer-migrations.md#shipped-migrations, the pi row of
71
+ * docs/installer-migrations.md#runtime-configuration-contract-registry, and
72
+ * the 2026-08-07 amendment to docs/adr/0008-installer-migration-module.md.
73
+ */
74
+ var __importDefault = (this && this.__importDefault) || function (mod) {
75
+ return (mod && mod.__esModule) ? mod : { "default": mod };
76
+ };
77
+ const node_fs_1 = __importDefault(require("node:fs"));
78
+ const node_path_1 = __importDefault(require("node:path"));
79
+ /** pi's reserved (and, pre-#3023, GSD-populated) legacy hook directory name. */
80
+ const HOOKS_DIR = 'hooks';
81
+ const FILE_REASON = "pi's startup check warns whenever hooks/ exists (checkDeprecatedExtensionDirs), and GSD's shared " +
82
+ 'hook bundle now installs at gsd-hooks/ instead (#3023), so the legacy files are superseded';
83
+ const FILE_OWNERSHIP_EVIDENCE = 'pre-#3023 pi installs record the shared hook bundle under hooks/… keys in gsd-file-manifest.json; ' +
84
+ 'the new install target is gsd-hooks/…, which this migration never touches because it only walks the ' +
85
+ 'hooks/ subtree';
86
+ const DIR_REASON = "pi's checkDeprecatedExtensionDirs() warns on hooks/'s mere existence, not its contents (#3023); the " +
87
+ 'reserved container is retired once every GSD-owned entry inside it is gone';
88
+ const DIR_OWNERSHIP_EVIDENCE = 'hooks/ and hooks/lib/ are GSD-installed container directories under the pi config root (the pre-#3023 ' +
89
+ 'default of hostBehaviors.sharedHooksDirName); removal is gated on emptiness by the shared ' +
90
+ 'remove-empty-dir action, so a directory that still holds an unmanifested user file — or any file-level ' +
91
+ 'action that failed to apply — is left in place rather than assumed empty';
92
+ /**
93
+ * Recursively collect files and directories under `relDir`, never following a
94
+ * symlink (whether it names a file or a directory) and never emitting a path
95
+ * that resolves outside `baseResolved`. Mirrors the traversal guard in
96
+ * migration 003 (`walkLegacyFiles`).
97
+ */
98
+ function walkPiHooksTree(root, relDir, baseResolved, files, dirs) {
99
+ const dir = node_path_1.default.join(root, relDir);
100
+ const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
101
+ for (const entry of entries) {
102
+ // Never follow a symlink into or through: it must not be traversed,
103
+ // hashed, or removed, regardless of what it points at.
104
+ if (entry.isSymbolicLink())
105
+ continue;
106
+ const relPath = node_path_1.default.posix.join(relDir, entry.name);
107
+ const resolved = node_path_1.default.resolve(root, relPath);
108
+ if (resolved !== baseResolved && !resolved.startsWith(baseResolved + node_path_1.default.sep))
109
+ continue;
110
+ if (entry.isDirectory()) {
111
+ dirs.push(relPath);
112
+ walkPiHooksTree(root, relPath, baseResolved, files, dirs);
113
+ }
114
+ else if (entry.isFile()) {
115
+ files.push(relPath);
116
+ }
117
+ }
118
+ }
119
+ const migration = {
120
+ id: '2026-08-07-pi-retire-reserved-hooks-dir',
121
+ title: "Retire pi's reserved hooks/ directory",
122
+ description: 'Remove manifest-managed files under <piConfigDir>/hooks/ and, once empty, the directory itself ' +
123
+ '(and its hooks/lib/ subdirectory), now that the shared hook bundle installs at gsd-hooks/ instead. pi ' +
124
+ 'reserves hooks/ as its own deprecated extension directory and warns on every startup while it exists (#3023).',
125
+ introducedIn: '1.9.2',
126
+ runtimes: ['pi'],
127
+ scopes: ['global', 'local'],
128
+ destructive: true,
129
+ plan: (ctx) => {
130
+ // Defense in depth ahead of the framework's own runtimes filter: a
131
+ // claude/kimi/opencode/etc. hooks/ directory is a live install surface,
132
+ // never a retirement target.
133
+ if (ctx.runtime !== 'pi')
134
+ return [];
135
+ const hooksRoot = node_path_1.default.join(ctx.configDir, HOOKS_DIR);
136
+ let rootLstat;
137
+ try {
138
+ rootLstat = node_fs_1.default.lstatSync(hooksRoot);
139
+ }
140
+ catch {
141
+ return []; // absent -> nothing to retire, idempotent
142
+ }
143
+ // Never follow a symlinked hooks/ root: walking through it could plan
144
+ // actions against paths outside the pi config directory entirely.
145
+ if (rootLstat.isSymbolicLink())
146
+ return [];
147
+ if (!rootLstat.isDirectory())
148
+ return [];
149
+ const baseResolved = node_path_1.default.resolve(ctx.configDir);
150
+ const files = [];
151
+ const dirs = [];
152
+ try {
153
+ walkPiHooksTree(ctx.configDir, HOOKS_DIR, baseResolved, files, dirs);
154
+ }
155
+ catch {
156
+ // Unreadable directory: nothing safe to plan.
157
+ return [];
158
+ }
159
+ const actions = [];
160
+ for (const relPath of files) {
161
+ const { classification } = ctx.classifyArtifact(relPath);
162
+ if (classification === 'managed-pristine') {
163
+ actions.push({ type: 'remove-managed', relPath, reason: FILE_REASON, ownershipEvidence: FILE_OWNERSHIP_EVIDENCE });
164
+ }
165
+ else if (classification === 'managed-modified') {
166
+ actions.push({ type: 'backup-and-remove', relPath, reason: FILE_REASON, ownershipEvidence: FILE_OWNERSHIP_EVIDENCE });
167
+ }
168
+ // 'unknown' (user-added, not manifest-recorded): no action, preserved.
169
+ // 'missing' / 'managed-missing': impossible here — relPath was just
170
+ // discovered by walking the live filesystem, so it currently exists.
171
+ }
172
+ // Deepest directories first, so a child has already been evaluated (and
173
+ // possibly removed) before its parent's own emptiness is re-checked by
174
+ // the executor. `hooks/` itself is appended last, unconditionally: the
175
+ // executor's own emptiness re-check is what actually decides whether it
176
+ // goes, not this ordering — this ordering only gives it the chance to.
177
+ const orderedDirs = [...dirs].sort((a, b) => b.split('/').length - a.split('/').length);
178
+ orderedDirs.push(HOOKS_DIR);
179
+ for (const relPath of orderedDirs) {
180
+ actions.push({
181
+ type: 'remove-empty-dir',
182
+ relPath,
183
+ reason: DIR_REASON,
184
+ ownershipEvidence: DIR_OWNERSHIP_EVIDENCE,
185
+ // Declared, not derived: classifyArtifact() hashes file contents via
186
+ // sha256File(), which throws EISDIR against a directory path. These
187
+ // relPaths name directories, so classification is stated directly
188
+ // (never 'unknown', so the planner's unknown-classification block
189
+ // never fires for them) rather than routed through the file
190
+ // classifier.
191
+ classification: 'managed-pristine',
192
+ originalHash: null,
193
+ currentHash: null,
194
+ });
195
+ }
196
+ return actions;
197
+ },
198
+ };
199
+ module.exports = migration;
@@ -16,32 +16,133 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
16
16
  const installer_migration_authoring_cjs_1 = require("./installer-migration-authoring.cjs");
17
17
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
18
18
  const clock_cjs_1 = require("./clock.cjs");
19
+ const install_scope_cjs_1 = require("./install-scope.cjs");
20
+ // #2874 (ADR-58 cleanup phase): this file is the ~1200-line migration
21
+ // plan/apply/rollback/lock/journal engine — almost none of it is on the
22
+ // installRuntimeArtifacts call tree. Only `readInstallManifest` and
23
+ // `classifyArtifact` are reached (via install-engine.cts's
24
+ // _migrateLegacyOpencodeCommandDir and retired-artifact-cleanup.cts's
25
+ // pruneRetiredRuntimeArtifacts), so only those two entry points — plus their
26
+ // shared `readJsonIfPresent` helper and `classifyArtifact`'s `sha256File`
27
+ // hashing helper — are routed through the injectable seam. Everything else
28
+ // in this file (locking, journal, apply/rollback, migration discovery)
29
+ // keeps using real `fs` directly: it is not reachable from
30
+ // installRuntimeArtifacts, so routing it would grow this seam past what
31
+ // AC2 actually requires. See install-fs-adapter.cts's module doc.
32
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
33
+ const installFsAdapter = require("./install-fs-adapter.cjs");
34
+ const { installFs } = installFsAdapter;
19
35
  const MANIFEST_NAME = 'gsd-file-manifest.json';
20
36
  const INSTALL_STATE_NAME = 'gsd-install-state.json';
21
37
  const INSTALL_MIGRATION_LOCK_NAME = 'gsd-install-migration.lock';
22
38
  const DEFAULT_MIGRATIONS_DIR = node_path_1.default.join(__dirname, 'installer-migrations');
23
39
  const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
24
40
  const STRICT_JSON = Symbol('strict-json');
41
+ // #2874: routed through installFs()'s openSync/readSync/closeSync trio
42
+ // instead of importing `node:fs` directly, so classifyArtifact — reachable
43
+ // from installRuntimeArtifacts — can be exercised against an injected
44
+ // adapter. This function was briefly converted to a single
45
+ // `installFs().readFileSync` call (buffering the whole file); that broke
46
+ // tests/installer-migrations.test.cjs's "classifies large files without
47
+ // loading the whole file through readFileSync", which monkeypatches real
48
+ // fs.readFileSync to throw for the file under test and asserts hashing still
49
+ // succeeds — an explicit, pre-existing contract that large files must be
50
+ // streamed, not buffered. Restored to the original raw-fd streaming shape,
51
+ // now going through the adapter instead of `node:fs` directly. This is the
52
+ // ONLY call site of sha256File in this file (confirmed by inspection) — no
53
+ // other caller is affected.
25
54
  function sha256File(filePath) {
26
55
  const hash = node_crypto_1.default.createHash('sha256');
27
56
  const buffer = Buffer.allocUnsafe(1024 * 1024);
28
- const fd = node_fs_1.default.openSync(filePath, 'r');
57
+ const fd = installFs().openSync(filePath, 'r');
29
58
  try {
30
59
  while (true) {
31
- const bytesRead = node_fs_1.default.readSync(fd, buffer, 0, buffer.length, null);
60
+ const bytesRead = installFs().readSync(fd, buffer, 0, buffer.length, null);
32
61
  if (bytesRead === 0)
33
62
  break;
34
63
  hash.update(buffer.subarray(0, bytesRead));
35
64
  }
36
65
  }
37
66
  finally {
38
- node_fs_1.default.closeSync(fd);
67
+ installFs().closeSync(fd);
39
68
  }
40
69
  return hash.digest('hex');
41
70
  }
42
71
  function sha256Text(value) {
43
72
  return node_crypto_1.default.createHash('sha256').update(value).digest('hex');
44
73
  }
74
+ /**
75
+ * Evaluate and, if safe, perform a `remove-empty-dir` action against `fullPath`.
76
+ *
77
+ * This is deliberately WEAKER than a recursive directory-removal primitive
78
+ * (which 003's docblock records as an intentional absence in the ADR-0008
79
+ * design): it only ever calls `fs.rmdirSync` — never `fs.rmSync`, never
80
+ * `{ recursive: true }`, never `{ force: true }` — so a non-empty directory
81
+ * fails the underlying syscall rather than being swept. The emptiness check
82
+ * immediately above the call is what turns that failure mode into a
83
+ * deliberate, non-error "left in place" outcome instead of surfacing ENOTEMPTY.
84
+ *
85
+ * Guards, in order:
86
+ * - lstat (not stat): a symlinked directory is refused outright, never
87
+ * followed. A missing target is reported distinctly so callers can tell
88
+ * "nothing was ever there" from "something was there and is left alone".
89
+ * - must actually be a directory (not a file masquerading under the relPath).
90
+ * - containment: the REALPATH of the target must resolve strictly inside the
91
+ * REALPATH of configDir — never equal to it (removing the config root
92
+ * itself is never in scope) and never escaping it (e.g. via an ancestor
93
+ * symlink the lstat check alone would not catch).
94
+ * - emptiness, re-checked here rather than trusted from planning time: a
95
+ * directory that still holds any entry (managed-but-undeleted, unknown,
96
+ * or created between plan and apply) is left in place. This is reported
97
+ * as 'skipped-not-empty', a successful no-op, not a failure.
98
+ *
99
+ * Any unexpected error along the way (EACCES, EBUSY, a race that removes the
100
+ * target between the lstat and the rmdir, etc.) degrades to 'left-in-place'.
101
+ * This action must never throw out of the executor, matching every sibling
102
+ * action type's failure posture.
103
+ */
104
+ function evaluateRemoveEmptyDir(configDir, fullPath) {
105
+ let stat;
106
+ try {
107
+ stat = node_fs_1.default.lstatSync(fullPath);
108
+ }
109
+ catch {
110
+ return 'missing';
111
+ }
112
+ if (stat.isSymbolicLink())
113
+ return 'left-in-place';
114
+ if (!stat.isDirectory())
115
+ return 'left-in-place';
116
+ let resolvedRoot;
117
+ let resolvedTarget;
118
+ try {
119
+ resolvedRoot = node_fs_1.default.realpathSync(configDir);
120
+ resolvedTarget = node_fs_1.default.realpathSync(fullPath);
121
+ }
122
+ catch {
123
+ return 'left-in-place';
124
+ }
125
+ if (resolvedTarget === resolvedRoot || !resolvedTarget.startsWith(resolvedRoot + node_path_1.default.sep)) {
126
+ // Refuses both "target IS configDir" and "target escaped configDir".
127
+ return 'left-in-place';
128
+ }
129
+ let entries;
130
+ try {
131
+ entries = node_fs_1.default.readdirSync(fullPath);
132
+ }
133
+ catch {
134
+ return 'left-in-place';
135
+ }
136
+ if (entries.length > 0)
137
+ return 'skipped-not-empty';
138
+ try {
139
+ node_fs_1.default.rmdirSync(fullPath);
140
+ return 'removed';
141
+ }
142
+ catch {
143
+ return 'left-in-place';
144
+ }
145
+ }
45
146
  /**
46
147
  * Copy a managed path for the rollback snapshot or the user-facing backup,
47
148
  * WITHOUT dereferencing a symlink.
@@ -60,24 +161,36 @@ function sha256Text(value) {
60
161
  * surfaces as an apply failure and triggers the normal rollback path, which is
61
162
  * the correct outcome — refusing to proceed beats silently copying referent
62
163
  * bytes.
164
+ *
165
+ * #2875 (epic #2866 Phase 6): all five fs calls routed through `installFs()`
166
+ * so this primitive can be reused on the routed install path (by
167
+ * user-artifact-staging.cts) without punching a hole through the seam Phase 5
168
+ * built. Every EXISTING caller of this function is on the migration
169
+ * plan/apply/rollback tree, which never wraps a call in `withInstallFs` — the
170
+ * ambient adapter there resolves to real `node:fs` by default, so this
171
+ * routing is behavior-preserving for them (test-matrix D2).
63
172
  */
64
173
  function copyPreservingSymlink(srcPath, destPath) {
65
- if (node_fs_1.default.lstatSync(srcPath).isSymbolicLink()) {
174
+ if (installFs().lstatSync(srcPath).isSymbolicLink()) {
66
175
  // symlinkSync fails with EEXIST on an occupied path, so clear it first.
67
176
  // Scoped to this branch on purpose: the regular-file path below keeps
68
177
  // copyFileSync's overwrite-in-place, so a mid-restore failure cannot leave
69
178
  // the destination destroyed.
70
- node_fs_1.default.rmSync(destPath, { force: true });
71
- node_fs_1.default.symlinkSync(node_fs_1.default.readlinkSync(srcPath), destPath);
179
+ installFs().rmSync(destPath, { force: true });
180
+ installFs().symlinkSync(installFs().readlinkSync(srcPath), destPath);
72
181
  return;
73
182
  }
74
- node_fs_1.default.copyFileSync(srcPath, destPath);
183
+ installFs().copyFileSync(srcPath, destPath);
75
184
  }
185
+ // Shared by readInstallManifest (on the installRuntimeArtifacts call tree —
186
+ // routed) and readInstallState/readJson (not on that call tree — the
187
+ // ambient default resolves to real fs for those, unchanged). Routing once
188
+ // here is safe for all three callers.
76
189
  function readJsonIfPresent(filePath, fallback) {
77
- if (!node_fs_1.default.existsSync(filePath))
190
+ if (!installFs().existsSync(filePath))
78
191
  return fallback;
79
192
  try {
80
- return JSON.parse(node_fs_1.default.readFileSync(filePath, 'utf8'));
193
+ return JSON.parse(installFs().readFileSync(filePath, 'utf8'));
81
194
  }
82
195
  catch (error) {
83
196
  if (fallback === STRICT_JSON) {
@@ -86,17 +199,81 @@ function readJsonIfPresent(filePath, fallback) {
86
199
  return fallback;
87
200
  }
88
201
  }
202
+ /** Lowest manifest schema version that records `runtime`/`scope` (#2872). */
203
+ const MANIFEST_SCHEMA_VERSION = 2;
204
+ /**
205
+ * Longest `runtime` string this reader will report. Real runtime ids are
206
+ * registry keys (`claude`, `antigravity`, `kimi-code` — 11 chars at the
207
+ * longest), so this loses nothing legitimate; it exists because the manifest
208
+ * is attacker-influenceable (a project-local one lives inside a repository a
209
+ * user may merely have cloned) and the value reaches a consumer that renders
210
+ * it. Same 64-char convention as `truncatePostureValue`
211
+ * (`agent-install-check.cts`), deliberately, so the subsystem caps reported
212
+ * values one way.
213
+ */
214
+ const MAX_REPORTED_RUNTIME_LENGTH = 64;
215
+ /**
216
+ * A manifest's `runtime` is reported as a FACT about the file — it is
217
+ * deliberately NOT validated against the capability registry, because an
218
+ * unregistered id is exactly the kind of mismatch the Installed Surface
219
+ * Resolver exists to surface (#2872 design row B8). It is, however, LENGTH
220
+ * bounded: "report the fact" never required "report unbounded bytes".
221
+ */
222
+ function normalizeReportedRuntime(raw) {
223
+ if (typeof raw !== 'string')
224
+ return null;
225
+ if (raw.trim() === '')
226
+ return null;
227
+ return raw.length > MAX_REPORTED_RUNTIME_LENGTH
228
+ ? `${raw.slice(0, MAX_REPORTED_RUNTIME_LENGTH)}…`
229
+ : raw;
230
+ }
231
+ /**
232
+ * Normalize a raw `manifestVersion`. Only a finite integer >= 1 is a version
233
+ * claim; everything else (absent, `"2"`, `0`, `-1`, `2.5`, `NaN`, `Infinity`)
234
+ * reads as `1` — a pre-#2872 manifest. Liberal in what it accepts, but the
235
+ * normalization is a stated value rather than a silent guess: a caller can
236
+ * always tell v1 (`1`) from "no manifest at all" (`null`).
237
+ */
238
+ function normalizeManifestVersion(raw) {
239
+ if (typeof raw !== 'number')
240
+ return 1;
241
+ if (!Number.isInteger(raw))
242
+ return 1;
243
+ if (raw < 1)
244
+ return 1;
245
+ return raw;
246
+ }
89
247
  function readInstallManifest(configDir) {
90
248
  const manifest = readJsonIfPresent(node_path_1.default.join(configDir, MANIFEST_NAME), null);
91
- if (!manifest || typeof manifest !== 'object') {
92
- return { version: null, timestamp: null, mode: null, files: {} };
249
+ // `typeof [] === 'object'` in JS, so a bare `typeof !== 'object'` guard lets
250
+ // a top-level JSON array (valid JSON, but not the manifest's documented
251
+ // object shape) fall through to the field reads below — `m.manifestVersion`
252
+ // reads `undefined` off an array, which `normalizeManifestVersion` then
253
+ // reports as `1` (a v1 manifest), misclassifying "not an object" as
254
+ // "installed". `Array.isArray` closes that gap explicitly rather than
255
+ // relying on the object-shape checks below to catch it incidentally.
256
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
257
+ return {
258
+ version: null,
259
+ timestamp: null,
260
+ mode: null,
261
+ files: {},
262
+ manifestVersion: null,
263
+ runtime: null,
264
+ scope: null,
265
+ };
93
266
  }
94
267
  const m = manifest;
268
+ const rawRuntime = m.runtime;
95
269
  return {
96
270
  version: typeof m.version === 'string' ? m.version : null,
97
271
  timestamp: typeof m.timestamp === 'string' ? m.timestamp : null,
98
272
  mode: typeof m.mode === 'string' ? m.mode : null,
99
273
  files: m.files && typeof m.files === 'object' ? m.files : {},
274
+ manifestVersion: normalizeManifestVersion(m.manifestVersion),
275
+ runtime: normalizeReportedRuntime(rawRuntime),
276
+ scope: (0, install_scope_cjs_1.isInstallScopeId)(m.scope) ? m.scope : null,
100
277
  };
101
278
  }
102
279
  function readInstallState(configDir) {
@@ -163,7 +340,7 @@ function classifyArtifact(configDir, relPath, manifest) {
163
340
  const normalized = normalizeRelPath(relPath);
164
341
  const originalHash = manifest.files[normalized] || null;
165
342
  const fullPath = node_path_1.default.join(configDir, normalized);
166
- if (!node_fs_1.default.existsSync(fullPath)) {
343
+ if (!installFs().existsSync(fullPath)) {
167
344
  return { classification: originalHash ? 'managed-missing' : 'missing', originalHash, currentHash: null };
168
345
  }
169
346
  const currentHash = sha256File(fullPath);
@@ -643,7 +820,8 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
643
820
  action.type !== 'backup-and-remove' &&
644
821
  action.type !== 'rewrite-json' &&
645
822
  action.type !== 'record-baseline' &&
646
- action.type !== 'baseline-preserve-user') {
823
+ action.type !== 'baseline-preserve-user' &&
824
+ action.type !== 'remove-empty-dir') {
647
825
  throw new Error(`unsupported migration action type: ${action.type}`);
648
826
  }
649
827
  const { normalized, fullPath } = ensureInsideConfig(configDir, action.relPath);
@@ -655,6 +833,18 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
655
833
  journal.actions.push(journalAction(action, action.type === 'record-baseline' ? 'recorded' : 'preserved'));
656
834
  continue;
657
835
  }
836
+ if (action.type === 'remove-empty-dir') {
837
+ // Directory actions never enter the file-copy/rollback machinery below:
838
+ // there is nothing to snapshot-and-restore for a directory node itself
839
+ // (its former CONTENTS were already snapshotted by their own file-level
840
+ // actions before this one runs), and rollback of a removed empty
841
+ // directory is simply re-creating it, which the rollback path below
842
+ // does not model. Non-recursive by construction (evaluateRemoveEmptyDir
843
+ // only ever calls fs.rmdirSync), so there is nothing destructive to undo
844
+ // beyond an mkdir the next install/migration run will happily redo.
845
+ journal.actions.push(journalAction(action, evaluateRemoveEmptyDir(configDir, fullPath)));
846
+ continue;
847
+ }
658
848
  const rollbackPath = node_path_1.default.join(rollbackRoot, normalized);
659
849
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(rollbackPath), { recursive: true });
660
850
  copyPreservingSymlink(fullPath, rollbackPath);
@@ -852,7 +1042,10 @@ module.exports = {
852
1042
  acquireInstallMigrationLock,
853
1043
  applyInstallerMigrationPlan,
854
1044
  classifyArtifact,
1045
+ copyPreservingSymlink,
855
1046
  discoverInstallerMigrations,
1047
+ evaluateRemoveEmptyDir,
1048
+ MANIFEST_SCHEMA_VERSION,
856
1049
  migrationChecksum,
857
1050
  planInstallerMigrations,
858
1051
  readInstallManifest,
@@ -118,6 +118,22 @@ function writeAllSync(fd, data) {
118
118
  }
119
119
  }
120
120
  }
121
+ /**
122
+ * The wire form of a JSON result: the exact bytes `output()` emits for it.
123
+ *
124
+ * Exported because a caller that has to reason about the size of its own
125
+ * response — graphify's `--budget` accounting (#2738) — must measure the string
126
+ * this function produces rather than a second, privately-maintained
127
+ * serialization that can drift from it. One definition, so estimator and
128
+ * emitter cannot disagree about indentation or shape.
129
+ *
130
+ * The `@file:` redirection in `output()` is a transport detail, not a payload
131
+ * change: the caller still consumes these bytes, so these are the right ones to
132
+ * measure.
133
+ */
134
+ function serializeForOutput(result) {
135
+ return JSON.stringify(result, null, 2);
136
+ }
121
137
  function output(result, raw, rawValue) {
122
138
  let data;
123
139
  if (raw && rawValue !== undefined) {
@@ -125,7 +141,7 @@ function output(result, raw, rawValue) {
125
141
  data = String(rawValue);
126
142
  }
127
143
  else {
128
- const json = JSON.stringify(result, null, 2);
144
+ const json = serializeForOutput(result);
129
145
  // Large payloads exceed Claude Code's Bash tool buffer (~50KB).
130
146
  // Write to tmpfile and output the path prefixed with @file: so callers can detect it.
131
147
  if (json.length > 50000) {
@@ -168,12 +184,23 @@ const ERROR_REASON = Object.freeze({
168
184
  // workflow / phase
169
185
  PHASE_NOT_FOUND: 'phase_not_found',
170
186
  PHASE_VERIFICATION_INCOMPLETE: 'phase_verification_incomplete',
187
+ PHASE_PLAN_COVERAGE_INCOMPLETE: 'phase_plan_coverage_incomplete',
171
188
  SUMMARY_NO_PLANNING: 'summary_no_planning',
189
+ // #3579: workstream-mode fail-safe guards (init.progress, phase.complete) —
190
+ // distinguishes "no marker/pointer anywhere" from "a marker exists but
191
+ // didn't resolve" so a JSON-error-mode caller can branch on `reason`
192
+ // instead of regexing the human message.
193
+ WORKSTREAM_MODE_NONE_ACTIVE: 'workstream_mode_none_active',
194
+ WORKSTREAM_MODE_MARKER_UNRESOLVED: 'workstream_mode_marker_unresolved',
172
195
  // graphify
173
196
  GRAPHIFY_NO_GRAPH: 'graphify_no_graph',
174
197
  GRAPHIFY_INVALID_QUERY: 'graphify_invalid_query',
175
198
  // hooks
176
199
  HOOKS_OPT_OUT: 'hooks_opt_out',
200
+ // commit-docs-guard (#3588)
201
+ COMMIT_DOCS_GUARD_NOT_A_REPO: 'commit_docs_guard_not_a_repo',
202
+ COMMIT_DOCS_GUARD_FOREIGN_HOOK: 'commit_docs_guard_foreign_hook',
203
+ COMMIT_DOCS_GUARD_HOOKS_PATH_SET: 'commit_docs_guard_hooks_path_set',
177
204
  // security-scan
178
205
  SECURITY_SCAN_FAILED: 'security_scan_failed',
179
206
  // generic
@@ -198,10 +225,17 @@ function getJsonErrorMode() { return _jsonErrorMode; }
198
225
  * process is in JSON-error mode, stderr receives `{ ok: false, reason,
199
226
  * message }` so callers can parse it; otherwise stderr keeps the plain
200
227
  * text form for human operators.
228
+ *
229
+ * `extra` (optional) lets a caller attach additional structured fields
230
+ * (e.g. `{ verification_stale_check_indeterminate: true }`) onto the
231
+ * JSON-error-mode payload, spread alongside `ok`/`reason`/`message`, so a
232
+ * test can assert on the value directly instead of regexing `message`'s
233
+ * human-readable text. Ignored entirely in plain-text mode — the human
234
+ * message is the only thing an operator sees there.
201
235
  */
202
- function error(message, reason = ERROR_REASON.UNKNOWN) {
236
+ function error(message, reason = ERROR_REASON.UNKNOWN, extra) {
203
237
  if (_jsonErrorMode) {
204
- const payload = JSON.stringify({ ok: false, reason, message }) + '\n';
238
+ const payload = JSON.stringify({ ok: false, reason, message, ...(extra || {}) }) + '\n';
205
239
  writeAllSync(2, payload);
206
240
  }
207
241
  else {
@@ -214,6 +248,7 @@ module.exports = {
214
248
  ensureGsdTempDir,
215
249
  reapStaleTempFiles,
216
250
  output,
251
+ serializeForOutput,
217
252
  ERROR_REASON,
218
253
  setJsonErrorMode,
219
254
  getJsonErrorMode,
@@ -14,6 +14,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.stripFencedCode = stripFencedCode;
15
15
  exports.stripInlineCode = stripInlineCode;
16
16
  exports.scanInlineCodeSpans = scanInlineCodeSpans;
17
+ exports.scanFencedBlocks = scanFencedBlocks;
17
18
  exports.extractFencedBlock = extractFencedBlock;
18
19
  exports.tokenizeHeadings = tokenizeHeadings;
19
20
  exports.collectSections = collectSections;
@@ -25,6 +26,7 @@ exports.stripTaggedBlocks = stripTaggedBlocks;
25
26
  exports.replaceSection = replaceSection;
26
27
  exports.withSection = withSection;
27
28
  exports.deleteSection = deleteSection;
29
+ const pattern_cjs_1 = require("./pattern.cjs");
28
30
  // ─── stripFencedCode ──────────────────────────────────────────────────────────
29
31
  /**
30
32
  * CommonMark-correct fenced-code-block stripper.
@@ -206,6 +208,11 @@ function stripInlineCodeLine(line) {
206
208
  * Tracked duplication (same status as `tokenizeHeadings`'s copy, see its
207
209
  * comment above): this is a second independent copy of the fence state
208
210
  * machine, pending a T-tier consolidation.
211
+ *
212
+ * Exported so `context-predicates.cts` can consume this seam directly for its
213
+ * line-preserving fenced-line skip detection, instead of carrying a third
214
+ * independent copy of the fence state machine (see that module's doc
215
+ * comment).
209
216
  */
210
217
  function scanFencedBlocks(lines) {
211
218
  const delimRe = /^( {0,3})(`{3,}|~{3,})(.*)$/;
@@ -786,7 +793,7 @@ function extractTaggedBlocks(content, tagName, allowAttributes = false) {
786
793
  * open>` marks the ACTIVE milestone and must be preserved, not stripped (#557).
787
794
  */
788
795
  function taggedBlockPattern(tagName, flags, allowAttributes) {
789
- const esc = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
796
+ const esc = (0, pattern_cjs_1.escapeRegex)(tagName);
790
797
  const open = allowAttributes ? `<${esc}(?:\\s[^>]{0,1000})?>` : `<${esc}>`;
791
798
  const boundary = allowAttributes ? `<${esc}[\\s>]` : `<${esc}>`;
792
799
  return new RegExp(`${open}((?:(?!${boundary})[\\s\\S])*?)</${esc}>`, flags);