@opengsd/gsd-core 1.7.0 → 1.9.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 (261) 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 +45 -1
  4. package/README.md +2 -0
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-codebase-mapper.md +1 -1
  7. package/agents/gsd-debug-session-manager.md +78 -4
  8. package/agents/gsd-debugger.md +87 -29
  9. package/agents/gsd-executor.md +49 -9
  10. package/agents/gsd-intel-updater.md +3 -3
  11. package/agents/gsd-phase-researcher.md +4 -2
  12. package/agents/gsd-plan-checker.md +20 -0
  13. package/agents/gsd-planner.md +44 -59
  14. package/agents/gsd-project-researcher.md +2 -2
  15. package/agents/gsd-ui-auditor.md +0 -40
  16. package/agents/gsd-verifier.md +2 -2
  17. package/bin/install.js +1338 -135
  18. package/commands/gsd/ai-integration-phase.md +1 -1
  19. package/commands/gsd/mempalace-capture.md +9 -5
  20. package/commands/gsd/new-milestone.md +1 -1
  21. package/commands/gsd/plan-phase.md +5 -3
  22. package/commands/gsd/plan-review-convergence.md +7 -2
  23. package/gsd-core/bin/gsd-tools.cjs +2690 -2472
  24. package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
  25. package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
  26. package/gsd-core/bin/lib/api-coverage.cjs +360 -53
  27. package/gsd-core/bin/lib/audit.cjs +8 -8
  28. package/gsd-core/bin/lib/broken-windows.cjs +716 -0
  29. package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
  30. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  31. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  32. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  33. package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
  34. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  35. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  36. package/gsd-core/bin/lib/capability-writer.cjs +6 -1
  37. package/gsd-core/bin/lib/check-command-router.cjs +140 -27
  38. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  39. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
  41. package/gsd-core/bin/lib/command-aliases.cjs +14 -0
  42. package/gsd-core/bin/lib/commands.cjs +326 -21
  43. package/gsd-core/bin/lib/config-loader.cjs +214 -30
  44. package/gsd-core/bin/lib/config.cjs +158 -22
  45. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  46. package/gsd-core/bin/lib/decisions.cjs +32 -8
  47. package/gsd-core/bin/lib/docs.cjs +6 -0
  48. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  49. package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
  50. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  51. package/gsd-core/bin/lib/gap-checker.cjs +17 -2
  52. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  53. package/gsd-core/bin/lib/init.cjs +155 -66
  54. package/gsd-core/bin/lib/install-engine.cjs +299 -23
  55. package/gsd-core/bin/lib/install-profiles.cjs +239 -1
  56. package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
  57. package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
  58. package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
  59. package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
  60. package/gsd-core/bin/lib/milestone.cjs +248 -14
  61. package/gsd-core/bin/lib/model-catalog.cjs +69 -4
  62. package/gsd-core/bin/lib/model-resolver.cjs +189 -7
  63. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  64. package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
  65. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  66. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  67. package/gsd-core/bin/lib/phase-id.cjs +304 -9
  68. package/gsd-core/bin/lib/phase.cjs +258 -17
  69. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  70. package/gsd-core/bin/lib/plan-scan.cjs +70 -2
  71. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  72. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  73. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  74. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  75. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  76. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  77. package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
  78. package/gsd-core/bin/lib/roadmap.cjs +23 -7
  79. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
  80. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
  81. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
  82. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  83. package/gsd-core/bin/lib/smart-entry.cjs +70 -5
  84. package/gsd-core/bin/lib/state-document.cjs +171 -24
  85. package/gsd-core/bin/lib/state-transition.cjs +50 -11
  86. package/gsd-core/bin/lib/state.cjs +206 -32
  87. package/gsd-core/bin/lib/surface.cjs +51 -9
  88. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  89. package/gsd-core/bin/lib/uat.cjs +428 -11
  90. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  91. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  92. package/gsd-core/bin/lib/validate.cjs +44 -8
  93. package/gsd-core/bin/lib/verification.cjs +163 -31
  94. package/gsd-core/bin/lib/verify.cjs +348 -42
  95. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  96. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  97. package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
  98. package/gsd-core/bin/shared/model-catalog.json +5 -0
  99. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  100. package/gsd-core/references/api-coverage.md +37 -7
  101. package/gsd-core/references/checkpoints.md +1 -1
  102. package/gsd-core/references/common-bug-patterns.md +13 -0
  103. package/gsd-core/references/context-budget.md +40 -0
  104. package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
  105. package/gsd-core/references/debugger-fix-acceptance.md +157 -0
  106. package/gsd-core/references/debugger-philosophy.md +1 -0
  107. package/gsd-core/references/debugger-prevention.md +98 -0
  108. package/gsd-core/references/debugger-rca-branching.md +98 -0
  109. package/gsd-core/references/debugger-repro-hardening.md +130 -0
  110. package/gsd-core/references/debugger-sbfl.md +110 -0
  111. package/gsd-core/references/debugger-semantic-recall.md +81 -0
  112. package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
  113. package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
  114. package/gsd-core/references/execute-phase-response-language.md +7 -0
  115. package/gsd-core/references/gate-prompts.md +6 -3
  116. package/gsd-core/references/model-profile-resolution.md +64 -13
  117. package/gsd-core/references/offer-next.md +88 -0
  118. package/gsd-core/references/planner-antipatterns.md +6 -0
  119. package/gsd-core/references/planner-mvp-mode.md +12 -13
  120. package/gsd-core/references/planner-preconditions.md +156 -0
  121. package/gsd-core/references/planner-reversibility.md +132 -0
  122. package/gsd-core/references/planning-config.md +2 -1
  123. package/gsd-core/references/reviewer-instances.md +28 -19
  124. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  125. package/gsd-core/references/skeleton-template.md +1 -1
  126. package/gsd-core/references/thinking-models-planning.md +3 -1
  127. package/gsd-core/references/ui-consideration-probe.md +2 -2
  128. package/gsd-core/references/worktree-branch-check.md +4 -4
  129. package/gsd-core/templates/DEBUG.md +5 -3
  130. package/gsd-core/templates/summary-minimal.md +4 -0
  131. package/gsd-core/templates/summary-standard.md +4 -0
  132. package/gsd-core/templates/summary.md +7 -0
  133. package/gsd-core/workflows/add-phase.md +2 -0
  134. package/gsd-core/workflows/add-tests.md +3 -1
  135. package/gsd-core/workflows/add-todo.md +32 -1
  136. package/gsd-core/workflows/ai-integration-phase.md +8 -6
  137. package/gsd-core/workflows/audit-fix.md +6 -2
  138. package/gsd-core/workflows/audit-milestone.md +8 -0
  139. package/gsd-core/workflows/autonomous.md +19 -15
  140. package/gsd-core/workflows/check-todos.md +5 -3
  141. package/gsd-core/workflows/cleanup.md +7 -1
  142. package/gsd-core/workflows/code-review-fix.md +14 -6
  143. package/gsd-core/workflows/code-review.md +93 -24
  144. package/gsd-core/workflows/complete-milestone.md +3 -0
  145. package/gsd-core/workflows/debug.md +35 -7
  146. package/gsd-core/workflows/diagnose-issues.md +5 -1
  147. package/gsd-core/workflows/discovery-phase.md +7 -0
  148. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  149. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  150. package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
  151. package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
  152. package/gsd-core/workflows/discuss-phase.md +2 -2
  153. package/gsd-core/workflows/do.md +7 -1
  154. package/gsd-core/workflows/docs-update.md +9 -0
  155. package/gsd-core/workflows/eval-review.md +4 -1
  156. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  157. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  158. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
  159. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
  160. package/gsd-core/workflows/execute-phase.md +110 -149
  161. package/gsd-core/workflows/execute-plan.md +20 -8
  162. package/gsd-core/workflows/explore.md +4 -0
  163. package/gsd-core/workflows/extract-learnings.md +21 -0
  164. package/gsd-core/workflows/graduation.md +3 -0
  165. package/gsd-core/workflows/health.md +7 -1
  166. package/gsd-core/workflows/help/modes/full.md +9 -5
  167. package/gsd-core/workflows/import.md +11 -2
  168. package/gsd-core/workflows/inbox.md +7 -0
  169. package/gsd-core/workflows/ingest-docs.md +19 -10
  170. package/gsd-core/workflows/manager.md +3 -1
  171. package/gsd-core/workflows/map-codebase.md +17 -10
  172. package/gsd-core/workflows/mvp-phase.md +3 -0
  173. package/gsd-core/workflows/new-milestone.md +79 -23
  174. package/gsd-core/workflows/new-project.md +28 -19
  175. package/gsd-core/workflows/new-workspace.md +3 -1
  176. package/gsd-core/workflows/next.md +5 -2
  177. package/gsd-core/workflows/onboard.md +3 -0
  178. package/gsd-core/workflows/plan-phase.md +56 -51
  179. package/gsd-core/workflows/plan-review-convergence.md +61 -12
  180. package/gsd-core/workflows/plant-seed.md +3 -0
  181. package/gsd-core/workflows/profile-user.md +7 -1
  182. package/gsd-core/workflows/progress.md +31 -3
  183. package/gsd-core/workflows/quick.md +33 -10
  184. package/gsd-core/workflows/remove-workspace.md +3 -0
  185. package/gsd-core/workflows/review.md +172 -585
  186. package/gsd-core/workflows/scan.md +10 -2
  187. package/gsd-core/workflows/secure-phase.md +13 -2
  188. package/gsd-core/workflows/settings-integrations.md +3 -0
  189. package/gsd-core/workflows/settings.md +3 -0
  190. package/gsd-core/workflows/ship.md +88 -11
  191. package/gsd-core/workflows/sketch.md +3 -0
  192. package/gsd-core/workflows/smart-entry.md +4 -1
  193. package/gsd-core/workflows/spike.md +7 -1
  194. package/gsd-core/workflows/ui-phase.md +11 -2
  195. package/gsd-core/workflows/ui-review.md +11 -1
  196. package/gsd-core/workflows/undo.md +7 -0
  197. package/gsd-core/workflows/update.md +106 -5
  198. package/gsd-core/workflows/validate-phase.md +13 -2
  199. package/gsd-core/workflows/verify-phase.md +2 -2
  200. package/gsd-core/workflows/verify-work.md +15 -4
  201. package/hooks/dist/gsd-context-monitor.js +27 -9
  202. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  203. package/hooks/dist/gsd-cursor-stop.js +6 -2
  204. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  205. package/hooks/dist/gsd-graphify-update.sh +9 -0
  206. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  207. package/hooks/dist/gsd-prompt-guard.js +101 -2
  208. package/hooks/dist/gsd-read-guard.js +100 -2
  209. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  210. package/hooks/dist/gsd-statusline.js +97 -9
  211. package/hooks/dist/gsd-workflow-guard.js +110 -6
  212. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  213. package/hooks/dist/lib/cursor-workspace.js +74 -0
  214. package/hooks/gsd-context-monitor.js +27 -9
  215. package/hooks/gsd-cursor-session-start.js +6 -2
  216. package/hooks/gsd-cursor-stop.js +6 -2
  217. package/hooks/gsd-cursor-subagent-start.js +6 -2
  218. package/hooks/gsd-graphify-update.sh +9 -0
  219. package/hooks/gsd-phase-boundary.sh +14 -2
  220. package/hooks/gsd-prompt-guard.js +101 -2
  221. package/hooks/gsd-read-guard.js +100 -2
  222. package/hooks/gsd-read-injection-scanner.js +109 -2
  223. package/hooks/gsd-statusline.js +97 -9
  224. package/hooks/gsd-workflow-guard.js +110 -6
  225. package/hooks/gsd-worktree-path-guard.js +132 -8
  226. package/hooks/lib/cursor-workspace.js +74 -0
  227. package/package.json +10 -8
  228. package/pi/gsd.cjs +34 -3
  229. package/scripts/changeset/lint.cjs +1 -0
  230. package/scripts/changeset/parse.cjs +26 -0
  231. package/scripts/check-coverage-gate.cjs +51 -0
  232. package/scripts/check-glossary-refs.cjs +244 -0
  233. package/scripts/ci-rebase-check.cjs +48 -4
  234. package/scripts/ci-test-scope.cjs +67 -17
  235. package/scripts/gen-adr-index.cjs +528 -0
  236. package/scripts/gen-capability-matrix.cjs +26 -2
  237. package/scripts/gen-capability-registry.cjs +132 -34
  238. package/scripts/gen-emitted-baseline.cjs +145 -0
  239. package/scripts/gen-test-timings.cjs +201 -0
  240. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  241. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  242. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  243. package/scripts/lint-portable-timeout.cjs +140 -0
  244. package/scripts/lint-resolution-provenance.cjs +9 -0
  245. package/scripts/lint-test-file-count.allowlist.json +1 -0
  246. package/scripts/mutation-matrix.cjs +4 -0
  247. package/scripts/prompt-injection-scan.sh +6 -0
  248. package/scripts/registry-schema.cjs +57 -8
  249. package/scripts/release-notes/conventional-title.cjs +19 -1
  250. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  251. package/scripts/release-tarball-smoke.cjs +18 -11
  252. package/scripts/run-tests.cjs +420 -58
  253. package/scripts/workflow-size.cjs +16 -8
  254. package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
  255. package/skills/gsd-mempalace-capture/SKILL.md +9 -5
  256. package/skills/gsd-new-milestone/SKILL.md +1 -1
  257. package/skills/gsd-plan-phase/SKILL.md +5 -3
  258. package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
  259. package/vscode/package.json +1 -1
  260. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  261. package/scripts/update-size-baseline.cjs +0 -68
@@ -16,15 +16,20 @@
16
16
  * → { ok:true, script, summary } | { ok:false, reason }
17
17
  * Maps GSD's wave/plan model 1:1 onto Workflow primitives:
18
18
  * wave → sequential `parallel()` stage barriers,
19
- * plan → `agent(brief, { agentType:'gsd-executor', isolation:'worktree' })`,
19
+ * plan → `agent(brief, { agentType:'gsd-executor', isolation:'worktree' })`
20
+ * — UNLESS the plan's `use_worktree` is explicitly `false`, in which case
21
+ * `isolation` is omitted entirely for that plan (#2772 / #2285 finding 1:
22
+ * a submodule-touching plan must never be forced into worktree isolation
23
+ * the inline path (execute-phase.md step 2.5) would keep it out of),
20
24
  * files_modified overlap → forces plans into separate sequential stages
21
25
  * (the same overlap rule execute-phase already applies inline),
22
26
  * resumeFromRunId → wired to the phase run id,
23
27
  * budgetTokens → a shared token pool.
24
- * The emitted script composes the SAME gsd-executor agent and worktree
25
- * isolation the inline path uses, so it produces the same artifacts/commits
26
- * (criterion 2). It is a generated string consumed by the orchestrator; this
27
- * module never invokes the Workflow tool itself.
28
+ * The emitted script composes the SAME gsd-executor agent the inline path
29
+ * uses, with per-plan worktree isolation mirroring the inline path's own
30
+ * per-plan decision, so it produces the same artifacts/commits (criterion 2).
31
+ * It is a generated string consumed by the orchestrator; this module never
32
+ * invokes the Workflow tool itself.
28
33
  *
29
34
  * Design laws:
30
35
  * - Gall's Law: ship a small working slice that composes existing primitives
@@ -259,6 +264,53 @@ function partitionStages(plans) {
259
264
  function quoteString(s) {
260
265
  return JSON.stringify(s);
261
266
  }
267
+ /**
268
+ * #2686 — the single decision of whether a resolved executor model is emittable,
269
+ * and what to emit. Shared by `agentOptions` (the emission) and the provenance
270
+ * comment (the claim about it) so the two can never disagree — a generated
271
+ * comment asserting something the generator does not actually do is the exact
272
+ * failure #2686 was filed for.
273
+ *
274
+ * Returns the model to emit, or `undefined` for "emit nothing":
275
+ * - non-string → malformed config; omit rather than throw, matching the
276
+ * defensive typeof guard `mapClaudeOverrideForRuntime`
277
+ * already carries in model-resolver for the same reason.
278
+ * - empty/whitespace → #2517: emitting `model: ""` 404s on runtimes without
279
+ * native tier aliases. Trimmed, so `" "` is also "none".
280
+ * - "inherit" → same rule; matched case-insensitively after trimming,
281
+ * since config is user-authored free text.
282
+ *
283
+ * NOTE it does NOT reject unscriptable characters — that is a hard input error,
284
+ * not a silent omission, and is rejected up front by `emitWorkflowScript` so the
285
+ * caller sees a reason instead of quietly losing their model routing.
286
+ */
287
+ function emittableModel(executorModel) {
288
+ if (typeof executorModel !== 'string')
289
+ return undefined;
290
+ const trimmed = executorModel.trim();
291
+ if (trimmed.length === 0)
292
+ return undefined;
293
+ if (trimmed.toLowerCase() === 'inherit')
294
+ return undefined;
295
+ return trimmed;
296
+ }
297
+ /**
298
+ * Render the `agent()` options object for a single plan — `isolation: "worktree"`
299
+ * ONLY when the plan's `use_worktree` is not explicitly `false` (#2772 / #2285
300
+ * finding 1). This is the single place that decides worktree isolation for the
301
+ * Workflow backend; it must never diverge from the inline path's per-plan gate.
302
+ */
303
+ function agentOptions(p, executorModel) {
304
+ const parts = ['agentType: "gsd-executor"'];
305
+ if (p.use_worktree !== false)
306
+ parts.push('isolation: "worktree"');
307
+ // #2686: carry the resolved executor model so this backend honors
308
+ // model_overrides / model_policy / model_profile exactly as the inline path.
309
+ const model = emittableModel(executorModel);
310
+ if (model !== undefined)
311
+ parts.push('model: ' + quoteString(model));
312
+ return '{ ' + parts.join(', ') + ' }';
313
+ }
262
314
  /**
263
315
  * True if `s` is a safe identifier/path token to interpolate into the generated
264
316
  * script WITHOUT requiring a string-literal context — i.e. it contains no
@@ -285,7 +337,7 @@ function emitWorkflowScript(input) {
285
337
  if (input === null || input === undefined || typeof input !== 'object') {
286
338
  return { ok: false, reason: 'invalid_input' };
287
339
  }
288
- const { phaseDir, waves, runId } = input;
340
+ const { phaseDir, waves, runId, executorModel } = input;
289
341
  // Identifiers/paths interpolated into the generated script must be free of any
290
342
  // character that could terminate a comment, break out of a string literal, or
291
343
  // smuggle control bytes — reject up front (security: #1143 review Finding 1).
@@ -295,9 +347,28 @@ function emitWorkflowScript(input) {
295
347
  if (!isScriptableIdentifier(runId)) {
296
348
  return { ok: false, reason: 'runId must be a non-empty string without newlines/quotes/backslash/control chars' };
297
349
  }
350
+ // #2686 security: the resolved model is interpolated into BOTH an object
351
+ // literal (safe under quoteString) and a `//` provenance comment (NOT safe
352
+ // under quoteString — U+2028/U+2029 are LineTerminators that end a single-line
353
+ // comment in every engine, so a hostile model id would make the rest of the
354
+ // line live code). Reject the whole emission rather than silently dropping the
355
+ // model: an unscriptable id is malformed input, and `resolveWaveDispatch` maps
356
+ // an emit failure to the inline backend WITH a reason, so the user sees it.
357
+ // Only a STRING carrying such a character is rejected. A non-string is a
358
+ // malformed config rather than an injection attempt, and stays on the existing
359
+ // defensive path: `emittableModel` omits it and emission proceeds.
360
+ if (typeof executorModel === 'string' && UNSCRIPTABLE_CHAR_RE.test(executorModel)) {
361
+ return { ok: false, reason: 'executorModel must not contain newlines/quotes/backslash/control/line-separator chars' };
362
+ }
298
363
  if (!Array.isArray(waves) || waves.length === 0) {
299
364
  return { ok: false, reason: 'waves must be a non-empty array' };
300
365
  }
366
+ // Wave ids must be unique ACROSS waves, not just plan ids within one (#2590).
367
+ // Each wave emits a `phase("Wave <id>")` call plus a matching meta.phases
368
+ // entry, and the Workflow tool matches phase titles by exact string — two
369
+ // waves sharing an id would collapse into one progress group and misattribute
370
+ // every agent in the second wave to the first.
371
+ const seenWaveIds = new Set();
301
372
  for (let i = 0; i < waves.length; i++) {
302
373
  const w = waves[i];
303
374
  if (w === null || typeof w !== 'object' || typeof w.id !== 'string') {
@@ -306,6 +377,10 @@ function emitWorkflowScript(input) {
306
377
  if (!isScriptableIdentifier(w.id)) {
307
378
  return { ok: false, reason: 'waves[' + i + '].id must not contain newlines/quotes/backslash/control chars' };
308
379
  }
380
+ if (seenWaveIds.has(w.id)) {
381
+ return { ok: false, reason: 'duplicate wave id "' + w.id + '" — wave ids must be unique (phase titles must map 1:1)' };
382
+ }
383
+ seenWaveIds.add(w.id);
309
384
  if (!Array.isArray(w.plans) || w.plans.length === 0) {
310
385
  return { ok: false, reason: 'waves[' + i + '] must have a non-empty plans array' };
311
386
  }
@@ -318,6 +393,9 @@ function emitWorkflowScript(input) {
318
393
  if (!isScriptableIdentifier(p.id)) {
319
394
  return { ok: false, reason: 'waves[' + i + '].plans[' + j + '].id must not contain newlines/quotes/backslash/control chars' };
320
395
  }
396
+ if (p.use_worktree !== undefined && typeof p.use_worktree !== 'boolean') {
397
+ return { ok: false, reason: 'waves[' + i + '].plans[' + j + '].use_worktree must be a boolean if present' };
398
+ }
321
399
  if (seenIds.has(p.id)) {
322
400
  return { ok: false, reason: 'waves[' + i + '] has duplicate plan id "' + p.id + '"' };
323
401
  }
@@ -333,14 +411,63 @@ function emitWorkflowScript(input) {
333
411
  ? Math.floor(input.budgetTokens)
334
412
  : null;
335
413
  const lines = [];
414
+ // `export const meta = {…}` MUST be the first statement in the script — the
415
+ // Workflow tool rejects the whole script otherwise (#2590). Leading comments
416
+ // are not statements, but the meta block is emitted first regardless so the
417
+ // contract holds under the strictest reading of "first statement".
418
+ //
419
+ // meta.phases must be a PURE LITERAL (no variables, calls, spreads, or
420
+ // template interpolation), and its titles are matched EXACTLY against the
421
+ // phase() calls emitted below.
422
+ lines.push('export const meta = {');
423
+ lines.push(' name: ' + quoteString('gsd-execute-' + runId) + ',');
424
+ lines.push(' description: ' + quoteString('GSD wave dispatch for ' + phaseDir) + ',');
425
+ lines.push(' phases: [');
426
+ for (const w of waves) {
427
+ lines.push(' { title: ' + quoteString('Wave ' + w.id) + ', detail: '
428
+ + quoteString(w.plans.length + ' plan(s)') + ' },');
429
+ }
430
+ lines.push(' ],');
431
+ lines.push('}');
432
+ lines.push('');
336
433
  lines.push('// GSD Workflow script — generated by the claude-orchestration capability (#1143)');
337
434
  lines.push('// phase: ' + phaseDir);
338
435
  lines.push('// BETA: preview-grade; on any failure the orchestrator falls back to inline dispatch.');
339
- lines.push('// Composes the SAME gsd-executor agent + worktree isolation as the inline path,');
340
- lines.push('// so artifacts (SUMMARY.md) and commits are produced identically.');
341
- lines.push('resumeFromRunId(' + quoteString(runId) + ')');
436
+ lines.push('// Composes the SAME gsd-executor agent as the inline path, so artifacts (SUMMARY.md)');
437
+ lines.push('// and commits are produced identically. Worktree isolation is per-plan (use_worktree)');
438
+ lines.push('// and mirrors execute-phase.md step 2.5\'s submodule gate exactly (#2772 / #2285).');
439
+ // #2686 / ADR-1411: state which model was applied — or that none resolved —
440
+ // so an opted-in user can SEE the routing decision instead of having to read
441
+ // the emitted options. A fallback must be a visible value, never silent.
442
+ //
443
+ // SECURITY: this is a `//` comment, and U+2028/U+2029 are ECMAScript
444
+ // LineTerminators that END a single-line comment in every engine — the ES2019
445
+ // change legalized them inside string LITERALS only, so `quoteString` alone is
446
+ // NOT sufficient here even though it is sufficient in the object literal
447
+ // above. An unscriptable model id would otherwise close the comment and make
448
+ // the remainder live top-level code. `emitWorkflowScript` rejects such ids
449
+ // before reaching this point (see the validation above), which is what makes
450
+ // interpolating here safe.
451
+ const provenanceModel = emittableModel(executorModel);
452
+ if (provenanceModel !== undefined) {
453
+ lines.push('// model: ' + quoteString(provenanceModel) + ' (resolved for gsd-executor, same source as the inline path)');
454
+ }
455
+ else {
456
+ lines.push('// model: none applied — resolved to "inherit"/empty, so each agent inherits the');
457
+ lines.push('// orchestrator model (#2517: emitting an empty model 404s on some runtimes).');
458
+ }
459
+ lines.push('//');
460
+ // resumeFromRunId is a Workflow TOOL INPUT parameter, not a script function —
461
+ // calling it threw "resumeFromRunId is not defined" (#2590). The run id is
462
+ // carried in summary.resumeRunId for the caller to pass as that input.
463
+ lines.push('// resume: pass ' + quoteString(runId) + ' as the Workflow tool\'s resumeFromRunId input');
464
+ lines.push('// (it is a tool parameter, NOT a script function).');
342
465
  if (budgetTokens !== null) {
343
- lines.push('budget(' + budgetTokens + ')');
466
+ // `budget` is a read-only object ({ total, spent(), remaining() }) supplied
467
+ // by the caller's token directive — a script cannot SET it, and `budget(n)`
468
+ // threw "budget is not a function" (#2590). Recorded as intent only.
469
+ lines.push('// budget: ' + budgetTokens + ' output tokens intended for this run; `budget` is');
470
+ lines.push('// read-only in a Workflow script — set it via the caller\'s token directive.');
344
471
  }
345
472
  lines.push('');
346
473
  const stagesByWave = [];
@@ -351,6 +478,8 @@ function emitWorkflowScript(input) {
351
478
  stagesByWave.push(stages);
352
479
  totalPlans += wave.plans.length;
353
480
  lines.push('// Wave ' + wave.id);
481
+ // Title must match this wave's meta.phases entry EXACTLY.
482
+ lines.push('phase(' + quoteString('Wave ' + wave.id) + ')');
354
483
  for (let si = 0; si < stages.length; si++) {
355
484
  const stagePlanIds = stages[si];
356
485
  // Resolve back to plan objects for briefs (ids are unique within a wave — validated above).
@@ -358,22 +487,15 @@ function emitWorkflowScript(input) {
358
487
  if (stages.length > 1) {
359
488
  lines.push('// Stage ' + si + (si > 0 ? ' (sequential — files_modified overlap)' : ''));
360
489
  }
361
- if (stagePlans.length === 1) {
362
- const p = stagePlans[0];
363
- lines.push('parallel(');
364
- lines.push(' agent(' + quoteString(p.brief) + ', { agentType: "gsd-executor", isolation: "worktree" })');
365
- lines.push(')');
366
- }
367
- else {
368
- lines.push('parallel(');
369
- for (const p of stagePlans) {
370
- lines.push(' agent(' + quoteString(p.brief) + ', { agentType: "gsd-executor", isolation: "worktree" }),');
371
- }
372
- // Replace trailing comma on the last agent line with nothing.
373
- const lastIdx = lines.length - 1;
374
- lines[lastIdx] = lines[lastIdx].replace(/,$/, '');
375
- lines.push(')');
490
+ // parallel() takes an ARRAY OF THUNKS — `parallel(agent(…), agent(…))`
491
+ // threw "parallel() expects an array of functions" (#2590). Passing
492
+ // agent() results directly would also start every agent eagerly, before
493
+ // parallel() could bound concurrency.
494
+ lines.push('await parallel([');
495
+ for (const p of stagePlans) {
496
+ lines.push(' () => agent(' + quoteString(p.brief) + ', ' + agentOptions(p, executorModel) + '),');
376
497
  }
498
+ lines.push('])');
377
499
  }
378
500
  if (wi < waves.length - 1)
379
501
  lines.push('');
@@ -393,9 +515,65 @@ function emitWorkflowScript(input) {
393
515
  },
394
516
  };
395
517
  }
518
+ /**
519
+ * #2285 — single composed decision seam for a PRE-wave dispatch-backend selector
520
+ * (e.g. the `execute:wave:pre` claude-orchestration contribution). Composes
521
+ * `detectWorkflowBackend` (gate ladder) with `emitWorkflowScript` (wave→plan
522
+ * mapping) into ONE call so the orchestrator (and its CLI wrapper,
523
+ * `claude-orchestration resolve-wave-dispatch`) never has to re-implement the
524
+ * two-step "detect, then maybe emit" sequencing.
525
+ *
526
+ * Fail-closed at every layer, matching the two composed functions:
527
+ * - `detectWorkflowBackend` resolving anything other than `'workflow'` →
528
+ * `inline` immediately; `emitWorkflowScript` is never invoked (no wasted
529
+ * work, no risk of a bad emit masking a correct inline fallback).
530
+ * - `detectWorkflowBackend` resolves `'workflow'` but `emitWorkflowScript`
531
+ * fails (`ok:false` — e.g. a malformed wave manifest) → `inline`, carrying
532
+ * the emit failure reason so the caller can surface it. Never a partial or
533
+ * broken script.
534
+ *
535
+ * This is the designated non-CLI-router, non-test caller of
536
+ * `detectWorkflowBackend` and `emitWorkflowScript` — the standalone CLI
537
+ * subcommands (`detect-backend`, `emit-workflow`) remain for inspection/
538
+ * debugging, but the orchestrator's real per-wave dispatch decision goes
539
+ * through this seam.
540
+ *
541
+ * Never throws on bad input.
542
+ */
543
+ function resolveWaveDispatch(input) {
544
+ if (input === null || input === undefined || typeof input !== 'object') {
545
+ return { backend: 'inline', reason: 'invalid_input' };
546
+ }
547
+ const detected = detectWorkflowBackend({
548
+ runtimeId: input.runtimeId,
549
+ hostIntegration: input.hostIntegration,
550
+ config: input.config,
551
+ agentSdkVersion: input.agentSdkVersion,
552
+ });
553
+ if (detected.backend !== 'workflow') {
554
+ return { backend: 'inline', reason: detected.reason };
555
+ }
556
+ const emitted = emitWorkflowScript({
557
+ phaseDir: input.phaseDir,
558
+ waves: input.waves,
559
+ runId: input.runId,
560
+ budgetTokens: input.budgetTokens,
561
+ executorModel: input.executorModel,
562
+ });
563
+ if (!emitted.ok) {
564
+ return { backend: 'inline', reason: 'emit_failed: ' + emitted.reason };
565
+ }
566
+ return {
567
+ backend: 'workflow',
568
+ reason: detected.reason,
569
+ script: emitted.script,
570
+ summary: emitted.summary,
571
+ };
572
+ }
396
573
  module.exports = {
397
574
  detectWorkflowBackend,
398
575
  emitWorkflowScript,
576
+ resolveWaveDispatch,
399
577
  compareSemver,
400
578
  isValidSemver,
401
579
  WORKFLOW_TOOL_FLOOR_VERSION,
@@ -728,6 +728,20 @@ exports.NON_FAMILY_COMMAND_ALIASES = [
728
728
  ],
729
729
  "mutation": true
730
730
  },
731
+ {
732
+ "canonical": "requirements.ready-ids",
733
+ "aliases": [
734
+ "requirements ready-ids"
735
+ ],
736
+ "mutation": false
737
+ },
738
+ {
739
+ "canonical": "requirements.revert-phase",
740
+ "aliases": [
741
+ "requirements revert-phase"
742
+ ],
743
+ "mutation": true
744
+ },
731
745
  {
732
746
  "canonical": "stats.json",
733
747
  "aliases": [