feat(#2584): Phase 2 — worktree create verb + orchestrator-exec resolver (#2625)

* feat(#2584): Phase 2 — worktree create verb + orchestrator-exec resolver

Phase 2 of the negotiated executor-isolation feature (ADR-1239 Codex-binding amendment). Two building blocks for `dispatch.isolation: orchestrator-worktree` hosts, both unconsumed — no scheduler wires them yet (that is Phase 3), so no runtime behavior changes.

worktree create verb (planWorktreeCreate / executeWorktreeCreatePlan / cmdWorktreeCreate in worktree-safety.cts, routed via routeWorktree in gsd-tools.cjs): validates the wave base, creates a bounded branch+worktree, records it in the run manifest reusing record-agent 4-field entry shape, returns the executor working directory. Bounded git (10s timeout, degrade-not-throw); all manifest read/parse/validate/dedupe precedes the single git side effect (no unmanifested-orphan on a bad manifest); timeout-only best-effort partial rollback (a clean collision-exit never removes a live peer worktree); fail-closed on bad base, unsafe leading-dash / .. inputs, and malformed/mis-shaped manifest.

resolveOrchestratorExec (host-integration.cts): pure descriptor->argv resolver reading the new runtime.orchestratorExec descriptor field (codex/opencode/kimi/kimi-code), fail-closed on missing/invalid shape. Validator (capability-validator.cjs) + a parity guard asserting every orchestrator-worktree host declares a resolvable orchestratorExec.

Adding the create route edits the installed gsd-core/bin/gsd-tools.cjs, so the golden-install-parity fixtures for all 19 runtimes are regenerated (npm run gen:golden) — the only changed hash is gsd-tools.cjs. CONTEXT.md glossary updated; capability-registry regenerated. Behavioral tests (worktree-safety + host-integration) incl. a fast-check property test and the parity sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: rebuild tracked state-transition.cjs to match #2400 source

The tracked compiled artifact drifted from src/state-transition.cts: #2400 (commit 2bcfaa2e2) added the progress.total_plans frontmatter sync to source but the tracked bin/lib/state-transition.cjs was never rebuilt, so the fix was not shipping to consumers of the compiled artifact. The mandatory build:lib step for Phase 2 surfaced the drift; recompiling makes the already-merged, already-changelogged #2400 fix effective. Artifact-only resync (no source/test change); drift class tracked by #2591.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-24 21:23:06 -04:00
committed by GitHub
parent 7e1c736a3e
commit 4a66d62d10
32 changed files with 1417 additions and 22 deletions

View File

@@ -739,6 +739,65 @@ function extensionEventSurfaceFor(extensionEvents: unknown): readonly string[] |
return EXTENSION_EVENT_SURFACES[extensionEvents] || null;
}
// ---------------------------------------------------------------------------
// resolveOrchestratorExec — ADR-1239 Codex-binding amendment (#2584), Phase 2
//
// The `orchestratorExec` descriptor field (sibling of `runtime.hostBehaviors`
// in capability.json) tells GSD how to process-spawn a host's own CLI as the
// executor inside a worktree GSD itself created (`isolation:
// 'orchestrator-worktree'`). Pure, no I/O — this only shapes the argv/cwd a
// caller would pass to a process-spawn primitive; it does not spawn anything
// itself. UNCONSUMED in Phase 2 — no scheduler calls this yet (Phase 3 wires
// it to the actual spawn).
// ---------------------------------------------------------------------------
interface OrchestratorExec {
command: string;
args?: string[];
cwdFlag?: string | null;
}
type OrchestratorExecResolution =
| { ok: true; command: string; args: string[]; cwd: string }
| { ok: false; reason: string };
/**
* Resolve an `orchestratorExec` descriptor + target cwd 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`
* exactly once (e.g. codex: `exec --cd <cwd>`); when `cwdFlag` is `null` or
* 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.
*/
function resolveOrchestratorExec(orchestratorExec: OrchestratorExec | undefined, cwd: string): OrchestratorExecResolution {
if (!orchestratorExec || typeof orchestratorExec !== 'object' || Array.isArray(orchestratorExec)) {
return { ok: false, reason: 'missing_command' };
}
const oe = orchestratorExec as unknown as Record<string, unknown>;
if (typeof oe.command !== 'string' || oe.command.length === 0) {
return { ok: false, reason: 'missing_command' };
}
if (typeof cwd !== 'string' || cwd.length === 0) {
return { ok: false, reason: 'invalid_cwd' };
}
if (oe.args !== undefined && (!Array.isArray(oe.args) || !oe.args.every((a) => typeof a === 'string'))) {
return { ok: false, reason: 'invalid_args' };
}
if (oe.cwdFlag !== undefined && oe.cwdFlag !== null && typeof oe.cwdFlag !== 'string') {
return { ok: false, reason: 'invalid_cwd_flag' };
}
const baseArgs = Array.isArray(oe.args) ? [...oe.args] : [];
const args = typeof oe.cwdFlag === 'string' && oe.cwdFlag.length > 0
? [...baseArgs, oe.cwdFlag, cwd]
: baseArgs;
return { ok: true, command: oe.command, args, cwd };
}
// ---------------------------------------------------------------------------
// Module export (CommonJS — matches existing src/*.cts pattern)
// ---------------------------------------------------------------------------
@@ -759,4 +818,5 @@ export = {
resolveDispatchType,
hookEventSurfaceFor,
extensionEventSurfaceFor,
resolveOrchestratorExec,
};