diff --git a/.changeset/sharp-dogs-purr.md b/.changeset/sharp-dogs-purr.md new file mode 100644 index 000000000..c8f55e9bb --- /dev/null +++ b/.changeset/sharp-dogs-purr.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 2635 +--- +**Parallel execute-phase waves now run on Codex, OpenCode, Kimi and Kimi Code** — previously only Claude Code could execute a wave's independent plans concurrently, because worktree isolation relied on its harness-native `isolation="worktree"` primitive and every other runtime failed closed to sequential. Executor isolation is now a negotiated capability: runtimes whose harness isolates executors (Claude Code, Cursor) use their own flag, and runtimes exposing a headless exec with a working directory (Codex, OpenCode, Kimi, Kimi Code) get worktrees that GSD creates, validates and merges itself. Runtimes with no isolation primitive still run sequentially, and an unknown declaration always degrades to sequential rather than to an unisolated parallel run. (#2627) diff --git a/CONTEXT.md b/CONTEXT.md index 53f953f96..24cdbed87 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -113,7 +113,7 @@ Cross-seam principle (ADR-1411, epic #1411): context resolution — config loadi Diagnostic-output convention for the Resolution Provenance principle (ADR-1411 P3, #1416). Config-interpreting read verbs expose `Resolution { value, configured, reason, warnings }` (`src/resolution.cts`); agent-skills is the first adopter, where `value = { block, skills_count }` and `source`/`degraded` remain config-provenance extras outside the envelope. Other read verbs expose at least `warnings[]` (e.g. capability-state `{ runtimeConfigDir, capabilities, warnings? }`) without `configured`/`reason`, which are meaningful only for config-interpreting verbs. Mutation verbs expose `warnings[]` (advisory) PLUS `errors[]` (operation-not-applied), e.g. capability-writer `{ capabilities, warnings, errors }`. The shared seam across all shapes is `warnings: string[]`; a single generic `Resolution` across read+write verbs was rejected by the deletion test (`configured`/`reason` are meaningless for capability verbs; `errors[]` cannot fold into `warnings[]`) — ADR-1411 P3 amendment. Recurrence prevention is delivered by P4's CI guard (a configured input resolving empty must carry a `reason`), not by a shared envelope. A CI guard (`scripts/lint-resolution-provenance.cjs`, wired into `lint:ci`) enforces that every registered config-interpreting read verb keeps a `configured_empty`/`not_configured` contract test; the registry in that script is the registration point for future verbs (ADR-1411 P4 / #1417). ### Worktree Safety Policy Module -CJS Module owning worktree lifecycle safety policy for the GSD orchestration layer. Interface: `resolveWorktreeContext(cwd, deps) → WorktreeContext` (linked-worktree root mapping), `parseWorktreePorcelain(output) → WorktreeEntry[]` (porcelain parser, skips detached HEAD), `planWorktreePrune(repoRoot, opts, deps) → PrunePlan` (metadata-prune plan, never destructive by default), `executeWorktreePrunePlan(plan, deps) → PruneResult` (executes prune; degrades gracefully on git timeout), `listLinkedWorktreePaths(repoRoot, deps) → LinkedPathsResult`, `inspectWorktreeHealth(repoRoot, opts, deps) → HealthResult` (orphan + stale detection), `snapshotWorktreeInventory(repoRoot, opts, deps) → InventoryResult`, `planWorktreeWaveCleanup(repoRoot, manifest) → CleanupPlan` (manifest-scoped, fail-closed), `executeWorktreeWaveCleanupPlan(plan, deps) → CleanupResult`, `planWorktreeRecordAgent(manifestRaw, fields) → RecordAgentPlan` (write-strict per-agent manifest append; validates each field at write time via the same `normalizeCleanupManifestEntry` rules the reader enforces; fail-closed on a missing/garbled field or a duplicate `(worktree_path, branch)` the reader would dedup away), `cmdWorktreeRecordAgent(cwd, args, deps) → RecordAgentCmdResult` (thin deps-injectable IO wrapper for the `worktree record-agent` verb), `planWorktreeCreate(fields) → WorktreeCreatePlan` (write-strict `worktree create` planner — same missing-field-hint and `normalizeCleanupManifestEntry` validation as `planWorktreeRecordAgent`, pure/no-git), `executeWorktreeCreatePlan(plan, repoRoot, deps) → WorktreeCreateResult` (bounded `git rev-parse --verify` base check THEN `git worktree add -b `; fail-closed `base_unresolved`/`git_timeout`/`worktree_add_failed`; returns `cwd` — the working directory an executor spawn would use), `cmdWorktreeCreate(cwd, args, deps) → WorktreeCreateCmdResult` (CLI verb: plans, creates the worktree, then appends the manifest entry so it is immediately manageable by cleanup-wave/reap-orphans; dedupes by `(worktree_path, branch)`). #2584 ADR-1239 Codex-binding amendment, Phase 2: `worktree create` is the git-worktree-creation primitive for `dispatch.isolation: orchestrator-worktree` hosts — declared and testable but UNCONSUMED (no scheduler calls it yet; Phase 3 wires it). Source of truth: `gsd-core/bin/lib/worktree-safety.cjs`. Timeout path: all git subprocess calls are bounded; callers receive `ok:false, reason:'git_timed_out'` rather than a thrown exception. Test anchor: `tests/worktree-safety.test.cjs`. The `core.cjs` re-export spine was retired in epic #1267: this module absorbed the two thin compositional wrappers that squatted in Core — `resolveWorktreeRoot(cwd, deps)` (a projection over `resolveWorktreeContext`) and `pruneOrphanedWorktrees(...)` (sequences `planWorktreePrune` + `executeWorktreePrunePlan` with a timeout warning) — so callers reach this single worktree-lifecycle seam directly. `gitWorktreeInfoInternal` did NOT move here — worktree-info detection belongs to the Git Query Module. +CJS Module owning worktree lifecycle safety policy for the GSD orchestration layer. Interface: `resolveWorktreeContext(cwd, deps) → WorktreeContext` (linked-worktree root mapping), `parseWorktreePorcelain(output) → WorktreeEntry[]` (porcelain parser, skips detached HEAD), `planWorktreePrune(repoRoot, opts, deps) → PrunePlan` (metadata-prune plan, never destructive by default), `executeWorktreePrunePlan(plan, deps) → PruneResult` (executes prune; degrades gracefully on git timeout), `listLinkedWorktreePaths(repoRoot, deps) → LinkedPathsResult`, `inspectWorktreeHealth(repoRoot, opts, deps) → HealthResult` (orphan + stale detection), `snapshotWorktreeInventory(repoRoot, opts, deps) → InventoryResult`, `planWorktreeWaveCleanup(repoRoot, manifest) → CleanupPlan` (manifest-scoped, fail-closed), `executeWorktreeWaveCleanupPlan(plan, deps) → CleanupResult`, `planWorktreeRecordAgent(manifestRaw, fields) → RecordAgentPlan` (write-strict per-agent manifest append; validates each field at write time via the same `normalizeCleanupManifestEntry` rules the reader enforces; fail-closed on a missing/garbled field or a duplicate `(worktree_path, branch)` the reader would dedup away), `cmdWorktreeRecordAgent(cwd, args, deps) → RecordAgentCmdResult` (thin deps-injectable IO wrapper for the `worktree record-agent` verb), `planWorktreeCreate(fields) → WorktreeCreatePlan` (write-strict `worktree create` planner — same missing-field-hint and `normalizeCleanupManifestEntry` validation as `planWorktreeRecordAgent`, pure/no-git), `executeWorktreeCreatePlan(plan, repoRoot, deps) → WorktreeCreateResult` (bounded `git rev-parse --verify` base check THEN `git worktree add -b `; fail-closed `base_unresolved`/`git_timeout`/`worktree_add_failed`; returns `cwd` — the working directory an executor spawn would use), `cmdWorktreeCreate(cwd, args, deps) → WorktreeCreateCmdResult` (CLI verb: plans, creates the worktree, then appends the manifest entry so it is immediately manageable by cleanup-wave/reap-orphans; dedupes by `(worktree_path, branch)`). #2584 ADR-1239 Codex-binding amendment: `worktree create` is the git-worktree-creation primitive for `dispatch.isolation: orchestrator-worktree` hosts, CONSUMED since Phase 3 (#2627) by `execute-phase`'s wave scheduler. Accepts an optional `--root `: when supplied, `--path` must resolve inside it (`path_outside_root`), confining a worktree the orchestrator will spawn a process into; absent the flag, behavior is unchanged from Phase 2. Source of truth: `gsd-core/bin/lib/worktree-safety.cjs`. Timeout path: all git subprocess calls are bounded; callers receive `ok:false, reason:'git_timed_out'` rather than a thrown exception. Test anchor: `tests/worktree-safety.test.cjs`. The `core.cjs` re-export spine was retired in epic #1267: this module absorbed the two thin compositional wrappers that squatted in Core — `resolveWorktreeRoot(cwd, deps)` (a projection over `resolveWorktreeContext`) and `pruneOrphanedWorktrees(...)` (sequences `planWorktreePrune` + `executeWorktreePrunePlan` with a timeout warning) — so callers reach this single worktree-lifecycle seam directly. `gitWorktreeInfoInternal` did NOT move here — worktree-info detection belongs to the Git Query Module. ### Worktree Lifecycle Module Workflow contract seam covering agent worktree lifecycle orchestration rules. The `worktree_branch_check` block lives in one canonical fragment (`gsd-core/references/worktree-branch-check.md`) that `execute-phase.md`, `quick.md`, `diagnose-issues.md`, and `execute-plan.md` embed at dispatch. Key invariants: `worktree_branch_check` is **verify-only and fail-closed** — the orchestrator owns worktree lifecycle and base recovery, so the sub-agent holds no state-correction primitives; HEAD attachment verified via `git symbolic-ref`; positive allow-list `^worktree-agent-*` enforced; `git update-ref` on protected refs is prohibited; on base mismatch the sub-agent halts with `exit 42` and surfaces to the orchestrator (#48); the orchestrator runs a cwd-drift guard at `execute_waves` entry that resolves the worktree root and refuses drift into an agent worktree (#48); cleanup is manifest-scoped (`WAVE_WORKTREE_MANIFEST`) not global-discovery-based; worktree spawning is sequential (one `run_in_background` at a time to avoid `config.lock` contention). Test anchor: `tests/worktree.test.cjs`. @@ -128,7 +128,7 @@ Module owning bounded, never-throw git repository introspection — the single s Module owning runtime identity normalization at runtime-selection seams. Canonicalizes alias signals from env/config (`GSD_RUNTIME`, `.planning/config.json:runtime`) to supported runtime IDs so output emitters and query runtime gates stay consistent across naming variants (for example `codex-app`/`codex-cli` -> `codex`). Sources: `gsd-core/bin/lib/runtime-name-policy.cjs`, alias manifest `gsd-core/bin/shared/runtime-aliases.manifest.json`. ### Host-Integration Interface -Pure, additive, no-I/O Module owning the versioned, negotiated contract over the six host-integration interface points (command, dispatch, model, hooks, state, artifact) — ADR-1239 Phase A. Extends the ADR-1016 runtime descriptor with nine closed-vocabulary axes carried under `capability.json` `runtime.hostIntegration`: `embeddingMode` (`imperative|declarative`), `commandSurface` (`slash-file|slash-programmatic|slash-toml|palette|prose-only`), `dispatch` (`{namedDispatch,nested,maxDepth,background,backgroundDispatch,subagentToolkit,isolation}`), `modelMode` (`active|passive`), `hookBus` (`host|engine|none`), `stateIO` (`filesystem|sandboxed-storage|session-log-append`), `transport` (`mcp|native-extension`), `runtime` (`node|bun|sandboxed-web|python|go|rust|electron|other`), `effortSurface` (`argv|none` — how reasoning effort reaches the host; ADR-1239 amendment #2481, the first axis whose consumer is an invocation-time argument rather than an install-time artifact). `dispatch.isolation` (`harness-worktree|orchestrator-worktree|none` — how a host isolates concurrent same-wave executors; ADR-1239 Codex-binding amendment #2584; declared and negotiated but not yet consumed by any scheduler — Phase 1 of #2584). `resolveOrchestratorExec(orchestratorExec, cwd) → { ok:true, command, args, cwd } | { ok:false, reason }` (#2584 Phase 2, pure, no I/O — resolves the `runtime.orchestratorExec` descriptor field, a sibling of `runtime.hostBehaviors` in `capability.json` carrying `{command, args?, cwdFlag?}`, into the concrete argv/cwd a process-spawn primitive would use for a `dispatch.isolation: orchestrator-worktree` host; appends `[cwdFlag, cwd]` to `args` when `cwdFlag` is a non-empty string, e.g. codex `exec --cd `, opencode `run --dir `, kimi `--work-dir `; when `cwdFlag` is `null`/absent — kimi-code's process-cwd case — no flag is appended and `cwd` alone is returned for the caller to bind via the subprocess's own working-directory option; fail-closed `missing_command`/`invalid_cwd`/`invalid_args`/`invalid_cwd_flag`; declared and testable but UNCONSUMED — no scheduler spawns anything with it yet, Phase 3 wires it). Interface: `negotiateHostCapabilities(host, engine?) → { protocolVersion, effective, points, warnings }` enforcing the trust-boundary invariant `effective ⊆ host-declared ∩ engine-known` (never augment with an undeclared or unknown/future-`protocolVersion` value — fail-closed via the most-restrictive-known `SAFE_DEFAULTS`); `degradationFor(point, axes) → { level, fallback }` (a pure Full/Degraded/Absent ladder table, never throws); `profileOf(axes) → 'programmatic-cli'|'declarative-cli'|'ide'|null`; plus `PROTOCOL_VERSION` (integer, starts at 1 — distinct from the package `version`/`engines.gsd` semver), `HOST_INTEGRATION_AXES` (the frozen closed vocabulary, single source of truth), `PROFILE_BASELINES`, and `shouldFlattenDispatch(dispatch) → boolean` (ADR-1239 Phase B / #1708 — graduates the #853 rule: returns `true` = run the orchestrator inline UNLESS the host is documented to background a nesting-capable orchestrator (`background === true && backgroundDispatch === true`); fail-closed to inline; exposed to the plan/execute workflows via the `gsd_run query dispatch-should-flatten --raw` CLI, which replaced the former scattered `RUNTIME === 'codex'` prose check). The runtime-descriptor validator (`gsd-core/bin/lib/capability-validator.cjs` `validateRuntimeBody`) mirrors the closed vocabulary inline (exported as `_HOST_INTEGRATION_VOCAB`) and is kept in lock-step by the parity guard `tests/host-integration-validator-parity.test.cjs`. Orthogonal axes (resolved explicitly per ADR-1239 Phase A): `commandStyle` (GSD emission style, retained) vs `commandSurface` (host surface type); `hookEvents` dialect vs `hookBus` ownership (a host with `hooksSurface:none` may still be `hookBus:host` — e.g. opencode); `runtimeCompat` (feature→host) vs these negotiated runtime→engine axes. Phase A defined the interface; Phase B (#1679) wires it incrementally — `destSubpath` write-confinement (#1704) and the typed documentation-sourced #853 dispatch-flatten (#1708, the first consumer of a negotiated `dispatch` axis); adapters/MCP/host-bindings remain Phases C–E. Source of truth: `gsd-core/bin/lib/host-integration.cjs` (generated from `src/host-integration.cts`). See ADR-1239 and ADR-1016. +Pure, additive, no-I/O Module owning the versioned, negotiated contract over the six host-integration interface points (command, dispatch, model, hooks, state, artifact) — ADR-1239 Phase A. Extends the ADR-1016 runtime descriptor with nine closed-vocabulary axes carried under `capability.json` `runtime.hostIntegration`: `embeddingMode` (`imperative|declarative`), `commandSurface` (`slash-file|slash-programmatic|slash-toml|palette|prose-only`), `dispatch` (`{namedDispatch,nested,maxDepth,background,backgroundDispatch,subagentToolkit,isolation}`), `modelMode` (`active|passive`), `hookBus` (`host|engine|none`), `stateIO` (`filesystem|sandboxed-storage|session-log-append`), `transport` (`mcp|native-extension`), `runtime` (`node|bun|sandboxed-web|python|go|rust|electron|other`), `effortSurface` (`argv|none` — how reasoning effort reaches the host; ADR-1239 amendment #2481, the first axis whose consumer is an invocation-time argument rather than an install-time artifact). `dispatch.isolation` (`harness-worktree|orchestrator-worktree|none` — how a host isolates concurrent same-wave executors; ADR-1239 Codex-binding amendment #2584; negotiated per-sub-field and CONSUMED since Phase 3 (#2627) by `execute-phase`, which branches on the negotiated value rather than on a runtime id, via the `gsd_run query dispatch-isolation` CLI — the sibling of `dispatch-should-flatten`). `resolveOrchestratorExec(orchestratorExec, cwd, prompt?) → { ok:true, command, args, cwd } | { ok:false, reason }` (#2584 Phase 2, extended in Phase 3 (#2627); pure, no I/O — resolves the `runtime.orchestratorExec` descriptor field, a sibling of `runtime.hostBehaviors` in `capability.json` carrying `{command, args?, cwdFlag?, promptFlag?}`, into the concrete argv/cwd a process-spawn primitive would use for a `dispatch.isolation: orchestrator-worktree` host; appends `[cwdFlag, cwd]` to `args` when `cwdFlag` is a non-empty string, e.g. codex `exec --cd `, opencode `run --dir `, kimi `--work-dir `; when `cwdFlag` is `null`/absent — kimi-code's process-cwd case — no flag is appended and `cwd` alone is returned for the caller to bind via the subprocess's own working-directory option; when `prompt` is supplied it is appended last — behind `promptFlag` when that is a non-empty string (kimi/kimi-code `--prompt

`), otherwise positionally (codex `exec

`, opencode `run

`) — so prompt passing is descriptor data rather than a per-host scheduler branch; omitting `prompt` yields a resolution byte-identical to Phase 2's; fail-closed `missing_command`/`invalid_cwd`/`invalid_args`/`invalid_cwd_flag`/`invalid_prompt_flag`/`invalid_prompt`, plus `unsafe_leading_dash_prompt`/`unsafe_leading_dash_cwd` mirroring the git-argument leading-dash guard in the Worktree Safety Policy Module — a dash-leading positional is parsed by the spawned CLI as a flag. CONSUMED since Phase 3 (#2627)). The harness-side counterpart is the `runtime.harnessIsolationFlag` descriptor string (claude `isolation="worktree"`, cursor `--worktree`), which `dispatch.isolation: harness-worktree` hosts must declare so the scheduler passes a declared token instead of branching on a runtime id; a host declaring `harness-worktree` without it degrades to `none`. Interface: `negotiateHostCapabilities(host, engine?) → { protocolVersion, effective, points, warnings }` enforcing the trust-boundary invariant `effective ⊆ host-declared ∩ engine-known` (never augment with an undeclared or unknown/future-`protocolVersion` value — fail-closed via the most-restrictive-known `SAFE_DEFAULTS`); `degradationFor(point, axes) → { level, fallback }` (a pure Full/Degraded/Absent ladder table, never throws); `profileOf(axes) → 'programmatic-cli'|'declarative-cli'|'ide'|null`; plus `PROTOCOL_VERSION` (integer, starts at 1 — distinct from the package `version`/`engines.gsd` semver), `HOST_INTEGRATION_AXES` (the frozen closed vocabulary, single source of truth), `PROFILE_BASELINES`, and `shouldFlattenDispatch(dispatch) → boolean` (ADR-1239 Phase B / #1708 — graduates the #853 rule: returns `true` = run the orchestrator inline UNLESS the host is documented to background a nesting-capable orchestrator (`background === true && backgroundDispatch === true`); fail-closed to inline; exposed to the plan/execute workflows via the `gsd_run query dispatch-should-flatten --raw` CLI, which replaced the former scattered `RUNTIME === 'codex'` prose check). The runtime-descriptor validator (`gsd-core/bin/lib/capability-validator.cjs` `validateRuntimeBody`) mirrors the closed vocabulary inline (exported as `_HOST_INTEGRATION_VOCAB`) and is kept in lock-step by the parity guard `tests/host-integration-validator-parity.test.cjs`. Orthogonal axes (resolved explicitly per ADR-1239 Phase A): `commandStyle` (GSD emission style, retained) vs `commandSurface` (host surface type); `hookEvents` dialect vs `hookBus` ownership (a host with `hooksSurface:none` may still be `hookBus:host` — e.g. opencode); `runtimeCompat` (feature→host) vs these negotiated runtime→engine axes. Phase A defined the interface; Phase B (#1679) wires it incrementally — `destSubpath` write-confinement (#1704) and the typed documentation-sourced #853 dispatch-flatten (#1708, the first consumer of a negotiated `dispatch` axis); adapters/MCP/host-bindings remain Phases C–E. Source of truth: `gsd-core/bin/lib/host-integration.cjs` (generated from `src/host-integration.cts`). See ADR-1239 and ADR-1016. ### Statusline Host-integration hook (`hooks/gsd-statusline.js`) that renders the session status line: model name, context-window meter, workspace directory, and the GSD-state segment (`formatGsdState()` projecting `.planning/` STATE.md). Opt-in segments are gated by `.planning/config.json` keys (`statusline.show_last_command`, `statusline.context_position`, plus the approved `statusline.show_context_tokens` and `statusline.state_format`), each registered across `gsd-core/bin/shared/config-schema.manifest.json` + `src/config.cts` + the `loadConfig` whitelist + `docs/CONFIGURATION.md`. The compact GSD-state format consumes the canonical status vocabulary from `normalizeStateStatus()` (STATE.md Document Module) rather than a parallel keyword list. **Data-source boundary (ADR-2164):** the statusline sources only local, read-only data — it refines the stdin payload Claude Code already sends and may add a new *local* source (e.g. `git`), but does not read credentials or call external/network APIs for data; account/usage/platform-level state is out of scope. diff --git a/bin/install.js b/bin/install.js index 791942c50..144e8ae54 100755 --- a/bin/install.js +++ b/bin/install.js @@ -4013,10 +4013,14 @@ Typed mapping (agent_type-capable schema only): inherited, or unsupported values; do not invent one-off effort literals in workflow prose. - \`fork_context: false\` by default — GSD agents load their own context via \`\` blocks -- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct Codex mapping. - Codex \`spawn_agent\` does not create or bind a git worktree automatically. - Workflows that require this isolation must fail closed or use an explicit - manual worktree protocol before spawning (#3360). +- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping, + but Codex declares \`dispatch.isolation: orchestrator-worktree\` (#2584). Codex + \`spawn_agent\` still does not create or bind a git worktree; instead GSD itself + creates the worktree and process-spawns the executor into it with + \`codex exec --cd

\`, performing every git operation on the executor's behalf + (its \`workspace-write\` sandbox makes \`.git\` read-only). Workflows must therefore + never fabricate a manual worktree protocol — route through the negotiated + isolation adapter, which still fails closed for hosts declaring \`none\` (#3360). Generic-agent workaround (multi_agent_v1 schema — NO agent_type field): When only the generic \`multi_agent_v1\` schema is available, typed GSD agent dispatch diff --git a/capabilities/claude/capability.json b/capabilities/claude/capability.json index b686fa399..0699662c1 100644 --- a/capabilities/claude/capability.json +++ b/capabilities/claude/capability.json @@ -82,6 +82,7 @@ "runtime": "node", "effortSurface": "argv" }, + "harnessIsolationFlag": "isolation=\"worktree\"", "hostBehaviors": { "attributionSource": "settings-json-commit", "authorsCanonicalWorkflow": true, diff --git a/capabilities/codex/capability.json b/capabilities/codex/capability.json index d6b782ab3..eb353d3d0 100644 --- a/capabilities/codex/capability.json +++ b/capabilities/codex/capability.json @@ -78,7 +78,8 @@ "orchestratorExec": { "command": "codex", "args": ["exec"], - "cwdFlag": "--cd" + "cwdFlag": "--cd", + "promptFlag": null }, "hostBehaviors": { "reapplyCommand": "$gsd-update --reapply", diff --git a/capabilities/cursor/capability.json b/capabilities/cursor/capability.json index 0ff01134d..fcf174542 100644 --- a/capabilities/cursor/capability.json +++ b/capabilities/cursor/capability.json @@ -101,6 +101,7 @@ "runtime": "node", "effortSurface": "undocumented" }, + "harnessIsolationFlag": "--worktree", "hostBehaviors": { "reapplyCommand": "gsd-update --reapply (mention the skill name)", "frontmatterDialect": "cursor", diff --git a/capabilities/kimi-code/capability.json b/capabilities/kimi-code/capability.json index 3399003a9..95e16acfd 100644 --- a/capabilities/kimi-code/capability.json +++ b/capabilities/kimi-code/capability.json @@ -74,9 +74,10 @@ "runtime": "node" }, "orchestratorExec": { - "command": "kimi-code", + "command": "kimi", "args": [], - "cwdFlag": null + "cwdFlag": null, + "promptFlag": "--prompt" }, "hostBehaviors": { "reapplyCommand": "/skill:gsd-update --reapply", diff --git a/capabilities/kimi/capability.json b/capabilities/kimi/capability.json index c0f76b9ed..ba3f9e557 100644 --- a/capabilities/kimi/capability.json +++ b/capabilities/kimi/capability.json @@ -80,8 +80,9 @@ }, "orchestratorExec": { "command": "kimi", - "args": [], - "cwdFlag": "--work-dir" + "args": ["--print"], + "cwdFlag": "--work-dir", + "promptFlag": "--prompt" }, "hostBehaviors": { "reapplyCommand": "/skill:gsd-update --reapply", diff --git a/capabilities/opencode/capability.json b/capabilities/opencode/capability.json index 5997fae07..46c8d5f2f 100644 --- a/capabilities/opencode/capability.json +++ b/capabilities/opencode/capability.json @@ -90,7 +90,8 @@ "orchestratorExec": { "command": "opencode", "args": ["run"], - "cwdFlag": "--dir" + "cwdFlag": "--dir", + "promptFlag": null }, "hostBehaviors": { "reapplyCommand": "/gsd-update --reapply", diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 388e1bb9f..4d77d2056 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -306,7 +306,7 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin | `workflow.max_discuss_passes` | number | `3` | Maximum number of question rounds in discuss-phase before the workflow stops asking. Useful in headless/auto mode to prevent infinite discussion loops. | | `workflow.skip_discuss` | boolean | `false` | When `true`, `/gsd-autonomous` bypasses the discuss-phase entirely, writing minimal CONTEXT.md from the ROADMAP phase goal. Useful for projects where developer preferences are fully captured in PROJECT.md/REQUIREMENTS.md. Added in v1.28 | | `workflow.text_mode` | boolean | `false` | Replaces AskUserQuestion TUI menus with plain-text numbered lists. Required for Claude Code remote sessions (`/rc` mode) where TUI menus don't render. Can also be set per-session with `--text` flag on discuss-phase. Added in v1.28 | -| `workflow.use_worktrees` | boolean | `true` | When `false`, disables git worktree isolation for parallel execution. Users who prefer sequential execution or whose environment does not support worktrees can disable this. Added in v1.31. **Branch-divergence note:** when your branch has diverged from `origin/HEAD`, GSD auto-degrades to sequential and prints a warning. See [`worktree.baseRef`](#worktree-settings) to restore parallel execution on a diverged branch. **Non-Claude note:** git worktree isolation uses Claude Code's `isolation="worktree"` agent primitive, which no other runtime honors. On any non-Claude install (Codex, Cursor, Antigravity, Qwen, etc.) a runtime-neutral `.planning/config.json` resolves the runtime to that install's own id and defaults this key to `false`; forcing `use_worktrees: true` on a non-Claude install fails closed before any executor dispatch (#1515, #1521). | +| `workflow.use_worktrees` | boolean | `true` | When `false`, disables git worktree isolation for parallel execution. Users who prefer sequential execution or whose environment does not support worktrees can disable this. Added in v1.31. **Branch-divergence note:** when your branch has diverged from `origin/HEAD`, GSD auto-degrades to sequential and prints a warning. See [`worktree.baseRef`](#worktree-settings) to restore parallel execution on a diverged branch. **Per-runtime note:** whether this key can be honored depends on the runtime's declared `dispatch.isolation` capability, not on its name (#2584). Three cases: runtimes whose own harness isolates each executor (**Claude Code**, **Cursor**) run parallel worktrees natively; runtimes that expose a headless exec with an explicit working directory (**Codex**, **OpenCode**, **Kimi**, **Kimi Code**) get parallel worktrees that GSD itself creates, validates and merges; every other runtime declares no isolation primitive, and forcing `use_worktrees: true` there still fails closed before any executor dispatch (#1515, #1521). See [Executor isolation per runtime](#executor-isolation-per-runtime). | | `workflow.worktree_skip_hooks` | boolean | `false` | When `true`, executor agents in worktree mode pass `--no-verify` (skipping pre-commit hooks) and post-wave hook validation runs against the merged result instead. Opt-in escape hatch for projects whose hooks cannot run in agent worktrees. Default `false` runs hooks on every commit (#2924). | | `workflow.code_review` | boolean | `true` | Enable `/gsd-code-review` and `/gsd-code-review --fix` commands. When `false`, the commands exit with a configuration gate message. Added in v1.34 | | `workflow.code_review_depth` | string | `standard` | Default review depth for `/gsd-code-review`: `quick` (pattern-matching only), `standard` (per-file analysis), or `deep` (cross-file with import graphs). Can be overridden per-run with `--depth=`. Added in v1.34 | @@ -347,6 +347,24 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin |---------|------|---------|-------------| | `worktree.baseRef` | string | (unset) | Controls which ref the worktree-based parallel executor uses as the base when creating new phase/wave worktrees. When unset, the executor bases new worktrees on the repository default branch (`origin/HEAD`); if the current branch has diverged, execute-phase auto-degrades to sequential execution rather than halting (as of v1.4.0). Set to `"head"` to base new worktrees on the local `HEAD` instead — the appropriate choice when working on a branch that has diverged from the default branch, as it prevents the exit-42 base-mismatch halt and allows wave-based parallel execution to proceed normally. See [Fix the worktree base-mismatch (exit 42) error](how-to/fix-worktree-base-mismatch.md). | +### Executor isolation per runtime + +When `/gsd-execute-phase` runs a wave containing several independent plans, it can execute them concurrently — but only if the runtime can keep each executor isolated. Two executors sharing one checkout race on files, git state, hooks, and `.planning/`. Which runtimes can do this is a **declared capability** (`dispatch.isolation`), not a hardcoded list, so the scheduler behaves the same way for every host that declares the same value. + +| Isolation | Runtimes | What happens | +|---|---|---| +| `harness-worktree` | `claude`, `cursor` | The runtime's own harness creates and binds a git worktree per executor. GSD passes the host's isolation flag and runs no git itself. | +| `orchestrator-worktree` | `codex`, `opencode`, `kimi`, `kimi-code` | The runtime has no harness-native isolation, but exposes a headless exec that accepts a working directory. **GSD** creates the worktree, spawns each executor into it, then validates and merges the result. All git operations are performed by GSD, never by the sandboxed executor. | +| `none` | every other runtime | No isolation primitive — plans in a wave run sequentially. Setting `workflow.use_worktrees: true` here fails closed before any executor is dispatched. | + +You do not configure this directly: set `workflow.use_worktrees` and GSD negotiates the rest. `use_worktrees: false` forces sequential execution on **every** runtime, including the ones that support isolation. An unknown or undeclared isolation value always degrades to sequential — GSD never guesses its way into an unisolated parallel run. + +To see what your current runtime negotiated: + +```bash +gsd-tools query dispatch-isolation --json +``` + ## Code Quality Settings The `code_quality.*` namespace gates optional structural-analysis tooling that augments `/gsd-code-review`. Settings are additive: each tool is independently opt-in and off by default. diff --git a/gsd-core/bin/gsd-tools.cjs b/gsd-core/bin/gsd-tools.cjs index 441729baa..fffb2630e 100755 --- a/gsd-core/bin/gsd-tools.cjs +++ b/gsd-core/bin/gsd-tools.cjs @@ -1187,6 +1187,96 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load } } + function routeDispatchIsolation({ args, cwd, raw, error }) { + // #2584 Phase 3 (#2627): typed query exposing the negotiated + // `dispatch.isolation` to the execute-phase wave scheduler, so the + // scheduler branches on a declared capability instead of on a runtime id. + // Direct sibling of `dispatch-should-flatten` above, which exists (#1708 / + // #853) for exactly this reason — it replaced a `RUNTIME === 'codex'` + // prose rule. + // + // Resolves the current runtime (GSD_RUNTIME > config.runtime > 'claude'), + // reads registry.runtimes[id].runtime.hostIntegration.dispatch.isolation, + // and validates it against the closed vocabulary. + // + // Fail-closed: unknown runtime, missing/undeclared/out-of-vocabulary value, + // or any thrown error yields `none` — sequential execution, never an + // unsafe parallel path (ADR-1239, "Fail-closed"). + // + // Output: + // --raw → prints exactly harness-worktree | orchestrator-worktree | none + // --json → prints { runtime, isolation, exec } + // default → same as --raw + // + // `exec` is the resolved orchestratorExec argv/cwd shape, present only for + // `orchestrator-worktree`. It requires `--cwd-target` (the GSD-created + // worktree path) and optionally `--prompt`; without a target there is + // nothing to bind, so `exec` is null. + const VALID_ISOLATION = new Set(['harness-worktree', 'orchestrator-worktree', 'none']); + let isolation = 'none'; + let runtimeId = null; + let exec = null; + let harnessFlag = null; + try { + const { resolveRuntime } = require('./lib/runtime-slash.cjs'); + runtimeId = resolveRuntime(cwd); + + const registry = require('./lib/capability-registry.cjs'); + const runtimeEntry = registry.runtimes != null + ? registry.runtimes[runtimeId] + : null; + const declared = runtimeEntry?.runtime?.hostIntegration?.dispatch?.isolation ?? null; + if (typeof declared === 'string' && VALID_ISOLATION.has(declared)) { + isolation = declared; + } + + if (isolation === 'harness-worktree') { + const declaredFlag = runtimeEntry?.runtime?.harnessIsolationFlag ?? null; + // A host claiming harness isolation with no declared flag gives the + // scheduler nothing to pass — degrade to sequential rather than + // dispatch unisolated executors believing they are isolated. + if (typeof declaredFlag === 'string' && declaredFlag.length > 0) { + harnessFlag = declaredFlag; + } else { + isolation = 'none'; + } + } + + if (isolation === 'orchestrator-worktree') { + const cwdIdx = args.indexOf('--cwd-target'); + const cwdTarget = cwdIdx !== -1 ? args[cwdIdx + 1] : ''; + if (cwdTarget) { + const promptIdx = args.indexOf('--prompt'); + const promptArg = promptIdx !== -1 ? args[promptIdx + 1] : undefined; + const hostIntegration = require('./lib/host-integration.cjs'); + const resolution = hostIntegration.resolveOrchestratorExec( + runtimeEntry?.runtime?.orchestratorExec, + cwdTarget, + promptArg, + ); + // A host declaring orchestrator-worktree whose exec descriptor does + // not resolve cannot be spawned — degrade to sequential rather than + // hand the scheduler an unusable command. + if (resolution.ok) { + exec = { command: resolution.command, args: resolution.args, cwd: resolution.cwd }; + } else { + isolation = 'none'; + } + } + } + } catch { + isolation = 'none'; + exec = null; + harnessFlag = null; + } + + if (args.indexOf('--json') !== -1) { + output({ runtime: runtimeId, isolation, exec, harnessFlag }, raw); + } else { + process.stdout.write(isolation); + } + } + function routeResolveDispatchType({ args, cwd, raw, error }) { // #2508 Phase 4 Option A: resolve a requested GSD subagent name to the // type an Agent() call should use on the current runtime. On @@ -2121,6 +2211,7 @@ const HOST_COMMAND_ROUTERS = { 'quick-tasks-append': routeQuickTasksAppend, 'normalize-test-command': routeNormalizeTestCommand, 'dispatch-should-flatten': routeDispatchShouldFlatten, + 'dispatch-isolation': routeDispatchIsolation, 'resolve-dispatch-type': routeResolveDispatchType, 'agent-skills': routeAgentSkills, 'skill-manifest': routeSkillManifest, diff --git a/gsd-core/bin/lib/capability-registry.cjs b/gsd-core/bin/lib/capability-registry.cjs index 41557688d..65b7cfc8d 100644 --- a/gsd-core/bin/lib/capability-registry.cjs +++ b/gsd-core/bin/lib/capability-registry.cjs @@ -517,6 +517,7 @@ const capabilities = { "runtime": "node", "effortSurface": "argv" }, + "harnessIsolationFlag": "isolation=\"worktree\"", "hostBehaviors": { "attributionSource": "settings-json-commit", "authorsCanonicalWorkflow": true, @@ -955,7 +956,8 @@ const capabilities = { "args": [ "exec" ], - "cwdFlag": "--cd" + "cwdFlag": "--cd", + "promptFlag": null }, "hostBehaviors": { "reapplyCommand": "$gsd-update --reapply", @@ -1165,6 +1167,7 @@ const capabilities = { "runtime": "node", "effortSurface": "undocumented" }, + "harnessIsolationFlag": "--worktree", "hostBehaviors": { "reapplyCommand": "gsd-update --reapply (mention the skill name)", "frontmatterDialect": "cursor", @@ -1762,8 +1765,11 @@ const capabilities = { }, "orchestratorExec": { "command": "kimi", - "args": [], - "cwdFlag": "--work-dir" + "args": [ + "--print" + ], + "cwdFlag": "--work-dir", + "promptFlag": "--prompt" }, "hostBehaviors": { "reapplyCommand": "/skill:gsd-update --reapply", @@ -1851,9 +1857,10 @@ const capabilities = { "runtime": "node" }, "orchestratorExec": { - "command": "kimi-code", + "command": "kimi", "args": [], - "cwdFlag": null + "cwdFlag": null, + "promptFlag": "--prompt" }, "hostBehaviors": { "reapplyCommand": "/skill:gsd-update --reapply", @@ -2184,7 +2191,8 @@ const capabilities = { "args": [ "run" ], - "cwdFlag": "--dir" + "cwdFlag": "--dir", + "promptFlag": null }, "hostBehaviors": { "reapplyCommand": "/gsd-update --reapply", @@ -4414,6 +4422,7 @@ const runtimes = { "runtime": "node", "effortSurface": "argv" }, + "harnessIsolationFlag": "isolation=\"worktree\"", "hostBehaviors": { "attributionSource": "settings-json-commit", "authorsCanonicalWorkflow": true, @@ -4703,7 +4712,8 @@ const runtimes = { "args": [ "exec" ], - "cwdFlag": "--cd" + "cwdFlag": "--cd", + "promptFlag": null }, "hostBehaviors": { "reapplyCommand": "$gsd-update --reapply", @@ -4913,6 +4923,7 @@ const runtimes = { "runtime": "node", "effortSurface": "undocumented" }, + "harnessIsolationFlag": "--worktree", "hostBehaviors": { "reapplyCommand": "gsd-update --reapply (mention the skill name)", "frontmatterDialect": "cursor", @@ -5215,8 +5226,11 @@ const runtimes = { }, "orchestratorExec": { "command": "kimi", - "args": [], - "cwdFlag": "--work-dir" + "args": [ + "--print" + ], + "cwdFlag": "--work-dir", + "promptFlag": "--prompt" }, "hostBehaviors": { "reapplyCommand": "/skill:gsd-update --reapply", @@ -5304,9 +5318,10 @@ const runtimes = { "runtime": "node" }, "orchestratorExec": { - "command": "kimi-code", + "command": "kimi", "args": [], - "cwdFlag": null + "cwdFlag": null, + "promptFlag": "--prompt" }, "hostBehaviors": { "reapplyCommand": "/skill:gsd-update --reapply", @@ -5413,7 +5428,8 @@ const runtimes = { "args": [ "run" ], - "cwdFlag": "--dir" + "cwdFlag": "--dir", + "promptFlag": null }, "hostBehaviors": { "reapplyCommand": "/gsd-update --reapply", diff --git a/gsd-core/bin/lib/capability-validator.cjs b/gsd-core/bin/lib/capability-validator.cjs index a66667661..2593966ca 100644 --- a/gsd-core/bin/lib/capability-validator.cjs +++ b/gsd-core/bin/lib/capability-validator.cjs @@ -1366,9 +1366,31 @@ function validateRuntimeBody(cap) { 'runtime.orchestratorExec.cwdFlag must be a string or null (got: ' + JSON.stringify(oe.cwdFlag) + ')', ); } + + // promptFlag — optional; string or null (#2627, Phase 3). `null`/absent + // means the host takes the executor prompt positionally (codex, opencode); + // a string names the flag that carries it (kimi/kimi-code: --prompt). + if (oe.promptFlag !== undefined && oe.promptFlag !== null && typeof oe.promptFlag !== 'string') { + errors.push( + 'runtime.orchestratorExec.promptFlag must be a string or null (got: ' + JSON.stringify(oe.promptFlag) + ')', + ); + } } } + // harnessIsolationFlag — ADR-1239 Codex-binding amendment (#2584), Phase 3 + // (#2627). OPTIONAL top-level field: the counterpart of orchestratorExec for + // `dispatch.isolation: 'harness-worktree'` hosts, naming the host's OWN + // isolation flag that GSD passes on dispatch (claude: isolation="worktree"; + // cursor: --worktree). Descriptor data so the scheduler passes a declared + // token instead of branching on a runtime id (ADR-1239: "the per-host + // dispatch invocation ... is descriptor data, not a scheduler branch"). + if (r.harnessIsolationFlag !== undefined && (typeof r.harnessIsolationFlag !== 'string' || r.harnessIsolationFlag.length === 0)) { + errors.push( + 'runtime.harnessIsolationFlag must be a non-empty string (got: ' + JSON.stringify(r.harnessIsolationFlag) + ')', + ); + } + // GATE A: installSurface ↔ hooksSurface consistency (DEFECT.GENERATIVE-FIX) // Only check if both fields are valid strings (individual field validators above report type errors). if (typeof r.installSurface === 'string' && typeof r.hooksSurface === 'string') { diff --git a/gsd-core/references/planning-config.md b/gsd-core/references/planning-config.md index 23de7a258..ac509c862 100644 --- a/gsd-core/references/planning-config.md +++ b/gsd-core/references/planning-config.md @@ -395,7 +395,7 @@ Several config fields affect each other or trigger special behavior: 8. **`sub_repos` auto-sync** -- On every config load, GSD scans for child directories with `.git` and updates the `sub_repos` array if the filesystem has changed. Legacy `multiRepo: true` is automatically migrated to a detected `sub_repos` array. -9. **`workflow.use_worktrees` and branch divergence** -- When `use_worktrees` is `true` (default), executor worktrees are forked from `origin/HEAD` by the Claude Code harness. If your current branch has commits that `origin/HEAD` does not (for example an unmerged milestone or feature branch), GSD automatically degrades to sequential execution for that run and prints a one-line `⚠ Worktree base mismatch` warning. To restore parallel execution permanently, set `worktree.baseRef:"head"` in `.claude/settings.local.json` (run `node gsd-tools.cjs worktree set-baseref`). This makes the harness fork worktrees from the live HEAD instead of `origin/HEAD`. Both fresh installs and upgrades of GSD Core set this automatically (no-clobber) when `use_worktrees` is enabled; you can also run the command manually at any time. Setting `workflow.use_worktrees: false` is the alternative if worktrees are not needed at all. +9. **`workflow.use_worktrees` and branch divergence** -- When `use_worktrees` is `true` (default), executor worktrees are forked from `origin/HEAD` -- by the host's own harness on `dispatch.isolation: harness-worktree` runtimes (Claude Code, Cursor), or by GSD itself on `orchestrator-worktree` runtimes (Codex, OpenCode, Kimi, Kimi Code). The divergence behavior below is identical either way, because the fork base is a property of the repository rather than of whoever creates the worktree. If your current branch has commits that `origin/HEAD` does not (for example an unmerged milestone or feature branch), GSD automatically degrades to sequential execution for that run and prints a one-line `⚠ Worktree base mismatch` warning. To restore parallel execution permanently, set `worktree.baseRef:"head"` in `.claude/settings.local.json` (run `node gsd-tools.cjs worktree set-baseref`). This makes the harness fork worktrees from the live HEAD instead of `origin/HEAD`. Both fresh installs and upgrades of GSD Core set this automatically (no-clobber) when `use_worktrees` is enabled; you can also run the command manually at any time. Setting `workflow.use_worktrees: false` is the alternative if worktrees are not needed at all. --- diff --git a/gsd-core/workflows/execute-phase.md b/gsd-core/workflows/execute-phase.md index 9548724e6..06d564e36 100644 --- a/gsd-core/workflows/execute-phase.md +++ b/gsd-core/workflows/execute-phase.md @@ -100,24 +100,14 @@ USE_WORKTREES=$(gsd_run query config-get workflow.use_worktrees --raw 2>/dev/nul EXECUTOR_STALL_INTERVAL_MINUTES=$(gsd_run query config-get executor.stall_detect_interval_minutes 2>/dev/null || echo "5") EXECUTOR_STALL_THRESHOLD_MINUTES=$(gsd_run query config-get executor.stall_threshold_minutes 2>/dev/null || echo "10") -if [ "$RUNTIME" != "claude" ] && [ "$USE_WORKTREES" != "false" ]; then - echo "FATAL: git worktree isolation (isolation=\"worktree\") is unsupported on runtime '$RUNTIME' — it would run executor agents unisolated against the main checkout. Set workflow.use_worktrees=false." >&2 - exit 1 -fi -# Sweep orphaned locked worktrees from prior crashed sessions before spawning executors (#3707). -[ "$USE_WORKTREES" != "false" ] && gsd_run query worktree.reap-orphans 2>/dev/null || true -# Auto-degrade to sequential if HEAD has diverged from the worktree fork base (#683). -# Only applies to Claude Code (isolation="worktree" is Claude-Code-specific). -if [ "$RUNTIME" = "claude" ] && [ "$USE_WORKTREES" != "false" ]; then - _SHOULD_DEGRADE=$(gsd_run query worktree.base-check --pick shouldDegrade 2>/dev/null || true) - if [ "$_SHOULD_DEGRADE" = "true" ]; then - _DEGRADE_MSG=$(gsd_run query worktree.base-check --pick message 2>/dev/null || true) - [ -n "$_DEGRADE_MSG" ] && printf '%s\n' "$_DEGRADE_MSG" >&2 - USE_WORKTREES=false - fi -fi +# Resolve ISOLATION + apply its guards: read and execute the "Resolve ISOLATION" +# section of execute-phase/steps/executor-isolation-dispatch.md. It sets +# ISOLATION (harness-worktree|orchestrator-worktree|none), forces none when +# USE_WORKTREES=false, fails closed when a host has no primitive, sweeps orphans, +# and applies the #683 fork-base auto-degrade. ``` -`isolation="worktree"` is a Claude-Code-specific agent primitive; no other runtime can honor it (Codex maps subagents to `spawn_agent`, others prohibit or omit worktree binding). Failing closed prevents main-checkout edits while the workflow believes agents are isolated. + +`ISOLATION` — not `RUNTIME` — is the ONLY fan-out branch point; **never add a `RUNTIME = "codex"` test here.** Per-host dispatch detail lives in `execute-phase/steps/executor-isolation-dispatch.md` (read from step 3). If the project uses git submodules, worktree isolation is unsafe **only when a plan touches a submodule path** — the executor commit protocol cannot correctly handle submodule commits inside isolated worktrees. Compute submodule paths once and intersect them per-plan with the plan's declared `files_modified` frontmatter. @@ -133,9 +123,9 @@ fi `SUBMODULE_PATHS` is exported to the `execute_waves` step, where the per-plan decision happens (see "Per-plan worktree decision" sub-step inside `execute_waves`). The decision is per-plan because different plans in the same wave can touch different files — only plans whose paths intersect a submodule must drop worktree isolation; plans nowhere near a submodule keep parallel isolation. -When `USE_WORKTREES` (project-level) is `false`, all executor agents run without `isolation="worktree"` — they execute sequentially on the main working tree instead of in parallel worktrees. The per-plan decision below has no effect when worktrees are project-disabled. +When `USE_WORKTREES` is `false`, `ISOLATION` is forced to `none`: executors run sequentially on the main working tree. The per-plan decision below has no effect when worktrees are project-disabled. -`USE_WORKTREES` is also automatically set to `false` for the duration of a run when `worktree base-check` detects that the orchestrator HEAD has diverged from the worktree fork base (the #683 condition — e.g. an unmerged milestone or feature branch). This check runs only when `RUNTIME=claude` because `isolation="worktree"` is a Claude Code-specific feature; other runtimes do not use it. The auto-degrade prints a one-line warning to stderr and falls through to the sequential path so executors do not hit the exit-42 worktree-branch-check halt. To restore parallel worktree execution, set `worktree.baseRef:"head"` in `.claude/settings.local.json` (or run `gsd-tools worktree set-baseref`) — this makes the fork base track the live HEAD instead of a fixed remote ref. The `worktree-branch-check` exit-42 guard inside each executor remains in place as a backstop. +`USE_WORKTREES` and `ISOLATION` are also reset for the run when `worktree base-check` detects the orchestrator HEAD has diverged from the worktree fork base (#683 — e.g. an unmerged milestone branch). This runs for **any** isolated run, not only Claude: fork-base divergence is a property of the repository, so it degrades a GSD-created worktree exactly as a harness-created one. The auto-degrade prints a one-line warning to stderr and falls through to the sequential path so executors do not hit the exit-42 worktree-branch-check halt. To restore parallel worktree execution, set `worktree.baseRef:"head"` in `.claude/settings.local.json` (or run `gsd-tools worktree set-baseref`) — this makes the fork base track the live HEAD instead of a fixed remote ref. The `worktree-branch-check` exit-42 guard inside each executor remains in place as a backstop. Read context window size for adaptive prompt enrichment: @@ -595,6 +585,8 @@ increases monotonically across waves. `{status}` is `complete` (success), fi ``` + **Isolation model.** The block below is the **`harness-worktree`** path. For `orchestrator-worktree` use the dispatch below it; for `none` use sequential mode. Both are detailed in `execute-phase/steps/executor-isolation-dispatch.md`. + **Sequential dispatch for parallel execution (waves with 2+ agents):** Dispatch each `Agent()` call **one at a time with `run_in_background: true`**. Do NOT send all Agent calls in a single message: simultaneous `git worktree add` calls race @@ -613,7 +605,10 @@ increases monotonically across waves. `{status}` is `complete` (success), # When executor_model is "inherit", omit this parameter entirely so # Claude Code inherits the orchestrator model automatically. model="{executor_model}", # omit this line when executor_model == "inherit" - isolation="worktree", + # The host's OWN declared isolation flag (`harnessFlag` from + # `dispatch-isolation --json`; see the isolation-dispatch fragment). + # Emit the declared token — do NOT hardcode a runtime's flag. + {harnessFlag}, prompt=" Execute plan {plan_number} of phase {phase_number}-{phase_name}. @@ -697,6 +692,8 @@ increases monotonically across waves. `{status}` is `complete` (success), > **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above to spawn executor agent(s), stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available. + **Orchestrator-managed worktree dispatch** (`ISOLATION=orchestrator-worktree`): read and execute `execute-phase/steps/executor-isolation-dispatch.md`. GSD creates each worktree (`worktree create`) and spawns the executor into it; the orchestrator performs every git operation. Merge-back and cleanup are the existing manifest-scoped gauntlet, unchanged. + **Sequential mode** (`USE_WORKTREES_FOR_PLAN` is `false` — either project-level `USE_WORKTREES=false`, or per-plan submodule intersection forced it false in step 2.5): Omit `isolation="worktree"` from the Agent call. Replace the `` block with: diff --git a/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md b/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md new file mode 100644 index 000000000..c0fe7f700 --- /dev/null +++ b/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md @@ -0,0 +1,160 @@ +# Executor isolation dispatch (ADR-1239 / #2584 Phase 3) + +Read and follow this fragment from `execute-phase.md` step 3 when dispatching a wave. +It owns the per-host dispatch detail so the host workflow stays inside its +ADR-857 Phase 6 byte budget (#1168) — the host step keeps only the `ISOLATION` +resolution and its fail-closed guard. + +## Resolve ISOLATION + +Run this in the config-gate step, right after `RUNTIME`/`USE_WORKTREES` are read. + +```bash +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +# Isolation is a NEGOTIATED CAPABILITY, not a runtime id (#2584). Fail-closed to none. +ISOLATION=$(gsd_run query dispatch-isolation --raw 2>/dev/null || echo "none") +case "$ISOLATION" in + harness-worktree|orchestrator-worktree|none) ;; + *) ISOLATION=none ;; +esac + +# Project-level opt-out wins on every host; a host with no primitive fails closed. +[ "$USE_WORKTREES" = "false" ] && ISOLATION=none +if [ "$ISOLATION" = "none" ] && [ "$USE_WORKTREES" != "false" ]; then + echo "FATAL: runtime '$RUNTIME' declares no executor-isolation primitive (dispatch.isolation=none) — executors would run unisolated against the main checkout. Set workflow.use_worktrees=false." >&2 + exit 1 +fi + +# Sweep orphaned locked worktrees from prior crashed sessions (#3707). +[ "$ISOLATION" != "none" ] && gsd_run query worktree.reap-orphans 2>/dev/null || true +# Auto-degrade if HEAD diverged from the fork base (#683) — both isolation models. +if [ "$ISOLATION" != "none" ]; then + _SHOULD_DEGRADE=$(gsd_run query worktree.base-check --pick shouldDegrade 2>/dev/null || true) + if [ "$_SHOULD_DEGRADE" = "true" ]; then + _DEGRADE_MSG=$(gsd_run query worktree.base-check --pick message 2>/dev/null || true) + [ -n "$_DEGRADE_MSG" ] && printf '%s\n' "$_DEGRADE_MSG" >&2 + USE_WORKTREES=false + ISOLATION=none + fi +fi +``` + +`ISOLATION` — not `RUNTIME` — selects how the wave fans out. These three values are the only +branch points; **never add a `RUNTIME = "codex"` test to the scheduler.** The per-host +invocation detail is descriptor data, surfaced by `dispatch-isolation --json` as +`harnessFlag` / `exec`. + +| `ISOLATION` | Fan-out | What the scheduler does | +|---|---|---| +| `harness-worktree` | host-driven | Pass the host's own declared isolation flag (`harnessFlag`) on each executor dispatch and let the harness create + bind the worktree. GSD runs no git. | +| `orchestrator-worktree` | GSD-driven | GSD creates the worktree (`worktree create`), then process-spawns the executor bound to it via the resolved `exec` argv/cwd. GSD performs all git operations. | +| `none` | none | Plans run inline, sequentially (unchanged). | + +Fail-closed is the invariant: an undeclared, unknown, or unresolvable isolation declaration +degrades to `none`, never to an unsafe parallel path. A `harness-worktree` host with no +declared flag, and an `orchestrator-worktree` host whose exec descriptor does not resolve, +both degrade to `none` rather than dispatching executors that only believe they are isolated. + +## harness-worktree — pass the host flag + +Read the flag once before dispatching; it is descriptor data, never hardcoded per runtime: + +```bash +HARNESS_FLAG=$(gsd_run query dispatch-isolation --json 2>/dev/null \ + | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const j=JSON.parse(s);process.stdout.write(j&&j.harnessFlag?j.harnessFlag:"")}catch{process.stdout.write("")}})') +[ -n "$HARNESS_FLAG" ] || { echo "FATAL: runtime declares dispatch.isolation=harness-worktree but no harnessIsolationFlag — refusing to dispatch executors that would believe they are isolated." >&2; exit 1; } +``` + +Substitute `$HARNESS_FLAG`'s value for the `{harnessFlag}` placeholder in the `Agent()` dispatch +in `execute-phase.md` step 3 (on Claude Code it is literally `isolation="worktree"`). + +## orchestrator-worktree — GSD creates the worktree and spawns the executor + +The host has no harness-native isolation primitive, so **GSD** creates each worktree and process-spawns the executor into it. Fan-out is OS-level (N processes), not the host's subagent tool. Per the Codex `workspace-write` sandbox constraint, **the orchestrator performs every git operation** — create, merge, cleanup; the spawned executor only edits files and commits inside its own worktree. + +Run the loop below once per runnable plan in the wave, **one plan at a time** (`git worktree add` races on `.git/config.lock`). + +**Before running the bash block, substitute the plan's identifiers into it** exactly as you do for the `Agent()` prompt on the harness path: replace `{plan_number}` and `{phase_number}` with this plan's values. They are template placeholders, not shell variables. `$ORCH_ROOT` and `$EXPECTED_BASE` are real shell variables, already assigned earlier in this step; `$WAVE_WORKTREE_MANIFEST` was initialized above. + +First build the executor prompt. It is the **same prompt text the harness path's `Agent()` call uses**, with the harness-only framing removed — drop the `` build-time embed note and the `` harness block, keep ``, the execution context, and `` verbatim. Assign it to a shell variable so it can be passed as one argument: + +```bash +# Compose the executor prompt for THIS plan. Single-quoted multi-line +# assignment (NOT a heredoc): these blocks are indented inside the workflow, +# and a heredoc terminator must sit at column 0 — `<<-` strips only tabs, not +# the leading spaces, so a heredoc here would never terminate. Single quotes +# also stop the shell expanding anything in the prompt body. +EXECUTOR_PROMPT=' +Execute plan {plan_number} of phase {phase_number}-{phase_name}. +Commit each task atomically. Create SUMMARY.md. +Do NOT update STATE.md or ROADMAP.md — the orchestrator owns those writes after all worktree agents in the wave complete. + + + +You are running as an executor in a git worktree GSD created for you. Your +working directory IS that worktree. Do not cd elsewhere, and do not run any +git command that targets the main checkout. Use normal git commits WITH hooks. +Do NOT use --no-verify. +REQUIRED ORDER: Write SUMMARY.md, commit, then any narration. + + + +- [ ] All tasks executed +- [ ] Each task committed individually +- [ ] SUMMARY.md created AND committed in the plan directory +' +[ -n "$EXECUTOR_PROMPT" ] || { echo "FATAL: executor prompt is empty for plan {plan_number}." >&2; exit 1; } +``` + +The prompt body must contain no single-quote character, since the assignment above is single-quoted; keep apostrophes out of it when editing. + +Then create the worktree and resolve the spawn: + +```bash +# 1. Create the worktree. Bounded, manifest-recorded, fail-closed, and +# root-confined by the verb itself — never hand-roll `git worktree add`. +AGENT_ID="agent-p{plan_number}-$(date -u +%s)" +WT_BRANCH="worktree-${AGENT_ID}" +WT_PATH="${ORCH_ROOT}/.claude/worktrees/${AGENT_ID}" +CREATE_JSON=$(gsd_run query worktree.create \ + --manifest "$WAVE_WORKTREE_MANIFEST" \ + --agent-id "$AGENT_ID" \ + --path "$WT_PATH" \ + --branch "$WT_BRANCH" \ + --base "$EXPECTED_BASE" \ + --root "$ORCH_ROOT" 2>&1) || { + echo "FATAL: worktree create failed for plan {plan_number}: $CREATE_JSON" >&2 + exit 1 + } + +# 2. Resolve the host's headless-exec argv for that worktree. Descriptor +# data — command, args, cwd flag and prompt flag all come from the +# capability descriptor, so no host is named here. +EXEC_JSON=$(gsd_run query dispatch-isolation --json \ + --cwd-target "$WT_PATH" \ + --prompt "$EXECUTOR_PROMPT") + +# 3. MANDATORY fail-closed check. `dispatch-isolation` degrades to +# isolation:"none" / exec:null rather than exiting non-zero, so the +# command substitution above ALWAYS "succeeds" — the exit code proves +# nothing. A worktree already exists at this point (step 1 is a real side +# effect), so an unusable exec must NOT be spawned and must NOT be left +# behind as an orphan: tear it down through the manifest-scoped cleanup +# and halt rather than silently running the wave unisolated. +EXEC_OK=$(printf '%s' "$EXEC_JSON" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const j=JSON.parse(s);process.stdout.write(j&&j.isolation==="orchestrator-worktree"&&j.exec&&j.exec.command?"true":"false")}catch{process.stdout.write("false")}})') +if [ "$EXEC_OK" != "true" ]; then + echo "FATAL: could not resolve an orchestrator-exec invocation for plan {plan_number} after its worktree was created. The wave is halted rather than run unisolated. Retained for inspection: $WT_PATH (branch $WT_BRANCH, recorded in $WAVE_WORKTREE_MANIFEST) — run 'gsd_run query worktree.cleanup-wave --manifest \"$WAVE_WORKTREE_MANIFEST\"' to merge/clean it." >&2 + exit 1 +fi +``` + +`worktree create` records the entry in `$WAVE_WORKTREE_MANIFEST` itself, so **do not** call `worktree.record-agent` for these plans — that verb is the harness-path counterpart, used because the harness creates the worktree behind GSD's back. Double-recording is deduped by path+branch, but the create verb is the single writer here. + +Spawn `EXEC_JSON`'s `command` + `args` as a background process with its working directory set to `EXEC_JSON.cwd`. The `cwd` is returned for **every** host, including those whose descriptor has no cwd flag (`cwdFlag: null`) and therefore bind through the process's own working directory — always set it, never assume the flag did the job. Wait for all spawned executors in the wave before merging. + +The executor never touches `STATE.md`/`ROADMAP.md`, and that guard needs no new code — `execute-plan` auto-detects worktree mode via the `IS_WORKTREE` (`.git`-is-a-file) primitive, which a GSD-created worktree trips identically to a harness-created one. + +Merge-back, validation, and cleanup are the **existing** gauntlet, unchanged: the serialized `worktree.cleanup-wave` merge loop that stops the wave and retains the worktree on conflict, and manifest-only cleanup (never glob-inferred). Because the manifest shape is identical, the orchestrator path reuses it verbatim. + +> **Declared-scope conformance (#2596):** ADR-1239 specifies that *both* isolation adapters route their merge through a check that each plan branch's committed diff stayed inside its declared `files_modified` scope. That check does not exist yet for either adapter (it is tracked as #2596). When it lands it must be wired into this path **and** the harness path together. + diff --git a/src/host-integration.cts b/src/host-integration.cts index 71d28b725..d715a7aa5 100644 --- a/src/host-integration.cts +++ b/src/host-integration.cts @@ -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 ""` and `opencode run ""` take it + * positionally; `kimi --print --prompt "

"` and Kimi Code's `kimi -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 }; } diff --git a/src/runtime-artifact-conversion.cts b/src/runtime-artifact-conversion.cts index 9c126fee3..4a5cf297a 100644 --- a/src/runtime-artifact-conversion.cts +++ b/src/runtime-artifact-conversion.cts @@ -1530,10 +1530,14 @@ Typed mapping (agent_type-capable schema only): inherited, or unsupported values; do not invent one-off effort literals in workflow prose. - \`fork_context: false\` by default — GSD agents load their own context via \`\` blocks -- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct Codex mapping. - Codex \`spawn_agent\` does not create or bind a git worktree automatically. - Workflows that require this isolation must fail closed or use an explicit - manual worktree protocol before spawning (#3360). +- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping, + but Codex declares \`dispatch.isolation: orchestrator-worktree\` (#2584). Codex + \`spawn_agent\` still does not create or bind a git worktree; instead GSD itself + creates the worktree and process-spawns the executor into it with + \`codex exec --cd

\`, performing every git operation on the executor's behalf + (its \`workspace-write\` sandbox makes \`.git\` read-only). Workflows must therefore + never fabricate a manual worktree protocol — route through the negotiated + isolation adapter, which still fails closed for hosts declaring \`none\` (#3360). Generic-agent workaround (multi_agent_v1 schema — NO agent_type field): When only the generic \`multi_agent_v1\` schema is available, typed GSD agent dispatch diff --git a/src/worktree-safety.cts b/src/worktree-safety.cts index df3b22837..bc754180a 100644 --- a/src/worktree-safety.cts +++ b/src/worktree-safety.cts @@ -1337,7 +1337,7 @@ function cmdWorktreeCreate(cwd: string, args: string[] = [], deps: RecordAgentCm const manifestPath = flag('--manifest'); if (!manifestPath) { - writeErr('Usage: worktree create --manifest --agent-id --path --branch --base \n'); + writeErr('Usage: worktree create --manifest --agent-id --path --branch --base [--root ]\n'); process.exitCode = 2; return { ok: false, reason: 'usage' }; } @@ -1410,6 +1410,40 @@ function cmdWorktreeCreate(cwd: string, args: string[] = [], deps: RecordAgentCm return { ok: false, reason: plan.reason, hint: plan.hint }; } + // 3b. Optional root confinement (#2627, Phase 3 — the confinement Phase 2 + // deferred here from planWorktreeCreate's path-traversal guard). + // planWorktreeCreate rejects a literal ".." SEGMENT, but a plain absolute + // path outside the project contains no ".." and passes. Phase 3 makes the + // orchestrator SPAWN executor processes into these paths, so an + // unconfined --path is a write primitive aimed anywhere on the filesystem. + // + // The root is DECLARED by the caller (`--root`) rather than inferred: agent + // worktrees legitimately live outside the orchestrator's own root (a lane + // orchestrator creates siblings under the repo's .claude/worktrees/), so + // there is no layout this module could derive without guessing. Absent + // `--root` the behavior is exactly as shipped in Phase 2 — the + // orchestrator-worktree scheduler path always passes it. + // + // Lexical by design: the worktree does not exist yet, so there is nothing + // to realpath, and resolving only the root would not close a symlinked-leaf + // hole. Pairs with the leading-dash and ".."-segment guards above. + const rootFlag = flag('--root'); + if (rootFlag) { + const absRoot = path.resolve(cwd, rootFlag); + const absWorktree = path.resolve(cwd, plan.entry.worktree_path); + const rel = path.relative(absRoot, absWorktree); + // rel === '' → the worktree IS the root (would clobber the checkout) + // rel === '..' / '../…' → escapes the root + // path.isAbsolute(rel) → a different Windows drive or UNC root + if (rel === '' || rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) { + const hint = `--path must resolve INSIDE --root (root="${absRoot}", path="${absWorktree}"). A worktree outside the declared root is unreachable by manifest-scoped cleanup and would let a spawned executor write outside the project.`; + writeErr(`[gsd] worktree.create: path_outside_root — ${hint}\n`); + write(`${JSON.stringify({ ok: false, reason: 'path_outside_root', hint }, null, 2)}\n`); + process.exitCode = 1; + return { ok: false, reason: 'path_outside_root', hint }; + } + } + // 4. Compute the deduped final manifest STRING in memory now — the ONLY // manifest work left is the guarded write in step 6, after git succeeds. // #2584 FIX 2: the on-disk entry is the SAME minimal 4-field shape diff --git a/tests/codex-config.test.cjs b/tests/codex-config.test.cjs index b1d0e9d8a..9875365c7 100644 --- a/tests/codex-config.test.cjs +++ b/tests/codex-config.test.cjs @@ -5747,9 +5747,17 @@ function parseWorkflowSteps(content) { name: match[1], // After #3797 architectural fix, callsites use gsd_run readsRuntimeConfig: body.includes('RUNTIME=$(gsd_run query config-get runtime --default claude'), - // #1521: guard generalized from Codex-specific to all non-Claude runtimes - codexWorktreeGuard: body.includes('git worktree isolation') && body.includes('unsupported on runtime'), - worktreeDispatchGuidance: body.includes('isolation="worktree"'), + // #1521 generalized the guard from Codex-specific to all non-Claude + // runtimes; #2584 Phase 3 (#2627) generalized it again — off runtime + // identity entirely and onto the negotiated `dispatch.isolation` + // capability. The step now resolves ISOLATION (delegating the block to + // the isolation-dispatch fragment) and fails closed when a host + // declares no primitive, which is what #3360 actually protects. + resolvesIsolationCapability: body.includes('Resolve ISOLATION'), + // Worktree dispatch guidance is no longer a hardcoded Claude flag — + // step 3 emits the host's DECLARED harness flag. + worktreeDispatchGuidance: body.includes('{harnessFlag}') + || body.includes('executor-isolation-dispatch.md'), }; }); } @@ -5764,27 +5772,58 @@ function executePhaseWorktreeContract(content) { const initialize = steps[initializeIndex]; return { initializeReadsRuntimeConfig: initialize.readsRuntimeConfig, - initializeHasCodexWorktreeGuard: initialize.codexWorktreeGuard, + initializeResolvesIsolationCapability: initialize.resolvesIsolationCapability, guardStepPrecedesWorktreeDispatch: initializeIndex <= firstWorktreeDispatchIndex, }; } -describe('#3360 — Codex execute-phase fails closed for unsupported worktree isolation', () => { - test('execute-phase reads runtime before worktree dispatch and blocks Codex worktree mode', () => { +describe('#3360 — execute-phase fails closed for unsupported worktree isolation', () => { + // #2584 Phase 3 (#2627) moved this from "Codex is blocked by name" to "a host + // with no declared isolation primitive is blocked". Codex now DECLARES + // orchestrator-worktree and gets a real isolated path, so the guard can no + // longer key on its name — but #3360's actual protection (never run executors + // unisolated against the main checkout) is unchanged and asserted below. + const ISOLATION_FRAGMENT = path.join( + ROOT, 'gsd-core', 'workflows', 'execute-phase', 'steps', 'executor-isolation-dispatch.md', + ); + + test('execute-phase resolves the isolation capability before any worktree dispatch', () => { const workflow = fs.readFileSync(EXECUTE_PHASE, 'utf8'); const contract = executePhaseWorktreeContract(workflow); assert.deepEqual(contract, { initializeReadsRuntimeConfig: true, - initializeHasCodexWorktreeGuard: true, + initializeResolvesIsolationCapability: true, guardStepPrecedesWorktreeDispatch: true, }); }); - test('Codex adapter documents that worktree isolation has no direct spawn_agent mapping', () => { + test('a host declaring no isolation primitive still fails closed', () => { + const fragment = fs.readFileSync(ISOLATION_FRAGMENT, 'utf8'); + assert.match(fragment, /ISOLATION="?none"?/, + 'fragment must resolve the none case'); + assert.match(fragment, /FATAL[^\n]*no executor-isolation primitive/, + 'a host with dispatch.isolation=none must fail closed before dispatch (#3360)'); + assert.match(fragment, /use_worktrees=false/, + 'the fail-closed message must tell the user how to proceed'); + }); + + test('the scheduler never gates worktree dispatch on a runtime name', () => { + const workflow = fs.readFileSync(EXECUTE_PHASE, 'utf8'); + const fragment = fs.readFileSync(ISOLATION_FRAGMENT, 'utf8'); + for (const [label, src] of [['execute-phase.md', workflow], ['isolation fragment', fragment]]) { + assert.ok( + !/\[\s*"\$RUNTIME"\s*(?:!=|=)\s*"(?:codex|claude)"\s*\]\s*&&\s*\[\s*"\$USE_WORKTREES"/.test(src), + `${label}: worktree dispatch must branch on dispatch.isolation, not a runtime name (ADR-1239)`, + ); + } + }); + + test('Codex adapter documents the orchestrator-managed worktree mapping', () => { const header = getCodexSkillAdapterHeader('gsd-execute-phase'); assert.match(header, /isolation="worktree"/); - assert.match(header, /no direct Codex mapping/i); + assert.match(header, /orchestrator-worktree/i, + 'the adapter header must no longer claim Codex has no worktree mapping — #2584 Phase 3 gave it one'); }); }); }); diff --git a/tests/fixtures/golden-install-parity/antigravity.json b/tests/fixtures/golden-install-parity/antigravity.json index faff1934b..9d7e0ee0b 100644 --- a/tests/fixtures/golden-install-parity/antigravity.json +++ b/tests/fixtures/golden-install-parity/antigravity.json @@ -39,7 +39,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74", - "gsd-core/bin/gsd-tools.cjs": "6578e0a1bc8adb0a", + "gsd-core/bin/gsd-tools.cjs": "3bc9c80de40245e9", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -122,7 +122,7 @@ "gsd-core/references/planner-reviews.md": "da39eace09a10743", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "2e2f418328e52f6a", + "gsd-core/references/planning-config.md": "3294f01933ac111e", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -247,8 +247,9 @@ "gsd-core/workflows/docs-update.md": "0b3023a3caa2123f", "gsd-core/workflows/edit-phase.md": "fc932e82ba1f585a", "gsd-core/workflows/eval-review.md": "9e8b169c05098b57", - "gsd-core/workflows/execute-phase.md": "910f445e1af26714", + "gsd-core/workflows/execute-phase.md": "e66b575d3c518cda", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "1832b97d0923fa67", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "0404bcd32f47715e", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "13aa54f8279960ae", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "476f88892510a09e", diff --git a/tests/fixtures/golden-install-parity/augment.json b/tests/fixtures/golden-install-parity/augment.json index 410422173..17b3f7cb4 100644 --- a/tests/fixtures/golden-install-parity/augment.json +++ b/tests/fixtures/golden-install-parity/augment.json @@ -110,7 +110,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "407f4e44045c6e24", + "gsd-core/bin/gsd-tools.cjs": "77acbef416b9e5f9", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -193,7 +193,7 @@ "gsd-core/references/planner-reviews.md": "dda0193a0fbd4947", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "ac409835e8260a3e", + "gsd-core/references/planning-config.md": "b867e826fab06880", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -318,8 +318,9 @@ "gsd-core/workflows/docs-update.md": "a320371be70f43d8", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "32d3d7e80ab2bfc7", - "gsd-core/workflows/execute-phase.md": "4b05739b7c8715ea", + "gsd-core/workflows/execute-phase.md": "30121657c6f64883", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "7a49bc47da5411ea", diff --git a/tests/fixtures/golden-install-parity/claude-local.json b/tests/fixtures/golden-install-parity/claude-local.json index b73cc6b36..ea8021c69 100644 --- a/tests/fixtures/golden-install-parity/claude-local.json +++ b/tests/fixtures/golden-install-parity/claude-local.json @@ -109,7 +109,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "407f4e44045c6e24", + "gsd-core/bin/gsd-tools.cjs": "77acbef416b9e5f9", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -192,7 +192,7 @@ "gsd-core/references/planner-reviews.md": "da39eace09a10743", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "b025429fc72f9285", + "gsd-core/references/planning-config.md": "9bf731bae8d712a6", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -317,8 +317,9 @@ "gsd-core/workflows/docs-update.md": "7aef1019ce141558", "gsd-core/workflows/edit-phase.md": "dbbb6191f5a8b65e", "gsd-core/workflows/eval-review.md": "11620e0dc4a003dd", - "gsd-core/workflows/execute-phase.md": "d45261ba907a1436", + "gsd-core/workflows/execute-phase.md": "54aaa0d54b341308", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "9a1f3deffb4dab14", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ea0ebff5ac00fee6", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "a7f9b9a45303382f", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "b14d7fa2c4ec2406", diff --git a/tests/fixtures/golden-install-parity/claude.json b/tests/fixtures/golden-install-parity/claude.json index 13dd5c9a5..891a8e0ee 100644 --- a/tests/fixtures/golden-install-parity/claude.json +++ b/tests/fixtures/golden-install-parity/claude.json @@ -38,7 +38,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "407f4e44045c6e24", + "gsd-core/bin/gsd-tools.cjs": "77acbef416b9e5f9", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -121,7 +121,7 @@ "gsd-core/references/planner-reviews.md": "da39eace09a10743", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "b025429fc72f9285", + "gsd-core/references/planning-config.md": "9bf731bae8d712a6", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -246,8 +246,9 @@ "gsd-core/workflows/docs-update.md": "ea47bd2d5a0d1d85", "gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a", "gsd-core/workflows/eval-review.md": "7c2f5ff8cc4d06e5", - "gsd-core/workflows/execute-phase.md": "8722fff6ecdc9444", + "gsd-core/workflows/execute-phase.md": "a3ccfb42c90834b7", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "d2020c5e01c7bd7f", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "7a49bc47da5411ea", diff --git a/tests/fixtures/golden-install-parity/cline.json b/tests/fixtures/golden-install-parity/cline.json index 671565364..d53b9843f 100644 --- a/tests/fixtures/golden-install-parity/cline.json +++ b/tests/fixtures/golden-install-parity/cline.json @@ -42,7 +42,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "476aa24e8c4f03cf", - "gsd-core/bin/gsd-tools.cjs": "b9c55c2bbeec85fe", + "gsd-core/bin/gsd-tools.cjs": "ab7a26552bae55dc", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -125,7 +125,7 @@ "gsd-core/references/planner-reviews.md": "dda0193a0fbd4947", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "5a40b2934db6a321", + "gsd-core/references/planning-config.md": "35550d812bdcd6ec", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -250,8 +250,9 @@ "gsd-core/workflows/docs-update.md": "7eb7095ca86d506f", "gsd-core/workflows/edit-phase.md": "9c9fadc047c61d74", "gsd-core/workflows/eval-review.md": "17fc9b4c4f7e1c74", - "gsd-core/workflows/execute-phase.md": "6583d13e35054091", + "gsd-core/workflows/execute-phase.md": "bb3a5f28fc4543f2", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "1b3558cb8f41b65a", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "e01459552d587863", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "35612890c1173577", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "26825e1aacf224d0", diff --git a/tests/fixtures/golden-install-parity/codebuddy.json b/tests/fixtures/golden-install-parity/codebuddy.json index 543aa8a90..404e0a4fd 100644 --- a/tests/fixtures/golden-install-parity/codebuddy.json +++ b/tests/fixtures/golden-install-parity/codebuddy.json @@ -110,7 +110,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "407f4e44045c6e24", + "gsd-core/bin/gsd-tools.cjs": "77acbef416b9e5f9", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -193,7 +193,7 @@ "gsd-core/references/planner-reviews.md": "dda0193a0fbd4947", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "ac409835e8260a3e", + "gsd-core/references/planning-config.md": "b867e826fab06880", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -318,8 +318,9 @@ "gsd-core/workflows/docs-update.md": "a320371be70f43d8", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "32d3d7e80ab2bfc7", - "gsd-core/workflows/execute-phase.md": "aa39a0f01567166c", + "gsd-core/workflows/execute-phase.md": "9d513e6bb52e340c", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "7a49bc47da5411ea", diff --git a/tests/fixtures/golden-install-parity/codex.json b/tests/fixtures/golden-install-parity/codex.json index bb35317e2..c410f8290 100644 --- a/tests/fixtures/golden-install-parity/codex.json +++ b/tests/fixtures/golden-install-parity/codex.json @@ -1,75 +1,75 @@ { - ".agents/skills/gsd-add-tests/SKILL.md": "f3c1594a059de3ee", - ".agents/skills/gsd-ai-integration-phase/SKILL.md": "279cc57f38c78756", - ".agents/skills/gsd-audit-fix/SKILL.md": "9cea2764b92aa352", - ".agents/skills/gsd-audit-milestone/SKILL.md": "0528ef8a5834b5df", - ".agents/skills/gsd-audit-uat/SKILL.md": "6c2aa0c2b62c5233", - ".agents/skills/gsd-autonomous/SKILL.md": "d56dc692750f4926", - ".agents/skills/gsd-capture/SKILL.md": "211d601d122b5dd6", - ".agents/skills/gsd-cleanup/SKILL.md": "18cba17463c6ef97", - ".agents/skills/gsd-code-review/SKILL.md": "74749a1aff4224d7", - ".agents/skills/gsd-complete-milestone/SKILL.md": "77bee8741f8cb381", - ".agents/skills/gsd-config/SKILL.md": "5fde92e30619edcb", - ".agents/skills/gsd-debug/SKILL.md": "7e56964e14fd0a10", - ".agents/skills/gsd-discuss-phase/SKILL.md": "837d46c55cc2424e", - ".agents/skills/gsd-docs-update/SKILL.md": "5028dc5dd44bee7d", - ".agents/skills/gsd-eval-review/SKILL.md": "a300e78a27ba74d6", - ".agents/skills/gsd-execute-phase/SKILL.md": "e77bee13068600d3", - ".agents/skills/gsd-explore/SKILL.md": "d660cde0e6f31ebf", - ".agents/skills/gsd-extract-learnings/SKILL.md": "e32388999da384e4", - ".agents/skills/gsd-fast/SKILL.md": "14c407a76a40e115", - ".agents/skills/gsd-forensics/SKILL.md": "6d496a16f5ca74b3", - ".agents/skills/gsd-graphify/SKILL.md": "1bbd96129fbacc5c", - ".agents/skills/gsd-health/SKILL.md": "656c9c63852e3787", - ".agents/skills/gsd-help/SKILL.md": "28eb8d0b487239f8", - ".agents/skills/gsd-import/SKILL.md": "4d15ee09e531342f", - ".agents/skills/gsd-inbox/SKILL.md": "1315033595643485", - ".agents/skills/gsd-ingest-docs/SKILL.md": "5bd2838bf0b6dc1b", - ".agents/skills/gsd-manager/SKILL.md": "cb1cf56f5d3f66d6", - ".agents/skills/gsd-map-codebase/SKILL.md": "cd27dc028718426b", - ".agents/skills/gsd-mempalace-capture/SKILL.md": "fb426a6700b98eed", - ".agents/skills/gsd-mempalace-recall/SKILL.md": "7205b02250e89f25", - ".agents/skills/gsd-milestone-summary/SKILL.md": "af84ecb400f23556", - ".agents/skills/gsd-mvp-phase/SKILL.md": "72b65ae927b280fe", - ".agents/skills/gsd-new-milestone/SKILL.md": "587574d2d475bb10", - ".agents/skills/gsd-new-project/SKILL.md": "129ac16e6a1f04ba", - ".agents/skills/gsd-next/SKILL.md": "7855fd70e387087e", - ".agents/skills/gsd-ns-context/SKILL.md": "9b496da79789b3f9", - ".agents/skills/gsd-ns-ideate/SKILL.md": "84ca1cde06110981", - ".agents/skills/gsd-ns-manage/SKILL.md": "909dafa0cd19ba5a", - ".agents/skills/gsd-ns-project/SKILL.md": "936b6998d42520ac", - ".agents/skills/gsd-ns-review/SKILL.md": "022253010db0e072", - ".agents/skills/gsd-ns-workflow/SKILL.md": "cba075bc819b539e", - ".agents/skills/gsd-onboard/SKILL.md": "f42fc2edebeda671", - ".agents/skills/gsd-pause-work/SKILL.md": "b379469eed78a196", - ".agents/skills/gsd-phase/SKILL.md": "25477edc97a90c91", - ".agents/skills/gsd-plan-phase/SKILL.md": "14b7a4f76825a5c2", - ".agents/skills/gsd-plan-review-convergence/SKILL.md": "c7d8a4db99e4f4b9", - ".agents/skills/gsd-pr-branch/SKILL.md": "6901da15e321913e", - ".agents/skills/gsd-profile-user/SKILL.md": "6259fabfb6afe7be", - ".agents/skills/gsd-progress/SKILL.md": "85d76286162b9189", - ".agents/skills/gsd-quick/SKILL.md": "b4f4e711ba664aa0", - ".agents/skills/gsd-resume-work/SKILL.md": "04f6c2e5b579e8c4", - ".agents/skills/gsd-review-backlog/SKILL.md": "469696b944c4e7b5", - ".agents/skills/gsd-review/SKILL.md": "a45ab2fdca06793b", - ".agents/skills/gsd-secure-phase/SKILL.md": "e64ad269c1e6e319", - ".agents/skills/gsd-settings/SKILL.md": "fcdda8dd545622ae", - ".agents/skills/gsd-ship/SKILL.md": "9615cc4c8f6de060", - ".agents/skills/gsd-sketch/SKILL.md": "90c219b843db7ec7", - ".agents/skills/gsd-spec-phase/SKILL.md": "56a5cbd606db9cba", - ".agents/skills/gsd-spike/SKILL.md": "77209fef7a04c11a", - ".agents/skills/gsd-stats/SKILL.md": "566024444ef71f44", - ".agents/skills/gsd-surface/SKILL.md": "e8b9d3c291a5c18c", - ".agents/skills/gsd-thread/SKILL.md": "85326c97c83a02d1", - ".agents/skills/gsd-ui-phase/SKILL.md": "a0c8fbcbe5e3c2b9", - ".agents/skills/gsd-ui-review/SKILL.md": "081a3292a2d357f5", - ".agents/skills/gsd-ultraplan-phase/SKILL.md": "06fb3d76eb785f94", - ".agents/skills/gsd-undo/SKILL.md": "007d3b307e027af9", - ".agents/skills/gsd-update/SKILL.md": "e6e49e117c7c8f35", - ".agents/skills/gsd-validate-phase/SKILL.md": "373059517a94a194", - ".agents/skills/gsd-verify-work/SKILL.md": "4272275698e9a3fe", - ".agents/skills/gsd-workspace/SKILL.md": "06d6400d68318361", - ".agents/skills/gsd-workstreams/SKILL.md": "2f77bb94db1be1a4", + ".agents/skills/gsd-add-tests/SKILL.md": "77b8cc2260347f34", + ".agents/skills/gsd-ai-integration-phase/SKILL.md": "a7d2527fdacdfca8", + ".agents/skills/gsd-audit-fix/SKILL.md": "dcf56fbc7dddfa05", + ".agents/skills/gsd-audit-milestone/SKILL.md": "5f0c7c1212a1aab9", + ".agents/skills/gsd-audit-uat/SKILL.md": "60e3113cd4f79d30", + ".agents/skills/gsd-autonomous/SKILL.md": "814575ceee96bf7f", + ".agents/skills/gsd-capture/SKILL.md": "24c497365eb1dc88", + ".agents/skills/gsd-cleanup/SKILL.md": "a98cf0c1a9cd043f", + ".agents/skills/gsd-code-review/SKILL.md": "8d437487fdf22dd8", + ".agents/skills/gsd-complete-milestone/SKILL.md": "4db2dfdb992f1ddc", + ".agents/skills/gsd-config/SKILL.md": "d461419ef4715e8a", + ".agents/skills/gsd-debug/SKILL.md": "0b6f9657b8d951b3", + ".agents/skills/gsd-discuss-phase/SKILL.md": "9576325fe81d2fa3", + ".agents/skills/gsd-docs-update/SKILL.md": "cb3b313895cb02c2", + ".agents/skills/gsd-eval-review/SKILL.md": "0047cb3cf40d1e56", + ".agents/skills/gsd-execute-phase/SKILL.md": "2f74b8bf59526a07", + ".agents/skills/gsd-explore/SKILL.md": "0114f763a0b23e1b", + ".agents/skills/gsd-extract-learnings/SKILL.md": "386a4dc07b1531bb", + ".agents/skills/gsd-fast/SKILL.md": "f56a7f5dcd552e1d", + ".agents/skills/gsd-forensics/SKILL.md": "8e316475309e0244", + ".agents/skills/gsd-graphify/SKILL.md": "89b024ca307dbef2", + ".agents/skills/gsd-health/SKILL.md": "6236cb12b1ad87a0", + ".agents/skills/gsd-help/SKILL.md": "dcec9a38b65a3fe7", + ".agents/skills/gsd-import/SKILL.md": "2bfbdb1d8e968e92", + ".agents/skills/gsd-inbox/SKILL.md": "6cb4b0a7d204a57f", + ".agents/skills/gsd-ingest-docs/SKILL.md": "89d7a1600280cea0", + ".agents/skills/gsd-manager/SKILL.md": "30871970a7f114fd", + ".agents/skills/gsd-map-codebase/SKILL.md": "9e8075f7e40f71b0", + ".agents/skills/gsd-mempalace-capture/SKILL.md": "90e246d2fcb69742", + ".agents/skills/gsd-mempalace-recall/SKILL.md": "af823f0ba710ae9f", + ".agents/skills/gsd-milestone-summary/SKILL.md": "bc764a054f502fb9", + ".agents/skills/gsd-mvp-phase/SKILL.md": "0510a25c1812c14e", + ".agents/skills/gsd-new-milestone/SKILL.md": "6a0f49871aa109ce", + ".agents/skills/gsd-new-project/SKILL.md": "2b0ed171caff620f", + ".agents/skills/gsd-next/SKILL.md": "e5bbd32e2ddf7898", + ".agents/skills/gsd-ns-context/SKILL.md": "701f9c8b888da86d", + ".agents/skills/gsd-ns-ideate/SKILL.md": "9d7fadf4e720d0f7", + ".agents/skills/gsd-ns-manage/SKILL.md": "7796f26fef6f37ed", + ".agents/skills/gsd-ns-project/SKILL.md": "334d912c373dd510", + ".agents/skills/gsd-ns-review/SKILL.md": "b8c0e8c006750d9e", + ".agents/skills/gsd-ns-workflow/SKILL.md": "74cfe054960e3d3a", + ".agents/skills/gsd-onboard/SKILL.md": "405f1bafd7923f63", + ".agents/skills/gsd-pause-work/SKILL.md": "035ba2eb44694897", + ".agents/skills/gsd-phase/SKILL.md": "fcaf25f68fb69aa9", + ".agents/skills/gsd-plan-phase/SKILL.md": "2e32fc011f5a967a", + ".agents/skills/gsd-plan-review-convergence/SKILL.md": "d2a06c49143781a7", + ".agents/skills/gsd-pr-branch/SKILL.md": "ec4b400d07d513ae", + ".agents/skills/gsd-profile-user/SKILL.md": "2121b67e7a15bab5", + ".agents/skills/gsd-progress/SKILL.md": "c437575d9db40ec5", + ".agents/skills/gsd-quick/SKILL.md": "2903daf3d7a7721d", + ".agents/skills/gsd-resume-work/SKILL.md": "ae8d0d364e4253e7", + ".agents/skills/gsd-review-backlog/SKILL.md": "e7f1864923ac2364", + ".agents/skills/gsd-review/SKILL.md": "7da0004c99fa4bb0", + ".agents/skills/gsd-secure-phase/SKILL.md": "c9e188e7245d1737", + ".agents/skills/gsd-settings/SKILL.md": "300cd125bcf6ed36", + ".agents/skills/gsd-ship/SKILL.md": "a4e4754b1ec4acc1", + ".agents/skills/gsd-sketch/SKILL.md": "aeb24dbbb9fdf53b", + ".agents/skills/gsd-spec-phase/SKILL.md": "8f83fab7e9bf7cf2", + ".agents/skills/gsd-spike/SKILL.md": "c1cf5ac37ab91f5a", + ".agents/skills/gsd-stats/SKILL.md": "baf175aeb2ab443d", + ".agents/skills/gsd-surface/SKILL.md": "aa799cd1fcd19314", + ".agents/skills/gsd-thread/SKILL.md": "dcf2f37027521f34", + ".agents/skills/gsd-ui-phase/SKILL.md": "f5566a58fb643a93", + ".agents/skills/gsd-ui-review/SKILL.md": "cc4621cf9f34c203", + ".agents/skills/gsd-ultraplan-phase/SKILL.md": "a3102b7aa3358374", + ".agents/skills/gsd-undo/SKILL.md": "eda2192b937bc7bf", + ".agents/skills/gsd-update/SKILL.md": "c6cef6debd103e68", + ".agents/skills/gsd-validate-phase/SKILL.md": "0e628cd8455f5127", + ".agents/skills/gsd-verify-work/SKILL.md": "7025ea9c3bb18bc4", + ".agents/skills/gsd-workspace/SKILL.md": "d7e1436cd4b79c54", + ".agents/skills/gsd-workstreams/SKILL.md": "88c2e54d79078ba3", ".gsd-profile": "0e716a5fef4e6dc1", ".gsd/defaults.json": "5ca58f5ff7ef0b92", "agents/gsd-advisor-researcher.md": "eea6d1604aaf305c", @@ -145,7 +145,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "407f4e44045c6e24", + "gsd-core/bin/gsd-tools.cjs": "77acbef416b9e5f9", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -228,7 +228,7 @@ "gsd-core/references/planner-reviews.md": "7889bfa28e82156b", "gsd-core/references/planner-revision.md": "2ebf1a714d1ec4bf", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "b01856d7a63e73a2", + "gsd-core/references/planning-config.md": "eedc6daa26054994", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -353,8 +353,9 @@ "gsd-core/workflows/docs-update.md": "bf32efe90219d3dd", "gsd-core/workflows/edit-phase.md": "e592a4d85ce5380f", "gsd-core/workflows/eval-review.md": "c89a63285ead961e", - "gsd-core/workflows/execute-phase.md": "a805ba08992a6b78", + "gsd-core/workflows/execute-phase.md": "a4040b3faa7fcd55", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "cd8678d082d6e191", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "7a49bc47da5411ea", diff --git a/tests/fixtures/golden-install-parity/copilot.json b/tests/fixtures/golden-install-parity/copilot.json index 4af05a50c..8c2dc2aef 100644 --- a/tests/fixtures/golden-install-parity/copilot.json +++ b/tests/fixtures/golden-install-parity/copilot.json @@ -40,7 +40,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74", - "gsd-core/bin/gsd-tools.cjs": "6578e0a1bc8adb0a", + "gsd-core/bin/gsd-tools.cjs": "3bc9c80de40245e9", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -123,7 +123,7 @@ "gsd-core/references/planner-reviews.md": "da39eace09a10743", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "309a566a0e13e43e", + "gsd-core/references/planning-config.md": "788802092a535772", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -248,8 +248,9 @@ "gsd-core/workflows/docs-update.md": "f64b067d0d729f04", "gsd-core/workflows/edit-phase.md": "8667c28b22b1599f", "gsd-core/workflows/eval-review.md": "1ae2d3d3fbf72893", - "gsd-core/workflows/execute-phase.md": "9c765cec7679b24c", + "gsd-core/workflows/execute-phase.md": "8febab8a96bcd729", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "ca37f91f7bd6f05c", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "e0ec3d9748a0fed9", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "25bebed74e645109", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "dc0b96d285327b18", diff --git a/tests/fixtures/golden-install-parity/cursor.json b/tests/fixtures/golden-install-parity/cursor.json index b09302a7a..ba4d1891a 100644 --- a/tests/fixtures/golden-install-parity/cursor.json +++ b/tests/fixtures/golden-install-parity/cursor.json @@ -110,7 +110,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "2525f1ae8b086828", - "gsd-core/bin/gsd-tools.cjs": "3ac4c0a9f3a509f9", + "gsd-core/bin/gsd-tools.cjs": "2ad128ce1ccf606e", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -193,7 +193,7 @@ "gsd-core/references/planner-reviews.md": "da39eace09a10743", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "a3f1b4aca291db59", + "gsd-core/references/planning-config.md": "0cda9fb6e0dea14f", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -318,8 +318,9 @@ "gsd-core/workflows/docs-update.md": "215b0122c4d2b1e5", "gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a", "gsd-core/workflows/eval-review.md": "9389c58f7384972d", - "gsd-core/workflows/execute-phase.md": "f55929bb18a78cac", + "gsd-core/workflows/execute-phase.md": "5038eb8c1e264b90", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "d2020c5e01c7bd7f", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "06b54ecd4015c64c", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "7a49bc47da5411ea", diff --git a/tests/fixtures/golden-install-parity/hermes.json b/tests/fixtures/golden-install-parity/hermes.json index 4f33ffe4b..2e0d23d5f 100644 --- a/tests/fixtures/golden-install-parity/hermes.json +++ b/tests/fixtures/golden-install-parity/hermes.json @@ -39,7 +39,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "3a3409215044af9f", - "gsd-core/bin/gsd-tools.cjs": "551974377e74c2bd", + "gsd-core/bin/gsd-tools.cjs": "c5ee45330e4b4e59", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -122,7 +122,7 @@ "gsd-core/references/planner-reviews.md": "da39eace09a10743", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "831d626c214d92e4", + "gsd-core/references/planning-config.md": "b2c11e48f913d3e5", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -247,8 +247,9 @@ "gsd-core/workflows/docs-update.md": "02811dd7aa32d5ba", "gsd-core/workflows/edit-phase.md": "7f27003f20e88fb8", "gsd-core/workflows/eval-review.md": "e7cd5dfcf18e2458", - "gsd-core/workflows/execute-phase.md": "b76c1701cee71add", + "gsd-core/workflows/execute-phase.md": "2c22f9d143c79deb", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "9b2193de25506b3b", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "4dfefdd56d814d06", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "26ee34c543926402", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "362123fdf99e9980", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "a691e3afe88031b8", diff --git a/tests/fixtures/golden-install-parity/kilo.json b/tests/fixtures/golden-install-parity/kilo.json index 1e459437e..277dd16f3 100644 --- a/tests/fixtures/golden-install-parity/kilo.json +++ b/tests/fixtures/golden-install-parity/kilo.json @@ -110,7 +110,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "407f4e44045c6e24", + "gsd-core/bin/gsd-tools.cjs": "77acbef416b9e5f9", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -193,7 +193,7 @@ "gsd-core/references/planner-reviews.md": "da39eace09a10743", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "37ab69107a2f7dc6", + "gsd-core/references/planning-config.md": "f51934f2fcfa9943", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -318,8 +318,9 @@ "gsd-core/workflows/docs-update.md": "3c0d5ef72202d246", "gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a", "gsd-core/workflows/eval-review.md": "35be91cd22a9bc11", - "gsd-core/workflows/execute-phase.md": "c9ba41385808e527", + "gsd-core/workflows/execute-phase.md": "9d2e270c04edc134", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "b1e6588cd32a6f08", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "7a49bc47da5411ea", diff --git a/tests/fixtures/golden-install-parity/kimi-code.json b/tests/fixtures/golden-install-parity/kimi-code.json index 3104e6e2a..e4fee5640 100644 --- a/tests/fixtures/golden-install-parity/kimi-code.json +++ b/tests/fixtures/golden-install-parity/kimi-code.json @@ -67,7 +67,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "407f4e44045c6e24", + "gsd-core/bin/gsd-tools.cjs": "77acbef416b9e5f9", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -150,7 +150,7 @@ "gsd-core/references/planner-reviews.md": "dda0193a0fbd4947", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "ac409835e8260a3e", + "gsd-core/references/planning-config.md": "b867e826fab06880", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -275,8 +275,9 @@ "gsd-core/workflows/docs-update.md": "a320371be70f43d8", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "32d3d7e80ab2bfc7", - "gsd-core/workflows/execute-phase.md": "68d41c524ab90953", + "gsd-core/workflows/execute-phase.md": "6eddb1de70dabecd", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "7a49bc47da5411ea", diff --git a/tests/fixtures/golden-install-parity/kimi.json b/tests/fixtures/golden-install-parity/kimi.json index 5d7951b7e..ad171de91 100644 --- a/tests/fixtures/golden-install-parity/kimi.json +++ b/tests/fixtures/golden-install-parity/kimi.json @@ -103,7 +103,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "407f4e44045c6e24", + "gsd-core/bin/gsd-tools.cjs": "77acbef416b9e5f9", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -186,7 +186,7 @@ "gsd-core/references/planner-reviews.md": "dda0193a0fbd4947", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "ac409835e8260a3e", + "gsd-core/references/planning-config.md": "b867e826fab06880", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -311,8 +311,9 @@ "gsd-core/workflows/docs-update.md": "a320371be70f43d8", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "32d3d7e80ab2bfc7", - "gsd-core/workflows/execute-phase.md": "4098733ed70e3898", + "gsd-core/workflows/execute-phase.md": "1eaaf339d670c496", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "7a49bc47da5411ea", diff --git a/tests/fixtures/golden-install-parity/opencode.json b/tests/fixtures/golden-install-parity/opencode.json index ee69e50e7..3293590ca 100644 --- a/tests/fixtures/golden-install-parity/opencode.json +++ b/tests/fixtures/golden-install-parity/opencode.json @@ -110,7 +110,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "407f4e44045c6e24", + "gsd-core/bin/gsd-tools.cjs": "77acbef416b9e5f9", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -193,7 +193,7 @@ "gsd-core/references/planner-reviews.md": "da39eace09a10743", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "37ab69107a2f7dc6", + "gsd-core/references/planning-config.md": "f51934f2fcfa9943", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -318,8 +318,9 @@ "gsd-core/workflows/docs-update.md": "f6210dc8ff7776d5", "gsd-core/workflows/edit-phase.md": "1876c855fb0a0a39", "gsd-core/workflows/eval-review.md": "1973f08d0eb34159", - "gsd-core/workflows/execute-phase.md": "acdb85a38ea89628", + "gsd-core/workflows/execute-phase.md": "b5b307cf98e1ff22", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "a15993affd62f4bf", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "f74ce2de21b2d252", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "c4cba01539f0368a", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "21f4d9b3abc334ce", diff --git a/tests/fixtures/golden-install-parity/pi.json b/tests/fixtures/golden-install-parity/pi.json index 03a2ac6b4..fca5722f5 100644 --- a/tests/fixtures/golden-install-parity/pi.json +++ b/tests/fixtures/golden-install-parity/pi.json @@ -6,7 +6,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "407f4e44045c6e24", + "gsd-core/bin/gsd-tools.cjs": "77acbef416b9e5f9", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -89,7 +89,7 @@ "gsd-core/references/planner-reviews.md": "dda0193a0fbd4947", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "ac409835e8260a3e", + "gsd-core/references/planning-config.md": "b867e826fab06880", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -214,8 +214,9 @@ "gsd-core/workflows/docs-update.md": "a320371be70f43d8", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "32d3d7e80ab2bfc7", - "gsd-core/workflows/execute-phase.md": "0e0359655e2913fd", + "gsd-core/workflows/execute-phase.md": "6bcbae12578d3cb8", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "7a49bc47da5411ea", diff --git a/tests/fixtures/golden-install-parity/qwen.json b/tests/fixtures/golden-install-parity/qwen.json index c5d8b9d55..5ad3b9494 100644 --- a/tests/fixtures/golden-install-parity/qwen.json +++ b/tests/fixtures/golden-install-parity/qwen.json @@ -39,7 +39,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "6e98d76e955e35a2", - "gsd-core/bin/gsd-tools.cjs": "509319fab53d5022", + "gsd-core/bin/gsd-tools.cjs": "658a035b209dd82d", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -122,7 +122,7 @@ "gsd-core/references/planner-reviews.md": "da39eace09a10743", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "4c6de9b6d66aca73", + "gsd-core/references/planning-config.md": "8491550a179906fd", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -247,8 +247,9 @@ "gsd-core/workflows/docs-update.md": "226550a6d556b09f", "gsd-core/workflows/edit-phase.md": "0fb5e0123cfc6f36", "gsd-core/workflows/eval-review.md": "88431952699d9fa6", - "gsd-core/workflows/execute-phase.md": "19d01bc7f7b45279", + "gsd-core/workflows/execute-phase.md": "febed65d77f311af", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "0e0949db56deebeb", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "2ca3c7397d44613f", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "0b04cc2dcab61107", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "9cbf100b17f4bcb1", diff --git a/tests/fixtures/golden-install-parity/trae.json b/tests/fixtures/golden-install-parity/trae.json index e8f395721..d44bbd477 100644 --- a/tests/fixtures/golden-install-parity/trae.json +++ b/tests/fixtures/golden-install-parity/trae.json @@ -39,7 +39,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "de4627dff103d527", - "gsd-core/bin/gsd-tools.cjs": "70126458e4c1216f", + "gsd-core/bin/gsd-tools.cjs": "4ad9a090f406f549", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -122,7 +122,7 @@ "gsd-core/references/planner-reviews.md": "da39eace09a10743", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "a686c1363ae626c5", + "gsd-core/references/planning-config.md": "c14f59545fbfb981", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -247,8 +247,9 @@ "gsd-core/workflows/docs-update.md": "89e760e665ff99f8", "gsd-core/workflows/edit-phase.md": "7facd0faa33c8cad", "gsd-core/workflows/eval-review.md": "90a29a4fa0cdd947", - "gsd-core/workflows/execute-phase.md": "729bf037d4bdec10", + "gsd-core/workflows/execute-phase.md": "6414076b8a380b33", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "296b812e8d0e9ce8", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "c1d6db937a51d433", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7d30384e9bf82664", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "90efedae3decaffe", diff --git a/tests/fixtures/golden-install-parity/windsurf.json b/tests/fixtures/golden-install-parity/windsurf.json index 969ad1567..3f8c3decf 100644 --- a/tests/fixtures/golden-install-parity/windsurf.json +++ b/tests/fixtures/golden-install-parity/windsurf.json @@ -39,7 +39,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "5636ca0b726871b2", - "gsd-core/bin/gsd-tools.cjs": "5e8b0e86049ca8ef", + "gsd-core/bin/gsd-tools.cjs": "2afce7fb45247a8b", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -122,7 +122,7 @@ "gsd-core/references/planner-reviews.md": "da39eace09a10743", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "1fc70920778d7c8e", + "gsd-core/references/planning-config.md": "3c961d736c3d9f4f", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -247,8 +247,9 @@ "gsd-core/workflows/docs-update.md": "6bd25d0123ee6c73", "gsd-core/workflows/edit-phase.md": "c0ae7d0063f3e789", "gsd-core/workflows/eval-review.md": "713de00ea5326988", - "gsd-core/workflows/execute-phase.md": "24db4d3799ad23bc", + "gsd-core/workflows/execute-phase.md": "706eef1de2da75a7", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "3194ecd382690755", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "becb931701890353", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "fb4497767cdf73a3", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "c54d55fad3ab5263", diff --git a/tests/fixtures/golden-install-parity/zcode.json b/tests/fixtures/golden-install-parity/zcode.json index 19085f154..41486e8a2 100644 --- a/tests/fixtures/golden-install-parity/zcode.json +++ b/tests/fixtures/golden-install-parity/zcode.json @@ -110,7 +110,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "407f4e44045c6e24", + "gsd-core/bin/gsd-tools.cjs": "77acbef416b9e5f9", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "2f60b9eaa6cf6bc1", @@ -193,7 +193,7 @@ "gsd-core/references/planner-reviews.md": "dda0193a0fbd4947", "gsd-core/references/planner-revision.md": "86ba8a511f081f05", "gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b", - "gsd-core/references/planning-config.md": "ac409835e8260a3e", + "gsd-core/references/planning-config.md": "b867e826fab06880", "gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6", "gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020", "gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8", @@ -318,8 +318,9 @@ "gsd-core/workflows/docs-update.md": "a320371be70f43d8", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "32d3d7e80ab2bfc7", - "gsd-core/workflows/execute-phase.md": "b5e7fced436a73fd", + "gsd-core/workflows/execute-phase.md": "8d16d4af7115e12b", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a", "gsd-core/workflows/execute-phase/steps/regression-gate.md": "7a49bc47da5411ea", diff --git a/tests/fixtures/install-tree/antigravity.json b/tests/fixtures/install-tree/antigravity.json index 90a025c25..e3ba38f69 100644 --- a/tests/fixtures/install-tree/antigravity.json +++ b/tests/fixtures/install-tree/antigravity.json @@ -249,6 +249,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/augment.json b/tests/fixtures/install-tree/augment.json index ef0e176a6..6cf3b03c8 100644 --- a/tests/fixtures/install-tree/augment.json +++ b/tests/fixtures/install-tree/augment.json @@ -320,6 +320,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/claude-local.json b/tests/fixtures/install-tree/claude-local.json index 2804a51e8..1c011e47a 100644 --- a/tests/fixtures/install-tree/claude-local.json +++ b/tests/fixtures/install-tree/claude-local.json @@ -319,6 +319,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/claude.json b/tests/fixtures/install-tree/claude.json index b7fd53a1a..c10932de4 100644 --- a/tests/fixtures/install-tree/claude.json +++ b/tests/fixtures/install-tree/claude.json @@ -248,6 +248,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/cline.json b/tests/fixtures/install-tree/cline.json index 1df0f4dbb..263383cf3 100644 --- a/tests/fixtures/install-tree/cline.json +++ b/tests/fixtures/install-tree/cline.json @@ -252,6 +252,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/codebuddy.json b/tests/fixtures/install-tree/codebuddy.json index 79f0e31bc..3ba833652 100644 --- a/tests/fixtures/install-tree/codebuddy.json +++ b/tests/fixtures/install-tree/codebuddy.json @@ -320,6 +320,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/codex.json b/tests/fixtures/install-tree/codex.json index 846145cbe..94b5406bf 100644 --- a/tests/fixtures/install-tree/codex.json +++ b/tests/fixtures/install-tree/codex.json @@ -355,6 +355,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/copilot.json b/tests/fixtures/install-tree/copilot.json index 38712b767..c4af4f629 100644 --- a/tests/fixtures/install-tree/copilot.json +++ b/tests/fixtures/install-tree/copilot.json @@ -250,6 +250,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/cursor.json b/tests/fixtures/install-tree/cursor.json index 71076997a..cf8a60f64 100644 --- a/tests/fixtures/install-tree/cursor.json +++ b/tests/fixtures/install-tree/cursor.json @@ -320,6 +320,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/hermes.json b/tests/fixtures/install-tree/hermes.json index 9bda2069b..89db6422c 100644 --- a/tests/fixtures/install-tree/hermes.json +++ b/tests/fixtures/install-tree/hermes.json @@ -249,6 +249,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/kilo.json b/tests/fixtures/install-tree/kilo.json index 260de6e6f..326c09315 100644 --- a/tests/fixtures/install-tree/kilo.json +++ b/tests/fixtures/install-tree/kilo.json @@ -320,6 +320,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/kimi-code.json b/tests/fixtures/install-tree/kimi-code.json index 24a329628..e470d8d64 100644 --- a/tests/fixtures/install-tree/kimi-code.json +++ b/tests/fixtures/install-tree/kimi-code.json @@ -277,6 +277,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/kimi.json b/tests/fixtures/install-tree/kimi.json index 9a6a6031b..ab6e6686b 100644 --- a/tests/fixtures/install-tree/kimi.json +++ b/tests/fixtures/install-tree/kimi.json @@ -313,6 +313,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/opencode.json b/tests/fixtures/install-tree/opencode.json index 679fcb959..865a39e2f 100644 --- a/tests/fixtures/install-tree/opencode.json +++ b/tests/fixtures/install-tree/opencode.json @@ -320,6 +320,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/pi.json b/tests/fixtures/install-tree/pi.json index 99f0e0717..a497ed424 100644 --- a/tests/fixtures/install-tree/pi.json +++ b/tests/fixtures/install-tree/pi.json @@ -216,6 +216,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/qwen.json b/tests/fixtures/install-tree/qwen.json index 04d650054..87c62c8fb 100644 --- a/tests/fixtures/install-tree/qwen.json +++ b/tests/fixtures/install-tree/qwen.json @@ -249,6 +249,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/trae.json b/tests/fixtures/install-tree/trae.json index 5ff8b45d2..97bec2ec0 100644 --- a/tests/fixtures/install-tree/trae.json +++ b/tests/fixtures/install-tree/trae.json @@ -249,6 +249,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/windsurf.json b/tests/fixtures/install-tree/windsurf.json index 8610ff8d7..1ac070e8c 100644 --- a/tests/fixtures/install-tree/windsurf.json +++ b/tests/fixtures/install-tree/windsurf.json @@ -249,6 +249,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/fixtures/install-tree/zcode.json b/tests/fixtures/install-tree/zcode.json index 782198b03..f30b96cd6 100644 --- a/tests/fixtures/install-tree/zcode.json +++ b/tests/fixtures/install-tree/zcode.json @@ -320,6 +320,7 @@ "gsd-core/workflows/eval-review.md", "gsd-core/workflows/execute-phase.md", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md", + "gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md", "gsd-core/workflows/execute-phase/steps/regression-gate.md", diff --git a/tests/host-integration.test.cjs b/tests/host-integration.test.cjs index 6e0b646e7..f89d14172 100644 --- a/tests/host-integration.test.cjs +++ b/tests/host-integration.test.cjs @@ -1344,23 +1344,129 @@ describe('resolveOrchestratorExec — the 4 shipped orchestrator-worktree descri assert.equal(result.cwd, CWD); }); - test('kimi: --work-dir (no leading verb)', () => { - const result = resolveOrchestratorExec({ command: 'kimi', args: [], cwdFlag: '--work-dir' }, CWD); + // #2627: `args` carries --print because kimi's working mode is otherwise the + // interactive TUI — an orchestrator spawning a TUI hangs forever rather than + // returning a completed plan. + test('kimi: --print --work-dir (headless flag leads)', () => { + const result = resolveOrchestratorExec({ command: 'kimi', args: ['--print'], cwdFlag: '--work-dir' }, CWD); assert.equal(result.ok, true); assert.equal(result.command, 'kimi'); - assert.deepEqual(result.args, ['--work-dir', CWD]); + assert.deepEqual(result.args, ['--print', '--work-dir', CWD]); assert.equal(result.cwd, CWD); }); - test('kimi-code: cwdFlag:null — NO flag appended, cwd still returned (process-cwd case)', () => { - const result = resolveOrchestratorExec({ command: 'kimi-code', args: [], cwdFlag: null }, CWD); + // #2627: the binary is `kimi`, NOT `kimi-code` — Moonshot's TypeScript Kimi + // Code installs its binary as `kimi`; `kimi-code` is only the npm package and + // config-home name, so spawning it is an immediate ENOENT. + test('kimi-code: command is "kimi"; cwdFlag:null appends NO flag, cwd still returned (process-cwd case)', () => { + const result = resolveOrchestratorExec({ command: 'kimi', args: [], cwdFlag: null }, CWD); assert.equal(result.ok, true); - assert.equal(result.command, 'kimi-code'); + assert.equal(result.command, 'kimi'); assert.deepEqual(result.args, []); assert.equal(result.cwd, CWD); }); }); +describe('resolveOrchestratorExec — prompt passing (#2627, Phase 3)', () => { + const CWD = '/repo/.claude/worktrees/agent-a1'; + const PROMPT = 'Execute plan 2 of phase 3.'; + + test('promptFlag:null → prompt appended POSITIONALLY, last (codex shape)', () => { + const result = resolveOrchestratorExec( + { command: 'codex', args: ['exec'], cwdFlag: '--cd', promptFlag: null }, CWD, PROMPT, + ); + assert.equal(result.ok, true); + assert.deepEqual(result.args, ['exec', '--cd', CWD, PROMPT]); + }); + + test('promptFlag absent → prompt appended POSITIONALLY (opencode shape)', () => { + const result = resolveOrchestratorExec( + { command: 'opencode', args: ['run'], cwdFlag: '--dir' }, CWD, PROMPT, + ); + assert.equal(result.ok, true); + assert.deepEqual(result.args, ['run', '--dir', CWD, PROMPT]); + }); + + test('promptFlag string → [flag, prompt] appended (kimi shape)', () => { + const result = resolveOrchestratorExec( + { command: 'kimi', args: ['--print'], cwdFlag: '--work-dir', promptFlag: '--prompt' }, CWD, PROMPT, + ); + assert.equal(result.ok, true); + assert.deepEqual(result.args, ['--print', '--work-dir', CWD, '--prompt', PROMPT]); + }); + + test('promptFlag string + cwdFlag null → prompt flag only, cwd via process cwd (kimi-code shape)', () => { + const result = resolveOrchestratorExec( + { command: 'kimi', args: [], cwdFlag: null, promptFlag: '--prompt' }, CWD, PROMPT, + ); + assert.equal(result.ok, true); + assert.deepEqual(result.args, ['--prompt', PROMPT]); + assert.equal(result.cwd, CWD, 'cwd is returned even with no cwd flag — caller binds it on the subprocess'); + }); + + test('omitting prompt is byte-identical to the Phase-2 two-arg resolution', () => { + const descriptor = { command: 'codex', args: ['exec'], cwdFlag: '--cd', promptFlag: null }; + assert.deepEqual( + resolveOrchestratorExec(descriptor, CWD), + resolveOrchestratorExec(descriptor, CWD, undefined), + ); + assert.deepEqual(resolveOrchestratorExec(descriptor, CWD).args, ['exec', '--cd', CWD]); + }); + + test('empty prompt → invalid_prompt (a prompt-less executor hangs, not degrades)', () => { + const result = resolveOrchestratorExec({ command: 'codex', args: ['exec'] }, CWD, ''); + assert.equal(result.ok, false); + assert.equal(result.reason, 'invalid_prompt'); + }); + + test('non-string promptFlag → invalid_prompt_flag', () => { + for (const bogus of [42, {}, []]) { + const result = resolveOrchestratorExec( + { command: 'codex', args: ['exec'], promptFlag: bogus }, CWD, PROMPT, + ); + assert.equal(result.ok, false, `promptFlag=${JSON.stringify(bogus)} must fail`); + assert.equal(result.reason, 'invalid_prompt_flag'); + } + }); + + // Parity with worktree-safety.cts's `unsafe_leading_dash` guard on git args: + // a dash-leading positional is parsed by the spawned CLI as a flag, not a + // value. Same hazard, same rejection — these two surfaces must not diverge. + test('a dash-leading prompt is rejected (would be parsed as a flag, not a prompt)', () => { + for (const hostile of ['--dangerously-skip-permissions', '-p', '--help']) { + const result = resolveOrchestratorExec( + { command: 'codex', args: ['exec'], cwdFlag: '--cd' }, CWD, hostile, + ); + assert.equal(result.ok, false, `prompt=${hostile} must be rejected`); + assert.equal(result.reason, 'unsafe_leading_dash_prompt'); + } + }); + + test('a dash-leading cwd is rejected', () => { + const result = resolveOrchestratorExec( + { command: 'codex', args: ['exec'], cwdFlag: '--cd' }, '-oProxyCommand=x', 'ok prompt', + ); + assert.equal(result.ok, false); + assert.equal(result.reason, 'unsafe_leading_dash_cwd'); + }); + + test('a prompt merely CONTAINING a dash is fine — only a leading dash is a flag', () => { + const result = resolveOrchestratorExec( + { command: 'codex', args: ['exec'], cwdFlag: '--cd' }, CWD, 'Execute plan 2 --verbose style', + ); + assert.equal(result.ok, true); + assert.ok(result.args.includes('Execute plan 2 --verbose style')); + }); + + test('empty-string promptFlag falls back to positional rather than emitting a bare ""', () => { + const result = resolveOrchestratorExec( + { command: 'codex', args: ['exec'], promptFlag: '' }, CWD, PROMPT, + ); + assert.equal(result.ok, true); + assert.deepEqual(result.args, ['exec', PROMPT]); + }); +}); + describe('resolveOrchestratorExec — fail-closed', () => { test('undefined descriptor → missing_command', () => { const result = resolveOrchestratorExec(undefined, '/repo/wt'); @@ -1447,7 +1553,13 @@ describe('resolveOrchestratorExec — fast-check property test', () => { fc.constant(undefined), fc.string({ minLength: 1 }).filter((s) => s.length > 0), ); - const cwdArb = fc.string({ minLength: 1 }).filter((s) => s.length > 0); + // #2627: a dash-leading cwd is now REJECTED (unsafe_leading_dash_cwd) — + // the spawned CLI would parse it as a flag, the same hazard worktree-safety's + // git-argument guard rejects. That is intentional new fail-closed behavior, + // so the ok:true property below is stated over the domain it actually holds + // on: real working directories. The rejected half is asserted explicitly in + // its own property immediately after, so narrowing here loses no coverage. + const cwdArb = fc.string({ minLength: 1 }).filter((s) => s.length > 0 && !s.startsWith('-')); test('property: ok:true, command preserved, cwdFlag appended exactly once (or never for null/absent)', () => { fc.assert( @@ -1475,6 +1587,43 @@ describe('resolveOrchestratorExec — fast-check property test', () => { { numRuns: 200, seed: 2584 }, ); }); + + // The complementary half of the narrowed domain above (#2627): every + // dash-leading cwd fails closed, for ANY descriptor shape. + test('property: a dash-leading cwd is always rejected, never silently passed through', () => { + fc.assert( + fc.property( + commandArb, + argsArb, + cwdFlagArb, + fc.string().map((s) => `-${s}`), + (command, args, cwdFlag, cwd) => { + const descriptor = cwdFlag === undefined ? { command, args } : { command, args, cwdFlag }; + const result = resolveOrchestratorExec(descriptor, cwd); + assert.equal(result.ok, false); + assert.equal(result.reason, 'unsafe_leading_dash_cwd'); + }, + ), + { numRuns: 200, seed: 2627 }, + ); + }); + + // Same shape for the prompt argument, which the resolver appends to argv. + test('property: a dash-leading prompt is always rejected', () => { + fc.assert( + fc.property( + commandArb, + argsArb, + fc.string().map((s) => `-${s}`), + (command, args, prompt) => { + const result = resolveOrchestratorExec({ command, args }, '/repo/wt', prompt); + assert.equal(result.ok, false); + assert.equal(result.reason, 'unsafe_leading_dash_prompt'); + }, + ), + { numRuns: 200, seed: 2627 }, + ); + }); }); describe('#2584 orchestratorExec — parity / divergence guard', () => { @@ -1511,6 +1660,78 @@ describe('#2584 orchestratorExec — parity / divergence guard', () => { // silently matching zero capabilities and passing vacuously). assert.deepEqual(orchestratorWorktreeHosts.sort(), ['codex', 'kimi', 'kimi-code', 'opencode']); }); + + // #2627: the two guards below encode the per-host research that found two + // shipped descriptors which would have failed at spawn time — kimi resolving + // to an interactive TUI (orchestrator hangs) and kimi-code naming a binary + // that does not exist (ENOENT). + test('every orchestrator-worktree descriptor resolves to a HEADLESS invocation, never an interactive TUI', () => { + // A host that binds cwd by flag alone, with no leading subcommand or + // headless flag, launches its interactive UI. Each host must contribute at + // least one non-cwd token (a subcommand like `exec`/`run`, or an explicit + // headless flag like `--print`) BEFORE the cwd flag — or bind by process + // cwd only, which implies a prompt flag carries the instruction. + const expected = { + codex: ['exec'], + opencode: ['run'], + kimi: ['--print'], + 'kimi-code': [], + }; + for (const [id, leadingArgs] of Object.entries(expected)) { + const cap = loadCapability(id); + const oe = cap.runtime.orchestratorExec; + assert.deepEqual(oe.args, leadingArgs, + `${id}: orchestratorExec.args must be ${JSON.stringify(leadingArgs)} — an empty/verb-less argv for a ` + + `flag-bound host launches the interactive TUI and the orchestrator waits on it forever`); + const resolved = resolveOrchestratorExec(oe, '/tmp/wt', 'do the thing'); + assert.equal(resolved.ok, true, `${id}: must resolve with a prompt`); + assert.ok(resolved.args.includes('do the thing'), + `${id}: the executor prompt must reach the argv, else the spawned process has no instruction`); + } + }); + + test('kimi and kimi-code both spawn the "kimi" binary — kimi-code is a package name, not a binary', () => { + // Moonshot ships both agents as a binary named `kimi`; `kimi-code` is the + // npm package / config-home name only. Declaring command:"kimi-code" is an + // immediate ENOENT at spawn. + for (const id of ['kimi', 'kimi-code']) { + assert.equal(loadCapability(id).runtime.orchestratorExec.command, 'kimi', + `${id}: orchestratorExec.command must be the real binary name "kimi"`); + } + }); + + test('every capability whose dispatch.isolation is "harness-worktree" declares a non-empty harnessIsolationFlag', () => { + const capIds = fs.readdirSync(CAPABILITIES_DIR).filter((entry) => ( + fs.existsSync(path.join(CAPABILITIES_DIR, entry, 'capability.json')) + )); + const harnessHosts = []; + for (const id of capIds) { + const cap = loadCapability(id); + const iso = cap?.runtime?.hostIntegration?.dispatch?.isolation; + if (iso !== 'harness-worktree') continue; + harnessHosts.push(id); + const flag = cap.runtime.harnessIsolationFlag; + assert.ok( + typeof flag === 'string' && flag.length > 0, + `${id}: dispatch.isolation:"harness-worktree" but no runtime.harnessIsolationFlag — the scheduler ` + + `would have nothing to pass and would dispatch UNISOLATED executors believing they are isolated`, + ); + } + assert.deepEqual(harnessHosts.sort(), ['claude', 'cursor']); + }); + + test('no capability declares BOTH isolation mechanisms (they are mutually exclusive models)', () => { + const capIds = fs.readdirSync(CAPABILITIES_DIR).filter((entry) => ( + fs.existsSync(path.join(CAPABILITIES_DIR, entry, 'capability.json')) + )); + for (const id of capIds) { + const cap = loadCapability(id); + const hasHarness = typeof cap?.runtime?.harnessIsolationFlag === 'string'; + const hasOrchestrator = cap?.runtime?.orchestratorExec !== undefined; + assert.ok(!(hasHarness && hasOrchestrator), + `${id}: declares both harnessIsolationFlag and orchestratorExec — a host has exactly one fan-out model`); + } + }); }); describe('#2584 orchestratorExec — validator', () => { @@ -1615,3 +1836,101 @@ describe('#2584 orchestratorExec — validator', () => { } }); }); + +// --------------------------------------------------------------------------- +// #2627 Phase 3 — the `dispatch-isolation` CLI route. +// +// Behavioral: each case SPAWNS the real gsd-tools CLI and asserts on its actual +// stdout, rather than inspecting the route's source. This is the scheduler +// consumer's only entry point — execute-phase branches on exactly this output. +// --------------------------------------------------------------------------- +describe('#2627 dispatch-isolation CLI route', () => { + const { execFileSync } = require('node:child_process'); + const GSD_TOOLS = path.join(REPO_ROOT, 'gsd-core', 'bin', 'gsd-tools.cjs'); + + function query(runtimeId, extraArgs = []) { + return execFileSync( + process.execPath, + [GSD_TOOLS, 'query', 'dispatch-isolation', ...extraArgs], + { cwd: REPO_ROOT, encoding: 'utf8', env: { ...process.env, GSD_RUNTIME: runtimeId } }, + ); + } + const queryJson = (runtimeId, extraArgs = []) => JSON.parse(query(runtimeId, ['--json', ...extraArgs])); + + test('raw output is the bare negotiated value for each isolation model', () => { + assert.equal(query('claude').trim(), 'harness-worktree'); + assert.equal(query('codex').trim(), 'orchestrator-worktree'); + assert.equal(query('pi').trim(), 'none'); + }); + + test('an undocumented isolation declaration degrades to none (fail-closed)', () => { + // cline declares isolation:"undocumented" — the corpus-wide sentinel. + assert.equal(query('cline').trim(), 'none'); + }); + + test('an unknown runtime degrades to none rather than erroring', () => { + assert.equal(query('no-such-runtime-xyz').trim(), 'none'); + }); + + test('harness-worktree surfaces the host\'s declared flag, and no exec', () => { + const claude = queryJson('claude'); + assert.equal(claude.isolation, 'harness-worktree'); + assert.equal(claude.harnessFlag, 'isolation="worktree"'); + assert.equal(claude.exec, null, 'GSD runs no git on the harness path — there is nothing to spawn'); + + assert.equal(queryJson('cursor').harnessFlag, '--worktree'); + }); + + test('orchestrator-worktree yields exec only when a cwd target is supplied', () => { + assert.equal(queryJson('codex').exec, null, 'no --cwd-target → nothing to bind'); + + const withTarget = queryJson('codex', ['--cwd-target', '/tmp/wt', '--prompt', 'do the thing']); + assert.equal(withTarget.isolation, 'orchestrator-worktree'); + assert.equal(withTarget.exec.command, 'codex'); + assert.deepEqual(withTarget.exec.args, ['exec', '--cd', '/tmp/wt', 'do the thing']); + assert.equal(withTarget.exec.cwd, '/tmp/wt'); + assert.equal(withTarget.harnessFlag, null); + }); + + test('each orchestrator-worktree host resolves to its own documented argv shape', () => { + const args = (id) => queryJson(id, ['--cwd-target', '/tmp/wt', '--prompt', 'P']).exec.args; + assert.deepEqual(args('opencode'), ['run', '--dir', '/tmp/wt', 'P']); + // kimi carries the prompt behind --prompt (its --print mode requires it), + // unlike codex/opencode which take it positionally. + assert.deepEqual(args('kimi'), ['--print', '--work-dir', '/tmp/wt', '--prompt', 'P']); + // kimi-code binds by process cwd — no flag in argv, but cwd still returned. + assert.deepEqual(args('kimi-code'), ['--prompt', 'P']); + assert.equal(queryJson('kimi-code', ['--cwd-target', '/tmp/wt', '--prompt', 'P']).exec.cwd, '/tmp/wt'); + }); + + test('a cwd target with no prompt still resolves (prompt is optional at this seam)', () => { + const noPrompt = queryJson('codex', ['--cwd-target', '/tmp/wt']); + assert.equal(noPrompt.isolation, 'orchestrator-worktree'); + assert.deepEqual(noPrompt.exec.args, ['exec', '--cd', '/tmp/wt']); + }); + + test('the route never reports an isolation model it cannot actually service', () => { + // The invariant the scheduler depends on: if isolation is non-none, the + // corresponding mechanism is present. Anything else would have the wave + // create a worktree and then discover it has nothing to spawn into it. + for (const id of ['claude', 'cursor', 'codex', 'opencode', 'kimi', 'kimi-code', 'pi', 'cline', 'zcode']) { + const r = queryJson(id, ['--cwd-target', '/tmp/wt', '--prompt', 'P']); + if (r.isolation === 'harness-worktree') { + assert.ok(r.harnessFlag && r.harnessFlag.length > 0, `${id}: harness-worktree without a flag`); + } else if (r.isolation === 'orchestrator-worktree') { + assert.ok(r.exec && r.exec.command, `${id}: orchestrator-worktree without a spawnable exec`); + } else { + assert.equal(r.isolation, 'none', `${id}: unexpected isolation value ${r.isolation}`); + assert.equal(r.exec, null); + assert.equal(r.harnessFlag, null); + } + } + }); + + test('a none-isolation host never yields an exec even when a target is supplied', () => { + const pi = queryJson('pi', ['--cwd-target', '/tmp/wt', '--prompt', 'P']); + assert.equal(pi.isolation, 'none'); + assert.equal(pi.exec, null); + assert.equal(pi.harnessFlag, null); + }); +}); diff --git a/tests/runtime-converters.test.cjs b/tests/runtime-converters.test.cjs index c8f21a5fa..0992e3289 100644 --- a/tests/runtime-converters.test.cjs +++ b/tests/runtime-converters.test.cjs @@ -1182,7 +1182,12 @@ test('property: _stampNonClaudeRuntimeDefaults is idempotent (#1521)', () => { test('execute-phase.md, quick.md, and diagnose-issues.md guards are generalized to != "claude" (not Codex-specific) (#1521)', () => { // allow-test-rule: emitted workflow runtime-resolution shell block is the runtime contract surface (#1521) - const GUARD_WORKFLOWS = ['execute-phase.md', 'quick.md', 'diagnose-issues.md']; + // #2584 Phase 3 (#2627): execute-phase.md graduated PAST the `!= "claude"` + // guard — worktree isolation there is now keyed on the negotiated + // `dispatch.isolation` capability, so no runtime name appears in its guard at + // all. quick.md and diagnose-issues.md still use the #1521 generalized form + // (they do not negotiate isolation), so they keep asserting it. + const GUARD_WORKFLOWS = ['quick.md', 'diagnose-issues.md']; for (const wf of GUARD_WORKFLOWS) { const src = fs.readFileSync( path.join(__dirname, '..', 'gsd-core', 'workflows', wf), @@ -1197,6 +1202,20 @@ test('execute-phase.md, quick.md, and diagnose-issues.md guards are generalized `${wf}: found Codex-specific guard — should have been generalized to != "claude"`, ); } + + // execute-phase.md: the guard must be capability-keyed, with NO runtime name. + const executePhase = fs.readFileSync( + path.join(__dirname, '..', 'gsd-core', 'workflows', 'execute-phase.md'), + 'utf8', + ); + assert.ok( + !/\[\s*"\$RUNTIME"\s*(?:!=|=)\s*"[a-z-]+"\s*\]\s*&&\s*\[\s*"\$USE_WORKTREES"/.test(executePhase), + 'execute-phase.md: worktree isolation must branch on dispatch.isolation, not on a runtime name (#2584)', + ); + assert.ok( + executePhase.includes('Resolve ISOLATION'), + 'execute-phase.md: must resolve the negotiated ISOLATION capability', + ); }); // --------------------------------------------------------------------------- diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index 3da1eac89..8c20d8a65 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -24,7 +24,7 @@ "docs-update.md": 56494, "edit-phase.md": 12927, "eval-review.md": 10332, - "execute-phase.md": 93277, + "execute-phase.md": 93263, "execute-plan.md": 35143, "explore.md": 11127, "extract-learnings.md": 12893, diff --git a/tests/worktree-safety.test.cjs b/tests/worktree-safety.test.cjs index 64cf791c9..ad06010dd 100644 --- a/tests/worktree-safety.test.cjs +++ b/tests/worktree-safety.test.cjs @@ -1330,6 +1330,77 @@ describe('cmdWorktreeCreate', () => { assert.match(out.join(''), /"ok": true/); }); + // #2627 Phase 3: --root confines the created worktree. Absent the flag the + // behavior is exactly Phase 2's (every test above passes unchanged); the + // orchestrator-worktree scheduler path always passes it, because Phase 3 is + // what starts SPAWNING processes into these directories. + describe('--root confinement', () => { + const rootedArgs = (wtPath, root) => [ + '--manifest', 'manifest.json', + '--agent-id', 'a1', + '--path', wtPath, + '--branch', 'worktree-agent-a1', + '--base', 'abc123', + '--root', root, + ]; + + function run(args) { + const out = []; + let gitCalled = false; + const result = withExitCode(() => cmdWorktreeCreate('/repo/main', args, { + readFile: () => '{"orchestrator_root":"/repo/main","worktrees":[]}', + writeFile: () => {}, + write: (s) => out.push(s), + writeErr: () => {}, + execGit: () => { gitCalled = true; return { exitCode: 0, stdout: '', stderr: '', timedOut: false }; }, + })); + return { result, out: out.join(''), gitCalled }; + } + + test('a path inside --root is accepted', () => { + const { result } = run(rootedArgs('/repo/main/.claude/worktrees/agent-a1', '/repo/main')); + assert.equal(result.ok, true); + assert.equal(result.reason, 'created'); + }); + + test('a sibling path OUTSIDE --root is rejected before any git runs', () => { + const { result, gitCalled } = run(rootedArgs('/repo/.claude/worktrees/agent-a1', '/repo/main')); + assert.equal(result.ok, false); + assert.equal(result.reason, 'path_outside_root'); + assert.equal(gitCalled, false, 'confinement must reject BEFORE the git side effect'); + }); + + test('an arbitrary absolute path is rejected (the hole a ".."-segment check cannot see)', () => { + const { result } = run(rootedArgs('/etc/gsd-evil', '/repo/main')); + assert.equal(result.ok, false); + assert.equal(result.reason, 'path_outside_root'); + }); + + test('a path EQUAL to --root is rejected (would clobber the checkout)', () => { + const { result } = run(rootedArgs('/repo/main', '/repo/main')); + assert.equal(result.ok, false); + assert.equal(result.reason, 'path_outside_root'); + }); + + test('a sibling whose name merely PREFIXES the root is rejected (not a substring check)', () => { + // '/repo/main-evil' starts with '/repo/main' textually but is not inside it. + const { result } = run(rootedArgs('/repo/main-evil/wt', '/repo/main')); + assert.equal(result.ok, false); + assert.equal(result.reason, 'path_outside_root'); + }); + + test('omitting --root preserves Phase-2 behavior (no confinement)', () => { + const { result } = run([ + '--manifest', 'manifest.json', + '--agent-id', 'a1', + '--path', '/repo/.claude/worktrees/agent-a1', + '--branch', 'worktree-agent-a1', + '--base', 'abc123', + ]); + assert.equal(result.ok, true, 'no --root → unchanged Phase-2 acceptance'); + }); + }); + test('boundary: appending to a manifest with 1 existing entry yields 2', () => { let writtenContent = null; const result = cmdWorktreeCreate('/repo/main', okArgs, { @@ -2841,16 +2912,27 @@ describe('bug-3707: startup orphan sweep is wired into workflow entry points', ( ); }); - test('execute-phase.md calls worktree.reap-orphans at startup when USE_WORKTREES is not false', () => { - const content = fs.readFileSync(EXECUTE_PHASE_PATH, 'utf8'); + test('execute-phase.md calls worktree.reap-orphans at startup, guarded by the isolation decision', () => { + // #2584 Phase 3 (#2627): the startup sweep moved into the isolation-dispatch + // fragment alongside the ISOLATION resolution it is guarded by (the host + // workflow keeps only a pointer, per the ADR-857 byte budget). The guard is + // now `ISOLATION != none`, which USE_WORKTREES=false forces — so the #3707 + // protection is unchanged, just keyed one level up. + const ISOLATION_FRAGMENT_PATH = path.join( + __dirname, '..', 'gsd-core', 'workflows', 'execute-phase', 'steps', 'executor-isolation-dispatch.md', + ); + const content = fs.readFileSync(EXECUTE_PHASE_PATH, 'utf8') + + fs.readFileSync(ISOLATION_FRAGMENT_PATH, 'utf8'); assert.ok( content.includes('worktree.reap-orphans'), - 'execute-phase.md must call gsd-sdk query worktree.reap-orphans at startup' + 'execute-phase must call gsd-sdk query worktree.reap-orphans at startup' ); assert.ok( /USE_WORKTREES.*!=.*false[\s\S]{0,200}worktree\.reap-orphans/m.test(content) || - /worktree\.reap-orphans[\s\S]{0,200}USE_WORKTREES.*!=.*false/m.test(content), - 'execute-phase.md startup sweep must be guarded by USE_WORKTREES != false' + /worktree\.reap-orphans[\s\S]{0,200}USE_WORKTREES.*!=.*false/m.test(content) || + /ISOLATION.*!=.*none[\s\S]{0,200}worktree\.reap-orphans/m.test(content) || + /worktree\.reap-orphans[\s\S]{0,200}ISOLATION.*!=.*none/m.test(content), + 'execute-phase startup sweep must be guarded by USE_WORKTREES != false or ISOLATION != none' ); });