@opengsd/gsd-core 1.10.0 → 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 (328) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-debug-session-manager.md +11 -0
  4. package/agents/gsd-doc-synthesizer.md +2 -4
  5. package/agents/gsd-executor.md +5 -5
  6. package/agents/gsd-mempalace-curator.md +5 -2
  7. package/agents/gsd-phase-researcher.md +20 -1
  8. package/agents/gsd-plan-checker.md +37 -0
  9. package/agents/gsd-planner.md +44 -46
  10. package/agents/gsd-user-profiler.md +3 -0
  11. package/agents/gsd-verifier.md +12 -3
  12. package/bin/install.js +841 -971
  13. package/bin/lib/ui-safety-gate.cjs +2 -0
  14. package/commands/gsd/code-review.md +1 -1
  15. package/commands/gsd/execute-phase.md +1 -1
  16. package/commands/gsd/map-codebase.md +1 -1
  17. package/commands/gsd/mempalace-capture.md +1 -1
  18. package/commands/gsd/mempalace-recall.md +1 -1
  19. package/commands/gsd/new-milestone.md +1 -1
  20. package/commands/gsd/quick.md +1 -1
  21. package/commands/gsd/review-backlog.md +2 -1
  22. package/commands/gsd/verify-work.md +1 -1
  23. package/gsd-core/bin/gsd-tools.cjs +469 -88
  24. package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
  25. package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
  26. package/gsd-core/bin/lib/api-coverage.cjs +3 -5
  27. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  28. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  29. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  30. package/gsd-core/bin/lib/audit.cjs +876 -240
  31. package/gsd-core/bin/lib/broken-windows.cjs +1 -1
  32. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  33. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +575 -101
  35. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  36. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  37. package/gsd-core/bin/lib/capability-validator.cjs +495 -22
  38. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  39. package/gsd-core/bin/lib/check-command-router.cjs +71 -37
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  41. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  43. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  44. package/gsd-core/bin/lib/commands.cjs +651 -86
  45. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  47. package/gsd-core/bin/lib/config-loader.cjs +75 -0
  48. package/gsd-core/bin/lib/config.cjs +10 -1
  49. package/gsd-core/bin/lib/core-utils.cjs +127 -29
  50. package/gsd-core/bin/lib/decisions.cjs +23 -0
  51. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  52. package/gsd-core/bin/lib/frontmatter.cjs +155 -20
  53. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  54. package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
  55. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  56. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  57. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  58. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  59. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  60. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  61. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  62. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  63. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  64. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  65. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  66. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  67. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  68. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  69. package/gsd-core/bin/lib/init.cjs +321 -129
  70. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  71. package/gsd-core/bin/lib/install-engine.cjs +745 -258
  72. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  73. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  74. package/gsd-core/bin/lib/install-profiles.cjs +134 -57
  75. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  76. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  77. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  78. package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
  79. package/gsd-core/bin/lib/io.cjs +10 -0
  80. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  81. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  82. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  83. package/gsd-core/bin/lib/milestone.cjs +754 -70
  84. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  85. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  86. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  87. package/gsd-core/bin/lib/pattern.cjs +122 -0
  88. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +444 -36
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  91. package/gsd-core/bin/lib/phase-locator.cjs +125 -18
  92. package/gsd-core/bin/lib/phase.cjs +646 -143
  93. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  94. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  95. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  96. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  97. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  98. package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
  99. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  100. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  101. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  102. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  103. package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
  104. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  105. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  106. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  107. package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
  108. package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
  109. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  110. package/gsd-core/bin/lib/roadmap.cjs +385 -94
  111. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
  112. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  113. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
  114. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  115. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  116. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
  117. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  118. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  119. package/gsd-core/bin/lib/security.cjs +104 -5
  120. package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
  121. package/gsd-core/bin/lib/smart-entry.cjs +142 -22
  122. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  123. package/gsd-core/bin/lib/state-document.cjs +152 -8
  124. package/gsd-core/bin/lib/state-transition.cjs +371 -117
  125. package/gsd-core/bin/lib/state.cjs +1794 -357
  126. package/gsd-core/bin/lib/surface.cjs +23 -9
  127. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  128. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  129. package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
  130. package/gsd-core/bin/lib/uat.cjs +399 -56
  131. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  132. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  133. package/gsd-core/bin/lib/unusable-input.cjs +24 -0
  134. package/gsd-core/bin/lib/update-context.cjs +8 -2
  135. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  136. package/gsd-core/bin/lib/validate.cjs +20 -6
  137. package/gsd-core/bin/lib/vendor/README.md +37 -0
  138. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  139. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  140. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  141. package/gsd-core/bin/lib/verification.cjs +258 -8
  142. package/gsd-core/bin/lib/verify.cjs +368 -888
  143. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  144. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  145. package/gsd-core/bin/lib/workstream.cjs +2 -2
  146. package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
  147. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  148. package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
  149. package/gsd-core/references/agent-contracts.md +43 -26
  150. package/gsd-core/references/checkpoints.md +2 -2
  151. package/gsd-core/references/context-budget.md +1 -1
  152. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  153. package/gsd-core/references/doc-conflict-engine.md +1 -1
  154. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  155. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  156. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  157. package/gsd-core/references/execute-phase-response-language.md +1 -1
  158. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  159. package/gsd-core/references/gate-prompts.md +1 -1
  160. package/gsd-core/references/git-planning-commit.md +2 -1
  161. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  162. package/gsd-core/references/model-profiles.md +12 -4
  163. package/gsd-core/references/mvp-concepts.md +9 -9
  164. package/gsd-core/references/planner-guidance.md +3 -9
  165. package/gsd-core/references/planner-preconditions.md +1 -1
  166. package/gsd-core/references/planner-reviews.md +1 -1
  167. package/gsd-core/references/planning-config.md +8 -6
  168. package/gsd-core/references/revision-loop.md +1 -1
  169. package/gsd-core/references/specless-probe-fallback.md +1 -1
  170. package/gsd-core/references/universal-anti-patterns.md +3 -3
  171. package/gsd-core/references/verifier-phase-gates.md +192 -0
  172. package/gsd-core/references/verify-mvp-mode.md +1 -1
  173. package/gsd-core/references/workstream-flag.md +22 -6
  174. package/gsd-core/templates/discussion-log.md +1 -1
  175. package/gsd-core/templates/phase-prompt.md +2 -4
  176. package/gsd-core/templates/state.md +4 -4
  177. package/gsd-core/templates/verification-report.md +9 -1
  178. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  179. package/gsd-core/workflows/autonomous.md +1 -1
  180. package/gsd-core/workflows/cleanup.md +62 -3
  181. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
  182. package/gsd-core/workflows/code-review-fix.md +37 -10
  183. package/gsd-core/workflows/code-review.md +38 -12
  184. package/gsd-core/workflows/complete-milestone.md +141 -18
  185. package/gsd-core/workflows/debug.md +7 -5
  186. package/gsd-core/workflows/diagnose-issues.md +35 -9
  187. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  188. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  189. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
  190. package/gsd-core/workflows/edit-phase.md +26 -1
  191. package/gsd-core/workflows/eval-review.md +3 -5
  192. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
  193. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  194. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
  195. package/gsd-core/workflows/execute-phase.md +38 -50
  196. package/gsd-core/workflows/execute-plan.md +36 -4
  197. package/gsd-core/workflows/explore.md +131 -4
  198. package/gsd-core/workflows/fast.md +10 -2
  199. package/gsd-core/workflows/health.md +73 -4
  200. package/gsd-core/workflows/import.md +4 -4
  201. package/gsd-core/workflows/ingest-docs.md +5 -5
  202. package/gsd-core/workflows/mvp-phase.md +6 -3
  203. package/gsd-core/workflows/new-milestone.md +14 -9
  204. package/gsd-core/workflows/new-project.md +14 -14
  205. package/gsd-core/workflows/next.md +12 -0
  206. package/gsd-core/workflows/plan-phase.md +41 -17
  207. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  208. package/gsd-core/workflows/progress.md +34 -6
  209. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
  210. package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
  211. package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
  212. package/gsd-core/workflows/quick.md +35 -15
  213. package/gsd-core/workflows/review.md +26 -5
  214. package/gsd-core/workflows/secure-phase.md +1 -1
  215. package/gsd-core/workflows/session-report.md +2 -1
  216. package/gsd-core/workflows/settings.md +66 -2
  217. package/gsd-core/workflows/ship.md +104 -44
  218. package/gsd-core/workflows/spec-phase.md +30 -12
  219. package/gsd-core/workflows/sync-skills.md +63 -8
  220. package/gsd-core/workflows/transition.md +46 -11
  221. package/gsd-core/workflows/ui-phase.md +5 -5
  222. package/gsd-core/workflows/ui-review.md +2 -2
  223. package/gsd-core/workflows/update.md +1 -1
  224. package/gsd-core/workflows/validate-phase.md +1 -1
  225. package/gsd-core/workflows/verify-work.md +9 -7
  226. package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
  227. package/hooks/dist/gsd-check-update-worker.js +56 -13
  228. package/hooks/dist/gsd-check-update.js +19 -1
  229. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  230. package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
  231. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  232. package/hooks/dist/gsd-prompt-guard.js +21 -20
  233. package/hooks/dist/gsd-read-injection-scanner.js +38 -24
  234. package/hooks/dist/gsd-statusline.js +18 -0
  235. package/hooks/dist/gsd-update-banner.js +22 -1
  236. package/hooks/dist/gsd-workflow-guard.js +134 -36
  237. package/hooks/dist/lib/git-cmd.js +92 -59
  238. package/hooks/dist/lib/injection-patterns.js +45 -0
  239. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  240. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  241. package/hooks/gsd-agent-isolation-guard.js +103 -14
  242. package/hooks/gsd-check-update-worker.js +56 -13
  243. package/hooks/gsd-check-update.js +19 -1
  244. package/hooks/gsd-cursor-pre-tool.js +0 -3
  245. package/hooks/gsd-cursor-subagent-start.js +77 -2
  246. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  247. package/hooks/gsd-prompt-guard.js +21 -20
  248. package/hooks/gsd-read-injection-scanner.js +38 -24
  249. package/hooks/gsd-statusline.js +18 -0
  250. package/hooks/gsd-update-banner.js +22 -1
  251. package/hooks/gsd-workflow-guard.js +134 -36
  252. package/hooks/lib/git-cmd.js +92 -59
  253. package/hooks/lib/injection-patterns.js +45 -0
  254. package/hooks/lib/isolation-deny-reason.js +39 -0
  255. package/hooks/lib/isolation-sentinel.js +9 -0
  256. package/package.json +21 -9
  257. package/pi/gsd.cjs +19 -5
  258. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  259. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  260. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  261. package/scripts/changeset/lint.cjs +60 -5
  262. package/scripts/check-alias-drift.cjs +7 -43
  263. package/scripts/check-contract-drift.cjs +297 -0
  264. package/scripts/ci-test-scope.cjs +19 -2
  265. package/scripts/command-contract-helpers.cjs +903 -1
  266. package/scripts/gen-adr-index.cjs +728 -38
  267. package/scripts/gen-capability-registry.cjs +3 -15
  268. package/scripts/gen-context-index.cjs +2 -11
  269. package/scripts/gen-health-docs.cjs +390 -0
  270. package/scripts/gen-inventory-manifest.cjs +50 -4
  271. package/scripts/gen-loop-host-contract.cjs +4 -24
  272. package/scripts/gen-registry.cjs +3 -14
  273. package/scripts/lib/alias-drift-families.cjs +46 -0
  274. package/scripts/lib/drift-scan.cjs +278 -0
  275. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  276. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  277. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  278. package/scripts/lint-canary-version-leak.cjs +73 -0
  279. package/scripts/lint-command-contract.cjs +96 -13
  280. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  281. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  282. package/scripts/lint-default-flip-documentation.cjs +193 -0
  283. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  284. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  285. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  286. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  287. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  288. package/scripts/lint-milestone-window-drift.cjs +468 -0
  289. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  290. package/scripts/lint-plan-count-drift.cjs +318 -0
  291. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  292. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  293. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  294. package/scripts/lint-regression-test-names.cjs +15 -13
  295. package/scripts/lint-removed-but-needed.cjs +320 -0
  296. package/scripts/lint-state-field-drift.cjs +805 -0
  297. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  298. package/scripts/lint-test-file-count.allowlist.json +21 -10
  299. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  300. package/scripts/lint-vendored-deps.cjs +124 -0
  301. package/scripts/pr-changed-files.cjs +63 -0
  302. package/scripts/pr-template-policy.cjs +14 -4
  303. package/scripts/prompt-injection-scan.sh +25 -0
  304. package/scripts/require-issue-link-policy.cjs +192 -0
  305. package/scripts/state-write-path-drift-baseline.json +19 -0
  306. package/scripts/sync-runtime-launcher.cjs +2 -4
  307. package/skills/gsd-autonomous/SKILL.md +0 -1
  308. package/skills/gsd-code-review/SKILL.md +1 -1
  309. package/skills/gsd-execute-phase/SKILL.md +1 -2
  310. package/skills/gsd-map-codebase/SKILL.md +1 -1
  311. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  312. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  313. package/skills/gsd-new-milestone/SKILL.md +1 -1
  314. package/skills/gsd-next/SKILL.md +0 -1
  315. package/skills/gsd-plan-phase/SKILL.md +0 -1
  316. package/skills/gsd-progress/SKILL.md +0 -1
  317. package/skills/gsd-quick/SKILL.md +1 -1
  318. package/skills/gsd-review-backlog/SKILL.md +2 -1
  319. package/skills/gsd-stats/SKILL.md +0 -1
  320. package/skills/gsd-verify-work/SKILL.md +1 -1
  321. package/vscode/package.json +1 -1
  322. package/gsd-core/workflows/discovery-phase.md +0 -298
  323. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  324. package/gsd-core/workflows/verify-phase.md +0 -574
  325. package/scripts/affected-tests-lib.cjs +0 -554
  326. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  327. package/scripts/run-affected-tests.cjs +0 -7
  328. package/scripts/run-tests.cjs +0 -1051
@@ -0,0 +1,381 @@
1
+ "use strict";
2
+ /**
3
+ * installed-surface-resolver.cts — Installed Surface Resolver Module (#2872,
4
+ * ADR-2866, Phase 3 — governed by
5
+ * `.gsd/phase/feat-2872-manifest-scope-runtime/40-design.md`).
6
+ *
7
+ * `resolveInstalledSurfaces(runtime?, opts?)` answers a question no existing
8
+ * module answers: "which surfaces actually exist on THIS machine, across
9
+ * BOTH install scopes, right now?" `capability-state.cts` answers "which
10
+ * capabilities are enabled in THIS config dir" — a different question at a
11
+ * different altitude. This module is a new leaf: it reads two scopes at
12
+ * once, composing three already-shipped, already-tested pieces
13
+ * (`resolveScope`, Phase 1; `resolveTriggerSurface`, Phase 2; and
14
+ * `readInstallManifest`, widened in this same phase) rather than
15
+ * re-implementing any of their rules.
16
+ *
17
+ * ── Probe-not-declared keying ───────────────────────────────────────────────
18
+ * A scope RECORD is keyed by the PROBED scope (`resolveScope`'s own `id`),
19
+ * never by what the manifest itself claims. `manifestVersion`/`runtime`/
20
+ * `scope` recorded inside a v2 manifest are corroboration, not identity: a
21
+ * manifest copied between config dirs, or a `--config-dir` install, must
22
+ * still be reported at the scope this machine actually resolves to.
23
+ *
24
+ * ── Report, don't correct ───────────────────────────────────────────────────
25
+ * When a declared `runtime`/`scope` disagrees with the probe, this module
26
+ * reports the mismatch (`declaredScopeMatchesProbe: false` /
27
+ * `declaredRuntimeMatchesProbe: false`) and otherwise proceeds exactly as it
28
+ * would for an undeclared (v1) manifest. It never silently substitutes the
29
+ * declared value for the probed one, and never throws on a mismatch — see
30
+ * B-row "Postel's Law" discussion in the design doc.
31
+ *
32
+ * ── One trigger call over installed scopes only ─────────────────────────────
33
+ * `resolveTriggerSurface` is called AT MOST once per runtime, with the
34
+ * scopes that are actually installed on this machine (per manifest
35
+ * presence — never per the new corroboration fields, which is what keeps a
36
+ * v1-only install fully functional with no reinstall required). A
37
+ * hypothetical "what if both scopes were installed" answer is deliberately
38
+ * not offered; #2218 is a fact about THIS machine, not a simulation.
39
+ *
40
+ * ── Installed-ness is manifest PRESENCE, never the new fields ───────────────
41
+ * `installed` is `manifestVersion !== null`. A v1 manifest (no
42
+ * `manifestVersion` key at all, `manifestVersion: 1` after normalization) is
43
+ * a correct manifest written by an older GSD — never a broken one, never
44
+ * grounds for `installed: false`, a warning, or a reinstall prompt.
45
+ *
46
+ * ── Stems come from the manifest, not the source tree ───────────────────────
47
+ * `resolveTriggerSurface` needs `stems`. They are derived from the
48
+ * INSTALLED manifest's own `files` keys (see the private stem-derivation
49
+ * helpers below), never from a roster read of the source tree — a global
50
+ * `claude` install ships no `commands/gsd` source, so a roster read would
51
+ * return `[]` in exactly the configuration #2218 is about. The derivation
52
+ * consumes `isNamespacedByDir` and `composeCommandFilename`
53
+ * (`runtime-artifact-layout.cjs`, #2871 Phase 2's exported helpers) rather
54
+ * than re-deriving either rule as a fourth independent copy.
55
+ */
56
+ var __importDefault = (this && this.__importDefault) || function (mod) {
57
+ return (mod && mod.__esModule) ? mod : { "default": mod };
58
+ };
59
+ Object.defineProperty(exports, "__esModule", { value: true });
60
+ exports.resolveInstalledSurfaces = resolveInstalledSurfaces;
61
+ const node_fs_1 = __importDefault(require("node:fs"));
62
+ const node_path_1 = __importDefault(require("node:path"));
63
+ const install_scope_cjs_1 = require("./install-scope.cjs");
64
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
65
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
66
+ const runtimeArtifactLayoutMod = require("./runtime-artifact-layout.cjs");
67
+ const { resolveRuntimeArtifactLayout, resolveRuntimeArtifactLayoutFromRegistry, resolveTriggerSurface, isNamespacedByDir, composeCommandFilename, } = runtimeArtifactLayoutMod;
68
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
69
+ const installerMigrationsMod = require("./installer-migrations.cjs");
70
+ const { readInstallManifest, MANIFEST_NAME } = installerMigrationsMod;
71
+ // In .cts (CommonJS output) files, `require` is available as a global.
72
+ const _require = require;
73
+ // ── Internals ───────────────────────────────────────────────────────────
74
+ /**
75
+ * A trigger stem is a kebab-case token and nothing else. Verified against the
76
+ * real roster: all 71 `commands/gsd/*.md` stems match this, so it has no false
77
+ * negatives today.
78
+ *
79
+ * This is a SECURITY boundary, not cosmetics. A stem is derived from a manifest
80
+ * key — attacker-influenceable text, since a project-local
81
+ * `gsd-file-manifest.json` lives inside a repository a user may merely have
82
+ * cloned — and Phase 4 (#2873) renders it back to the user as `/gsd-<stem>`.
83
+ * Without this, `skills/gsd-../../../x` yields the stem `..`, and control
84
+ * characters, newlines, ANSI escapes and RTL-override codepoints all survive
85
+ * into that rendered trigger. The `commands` branch happened to be protected by
86
+ * its `composeCommandFilename` round-trip; the `skills` branch had no
87
+ * equivalent, so the rule is stated once here and applied to both.
88
+ */
89
+ const SAFE_STEM = /^[a-z0-9][a-z0-9-]*$/;
90
+ function getDefaultRuntimeRegistry() {
91
+ return _require('./capability-registry.cjs');
92
+ }
93
+ /** C7: every registered runtime id, sorted for determinism. Reads
94
+ * `opts.registry` when provided (so a sweep is assertable without the real
95
+ * capability registry), else the real one. */
96
+ function listRegisteredRuntimeIds(opts) {
97
+ const registry = opts.registry ?? getDefaultRuntimeRegistry();
98
+ return Object.keys(registry.runtimes).sort();
99
+ }
100
+ /**
101
+ * D — the stem inverse for one `commands`/`skills` layout kind entry.
102
+ * Consumes `isNamespacedByDir` (to decide the commands filename shape) and
103
+ * `composeCommandFilename` (to VERIFY a candidate stem round-trips to the
104
+ * exact filename seen) rather than re-deriving either rule locally (design
105
+ * "Rejected" #6). Manifest keys are normalized with an unconditional
106
+ * `.replace(/\\/g, '/')` (D7) — never gated on `path.sep` — matching this
107
+ * repo's recorded path-separator-normalization defect class.
108
+ */
109
+ function deriveStemsForKindEntry(kind, destSubpath, prefix, fileKeys) {
110
+ const destSubpathNorm = (0, shell_command_projection_cjs_1.posixNormalize)(destSubpath);
111
+ const boundary = `${destSubpathNorm}/`;
112
+ const namespacedByDir = isNamespacedByDir(kind, destSubpath, prefix);
113
+ const stems = new Set();
114
+ for (const rawKey of fileKeys) {
115
+ const key = rawKey.replace(/\\/g, '/');
116
+ if (!key.startsWith(boundary))
117
+ continue; // D4: key not under this declared subpath
118
+ const rest = key.slice(boundary.length);
119
+ if (rest === '')
120
+ continue;
121
+ const segments = rest.split('/');
122
+ if (kind === 'skills') {
123
+ const dirSegment = segments[0];
124
+ if (!dirSegment.startsWith(prefix))
125
+ continue; // D5: subpath matches, prefix does not
126
+ const stem = dirSegment.slice(prefix.length);
127
+ if (stem === '')
128
+ continue; // D7/B7: empty stem never emitted
129
+ if (!SAFE_STEM.test(stem))
130
+ continue; // security: reject anything but a kebab-case token
131
+ stems.add(stem); // D6: several files under one dir -> one stem
132
+ continue;
133
+ }
134
+ // commands: exactly one path segment — the composed filename itself.
135
+ if (segments.length !== 1)
136
+ continue;
137
+ const filename = segments[0];
138
+ if (!filename.endsWith('.md'))
139
+ continue;
140
+ const base = filename.slice(0, -3);
141
+ const candidateStem = namespacedByDir
142
+ ? base
143
+ : (base.startsWith(prefix) ? base.slice(prefix.length) : '');
144
+ if (candidateStem === '')
145
+ continue;
146
+ if (composeCommandFilename(namespacedByDir, prefix, candidateStem) !== filename)
147
+ continue;
148
+ if (!SAFE_STEM.test(candidateStem))
149
+ continue; // security: reject anything but a kebab-case token
150
+ stems.add(candidateStem);
151
+ }
152
+ return [...stems];
153
+ }
154
+ /** D — full stem set for one scope: every `commands`/`skills` layout kind
155
+ * entry, unioned. Empty `files` (C13) short-circuits to `[]` without
156
+ * resolving a layout at all. */
157
+ function deriveStemsFromManifest(runtime, scopeId, configHome, files, opts) {
158
+ const fileKeys = Object.keys(files);
159
+ if (fileKeys.length === 0)
160
+ return [];
161
+ const layout = opts.registry !== undefined
162
+ ? resolveRuntimeArtifactLayoutFromRegistry(opts.registry, runtime, configHome, scopeId)
163
+ : resolveRuntimeArtifactLayout(runtime, configHome, scopeId);
164
+ const stems = new Set();
165
+ for (const kindEntry of layout.kinds) {
166
+ if (kindEntry.kind !== 'commands' && kindEntry.kind !== 'skills')
167
+ continue; // excludes agents/kimi-agents (D4)
168
+ for (const stem of deriveStemsForKindEntry(kindEntry.kind, kindEntry.destSubpath, kindEntry.prefix, fileKeys)) {
169
+ stems.add(stem);
170
+ }
171
+ }
172
+ return [...stems].sort();
173
+ }
174
+ /**
175
+ * True when `p` is a symlink. An `lstatSync` throw (ENOENT — nothing at this
176
+ * path) is NOT evidence of a symlink; it is treated as "not a symlink" here
177
+ * and left for `readManifest` to classify (it already owns the absent-file
178
+ * case, per C14 above).
179
+ */
180
+ function isSymlinkPath(p, lstatSync) {
181
+ try {
182
+ return lstatSync(p).isSymbolicLink();
183
+ }
184
+ catch {
185
+ return false;
186
+ }
187
+ }
188
+ /**
189
+ * The shared "not installed" degraded shape (C14's EACCES path, and this
190
+ * phase's new symlink-guard path). A FACTORY, not a module-level constant
191
+ * object: a `const` object spread at each return site would still share the
192
+ * same `stems` ARRAY reference across every call (`...` shallow-copies the
193
+ * object but not the array a property points at), which would violate this
194
+ * module's own "builds fresh arrays/objects on every call" contract (C15) —
195
+ * a caller mutating one degraded record's `stems` must never be visible on
196
+ * another's.
197
+ */
198
+ function notInstalledScopeRecordFields() {
199
+ return {
200
+ installed: false,
201
+ manifestVersion: null,
202
+ declaredRuntime: null,
203
+ declaredScope: null,
204
+ declaredScopeMatchesProbe: null,
205
+ declaredRuntimeMatchesProbe: null,
206
+ stems: [],
207
+ };
208
+ }
209
+ /**
210
+ * Build one scope's record. `resolvedConfigHome` has already been probed
211
+ * successfully by the time this is called (a `resolveScope` `TypeError` is
212
+ * handled by the caller, per C8 — it is never this function's concern).
213
+ *
214
+ * Two distinct failure modes are handled with two distinct, narrowly-scoped
215
+ * try/catches — NOT one catch-all around the whole function:
216
+ *
217
+ * - The manifest READ (`readManifest`) is the only thing that can justify
218
+ * `installed: false` (C14, e.g. EACCES). A failure here means "we could
219
+ * not even tell whether this scope is installed".
220
+ * - The stem DERIVATION (`deriveStemsFromManifest`, which resolves a
221
+ * runtime artifact layout) is a separate concern. A `TypeError` thrown
222
+ * while deriving stems is a layout-lookup failure, not evidence the
223
+ * manifest is absent — the manifest was already read successfully, so
224
+ * `installed` and every declared/*MatchesProbe field stay exactly as the
225
+ * manifest reported. Conflating the two would report a genuinely
226
+ * installed runtime as `installed: false`, hiding a real install from
227
+ * #2218 shadow detection precisely when this module exists to surface it.
228
+ */
229
+ function buildScopeRecord(runtime, scopeId, resolvedConfigHome, opts) {
230
+ // Hardening requirement 2 (#2873 design doc, "Hardening requirements
231
+ // claimed from #2873's comment"): `readInstallManifest` -> `readJsonIfPresent`
232
+ // uses `existsSync` + `readFileSync` and therefore FOLLOWS symlinks, and
233
+ // this module resolves `local` against `process.cwd()` — a directory that,
234
+ // as of this phase, becomes reachable from an arbitrary cloned repository
235
+ // (this is the same phase that makes the local scope's manifest a first
236
+ // read target, not merely a write target). The in-tree precedent is
237
+ // `getAgentsDir` (`agent-install-check.cts`), which probes with
238
+ // `fs.lstatSync(...).isDirectory()`/`.isFile()` and deliberately does not
239
+ // follow. #2872 left this resolver's read un-guarded only because the path
240
+ // had zero callers at the time; refusing to follow a symlinked config dir
241
+ // or manifest here closes that asymmetry rather than carrying it forward.
242
+ // Degrading to `installed: false` reuses the SAME shape the EACCES catch
243
+ // below already returns — no new failure shape is introduced.
244
+ const lstatSync = opts.lstatSync ?? node_fs_1.default.lstatSync;
245
+ const manifestPath = node_path_1.default.join(resolvedConfigHome, MANIFEST_NAME);
246
+ if (isSymlinkPath(resolvedConfigHome, lstatSync) || isSymlinkPath(manifestPath, lstatSync)) {
247
+ return { scope: scopeId, configHome: resolvedConfigHome, ...notInstalledScopeRecordFields() };
248
+ }
249
+ let manifest;
250
+ try {
251
+ const readManifest = opts.readManifest ?? readInstallManifest;
252
+ manifest = readManifest(resolvedConfigHome);
253
+ }
254
+ catch {
255
+ // C14: manifest read/probe failure degrades to not-installed, never throws.
256
+ // Deliberately a BARE catch, unlike the stem-derivation catch below: any
257
+ // read failure at all (EACCES, ENOENT-after-race, a corrupt filesystem)
258
+ // legitimately means "cannot tell whether installed" (design row C14), so
259
+ // there is no error TYPE here that should instead propagate.
260
+ return { scope: scopeId, configHome: resolvedConfigHome, ...notInstalledScopeRecordFields() };
261
+ }
262
+ const installed = manifest.manifestVersion !== null; // C9: presence, never the new fields
263
+ const declaredRuntime = manifest.runtime;
264
+ const declaredScope = manifest.scope;
265
+ const declaredRuntimeMatchesProbe = declaredRuntime === null ? null : declaredRuntime === runtime;
266
+ const declaredScopeMatchesProbe = declaredScope === null ? null : declaredScope === scopeId;
267
+ let stems = [];
268
+ if (installed) {
269
+ try {
270
+ stems = deriveStemsFromManifest(runtime, scopeId, resolvedConfigHome, manifest.files, opts);
271
+ }
272
+ catch (error) {
273
+ if (!(error instanceof TypeError))
274
+ throw error;
275
+ // A layout-lookup failure means "we could not enumerate this scope's
276
+ // triggers", NOT "this scope is not installed" — the manifest read
277
+ // already succeeded above, so `installed` and the declared fields
278
+ // stay as reported. Conflating the two would hide a real install.
279
+ // Anything that is not the expected `TypeError` (a genuine bug in
280
+ // `deriveStemsFromManifest`/`resolveRuntimeArtifactLayout`) is
281
+ // rethrown rather than silently degrading to `stems: []` — this
282
+ // matches `resolveInstalledSurfaces`'s own `TypeError` narrowing
283
+ // below, so the two catches cannot drift apart.
284
+ stems = [];
285
+ }
286
+ }
287
+ return {
288
+ scope: scopeId,
289
+ configHome: resolvedConfigHome,
290
+ installed,
291
+ manifestVersion: manifest.manifestVersion,
292
+ declaredRuntime,
293
+ declaredScope,
294
+ declaredScopeMatchesProbe,
295
+ declaredRuntimeMatchesProbe,
296
+ stems,
297
+ };
298
+ }
299
+ /**
300
+ * Resolve one runtime's full installed surface. A `resolveScope` `TypeError`
301
+ * (unknown runtime, or `configHome.kind === 'none'`, e.g. vscode) propagates
302
+ * from here uncaught — `resolveInstalledSurfaces` decides whether that
303
+ * means "skip" (C7 sweep) or "propagate" (explicit single-runtime ask, C8).
304
+ */
305
+ function resolveOneRuntime(runtime, opts) {
306
+ const scopes = install_scope_cjs_1.SCOPE_ORDER.map((scopeId) => {
307
+ const resolved = (0, install_scope_cjs_1.resolveScope)({
308
+ id: scopeId,
309
+ runtime,
310
+ env: opts.env,
311
+ home: opts.home,
312
+ existsSync: opts.existsSync,
313
+ cwd: opts.cwd,
314
+ });
315
+ return buildScopeRecord(runtime, scopeId, resolved.configHome, opts);
316
+ });
317
+ // C12: dedupe by resolved configHome BEFORE the trigger call, keeping the
318
+ // higher-ranked (global, first in SCOPE_ORDER) scope. Both scope RECORDS
319
+ // above are unaffected — only the scope list handed to resolveTriggerSurface
320
+ // is deduped.
321
+ const seenConfigHomes = new Set();
322
+ const triggerScopeIds = [];
323
+ for (const record of scopes) {
324
+ if (!record.installed)
325
+ continue;
326
+ if (seenConfigHomes.has(record.configHome))
327
+ continue;
328
+ seenConfigHomes.add(record.configHome);
329
+ triggerScopeIds.push(record.scope);
330
+ }
331
+ const stemUnion = new Set();
332
+ for (const scopeId of triggerScopeIds) {
333
+ const record = scopes.find((s) => s.scope === scopeId);
334
+ for (const stem of record?.stems ?? [])
335
+ stemUnion.add(stem);
336
+ }
337
+ const triggers = triggerScopeIds.length > 0
338
+ ? resolveTriggerSurface(runtime, triggerScopeIds, {
339
+ stems: [...stemUnion].sort(),
340
+ registry: opts.registry,
341
+ })
342
+ : [];
343
+ return { runtime, scopes, triggers };
344
+ }
345
+ /**
346
+ * Read-only. Probes both install scopes for a runtime (or every registered
347
+ * runtime, sorted by id, when `runtime` is omitted), reads each scope's
348
+ * manifest, and resolves the trigger surface across the scopes that are
349
+ * actually installed on this machine. See the module-level comment for the
350
+ * non-obvious choices (probe-not-declared keying, report-don't-correct, one
351
+ * trigger call over installed scopes only).
352
+ *
353
+ * Pure with respect to caller-visible state: builds fresh arrays/objects on
354
+ * every call (C15) and performs no writes. Filesystem reads happen only via
355
+ * `resolveScope`'s injected `existsSync`/`env`/`home`/`cwd` and via
356
+ * `readManifest` (default: `readInstallManifest`).
357
+ *
358
+ * @throws {TypeError} when `runtime` is given explicitly and it is unknown,
359
+ * or has no installable config directory (`configHome.kind === 'none'`,
360
+ * e.g. vscode) — same contract `resolveScope` throws. In the all-runtimes
361
+ * sweep (`runtime` omitted), a runtime that would throw this same
362
+ * `TypeError` is skipped instead, so one non-installable runtime cannot
363
+ * kill the sweep (C7/C8). Any other error type is never swallowed here.
364
+ */
365
+ function resolveInstalledSurfaces(runtime, opts = {}) {
366
+ if (typeof runtime === 'string') {
367
+ return [resolveOneRuntime(runtime, opts)];
368
+ }
369
+ const results = [];
370
+ for (const runtimeId of listRegisteredRuntimeIds(opts)) {
371
+ try {
372
+ results.push(resolveOneRuntime(runtimeId, opts));
373
+ }
374
+ catch (error) {
375
+ if (error instanceof TypeError)
376
+ continue; // C8: sweep skips, never dies
377
+ throw error;
378
+ }
379
+ }
380
+ return results;
381
+ }
@@ -16,51 +16,61 @@ 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
  }
45
- /**
46
- * Copy a managed path for the rollback snapshot or the user-facing backup,
47
- * WITHOUT dereferencing a symlink.
48
- *
49
- * `fs.copyFileSync` follows symlinks, so a managed path that has been replaced
50
- * by a link (tampering, or an unexpected user layout) would have had the
51
- * LINK TARGET's bytes copied into `gsd-migration-journal/…-backups/` — e.g. a
52
- * `gsd.cjs` symlinked at `~/.ssh/id_rsa` would land that key's contents in the
53
- * backup tree. Nothing GSD installs is ever a symlink, so the faithful snapshot
54
- * of a symlinked managed path is the link itself: recreating it preserves
55
- * rollback fidelity (restore re-creates the same link) while never reading the
56
- * referent. Deletion was already safe — `fs.rmSync` unlinks the link, never the
57
- * target.
58
- *
59
- * Windows note: `fs.symlinkSync` can throw EPERM for unprivileged users. That
60
- * surfaces as an apply failure and triggers the normal rollback path, which is
61
- * the correct outcome — refusing to proceed beats silently copying referent
62
- * bytes.
63
- */
64
74
  /**
65
75
  * Evaluate and, if safe, perform a `remove-empty-dir` action against `fullPath`.
66
76
  *
@@ -133,23 +143,54 @@ function evaluateRemoveEmptyDir(configDir, fullPath) {
133
143
  return 'left-in-place';
134
144
  }
135
145
  }
146
+ /**
147
+ * Copy a managed path for the rollback snapshot or the user-facing backup,
148
+ * WITHOUT dereferencing a symlink.
149
+ *
150
+ * `fs.copyFileSync` follows symlinks, so a managed path that has been replaced
151
+ * by a link (tampering, or an unexpected user layout) would have had the
152
+ * LINK TARGET's bytes copied into `gsd-migration-journal/…-backups/` — e.g. a
153
+ * `gsd.cjs` symlinked at `~/.ssh/id_rsa` would land that key's contents in the
154
+ * backup tree. Nothing GSD installs is ever a symlink, so the faithful snapshot
155
+ * of a symlinked managed path is the link itself: recreating it preserves
156
+ * rollback fidelity (restore re-creates the same link) while never reading the
157
+ * referent. Deletion was already safe — `fs.rmSync` unlinks the link, never the
158
+ * target.
159
+ *
160
+ * Windows note: `fs.symlinkSync` can throw EPERM for unprivileged users. That
161
+ * surfaces as an apply failure and triggers the normal rollback path, which is
162
+ * the correct outcome — refusing to proceed beats silently copying referent
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).
172
+ */
136
173
  function copyPreservingSymlink(srcPath, destPath) {
137
- if (node_fs_1.default.lstatSync(srcPath).isSymbolicLink()) {
174
+ if (installFs().lstatSync(srcPath).isSymbolicLink()) {
138
175
  // symlinkSync fails with EEXIST on an occupied path, so clear it first.
139
176
  // Scoped to this branch on purpose: the regular-file path below keeps
140
177
  // copyFileSync's overwrite-in-place, so a mid-restore failure cannot leave
141
178
  // the destination destroyed.
142
- node_fs_1.default.rmSync(destPath, { force: true });
143
- 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);
144
181
  return;
145
182
  }
146
- node_fs_1.default.copyFileSync(srcPath, destPath);
183
+ installFs().copyFileSync(srcPath, destPath);
147
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.
148
189
  function readJsonIfPresent(filePath, fallback) {
149
- if (!node_fs_1.default.existsSync(filePath))
190
+ if (!installFs().existsSync(filePath))
150
191
  return fallback;
151
192
  try {
152
- return JSON.parse(node_fs_1.default.readFileSync(filePath, 'utf8'));
193
+ return JSON.parse(installFs().readFileSync(filePath, 'utf8'));
153
194
  }
154
195
  catch (error) {
155
196
  if (fallback === STRICT_JSON) {
@@ -158,17 +199,81 @@ function readJsonIfPresent(filePath, fallback) {
158
199
  return fallback;
159
200
  }
160
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
+ }
161
247
  function readInstallManifest(configDir) {
162
248
  const manifest = readJsonIfPresent(node_path_1.default.join(configDir, MANIFEST_NAME), null);
163
- if (!manifest || typeof manifest !== 'object') {
164
- 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
+ };
165
266
  }
166
267
  const m = manifest;
268
+ const rawRuntime = m.runtime;
167
269
  return {
168
270
  version: typeof m.version === 'string' ? m.version : null,
169
271
  timestamp: typeof m.timestamp === 'string' ? m.timestamp : null,
170
272
  mode: typeof m.mode === 'string' ? m.mode : null,
171
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,
172
277
  };
173
278
  }
174
279
  function readInstallState(configDir) {
@@ -235,7 +340,7 @@ function classifyArtifact(configDir, relPath, manifest) {
235
340
  const normalized = normalizeRelPath(relPath);
236
341
  const originalHash = manifest.files[normalized] || null;
237
342
  const fullPath = node_path_1.default.join(configDir, normalized);
238
- if (!node_fs_1.default.existsSync(fullPath)) {
343
+ if (!installFs().existsSync(fullPath)) {
239
344
  return { classification: originalHash ? 'managed-missing' : 'missing', originalHash, currentHash: null };
240
345
  }
241
346
  const currentHash = sha256File(fullPath);
@@ -937,8 +1042,10 @@ module.exports = {
937
1042
  acquireInstallMigrationLock,
938
1043
  applyInstallerMigrationPlan,
939
1044
  classifyArtifact,
1045
+ copyPreservingSymlink,
940
1046
  discoverInstallerMigrations,
941
1047
  evaluateRemoveEmptyDir,
1048
+ MANIFEST_SCHEMA_VERSION,
942
1049
  migrationChecksum,
943
1050
  planInstallerMigrations,
944
1051
  readInstallManifest,
@@ -186,11 +186,21 @@ const ERROR_REASON = Object.freeze({
186
186
  PHASE_VERIFICATION_INCOMPLETE: 'phase_verification_incomplete',
187
187
  PHASE_PLAN_COVERAGE_INCOMPLETE: 'phase_plan_coverage_incomplete',
188
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',
189
195
  // graphify
190
196
  GRAPHIFY_NO_GRAPH: 'graphify_no_graph',
191
197
  GRAPHIFY_INVALID_QUERY: 'graphify_invalid_query',
192
198
  // hooks
193
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',
194
204
  // security-scan
195
205
  SECURITY_SCAN_FAILED: 'security_scan_failed',
196
206
  // generic
@@ -26,6 +26,7 @@ exports.stripTaggedBlocks = stripTaggedBlocks;
26
26
  exports.replaceSection = replaceSection;
27
27
  exports.withSection = withSection;
28
28
  exports.deleteSection = deleteSection;
29
+ const pattern_cjs_1 = require("./pattern.cjs");
29
30
  // ─── stripFencedCode ──────────────────────────────────────────────────────────
30
31
  /**
31
32
  * CommonMark-correct fenced-code-block stripper.
@@ -792,7 +793,7 @@ function extractTaggedBlocks(content, tagName, allowAttributes = false) {
792
793
  * open>` marks the ACTIVE milestone and must be preserved, not stripped (#557).
793
794
  */
794
795
  function taggedBlockPattern(tagName, flags, allowAttributes) {
795
- const esc = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
796
+ const esc = (0, pattern_cjs_1.escapeRegex)(tagName);
796
797
  const open = allowAttributes ? `<${esc}(?:\\s[^>]{0,1000})?>` : `<${esc}>`;
797
798
  const boundary = allowAttributes ? `<${esc}[\\s>]` : `<${esc}>`;
798
799
  return new RegExp(`${open}((?:(?!${boundary})[\\s\\S])*?)</${esc}>`, flags);