@opengsd/gsd-core 1.20.1 → 1.21.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 (135) 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 +33 -2
  4. package/agents/gsd-executor.md +1 -1
  5. package/agents/gsd-plan-checker.md +1 -1
  6. package/agents/gsd-planner.md +3 -3
  7. package/agents/gsd-verifier.md +2 -2
  8. package/bin/install.js +234 -126
  9. package/commands/gsd/autonomous.md +1 -1
  10. package/gsd-core/bin/gsd-tools.cjs +450 -753
  11. package/gsd-core/bin/lib/capability-activation.cjs +8 -2
  12. package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
  13. package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
  14. package/gsd-core/bin/lib/capability-lock.cjs +19 -27
  15. package/gsd-core/bin/lib/capability-registry.cjs +74 -71
  16. package/gsd-core/bin/lib/capability-validator.cjs +1 -0
  17. package/gsd-core/bin/lib/capability-writer.cjs +7 -4
  18. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
  19. package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
  20. package/gsd-core/bin/lib/cli-surface.cjs +545 -0
  21. package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
  22. package/gsd-core/bin/lib/commands.cjs +80 -2
  23. package/gsd-core/bin/lib/config-file-read.cjs +85 -0
  24. package/gsd-core/bin/lib/config-loader.cjs +23 -15
  25. package/gsd-core/bin/lib/config.cjs +139 -101
  26. package/gsd-core/bin/lib/configuration.cjs +41 -2
  27. package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
  28. package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
  29. package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
  30. package/gsd-core/bin/lib/gap-checker.cjs +12 -1
  31. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
  32. package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
  33. package/gsd-core/bin/lib/graphify.cjs +19 -2
  34. package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
  35. package/gsd-core/bin/lib/init.cjs +13 -11
  36. package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
  37. package/gsd-core/bin/lib/install-engine.cjs +11 -19
  38. package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
  39. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
  40. package/gsd-core/bin/lib/io.cjs +6 -0
  41. package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
  42. package/gsd-core/bin/lib/markdown-table.cjs +39 -1
  43. package/gsd-core/bin/lib/milestone.cjs +8 -6
  44. package/gsd-core/bin/lib/model-resolver.cjs +8 -4
  45. package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
  46. package/gsd-core/bin/lib/phase-locator.cjs +8 -3
  47. package/gsd-core/bin/lib/phase.cjs +107 -31
  48. package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
  49. package/gsd-core/bin/lib/planning-snapshot.cjs +8 -4
  50. package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
  51. package/gsd-core/bin/lib/profile-output.cjs +9 -6
  52. package/gsd-core/bin/lib/project-context.cjs +439 -0
  53. package/gsd-core/bin/lib/project-root.cjs +9 -230
  54. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
  55. package/gsd-core/bin/lib/report-parser.cjs +16 -0
  56. package/gsd-core/bin/lib/roadmap-parser.cjs +328 -50
  57. package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
  58. package/gsd-core/bin/lib/roadmap.cjs +2 -2
  59. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -4
  60. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
  61. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
  62. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  63. package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
  64. package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
  65. package/gsd-core/bin/lib/state-transition.cjs +91 -41
  66. package/gsd-core/bin/lib/state.cjs +446 -61
  67. package/gsd-core/bin/lib/surface.cjs +4 -7
  68. package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
  69. package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
  70. package/gsd-core/bin/lib/verify.cjs +49 -6
  71. package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
  72. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  73. package/gsd-core/references/autonomous-smart-discuss.md +99 -18
  74. package/gsd-core/references/checkpoints.md +1 -1
  75. package/gsd-core/references/planner-guidance.md +11 -0
  76. package/gsd-core/references/planner-human-verify-mode.md +1 -1
  77. package/gsd-core/references/planner-reversibility.md +2 -0
  78. package/gsd-core/references/planner-verify-command-grounding.md +22 -6
  79. package/gsd-core/workflows/autonomous.md +4 -4
  80. package/gsd-core/workflows/code-review-fix.md +5 -7
  81. package/gsd-core/workflows/execute-phase.md +21 -7
  82. package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
  83. package/gsd-core/workflows/plan-phase.md +18 -19
  84. package/hooks/dist/gsd-agent-isolation-guard.js +3 -2
  85. package/hooks/dist/gsd-config-reload.js +2 -2
  86. package/hooks/dist/gsd-context-monitor.js +2 -1
  87. package/hooks/dist/gsd-cursor-subagent-start.js +8 -5
  88. package/hooks/dist/gsd-read-injection-scanner.js +2 -2
  89. package/hooks/dist/gsd-secret-read-guard.js +141 -6
  90. package/hooks/dist/gsd-statusline.js +3 -2
  91. package/hooks/dist/gsd-workflow-guard.js +2 -1
  92. package/hooks/dist/lib/config-read.js +49 -0
  93. package/hooks/dist/lib/git-cmd.js +3 -1
  94. package/hooks/dist/lib/git-head-tree.js +474 -0
  95. package/hooks/dist/lib/isolation-sentinel.js +97 -17
  96. package/hooks/dist/lib/secret-read-exemptions.js +161 -0
  97. package/hooks/gsd-agent-isolation-guard.js +3 -2
  98. package/hooks/gsd-config-reload.js +2 -2
  99. package/hooks/gsd-context-monitor.js +2 -1
  100. package/hooks/gsd-cursor-subagent-start.js +8 -5
  101. package/hooks/gsd-read-injection-scanner.js +2 -2
  102. package/hooks/gsd-secret-read-guard.js +141 -6
  103. package/hooks/gsd-statusline.js +3 -2
  104. package/hooks/gsd-workflow-guard.js +2 -1
  105. package/hooks/lib/config-read.js +49 -0
  106. package/hooks/lib/git-cmd.js +3 -1
  107. package/hooks/lib/git-head-tree.js +474 -0
  108. package/hooks/lib/isolation-sentinel.js +97 -17
  109. package/hooks/lib/secret-read-exemptions.js +161 -0
  110. package/package.json +7 -2
  111. package/pi/package-agents/gsd-executor.md +1 -1
  112. package/pi/package-agents/gsd-plan-checker.md +1 -1
  113. package/pi/package-agents/gsd-planner.md +3 -3
  114. package/pi/package-agents/gsd-verifier.md +2 -2
  115. package/pi/package-agents/manifest.json +4 -4
  116. package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
  117. package/scripts/changeset/cli.cjs +2 -2
  118. package/scripts/changeset/serialize.cjs +12 -2
  119. package/scripts/ci-compact-v8-coverage.cjs +204 -0
  120. package/scripts/ci-fix-next-rerun.cjs +149 -0
  121. package/scripts/ci-next-health.cjs +52 -9
  122. package/scripts/ci-test-scope.cjs +77 -71
  123. package/scripts/command-contract-helpers.cjs +56 -7
  124. package/scripts/docs-guard-registry.cjs +3 -0
  125. package/scripts/gen-cli-surface-manifest.cjs +287 -0
  126. package/scripts/gen-platform-conformance-tier.cjs +258 -19
  127. package/scripts/lib/macos-conformance-tier.generated.cjs +55 -47
  128. package/scripts/lib/platform-conformance-tier.generated.cjs +19 -64
  129. package/scripts/lib/shellcheck-fetch.cjs +71 -4
  130. package/scripts/lint-removed-but-needed.cjs +562 -50
  131. package/scripts/release-notes/changelog-release-notes.cjs +345 -0
  132. package/scripts/release-notes/discord-release-summary.cjs +82 -1
  133. package/scripts/timeout-evidence-from-ci.cjs +964 -0
  134. package/skills/gsd-autonomous/SKILL.md +1 -1
  135. package/vscode/package.json +1 -1
@@ -5,12 +5,20 @@
5
5
  * #1143 — adopts Claude Code's Workflow tool (the engine behind `/effort ultracode`)
6
6
  * as an optional, runtime-gated parallel-execution backend for the GSD loop.
7
7
  *
8
- * This module is the pure, testable core of the capability. It owns two seams:
8
+ * This module is the pure, testable core of the capability. It owns three seams:
9
9
  *
10
- * detectWorkflowBackend({ runtimeId, hostIntegration, config, agentSdkVersion })
10
+ * resolveWorkflowAvailability({ env, sources })
11
+ * → { disabled: boolean, reason: string }
12
+ * Reads Claude Code's documented workflow kill switches (the
13
+ * CLAUDE_CODE_DISABLE_WORKFLOWS env var and the `disableWorkflows` /
14
+ * `enableWorkflows` settings keys) from precedence-ordered settings sources
15
+ * the caller has already read. Fail-closed on unreadable or malformed input.
16
+ *
17
+ * detectWorkflowBackend({ runtimeId, hostIntegration, config, workflowAvailability })
11
18
  * → { available: boolean, backend: 'workflow'|'inline', reason: string }
12
19
  * Fail-closed: every miss degrades to `inline` (today's behaviour), so the
13
20
  * core loop is byte-identical unless every gate opens. This is criteria 3 + 6.
21
+ * `workflowAvailability` is the resolveWorkflowAvailability result (#4747).
14
22
  *
15
23
  * emitWorkflowScript({ phaseDir, waves, runId, budgetTokens? })
16
24
  * → { ok:true, script, summary } | { ok:false, reason }
@@ -25,6 +33,14 @@
25
33
  * (the same overlap rule execute-phase already applies inline),
26
34
  * resumeFromRunId → wired to the phase run id,
27
35
  * budgetTokens → a shared token pool.
36
+ * Runtime caps (#4747): a single parallel() call accepts at most
37
+ * WORKFLOW_MAX_ITEMS_PER_CALL items and a run at most
38
+ * WORKFLOW_MAX_AGENTS_PER_RUN agents; a manifest over either cap is rejected
39
+ * at emit time (`ok:false`) instead of failing inside the Workflow tool.
40
+ * Hand-back (#3302 / #4747): worktree plans pass a JSON `schema`
41
+ * (WORKTREE_METADATA_SCHEMA) to agent(), so the result is a validated object;
42
+ * every agent() call is wrapped so a throw becomes `{ error }` for that plan
43
+ * only, and each outcome entry is { plan, expects_worktree, metadata, error }.
28
44
  * The emitted script composes the SAME gsd-executor agent the inline path
29
45
  * uses, with per-plan worktree isolation mirroring the inline path's own
30
46
  * per-plan decision, so it produces the same artifacts/commits (criterion 2).
@@ -38,7 +54,7 @@
38
54
  * barrier/pipeline/budget/resume semantics instead of hand-rolling them.
39
55
  * - Postel's Law: liberal in input (missing fields → inline), conservative in
40
56
  * output (workflow only when every gate opens).
41
- * - Fail-closed: an unknown version, a missing descriptor, or a disabled
57
+ * - Fail-closed: unknown availability, a missing descriptor, or a disabled
42
58
  * toggle all resolve to `inline`, never to `workflow`.
43
59
  *
44
60
  * Zero external dependencies. Pure functions. Never throws on bad input.
@@ -48,98 +64,126 @@ const fileOverlapPartitionerMod = require("./file-overlap-partitioner.cjs");
48
64
  const { partitionByFileOverlap } = fileOverlapPartitionerMod;
49
65
  // ─── Constants ────────────────────────────────────────────────────────────────
50
66
  /**
51
- * The Agent SDK version that introduced the Workflow tool (#1143 prior art).
52
- * Used as the default floor when config does not override it. A runtime reporting
53
- * an agentSdkVersion below this cannot host the Workflow backend.
67
+ * Documented Workflow-tool runtime caps (code.claude.com/docs/en/workflows):
68
+ * parallel()/pipeline() reject a list longer than 4,096 items, and one run may
69
+ * start at most 1,000 agents (#4747).
70
+ */
71
+ const WORKFLOW_MAX_ITEMS_PER_CALL = 4096;
72
+ const WORKFLOW_MAX_AGENTS_PER_RUN = 1000;
73
+ /** Every reason code detectWorkflowBackend can return (#4747; hyrums-law). */
74
+ const DETECT_REASONS = Object.freeze([
75
+ 'workflow_backend_active', 'capability_disabled', 'runtime_not_claude', 'backend_inline',
76
+ 'workflow_tool_unavailable', 'workflows_disabled_by_env', 'workflows_disabled_by_managed_settings',
77
+ 'workflows_disabled_by_settings', 'workflow_settings_unreadable', 'workflow_availability_unknown',
78
+ ]);
79
+ /** The subset of DETECT_REASONS a resolveWorkflowAvailability result may carry when disabled. */
80
+ const AVAILABILITY_DISABLE_REASONS = new Set([
81
+ 'workflows_disabled_by_env', 'workflows_disabled_by_managed_settings', 'workflows_disabled_by_settings',
82
+ 'workflow_settings_unreadable', 'workflow_availability_unknown',
83
+ ]);
84
+ /**
85
+ * JSON schema a worktree plan's agent() must satisfy: the four fields the
86
+ * orchestrator records via worktree.record-agent (#3302 / #4747). Deep-frozen so
87
+ * the exported literal cannot drift from what the emitted script declares.
54
88
  */
55
- const WORKFLOW_TOOL_FLOOR_VERSION = '0.3.149';
89
+ const WORKTREE_METADATA_SCHEMA = Object.freeze({
90
+ type: 'object',
91
+ properties: Object.freeze({
92
+ agent_id: Object.freeze({ type: 'string', minLength: 1 }),
93
+ worktree_path: Object.freeze({ type: 'string', minLength: 1 }),
94
+ branch: Object.freeze({ type: 'string', minLength: 1 }),
95
+ expected_base: Object.freeze({ type: 'string', minLength: 1 }),
96
+ }),
97
+ required: Object.freeze(['agent_id', 'worktree_path', 'branch', 'expected_base']),
98
+ });
56
99
  /** Closed enum for the `claude_orchestration.execution_backend` config key. */
57
100
  const BACKEND_VALUES = new Set(['auto', 'workflow', 'inline']);
58
101
  /** Only this runtime can host the Workflow tool (Claude Code / Agent SDK). */
59
102
  const WORKFLOW_RUNTIME = 'claude';
60
- // ─── Semver helpers ───────────────────────────────────────────────────────────
61
- /** Official-ish strict SemVer 2.0.0 numeric triple (+ optional pre/build). */
62
- const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
63
- /** True for a syntactically valid semver string. */
64
- function isValidSemver(s) {
65
- return typeof s === 'string' && SEMVER_RE.test(s);
103
+ function isPlainObject(v) {
104
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
66
105
  }
106
+ const SETTINGS_SCOPES = new Set(['managed', 'projectLocal', 'project', 'user']);
107
+ const SETTINGS_STATUSES = new Set(['absent', 'ok', 'unreadable']);
67
108
  /**
68
- * Compare two semver strings.
69
- * Returns -1/0/1 in the usual sense. Garbage in either position → -1 (fail-closed:
70
- * an unparseable version is treated as "less than" any real floor, so detection
71
- * never accidentally enables the preview backend on an unknown SDK).
109
+ * Resolve whether a Claude Code workflow kill switch is set (#4747).
110
+ *
111
+ * Sources: code.claude.com/docs/en/workflows (kill switches
112
+ * `CLAUDE_CODE_DISABLE_WORKFLOWS` and `disableWorkflows`/`enableWorkflows`) and
113
+ * code.claude.com/docs/en/settings (precedence managed > --settings >
114
+ * project-local > project > user; the highest level that sets a key wins).
115
+ * `--settings` is not observable and is not read.
116
+ *
117
+ * `input.sources` MUST be precedence-ordered, highest first (caller's
118
+ * responsibility): managed, projectLocal, project, user. Each entry is
119
+ * `{ scope, status: 'absent'|'ok'|'unreadable', settings? }`.
72
120
  *
73
- * Pre-release/build metadata are ignored for the comparison — only the numeric
74
- * major.minor.patch triple participates, matching how the Workflow-tool floor is
75
- * specified (a plain "0.3.149").
121
+ * Fail-closed: a malformed input, an unreadable source, or a non-boolean kill
122
+ * switch value disables. A truthy env value other than `0`/`false`/empty disables.
123
+ *
124
+ * Never throws.
76
125
  */
77
- function compareSemver(a, b) {
78
- if (!isValidSemver(a) || !isValidSemver(b))
79
- return -1;
80
- // Split numeric triple from pre-release/build metadata.
81
- const parseTriple = (s) => {
82
- const core = s.split('-')[0].split('+')[0].split('.');
83
- return [parseInt(core[0], 10), parseInt(core[1], 10), parseInt(core[2], 10)];
84
- };
85
- const hasPre = (s) => s.indexOf('-') !== -1;
86
- const preIdentifiers = (s) => (s.split('-')[1] || '').split('+')[0].split('.').filter((x) => x.length > 0);
87
- const am = parseTriple(a);
88
- const bm = parseTriple(b);
89
- for (let i = 0; i < 3; i++) {
90
- if (am[i] < bm[i])
91
- return -1;
92
- if (am[i] > bm[i])
93
- return 1;
94
- }
95
- // Numeric triple is equal. SemVer 2.0.0 §11 precedence:
96
- // - a version WITH a pre-release tag is LOWER than the same triple WITHOUT one
97
- // (keeps the floor fail-closed for pre-release builds of the GA floor);
98
- // - two pre-releases of the same triple are ordered by their dot-separated
99
- // identifiers (numeric < alphanumeric; numeric compared numerically,
100
- // alphanumeric lexically; fewer identifiers < more).
101
- const aPre = hasPre(a);
102
- const bPre = hasPre(b);
103
- if (aPre && !bPre)
104
- return -1;
105
- if (!aPre && bPre)
106
- return 1;
107
- if (aPre && bPre) {
108
- const ai = preIdentifiers(a);
109
- const bi = preIdentifiers(b);
110
- const len = Math.min(ai.length, bi.length);
111
- for (let i = 0; i < len; i++) {
112
- const ax = ai[i];
113
- const bx = bi[i];
114
- const aNum = /^\d+$/.test(ax);
115
- const bNum = /^\d+$/.test(bx);
116
- if (aNum && bNum) {
117
- const an = parseInt(ax, 10);
118
- const bn = parseInt(bx, 10);
119
- if (an < bn)
120
- return -1;
121
- if (an > bn)
122
- return 1;
123
- }
124
- else if (aNum && !bNum) {
125
- return -1; // numeric identifiers always lower than alphanumeric
126
+ function resolveWorkflowAvailability(input) {
127
+ if (!isPlainObject(input)) {
128
+ return { disabled: true, reason: 'workflow_availability_unknown' };
129
+ }
130
+ const env = isPlainObject(input['env']) ? input['env'] : {};
131
+ const raw = env['CLAUDE_CODE_DISABLE_WORKFLOWS'];
132
+ if (typeof raw === 'string') {
133
+ const v = raw.trim().toLowerCase();
134
+ if (v !== '' && v !== '0' && v !== 'false') {
135
+ return { disabled: true, reason: 'workflows_disabled_by_env' };
136
+ }
137
+ }
138
+ const sources = input['sources'];
139
+ if (!Array.isArray(sources)) {
140
+ return { disabled: true, reason: 'workflow_availability_unknown' };
141
+ }
142
+ for (const s of sources) {
143
+ if (!isPlainObject(s)
144
+ || typeof s['scope'] !== 'string' || !SETTINGS_SCOPES.has(s['scope'])
145
+ || typeof s['status'] !== 'string' || !SETTINGS_STATUSES.has(s['status'])) {
146
+ return { disabled: true, reason: 'workflow_availability_unknown' };
147
+ }
148
+ }
149
+ const okSources = [];
150
+ for (const s of sources) {
151
+ if (s['status'] === 'unreadable') {
152
+ return { disabled: true, reason: 'workflow_settings_unreadable' };
153
+ }
154
+ if (s['status'] === 'ok') {
155
+ if (!isPlainObject(s['settings'])) {
156
+ return { disabled: true, reason: 'workflow_settings_unreadable' };
126
157
  }
127
- else if (!aNum && bNum) {
128
- return 1;
158
+ okSources.push({ scope: s['scope'], settings: s['settings'] });
159
+ }
160
+ }
161
+ const KEYS = ['disableWorkflows', 'enableWorkflows'];
162
+ for (const src of okSources) {
163
+ for (const key of KEYS) {
164
+ if (Object.prototype.hasOwnProperty.call(src.settings, key) && typeof src.settings[key] !== 'boolean') {
165
+ return { disabled: true, reason: 'workflow_settings_unreadable' };
129
166
  }
130
- else {
131
- if (ax < bx)
132
- return -1;
133
- if (ax > bx)
134
- return 1;
167
+ }
168
+ }
169
+ const winner = (key) => {
170
+ for (const src of okSources) {
171
+ if (Object.prototype.hasOwnProperty.call(src.settings, key)) {
172
+ return { scope: src.scope, value: src.settings[key] };
135
173
  }
136
174
  }
137
- if (ai.length < bi.length)
138
- return -1;
139
- if (ai.length > bi.length)
140
- return 1;
175
+ return null;
176
+ };
177
+ const reasonFor = (scope) => scope === 'managed' ? 'workflows_disabled_by_managed_settings' : 'workflows_disabled_by_settings';
178
+ const dis = winner('disableWorkflows');
179
+ if (dis !== null && dis.value === true) {
180
+ return { disabled: true, reason: reasonFor(dis.scope) };
181
+ }
182
+ const en = winner('enableWorkflows');
183
+ if (en !== null && en.value === false) {
184
+ return { disabled: true, reason: reasonFor(en.scope) };
141
185
  }
142
- return 0;
186
+ return { disabled: false, reason: 'workflows_available' };
143
187
  }
144
188
  /** Inline result shorthand. */
145
189
  function inline(reason, available = false) {
@@ -153,10 +197,16 @@ function inline(reason, available = false) {
153
197
  * 2. runtime is Claude (the only runtime that exposes the Workflow tool)
154
198
  * 3. execution_backend !== 'inline'
155
199
  * 4. host descriptor signals nested+background dispatch (Workflow-tool capable)
156
- * 5. agentSdkVersion is a known, valid semver
157
- * 6. agentSdkVersion >= the configured floor (default WORKFLOW_TOOL_FLOOR_VERSION)
200
+ * 5. workflowAvailability is a well-formed resolveWorkflowAvailability result
201
+ * (else `workflow_availability_unknown`)
202
+ * 6. workflowAvailability.disabled is false (else inline with its own reason:
203
+ * env / managed settings / settings kill switch, or unreadable settings)
158
204
  * 7. execution_backend === 'workflow' OR 'auto' (both reach here; 'inline' exited at 3)
159
205
  *
206
+ * The `claude_orchestration.min_agent_sdk_version` config key is deprecated and
207
+ * ignored (#4747): the Agent SDK version does not reflect whether a Claude Code
208
+ * session has dynamic workflows.
209
+ *
160
210
  * Never throws. Destructures defensively.
161
211
  */
162
212
  function detectWorkflowBackend(input) {
@@ -203,15 +253,14 @@ function detectWorkflowBackend(input) {
203
253
  if (nested !== true || background !== true) {
204
254
  return inline('workflow_tool_unavailable');
205
255
  }
206
- // 5. an unknown agentSdkVersion cannot be trusted to meet the floor.
207
- if (!isValidSemver(input.agentSdkVersion)) {
208
- return inline('agent_sdk_version_unknown');
256
+ // 5-6. Claude Code's documented workflow kill switches (#4747).
257
+ const av = input.workflowAvailability;
258
+ if (av === null || typeof av !== 'object' || Array.isArray(av) || typeof av.disabled !== 'boolean') {
259
+ return inline('workflow_availability_unknown');
209
260
  }
210
- // 6. version floor (config override > default constant).
211
- const floorRaw = cfg['claude_orchestration.min_agent_sdk_version'];
212
- const floor = typeof floorRaw === 'string' && isValidSemver(floorRaw) ? floorRaw : WORKFLOW_TOOL_FLOOR_VERSION;
213
- if (compareSemver(input.agentSdkVersion, floor) < 0) {
214
- return inline('agent_sdk_version_below_floor');
261
+ if (av.disabled) {
262
+ const r = av.reason;
263
+ return inline(typeof r === 'string' && AVAILABILITY_DISABLE_REASONS.has(r) ? r : 'workflow_availability_unknown');
215
264
  }
216
265
  // 7. auto/workflow both reach the workflow backend once every gate passes.
217
266
  return { available: true, backend: 'workflow', reason: 'workflow_backend_active' };
@@ -294,6 +343,9 @@ function agentOptions(p, executorModel) {
294
343
  const model = emittableModel(executorModel);
295
344
  if (model !== undefined)
296
345
  parts.push('model: ' + quoteString(model));
346
+ // #4747: worktree plans must return the validated metadata object.
347
+ if (p.use_worktree !== false)
348
+ parts.push('schema: GSD_WORKTREE_METADATA_SCHEMA');
297
349
  return '{ ' + parts.join(', ') + ' }';
298
350
  }
299
351
  /**
@@ -395,6 +447,31 @@ function emitWorkflowScript(input) {
395
447
  const budgetTokens = (typeof input.budgetTokens === 'number' && Number.isFinite(input.budgetTokens) && input.budgetTokens > 0)
396
448
  ? Math.floor(input.budgetTokens)
397
449
  : null;
450
+ // #4747: partition once; enforce the documented Workflow runtime caps at emit
451
+ // time (per-call first, then per-run) instead of failing inside the tool.
452
+ const stagesByWave = waves.map((w) => partitionStages(w.plans));
453
+ for (let wi = 0; wi < waves.length; wi++) {
454
+ const stages = stagesByWave[wi];
455
+ for (let si = 0; si < stages.length; si++) {
456
+ if (stages[si].length > WORKFLOW_MAX_ITEMS_PER_CALL) {
457
+ return {
458
+ ok: false,
459
+ reason: 'wave "' + waves[wi].id + '" stage ' + si + ' has ' + stages[si].length
460
+ + ' plans — exceeds the Workflow runtime cap of ' + WORKFLOW_MAX_ITEMS_PER_CALL + ' items per parallel() call',
461
+ };
462
+ }
463
+ }
464
+ }
465
+ let totalPlans = 0;
466
+ for (const w of waves)
467
+ totalPlans += w.plans.length;
468
+ if (totalPlans > WORKFLOW_MAX_AGENTS_PER_RUN) {
469
+ return {
470
+ ok: false,
471
+ reason: 'manifest has ' + totalPlans + ' plans — exceeds the Workflow runtime cap of '
472
+ + WORKFLOW_MAX_AGENTS_PER_RUN + ' agents per run',
473
+ };
474
+ }
398
475
  const lines = [];
399
476
  // `export const meta = {…}` MUST be the first statement in the script — the
400
477
  // Workflow tool rejects the whole script otherwise (#2590). Leading comments
@@ -455,39 +532,45 @@ function emitWorkflowScript(input) {
455
532
  lines.push('// read-only in a Workflow script — set it via the caller\'s token directive.');
456
533
  }
457
534
  lines.push('');
458
- // #3302: the generated script must hand the per-agent executor results back
459
- // to the orchestrator so it can feed the wave merge chain. Emitted right
460
- // after the header comments (meta stays the first statement): a helper that
461
- // extracts the executor's <worktree_metadata> JSON (agents/gsd-executor.md
462
- // <worktree_metadata_capture>) from one agent() result, plus the outcomes
463
- // accumulator the stage barriers below push into. `metadata` is null when
464
- // the result carried no parseable block — the LOUD-failure input the
465
- // orchestrator halts on for expects_worktree plans (never a silent skip).
466
- lines.push('// #3302: extract the executor-returned <worktree_metadata> JSON so the');
467
- lines.push('// orchestrator can record it into WAVE_WORKTREE_MANIFEST after the run');
535
+ // #3302 / #4747: the generated script hands the per-agent executor results
536
+ // back to the orchestrator so it can feed the wave merge chain. Worktree plans
537
+ // pass GSD_WORKTREE_METADATA_SCHEMA to agent(), so the result is a validated
538
+ // object; gsdWorktreeMetadata re-checks the four fields and returns null on
539
+ // any miss (the LOUD-failure input the orchestrator halts on for
540
+ // expects_worktree plans — never a silent skip). gsdRunAgent catches a throw
541
+ // (schema retries exhausted, budget hit) so one plan's failure becomes
542
+ // { error } for that plan only and its siblings still complete.
543
+ lines.push('// #3302 / #4747: worktree plans return schema-validated metadata (an object);');
544
+ lines.push('// the orchestrator records it into WAVE_WORKTREE_MANIFEST after the run');
468
545
  lines.push('// (worktree.record-agent -> worktree.cleanup-wave, the same manifest-scoped');
469
- lines.push('// merge chain inline dispatch feeds). null = absent/unparseable/interrupted.');
546
+ lines.push('// merge chain inline dispatch feeds). null = absent/invalid/interrupted.');
547
+ lines.push('const GSD_WORKTREE_METADATA_SCHEMA = ' + JSON.stringify(WORKTREE_METADATA_SCHEMA));
470
548
  lines.push('function gsdWorktreeMetadata(agentResult) {');
471
- lines.push(' if (typeof agentResult !== \'string\') return null;');
472
- lines.push(' const m = agentResult.match(/<worktree_metadata>([\\s\\S]*?)<\\/worktree_metadata>/);');
473
- lines.push(' if (m === null) return null;');
549
+ lines.push(' if (agentResult === null || typeof agentResult !== \'object\' || Array.isArray(agentResult)) return null;');
550
+ lines.push(' const out = {};');
551
+ lines.push(' for (const k of GSD_WORKTREE_METADATA_SCHEMA.required) {');
552
+ lines.push(' const v = agentResult[k];');
553
+ lines.push(' if (typeof v !== \'string\' || v.length === 0) return null;');
554
+ lines.push(' out[k] = v;');
555
+ lines.push(' }');
556
+ lines.push(' return out;');
557
+ lines.push('}');
558
+ lines.push('function gsdAgentError(agentResult) {');
559
+ lines.push(' return (agentResult !== null && typeof agentResult === \'object\' && typeof agentResult.gsdAgentError === \'string\') ? agentResult.gsdAgentError : null;');
560
+ lines.push('}');
561
+ lines.push('async function gsdRunAgent(brief, opts) {');
474
562
  lines.push(' try {');
475
- lines.push(' const parsed = JSON.parse(m[1]);');
476
- lines.push(' return (parsed !== null && typeof parsed === \'object\') ? parsed : null;');
563
+ lines.push(' return await agent(brief, opts);');
477
564
  lines.push(' } catch (e) {');
478
- lines.push(' return null;');
565
+ lines.push(' return { gsdAgentError: (e !== null && typeof e === \'object\' && typeof e.message === \'string\') ? e.message : String(e) };');
479
566
  lines.push(' }');
480
567
  lines.push('}');
481
568
  lines.push('const gsdAgentOutcomes = [];');
482
569
  lines.push('');
483
- const stagesByWave = [];
484
- let totalPlans = 0;
485
570
  let worktreePlans = 0;
486
571
  for (let wi = 0; wi < waves.length; wi++) {
487
572
  const wave = waves[wi];
488
- const stages = partitionStages(wave.plans);
489
- stagesByWave.push(stages);
490
- totalPlans += wave.plans.length;
573
+ const stages = stagesByWave[wi];
491
574
  for (const p of wave.plans) {
492
575
  if (p.use_worktree !== false)
493
576
  worktreePlans += 1;
@@ -512,7 +595,7 @@ function emitWorkflowScript(input) {
512
595
  // worktree-wf_* branch: the merge chain had no input (#3302).
513
596
  lines.push('const gsdStage_' + wi + '_' + si + ' = await parallel([');
514
597
  for (const p of stagePlans) {
515
- lines.push(' () => agent(' + quoteString(p.brief) + ', ' + agentOptions(p, executorModel) + '),');
598
+ lines.push(' () => gsdRunAgent(' + quoteString(p.brief) + ', ' + agentOptions(p, executorModel) + '),');
516
599
  }
517
600
  lines.push('])');
518
601
  // Positional tagging is decided at EMIT time from the validated manifest,
@@ -523,7 +606,13 @@ function emitWorkflowScript(input) {
523
606
  for (let pi = 0; pi < stagePlans.length; pi++) {
524
607
  const p = stagePlans[pi];
525
608
  const tail = pi < stagePlans.length - 1 ? ',' : '';
526
- lines.push(' { plan: ' + quoteString(p.id) + ', expects_worktree: ' + (p.use_worktree !== false) + ', metadata: gsdWorktreeMetadata(gsdStage_' + wi + '_' + si + '[' + pi + ']) }' + tail);
609
+ const ref = 'gsdStage_' + wi + '_' + si + '[' + pi + ']';
610
+ if (p.use_worktree !== false) {
611
+ lines.push(' { plan: ' + quoteString(p.id) + ', expects_worktree: true, metadata: gsdWorktreeMetadata(' + ref + '), error: gsdAgentError(' + ref + ') }' + tail);
612
+ }
613
+ else {
614
+ lines.push(' { plan: ' + quoteString(p.id) + ', expects_worktree: false, metadata: null, error: gsdAgentError(' + ref + ') }' + tail);
615
+ }
527
616
  }
528
617
  lines.push(')');
529
618
  }
@@ -531,10 +620,11 @@ function emitWorkflowScript(input) {
531
620
  lines.push('');
532
621
  }
533
622
  // #3302: the script's top-level return value is what the Workflow tool hands
534
- // back to the orchestrator. One { plan, expects_worktree, metadata } entry
535
- // per dispatched plan; the orchestrator records every worktree entry via
623
+ // back to the orchestrator. One { plan, expects_worktree, metadata, error }
624
+ // entry per dispatched plan; the orchestrator records every worktree entry via
536
625
  // `gsd_run query worktree.record-agent` and HALTS on a null metadata entry
537
- // for an expects_worktree plan (see the execute:wave:pre fragment) — a
626
+ // for an expects_worktree plan (see
627
+ // capabilities/claude-orchestration/docs/workflow-backend-dispatch.md) — a
538
628
  // silently-empty manifest is the exact #3302 failure mode.
539
629
  lines.push('return gsdAgentOutcomes');
540
630
  const script = lines.join('\n');
@@ -586,7 +676,7 @@ function resolveWaveDispatch(input) {
586
676
  runtimeId: input.runtimeId,
587
677
  hostIntegration: input.hostIntegration,
588
678
  config: input.config,
589
- agentSdkVersion: input.agentSdkVersion,
679
+ workflowAvailability: input.workflowAvailability,
590
680
  });
591
681
  if (detected.backend !== 'workflow') {
592
682
  return { backend: 'inline', reason: detected.reason };
@@ -612,9 +702,11 @@ module.exports = {
612
702
  detectWorkflowBackend,
613
703
  emitWorkflowScript,
614
704
  resolveWaveDispatch,
615
- compareSemver,
616
- isValidSemver,
617
- WORKFLOW_TOOL_FLOOR_VERSION,
705
+ resolveWorkflowAvailability,
618
706
  BACKEND_VALUES,
619
707
  WORKFLOW_RUNTIME,
708
+ DETECT_REASONS,
709
+ WORKFLOW_MAX_ITEMS_PER_CALL,
710
+ WORKFLOW_MAX_AGENTS_PER_RUN,
711
+ WORKTREE_METADATA_SCHEMA,
620
712
  };