@opengsd/gsd-core 1.12.0 → 1.13.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 (286) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +12 -0
  4. package/agents/gsd-executor.md +63 -35
  5. package/agents/gsd-plan-checker.md +76 -57
  6. package/agents/gsd-planner.md +14 -0
  7. package/agents/gsd-ui-checker.md +19 -3
  8. package/agents/gsd-ui-researcher.md +29 -0
  9. package/agents/gsd-verifier.md +23 -1
  10. package/bin/install.js +239 -67
  11. package/commands/gsd/execute-phase.md +1 -1
  12. package/commands/gsd/ns-workflow.md +2 -1
  13. package/commands/gsd/phase.md +1 -1
  14. package/commands/gsd/quick-batch.md +105 -0
  15. package/commands/gsd/surface.md +18 -8
  16. package/gsd-core/bin/gsd-tools.cjs +195 -50
  17. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  18. package/gsd-core/bin/lib/capability-registry.cjs +514 -114
  19. package/gsd-core/bin/lib/capability-state.cjs +7 -1
  20. package/gsd-core/bin/lib/capability-validator.cjs +120 -4
  21. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  22. package/gsd-core/bin/lib/check-command-router.cjs +85 -2
  23. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  24. package/gsd-core/bin/lib/clusters.cjs +1 -0
  25. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  26. package/gsd-core/bin/lib/commands.cjs +337 -13
  27. package/gsd-core/bin/lib/config-loader.cjs +3 -0
  28. package/gsd-core/bin/lib/core-utils.cjs +34 -7
  29. package/gsd-core/bin/lib/decisions.cjs +213 -1
  30. package/gsd-core/bin/lib/edge-probe.cjs +14 -1
  31. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  32. package/gsd-core/bin/lib/frontmatter.cjs +137 -23
  33. package/gsd-core/bin/lib/gap-checker.cjs +22 -13
  34. package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
  35. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  36. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
  37. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
  38. package/gsd-core/bin/lib/host-integration.cjs +57 -5
  39. package/gsd-core/bin/lib/init-command-router.cjs +14 -0
  40. package/gsd-core/bin/lib/init.cjs +132 -15
  41. package/gsd-core/bin/lib/install-engine.cjs +184 -12
  42. package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
  43. package/gsd-core/bin/lib/install-profiles.cjs +22 -14
  44. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  45. package/gsd-core/bin/lib/io.cjs +35 -0
  46. package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
  47. package/gsd-core/bin/lib/markdown-table.cjs +123 -0
  48. package/gsd-core/bin/lib/milestone.cjs +22 -2
  49. package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
  50. package/gsd-core/bin/lib/phase-id.cjs +251 -9
  51. package/gsd-core/bin/lib/phase.cjs +774 -35
  52. package/gsd-core/bin/lib/plan-document.cjs +10 -0
  53. package/gsd-core/bin/lib/planning-snapshot.cjs +147 -20
  54. package/gsd-core/bin/lib/planning-workspace.cjs +103 -28
  55. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  56. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  57. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  58. package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
  59. package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
  60. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  61. package/gsd-core/bin/lib/roadmap-parser.cjs +499 -26
  62. package/gsd-core/bin/lib/roadmap.cjs +187 -58
  63. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +233 -33
  64. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
  65. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +286 -108
  66. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -43
  67. package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
  68. package/gsd-core/bin/lib/smart-entry.cjs +7 -9
  69. package/gsd-core/bin/lib/state-document.cjs +30 -5
  70. package/gsd-core/bin/lib/state-md-schema.cjs +23 -13
  71. package/gsd-core/bin/lib/state-transition.cjs +333 -44
  72. package/gsd-core/bin/lib/state.cjs +684 -125
  73. package/gsd-core/bin/lib/surface.cjs +23 -8
  74. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  75. package/gsd-core/bin/lib/uat.cjs +1419 -515
  76. package/gsd-core/bin/lib/update-context.cjs +6 -2
  77. package/gsd-core/bin/lib/validate.cjs +230 -12
  78. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  79. package/gsd-core/bin/lib/verification.cjs +273 -12
  80. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  81. package/gsd-core/bin/lib/verify.cjs +346 -16
  82. package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
  83. package/gsd-core/bin/lib/worktree-safety.cjs +8 -0
  84. package/gsd-core/bin/shared/config-schema.manifest.json +8 -0
  85. package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
  86. package/gsd-core/references/agent-contracts.md +3 -3
  87. package/gsd-core/references/edge-probe.md +17 -13
  88. package/gsd-core/references/execute-mvp-tdd.md +18 -16
  89. package/gsd-core/references/execute-phase-response-language.md +6 -0
  90. package/gsd-core/references/executor-examples.md +42 -0
  91. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  92. package/gsd-core/references/mvp-concepts.md +2 -2
  93. package/gsd-core/references/plan-checker-examples.md +41 -0
  94. package/gsd-core/references/planner-antipatterns.md +25 -0
  95. package/gsd-core/references/planner-chunked.md +5 -1
  96. package/gsd-core/references/planner-coupling.md +42 -0
  97. package/gsd-core/references/planner-quick-batch.md +71 -0
  98. package/gsd-core/references/planner-reviews.md +47 -0
  99. package/gsd-core/references/planner-revision.md +75 -2
  100. package/gsd-core/references/planning-config.md +2 -1
  101. package/gsd-core/references/response-language-directive.md +9 -0
  102. package/gsd-core/references/revision-loop.md +118 -11
  103. package/gsd-core/references/tdd.md +14 -9
  104. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  105. package/gsd-core/templates/phase-prompt.md +4 -0
  106. package/gsd-core/templates/verification-report.md +5 -0
  107. package/gsd-core/workflows/add-backlog.md +2 -0
  108. package/gsd-core/workflows/add-phase.md +2 -0
  109. package/gsd-core/workflows/add-tests.md +1 -1
  110. package/gsd-core/workflows/add-todo.md +1 -1
  111. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  112. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  113. package/gsd-core/workflows/audit-fix.md +2 -0
  114. package/gsd-core/workflows/audit-milestone.md +2 -0
  115. package/gsd-core/workflows/audit-uat.md +2 -0
  116. package/gsd-core/workflows/autonomous.md +2 -0
  117. package/gsd-core/workflows/check-todos.md +1 -1
  118. package/gsd-core/workflows/cleanup.md +1 -1
  119. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +15 -13
  120. package/gsd-core/workflows/code-review-fix.md +2 -0
  121. package/gsd-core/workflows/code-review.md +73 -31
  122. package/gsd-core/workflows/complete-milestone.md +13 -4
  123. package/gsd-core/workflows/debug.md +1 -1
  124. package/gsd-core/workflows/diagnose-issues.md +5 -1
  125. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
  126. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  127. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  128. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  129. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  130. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
  131. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  132. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  133. package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
  134. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  135. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  136. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  137. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  138. package/gsd-core/workflows/discuss-phase.md +1 -1
  139. package/gsd-core/workflows/do.md +43 -13
  140. package/gsd-core/workflows/docs-update.md +1 -1
  141. package/gsd-core/workflows/edit-phase.md +2 -0
  142. package/gsd-core/workflows/eval-review.md +1 -1
  143. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +2 -0
  144. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +17 -1
  145. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
  146. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
  147. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  148. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  149. package/gsd-core/workflows/execute-phase.md +32 -14
  150. package/gsd-core/workflows/execute-plan.md +8 -8
  151. package/gsd-core/workflows/explore.md +2 -0
  152. package/gsd-core/workflows/extract-learnings.md +2 -0
  153. package/gsd-core/workflows/fast.md +6 -0
  154. package/gsd-core/workflows/forensics.md +2 -0
  155. package/gsd-core/workflows/graduation.md +1 -1
  156. package/gsd-core/workflows/health.md +1 -1
  157. package/gsd-core/workflows/help/modes/brief.md +2 -0
  158. package/gsd-core/workflows/help/modes/default.md +2 -0
  159. package/gsd-core/workflows/help/modes/full.md +12 -0
  160. package/gsd-core/workflows/help/modes/topic.md +2 -0
  161. package/gsd-core/workflows/help.md +2 -0
  162. package/gsd-core/workflows/import.md +3 -3
  163. package/gsd-core/workflows/inbox.md +1 -1
  164. package/gsd-core/workflows/ingest-docs.md +1 -1
  165. package/gsd-core/workflows/insert-phase.md +2 -0
  166. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  167. package/gsd-core/workflows/list-seeds.md +2 -0
  168. package/gsd-core/workflows/list-workspaces.md +2 -0
  169. package/gsd-core/workflows/manager.md +3 -3
  170. package/gsd-core/workflows/map-codebase.md +2 -0
  171. package/gsd-core/workflows/milestone-summary.md +2 -0
  172. package/gsd-core/workflows/mvp-phase.md +1 -1
  173. package/gsd-core/workflows/new-milestone.md +1 -1
  174. package/gsd-core/workflows/new-project.md +5 -3
  175. package/gsd-core/workflows/new-workspace.md +1 -1
  176. package/gsd-core/workflows/next.md +2 -0
  177. package/gsd-core/workflows/node-repair.md +2 -0
  178. package/gsd-core/workflows/note.md +2 -0
  179. package/gsd-core/workflows/onboard.md +1 -1
  180. package/gsd-core/workflows/pause-work.md +19 -4
  181. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  182. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
  183. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
  184. package/gsd-core/workflows/plan-phase.md +130 -12
  185. package/gsd-core/workflows/plan-review-convergence.md +102 -10
  186. package/gsd-core/workflows/plant-seed.md +1 -1
  187. package/gsd-core/workflows/pr-branch.md +11 -3
  188. package/gsd-core/workflows/profile-user.md +1 -1
  189. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  190. package/gsd-core/workflows/progress.md +25 -3
  191. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
  192. package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
  193. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  194. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  195. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  196. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  197. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  198. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  199. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  200. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  201. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  202. package/gsd-core/workflows/quick-batch.md +203 -0
  203. package/gsd-core/workflows/quick.md +13 -3
  204. package/gsd-core/workflows/reapply-patches.md +2 -0
  205. package/gsd-core/workflows/remove-phase.md +2 -0
  206. package/gsd-core/workflows/remove-workspace.md +1 -1
  207. package/gsd-core/workflows/resume-project.md +6 -2
  208. package/gsd-core/workflows/review.md +215 -10
  209. package/gsd-core/workflows/scan.md +2 -0
  210. package/gsd-core/workflows/section-manifest.json +12 -0
  211. package/gsd-core/workflows/secure-phase.md +1 -1
  212. package/gsd-core/workflows/session-report.md +2 -0
  213. package/gsd-core/workflows/settings-advanced.md +2 -0
  214. package/gsd-core/workflows/settings-integrations.md +9 -8
  215. package/gsd-core/workflows/settings.md +1 -1
  216. package/gsd-core/workflows/ship.md +10 -10
  217. package/gsd-core/workflows/sketch-wrap-up.md +2 -0
  218. package/gsd-core/workflows/sketch.md +1 -1
  219. package/gsd-core/workflows/smart-entry.md +1 -1
  220. package/gsd-core/workflows/spec-phase.md +24 -19
  221. package/gsd-core/workflows/spike-wrap-up.md +2 -0
  222. package/gsd-core/workflows/spike.md +1 -1
  223. package/gsd-core/workflows/stats.md +2 -0
  224. package/gsd-core/workflows/sync-skills.md +12 -4
  225. package/gsd-core/workflows/thread.md +2 -0
  226. package/gsd-core/workflows/transition.md +2 -0
  227. package/gsd-core/workflows/ui-phase.md +26 -5
  228. package/gsd-core/workflows/ui-review.md +1 -1
  229. package/gsd-core/workflows/ultraplan-phase.md +2 -0
  230. package/gsd-core/workflows/undo.md +1 -1
  231. package/gsd-core/workflows/update.md +41 -38
  232. package/gsd-core/workflows/validate-phase.md +1 -1
  233. package/gsd-core/workflows/verify-work.md +49 -3
  234. package/hooks/dist/gsd-check-update-worker.js +19 -2
  235. package/hooks/dist/gsd-context-monitor.js +283 -12
  236. package/hooks/dist/gsd-node-runner.sh +1 -0
  237. package/hooks/dist/gsd-prompt-guard.js +30 -5
  238. package/hooks/dist/gsd-read-guard.js +2 -0
  239. package/hooks/dist/gsd-read-injection-scanner.js +5 -5
  240. package/hooks/dist/gsd-secret-read-guard.js +1079 -0
  241. package/hooks/dist/gsd-statusline.js +7 -3
  242. package/hooks/dist/gsd-validate-commit.sh +444 -7
  243. package/hooks/dist/gsd-workflow-guard.js +2 -1
  244. package/hooks/dist/lib/git-cmd.js +210 -1
  245. package/hooks/dist/lib/injection-patterns.js +36 -6
  246. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  247. package/hooks/gsd-check-update-worker.js +19 -2
  248. package/hooks/gsd-context-monitor.js +283 -12
  249. package/hooks/gsd-node-runner.sh +1 -0
  250. package/hooks/gsd-prompt-guard.js +30 -5
  251. package/hooks/gsd-read-guard.js +2 -0
  252. package/hooks/gsd-read-injection-scanner.js +5 -5
  253. package/hooks/gsd-secret-read-guard.js +1079 -0
  254. package/hooks/gsd-statusline.js +7 -3
  255. package/hooks/gsd-validate-commit.sh +444 -7
  256. package/hooks/gsd-workflow-guard.js +2 -1
  257. package/hooks/hooks.json +6 -0
  258. package/hooks/lib/git-cmd.js +210 -1
  259. package/hooks/lib/injection-patterns.js +36 -6
  260. package/hooks/managed-hooks-registry.cjs +1 -0
  261. package/package.json +5 -5
  262. package/scripts/build-hooks.js +11 -4
  263. package/scripts/ci-test-scope.cjs +7 -0
  264. package/scripts/docs-guard-registry.cjs +10 -0
  265. package/scripts/gen-loop-host-contract.cjs +67 -15
  266. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  267. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  268. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  269. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  270. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +5 -0
  271. package/scripts/lint-phase-enumeration-drift.cjs +24 -6
  272. package/scripts/lint-phase-id-drift.cjs +133 -8
  273. package/scripts/lint-portable-grep.cjs +176 -0
  274. package/scripts/lint-response-language-coverage.cjs +524 -0
  275. package/scripts/lint-test-file-count.allowlist.json +3 -1
  276. package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
  277. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  278. package/scripts/npm-audit-baseline.cjs +376 -0
  279. package/scripts/prompt-injection-scan.sh +8 -0
  280. package/scripts/require-issue-link-policy.cjs +16 -1
  281. package/skills/gsd-execute-phase/SKILL.md +1 -1
  282. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  283. package/skills/gsd-phase/SKILL.md +1 -1
  284. package/skills/gsd-quick-batch/SKILL.md +105 -0
  285. package/skills/gsd-surface/SKILL.md +18 -8
  286. package/vscode/package.json +1 -1
@@ -34,6 +34,9 @@ const phaseId = require("./phase-id.cjs");
34
34
  const worktreeSafety = require("./worktree-safety.cjs");
35
35
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
36
36
  const planningWorkspace = require("./planning-workspace.cjs");
37
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
38
+ const planningScopeMod = require("./planning-scope.cjs");
39
+ const { SCOPE } = planningScopeMod;
37
40
  const secrets_cjs_1 = require("./secrets.cjs");
38
41
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
39
42
  const scanPhasePlans = require("./plan-scan.cjs");
@@ -83,7 +86,7 @@ const { resolveModelInternal, resolveGranularityInternal, assertValidGranularity
83
86
  const { findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocator;
84
87
  const { getRoadmapPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
85
88
  const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
86
- const { normalizePhaseName, matchPhaseDirs, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase } = phaseId;
89
+ const { comparePhaseNum, normalizePhaseName, matchPhaseDirs, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase } = phaseId;
87
90
  const { pruneOrphanedWorktrees } = worktreeSafety;
88
91
  const { planningPaths, planningDir, planningRoot, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, } = planningWorkspace;
89
92
  const { determinePhaseStatus } = commandsMod;
@@ -771,7 +774,6 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
771
774
  ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
772
775
  : null;
773
776
  const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
774
- const wf = (config.workflow ?? {});
775
777
  // #3188: these paths are null when the file is absent, matching the contract
776
778
  // the conditional sibling fields (context_path, patterns_path, ...) already
777
779
  // honour and that ultraplan-phase.md / execute-phase.md gate on. Hoisted so
@@ -782,7 +784,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
782
784
  const result = {
783
785
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
784
786
  verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
785
- tdd_mode: options['tdd'] || Boolean(wf['tdd_mode']) || false,
787
+ tdd_mode: options['tdd'] || Boolean(config.tdd_mode) || false,
786
788
  commit_docs: config.commit_docs,
787
789
  sub_repos: config.sub_repos,
788
790
  parallelization: config.parallelization,
@@ -924,7 +926,6 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
924
926
  const granularityOverride = options['granularity'];
925
927
  assertValidGranularityOverride(granularityOverride, error);
926
928
  const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
927
- const wf = (config.workflow ?? {});
928
929
  // #3188: see cmdInitExecutePhase — null when absent, parity with the
929
930
  // conditional sibling fields in this same result object.
930
931
  const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
@@ -934,11 +935,11 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
934
935
  researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
935
936
  planner_model: resolveModelInternal(cwd, 'gsd-planner'),
936
937
  checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
937
- tdd_mode: options['tdd'] || Boolean(wf['tdd_mode']) || false,
938
+ tdd_mode: options['tdd'] || Boolean(config.tdd_mode) || false,
938
939
  granularity,
939
- research_enabled: wf['research'],
940
+ research_enabled: config.research,
940
941
  plan_checker_enabled: config.plan_checker,
941
- nyquist_validation_enabled: wf['nyquist_validation'],
942
+ nyquist_validation_enabled: config.nyquist_validation,
942
943
  commit_docs: config.commit_docs,
943
944
  text_mode: config.text_mode,
944
945
  auto_advance: !!(config.auto_advance),
@@ -960,6 +961,11 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
960
961
  : 'Pending',
961
962
  has_research: phaseInfo?.['has_research'] || false,
962
963
  has_context: phaseInfo?.['has_context'] || false,
964
+ // #4014 (epic #3473 B4-unreadable): additive scope signal adjacent to
965
+ // has_context — SCOPE.COMPLETE by default (no phase directory to read is
966
+ // a genuine, not-unreadable answer), overwritten below to whatever
967
+ // findContextMdIn(phaseDirFull) reports once a directory is known.
968
+ context_scope: SCOPE.COMPLETE,
963
969
  has_reviews: phaseInfo?.['has_reviews'] || false,
964
970
  has_plans: (phaseInfo?.['plans']?.length || 0) > 0,
965
971
  plan_count: phaseInfo?.['plans']?.length || 0,
@@ -974,6 +980,13 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
974
980
  };
975
981
  if (phaseInfo?.['directory']) {
976
982
  const phaseDirFull = node_path_1.default.join(cwd, phaseInfo['directory']);
983
+ // #4014 (epic #3473 B4-unreadable): findContextMdIn's directory-string
984
+ // form never throws, so this can record the real scope BEFORE the
985
+ // pre-existing `fs.readdirSync(phaseDirFull)` immediately below
986
+ // (unchanged) throws on the same unreadable directory and is caught
987
+ // exactly as before — additive only, the failure control-flow for
988
+ // context_path/research_path/etc. is untouched.
989
+ result['context_scope'] = findContextMdIn(phaseDirFull).scope;
977
990
  try {
978
991
  const files = node_fs_1.default.readdirSync(phaseDirFull);
979
992
  const phaseDirName = node_path_1.default.basename(phaseDirFull);
@@ -1100,6 +1113,34 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
1100
1113
  result['prior_verify_commands'] = priorVerifyCommands;
1101
1114
  output(withProjectRoot(cwd, result), raw);
1102
1115
  }
1116
+ // #4040: shared partial-init discriminator for the init.progress / init.resume /
1117
+ // init.new-project payloads. A bootstrap interrupted before the core quartet
1118
+ // (PROJECT.md / REQUIREMENTS.md / ROADMAP.md / STATE.md) all landed is a
1119
+ // DISTINCT routing state from "new project" and from "between milestones";
1120
+ // pre-#4040 the payloads could not express it, so progress.md mis-routed it to
1121
+ // Route F and resume-project.md offered STATE.md reconstruction.
1122
+ //
1123
+ // Negative-space guard: `milestone.complete` archives ROADMAP.md (and
1124
+ // REQUIREMENTS.md) but always leaves MILESTONES.md behind — so MILESTONES.md
1125
+ // present proves missing core files are archival (between-milestones), never an
1126
+ // unfinished bootstrap. REQUIREMENTS.md is written by new-project BEFORE
1127
+ // ROADMAP/STATE, so its absence also proves init never finished.
1128
+ function buildInitCompletenessFields(cwd) {
1129
+ const dir = planningDir(cwd);
1130
+ const planningExists = node_fs_1.default.existsSync(dir);
1131
+ const requirementsExists = node_fs_1.default.existsSync(node_path_1.default.join(dir, 'REQUIREMENTS.md'));
1132
+ const milestonesExists = node_fs_1.default.existsSync(node_path_1.default.join(dir, 'MILESTONES.md'));
1133
+ const coreComplete = node_fs_1.default.existsSync(node_path_1.default.join(dir, 'PROJECT.md')) &&
1134
+ requirementsExists &&
1135
+ node_fs_1.default.existsSync(node_path_1.default.join(dir, 'ROADMAP.md')) &&
1136
+ node_fs_1.default.existsSync(node_path_1.default.join(dir, 'STATE.md'));
1137
+ return {
1138
+ planning_exists: planningExists,
1139
+ requirements_exists: requirementsExists,
1140
+ milestones_exists: milestonesExists,
1141
+ init_incomplete: planningExists && !coreComplete && !milestonesExists,
1142
+ };
1143
+ }
1103
1144
  function cmdInitNewProject(cwd, raw, options = {}) {
1104
1145
  const config = loadConfig(cwd);
1105
1146
  const homedir = node_os_1.default.homedir();
@@ -1119,6 +1160,11 @@ function cmdInitNewProject(cwd, raw, options = {}) {
1119
1160
  synthesizer_model: resolveModelInternal(cwd, 'gsd-research-synthesizer'),
1120
1161
  roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'),
1121
1162
  commit_docs: config.commit_docs,
1163
+ // #4040: partial-init discriminator (see buildInitCompletenessFields).
1164
+ // Spread BEFORE this literal's own planning_exists so the existing
1165
+ // root-scoped (`pathExistsInternal(cwd, '.planning')`) semantics for that
1166
+ // one key stay byte-identical for existing consumers.
1167
+ ...buildInitCompletenessFields(cwd),
1122
1168
  project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1123
1169
  has_codebase_map: hasCodebaseMap,
1124
1170
  planning_exists: pathExistsInternal(cwd, '.planning'),
@@ -1158,13 +1204,12 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
1158
1204
  // owns — routed through it instead of a local readdirSync + hand-rolled
1159
1205
  // window filter (which also never excluded sentinels, unlike the owner).
1160
1206
  const phaseDirCount = listMilestonePhaseDirs(phasesDir, { cwd }).value.length;
1161
- const wf = (config.workflow ?? {});
1162
1207
  const result = {
1163
1208
  researcher_model: resolveModelInternal(cwd, 'gsd-project-researcher'),
1164
1209
  synthesizer_model: resolveModelInternal(cwd, 'gsd-research-synthesizer'),
1165
1210
  roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'),
1166
1211
  commit_docs: config.commit_docs,
1167
- research_enabled: wf['research'],
1212
+ research_enabled: config.research,
1168
1213
  // #3216 review Finding 2: `?? null` so an unresolved milestone still emits
1169
1214
  // the key with an explicit `null` rather than letting JSON.stringify drop
1170
1215
  // it — an omitted key reaches the prompt layer's `{current_milestone}`
@@ -1246,6 +1291,10 @@ function cmdInitQuick(cwd, description, raw, options = {}) {
1246
1291
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
1247
1292
  checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
1248
1293
  verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
1294
+ // #3936: Step 4.75 dispatches gsd-phase-researcher; resolve its own tier
1295
+ // (parity with cmdInitPlanPhase) so the research spawn stops pinning
1296
+ // planner_model.
1297
+ researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
1249
1298
  // #2072: the quick review step spawns gsd-code-reviewer; resolve its own model
1250
1299
  // so model_overrides / models.verification apply (was reusing executor_model).
1251
1300
  reviewer_model: resolveModelInternal(cwd, 'gsd-code-reviewer'),
@@ -1281,6 +1330,45 @@ function cmdInitQuick(cwd, description, raw, options = {}) {
1281
1330
  result['section_manifest'] = buildSectionManifestField(cwd, null, sectionManifestOptions, 'quick');
1282
1331
  output(withProjectRoot(cwd, result), raw);
1283
1332
  }
1333
+ /**
1334
+ * `init.quick-batch` (#3676, Phase 4 of epic #3344, ADR-1239 "Quick-batch
1335
+ * binding"). Unlike `cmdInitQuick`, this init bundle does NOT allocate a
1336
+ * quick id / slug / task directory itself — batch-level id allocation and
1337
+ * `BATCH.json` creation is the job of the `quick-batch create` CLI verb
1338
+ * (`src/quick-batch-command-router.cts`, wrapping `createBatch` in
1339
+ * `src/quick-batch.cts`). This bundle supplies the per-role model profiles,
1340
+ * `commit_docs`, the roadmap/planning existence checks `quick-batch.md`'s
1341
+ * ROADMAP.md gate needs (same check `cmdInitQuick` runs), the `.planning/quick`
1342
+ * directory path, and the `section_manifest` field gating the optional
1343
+ * `--research`/`--validate` step fragments — the same `flag:--research`/
1344
+ * `flag:--validate` atoms `quick`'s own section manifest already uses
1345
+ * (`WHEN_VOCABULARY` is workflow-agnostic; no new atom is needed). `--discuss`/
1346
+ * `--full` are rejected by `quick-batch-dispatch.cts`'s `parseQuickBatchArgs`
1347
+ * before this init bundle is ever reached, so no `discuss`/`full` flag key is
1348
+ * accepted here (unlike `cmdInitQuick`, which still supports both).
1349
+ */
1350
+ function cmdInitQuickBatch(cwd, raw, options = {}) {
1351
+ const config = loadConfig(cwd);
1352
+ const result = {
1353
+ planner_model: resolveModelInternal(cwd, 'gsd-planner'),
1354
+ executor_model: resolveModelInternal(cwd, 'gsd-executor'),
1355
+ checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
1356
+ verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
1357
+ researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
1358
+ reviewer_model: resolveModelInternal(cwd, 'gsd-code-reviewer'),
1359
+ commit_docs: config.commit_docs,
1360
+ // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase; a
1361
+ // per-item task_dir is re-derived by the workflow itself (quick_id +
1362
+ // generate-slug over that item's description), never allocated here.
1363
+ quick_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'quick')),
1364
+ quick_batches_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'quick-batches')),
1365
+ roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1366
+ planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
1367
+ };
1368
+ // #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
1369
+ result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'quick-batch');
1370
+ output(withProjectRoot(cwd, result), raw);
1371
+ }
1284
1372
  function cmdInitIngestDocs(cwd, raw) {
1285
1373
  const config = loadConfig(cwd);
1286
1374
  const result = {
@@ -1305,12 +1393,11 @@ function cmdInitIngestDocs(cwd, raw) {
1305
1393
  }
1306
1394
  function cmdInitOnboard(cwd, raw, options = {}) {
1307
1395
  const config = loadConfig(cwd);
1308
- const workflowConfig = (config.workflow ?? {});
1309
1396
  const result = {
1310
1397
  ...buildOnboardProjection(cwd, {
1311
1398
  commitDocs: !!config.commit_docs,
1312
1399
  fast: options['fast'] === true,
1313
- textMode: options['text'] === true || !!config.text_mode || !!workflowConfig['text_mode'],
1400
+ textMode: options['text'] === true || !!config.text_mode,
1314
1401
  }),
1315
1402
  ...getInitGitState(cwd),
1316
1403
  };
@@ -1323,6 +1410,11 @@ function cmdInitResume(cwd, raw) {
1323
1410
  if (agentIdRaw !== null)
1324
1411
  interruptedAgentId = agentIdRaw.trim();
1325
1412
  const result = {
1413
+ // #4040: partial-init discriminator (see buildInitCompletenessFields).
1414
+ // Spread FIRST so this literal's own root-scoped planning_exists
1415
+ // (planningRoot) keeps its existing semantics; init_incomplete itself
1416
+ // keys off the artifact dir (planningDir) where the core docs live.
1417
+ ...buildInitCompletenessFields(cwd),
1326
1418
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1327
1419
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1328
1420
  project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
@@ -1720,6 +1812,9 @@ function cmdInitPhaseOp(cwd, phase, raw) {
1720
1812
  padded_phase: phaseNumber ? normalizePhaseName(phaseNumber) : null,
1721
1813
  has_research: phaseInfo?.['has_research'] || false,
1722
1814
  has_context: phaseInfo?.['has_context'] || false,
1815
+ // #4014 (epic #3473 B4-unreadable): see the parallel field in
1816
+ // cmdInitPlanPhase — additive scope signal adjacent to has_context.
1817
+ context_scope: SCOPE.COMPLETE,
1723
1818
  has_plans: (phaseInfo?.['plans']?.length || 0) > 0,
1724
1819
  has_verification: phaseInfo?.['has_verification'] || false,
1725
1820
  has_reviews: phaseInfo?.['has_reviews'] || false,
@@ -1734,6 +1829,9 @@ function cmdInitPhaseOp(cwd, phase, raw) {
1734
1829
  };
1735
1830
  if (phaseInfo?.['directory']) {
1736
1831
  const phaseDirFull = node_path_1.default.join(cwd, phaseInfo['directory']);
1832
+ // #4014 (epic #3473 B4-unreadable): see the parallel site in
1833
+ // cmdInitPlanPhase — additive only, failure control-flow below unchanged.
1834
+ result['context_scope'] = findContextMdIn(phaseDirFull).scope;
1737
1835
  try {
1738
1836
  const files = node_fs_1.default.readdirSync(phaseDirFull);
1739
1837
  const phaseDirName = node_path_1.default.basename(phaseDirFull);
@@ -2062,6 +2160,9 @@ function cmdInitManager(cwd, raw) {
2062
2160
  let hasResearch = false;
2063
2161
  let lastActivity = null;
2064
2162
  let isActive = false;
2163
+ // #4014 (epic #3473 B4-unreadable): default COMPLETE — no directory at
2164
+ // all (dirMatch not found) is a genuine, not-unreadable answer.
2165
+ let contextScope = SCOPE.COMPLETE;
2065
2166
  let completion = buildPhaseCompletionProjection(cwd, phaseNum, null, planCount, summaryCount, _slashRuntime);
2066
2167
  try {
2067
2168
  // #3185 (ADR-3180 Decision 2) moved this lookup off the
@@ -2072,6 +2173,17 @@ function cmdInitManager(cwd, raw) {
2072
2173
  if (dirMatch) {
2073
2174
  const fullDir = node_path_1.default.join(phasesDir, dirMatch);
2074
2175
  const phaseDirRel = toPosixPath(node_path_1.default.relative(cwd, fullDir));
2176
+ // #4014 (epic #3473 B4-unreadable): this whole block used to swallow
2177
+ // ANY readdirSync failure below into the bare `catch { /* empty */ }`
2178
+ // at the bottom — an unreadable phase directory reported the exact
2179
+ // same `has_context: false` / `disk_status: 'no_directory'` as a
2180
+ // directory that never existed. findContextMdIn's directory-string
2181
+ // form never throws, so it can record the real scope (COMPLETE vs
2182
+ // UNREADABLE) here, BEFORE the pre-existing `fs.readdirSync(fullDir)`
2183
+ // immediately below (unchanged) throws on the exact same unreadable
2184
+ // directory and is caught exactly as before — this line is additive
2185
+ // only, the failure control-flow is untouched.
2186
+ contextScope = findContextMdIn(fullDir).scope;
2075
2187
  const phaseFiles = node_fs_1.default.readdirSync(fullDir);
2076
2188
  planCount = listPhasePlanFiles(fullDir).length;
2077
2189
  summaryCount = listPhaseSummaryFiles(fullDir).length;
@@ -2146,6 +2258,7 @@ function cmdInitManager(cwd, raw) {
2146
2258
  ...completion,
2147
2259
  last_activity: lastActivity,
2148
2260
  is_active: isActive,
2261
+ context_scope: contextScope,
2149
2262
  });
2150
2263
  }
2151
2264
  const MAX_NAME_WIDTH = 20;
@@ -2522,7 +2635,7 @@ function cmdInitTransition(cwd, raw, options = {}) {
2522
2635
  * location has one source instead of two kept in sync by hand.
2523
2636
  * - `debugger_model` — `resolveModelInternal`, which IS what `query
2524
2637
  * resolve-model --pick model` returns (`cmdResolveModel`, src/commands.cts).
2525
- * - `tdd_mode` — the `Boolean(wf['tdd_mode'])` idiom `cmdInitExecutePhase` and
2638
+ * - `tdd_mode` — the `Boolean(config.tdd_mode)` idiom `cmdInitExecutePhase` and
2526
2639
  * `cmdInitPlanPhase` already use. `/gsd:debug` has no `--tdd` flag, so the
2527
2640
  * sibling handlers' `options['tdd'] ||` disjunct is deliberately omitted
2528
2641
  * rather than carried as a phantom.
@@ -2540,7 +2653,6 @@ function cmdInitTransition(cwd, raw, options = {}) {
2540
2653
  */
2541
2654
  function cmdInitDebug(cwd, raw, options = {}) {
2542
2655
  const config = loadConfig(cwd);
2543
- const wf = (config.workflow ?? {});
2544
2656
  const result = {
2545
2657
  commit_docs: config.commit_docs,
2546
2658
  // #2376: absolute — debug.md builds `debug_file_path` as
@@ -2548,7 +2660,7 @@ function cmdInitDebug(cwd, raw, options = {}) {
2548
2660
  // own cwd may differ from the orchestrator's.
2549
2661
  debug_dir: toPosixPath(planningPaths(cwd).debug),
2550
2662
  debugger_model: resolveModelInternal(cwd, 'gsd-debugger'),
2551
- tdd_mode: Boolean(wf['tdd_mode']),
2663
+ tdd_mode: Boolean(config.tdd_mode),
2552
2664
  diagnose: options['diagnose'] === true,
2553
2665
  };
2554
2666
  // Additive, optional field — degrades to null while `debug` has no key in
@@ -2709,7 +2821,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
2709
2821
  }
2710
2822
  }
2711
2823
  }
2712
- phases.sort((a, b) => parseInt(a['number'], 10) - parseInt(b['number'], 10));
2824
+ phases.sort((a, b) => comparePhaseNum(a['number'], b['number']));
2713
2825
  // #3581: the frontier is ROADMAP ORDER, not artifact presence. The disk loop
2714
2826
  // above could claim nextPhase from a stray out-of-order artifact directory
2715
2827
  // (a phase-9 UAT evidence dir while roadmap phase 8 was pending and
@@ -2771,6 +2883,10 @@ function cmdInitProgress(cwd, raw, options = {}) {
2771
2883
  project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
2772
2884
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
2773
2885
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
2886
+ // #4040: partial-init discriminator (see buildInitCompletenessFields) —
2887
+ // also adds planning_exists / requirements_exists / milestones_exists so
2888
+ // progress.md's init_context routing never has to fall back to Glob.
2889
+ ...buildInitCompletenessFields(cwd),
2774
2890
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
2775
2891
  state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
2776
2892
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
@@ -3419,6 +3535,7 @@ module.exports = {
3419
3535
  cmdInitNewProject,
3420
3536
  cmdInitNewMilestone,
3421
3537
  cmdInitQuick,
3538
+ cmdInitQuickBatch,
3422
3539
  cmdInitIngestDocs,
3423
3540
  cmdInitOnboard,
3424
3541
  cmdInitResume,
@@ -55,6 +55,148 @@ const { processAttribution } = runtimeArtifactConversion;
55
55
  // resolveRuntimeArtifactLayout: accessed via module ref (not destructured) so
56
56
  // test stubs that monkeypatch the module's exports are seen at call time.
57
57
  const { getDirName } = runtimeNamePolicy;
58
+ function withInstallerPackageSource(configDir, fn) {
59
+ // Reuse the existing compatibility-marker contract through the existing fs
60
+ // seam. The marker exists only in this synchronous call tree: no disk state,
61
+ // layout export, stage argument, or caller-settable authority flag is added.
62
+ const markerPath = node_path_1.default.resolve(configDir, '.gsd-source');
63
+ const packageCommandsRoot = runtimeArtifactLayout.findInstallSourceRoot();
64
+ const markerBytes = Buffer.from(packageCommandsRoot + '\n');
65
+ const base = installFs();
66
+ const overlay = {
67
+ ...base,
68
+ existsSync: (candidate) => node_path_1.default.resolve(candidate) === markerPath || base.existsSync(candidate),
69
+ lstatSync: (candidate) => node_path_1.default.resolve(candidate) === markerPath
70
+ ? { isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false }
71
+ : base.lstatSync(candidate),
72
+ readFileSync: ((candidate, encoding) => {
73
+ if (node_path_1.default.resolve(candidate) !== markerPath) {
74
+ return encoding ? base.readFileSync(candidate, encoding) : base.readFileSync(candidate);
75
+ }
76
+ return encoding ? markerBytes.toString(encoding) : Buffer.from(markerBytes);
77
+ }),
78
+ };
79
+ return withInstallFs(overlay, fn);
80
+ }
81
+ function isRuntimeSurfaceSourceUnavailable(message) {
82
+ return message.startsWith('Runtime Surface source is unavailable or incomplete for ') &&
83
+ message.endsWith('install or upgrade gsd-core before materializing this surface.');
84
+ }
85
+ function assertCorpusTreeHasNoSymlinks(root) {
86
+ if (!installFs().existsSync(root))
87
+ return;
88
+ const stat = installFs().lstatSync(root);
89
+ if (stat.isSymbolicLink()) {
90
+ throw new Error(`Runtime Surface corpus path is a symlink: ${root}`);
91
+ }
92
+ if (!stat.isDirectory())
93
+ return;
94
+ for (const name of installFs().readdirSync(root)) {
95
+ assertCorpusTreeHasNoSymlinks(node_path_1.default.join(root, name));
96
+ }
97
+ }
98
+ function previousOwnedCorpusFiles(configDir, prefix) {
99
+ try {
100
+ const files = installerMigrations.readInstallManifest(configDir).files;
101
+ return Object.keys(files)
102
+ .filter((entry) => entry.startsWith(prefix))
103
+ .map((entry) => entry.slice(prefix.length))
104
+ .filter((entry) => entry !== '' && !node_path_1.default.posix.isAbsolute(entry) && !entry.split('/').some((part) => part === '' || part === '.' || part === '..'));
105
+ }
106
+ catch {
107
+ // An absent or unreadable prior manifest provides no ownership evidence.
108
+ // Preserve existing entries rather than guessing that they are stale.
109
+ return [];
110
+ }
111
+ }
112
+ function pruneEmptyCorpusParents(start, stop) {
113
+ let current = node_path_1.default.dirname(start);
114
+ while (current !== stop && current.startsWith(stop + node_path_1.default.sep)) {
115
+ if (installFs().readdirSync(current).length > 0)
116
+ return;
117
+ installFs().rmdirSync(current);
118
+ current = node_path_1.default.dirname(current);
119
+ }
120
+ }
121
+ function syncRuntimeSurfaceCorpus(source, destination, configDir, manifestPrefix) {
122
+ if (hasExistingSymlinkBetween(node_path_1.default.resolve(configDir), destination, { allowOptInFollow: isSymlinkedDestOptIn() })) {
123
+ throw new Error(`syncRuntimeSurfaceCorpus: destination "${destination}" contains a symlink the install root "${configDir}" does not trust — refusing to write.`);
124
+ }
125
+ assertCorpusTreeHasNoSymlinks(destination);
126
+ // Remove only paths the previous manifest proves GSD owned and which the
127
+ // executing package no longer ships. Unknown neighbouring files survive.
128
+ for (const relative of previousOwnedCorpusFiles(configDir, manifestPrefix)) {
129
+ const sourceEntry = node_path_1.default.join(source, ...relative.split('/'));
130
+ let sourceIsFile = false;
131
+ try {
132
+ sourceIsFile = installFs().lstatSync(sourceEntry).isFile();
133
+ }
134
+ catch {
135
+ sourceIsFile = false;
136
+ }
137
+ if (sourceIsFile)
138
+ continue;
139
+ const target = node_path_1.default.join(destination, ...relative.split('/'));
140
+ if (!installFs().existsSync(target))
141
+ continue;
142
+ const targetStat = installFs().lstatSync(target);
143
+ if (!targetStat.isFile()) {
144
+ throw new Error(`Runtime Surface corpus ownership conflict at ${target}`);
145
+ }
146
+ installFs().rmSync(target, { force: true });
147
+ pruneEmptyCorpusParents(target, destination);
148
+ }
149
+ installFs().mkdirSync(node_path_1.default.dirname(destination), { recursive: true });
150
+ installFs().cpSync(source, destination, { recursive: true });
151
+ }
152
+ /**
153
+ * Provision the raw, installation-owned input needed to re-materialize a
154
+ * global Runtime Surface after the executing package tree disappears.
155
+ *
156
+ * The corpus deliberately lives below the already-installed `gsd-core/`
157
+ * tree and is accepted only after its manifest ownership and hashes verify.
158
+ * The compatibility marker does not replace that installed-corpus authority.
159
+ */
160
+ function provisionRuntimeSurfaceCorpus(layout, configDir, scope) {
161
+ const required = new Set();
162
+ if ((0, install_scope_cjs_1.isGlobalScope)(scope)) {
163
+ for (const kind of layout.kinds) {
164
+ if (kind.kind === 'commands' || kind.kind === 'skills')
165
+ required.add('commands');
166
+ if (kind.kind === 'agents' || kind.kind === 'kimi-agents')
167
+ required.add('agents');
168
+ }
169
+ }
170
+ if (required.size === 0)
171
+ return;
172
+ const corpusRoot = node_path_1.default.join(configDir, 'gsd-core');
173
+ if (required.has('commands')) {
174
+ const source = runtimeArtifactLayout.findInstallSourceRoot();
175
+ const destination = node_path_1.default.join(corpusRoot, 'commands', 'gsd');
176
+ syncRuntimeSurfaceCorpus(source, destination, configDir, 'gsd-core/commands/gsd/');
177
+ }
178
+ if (required.has('agents')) {
179
+ const source = node_path_1.default.join(executingPackageRoot(), 'agents');
180
+ const destination = node_path_1.default.join(corpusRoot, 'agents');
181
+ syncRuntimeSurfaceCorpus(source, destination, configDir, 'gsd-core/agents/');
182
+ }
183
+ const markerFile = _hostBehaviors(layout.runtime).sourceMarkerFile;
184
+ if (typeof markerFile === 'string' && markerFile !== '' && required.has('commands')) {
185
+ try {
186
+ const markerPath = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, markerFile);
187
+ if (hasExistingSymlinkBetween(node_path_1.default.resolve(configDir), markerPath, { allowOptInFollow: isSymlinkedDestOptIn() })) {
188
+ throw new Error(`compatibility marker "${markerPath}" contains an untrusted symlink`);
189
+ }
190
+ installFs().writeFileSync(markerPath, node_path_1.default.join(corpusRoot, 'commands', 'gsd') + '\n', 'utf8');
191
+ }
192
+ catch {
193
+ // The existing installer marker writer owns the user-facing warning and
194
+ // keeps marker failure non-fatal. The installed corpus remains usable
195
+ // without the compatibility marker.
196
+ }
197
+ }
198
+ }
199
+ function executingPackageRoot() { return node_path_1.default.dirname(node_path_1.default.dirname(runtimeArtifactLayout.findInstallSourceRoot())); }
58
200
  // ---------------------------------------------------------------------------
59
201
  // USER_OWNED_ARTIFACTS
60
202
  // ---------------------------------------------------------------------------
@@ -977,6 +1119,7 @@ function _runLegacyUninstallCleanup(runtime, configDir, scope = 'global') {
977
1119
  */
978
1120
  function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution = () => undefined, capabilityRegistry, deps = {}) {
979
1121
  return withInstallFs(deps.fs, () => {
1122
+ const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, configDir, scope, capabilityRegistry);
980
1123
  // A removed descriptor kind is no longer visited by the layout loop, so it
981
1124
  // cannot prune its own previous output. Clean manifest-proven retired files
982
1125
  // before materializing the current layout (#2644).
@@ -986,6 +1129,7 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
986
1129
  // generic layout-driven loop below, mirroring the bespoke install path that
987
1130
  // previously lived inline in bin/install.js.
988
1131
  const behaviors = _hostBehaviors(runtime);
1132
+ const projectDir = scope === 'global' ? process.cwd() : configDir;
989
1133
  if (behaviors.combinedFamilyInstall) {
990
1134
  // #2329: combined-family runtimes (OpenCode/Kilo) bypass
991
1135
  // _runLegacyInstallMigrations below entirely (early return), so their
@@ -994,17 +1138,24 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
994
1138
  // #2874 design row 2: this early return must ALSO return an executed
995
1139
  // plan — installOpencodeFamilyArtifacts reports what it wrote, so a
996
1140
  // whole runtime family returning undefined is no longer a hole.
997
- return installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution, behaviors, capabilityRegistry);
1141
+ // An injected filesystem supplies its own hermetic corpus fixture. The
1142
+ // real installer is the authority that refreshes package bytes into the
1143
+ // durable installed corpus; attempting that cross-filesystem copy through
1144
+ // an in-memory destination adapter would read from the wrong filesystem.
1145
+ if (!deps.fs)
1146
+ provisionRuntimeSurfaceCorpus(layout, configDir, scope);
1147
+ return installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution, behaviors, capabilityRegistry, deps.packageRoot, projectDir);
998
1148
  }
999
1149
  // Legacy cleanup before layout-driven writes
1000
1150
  _runLegacyInstallMigrations(runtime, configDir, scope);
1001
- const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, configDir, scope, capabilityRegistry);
1151
+ if (!deps.fs)
1152
+ provisionRuntimeSurfaceCorpus(layout, configDir, scope);
1002
1153
  // #3712: a global `home` override escapes the sandboxed configDir. Refuse to
1003
1154
  // execute when a test run would land that escape in the developer's real home.
1004
1155
  testHomeGuard.assertTestHomeSandboxed('installRuntimeArtifacts', runtime, layout?.kinds, {
1005
1156
  os: deps.os, env: deps.env,
1006
1157
  });
1007
- const planResult = runtimeArtifactInstallPlan.createRuntimeArtifactInstallPlan({
1158
+ const createPlan = () => runtimeArtifactInstallPlan.createRuntimeArtifactInstallPlan({
1008
1159
  // `Layout` is structurally identical across the layout/install-plan .cjs
1009
1160
  // modules but nominally distinct to tsc (untyped .cjs boundary) — bridge it.
1010
1161
  layout: layout,
@@ -1012,7 +1163,16 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
1012
1163
  homedir: () => node_os_1.default.homedir(),
1013
1164
  platform: process.platform,
1014
1165
  resolveAttribution,
1166
+ projectDir,
1015
1167
  });
1168
+ let planResult = createPlan();
1169
+ if (scope === 'global' &&
1170
+ !planResult.ok &&
1171
+ planResult.kind === 'stage_failed' &&
1172
+ planResult.cleanupDirs.length === 0 &&
1173
+ isRuntimeSurfaceSourceUnavailable(planResult.message)) {
1174
+ planResult = withInstallerPackageSource(configDir, createPlan);
1175
+ }
1016
1176
  const cleanupDirs = planResult.ok ? planResult.plan.cleanupDirs : planResult.cleanupDirs;
1017
1177
  // #2874 row 1/4/5: per-kind executed-plan entries, appended only as the
1018
1178
  // loop below actually finishes writing each kind — a kind that throws
@@ -1176,8 +1336,10 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
1176
1336
  // is safe even when configDir has no .gsd-source marker (artifactLayout: []).
1177
1337
  let nativePluginInstalled = false;
1178
1338
  if (behaviors.nativePlugin) {
1179
- const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
1180
- const src = node_path_1.default.dirname(node_path_1.default.dirname(commandsGsdDir));
1339
+ // Native plugin sources live only in the executing package, never in the
1340
+ // durable Runtime Surface corpus. Do not derive this package root from a
1341
+ // config-scoped provider that may now correctly resolve installed input.
1342
+ const src = deps.packageRoot ?? executingPackageRoot();
1181
1343
  _installNativePluginIfDeclared(runtime, configDir, behaviors, src);
1182
1344
  nativePluginInstalled = true;
1183
1345
  }
@@ -1388,12 +1550,13 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
1388
1550
  * @param capabilityRegistry - #2362: optional composed capability registry, threaded
1389
1551
  * straight through to resolveRuntimeArtifactLayout (unused by the agents kind today,
1390
1552
  * but kept for signature parity with the skills/commands siblings on this call tree)
1553
+ * @param projectDir - project/config discovery root, distinct from the artifact destination
1391
1554
  * @returns `{ sourceDir, destDir }` describing what was written, or `null` when the
1392
1555
  * runtime's layout declares no `agents` kind.
1393
1556
  */
1394
- function installAgentsKindStandalone(runtime, targetDir, scope, resolvedProfile, pathPrefix, resolveAttribution = () => undefined, capabilityRegistry) {
1557
+ function installAgentsKindStandalone(runtime, targetDir, scope, resolvedProfile, pathPrefix, resolveAttribution = () => undefined, capabilityRegistry, projectDir) {
1395
1558
  const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, targetDir, scope, capabilityRegistry);
1396
- const agentsKindEntry = layout.kinds.find((k) => k.kind === 'agents');
1559
+ const agentsKindEntry = layout.kinds.find((kind) => kind.kind === 'agents');
1397
1560
  if (!agentsKindEntry)
1398
1561
  return null;
1399
1562
  // #3712: this writer selects `agentsKindEntry.home` over targetDir below and then
@@ -1408,8 +1571,16 @@ function installAgentsKindStandalone(runtime, targetDir, scope, resolvedProfile,
1408
1571
  // for the generic layout-driven loop (runtime-artifact-install-plan.cts) —
1409
1572
  // targetDir IS the install root the inline agent loop called `targetDir`.
1410
1573
  const attribution = resolveAttribution ? resolveAttribution(runtime) : undefined;
1411
- const agentCtx = { runtime, pathPrefix, attribution, targetDir };
1412
- const stagedDir = agentsKindEntry.stage(resolvedProfile, agentCtx);
1574
+ const agentCtx = { runtime, pathPrefix, attribution, targetDir, projectDir: projectDir ?? targetDir };
1575
+ let stagedDir;
1576
+ try {
1577
+ stagedDir = agentsKindEntry.stage(resolvedProfile, agentCtx);
1578
+ }
1579
+ catch (err) {
1580
+ if (scope !== 'global' || !isRuntimeSurfaceSourceUnavailable(err.message))
1581
+ throw err;
1582
+ stagedDir = withInstallerPackageSource(targetDir, () => agentsKindEntry.stage(resolvedProfile, agentCtx));
1583
+ }
1413
1584
  const stagedAgentFiles = installFs().existsSync(stagedDir)
1414
1585
  ? installFs().readdirSync(stagedDir).filter((f) => f.endsWith('.md'))
1415
1586
  : [];
@@ -1670,12 +1841,13 @@ function _migrateLegacyOpencodeCommandDir(runtime, configDir, behaviors) {
1670
1841
  * installOpencodeFamilySkills so an installed third-party capability skill
1671
1842
  * materializes for this combined-family (OpenCode/Kilo) install path too.
1672
1843
  * Absent -> no third-party skills staged (fail closed).
1844
+ * @param projectDir - project/config discovery root, distinct from configDir for global installs
1673
1845
  * @returns #2874 design row 2: an executed-plan value, same top-level shape
1674
1846
  * (`runtime`/`scope`/`kinds`/`cleanup`/`postSteps`) as the generic
1675
1847
  * `installRuntimeArtifacts` branch — this was the one early return a
1676
1848
  * `void`-shaped hole survived unnoticed in.
1677
1849
  */
1678
- function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution = () => undefined, behaviors = {}, capabilityRegistry) {
1850
+ function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution = () => undefined, behaviors = {}, capabilityRegistry, packageRoot, projectDir) {
1679
1851
  // #2870: `scope` keeps its exported required `string` signature (no
1680
1852
  // signature change). It is always the `installRuntimeArtifacts`-forwarded
1681
1853
  // 'global' | 'local' literal produced by bin/install.js's scope-resolution
@@ -1688,7 +1860,7 @@ function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfi
1688
1860
  // into stageSkillsForProfile/stageSkillsForRuntimeAsSkills. The repo/package
1689
1861
  // root (needed below for the native plugin source) is two levels up.
1690
1862
  const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
1691
- const src = node_path_1.default.dirname(node_path_1.default.dirname(commandsGsdDir));
1863
+ const src = packageRoot ?? executingPackageRoot();
1692
1864
  const rawCommandsDir = installProfiles.stageSkillsForProfile(commandsGsdDir, resolvedProfile);
1693
1865
  const pathPrefix = runtimeArtifactConversion._computePathPrefix({
1694
1866
  isGlobal,
@@ -1711,7 +1883,7 @@ function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfi
1711
1883
  // generic layout-driven loop uses (see installAgentsKindStandalone's own
1712
1884
  // doc). A `null` result means this runtime's layout declares no `agents`
1713
1885
  // kind — nothing written, nothing reported (no #1879-F15 inert claim).
1714
- const agentsResult = installAgentsKindStandalone(runtime, configDir, scope, resolvedProfile, pathPrefix, resolveAttribution, capabilityRegistry);
1886
+ const agentsResult = installAgentsKindStandalone(runtime, configDir, scope, resolvedProfile, pathPrefix, resolveAttribution, capabilityRegistry, projectDir);
1715
1887
  _installNativePluginIfDeclared(runtime, configDir, behaviors, src);
1716
1888
  // #2874 design row 2: report what this combined-family install wrote,
1717
1889
  // mirroring the generic branch's top-level shape. `cleanup` is `[]` — this