@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
@@ -276,8 +276,22 @@ const kiloAgentPermissionOrder = [
276
276
  'codesearch',
277
277
  'lsp',
278
278
  ];
279
+ const kiloMcpPermissionPattern = /^mcp__([A-Za-z0-9_-]+)__((?:[A-Za-z0-9_-]+)|\*)$/;
280
+ /**
281
+ * Derive Kilo's native `{server}_{tool}` MCP permission key (kilo.ai/docs —
282
+ * external, fixed format we don't control). Not injective: both capture
283
+ * groups allow `_`, so e.g. `mcp__a_b__c` and `mcp__a__b_c` derive the same
284
+ * key. `buildKiloAgentPermissionBlock`'s `Set` resolves any such collision
285
+ * deterministically to first-seen-wins — see its regression test. Changing
286
+ * the derivation to avoid the collision isn't an option: Kilo's own runtime
287
+ * only recognizes this exact key shape.
288
+ */
279
289
  function convertClaudeToKiloPermissionTool(claudeTool) {
280
- return claudeToKiloAgentPermissions[claudeTool] || null;
290
+ const builtinPermission = claudeToKiloAgentPermissions[claudeTool];
291
+ if (builtinPermission)
292
+ return builtinPermission;
293
+ const mcpPermission = kiloMcpPermissionPattern.exec(claudeTool);
294
+ return mcpPermission ? `${mcpPermission[1]}_${mcpPermission[2]}` : null;
281
295
  }
282
296
  function buildKiloAgentPermissionBlock(claudeTools) {
283
297
  const allowedPermissions = new Set();
@@ -291,6 +305,11 @@ function buildKiloAgentPermissionBlock(claudeTools) {
291
305
  for (const permission of kiloAgentPermissionOrder) {
292
306
  lines.push(` ${permission}: ${allowedPermissions.has(permission) ? 'allow' : 'deny'}`);
293
307
  }
308
+ for (const permission of allowedPermissions) {
309
+ if (kiloAgentPermissionOrder.includes(permission))
310
+ continue;
311
+ lines.push(` ${permission}: allow`);
312
+ }
294
313
  return lines;
295
314
  }
296
315
  function replaceRelativePathReference(content, fromPath, toPath) {
@@ -645,6 +664,137 @@ function convertClaudeCommandToKimiCodeSkill(content, skillName, _runtime = null
645
664
  return convertClaudeCommandToKimiSkill(content, skillName, _runtime, cmdNames);
646
665
  }
647
666
  const KIMI_CANONICAL_GSD_AGENT_RE = /^gsd-[a-z0-9-]+$/;
667
+ /** Split a `tools:` comma list without tearing a quoted scalar that contains a literal comma. */
668
+ function splitToolScalars(text) {
669
+ const parts = [];
670
+ let current = '';
671
+ let quote = null;
672
+ for (const ch of text) {
673
+ if (quote) {
674
+ current += ch;
675
+ if (ch === quote)
676
+ quote = null;
677
+ }
678
+ else if (ch === '"' || ch === "'") {
679
+ quote = ch;
680
+ current += ch;
681
+ }
682
+ else if (ch === ',') {
683
+ parts.push(current);
684
+ current = '';
685
+ }
686
+ else {
687
+ current += ch;
688
+ }
689
+ }
690
+ parts.push(current);
691
+ return parts;
692
+ }
693
+ /** Normalize one complete frontmatter tool scalar through the shared YAML parser. */
694
+ function decodeToolScalar(raw) {
695
+ if (typeof raw !== 'string')
696
+ return null;
697
+ const value = raw.trim();
698
+ if (!value)
699
+ return null;
700
+ if (value.startsWith('"') || value.startsWith("'")) {
701
+ try {
702
+ const decoded = frontmatterModule.parseFrontmatter('---\ntool: ' + value + '\n---\n').tool;
703
+ return typeof decoded === 'string' && decoded.trim() ? decoded.trim() : null;
704
+ }
705
+ catch {
706
+ return null;
707
+ }
708
+ }
709
+ // A bare (unquoted) scalar may carry a trailing ` # comment` (YAML requires
710
+ // whitespace before `#` to start a comment) — every caller here passes a
711
+ // single already-comma-split token, never the whole `tools:` line, so this
712
+ // is safe to strip unconditionally rather than pushing the concern onto
713
+ // each call site. Strip BEFORE the malformed-quote check below: a comment
714
+ // may itself contain a `"`/`'` (e.g. `Bash # note: "internal"`), which is
715
+ // not a real YAML quote delimiter and must not cause a false rejection.
716
+ const commentIndex = value.search(/[ \t]#/);
717
+ const stripped = commentIndex === -1 ? value : value.slice(0, commentIndex).trimEnd();
718
+ if (!stripped)
719
+ return null;
720
+ if (stripped.endsWith('"') || stripped.endsWith("'"))
721
+ return null;
722
+ return stripped;
723
+ }
724
+ /** Append validated canonical grants without reserializing unrelated frontmatter. */
725
+ function appendAgentTools(content, grants) {
726
+ if (grants.length === 0)
727
+ return content;
728
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
729
+ const lines = content.split(eol);
730
+ if (lines[0] !== '---')
731
+ return content;
732
+ const frontmatterEnd = lines.indexOf('---', 1);
733
+ if (frontmatterEnd === -1)
734
+ return content;
735
+ const toolsIndex = lines.findIndex((line, index) => index < frontmatterEnd && /^tools:[ \t]*(.*)$/.test(line));
736
+ if (toolsIndex === -1)
737
+ return content;
738
+ const toolsMatch = /^tools:[ \t]*(.*)$/.exec(lines[toolsIndex]);
739
+ if (!toolsMatch)
740
+ return content;
741
+ // Plain (non-leading-quote) YAML scalars have no internal quoting — a `#`
742
+ // after whitespace is a real comment start regardless of nearby quote
743
+ // characters (verified: real YAML truncates `Read, "x #1"` at the space
744
+ // before `#` too), so this scan does not need to be quote-aware. The one
745
+ // case that DOES need protection — a value that IS a leading quoted scalar
746
+ // — is refused outright below rather than parsed past.
747
+ const commentIndex = toolsMatch[1].search(/[ \t]#/);
748
+ let inlineValue = commentIndex === -1 ? toolsMatch[1] : toolsMatch[1].slice(0, commentIndex);
749
+ let inlineComment = commentIndex === -1 ? '' : toolsMatch[1].slice(commentIndex);
750
+ // A header that is ONLY a comment (`tools: # note`) has no leading space in
751
+ // the captured group — the outer regex's `[ \t]*` already consumed it — so
752
+ // the `[ \t]#` scan above never fires. Reclassify as no inline value so the
753
+ // block-list scan below runs instead of swallowing the comment as content.
754
+ if (inlineValue.trim().startsWith('#')) {
755
+ inlineComment = toolsMatch[1];
756
+ inlineValue = '';
757
+ }
758
+ // A value that STARTS with a quote is a YAML quoted scalar occupying the
759
+ // whole node — nothing may follow it on the same line except a comment
760
+ // (`tools: "Bash"` is valid; `tools: "Bash", Read` is not, even before this
761
+ // function touches it). Appending in place would corrupt otherwise-valid
762
+ // frontmatter, so refuse rather than emit invalid YAML. A value that STARTS
763
+ // with `[` is a YAML flow sequence (`tools: [Bash, Read]`) — its own commas
764
+ // are node-internal, not scalar separators, and content cannot follow its
765
+ // closing `]` on the same line either, so the same refusal applies.
766
+ if (/^["'[]/.test(inlineValue.trim()))
767
+ return content;
768
+ const existing = [];
769
+ let insertAt = toolsIndex + 1;
770
+ if (inlineValue.trim()) {
771
+ existing.push(...splitToolScalars(inlineValue).map(decodeToolScalar).filter((value) => value !== null));
772
+ }
773
+ else {
774
+ while (insertAt < frontmatterEnd) {
775
+ const item = /^([ \t]+)-[ \t]*(\S.*)$/.exec(lines[insertAt]);
776
+ if (!item)
777
+ break;
778
+ const decoded = decodeToolScalar(item[2]);
779
+ if (decoded !== null)
780
+ existing.push(decoded);
781
+ insertAt += 1;
782
+ }
783
+ }
784
+ const present = new Set(existing);
785
+ const additions = grants.filter((grant) => !present.has(grant) && (present.add(grant), true));
786
+ if (additions.length === 0)
787
+ return content;
788
+ if (inlineValue.trim()) {
789
+ lines[toolsIndex] = `tools: ${inlineValue.trimEnd()}, ${additions.join(', ')}${inlineComment}`;
790
+ }
791
+ else {
792
+ const firstItem = /^([ \t]+)-/.exec(lines[toolsIndex + 1]);
793
+ const indent = firstItem ? firstItem[1] : ' ';
794
+ lines.splice(insertAt, 0, ...additions.map((grant) => `${indent}- ${JSON.stringify(grant)}`));
795
+ }
796
+ return lines.join(eol);
797
+ }
648
798
  function parseKimiAgentSource(source) {
649
799
  if (typeof source === 'string') {
650
800
  return {
@@ -672,7 +822,9 @@ function parseFrontmatterTools(frontmatter) {
672
822
  continue;
673
823
  if (collecting) {
674
824
  if (trimmed.startsWith('- ')) {
675
- tools.push(trimmed.slice(2).trim());
825
+ const tool = decodeToolScalar(trimmed.slice(2));
826
+ if (tool !== null)
827
+ tools.push(tool);
676
828
  continue;
677
829
  }
678
830
  collecting = false;
@@ -683,10 +835,13 @@ function parseFrontmatterTools(frontmatter) {
683
835
  }
684
836
  if (trimmed.startsWith('tools:') || trimmed.startsWith('allowed-tools:')) {
685
837
  const value = trimmed.slice(trimmed.indexOf(':') + 1).trim();
686
- if (value) {
687
- for (const tool of value.split(',')) {
688
- const name = tool.trim();
689
- if (name)
838
+ // A comment-only value (`tools: # note`) has no real inline content —
839
+ // fall through to the block-list scan below instead of decoding the
840
+ // comment text as a bogus tool name and silently dropping the list.
841
+ if (value && !value.startsWith('#')) {
842
+ for (const tool of splitToolScalars(value)) {
843
+ const name = decodeToolScalar(tool);
844
+ if (name !== null)
690
845
  tools.push(name);
691
846
  }
692
847
  }
@@ -1623,26 +1778,37 @@ Execute mode fallback:
1623
1778
  ## C. Task() → spawn_agent Mapping
1624
1779
  GSD workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collaboration tools:
1625
1780
 
1626
- **Schema detection (required first step):** Codex exposes two \`spawn_agent\` schemas:
1627
- - **agent_type-capable schema** (e.g. \`multi_agent_v2\`): \`spawn_agent\` accepts \`agent_type\`, \`message\`, \`reasoning_effort\`, \`fork_context\`, etc. — typed GSD agent dispatch is available.
1628
- - **Generic schema** (\`multi_agent_v1\`): \`spawn_agent\` accepts only \`message\`, \`items\`, \`fork_context\` — there is **no \`agent_type\` field**. Typed GSD agent dispatch is unavailable in this session.
1781
+ **Schema detection (required first step):** Before spawning, inspect the \`spawn_agent\`
1782
+ tool's visible parameter schema (via \`tool_search\` or the tool list). Use the presence
1783
+ of \`agent_type\` only to choose typed dispatch versus the generic-agent workaround.
1784
+ Detect optional fields independently: \`model\`, \`reasoning_effort\`, \`task_name\`,
1785
+ \`fork_turns\`, and \`fork_context\` may be added or removed without \`agent_type\` changing.
1786
+ Never infer one field from a schema/version label or from the presence of another field.
1629
1787
 
1630
- Before spawning, inspect the \`spawn_agent\` tool's visible parameter schema (via \`tool_search\` or the tool list) to determine which form is active.
1788
+ - **agent_type-capable schema:** \`spawn_agent\` advertises \`agent_type\` — typed GSD agent dispatch is available.
1789
+ - **Generic schema:** \`spawn_agent\` does not advertise \`agent_type\` — typed GSD agent dispatch is unavailable in this session, even if other optional fields are present.
1631
1790
 
1632
1791
  Typed mapping (agent_type-capable schema only):
1633
1792
  - \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
1634
1793
  - \`Agent(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
1635
- - \`Task(model="...")\` → omit. \`spawn_agent\` has no inline \`model\` parameter;
1636
- GSD embeds the resolved per-agent model directly into each agent's \`.toml\`
1637
- at install time so \`model_overrides\` from \`.planning/config.json\` and
1638
- \`~/.gsd/defaults.json\` are honored automatically by Codex's agent router.
1639
- - Resolved \`reasoning_effort="low|medium|high|xhigh"\` (\`xhigh\` is a GSD/Codex tier, not a generic runtime enum) → pass \`reasoning_effort\`
1640
- to \`spawn_agent\` when the runtime/tool supports it. Omit missing, empty,
1641
- inherited, or unsupported values; do not invent one-off effort literals in
1642
- workflow prose.
1794
+ - \`Task(model="{resolved_model}")\` → pass \`model="{resolved_model}"\` when the
1795
+ visible \`spawn_agent\` schema advertises \`model\` and the resolved value is explicit.
1796
+ This is how \`model_profile\` tier routing, including \`adaptive\`, reaches the child agent.
1797
+ Omit \`model\` only when the schema does not advertise \`model\`, or when the value is
1798
+ missing, empty, or \`"inherit"\`; omission deliberately inherits the session/static agent
1799
+ configuration. Explicit \`model_overrides\` may also be embedded in agent \`.toml\` files,
1800
+ but ordinary profile-resolved models are not, so a TOML file is not a reason to discard
1801
+ an available inline value.
1802
+ - Before each typed spawn, obtain the paired effort for its role with
1803
+ \`gsd_run query resolve-model <subagent_type> --pick effort\` when the workflow has not
1804
+ already exposed it. The resolver's unified \`effort\` field maps to the Codex spawn argument
1805
+ \`reasoning_effort\`; do not look for a resolver field named \`reasoning_effort\`.
1806
+ Pass it when the visible \`spawn_agent\` schema advertises \`reasoning_effort\`. Omit the
1807
+ field when it is not advertised, or when the value is missing, empty, \`"inherit"\`, or
1808
+ unsupported; do not invent one-off effort literals in workflow prose.
1643
1809
  - \`fork_context: false\` by default — GSD agents load their own context via \`<required_reading>\` blocks
1644
- - \`task_name\` — required by the collaboration schema; provide a descriptive name for each spawned task
1645
- - \`fork_turns\` — optional parameter controlling turn-forking depth; coexists with \`fork_context\` (not a replacement)
1810
+ - \`task_name\` — when advertised, provide a descriptive name for each spawned task
1811
+ - \`fork_turns\` — when advertised, controls turn-forking depth; coexists with \`fork_context\` (not a replacement)
1646
1812
  - \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping,
1647
1813
  but Codex declares \`dispatch.isolation: orchestrator-worktree\` (#2584). Codex
1648
1814
  \`spawn_agent\` still does not create or bind a git worktree; instead GSD itself
@@ -1948,7 +2114,9 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverrid
1948
2114
  }
1949
2115
  if (isAgent && inAgentTools) {
1950
2116
  if (trimmed.startsWith('- ')) {
1951
- agentTools.push(trimmed.substring(2).trim());
2117
+ const tool = decodeToolScalar(trimmed.substring(2));
2118
+ if (tool !== null)
2119
+ agentTools.push(tool);
1952
2120
  continue;
1953
2121
  }
1954
2122
  if (trimmed && !trimmed.startsWith('-')) {
@@ -1959,8 +2127,11 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverrid
1959
2127
  if (trimmed.startsWith('tools:')) {
1960
2128
  if (isAgent) {
1961
2129
  const toolsValue = trimmed.substring(6).trim();
1962
- if (toolsValue) {
1963
- const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t);
2130
+ // A comment-only value (`tools: # note`) is not real inline content —
2131
+ // fall through to the block-list scan (`inAgentTools`) instead of
2132
+ // decoding the comment as a bogus tool name and dropping the list.
2133
+ if (toolsValue && !toolsValue.startsWith('#')) {
2134
+ const tools = splitToolScalars(toolsValue).map(decodeToolScalar).filter((tool) => tool !== null);
1964
2135
  agentTools.push(...tools);
1965
2136
  }
1966
2137
  else {
@@ -2022,7 +2193,9 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverrid
2022
2193
  if (trimmed.startsWith('- ')) {
2023
2194
  const tool = trimmed.substring(2).trim();
2024
2195
  if (isAgent) {
2025
- agentTools.push(tool);
2196
+ const decoded = decodeToolScalar(tool);
2197
+ if (decoded !== null)
2198
+ agentTools.push(decoded);
2026
2199
  }
2027
2200
  else {
2028
2201
  allowedTools.push(tool);
@@ -2355,11 +2528,16 @@ function convertClaudeAgentToQwenAgent(content) {
2355
2528
  * Byte-identical for an agent with no `mcp__*` grants (the common case) and
2356
2529
  * for an agent with no frontmatter at all.
2357
2530
  */
2531
+ /** Fail-closed: an undecodable scalar is dropped, never kept (ZCode's contract is "never emit mcp__*"). */
2532
+ function zcodeKeepsGrant(rawTool) {
2533
+ const decoded = decodeToolScalar(rawTool);
2534
+ return decoded !== null && !decoded.startsWith('mcp__');
2535
+ }
2358
2536
  function convertClaudeAgentToZcodeAgent(content) {
2359
- // Fast path: no MCP grant token anywhere means nothing to strip. (A body
2360
- // mention alone is not a grant — the line scan below finds no tools-line
2361
- // change and returns `content` unchanged anyway; this just skips the scan.)
2362
- if (!content.includes('mcp__'))
2537
+ // A double-quoted YAML scalar may encode the leading "m" in mcp__ as an
2538
+ // escape, so only skip the shared scalar-decoder scan when neither form is
2539
+ // present. The unchanged scan below still preserves byte-identical content.
2540
+ if (!content.includes('mcp__') && !content.includes('\\'))
2363
2541
  return content;
2364
2542
  const lines = content.split('\n');
2365
2543
  if (lines[0] !== '---')
@@ -2379,9 +2557,14 @@ function convertClaudeAgentToZcodeAgent(content) {
2379
2557
  while (i < fmEnd) {
2380
2558
  const line = lines[i];
2381
2559
  const inlineTools = /^tools:[ \t]*(.+)$/.exec(line);
2382
- if (inlineTools) {
2383
- const grants = inlineTools[1].split(',').map((tool) => tool.trim()).filter((tool) => tool !== '');
2384
- const kept = grants.filter((tool) => !tool.startsWith('mcp__'));
2560
+ // A header that is ONLY a comment (`tools: # note`) is not real inline
2561
+ // content — fall through to the block-list scan below instead of
2562
+ // matching here, or a following block list's mcp__* items never get
2563
+ // scanned and leak through unstripped.
2564
+ const commentOnlyHeader = inlineTools !== null && inlineTools[1].trim().startsWith('#');
2565
+ if (inlineTools && !commentOnlyHeader) {
2566
+ const grants = splitToolScalars(inlineTools[1]).map((tool) => tool.trim()).filter((tool) => tool !== '');
2567
+ const kept = grants.filter((tool) => zcodeKeepsGrant(tool));
2385
2568
  if (kept.length === grants.length) {
2386
2569
  out.push(line); // no mcp__* grants — keep the line byte-identical
2387
2570
  }
@@ -2395,7 +2578,7 @@ function convertClaudeAgentToZcodeAgent(content) {
2395
2578
  i++;
2396
2579
  continue;
2397
2580
  }
2398
- if (/^tools:[ \t]*$/.test(line)) {
2581
+ if (/^tools:[ \t]*$/.test(line) || commentOnlyHeader) {
2399
2582
  // Block-list form: collect the following `- item` lines.
2400
2583
  const items = [];
2401
2584
  let j = i + 1;
@@ -2405,7 +2588,7 @@ function convertClaudeAgentToZcodeAgent(content) {
2405
2588
  }
2406
2589
  const kept = items.filter((item) => {
2407
2590
  const name = /^([ \t]*)-[ \t]*(\S.*)$/.exec(item)[2].trim();
2408
- return !name.startsWith('mcp__');
2591
+ return zcodeKeepsGrant(name);
2409
2592
  });
2410
2593
  if (kept.length !== items.length) {
2411
2594
  changed = true;
@@ -2940,6 +3123,20 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2940
3123
  content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
2941
3124
  content = processAttribution(content, attribution);
2942
3125
  break;
3126
+ case 'zcode':
3127
+ // #4002: ZCode is a Claude-Code-shaped host (dot-home `.zcode`, `@~`-ref
3128
+ // expansion, `~/.zcode/...` documented paths) whose commands install with
3129
+ // `converter: null` — this pass is their only chance to receive
3130
+ // runtime-correct paths. Same shape as `claude`, including the tilde
3131
+ // restore: the tilde form is what ZCode expands and what its docs use.
3132
+ // `${_GSD_RUNTIME_ROOT}/.claude/…` matches none of these regexes and
3133
+ // survives as the project-local fallback, exactly as on every sibling.
3134
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
3135
+ content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
3136
+ content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
3137
+ content = restoreClaudeGlobalAtRefTilde(content, pathPrefix);
3138
+ content = processAttribution(content, attribution);
3139
+ break;
2943
3140
  default:
2944
3141
  // Unknown runtime — no rewrites (OpenCode/Kilo handled by their own install path).
2945
3142
  break;
@@ -3364,6 +3561,7 @@ function processAttribution(content, attribution) {
3364
3561
  }
3365
3562
  module.exports = {
3366
3563
  processAttribution,
3564
+ appendAgentTools,
3367
3565
  // #2103: public accessor for hostBehaviors.agentFileExtension, exported so
3368
3566
  // surface.cts's _syncGsdDir can derive the .agent.md rename from the SAME
3369
3567
  // descriptor read as install-engine.cts (folds a duplicated hardcoded
@@ -3420,6 +3618,8 @@ module.exports = {
3420
3618
  // opencode/kilo command install through the engine instead of the bespoke path).
3421
3619
  convertClaudeToOpencodeFrontmatter,
3422
3620
  convertClaudeToKiloFrontmatter,
3621
+ _decodeToolScalar: decodeToolScalar,
3622
+ _splitToolScalars: splitToolScalars,
3423
3623
  readGsdCommandNames,
3424
3624
  transformContentToHyphen,
3425
3625
  // #1383: version resolver (exported for regression test of the Codex
@@ -51,7 +51,7 @@ function addCleanupDir(cleanupDirs, stagedDir, rewrittenDir) {
51
51
  return sourceDir;
52
52
  }
53
53
  function createRuntimeArtifactInstallPlan(args) {
54
- const { layout, resolvedProfile, homedir, platform, resolveAttribution, deps = {}, } = args;
54
+ const { layout, resolvedProfile, homedir, platform, resolveAttribution, projectDir, deps = {}, } = args;
55
55
  const conversionExports = _require('./runtime-artifact-conversion.cjs');
56
56
  const rewriteStagedSkillBodies = deps.rewriteStagedSkillBodies ?? conversionExports.rewriteStagedSkillBodies;
57
57
  const rewriteStagedCommandBodies = deps.rewriteStagedCommandBodies ?? conversionExports.rewriteStagedCommandBodies;
@@ -66,11 +66,10 @@ function createRuntimeArtifactInstallPlan(args) {
66
66
  platform,
67
67
  resolveAttribution,
68
68
  };
69
- // ADR-1235 §1: build agentCtx once per plan so agents kind entries can apply
70
- // the CORRECT pre-converter cross-cutting (path rewrites → attribution → converter
71
- // → normalize). This mirrors the exact per-file order in the inline agent loop
72
- // in bin/install.js (lines 9330-9415). agentCtx is passed as the second arg
73
- // to kind.stage() for agents kind entries with a converter (convertedAgentsKind).
69
+ // ADR-1235 §1: build the staging context once per plan. Agent kinds apply
70
+ // the CORRECT pre-converter cross-cutting (path rewrites → attribution →
71
+ // converter → normalize). This
72
+ // mirrors the exact per-file order in the former inline agent loop.
74
73
  // NO _stampNonClaudeRuntimeDefaults — agents are NOT stamped in the inline loop.
75
74
  const os = _require('node:os');
76
75
  const { posixNormalize } = _require('./shell-command-projection.cjs');
@@ -88,19 +87,19 @@ function createRuntimeArtifactInstallPlan(args) {
88
87
  const attribution = resolveAttribution ? resolveAttribution(layout.runtime) : undefined;
89
88
  // #2875 Part 2 (row I1): layout.configDir IS the install root the inline
90
89
  // agent loop called `targetDir` — same value, same resolution.
91
- const agentCtx = { runtime: layout.runtime, pathPrefix, attribution, targetDir: layout.configDir };
90
+ const agentCtx = {
91
+ runtime: layout.runtime,
92
+ pathPrefix,
93
+ attribution,
94
+ targetDir: layout.configDir,
95
+ projectDir: projectDir ?? layout.configDir,
96
+ };
92
97
  for (const kind of layout.kinds) {
93
98
  let stagedDir;
94
99
  try {
95
- if (kind.kind === 'agents') {
96
- // ADR-1235 §1: pass agentCtx so stageAgentsForRuntimeWithConverter applies
97
- // the full inline-loop order: pathRewrites → attribution → converter → normalize.
98
- // The cross-cutting is now PRE-converter (inside staging), not POST.
99
- stagedDir = kind.stage(resolvedProfile, agentCtx);
100
- }
101
- else {
102
- stagedDir = kind.stage(resolvedProfile);
103
- }
100
+ // Agent kinds use the context for their pre-converter cross-cutting
101
+ // sequence; other kinds ignore it.
102
+ stagedDir = kind.stage(resolvedProfile, agentCtx);
104
103
  }
105
104
  catch (err) {
106
105
  return { ok: false, kind: 'stage_failed', message: errorMessage(err), cleanupDirs, failedKind: kind.kind };
@@ -115,7 +114,7 @@ function createRuntimeArtifactInstallPlan(args) {
115
114
  const rewrittenDir = rewriteStagedSkillBodies(stagedDir, rewriteOpts);
116
115
  sourceDir = addCleanupDir(cleanupDirs, stagedDir, rewrittenDir);
117
116
  }
118
- // agents kind: cross-cutting already applied INSIDE kind.stage() via agentCtx.
117
+ // Agent kinds: cross-cutting already applied INSIDE kind.stage() via agentCtx.
119
118
  // No POST-step needed. sourceDir stays as stagedDir.
120
119
  }
121
120
  catch (err) {