The claude-orchestration capability (#1143) shipped registered 'active' but fully inert: detectWorkflowBackend/emitWorkflowScript had no caller outside their own CLI router, and execute-phase.md declared an execute:wave:pre hook point that the workflow body never rendered — so claude_orchestration.enabled:true had zero effect on real runs. Approach B (maintainer-chosen): - execute-phase.md now renders the execute:wave:pre hook (gsd_run loop render-hooks execute:wave:pre) at a new step 2.75, immediately before each wave's Agent() dispatch — fixing the latent dead-hook gap for any pre-wave capability. - Move the claude-orchestration contribution execute:wave:post -> execute:wave:pre (a pre-wave backend selector belongs before dispatch, not after); rename fragments/execute-wave-post.md -> execute-wave-pre.md with prose instructing the orchestrator to call resolve-wave-dispatch before step 3. Unrelated wave:post contributions (ui.safety-gate, drift, external-job, mempalace) untouched. - New .cts seam resolveWaveDispatch(input) composes detectWorkflowBackend + emitWorkflowScript into one {backend:'inline'|'workflow', ...} result; exposed as gsd-tools claude-orchestration resolve-wave-dispatch. This is a real non-CLI-router, non-test caller of both functions. Fail-closed: any gate miss (disabled, non-Claude runtime, Workflow tool absent, SDK below floor, execution_backend:inline, malformed input) or an emit failure resolves to inline with a byte-identical result shape — no regression to the default-off execute-phase path. Regression tests (tests/fix-2285-*) cover happy-path activation + SDK-floor BVA, the fail-closed gate-miss table with detectWorkflowBackend parity, a fast-check composition property, capability.json contribution assertions, and a source-contract guard that execute:wave:pre is now actually rendered. Dependent registry-shape assertions updated in-scope. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,20 @@
|
||||
* emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]
|
||||
* Reads a wave/plan manifest JSON file and emits the generated Workflow
|
||||
* script + summary. The manifest shape matches emitWorkflowScript's input:
|
||||
* { waves: [{ id, plans: [{ id, brief, files_modified: string[] }] }] }.
|
||||
* { waves: [{ id, plans: [{ id, brief, files_modified: string[], use_worktree?: boolean }] }] }.
|
||||
* `use_worktree` defaults to true; pass `false` for a plan the inline path
|
||||
* (execute-phase.md step 2.5) would also keep out of worktree isolation
|
||||
* (submodule-touching plans — #2772 / #2285 finding 1).
|
||||
*
|
||||
* resolve-wave-dispatch --waves <path> --run-id <id> [--runtime <id>]
|
||||
* [--agent-sdk-version <ver>] [--no-nested-dispatch] [--phase-dir <dir>]
|
||||
* [--budget <n>]
|
||||
* #2285 — the single composed seam a PRE-wave dispatch-backend selector
|
||||
* (`execute:wave:pre`) uses: resolves detect-backend + emit-workflow in
|
||||
* ONE call. Emits { backend: 'inline'|'workflow', reason, script?, summary? }.
|
||||
* Fail-closed identically to detect-backend/emit-workflow individually —
|
||||
* any gate miss, or an emit failure on a malformed --waves manifest,
|
||||
* resolves to 'inline' with no script.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
@@ -34,7 +47,7 @@ import core = require('./claude-orchestration.cjs');
|
||||
import configLoader = require('./config-loader.cjs');
|
||||
|
||||
const { output } = io;
|
||||
const { detectWorkflowBackend, emitWorkflowScript } = core;
|
||||
const { detectWorkflowBackend, emitWorkflowScript, resolveWaveDispatch } = core;
|
||||
|
||||
const CAPABLE_HOST = { dispatch: { nested: true, background: true } };
|
||||
|
||||
@@ -47,9 +60,10 @@ interface RouterOpts {
|
||||
|
||||
function usage(error: (msg: string, reason?: string) => void): void {
|
||||
error(
|
||||
'Usage: gsd-tools claude-orchestration <detect-backend|emit-workflow> [...]\n' +
|
||||
'Usage: gsd-tools claude-orchestration <detect-backend|emit-workflow|resolve-wave-dispatch> [...]\n' +
|
||||
' detect-backend [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch]\n' +
|
||||
' emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]',
|
||||
' emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]\n' +
|
||||
' resolve-wave-dispatch --waves <path> --run-id <id> [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch] [--phase-dir <dir>] [--budget <n>]',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,19 +73,13 @@ function argValue(args: string[], flag: string): string | undefined {
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether the Workflow backend should activate for the current/given
|
||||
* runtime. Reads `claude_orchestration.*` from the project config; runtime and
|
||||
* SDK version come from flags (the orchestrator already knows these) or env.
|
||||
* Resolve the `claude_orchestration.*` config slice from the project config
|
||||
* (federated keys are merged by loadConfig as a nested object), flattened into
|
||||
* the dotted-key shape `detectWorkflowBackend`/`resolveWaveDispatch` expect. A
|
||||
* config read failure degrades to an empty slice — it must not break the core
|
||||
* loop. Shared by `detect-backend` and `resolve-wave-dispatch`.
|
||||
*/
|
||||
function cmdDetectBackend(args: string[], cwd: string, raw: boolean): void {
|
||||
const runtimeId = argValue(args, '--runtime') || process.env['GSD_RUNTIME'] || 'unknown';
|
||||
const agentSdkVersion = argValue(args, '--agent-sdk-version');
|
||||
const noNested = args.includes('--no-nested-dispatch');
|
||||
const hostIntegration = noNested ? { dispatch: { nested: false, background: true } } : CAPABLE_HOST;
|
||||
|
||||
// Resolve the claude_orchestration.* slice from the project config (federated
|
||||
// keys are merged by loadConfig as a nested object). A config read failure
|
||||
// degrades to inline — it must not break the core loop.
|
||||
function resolveFlatClaudeOrchestrationConfig(cwd: string): Record<string, unknown> {
|
||||
let claudeSlice: Record<string, unknown> = {};
|
||||
try {
|
||||
const loaded = configLoader.loadConfig(cwd);
|
||||
@@ -83,12 +91,63 @@ function cmdDetectBackend(args: string[], cwd: string, raw: boolean): void {
|
||||
claudeSlice = {};
|
||||
}
|
||||
|
||||
// Flatten the nested slice into the dotted-key shape detectWorkflowBackend expects.
|
||||
const flatConfig: Record<string, unknown> = {};
|
||||
for (const k of Object.keys(claudeSlice)) {
|
||||
flatConfig['claude_orchestration.' + k] = claudeSlice[k];
|
||||
}
|
||||
return flatConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve `--runtime`/`--agent-sdk-version`/`--no-nested-dispatch` into the
|
||||
* `{ runtimeId, hostIntegration, agentSdkVersion }` triple both `detect-backend`
|
||||
* and `resolve-wave-dispatch` pass to the pure detection seam.
|
||||
*/
|
||||
function resolveDetectionArgs(args: string[]): { runtimeId: string; hostIntegration: { dispatch: { nested: boolean; background: boolean } }; agentSdkVersion: string | undefined } {
|
||||
const runtimeId = argValue(args, '--runtime') || process.env['GSD_RUNTIME'] || 'unknown';
|
||||
const agentSdkVersion = argValue(args, '--agent-sdk-version');
|
||||
const noNested = args.includes('--no-nested-dispatch');
|
||||
const hostIntegration = noNested ? { dispatch: { nested: false, background: true } } : CAPABLE_HOST;
|
||||
return { runtimeId, hostIntegration, agentSdkVersion };
|
||||
}
|
||||
|
||||
/** Discriminated result for readWavesManifest — see doc comment below. */
|
||||
type WavesReadResult =
|
||||
| { ok: true; waves: unknown }
|
||||
| { ok: false };
|
||||
|
||||
/**
|
||||
* Read and parse a `--waves <path>` manifest file.
|
||||
*
|
||||
* #2285 finding 2: a real read/parse failure (`ok:false`) is DISTINCT from a
|
||||
* manifest that parsed fine but has no top-level `waves` key (`ok:true, waves:
|
||||
* undefined`) — collapsing both into the same sentinel made the missing-key
|
||||
* case exit 0 with ZERO output (fail-silent), breaking the "exit 0 => parseable
|
||||
* JSON verdict" contract callers rely on. Only the `ok:false` (read/parse threw)
|
||||
* case calls `error(...)` and should short-circuit the caller; `ok:true` with a
|
||||
* missing/malformed `waves` value must flow through to `emitWorkflowScript`'s
|
||||
* own validation (matching how `{"waves": null}` already behaves) so the caller
|
||||
* emits an explicit, non-empty verdict instead of silently doing nothing.
|
||||
*/
|
||||
function readWavesManifest(wavesPath: string, error: (msg: string, reason?: string) => void): WavesReadResult {
|
||||
try {
|
||||
const content = fs.readFileSync(path.resolve(wavesPath), 'utf8');
|
||||
const parsed = JSON.parse(content) as Record<string, unknown>;
|
||||
return { ok: true, waves: parsed['waves'] };
|
||||
} catch (e) {
|
||||
error('could not read/parse --waves file "' + wavesPath + '": ' + (e instanceof Error ? e.message : String(e)));
|
||||
return { ok: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether the Workflow backend should activate for the current/given
|
||||
* runtime. Reads `claude_orchestration.*` from the project config; runtime and
|
||||
* SDK version come from flags (the orchestrator already knows these) or env.
|
||||
*/
|
||||
function cmdDetectBackend(args: string[], cwd: string, raw: boolean): void {
|
||||
const { runtimeId, hostIntegration, agentSdkVersion } = resolveDetectionArgs(args);
|
||||
const flatConfig = resolveFlatClaudeOrchestrationConfig(cwd);
|
||||
const result = detectWorkflowBackend({ runtimeId, hostIntegration, config: flatConfig, agentSdkVersion });
|
||||
output(result, raw);
|
||||
}
|
||||
@@ -111,15 +170,8 @@ function cmdEmitWorkflow(args: string[], _cwd: string, raw: boolean, error: (msg
|
||||
return;
|
||||
}
|
||||
|
||||
let waves: unknown;
|
||||
try {
|
||||
const content = fs.readFileSync(path.resolve(wavesPath), 'utf8');
|
||||
const parsed = JSON.parse(content) as Record<string, unknown>;
|
||||
waves = parsed['waves'];
|
||||
} catch (e) {
|
||||
error('emit-workflow: could not read/parse --waves file "' + wavesPath + '": ' + (e instanceof Error ? e.message : String(e)));
|
||||
return;
|
||||
}
|
||||
const read = readWavesManifest(wavesPath, (msg) => error('emit-workflow: ' + msg));
|
||||
if (!read.ok) return; // read/parse failure — error() already surfaced it loudly above
|
||||
|
||||
const budgetTokens = budgetRaw !== undefined ? parseInt(budgetRaw, 10) : undefined;
|
||||
const budget = (typeof budgetTokens === 'number' && !Number.isNaN(budgetTokens)) ? budgetTokens : undefined;
|
||||
@@ -127,7 +179,7 @@ function cmdEmitWorkflow(args: string[], _cwd: string, raw: boolean, error: (msg
|
||||
const result = emitWorkflowScript({
|
||||
phaseDir,
|
||||
runId,
|
||||
waves: waves as EmitInput['waves'],
|
||||
waves: read.waves as EmitInput['waves'],
|
||||
budgetTokens: budget,
|
||||
});
|
||||
|
||||
@@ -138,9 +190,53 @@ function cmdEmitWorkflow(args: string[], _cwd: string, raw: boolean, error: (msg
|
||||
output({ script: result.script, summary: result.summary }, raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* #2285 — the single composed seam a PRE-wave dispatch-backend selector
|
||||
* (`execute:wave:pre`) uses: resolves `detect-backend` + `emit-workflow` in
|
||||
* ONE call via `resolveWaveDispatch`. Emits
|
||||
* `{ backend: 'inline'|'workflow', reason, script?, summary? }`.
|
||||
*/
|
||||
function cmdResolveWaveDispatch(args: string[], cwd: string, raw: boolean, error: (msg: string, reason?: string) => void): void {
|
||||
const wavesPath = argValue(args, '--waves');
|
||||
const runId = argValue(args, '--run-id');
|
||||
const phaseDir = argValue(args, '--phase-dir') || '.planning/phases/current';
|
||||
const budgetRaw = argValue(args, '--budget');
|
||||
|
||||
if (!wavesPath) {
|
||||
error('resolve-wave-dispatch requires --waves <path>');
|
||||
return;
|
||||
}
|
||||
if (!runId) {
|
||||
error('resolve-wave-dispatch requires --run-id <id>');
|
||||
return;
|
||||
}
|
||||
|
||||
const read = readWavesManifest(wavesPath, (msg) => error('resolve-wave-dispatch: ' + msg));
|
||||
if (!read.ok) return; // read/parse failure — error() already surfaced it loudly above
|
||||
|
||||
const { runtimeId, hostIntegration, agentSdkVersion } = resolveDetectionArgs(args);
|
||||
const flatConfig = resolveFlatClaudeOrchestrationConfig(cwd);
|
||||
|
||||
const budgetTokens = budgetRaw !== undefined ? parseInt(budgetRaw, 10) : undefined;
|
||||
const budget = (typeof budgetTokens === 'number' && !Number.isNaN(budgetTokens)) ? budgetTokens : undefined;
|
||||
|
||||
const result = resolveWaveDispatch({
|
||||
runtimeId,
|
||||
hostIntegration,
|
||||
config: flatConfig,
|
||||
agentSdkVersion,
|
||||
phaseDir,
|
||||
runId,
|
||||
waves: read.waves as EmitInput['waves'],
|
||||
budgetTokens: budget,
|
||||
});
|
||||
|
||||
output(result, raw);
|
||||
}
|
||||
|
||||
// Re-declared minimal input type for the cast above (avoids importing private types).
|
||||
interface EmitInput {
|
||||
waves: Array<{ id: string; plans: Array<{ id: string; brief: string; files_modified: string[] }> }>;
|
||||
waves: Array<{ id: string; plans: Array<{ id: string; brief: string; files_modified: string[]; use_worktree?: boolean }> }>;
|
||||
}
|
||||
|
||||
function routeClaudeOrchestrationCommand(opts: RouterOpts): void {
|
||||
@@ -151,6 +247,8 @@ function routeClaudeOrchestrationCommand(opts: RouterOpts): void {
|
||||
cmdDetectBackend(args, cwd, raw);
|
||||
} else if (subcommand === 'emit-workflow') {
|
||||
cmdEmitWorkflow(args, cwd, raw, error);
|
||||
} else if (subcommand === 'resolve-wave-dispatch') {
|
||||
cmdResolveWaveDispatch(args, cwd, raw, error);
|
||||
} else {
|
||||
usage(error);
|
||||
}
|
||||
|
||||
@@ -15,15 +15,20 @@
|
||||
* → { ok:true, script, summary } | { ok:false, reason }
|
||||
* Maps GSD's wave/plan model 1:1 onto Workflow primitives:
|
||||
* wave → sequential `parallel()` stage barriers,
|
||||
* plan → `agent(brief, { agentType:'gsd-executor', isolation:'worktree' })`,
|
||||
* plan → `agent(brief, { agentType:'gsd-executor', isolation:'worktree' })`
|
||||
* — UNLESS the plan's `use_worktree` is explicitly `false`, in which case
|
||||
* `isolation` is omitted entirely for that plan (#2772 / #2285 finding 1:
|
||||
* a submodule-touching plan must never be forced into worktree isolation
|
||||
* the inline path (execute-phase.md step 2.5) would keep it out of),
|
||||
* files_modified overlap → forces plans into separate sequential stages
|
||||
* (the same overlap rule execute-phase already applies inline),
|
||||
* resumeFromRunId → wired to the phase run id,
|
||||
* budgetTokens → a shared token pool.
|
||||
* The emitted script composes the SAME gsd-executor agent and worktree
|
||||
* isolation the inline path uses, so it produces the same artifacts/commits
|
||||
* (criterion 2). It is a generated string consumed by the orchestrator; this
|
||||
* module never invokes the Workflow tool itself.
|
||||
* The emitted script composes the SAME gsd-executor agent the inline path
|
||||
* uses, with per-plan worktree isolation mirroring the inline path's own
|
||||
* per-plan decision, so it produces the same artifacts/commits (criterion 2).
|
||||
* It is a generated string consumed by the orchestrator; this module never
|
||||
* invokes the Workflow tool itself.
|
||||
*
|
||||
* Design laws:
|
||||
* - Gall's Law: ship a small working slice that composes existing primitives
|
||||
@@ -251,6 +256,21 @@ interface Plan {
|
||||
id: string;
|
||||
brief: string;
|
||||
files_modified: string[];
|
||||
/**
|
||||
* #2772 / #2285 finding 1 — mirrors execute-phase.md step 2.5's
|
||||
* `USE_WORKTREES_FOR_PLAN` (per-plan submodule-intersection + project-level
|
||||
* `workflow.use_worktrees` gate). The inline dispatch path in step 3 omits
|
||||
* `isolation="worktree"` for a plan that touches a submodule path (the
|
||||
* executor commit protocol cannot correctly handle submodule commits inside
|
||||
* an isolated worktree). The Workflow backend MUST honor the SAME per-plan
|
||||
* decision — it must never force worktree isolation on a plan the inline
|
||||
* path would keep out of worktrees.
|
||||
*
|
||||
* Optional, defaults to `true` (preserves prior behavior for callers that
|
||||
* don't populate it — e.g. a manifest with no submodule paths at all).
|
||||
* Only an explicit `false` omits `isolation: "worktree"` for that plan.
|
||||
*/
|
||||
use_worktree?: boolean;
|
||||
}
|
||||
|
||||
interface Wave {
|
||||
@@ -331,6 +351,18 @@ function quoteString(s: string): string {
|
||||
return JSON.stringify(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `agent()` options object for a single plan — `isolation: "worktree"`
|
||||
* ONLY when the plan's `use_worktree` is not explicitly `false` (#2772 / #2285
|
||||
* finding 1). This is the single place that decides worktree isolation for the
|
||||
* Workflow backend; it must never diverge from the inline path's per-plan gate.
|
||||
*/
|
||||
function agentOptions(p: Plan): string {
|
||||
return p.use_worktree === false
|
||||
? '{ agentType: "gsd-executor" }'
|
||||
: '{ agentType: "gsd-executor", isolation: "worktree" }';
|
||||
}
|
||||
|
||||
/**
|
||||
* True if `s` is a safe identifier/path token to interpolate into the generated
|
||||
* script WITHOUT requiring a string-literal context — i.e. it contains no
|
||||
@@ -390,6 +422,9 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
|
||||
if (!isScriptableIdentifier(p.id)) {
|
||||
return { ok: false, reason: 'waves[' + i + '].plans[' + j + '].id must not contain newlines/quotes/backslash/control chars' };
|
||||
}
|
||||
if (p.use_worktree !== undefined && typeof p.use_worktree !== 'boolean') {
|
||||
return { ok: false, reason: 'waves[' + i + '].plans[' + j + '].use_worktree must be a boolean if present' };
|
||||
}
|
||||
if (seenIds.has(p.id)) {
|
||||
return { ok: false, reason: 'waves[' + i + '] has duplicate plan id "' + p.id + '"' };
|
||||
}
|
||||
@@ -410,8 +445,9 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
|
||||
lines.push('// GSD Workflow script — generated by the claude-orchestration capability (#1143)');
|
||||
lines.push('// phase: ' + phaseDir);
|
||||
lines.push('// BETA: preview-grade; on any failure the orchestrator falls back to inline dispatch.');
|
||||
lines.push('// Composes the SAME gsd-executor agent + worktree isolation as the inline path,');
|
||||
lines.push('// so artifacts (SUMMARY.md) and commits are produced identically.');
|
||||
lines.push('// Composes the SAME gsd-executor agent as the inline path, so artifacts (SUMMARY.md)');
|
||||
lines.push('// and commits are produced identically. Worktree isolation is per-plan (use_worktree)');
|
||||
lines.push('// and mirrors execute-phase.md step 2.5\'s submodule gate exactly (#2772 / #2285).');
|
||||
lines.push('resumeFromRunId(' + quoteString(runId) + ')');
|
||||
if (budgetTokens !== null) {
|
||||
lines.push('budget(' + budgetTokens + ')');
|
||||
@@ -438,12 +474,12 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
|
||||
if (stagePlans.length === 1) {
|
||||
const p = stagePlans[0];
|
||||
lines.push('parallel(');
|
||||
lines.push(' agent(' + quoteString(p.brief) + ', { agentType: "gsd-executor", isolation: "worktree" })');
|
||||
lines.push(' agent(' + quoteString(p.brief) + ', ' + agentOptions(p) + ')');
|
||||
lines.push(')');
|
||||
} else {
|
||||
lines.push('parallel(');
|
||||
for (const p of stagePlans) {
|
||||
lines.push(' agent(' + quoteString(p.brief) + ', { agentType: "gsd-executor", isolation: "worktree" }),');
|
||||
lines.push(' agent(' + quoteString(p.brief) + ', ' + agentOptions(p) + '),');
|
||||
}
|
||||
// Replace trailing comma on the last agent line with nothing.
|
||||
const lastIdx = lines.length - 1;
|
||||
@@ -472,11 +508,99 @@ function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitE
|
||||
};
|
||||
}
|
||||
|
||||
// ─── resolveWaveDispatch ──────────────────────────────────────────────────────
|
||||
|
||||
interface ResolveWaveDispatchInput {
|
||||
runtimeId?: string;
|
||||
hostIntegration?: HostIntegration | null;
|
||||
config?: BackendConfig | null;
|
||||
agentSdkVersion?: string;
|
||||
phaseDir: string;
|
||||
waves: Wave[];
|
||||
runId: string;
|
||||
budgetTokens?: number;
|
||||
}
|
||||
|
||||
interface ResolveWaveDispatchInline {
|
||||
backend: 'inline';
|
||||
reason: string;
|
||||
}
|
||||
|
||||
interface ResolveWaveDispatchWorkflow {
|
||||
backend: 'workflow';
|
||||
reason: string;
|
||||
script: string;
|
||||
summary: EmitOk['summary'];
|
||||
}
|
||||
|
||||
type ResolveWaveDispatchResult = ResolveWaveDispatchInline | ResolveWaveDispatchWorkflow;
|
||||
|
||||
/**
|
||||
* #2285 — single composed decision seam for a PRE-wave dispatch-backend selector
|
||||
* (e.g. the `execute:wave:pre` claude-orchestration contribution). Composes
|
||||
* `detectWorkflowBackend` (gate ladder) with `emitWorkflowScript` (wave→plan
|
||||
* mapping) into ONE call so the orchestrator (and its CLI wrapper,
|
||||
* `claude-orchestration resolve-wave-dispatch`) never has to re-implement the
|
||||
* two-step "detect, then maybe emit" sequencing.
|
||||
*
|
||||
* Fail-closed at every layer, matching the two composed functions:
|
||||
* - `detectWorkflowBackend` resolving anything other than `'workflow'` →
|
||||
* `inline` immediately; `emitWorkflowScript` is never invoked (no wasted
|
||||
* work, no risk of a bad emit masking a correct inline fallback).
|
||||
* - `detectWorkflowBackend` resolves `'workflow'` but `emitWorkflowScript`
|
||||
* fails (`ok:false` — e.g. a malformed wave manifest) → `inline`, carrying
|
||||
* the emit failure reason so the caller can surface it. Never a partial or
|
||||
* broken script.
|
||||
*
|
||||
* This is the designated non-CLI-router, non-test caller of
|
||||
* `detectWorkflowBackend` and `emitWorkflowScript` — the standalone CLI
|
||||
* subcommands (`detect-backend`, `emit-workflow`) remain for inspection/
|
||||
* debugging, but the orchestrator's real per-wave dispatch decision goes
|
||||
* through this seam.
|
||||
*
|
||||
* Never throws on bad input.
|
||||
*/
|
||||
function resolveWaveDispatch(input: ResolveWaveDispatchInput | null | undefined): ResolveWaveDispatchResult {
|
||||
if (input === null || input === undefined || typeof input !== 'object') {
|
||||
return { backend: 'inline', reason: 'invalid_input' };
|
||||
}
|
||||
|
||||
const detected = detectWorkflowBackend({
|
||||
runtimeId: input.runtimeId,
|
||||
hostIntegration: input.hostIntegration,
|
||||
config: input.config,
|
||||
agentSdkVersion: input.agentSdkVersion,
|
||||
});
|
||||
|
||||
if (detected.backend !== 'workflow') {
|
||||
return { backend: 'inline', reason: detected.reason };
|
||||
}
|
||||
|
||||
const emitted = emitWorkflowScript({
|
||||
phaseDir: input.phaseDir,
|
||||
waves: input.waves,
|
||||
runId: input.runId,
|
||||
budgetTokens: input.budgetTokens,
|
||||
});
|
||||
|
||||
if (!emitted.ok) {
|
||||
return { backend: 'inline', reason: 'emit_failed: ' + emitted.reason };
|
||||
}
|
||||
|
||||
return {
|
||||
backend: 'workflow',
|
||||
reason: detected.reason,
|
||||
script: emitted.script,
|
||||
summary: emitted.summary,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Exports ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export = {
|
||||
detectWorkflowBackend,
|
||||
emitWorkflowScript,
|
||||
resolveWaveDispatch,
|
||||
compareSemver,
|
||||
isValidSemver,
|
||||
WORKFLOW_TOOL_FLOOR_VERSION,
|
||||
|
||||
Reference in New Issue
Block a user