feat(#2584): Phase 3 — scheduler consumer + isolation adapters (#2635)

Final phase of #2584 (ADR-1239 Codex-binding amendment). execute-phase now negotiates dispatch.isolation and dispatches through the matching adapter, so a wave's independent plans run concurrently on six runtimes instead of one — with no runtime=== branch in the scheduler.

harness-worktree passes the host's declared isolation flag (claude, cursor); orchestrator-worktree creates the worktree via the Phase-2 verb and spawns the executor into it with the resolved argv/cwd (codex, opencode, kimi, kimi-code); none stays sequential. Undeclared/unknown/unresolvable isolation degrades to none — never an unisolated parallel run.

Fixes two shipped Phase-2 descriptors that per-host research found would fail at spawn: kimi lacked its headless flag (would launch the interactive TUI and hang the orchestrator), and kimi-code named a non-existent binary (Kimi Code installs as 'kimi'). Adds the worktree-path root confinement Phase 2 deferred here, and leading-dash guards on the resolver's prompt/cwd matching the existing git-argument guard.

Closes #2627

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-25 01:50:20 -04:00
committed by GitHub
parent 461c744c31
commit 6ad30f74b6
62 changed files with 1105 additions and 205 deletions

View File

@@ -755,6 +755,7 @@ interface OrchestratorExec {
command: string;
args?: string[];
cwdFlag?: string | null;
promptFlag?: string | null;
}
type OrchestratorExecResolution =
@@ -762,8 +763,8 @@ type OrchestratorExecResolution =
| { ok: false; reason: string };
/**
* Resolve an `orchestratorExec` descriptor + target cwd into a concrete
* argv/cwd shape for a process-spawn primitive.
* Resolve an `orchestratorExec` descriptor + target cwd (+ optional executor
* prompt) into a concrete argv/cwd shape for a process-spawn primitive.
*
* Fail-closed: never throws, always returns a discriminated result. When
* `cwdFlag` is a non-empty string, `[cwdFlag, cwd]` is appended to `args`
@@ -771,8 +772,23 @@ type OrchestratorExecResolution =
* absent (e.g. kimi-code, which binds via the spawned process's own cwd —
* "process-cwd" case), no flag is appended and `cwd` is returned for the
* caller to bind via the subprocess's own working-directory option.
*
* Prompt passing (Phase 3, #2627) is descriptor data for the same reason the
* cwd flag is: the confirmed `orchestrator-worktree` hosts disagree on the
* shape. `codex exec "<prompt>"` and `opencode run "<prompt>"` take it
* positionally; `kimi --print --prompt "<p>"` and Kimi Code's `kimi -p "<p>"`
* take a flag. Encoding that as `promptFlag` keeps the scheduler free of the
* per-host branch ADR-1239 exists to remove. Omit `prompt` entirely and the
* resolution is byte-identical to Phase 2's (the unconsumed-resolver shape).
*
* Argv order is base args → cwd flag → prompt, so the prompt stays the final
* positional token for the hosts that read it that way.
*/
function resolveOrchestratorExec(orchestratorExec: OrchestratorExec | undefined, cwd: string): OrchestratorExecResolution {
function resolveOrchestratorExec(
orchestratorExec: OrchestratorExec | undefined,
cwd: string,
prompt?: string,
): OrchestratorExecResolution {
if (!orchestratorExec || typeof orchestratorExec !== 'object' || Array.isArray(orchestratorExec)) {
return { ok: false, reason: 'missing_command' };
}
@@ -789,12 +805,42 @@ function resolveOrchestratorExec(orchestratorExec: OrchestratorExec | undefined,
if (oe.cwdFlag !== undefined && oe.cwdFlag !== null && typeof oe.cwdFlag !== 'string') {
return { ok: false, reason: 'invalid_cwd_flag' };
}
if (oe.promptFlag !== undefined && oe.promptFlag !== null && typeof oe.promptFlag !== 'string') {
return { ok: false, reason: 'invalid_prompt_flag' };
}
// An executor spawned with no instruction is a hang, not a degraded run —
// fail closed rather than launching a prompt-less process.
if (prompt !== undefined && (typeof prompt !== 'string' || prompt.length === 0)) {
return { ok: false, reason: 'invalid_prompt' };
}
// Leading-dash guard, mirroring worktree-safety.cts's `unsafe_leading_dash`
// check on git arguments. A positional prompt (or a cwd) beginning with '-'
// is parsed by the spawned CLI as a FLAG, not a value — the same failure the
// git path already rejects, and for the same reason: `--` end-of-options
// support is inconsistent across these CLIs, so rejecting outright is the
// portable fix rather than relying on a separator. Applied to the resolver
// (not just its current caller) because this is a general descriptor->argv
// seam: a future caller must not have to rediscover the hazard.
if (typeof prompt === 'string' && prompt.startsWith('-')) {
return { ok: false, reason: 'unsafe_leading_dash_prompt' };
}
if (cwd.startsWith('-')) {
return { ok: false, reason: 'unsafe_leading_dash_cwd' };
}
const baseArgs = Array.isArray(oe.args) ? [...oe.args] : [];
const args = typeof oe.cwdFlag === 'string' && oe.cwdFlag.length > 0
? [...baseArgs, oe.cwdFlag, cwd]
: baseArgs;
if (typeof prompt === 'string') {
if (typeof oe.promptFlag === 'string' && oe.promptFlag.length > 0) {
args.push(oe.promptFlag, prompt);
} else {
args.push(prompt);
}
}
return { ok: true, command: oe.command, args, cwd };
}