@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,262 @@
1
+ 'use strict';
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ /**
6
+ * Install Fs Adapter — #2874 (epic #2866 Phase 5), governed by ADR-58.
7
+ *
8
+ * Narrow, enumerated fs seam for the install/staging call tree rooted at
9
+ * `installRuntimeArtifacts` (install-engine.cts): layout source-root
10
+ * resolution (runtime-artifact-layout.cts), profile staging
11
+ * (install-profiles.cts), content-rewrite passes
12
+ * (runtime-artifact-conversion.cts), the CommonJS module-type marker
13
+ * (commonjs-marker.cts), and the two installer-migrations entry points this
14
+ * call tree reaches — `readInstallManifest` / `classifyArtifact`
15
+ * (installer-migrations.cts). Extends the `deps` bag precedent already
16
+ * established by `createRuntimeArtifactInstallPlan`
17
+ * (runtime-artifact-install-plan.cts:155) — this is NOT a general-purpose
18
+ * `node:fs` wrapper (40-design.md "Rejected" #1): only the operations this
19
+ * call tree actually performs are enumerated below. installer-migrations.cts
20
+ * is ~1200 lines covering migration planning/apply/rollback/locking/journal
21
+ * machinery unrelated to `installRuntimeArtifacts` — only its two reachable
22
+ * entry points (and `sha256File`, their shared hashing helper — still raw-fd
23
+ * streaming via `openSync`/`readSync`/`closeSync`, now routed through this
24
+ * seam instead of calling `node:fs` directly, so large-file hashing never
25
+ * buffers a whole file through the injected adapter either) are routed; the
26
+ * rest of that file is untouched, deliberately, because it is off this call
27
+ * tree.
28
+ *
29
+ * ─────────────────────────────────────────────────────────────────────────
30
+ * DELIVERY MECHANISM — ambient, not threaded (read this before adding a call
31
+ * site that needs a different adapter mid-call)
32
+ * ─────────────────────────────────────────────────────────────────────────
33
+ *
34
+ * A single mutable "current adapter" (`current`, below) is swapped for the
35
+ * duration of one synchronous `installRuntimeArtifacts` call via
36
+ * `withInstallFs`, rather than a `deps` parameter threaded through every
37
+ * function on the call tree. The rejected alternative was threading: it
38
+ * would touch signatures across `install-profiles.cts` (5 staging
39
+ * functions), the 3000+-line `runtime-artifact-conversion.cts` (rewrite
40
+ * passes), `commonjs-marker.cts`, and `installer-migrations.cts` — a dozen+
41
+ * unrelated call sites — for no behavioral gain over an ambient swap, and
42
+ * `createRuntimeArtifactInstallPlan`'s own `deps` bag (the precedent this
43
+ * seam extends) does not reach that deep either. Every fs-touching site on
44
+ * the call tree reads the active adapter via `installFs()` instead of
45
+ * importing `node:fs` directly.
46
+ *
47
+ * SYNCHRONOUS-ONLY / RE-ENTRANCY ASSUMPTION (load-bearing, not incidental):
48
+ * every method on this seam is `*Sync`, and `installRuntimeArtifacts` never
49
+ * awaits mid-call — the whole call tree from the top-level `withInstallFs`
50
+ * wrap down to the last `fs` touch runs on one turn of the event loop with
51
+ * no interleaving. That is what makes a single ambient variable safe instead
52
+ * of a race: nothing else can observe or mutate `current` while it is set.
53
+ * This assumption BREAKS if `installRuntimeArtifacts` (or anything it calls)
54
+ * ever becomes `async`, or if two installs run concurrently in the same
55
+ * process (the second `withInstallFs` call would clobber the first's
56
+ * adapter mid-flight) — neither is true today, but a future change that
57
+ * introduces either must revisit this module before trusting it.
58
+ *
59
+ * DEFERRED CLEANUP ACROSS THE RESTORE (#2874 leak-fix): staged directories
60
+ * outlive one `withInstallFs` call — `install-profiles.cts`'s
61
+ * `cleanupStagedSkills` runs later, from a `process.on('exit'/'SIGINT'/…)`
62
+ * handler, by which time `withInstallFs`'s `finally` has already restored
63
+ * `current` back to whatever was active before (real fs, in the top-level
64
+ * case). A cleanup handler that resolved "which adapter do I use" via
65
+ * `installFs()` at CLEANUP time would therefore always see the real adapter,
66
+ * even for a directory that was staged entirely inside a fake-adapter call —
67
+ * performing real filesystem IO on a path that only ever existed in the
68
+ * fake's in-memory store. `install-profiles.cts` avoids this by capturing
69
+ * the adapter OBJECT `installFs()` returns at STAGING (registration) time,
70
+ * keyed by path, in its own `STAGED_DIR_ADAPTERS` map, and replaying that
71
+ * captured object — not the ambient `current` — at cleanup time. A real
72
+ * install's dirs were staged with the real adapter object, so their cleanup
73
+ * is byte-identical to before this fix.
74
+ *
75
+ * `withInstallFs` ALWAYS restores the previous adapter in a `finally`, even
76
+ * on throw — a failed install (or a test that intentionally throws to prove
77
+ * a guard) must never leak a fake adapter into whatever runs next in the
78
+ * same process (e.g. the next `node:test` in a shared worker).
79
+ *
80
+ * ⚠️ PARTIAL-ADAPTER TRAP (#2875 defect fix — CLOSED, not merely documented):
81
+ * `withInstallFs` used to merge the injected `partial` OVER the real adapter
82
+ * (`{ ...REAL_ADAPTER, ...partial }`), so any method the partial did not
83
+ * define silently resolved to REAL `node:fs`. An incomplete fake was not a
84
+ * smaller fake adapter; for the methods it omitted, it WAS the real
85
+ * filesystem — a genuine production hazard, not only a test-authoring
86
+ * footgun: `user-artifact-staging.cts`'s `stageUserArtifacts` calls
87
+ * `installFs().rmSync(entryDir)` unconditionally as its entryDir-clearing
88
+ * step, and a `deps.fs` that omits `rmSync` (an easy oversight — nothing in
89
+ * the type system catches a plain-JS test object missing a key at runtime)
90
+ * silently deleted the real `<configDir>/.gsd-staging/<key>` on disk instead
91
+ * of failing loudly or staying confined to the fake's own store.
92
+ *
93
+ * `withInstallFs` now builds a GUARDED adapter instead: every method the
94
+ * partial does not define — except `realpathSync`, the one method this
95
+ * module's own contract documents as intentionally degrading to real fs
96
+ * (see its own doc comment below) — throws immediately IF CALLED, naming the
97
+ * missing method, instead of silently delegating to real fs. This only
98
+ * changes behavior for the injected-partial path (`withInstallFs(partial,
99
+ * fn)` with a defined `partial`); the no-adapter-injected default (AC4)
100
+ * still resolves to `REAL_ADAPTER` untouched, and any partial fake that
101
+ * genuinely implements every method its code path touches — the documented,
102
+ * intended usage — is byte-identical to before. A test asserting "no real IO
103
+ * happened" against a partial fake can now rely on this guard directly
104
+ * instead of having to hand-poison real `fs` methods itself (see
105
+ * `tests/executed-plan.test.cjs`'s F2 test, which still poisons real `fs` as
106
+ * defense-in-depth belt-and-suspenders, not because this guard is
107
+ * insufficient on its own).
108
+ *
109
+ * ─────────────────────────────────────────────────────────────────────────
110
+ * SECURITY NOTE (40-design.md rows 6/7, H1-H5)
111
+ * ─────────────────────────────────────────────────────────────────────────
112
+ * This module carries NO policy. `hasExistingSymlinkBetween` and
113
+ * `assertDestWithinConfigHome` keep their REFUSAL DECISIONS outside this
114
+ * seam — only their probe calls (existsSync/lstatSync/realpathSync) are
115
+ * routed through it. Injecting a fake adapter can only change what those
116
+ * probes observe for paths that were never real to begin with; it cannot
117
+ * flip the decision logic itself.
118
+ *
119
+ * ─────────────────────────────────────────────────────────────────────────
120
+ * DELIBERATELY NOT ROUTED (see the call sites themselves for the full
121
+ * reasoning — this is the index)
122
+ * ─────────────────────────────────────────────────────────────────────────
123
+ * `findInstallSourceRoot` / `findAgentsSourceRoot`'s walk-up-from-__dirname
124
+ * step (runtime-artifact-layout.cts) locates THIS PACKAGE'S OWN source tree
125
+ * (`commands/gsd/`, `agents/`) — not the install destination — so it uses
126
+ * `fs.statSync` directly, unrouted. A fake destination adapter's store
127
+ * starts empty and is never seeded with real repo paths; routing this walk
128
+ * through it would make every fake-adapter install throw
129
+ * "could not locate commands/gsd", not gracefully stage nothing. See the
130
+ * comment at each function's Step 2 for the full argument.
131
+ *
132
+ * RULE (40-design.md "Known limits"): this seam makes *destination* IO
133
+ * fake-able; package-source IO (this section) stays real by design — the F2
134
+ * test's poison list should be derived FROM that rule, not the reverse, or a
135
+ * future "complete the poison list" edit that adds `statSync` will break a
136
+ * correct `findInstallSourceRoot` for the wrong reason.
137
+ */
138
+ const node_fs_1 = __importDefault(require("node:fs"));
139
+ const node_os_1 = __importDefault(require("node:os"));
140
+ const node_path_1 = __importDefault(require("node:path"));
141
+ const node_crypto_1 = __importDefault(require("node:crypto"));
142
+ const REAL_ADAPTER = {
143
+ existsSync: (p) => node_fs_1.default.existsSync(p),
144
+ mkdirSync: (p, opts) => node_fs_1.default.mkdirSync(p, opts),
145
+ rmSync: (p, opts) => node_fs_1.default.rmSync(p, opts),
146
+ readdirSync: ((p, opts) => (opts ? node_fs_1.default.readdirSync(p, opts) : node_fs_1.default.readdirSync(p))),
147
+ readFileSync: ((p, encoding) => (encoding ? node_fs_1.default.readFileSync(p, encoding) : node_fs_1.default.readFileSync(p))),
148
+ writeFileSync: (p, data, opts) => node_fs_1.default.writeFileSync(p, data, opts),
149
+ copyFileSync: (src, dest) => node_fs_1.default.copyFileSync(src, dest),
150
+ cpSync: (src, dest, opts) => node_fs_1.default.cpSync(src, dest, opts),
151
+ lstatSync: (p) => node_fs_1.default.lstatSync(p),
152
+ realpathSync: (p) => node_fs_1.default.realpathSync(p),
153
+ unlinkSync: (p) => node_fs_1.default.unlinkSync(p),
154
+ rmdirSync: (p) => node_fs_1.default.rmdirSync(p),
155
+ symlinkSync: (target, p) => node_fs_1.default.symlinkSync(target, p),
156
+ readlinkSync: (p) => node_fs_1.default.readlinkSync(p),
157
+ openSync: (p, flags) => node_fs_1.default.openSync(p, flags),
158
+ readSync: (fd, buffer, offset, length, position) => node_fs_1.default.readSync(fd, buffer, offset, length, position),
159
+ closeSync: (fd) => node_fs_1.default.closeSync(fd),
160
+ };
161
+ let current = REAL_ADAPTER;
162
+ /**
163
+ * Returns the fs adapter active for the currently-running install call —
164
+ * the real adapter when no `deps.fs` was injected, or the injected partial
165
+ * adapter merged over the real one (any method it did not override still
166
+ * resolves to real fs — see the module doc's "PARTIAL-ADAPTER TRAP").
167
+ */
168
+ function installFs() {
169
+ return current;
170
+ }
171
+ // Methods allowed to silently degrade to real fs when a partial injected
172
+ // adapter omits them — see the module doc's "PARTIAL-ADAPTER TRAP". This is
173
+ // deliberately a single, narrow, documented exception: `realpathSync`'s own
174
+ // interface doc comment already promises graceful real-fs fallback (the
175
+ // symlink guard treats a `realpathSync` failure as "fall back to the lexical
176
+ // form" anyway, so an absent method degrading the same way changes nothing
177
+ // observable). Every other method is REQUIRED by `InstallFsAdapter`'s own
178
+ // type (no `?`) and now enforces that at runtime too.
179
+ const OPTIONAL_ADAPTER_METHODS = new Set(['realpathSync']);
180
+ /**
181
+ * Build a merged adapter for an injected `partial`: every method `partial`
182
+ * defines is used as-is; `realpathSync` (only) falls back to real fs when
183
+ * absent; every other omitted method becomes a stub that THROWS if actually
184
+ * called, naming the missing method — turning a silent real-fs fall-through
185
+ * into an immediate, diagnosable failure (module doc "PARTIAL-ADAPTER TRAP").
186
+ * A method never invoked by the exercised code path never throws, so this is
187
+ * safe for any existing partial fake that only implements what it touches.
188
+ */
189
+ function buildGuardedAdapter(partial) {
190
+ const guarded = {};
191
+ for (const key of Object.keys(REAL_ADAPTER)) {
192
+ if (Object.prototype.hasOwnProperty.call(partial, key)) {
193
+ guarded[key] = partial[key];
194
+ }
195
+ else if (OPTIONAL_ADAPTER_METHODS.has(key)) {
196
+ guarded[key] = REAL_ADAPTER[key];
197
+ }
198
+ else {
199
+ guarded[key] = (..._args) => {
200
+ throw new Error(`installFs().${key}(...) was called but the injected partial adapter does not implement it. ` +
201
+ `A fake adapter must implement every method its code path touches (install-fs-adapter.cts's ` +
202
+ `PARTIAL-ADAPTER TRAP) — falling through to real fs is no longer allowed for this method.`);
203
+ };
204
+ }
205
+ }
206
+ return guarded;
207
+ }
208
+ /**
209
+ * Run `fn` with a GUARDED merge of `partial` over the real adapter as the
210
+ * active install-fs adapter, restoring the previous adapter afterward — even
211
+ * on throw (see the module doc's re-entrancy/synchronous-only assumption for
212
+ * why a bare module-level variable is safe here, and why it would not be
213
+ * under async interleaving or concurrent installs). `partial` undefined is
214
+ * a no-op: `fn` runs against whatever adapter was already active (real fs
215
+ * by default) — this is what keeps every existing `deps`-less call site
216
+ * (AC4) byte-identical. See `buildGuardedAdapter` for what "guarded" means.
217
+ */
218
+ function withInstallFs(partial, fn) {
219
+ if (!partial)
220
+ return fn();
221
+ const previous = current;
222
+ current = buildGuardedAdapter(partial);
223
+ try {
224
+ return fn();
225
+ }
226
+ finally {
227
+ current = previous;
228
+ }
229
+ }
230
+ /**
231
+ * Create a fresh, uniquely-named temp directory.
232
+ *
233
+ * AC4 requires the no-adapter-injected path to stay byte-identical to the
234
+ * pre-#2874 code, which called real `fs.mkdtempSync` directly — several
235
+ * existing tests (e.g. tests/install-runtime-artifacts.test.cjs's "rmSync is
236
+ * called on the tempDir when readFileSync throws") monkeypatch real
237
+ * `fs.mkdtempSync` to capture the exact directory a call under test creates,
238
+ * and stop working if that real syscall is no longer made. So: when NO fake
239
+ * adapter is active (`current === REAL_ADAPTER`, the exact top-level-install
240
+ * default), this calls `nodeFs.mkdtempSync` directly — the same real call
241
+ * the old code made, still visible to a monkeypatch applied after import
242
+ * because it is a live property lookup on the `node:fs` module object, not a
243
+ * captured reference.
244
+ *
245
+ * When a fake adapter IS active (any `withInstallFs(partial, …)` call, even
246
+ * a partial one — see `current`'s assignment in `withInstallFs`, which
247
+ * always produces a NEW merged object, never `=== REAL_ADAPTER`), this falls
248
+ * back to synthesizing a unique name and creating it via
249
+ * `installFs().mkdirSync`, exactly as before: the injected adapter contract
250
+ * still does not require `mkdtempSync`, so a fake (which only needs to
251
+ * implement `mkdirSync`) can drive the full staging pipeline without ever
252
+ * touching real fs.
253
+ */
254
+ function mkInstallTempDir(prefix) {
255
+ if (current === REAL_ADAPTER) {
256
+ return node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), prefix));
257
+ }
258
+ const dir = node_path_1.default.join(node_os_1.default.tmpdir(), `${prefix}${node_crypto_1.default.randomBytes(8).toString('hex')}`);
259
+ installFs().mkdirSync(dir, { recursive: true });
260
+ return dir;
261
+ }
262
+ module.exports = { installFs, withInstallFs, mkInstallTempDir };
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ /**
6
+ * install-model-override-resolver — install-time per-agent model-override
7
+ * resolution (#2256 / #2794), extracted from the package-root `bin/install.js`
8
+ * (#2875 Part 2 / J8).
9
+ *
10
+ * bin/install.js's inline agent-staging loop duplicated this EXACT precedence
11
+ * chain across two runtime branches (OpenCode ~24 lines, Kilo ~24 lines) —
12
+ * `model_overrides[agent]` > `model_profile_overrides.<runtime>.<tier>` > omit.
13
+ * Extracted here — a `src/*.cts` module compiled into the shipped
14
+ * `gsd-core/bin/lib/` tree, mirroring `install-effort-resolver.cts`'s existing
15
+ * precedent (#2071) — so the descriptor-driven agents pipeline
16
+ * (`convertedAgentsKind` / `stageAgentsForRuntimeWithConverter`) and
17
+ * bin/install.js's own callers resolve through the SAME code. A single source
18
+ * of truth makes the two paths diverge only if this module changes, not
19
+ * silently across two hand-maintained copies (the Generative Fix Divergence
20
+ * class CLAUDE.md's "Known Defects" section warns about).
21
+ *
22
+ * `readGsdGlobalModelOverrides` / `readGsdEffectiveModelOverrides` /
23
+ * `readGsdRuntimeProfileResolver` are impure (config-file reads);
24
+ * `resolveAgentModelOverride` is pure given their pre-resolved outputs.
25
+ *
26
+ * #2875 defect fix: this module is on the `installRuntimeArtifacts` call tree
27
+ * (reached from `runtime-artifact-layout.cts`'s agents-kind `stage()` for the
28
+ * opencode/kilo converters) — every fs touch below routes through
29
+ * `installFs()` (install-fs-adapter.cts), matching `retired-artifact-cleanup.cts`
30
+ * / `user-artifact-staging.cts`'s existing precedent, instead of calling
31
+ * `node:fs` directly. A raw `fs` call here silently bypassed a fake adapter
32
+ * injected via `withInstallFs`/`installRuntimeArtifacts(..., { fs })`,
33
+ * exactly the class of bug those two modules were fixed for.
34
+ */
35
+ const node_path_1 = __importDefault(require("node:path"));
36
+ const node_os_1 = __importDefault(require("node:os"));
37
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- install-fs-adapter.cjs is an export= CommonJS module
38
+ const installFsAdapter = require("./install-fs-adapter.cjs");
39
+ const { installFs } = installFsAdapter;
40
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- install-effort-resolver.cjs is an export= CommonJS module
41
+ const installEffortResolver = require("./install-effort-resolver.cjs");
42
+ const { _readGsdConfigFile, _findAncestorGsdConfigPath } = installEffortResolver;
43
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- model-catalog.cjs is an export= CommonJS module
44
+ const modelCatalog = require("./model-catalog.cjs");
45
+ const { MODEL_PROFILES: GSD_MODEL_PROFILES } = modelCatalog;
46
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- model-resolver.cjs is an export= CommonJS module
47
+ const modelResolverModule = require("./model-resolver.cjs");
48
+ const { resolveTierEntry: gsdResolveTierEntry } = modelResolverModule;
49
+ /**
50
+ * Read `model_overrides` from `~/.gsd/defaults.json` at install time.
51
+ * Returns an object mapping agent names to model IDs, or null if the file
52
+ * doesn't exist or has no `model_overrides` entry.
53
+ *
54
+ * Mirrors bin/install.js's prior local `readGsdGlobalModelOverrides` exactly
55
+ * (silent try/catch — no stderr warning on malformed JSON, unlike
56
+ * `_readGsdConfigFile`'s effort-config sibling — preserved for byte-parity).
57
+ */
58
+ function readGsdGlobalModelOverrides(options = {}) {
59
+ try {
60
+ const home = options.homedir ? options.homedir() : node_os_1.default.homedir();
61
+ const defaultsPath = node_path_1.default.join(home, '.gsd', 'defaults.json');
62
+ if (!installFs().existsSync(defaultsPath))
63
+ return null;
64
+ const raw = installFs().readFileSync(defaultsPath, 'utf-8');
65
+ const parsed = JSON.parse(raw);
66
+ const overrides = parsed.model_overrides;
67
+ if (!overrides || typeof overrides !== 'object')
68
+ return null;
69
+ return overrides;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ /**
76
+ * Effective per-agent `model_overrides` for the install path.
77
+ *
78
+ * Merges `~/.gsd/defaults.json` (global) with per-project
79
+ * `<project>/.planning/config.json`. Per-project keys win on conflict; keys
80
+ * present in only one source are preserved from that source (#2256).
81
+ *
82
+ * `targetDir` is the consuming runtime's install root (walks up to find
83
+ * `.planning/`). When `targetDir` is null/undefined only the global file is
84
+ * consulted.
85
+ *
86
+ * Returns a plain `{ agentName: modelId }` object, or `null` when neither
87
+ * source defines `model_overrides`.
88
+ */
89
+ function readGsdEffectiveModelOverrides(targetDir = null, options = {}) {
90
+ const global = readGsdGlobalModelOverrides(options);
91
+ let projectOverrides = null;
92
+ if (targetDir) {
93
+ // #2875 defect fix (Generative Fix Divergence): the 8-deep upward walk to
94
+ // `.planning/config.json` is single-sourced in install-effort-resolver.cts
95
+ // (`_findAncestorGsdConfigPath`) — this module was itself extracted FROM
96
+ // that one to stop duplicating shared install-time config logic, so a
97
+ // second hand-rolled copy of the walk here defeated the point.
98
+ const candidate = _findAncestorGsdConfigPath(targetDir);
99
+ if (candidate) {
100
+ try {
101
+ const parsed = JSON.parse(installFs().readFileSync(candidate, 'utf-8'));
102
+ if (parsed && typeof parsed === 'object' && parsed.model_overrides && typeof parsed.model_overrides === 'object') {
103
+ projectOverrides = parsed.model_overrides;
104
+ }
105
+ }
106
+ catch {
107
+ // Malformed config.json — fall back to global; readGsdRuntimeProfileResolver
108
+ // surfaces a parse warning via _readGsdConfigFile already.
109
+ }
110
+ }
111
+ }
112
+ if (!global && !projectOverrides)
113
+ return null;
114
+ // Per-project wins on conflict; preserve non-conflicting global keys.
115
+ return { ...(global || {}), ...(projectOverrides || {}) };
116
+ }
117
+ /**
118
+ * Build a runtime-aware tier resolver for the install path (#2517).
119
+ *
120
+ * Probes BOTH per-project `<targetDir>/.planning/config.json` AND
121
+ * `~/.gsd/defaults.json`, with per-project keys winning over global.
122
+ *
123
+ * Returns null if no `runtime` is configured, if `model_profile` is
124
+ * `inherit`, or if no project config is reachable AND `~/.gsd/defaults.json`
125
+ * declares no `model_profile` (#3543 — an unverifiable profile is never
126
+ * baked, letting the runtime's own default/session model govern).
127
+ */
128
+ function readGsdRuntimeProfileResolver(targetDir = null) {
129
+ const homeDefaults = _readGsdConfigFile(node_path_1.default.join(node_os_1.default.homedir(), '.gsd', 'defaults.json'), '~/.gsd/defaults.json');
130
+ // #2875 defect fix (Generative Fix Divergence): same shared walk as
131
+ // readGsdEffectiveModelOverrides above — see that call site's comment.
132
+ let projectConfig = null;
133
+ if (targetDir) {
134
+ const candidate = _findAncestorGsdConfigPath(targetDir);
135
+ if (candidate)
136
+ projectConfig = _readGsdConfigFile(candidate, '.planning/config.json');
137
+ }
138
+ const merged = {
139
+ runtime: (projectConfig && projectConfig.runtime) ||
140
+ (homeDefaults && homeDefaults.runtime) ||
141
+ null,
142
+ model_profile: (projectConfig && projectConfig.model_profile) ||
143
+ (homeDefaults && homeDefaults.model_profile) ||
144
+ 'balanced',
145
+ model_profile_overrides: (projectConfig && projectConfig.model_profile_overrides) ||
146
+ (homeDefaults && homeDefaults.model_profile_overrides) ||
147
+ null,
148
+ };
149
+ if (!merged.runtime)
150
+ return null;
151
+ if (!projectConfig && !(homeDefaults && homeDefaults.model_profile)) {
152
+ return null;
153
+ }
154
+ const profile = String(merged.model_profile).toLowerCase();
155
+ if (profile === 'inherit')
156
+ return null;
157
+ const runtime = merged.runtime;
158
+ return {
159
+ runtime,
160
+ resolve(agentName) {
161
+ const agentModels = GSD_MODEL_PROFILES[agentName];
162
+ if (!agentModels)
163
+ return null;
164
+ const tier = agentModels[profile] || agentModels.balanced;
165
+ if (!tier)
166
+ return null;
167
+ return gsdResolveTierEntry({
168
+ runtime,
169
+ tier,
170
+ overrides: merged.model_profile_overrides,
171
+ });
172
+ },
173
+ };
174
+ }
175
+ /**
176
+ * Resolve the effective model override for a single agent, given a
177
+ * pre-resolved `modelOverrides` map and `runtimeResolver` (both from the
178
+ * functions above). Pure — no filesystem access.
179
+ *
180
+ * Precedence (J8 — identical for kilo and opencode, resolved through this ONE
181
+ * shared function so the two runtimes can never diverge):
182
+ * 1. modelOverrides[agentName] (#2256 — explicit per-agent override)
183
+ * 2. runtimeResolver.resolve(agentName)?.model
184
+ * (#2794 — tier-based model_profile_overrides.<runtime>.<tier>)
185
+ * 3. null (omit — J7: the frontmatter key must not appear, not `null`/`""`)
186
+ */
187
+ function resolveAgentModelOverride(agentName, modelOverrides, runtimeResolver) {
188
+ const explicit = modelOverrides ? modelOverrides[agentName] : undefined;
189
+ if (explicit)
190
+ return explicit;
191
+ if (runtimeResolver) {
192
+ const entry = runtimeResolver.resolve(agentName);
193
+ if (entry && entry.model)
194
+ return entry.model;
195
+ }
196
+ return null;
197
+ }
198
+ module.exports = {
199
+ readGsdGlobalModelOverrides,
200
+ readGsdEffectiveModelOverrides,
201
+ readGsdRuntimeProfileResolver,
202
+ resolveAgentModelOverride,
203
+ };