Cross-runtime plan execution: per-plan `executor_runtime`, so a harness-worktree host can dispatch one plan to an orchestrator-worktree executor
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-featurebefore 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-worktreedispatch. That backend (Codex/OpenCode/Kimi process-spawn) has nosubagent_typeto swap, so v1 routes theAgent()-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:
---
# 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-isolationalready computes an exec descriptor throughresolveOrchestratorExec()insrc/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.ctsalready proves GSD is willing to spawn a foreign CLI from a Claude host, with acommand-existsprobe, 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 createplus therecord-dispatch-isolationsentinel already carry the isolation contract the guard hooks read.
Proposed CLI surface, matching the resolve-agent precedent from #3417:
$ 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:
- 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.
codexdeclarespromptFlag: null, so stdin is the channel. - 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_runtimeat validation time rather than silently skipping the gate. - 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 beexecute.models.<runtime>. - 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 ofexecute-phase.mdto respect the ADR-857 Phase 6 byte ceilingtests/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, parseexecutor_runtimeinto the plan-index JSON alongsideagent_hint(RawPlan.executorRuntime, emitted asexecutor_runtime,nullwhen unset), so dispatch does no per-plan file I/Ogsd-core/bin/gsd-tools.cjs, extend theresolve-dispatch-isolationroute to accept--plan/--executor-runtimeand resolve against the named registry entry instead of the host entry; keep the unconditional sentinel write as the sole persistence pathsrc/host-integration.cts, allowresolveOrchestratorExec()to be called with an arbitrary runtime's descriptor, plus acommand-existsprobe before returningoksrc/config.cts,gsd-core/bin/shared/config-schema.manifest.json,gsd-core/bin/shared/config-defaults.manifest.json, registerworkflow.cross_runtime_execution(default false) and theexecute.models.<runtime>key familygsd-core/workflows/execute-phase.md, one-line reference to the new fragment at the dispatch pointgsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md, document thatorchestrator-worktreeis now reachable from a harness hostgsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md, state precedence betweenagent_hintandexecutor_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 hostdocs/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,
nullwhen 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
- 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.
- 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.
- 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.
- 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: codexunder a Claude host, withworkflow.cross_runtime_execution=true, is executed by a spawnedcodex exec --cd <worktree>process against a GSD-created worktree -
gsd-tools resolve-dispatch-isolation --plan <id> --cwd-target <path> --jsonreportsruntime,executorRuntime,isolation,exec, andfallback - With
workflow.cross_runtime_executionunset or false, a plan declaringexecutor_runtimedispatches byte-identically to today and emits a one-line advisory - An unknown runtime id, a binary absent from PATH, or an
orchestratorExecdescriptor 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_runtimeis 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_hintandexecutor_runtimeon the same plan is a validation error, not a silent precedence rule - Plans without
executor_runtimedispatch 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.
orchestratorExecdescriptors already track this for native hosts, so the surface is shared rather than new, but acodex execflag 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-existspattern. - Weaker guarantees, and they must stay visible. A spawned process is outside the host's hook layer, so
PreToolUseguards 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
- 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.
- Set
GSD_RUNTIME=codexin 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. - Run
codex execby 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. - Extend
agent_hintto accept aruntime:nameform. Rejected as the primary surface because the two mechanisms differ in kind, not degree: one swaps asubagent_typeinside 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. - A capability overlay instead of core. Rejected for the reason #1689 documented: hooks cannot mutate
Agenttool 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. - 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_hintexecutor routing. This issue is the follow-up that PR's scope note names. - #2584, negotiated executor-worktree isolation, which introduced
dispatch.isolationand 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 --codexalready 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.
Source: open-gsd/gsd-core