#3711·gsd-core

Cross-runtime plan execution: per-plan `executor_runtime`, so a harness-worktree host can dispatch one plan to an orchestrator-worktree executor

Author: ccultureCreated Aug 20, 2026Updated Sep 21, 2026
Labelsready-for-humanStale

Pre-submission checklist

  • I have searched existing issues and discussions, this has not been proposed and declined before (closest prior art: #1689 / #3417 and #2584, both scoped to a single runtime; #2291 is provider fallback inside one runtime, not executor selection)
  • I have read CONTRIBUTING.md and understand that I must wait for approved-feature before writing any code
  • I have read the existing GSD commands and workflows and confirmed this feature does not duplicate existing behavior
  • This feature solves a problem for solo developers using AI coding tools, not a personal preference or workflow I happen to like

Feature name

Cross-runtime plan execution: per-plan executor_runtime, so a harness-worktree host can dispatch one plan to an orchestrator-worktree executor

Type of addition

New runtime integration

The solo developer problem

Today the executor binary is a function of who the host is, and nothing else. resolve-dispatch-isolation (gsd-core/bin/gsd-tools.cjs) resolves the runtime id (GSD_RUNTIME > config.runtime > claude), reads registry.runtimes[id].runtime.hostIntegration.dispatch.isolation, and that single value decides how every plan in every wave is executed:

Runtime dispatch.isolation executor launch
claude harness-worktree Agent(subagent_type=…, isolation="worktree"), orchestratorExec is null
codex orchestrator-worktree codex exec --cd <worktree>
opencode orchestrator-worktree opencode run --dir <worktree>
kimi-code orchestrator-worktree kimi --prompt …

Two concrete failures for a solo developer follow from that binding.

1. A rate limit stops the phase even though a paid executor is sitting idle. A solo developer runs /gsd-execute-phase on a 6-plan phase in Claude Code and exhausts their weekly limit at plan 4. They also pay for a Codex subscription, and the GSD registry already knows how to spawn it (orchestratorExec: {command:"codex", args:["exec"], cwdFlag:"--cd"}). There is no supported way to finish plans 5 and 6 on that executor. The phase halts, the worktrees sit half-merged, and the only way forward is to hand-run the plans outside GSD, losing the atomic-commit protocol, the wave gating, and the state recording that make the artifacts trustworthy.

2. Model fit cannot be expressed per plan, only per project. In a single phase, plan 1 may be a subtle authorization change and plan 5 a mechanical 40-file rename. #1689 established that plans are not interchangeable and deserve different executors, and #3417 shipped agent_hint for exactly that. But agent_hint resolves only within the host runtime's own agent directory, so "route the mechanical sweep to a cheaper executor I already pay for" remains unexpressible. PR #3417 states the boundary explicitly:

Scope boundary, orchestrator-worktree dispatch. That backend (Codex/OpenCode/Kimi process-spawn) has no subagent_type to swap, so v1 routes the Agent()-based paths (harness-worktree + sequential) only. […] Full routing there is a deliberate follow-up.

This issue asks for the other half of that follow-up: not just routing within the orchestrator-worktree backend, but reaching it from a harness-worktree host.

What this feature adds

One optional frontmatter field on a plan, mirroring agent_hint:

yaml
---
# 03-05-PLAN.md
executor_runtime: codex     # spawn this plan's executor as a foreign process
---

and a config gate workflow.cross_runtime_execution (default false, opt-in, unlike agent_hint_routing), because this one spends money on a second vendor and weakens the harness guarantees described below.

Resolution reuses machinery that already exists on both ends:

  • resolve-dispatch-isolation already computes an exec descriptor through resolveOrchestratorExec() in src/host-integration.cts. Today it can only read the host entry; the change is to let a per-plan override select a different registry entry.
  • src/review-lane-descriptor.cts already proves GSD is willing to spawn a foreign CLI from a Claude host, with a command-exists probe, a timeout floor, an empty-output guard, and a per-lane model key (review.models.codex). The review lane is the precedent; this is the same shape pointed at execution instead of critique.
  • worktree create plus the record-dispatch-isolation sentinel already carry the isolation contract the guard hooks read.

Proposed CLI surface, matching the resolve-agent precedent from #3417:

bash
$ gsd-tools resolve-dispatch-isolation --plan 03-05 --cwd-target .worktrees/03-05 --json
{
  "runtime": "claude",
  "executorRuntime": "codex",
  "isolation": "orchestrator-worktree",
  "exec": { "command": "codex", "args": ["exec", "--cd", ".worktrees/03-05"], "cwd": "…" },
  "fallback": false
}

Fail-closed in every unhappy case, consistent with ADR-1239: gate off, unknown runtime id, binary not on PATH, or an orchestratorExec descriptor that does not resolve all degrade to the host's normal dispatch and emit a one-line advisory, exactly as #3417 does when a hint cannot be honored.

Behavior the foreign executor must inherit, and where it differs:

  1. The prompt must be self-contained. A spawned CLI cannot read the host's skills or agent definitions, so the dispatch has to render the execute-plan contract (plan path, atomic-commit protocol, checkpoint rules, scope boundary) into the prompt it feeds on stdin. codex declares promptFlag: null, so stdin is the channel.
  2. Checkpoints fail closed. A foreign executor cannot raise a checkpoint back to the host. Any plan whose frontmatter declares a checkpoint or user gate must refuse executor_runtime at validation time rather than silently skipping the gate.
  3. Model and effort resolve from the target runtime's own catalog, never the host's, so this cannot reintroduce the cross-runtime model poisoning of #2297. The review.models.<lane> key is the precedent; the analogue would be execute.models.<runtime>.
  4. Verification stays on the host. The wave's diff-scope validation (#2596), the plan drift guard, and goal-backward verification all run in the host as they do now. The foreign process only writes code inside its own worktree.

Full scope of changes

Files that would be created:

  • gsd-core/workflows/execute-phase/steps/cross-runtime-executor-dispatch.md, the render fragment for the foreign-spawn path, kept out of execute-phase.md to respect the ADR-857 Phase 6 byte ceiling
  • tests/cross-runtime-execution.test.cjs, resolver unit tests (gate off, unknown runtime, binary absent, descriptor unresolvable, checkpoint plan refused), route tests for --json/--raw, and a data-path test for the new frontmatter field

Files that would be modified:

  • src/phase.cts, parse executor_runtime into the plan-index JSON alongside agent_hint (RawPlan.executorRuntime, emitted as executor_runtime, null when unset), so dispatch does no per-plan file I/O
  • gsd-core/bin/gsd-tools.cjs, extend the resolve-dispatch-isolation route to accept --plan/--executor-runtime and resolve against the named registry entry instead of the host entry; keep the unconditional sentinel write as the sole persistence path
  • src/host-integration.cts, allow resolveOrchestratorExec() to be called with an arbitrary runtime's descriptor, plus a command-exists probe before returning ok
  • src/config.cts, gsd-core/bin/shared/config-schema.manifest.json, gsd-core/bin/shared/config-defaults.manifest.json, register workflow.cross_runtime_execution (default false) and the execute.models.<runtime> key family
  • gsd-core/workflows/execute-phase.md, one-line reference to the new fragment at the dispatch point
  • gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md, document that orchestrator-worktree is now reachable from a harness host
  • gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md, state precedence between agent_hint and executor_runtime (they are mutually exclusive: one names a host subagent, the other a foreign process)
  • src/worktree-safety.cts, confirm the GSD-created worktree path is passed to a foreign cwd the same way it is for a native orchestrator host
  • docs/CONFIGURATION.md, docs/reference/plan-md.md, docs/INVENTORY-MANIFEST.json, CHANGELOG.md / .changeset

Systems affected:

  • Dispatch isolation resolution and its sentinel, which the guard hooks read
  • Plan frontmatter schema (additive, null when unset)
  • Model and effort resolution (must stay runtime-local, see #2297 and #3160)
  • Wave scheduling and diff-scope validation (unchanged, but must be re-asserted against a foreign writer)

User stories

  1. As a solo developer, I want to finish the remaining plans of a phase on a runtime I already pay for when my primary runtime hits a rate limit, so that a subscription ceiling does not strand a half-executed phase.
  2. As a solo developer, I want to send a mechanical, high-token plan to a cheaper executor while keeping the subtle plans on my strongest model, so that phase cost tracks plan difficulty instead of being flat.
  3. As a solo developer, I want that delegation to keep the atomic-commit protocol, worktree isolation, and state recording, so that a cross-runtime plan produces the same trustworthy artifacts as a native one.
  4. As a solo developer, I want a plan that declares a checkpoint to refuse cross-runtime execution outright, so that a foreign process can never silently skip a gate I asked for.

Acceptance criteria

  • A plan declaring executor_runtime: codex under a Claude host, with workflow.cross_runtime_execution=true, is executed by a spawned codex exec --cd <worktree> process against a GSD-created worktree
  • gsd-tools resolve-dispatch-isolation --plan <id> --cwd-target <path> --json reports runtime, executorRuntime, isolation, exec, and fallback
  • With workflow.cross_runtime_execution unset or false, a plan declaring executor_runtime dispatches byte-identically to today and emits a one-line advisory
  • An unknown runtime id, a binary absent from PATH, or an orchestratorExec descriptor that does not resolve each degrade to the host's normal dispatch, never to an unisolated or silently skipped execution
  • A plan that declares a checkpoint or user gate is refused at validation time when executor_runtime is set, with an explicit message naming the conflict
  • Model and effort for the foreign executor resolve from that runtime's own catalog entry, and a test asserts the host's model never leaks into the spawned argv
  • The dispatch sentinel records the resolved mode for a cross-runtime plan, so the guard hooks see the same contract they see for a native orchestrator host
  • The wave's diff-scope validation (#2596) passes over a foreign executor's commits exactly as over a native executor's
  • agent_hint and executor_runtime on the same plan is a validation error, not a silent precedence rule
  • Plans without executor_runtime dispatch byte-identically to today
  • All existing tests still pass, and new tests cover every fallback arm above

Which area does this primarily affect?

Runtime integration (hooks, statusline, settings)

Applicable runtimes

  • Claude Code
  • OpenCode
  • Codex
  • Copilot
  • Antigravity
  • Cursor
  • Windsurf
  • All runtimes

(Any runtime that already declares an orchestratorExec descriptor becomes a valid target for free; any runtime can be the host.)

Breaking changes assessment

None. executor_runtime is additive plan frontmatter that parses to null when unset, and the config gate defaults to false, so an existing project's dispatch is byte-identical until the developer opts in twice: once in config, once per plan. The resolve-dispatch-isolation route keeps its current output shape and gains two fields; existing --raw consumers still read one of the three vocabulary values.

Maintenance burden

Honest accounting, because this is the expensive part of the proposal:

  • A second executor contract to keep true. The atomic-commit protocol currently lives in prose that a subagent reads through the harness. Rendering it into a stdin prompt means a second copy of the contract that can drift from the first. Mitigation: render from the same fragment, do not fork the text.
  • Foreign CLIs change their argv. orchestratorExec descriptors already track this for native hosts, so the surface is shared rather than new, but a codex exec flag rename now breaks execution as well as review.
  • No new dependencies. Everything is process-spawn against a binary the developer already installed, probed with the existing command-exists pattern.
  • Weaker guarantees, and they must stay visible. A spawned process is outside the host's hook layer, so PreToolUse guards do not apply to it. The compensating controls are the worktree boundary and the host-side diff-scope validation, both of which already exist. This is why the gate defaults to off and why checkpoint plans are refused.
  • Interaction with future dispatch work. Any change to wave scheduling, worktree base refs, or the sentinel now has two dispatch shapes to satisfy from one host. #3417 already split the routing logic into its own fragment, which limits the blast radius.

Alternatives considered

  1. Install GSD into Codex and run the pipeline there. Rejected: it swaps the whole pipeline rather than one plan. Planning, review, and verification agents move too, project state ends up split across two harness installs, and the developer loses the Claude-side agents they chose deliberately. It also answers the wrong question: the need is heterogeneous execution inside one project, not a second project.
  2. Set GSD_RUNTIME=codex in a Claude session. Rejected: the resolver that reads it is not confined to dispatch. Runtime identity also drives skills directories, frontmatter dialect, hook surfaces, and agent resolution (runtime-name-policy, runtime-homes, stale-bake-guard). It would produce a session that lies about itself to every subsystem. It is a footgun, not a feature.
  3. Run codex exec by hand outside GSD. Rejected: this is the current workaround and it is exactly what GSD exists to prevent. No plan manifest, no atomic-commit protocol, no state recording, no wave gating, no diff-scope validation. The artifacts stop being trustworthy at the moment the developer most needs them to be.
  4. Extend agent_hint to accept a runtime:name form. Rejected as the primary surface because the two mechanisms differ in kind, not degree: one swaps a subagent_type inside the harness, the other spawns a process outside it, with different failure modes and a different safety story. Overloading one field would hide that difference at exactly the point a reader needs to see it. A separate, mutually exclusive field keeps the distinction legible in the plan frontmatter.
  5. A capability overlay instead of core. Rejected for the reason #1689 documented: hooks cannot mutate Agent tool input, and the pre-dispatch render seam does not exist. Selecting an executor is a deterministic decision that belongs in the resolver, which is where #3417 put it.
  6. Rate-limit-aware provider fallback (#2291, closed). Different axis: that proposal swaps model providers inside one runtime. This one keeps the runtime's own model resolution intact and swaps the process that does the work.

Prior art and references

  • #1689 and PR #3417, per-plan agent_hint executor routing. This issue is the follow-up that PR's scope note names.
  • #2584, negotiated executor-worktree isolation, which introduced dispatch.isolation and the three-value vocabulary this proposal reuses unchanged.
  • src/review-lane-descriptor.cts, the existing pattern for spawning a foreign CLI from any host, with probe, timeout floor, empty-output guard, and per-lane model key. /gsd-review --codex already does the vendor-crossing part of this; it just returns prose instead of commits.
  • #2297 and #3160, on cross-runtime model and effort resolution, which set the constraint that the foreign executor must resolve from its own catalog.
  • #2291, closed, provider fallback, noted above as a different axis.

Additional context

The motivating observation, from a Claude Code user: /gsd-review --codex proves GSD is already comfortable handing work to another vendor's CLI and reading the result back. The asymmetry is that the same vendor can vote on a plan but never execute one, purely because executor selection is bound to host identity rather than expressed per plan. Everything needed to close that gap already exists in the registry (orchestratorExec for codex, opencode, kimi-code), in the resolver, and in the review-lane spawn pattern. What is missing is permission to combine them.