fix(#1521): resolve own runtime + worktrees-off for all non-Claude installs (#1537)

* fix(#1521): resolve own runtime + worktrees-off for all non-Claude installs

Generalizes the Codex-only #1515/#1519 fix to every non-Claude runtime, and
wires it into the real install path (where it was previously dead-on-arrival).

Root causes:
1. The runtime-default stamping lived only in `_applyRuntimeRewrites`, but the
   installer emits `gsd-core/workflows/*.md` via `copyWithPathReplacement`, which
   never calls it — so a real `--codex`/`--cursor`/etc. install emitted
   `--default claude` and worktrees-on. RUNTIME mis-resolved to claude and the
   workflow ran executors unisolated against the main checkout. (#1515/#1519 were
   also dead-on-arrival in real installs; this repairs them.)
2. Only `case 'codex'` was stamped; every other non-Claude runtime kept the
   Claude default.

Fix:
- New `_stampNonClaudeRuntimeDefaults(content, runtime)` (single shared helper)
  stamps `--default <runtime>` + `use_worktrees=false` for every `runtime !=
  claude`; called from both `_applyRuntimeRewrites` and, crucially,
  `copyWithPathReplacement` in bin/install.js (the real workflow emit path).
- Generalize the fail-closed worktree guard `= codex` -> `!= claude` in
  execute-phase/quick/diagnose-issues (worktree isolation is Claude-Code-only).
- Flip manager/autonomous inline-vs-background gating to `codex -> background,
  everything-else -> inline` (research: only Codex can background-nest the
  pipeline's subagents; all others run inline, which they support).

Worktree-capability determination is research-backed (official docs for all 14
non-Claude runtimes: none honor GSD's isolation="worktree" mechanism, only Codex
background-nests). New end-to-end real-install test asserts the EMITTED workflow
is stamped — the regression guard that would have caught the dead-on-arrival bug.

Closes #1521

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013vX5eUtWa2wsZEyeMf5i3r

* chore(#1521): backfill changeset PR number (#1537)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013vX5eUtWa2wsZEyeMf5i3r

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-21 13:48:47 -04:00
committed by GitHub
parent 2436b76980
commit 2c718bf972
15 changed files with 514 additions and 114 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 1537
---
**Non-Claude runtime installs now resolve their own runtime and never attempt Claude-only worktree isolation** — on any non-Claude install (Cursor, Gemini, Qwen, etc.) a runtime-neutral `.planning/config.json` previously resolved `runtime=claude` and enabled git worktree isolation, which only Claude Code's `isolation="worktree"` can honor — risking main-checkout edits while the workflow believed agents were isolated. Every non-Claude install now resolves its own runtime identity, defaults `workflow.use_worktrees` to `false`, fails closed if worktrees are forced on, and runs plan/execute inline in the manager/autonomous flows since only Codex can background-nest the pipeline's subagents. (#1521)

View File

@@ -6698,6 +6698,7 @@ function migrateLegacyDevPreferencesToSkill(targetDir, saved, runtime, scope = '
// reference-identical to the conversion module (consistent with the walkers above).
// All call sites are below this line → no TDZ hazard.
const _applyRuntimeRewrites = runtimeArtifactConversion._applyRuntimeRewrites;
const _stampNonClaudeRuntimeDefaults = runtimeArtifactConversion._stampNonClaudeRuntimeDefaults;
/**
* Copy a staged directory's contents into destDir.
@@ -7289,6 +7290,15 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
}
content = processAttribution(content, getCommitAttribution(runtime));
// #1521: stamp the workflow runtime-resolution block so every non-Claude
// install resolves its own runtime identity and defaults use_worktrees=false.
// copyWithPathReplacement is the emit path for gsd-core/workflows/*.md;
// _applyRuntimeRewrites is NOT invoked here, so this is what makes the fix
// live in real installs (it is a no-op for files without those lines).
if (runtime !== 'claude') {
content = _stampNonClaudeRuntimeDefaults(content, runtime);
}
// #3683 — normalize /gsd:<cmd> → /gsd-<cmd> in any body passing through
// copyWithPathReplacement for runtimes that register commands under the
// hyphen form; normalizeAgentBodyForRuntime self-gates on

View File

@@ -248,7 +248,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. **Codex note:** Codex maps subagents to `spawn_agent` and cannot honor Claude Code's `isolation="worktree"`, so a Codex-installed workflow resolves its runtime as `codex` and defaults this key to `false` even when `.planning/config.json` is runtime-neutral; forcing `use_worktrees: true` on a Codex install fails closed before any executor dispatch (#1515). |
| `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, Gemini, 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.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 |

View File

@@ -61,7 +61,7 @@ fi
When `--only` is set, also set `FROM_PHASE` to the same value so existing filter logic applies.
When `--interactive` is set, discuss runs inline with questions (not auto-answered). On runtimes where a backgrounded agent can spawn subagents, plan and execute are dispatched as background agents — keeping the main context lean (only discuss conversations accumulate) and enabling overlap. On Claude Code, where a backgrounded agent cannot nest subagents, plan and execute run inline to preserve worktree isolation and independent verification, so they run sequentially and their work accumulates in the main context. Either way, user input is preserved on all design decisions.
When `--interactive` is set, discuss runs inline with questions (not auto-answered). On Codex, where a backgrounded agent can still spawn subagents, plan and execute are dispatched as background agents — keeping the main context lean (only discuss conversations accumulate) and enabling overlap. On every other runtime (Claude Code and all other non-Codex runtimes), backgrounded agents cannot reliably nest subagents, so plan and execute run inline to preserve worktree isolation and independent verification, and phases run sequentially with their work accumulating in the main context. Either way, user input is preserved on all design decisions.
When `PLAN_STRATEGY=converge`, the planning step MUST invoke the plan-review convergence workflow instead of `gsd-plan-phase`. `--cross-ai` is an alias for `--converge`. Forward `CONVERGENCE_ARGS` exactly as parsed so reviewer flags and `--max-cycles N` retain the same meaning as they have on `/gsd:plan-review-convergence`.
@@ -111,7 +111,7 @@ Display startup banner:
If `ONLY_PHASE` is set, display: `Single phase mode: Phase ${ONLY_PHASE}`
Else if `FROM_PHASE` is set, display: `Starting from phase ${FROM_PHASE}`
If `TO_PHASE` is set, display: `Stopping after phase ${TO_PHASE}`
If `INTERACTIVE` is set, display: `Mode: Interactive (discuss inline, plan+execute in background)`
If `INTERACTIVE` is set, display: `Mode: Interactive (discuss inline, plan+execute inline — background on Codex only)`
If `PLAN_STRATEGY` is `converge`, display: `Planning: Plan-review convergence enabled`
</step>
@@ -357,27 +357,13 @@ UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
**3b. Plan**
**If `INTERACTIVE` is set:** Background dispatch is only safe where a backgrounded agent can still spawn subagents. On Claude Code a backgrounded agent has no `Agent`/`Task` tool, so the plan-checker never runs and `workflow.plan_check` silently degrades to a self-check. Resolve the runtime first:
**If `INTERACTIVE` is set:** Background dispatch is only safe on a runtime where a backgrounded agent can still nest the pipeline's subagents (plan-checker / worktree executors / verifier). Among supported runtimes only **Codex** (`spawn_agent`) can do this; Claude Code's backgrounded agents have no `Agent`/`Task` tool, and every other runtime either prohibits nested subagents or disables them by default. So run **inline** everywhere except Codex, which is dispatched in the background. Resolve the runtime first:
```bash
RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")
```
- **On Claude Code (`RUNTIME` is `claude`):** Run plan **inline** (do NOT background) so the plan-checker runs. The next phase's discuss does not overlap planning here — correctness over overlap.
- If `PLAN_STRATEGY=converge`:
```
Skill(skill="gsd-plan-review-convergence", args="${PHASE_NUM} ${CONVERGENCE_ARGS}")
```
- Otherwise (local planning):
```
Skill(skill="gsd-plan-phase", args="${PHASE_NUM}")
```
- **On other runtimes:** Dispatch plan as a background agent to keep the main context lean. While plan runs, the workflow can immediately start discussing the next phase (see step 4).
- **If `RUNTIME` is `codex`:** Dispatch plan as a background agent to keep the main context lean. While plan runs, the workflow can immediately start discussing the next phase (see step 4).
- If `PLAN_STRATEGY=converge`, print: `◆ Spawning background plan-convergence loop for phase ${PHASE_NUM}... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)`
@@ -401,6 +387,20 @@ RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null ||
Store the agent task_id. After discuss for the next phase completes (or if no next phase), wait for the plan agent to finish before proceeding to execute.
- **Otherwise (Claude Code or any other non-Codex runtime):** Run plan **inline** (do NOT background) so the plan-checker runs. The next phase's discuss does not overlap planning here — correctness over overlap.
- If `PLAN_STRATEGY=converge`:
```
Skill(skill="gsd-plan-review-convergence", args="${PHASE_NUM} ${CONVERGENCE_ARGS}")
```
- Otherwise (local planning):
```
Skill(skill="gsd-plan-phase", args="${PHASE_NUM}")
```
**If `INTERACTIVE` is NOT set (default):** Run plan inline.
If `PLAN_STRATEGY=converge`, run the convergence loop:
@@ -419,19 +419,13 @@ Verify plan produced output — re-run `init phase-op` and check `has_plans`. If
**3c. Execute**
**If `INTERACTIVE` is set:** Wait for the plan agent to complete (if not already) and verify plans exist. Background dispatch is only safe where a backgrounded agent can still spawn subagents. On Claude Code a backgrounded agent has no `Agent`/`Task` tool, so the per-plan worktree-isolated executors and the verifier never run (`workflow.use_worktrees` and `workflow.verifier` silently degrade). Resolve the runtime first:
**If `INTERACTIVE` is set:** Wait for the plan agent to complete (if not already) and verify plans exist. Background dispatch is only safe on a runtime where a backgrounded agent can still nest the pipeline's subagents (plan-checker / worktree executors / verifier). Among supported runtimes only **Codex** (`spawn_agent`) can do this; Claude Code's backgrounded agents have no `Agent`/`Task` tool, and every other runtime either prohibits nested subagents or disables them by default. So run **inline** everywhere except Codex, which is dispatched in the background. Resolve the runtime first:
```bash
RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")
```
- **On Claude Code (`RUNTIME` is `claude`):** Run execute **inline** (do NOT background) so worktree isolation and verification run:
```
Skill(skill="gsd-execute-phase", args="${PHASE_NUM} --no-transition")
```
- **On other runtimes:** Dispatch execute as a background agent:
- **If `RUNTIME` is `codex`:** Dispatch execute as a background agent:
```
Agent(
@@ -443,6 +437,12 @@ Agent(
Store the agent task_id. The workflow can now start discussing the next phase while this phase executes in the background. Before starting post-execution routing for this phase, wait for the execute agent to complete.
- **Otherwise (Claude Code or any other non-Codex runtime):** Run execute **inline** (do NOT background) so worktree isolation and verification run:
```
Skill(skill="gsd-execute-phase", args="${PHASE_NUM} --no-transition")
```
**If `INTERACTIVE` is NOT set (default):** Run execute inline as before.
```
@@ -656,12 +656,12 @@ Check for blockers in the Blockers/Concerns section. If blockers are found, go t
If incomplete phases remain: proceed to next phase, loop back to execute_phase.
**Interactive mode overlap:** When `INTERACTIVE` is set, the iterate step enables pipeline parallelism **on runtimes where a backgrounded agent can spawn subagents** (on Claude Code, plan/execute run inline — see 3b/3c — so there is no overlap and phases run sequentially):
**Interactive mode overlap:** When `INTERACTIVE` is set, the iterate step enables pipeline parallelism **on Codex** (on every other runtime, plan/execute run inline — see 3b/3c — so there is no overlap and phases run sequentially):
1. After discuss completes for Phase N, dispatch plan+execute as background agents
2. Immediately start discuss for Phase N+1 (the next incomplete phase) while Phase N builds
3. Before starting plan for Phase N+1, wait for Phase N's execute agent to complete and handle its post-execution routing (verification, gap closure, etc.)
This means the user is always answering discuss questions (lightweight, interactive) while the heavy work (planning, code generation) runs in the background. The main context only accumulates discuss conversations — plan and execute contexts are isolated in their agents. (On Claude Code, plan and execute run inline, so they run sequentially and their work accumulates in the main context.)
This means the user is always answering discuss questions (lightweight, interactive) while the heavy work (planning, code generation) runs in the background. The main context only accumulates discuss conversations — plan and execute contexts are isolated in their agents. (On Claude Code and all other non-Codex runtimes, plan and execute run inline, so they run sequentially and their work accumulates in the main context.)
If all phases complete, proceed to lifecycle step.
@@ -873,9 +873,9 @@ When any phase operation fails or a blocker is detected, present 3 options via A
- [ ] `--to N` handle_blocker resume message preserves --to flag
- [ ] `--to N` skips lifecycle when not all milestone phases complete
- [ ] `--interactive` runs discuss inline via gsd-discuss-phase (asks questions, waits for user)
- [ ] `--interactive` dispatches plan and execute as background agents on runtimes that support nested background dispatch; runs them inline on Claude Code
- [ ] `--interactive` enables pipeline parallelism (discuss Phase N+1 while Phase N builds) on runtimes with background dispatch; phases run sequentially on Claude Code
- [ ] `--interactive` main context only accumulates discuss conversations on runtimes with background dispatch (on Claude Code, inline plan/execute also accumulate)
- [ ] `--interactive` dispatches plan and execute as background agents on Codex (the only runtime where a backgrounded agent can nest subagents); runs them inline on all other runtimes
- [ ] `--interactive` enables pipeline parallelism (discuss Phase N+1 while Phase N builds) on Codex; phases run sequentially on all other runtimes
- [ ] `--interactive` main context only accumulates discuss conversations on Codex (on all other runtimes, inline plan/execute also accumulate)
- [ ] `--interactive` waits for background agents before post-execution routing
- [ ] `--interactive` compatible with `--only`, `--from`, and `--to` flags
- [ ] `--converge` routes planning through `gsd-plan-review-convergence`

View File

@@ -61,8 +61,8 @@ gaps = [
_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 "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$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
USE_WORKTREES=$(gsd_run query config-get workflow.use_worktrees --raw 2>/dev/null || echo "true")
RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")
if [ "$RUNTIME" = "codex" ] && [ "$USE_WORKTREES" != "false" ]; then
echo "FATAL: Codex worktree isolation is unsupported. Set workflow.use_worktrees=false or use a runtime with Agent isolation=\"worktree\" support." >&2
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
```

View File

@@ -96,8 +96,8 @@ 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" = "codex" ] && [ "$USE_WORKTREES" != "false" ]; then
echo "FATAL: Codex execute-phase worktree isolation is unsupported. Set workflow.use_worktrees=false or use a runtime with Agent isolation=\"worktree\" support." >&2
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).
@@ -113,7 +113,7 @@ if [ "$RUNTIME" = "claude" ] && [ "$USE_WORKTREES" != "false" ]; then
fi
fi
```
Codex maps subagents to `spawn_agent`, which has no direct Codex mapping for Claude Code's `isolation="worktree"` parameter. Failing closed prevents main-checkout edits while the workflow believes agents are isolated.
`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.
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. The previous behavior unconditionally disabled worktree isolation whenever `.gitmodules` existed, which penalised every plan in a submodule project even when the plan was nowhere near a submodule. Compute submodule paths once and intersect them per-plan with the plan's declared `files_modified` frontmatter.

View File

@@ -1,6 +1,6 @@
<purpose>
Interactive command center for managing a milestone from a single terminal. Shows a dashboard of all phases with visual status, dispatches discuss inline and plan/execute as background agents, and loops back to the dashboard after each action. Enables parallel phase work from one terminal.
Interactive command center for managing a milestone from a single terminal. Shows a dashboard of all phases with visual status, dispatches discuss inline and runs plan/execute inline (backgrounded only on Codex), and loops back to the dashboard after each action. Enables parallel phase work from one terminal.
</purpose>
@@ -45,7 +45,7 @@ Display startup banner:
{milestone_version} — {milestone_name}
{phase_count} phases · {completed_count} complete
✓ Discuss → inline ◆ Plan/Execute → background
✓ Discuss → inline ◆ Plan/Execute → inline (background on Codex)
Dashboard auto-refreshes when background work is active.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
@@ -221,8 +221,8 @@ Go to exit step.
When the user selects a compound option, behavior depends on the runtime — the Plan Phase N / Execute Phase N handlers below resolve it via `gsd_run query config-get runtime`:
- **On Claude Code:** a backgrounded agent cannot nest the pipeline's subagents, so run the chosen plan/execute step(s) **inline** via their handlers below (in order), then run the inline discuss. There is no overlap.
- **On other runtimes:** **Spawn all background agents first** (plan/execute) — dispatch them in parallel using the Plan Phase N / Execute Phase N handlers below — then run the inline discuss; the background agents continue while you discuss.
- **On Codex:** **Spawn all background agents first** (plan/execute) — dispatch them in parallel using the Plan Phase N / Execute Phase N handlers below — then run the inline discuss; the background agents continue while you discuss.
- **Otherwise (Claude Code or any other non-Codex runtime):** a backgrounded agent cannot reliably nest the pipeline's subagents, so run the chosen plan/execute step(s) **inline** via their handlers below (in order), then run the inline discuss. There is no overlap.
Inline discuss:
@@ -244,27 +244,13 @@ After discuss completes, loop back to dashboard step.
### Plan Phase N
Planning runs autonomously. **First resolve the runtime.** On Claude Code a backgrounded agent has no `Agent`/`Task` tool, so it cannot spawn the plan-checker the pipeline relies on — backgrounding it there silently turns `workflow.plan_check` into a self-check. So run plan **inline** on Claude Code, and **background** it only on runtimes where a backgrounded agent can still nest subagents.
Planning runs autonomously. **First resolve the runtime.** Background dispatch is only safe on a runtime where a backgrounded agent can still nest the pipeline's subagents (plan-checker / worktree executors / verifier). Among supported runtimes only **Codex** (`spawn_agent`) can do this; Claude Code's backgrounded agents have no `Agent`/`Task` tool, and every other runtime either prohibits nested subagents or disables them by default. So run **inline** everywhere except Codex, which is dispatched in the background.
```bash
RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")
```
**If `RUNTIME` is `claude` (Claude Code):** Run plan inline so the plan-checker and quality gates actually run — do NOT wrap it in `Agent(run_in_background=true, …)`:
```
Skill(skill="gsd-plan-phase", args="{N} --auto {manager_flags.plan}")
```
Display while it runs:
```
◆ Planning Phase {N}: {phase_name}... (runs inline so the plan-checker runs — the dashboard resumes when it returns, ~1–5 min; expected, not a freeze)
```
Then loop back to dashboard step.
**If `RUNTIME` is not `claude` (e.g. Codex):** Spawn a background agent that delegates to the Skill pipeline with any configured flags:
**If `RUNTIME` is `codex`:** Spawn a background agent that delegates to the Skill pipeline with any configured flags:
```
Agent(
@@ -286,7 +272,7 @@ Important: You are running in the background. Do NOT use AskUserQuestion — mak
)
```
> **ORCHESTRATOR RULE — NON-CLAUDE RUNTIME**: After calling Agent() above with `run_in_background=true`, do NOT do any planning work for this phase independently. Return to the dashboard immediately and wait for the background agent to report back. Only resume planning-related work when the subagent result is available.
> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above with `run_in_background=true`, do NOT do any planning work for this phase independently. Return to the dashboard immediately and wait for the background agent to report back. Only resume planning-related work when the subagent result is available.
Display:
@@ -296,29 +282,29 @@ Display:
Loop back to dashboard step.
### Execute Phase N
Execution runs autonomously. **First resolve the runtime.** On Claude Code a backgrounded agent has no `Agent`/`Task` tool, so it cannot spawn the per-plan worktree-isolated executors or the verifier — backgrounding it there silently disables `workflow.use_worktrees` isolation and `workflow.verifier`. So run execute **inline** on Claude Code, and **background** it only on runtimes where a backgrounded agent can still nest subagents.
```bash
RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")
```
**If `RUNTIME` is `claude` (Claude Code):** Run execute inline so worktree isolation and the verifier actually run — do NOT wrap it in `Agent(run_in_background=true, …)`:
**Otherwise (Claude Code or any other non-Codex runtime):** Run plan inline so the plan-checker and quality gates actually run — do NOT wrap it in `Agent(run_in_background=true, …)`:
```
Skill(skill="gsd-execute-phase", args="{N} {manager_flags.execute}")
Skill(skill="gsd-plan-phase", args="{N} --auto {manager_flags.plan}")
```
Display while it runs:
```
◆ Executing Phase {N}: {phase_name}... (runs inline so worktree isolation and verification run — the dashboard resumes when it returns; expected, not a freeze)
◆ Planning Phase {N}: {phase_name}... (runs inline so the plan-checker runs — the dashboard resumes when it returns, ~1–5 min; expected, not a freeze)
```
Then loop back to dashboard step.
**If `RUNTIME` is not `claude` (e.g. Codex):** Spawn a background agent that delegates to the Skill pipeline with any configured flags:
### Execute Phase N
Execution runs autonomously. **First resolve the runtime.** Background dispatch is only safe on a runtime where a backgrounded agent can still nest the pipeline's subagents (plan-checker / worktree executors / verifier). Among supported runtimes only **Codex** (`spawn_agent`) can do this; Claude Code's backgrounded agents have no `Agent`/`Task` tool, and every other runtime either prohibits nested subagents or disables them by default. So run **inline** everywhere except Codex, which is dispatched in the background.
```bash
RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")
```
**If `RUNTIME` is `codex`:** Spawn a background agent that delegates to the Skill pipeline with any configured flags:
```
Agent(
@@ -340,7 +326,7 @@ Important: You are running in the background. Do NOT use AskUserQuestion — mak
)
```
> **ORCHESTRATOR RULE — NON-CLAUDE RUNTIME**: After calling Agent() above with `run_in_background=true`, do NOT do any execution work for this phase independently. Return to the dashboard immediately and wait for the background agent to report back. Only resume execution-related work when the subagent result is available.
> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above with `run_in_background=true`, do NOT do any execution work for this phase independently. Return to the dashboard immediately and wait for the background agent to report back. Only resume execution-related work when the subagent result is available.
Display:
@@ -350,6 +336,20 @@ Display:
Loop back to dashboard step.
**Otherwise (Claude Code or any other non-Codex runtime):** Run execute inline so worktree isolation and the verifier actually run — do NOT wrap it in `Agent(run_in_background=true, …)`:
```
Skill(skill="gsd-execute-phase", args="{N} {manager_flags.execute}")
```
Display while it runs:
```
◆ Executing Phase {N}: {phase_name}... (runs inline so worktree isolation and verification run — the dashboard resumes when it returns; expected, not a freeze)
```
Then loop back to dashboard step.
</step>
<step name="background_completion">
@@ -422,8 +422,8 @@ Display final status with progress bar:
- [ ] Dependency resolution: blocked phases show which deps are missing
- [ ] Recommendations prioritize: execute > plan > discuss
- [ ] Discuss phases run inline via Skill() — interactive questions work
- [ ] Plan phases spawn background Task agents — return to dashboard immediately
- [ ] Execute phases spawn background Task agents — return to dashboard immediately
- [ ] Plan phases run inline (or as background Task agents on Codex) — dashboard resumes when complete
- [ ] Execute phases run inline (or as background Task agents on Codex) — dashboard resumes when complete
- [ ] Dashboard refreshes pick up changes from background agents via disk state
- [ ] Background agent completion triggers notification and dashboard refresh
- [ ] Background agent errors present retry/skip options

View File

@@ -139,8 +139,8 @@ Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_mo
```bash
USE_WORKTREES=$(gsd_run query config-get workflow.use_worktrees --raw 2>/dev/null || echo "true")
RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")
if [ "$RUNTIME" = "codex" ] && [ "$USE_WORKTREES" != "false" ]; then
echo "FATAL: Codex worktree isolation is unsupported. Set workflow.use_worktrees=false or use a runtime with Agent isolation=\"worktree\" support." >&2
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
```

View File

@@ -2119,6 +2119,40 @@ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost
return `${resolvedTarget}/`;
}
/**
* Canonical list of every non-Claude runtime that gsd-core emits artifacts for.
* Exported so test files can import this single source of truth rather than
* maintaining divergent hand-rolled arrays (#1521).
*
* Keep in sync with the runtime flags in bin/install.js and getDirName().
*/
const NON_CLAUDE_RUNTIMES: string[] = [
'codex', 'opencode', 'kilo', 'gemini', 'copilot', 'antigravity',
'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'kimi',
'codebuddy', 'cline',
];
/**
* #1521: Every non-Claude runtime resolves its own runtime identity from a
* runtime-neutral config, and defaults workflow.use_worktrees to false —
* GSD's worktree isolation uses Claude Code's isolation="worktree" spawn
* parameter, which no other runtime honors. Stamped into the emitted
* workflow runtime-resolution blocks. (Generalizes the Codex-only #1515 fix.)
*
* @private — exported as `_stampNonClaudeRuntimeDefaults` for tests.
*/
function _stampNonClaudeRuntimeDefaults(content: string, runtime: string): string {
content = content.replace(
/config-get workflow\.use_worktrees --raw 2>\/dev\/null \|\| echo "true"/g,
'config-get workflow.use_worktrees --default false --raw 2>/dev/null || echo "false"',
);
content = content.replace(
/config-get runtime --default claude --raw 2>\/dev\/null \|\| echo "claude"/g,
`config-get runtime --default ${runtime} --raw 2>/dev/null || echo "${runtime}"`,
);
return content;
}
/**
* Apply the per-runtime rewrite table to a single content string.
* Relocated from bin/install.js `_applyRuntimeRewrites`.
@@ -2133,26 +2167,20 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
const dirName = getDirName(runtime);
const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
// #1521: stamp runtime identity + use_worktrees=false for every non-Claude runtime
// before brand-specific path rewrites, so the replace operates on the pristine
// source line and is idempotent regardless of subsequent path substitutions.
if (runtime !== 'claude') {
content = _stampNonClaudeRuntimeDefaults(content, runtime);
}
switch (runtime) {
case 'codex':
content = content.replace(/~\/\.claude\//g, pathPrefix);
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
content = content.replace(/~\/\.codex\//g, pathPrefix);
// #1515: stamp Codex's own runtime identity + safe worktree default into
// emitted workflow runtime-resolution blocks. A Codex install with a
// runtime-neutral .planning/config.json must resolve RUNTIME=codex (Codex
// cannot honor Claude's isolation="worktree"), and default
// workflow.use_worktrees to false so the fail-closed guard lets execution
// proceed without worktrees instead of falling back to Claude semantics.
content = content.replace(
/config-get runtime --default claude --raw 2>\/dev\/null \|\| echo "claude"/g,
'config-get runtime --default codex --raw 2>/dev/null || echo "codex"',
);
content = content.replace(
/config-get workflow\.use_worktrees --raw 2>\/dev\/null \|\| echo "true"/g,
'config-get workflow.use_worktrees --default false --raw 2>/dev/null || echo "false"',
);
// #1515 stamp moved to _stampNonClaudeRuntimeDefaults (#1521 generalisation).
content = processAttribution(content, attribution);
break;
@@ -2531,4 +2559,7 @@ export = {
rewriteStagedCommandBodies,
_computePathPrefix: computePathPrefix,
_applyRuntimeRewrites,
_stampNonClaudeRuntimeDefaults,
// #1521: canonical non-Claude runtime list for test files and tooling
NON_CLAUDE_RUNTIMES,
};

View File

@@ -28,7 +28,8 @@ 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'),
codexWorktreeGuard: body.includes('Codex execute-phase worktree isolation is unsupported'),
// #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"'),
};
});

View File

@@ -5,7 +5,8 @@
* dispatched Plan/Execute via Agent(run_in_background=true). On Claude Code a
* backgrounded agent has no Agent/Task tool, so it cannot spawn the nested
* subagents (worktree executors, plan-checker, verifier). The workflows must
* now resolve the runtime and run inline on Claude Code.
* now resolve the runtime and run inline everywhere except Codex, which is the
* only supported runtime where a backgrounded agent can still nest subagents.
*/
const { describe, test } = require('node:test');
@@ -24,23 +25,47 @@ describe('bug-853 — manager/autonomous gate background dispatch by runtime', (
assert.ok(matches.length >= 2, 'manager.md must resolve runtime for both plan and execute dispatch');
});
test('manager.md documents why Claude Code cannot background-dispatch', () => {
assert.match(MANAGER, /backgrounded agent has no `Agent`\/`Task` tool/);
test('manager.md documents why most runtimes cannot background-dispatch', () => {
// Accept both old singular form (backgrounded agent has no) and new plural form (backgrounded agents have no)
assert.match(MANAGER, /backgrounded agents? ha(?:s|ve) no `Agent`\/`Task` tool/);
});
test('manager.md runs plan/execute inline on Claude Code', () => {
assert.match(MANAGER, /If `RUNTIME` is `claude`[\s\S]{0,400}?Skill\(skill="gsd-plan-phase"/);
assert.match(MANAGER, /If `RUNTIME` is `claude`[\s\S]{0,400}?Skill\(skill="gsd-execute-phase"/);
test('manager.md gates background dispatch on codex and runs plan/execute inline otherwise', () => {
// Codex takes the background path
assert.match(MANAGER, /If `RUNTIME` is `codex`[\s\S]{0,400}?run_in_background=true/);
// Inline is the default/else branch for plan — anchored on the explicit non-Codex label
assert.match(
MANAGER,
/Otherwise \(Claude Code or any other non-Codex runtime\)[\s\S]{0,400}?Skill\(skill="gsd-plan-phase"/,
);
// Inline is the default/else branch for execute — anchored on the explicit non-Codex label
assert.match(
MANAGER,
/Otherwise \(Claude Code or any other non-Codex runtime\)[\s\S]{0,400}?Skill\(skill="gsd-execute-phase"/,
);
});
test('autonomous.md gates interactive background dispatch by runtime', () => {
const autoRuntimeMatches = AUTONOMOUS.match(/config-get runtime/g) || [];
assert.ok(autoRuntimeMatches.length >= 2, 'autonomous.md must resolve runtime in both 3b (plan) and 3c (execute) interactive branches');
assert.match(AUTONOMOUS, /backgrounded agent has no `Agent`\/`Task` tool/);
// Accept both old singular form (backgrounded agent has no) and new plural form (backgrounded agents have no)
assert.match(AUTONOMOUS, /backgrounded agents? ha(?:s|ve) no `Agent`\/`Task` tool/);
});
test('autonomous.md runs plan/execute inline on Claude Code in interactive mode', () => {
assert.match(AUTONOMOUS, /On Claude Code \(`RUNTIME` is `claude`\)[\s\S]{0,400}?Skill\(skill="gsd-plan-phase"/);
assert.match(AUTONOMOUS, /On Claude Code \(`RUNTIME` is `claude`\)[\s\S]{0,400}?Skill\(skill="gsd-execute-phase"/);
test('autonomous.md gates interactive background dispatch on codex; runs plan/execute inline otherwise', () => {
// Codex block: run_in_background=true appears within the codex branch and gsd-plan-phase is nearby
assert.match(AUTONOMOUS, /If `RUNTIME` is `codex`[\s\S]{0,1200}?run_in_background=true[\s\S]{0,600}?gsd-plan-phase/);
// Codex block: run_in_background=true appears within the codex branch and gsd-execute-phase is nearby
assert.match(AUTONOMOUS, /If `RUNTIME` is `codex`[\s\S]{0,3000}?run_in_background=true[\s\S]{0,200}?gsd-execute-phase/);
// Inline is the otherwise/else branch for plan — anchored on the explicit non-Codex label
assert.match(
AUTONOMOUS,
/Otherwise \(Claude Code or any other non-Codex runtime\)[\s\S]{0,400}?Skill\(skill="gsd-plan-phase"/,
);
// Inline is the otherwise/else branch for execute — anchored on the explicit non-Codex label
assert.match(
AUTONOMOUS,
/Otherwise \(Claude Code or any other non-Codex runtime\)[\s\S]{0,400}?Skill\(skill="gsd-execute-phase"/,
);
});
});

View File

@@ -66,17 +66,19 @@ test('codex emit defaults workflow.use_worktrees to false', () => {
);
});
test('non-codex runtime (cursor) does NOT rewrite the runtime default — stamping is codex-scoped', () => {
test('claude runtime does NOT rewrite the runtime default — stamping is non-claude-scoped (#1521 inversion)', () => {
// #1521 generalizes stamping to ALL non-Claude runtimes. The negative case
// (no stamping) is now the 'claude' runtime, not other non-Claude runtimes.
const line =
'RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")\n';
const out = conversion._applyRuntimeRewrites(line, 'cursor', '/home/u/.cursor/', true, undefined);
const out = conversion._applyRuntimeRewrites(line, 'claude', '$HOME/.claude/', true, undefined);
assert.ok(
out.includes('--default claude --raw'),
`Expected cursor output to preserve '--default claude --raw'; got:\n${out}`,
`Expected claude output to preserve '--default claude --raw'; got:\n${out}`,
);
assert.ok(
!out.includes('--default codex'),
`Expected cursor output NOT to contain '--default codex'; got:\n${out}`,
`Expected claude output NOT to contain '--default codex'; got:\n${out}`,
);
});
@@ -107,14 +109,17 @@ test('regression: every edited workflow gets codex-stamped (source↔engine pari
// Property tests (RULESET.TESTS.property-based-testing)
// ---------------------------------------------------------------------------
test('property: runtime stamping applies iff runtime is codex (#1515)', () => {
const RUNTIMES = ['claude','codex','cursor','cline','windsurf','augment','trae','qwen','hermes','gemini','opencode','kilo','copilot','antigravity','codebuddy'];
test('property: runtime stamping applies for ALL non-claude runtimes; only claude leaves --default claude unchanged (#1521)', () => {
// #1521: generalised from codex-only to all non-claude runtimes.
// Use the canonical list from the conversion module to avoid hand-rolled array drift.
const { NON_CLAUDE_RUNTIMES } = conversion;
const RUNTIMES = ['claude', ...NON_CLAUDE_RUNTIMES];
const line = 'RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")\n';
fc.assert(fc.property(fc.constantFrom(...RUNTIMES), (rt) => {
const out = conversion._applyRuntimeRewrites(line, rt, `$HOME/.${rt}/`, true, undefined);
return rt === 'codex'
? out.includes('--default codex --raw') && !out.includes('--default claude')
: out.includes('--default claude --raw') && !out.includes('--default codex');
return rt === 'claude'
? out.includes('--default claude --raw') && !out.includes('--default codex')
: out.includes(`--default ${rt} --raw`) && !out.includes('--default claude');
}));
});

View File

@@ -0,0 +1,228 @@
'use strict';
/**
* Regression tests for #1521: every non-Claude runtime stamps its own runtime
* identity + workflow.use_worktrees=false into emitted workflows.
*
* GSD's worktree isolation relies on Claude Code's isolation="worktree" spawn
* parameter, which no other runtime honors. #1519 (Codex-only fix) is
* generalized here to ALL non-Claude runtimes.
*
* All tests assert on the SUT's RETURN VALUE (engine output), not raw file reads,
* except the parity integration test which carries the allow-test-rule exemption.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const fc = require('fast-check');
const conversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
// #1521: use the canonical list from the conversion module rather than a hand-rolled
// local array that can drift from the real runtime set.
const { NON_CLAUDE_RUNTIMES: NON_CLAUDE } = conversion;
const WORKFLOWS = [
'execute-phase.md', 'autonomous.md', 'manager.md', 'diagnose-issues.md', 'quick.md',
];
const CLAUDE_RUNTIME_LINE = 'config-get runtime --default claude --raw 2>/dev/null || echo "claude"';
const TRUE_WT_LINE = 'config-get workflow.use_worktrees --raw 2>/dev/null || echo "true"';
const FALSE_WT_LINE = 'config-get workflow.use_worktrees --default false --raw 2>/dev/null || echo "false"';
// ---------------------------------------------------------------------------
// Parity across ALL non-Claude runtimes × all 5 workflows
// ---------------------------------------------------------------------------
test('parity: every non-Claude runtime stamps its own runtime default and use_worktrees=false on all workflows (#1521)', () => {
// allow-test-rule: emitted workflow runtime-resolution shell block is the runtime contract surface (#1521)
for (const rt of NON_CLAUDE) {
for (const wf of WORKFLOWS) {
const src = fs.readFileSync(
path.join(__dirname, '..', 'gsd-core', 'workflows', wf),
'utf8',
);
const out = conversion._applyRuntimeRewrites(src, rt, `$HOME/.${rt}/`, true, undefined);
// No un-stamped claude runtime line may survive
assert.ok(
!out.includes(CLAUDE_RUNTIME_LINE),
`${rt}/${wf}: residual un-stamped claude runtime read — _stampNonClaudeRuntimeDefaults not applied`,
);
// No un-stamped use_worktrees=true line may survive
assert.ok(
!out.includes(TRUE_WT_LINE),
`${rt}/${wf}: residual un-stamped use_worktrees=true read — _stampNonClaudeRuntimeDefaults not applied`,
);
// If the source had a runtime read, the output must have --default <rt>
if (src.includes(CLAUDE_RUNTIME_LINE)) {
assert.ok(
out.includes(`config-get runtime --default ${rt} --raw 2>/dev/null || echo "${rt}"`),
`${rt}/${wf}: runtime line not stamped to --default ${rt}`,
);
}
// If the source had a use_worktrees read, the output must have --default false
if (src.includes(TRUE_WT_LINE)) {
assert.ok(
out.includes(FALSE_WT_LINE),
`${rt}/${wf}: use_worktrees line not defaulted to false`,
);
}
}
}
});
// ---------------------------------------------------------------------------
// Claude unchanged — no stamping for the native runtime
// ---------------------------------------------------------------------------
test('claude runtime leaves runtime default and use_worktrees=true unchanged (#1521)', () => {
const src = fs.readFileSync(
path.join(__dirname, '..', 'gsd-core', 'workflows', 'execute-phase.md'),
'utf8',
);
const out = conversion._applyRuntimeRewrites(src, 'claude', '$HOME/.claude/', true, undefined);
// Claude emit must preserve the original --default claude line
if (src.includes(CLAUDE_RUNTIME_LINE)) {
assert.ok(
out.includes(CLAUDE_RUNTIME_LINE),
`claude/execute-phase.md: expected original claude runtime line to survive; got mutated`,
);
}
// Claude emit must NOT gain --default false for use_worktrees
assert.ok(
!out.includes(FALSE_WT_LINE),
`claude/execute-phase.md: use_worktrees line must NOT be stamped false for claude runtime`,
);
});
// ---------------------------------------------------------------------------
// fc property — identity: each runtime stamps itself, claude stays unchanged
// ---------------------------------------------------------------------------
test('property: _stampNonClaudeRuntimeDefaults stamps each non-claude runtime and leaves claude unchanged (#1521)', () => {
const line =
'RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")\n';
fc.assert(
fc.property(fc.constantFrom(...NON_CLAUDE, 'claude'), (rt) => {
const out = conversion._applyRuntimeRewrites(line, rt, `$HOME/.${rt}/`, true, undefined);
if (rt === 'claude') {
return out.includes('--default claude') && !/--default (?!claude)/.test(out);
}
return out.includes(`--default ${rt}`) && !out.includes('--default claude');
}),
);
});
// ---------------------------------------------------------------------------
// fc property — idempotence: stamping twice equals once
// ---------------------------------------------------------------------------
test('property: _stampNonClaudeRuntimeDefaults is idempotent (#1521)', () => {
fc.assert(
fc.property(
fc.constantFrom(...NON_CLAUDE),
fc.constantFrom('runtime', 'use_worktrees'),
(rt, which) => {
const line =
which === 'runtime'
? 'RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")\n'
: 'USE_WORKTREES=$(gsd_run query config-get workflow.use_worktrees --raw 2>/dev/null || echo "true")\n';
const once = conversion._applyRuntimeRewrites(line, rt, `$HOME/.${rt}/`, true, undefined);
const twice = conversion._applyRuntimeRewrites(once, rt, `$HOME/.${rt}/`, true, undefined);
return once === twice;
},
),
);
});
// ---------------------------------------------------------------------------
// Guard generalization: execute-phase.md uses != "claude" not = "codex"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Guard generalization: execute-phase.md, quick.md, and diagnose-issues.md
// all use != "claude" (not = "codex") for the worktree guard (#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'];
for (const wf of GUARD_WORKFLOWS) {
const src = fs.readFileSync(
path.join(__dirname, '..', 'gsd-core', 'workflows', wf),
'utf8',
);
assert.ok(
src.includes('[ "$RUNTIME" != "claude" ] && [ "$USE_WORKTREES" != "false" ]'),
`${wf}: expected generalized guard [ "$RUNTIME" != "claude" ] && [ "$USE_WORKTREES" != "false" ]`,
);
assert.ok(
!src.includes('[ "$RUNTIME" = "codex" ] && [ "$USE_WORKTREES" != "false" ]'),
`${wf}: found Codex-specific guard — should have been generalized to != "claude"`,
);
}
});
// ---------------------------------------------------------------------------
// Orchestration gating: manager.md + autonomous.md now gate on codex for
// background dispatch, not on "not claude". (#1521 Stage 2)
// ---------------------------------------------------------------------------
test('manager.md and autonomous.md gate run_in_background on codex specifically (#1521)', () => {
// allow-test-rule: orchestration dispatch gating in manager/autonomous .md is the runtime contract surface (#1521)
const manager = fs.readFileSync(
path.join(__dirname, '..', 'gsd-core', 'workflows', 'manager.md'),
'utf8',
);
const autonomous = fs.readFileSync(
path.join(__dirname, '..', 'gsd-core', 'workflows', 'autonomous.md'),
'utf8',
);
// Both files must gate run_in_background on codex (not on a generic "not claude" condition)
assert.ok(
/`RUNTIME` is `codex`[\s\S]{0,500}?run_in_background=true/.test(manager),
'manager.md: expected run_in_background dispatch gated on RUNTIME=codex specifically',
);
assert.ok(
/`RUNTIME` is `codex`[\s\S]{0,700}?run_in_background=true/.test(autonomous),
'autonomous.md: expected run_in_background dispatch gated on RUNTIME=codex specifically',
);
// Inline is the default/else branch (not just claude)
assert.ok(
/Otherwise[\s\S]{0,200}?Claude Code or any other non-Codex runtime/.test(manager),
'manager.md: expected "Otherwise (Claude Code or any other non-Codex runtime)" inline branch',
);
assert.ok(
/Otherwise[\s\S]{0,200}?Claude Code or any other non-Codex runtime/.test(autonomous),
'autonomous.md: expected "Otherwise (Claude Code or any other non-Codex runtime)" inline branch',
);
});
test('manager.md and autonomous.md no longer contain old "not claude" background-dispatch gating (#1521)', () => {
// allow-test-rule: orchestration dispatch gating in manager/autonomous .md is the runtime contract surface (#1521)
const manager = fs.readFileSync(
path.join(__dirname, '..', 'gsd-core', 'workflows', 'manager.md'),
'utf8',
);
const autonomous = fs.readFileSync(
path.join(__dirname, '..', 'gsd-core', 'workflows', 'autonomous.md'),
'utf8',
);
// The old phrasing that unconditionally sent every non-claude runtime to background must be gone
assert.ok(
!manager.includes('If `RUNTIME` is not `claude` (e.g. Codex)'),
'manager.md: old "If `RUNTIME` is not `claude` (e.g. Codex)" gating must be replaced',
);
assert.ok(
!autonomous.includes('On other runtimes:'),
'autonomous.md: old "On other runtimes:" branch label must be replaced',
);
});

View File

@@ -0,0 +1,95 @@
'use strict';
/**
* E2E regression tests for #1521: real install path (copyWithPathReplacement)
* MUST stamp non-Claude runtime defaults into emitted gsd-core/workflows/*.md.
*
* The earlier unit tests in fix-1521-non-claude-runtime-default-resolution.test.cjs
* only verify the engine (_applyRuntimeRewrites). This test verifies the wiring:
* that a REAL `node bin/install.js --codex/--cursor --global` actually emits
* execute-phase.md with --default codex / --default cursor (not --default claude).
*
* Root cause: copyWithPathReplacement is the emit path for gsd-core/workflows/*.md;
* it did its own inline path rewrites but never called _stampNonClaudeRuntimeDefaults,
* so the stamping was dead-on-arrival in real installs.
*
* This test must be RED before the fix is applied (Step 1) and GREEN after (Step 2).
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { cleanup } = require('./helpers.cjs');
const INSTALL = path.join(__dirname, '..', 'bin', 'install.js');
/**
* Run a real install into a temp config dir and return the emitted
* execute-phase.md content.
* @param {string} runtime e.g. 'codex', 'cursor', 'claude'
* @returns {string}
*/
function installAndRead(runtime) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-inst-${runtime}-`));
const res = spawnSync(
process.execPath,
[INSTALL, `--${runtime}`, '--global', '--config-dir', dir],
{ encoding: 'utf8', timeout: 120000 },
);
assert.strictEqual(res.status, 0, `install --${runtime} failed: ${res.stderr || res.stdout}`);
const wf = path.join(dir, 'gsd-core', 'workflows', 'execute-phase.md');
assert.ok(fs.existsSync(wf), `emitted workflow missing for ${runtime}: ${wf}`);
const content = fs.readFileSync(wf, 'utf8');
cleanup(dir);
return content;
}
// ---------------------------------------------------------------------------
// RED tests: these MUST FAIL before the copyWithPathReplacement wiring is added
// ---------------------------------------------------------------------------
test('real install: codex-emitted execute-phase.md resolves runtime=codex and defaults worktrees off (#1521)', () => {
const c = installAndRead('codex');
assert.ok(
c.includes('config-get runtime --default codex --raw'),
'codex runtime default not stamped in real install',
);
assert.ok(
c.includes('config-get workflow.use_worktrees --default false --raw'),
'codex use_worktrees not defaulted false in real install',
);
assert.ok(
!c.includes('config-get runtime --default claude --raw'),
'residual claude default in codex install',
);
});
test('real install: cursor-emitted execute-phase.md resolves runtime=cursor (#1521)', () => {
const c = installAndRead('cursor');
assert.ok(
c.includes('config-get runtime --default cursor --raw'),
'cursor runtime default not stamped in real install',
);
assert.ok(
!c.includes('config-get runtime --default claude --raw'),
'residual claude default in cursor install',
);
});
test('real install: claude-emitted execute-phase.md keeps claude default + worktrees on (#1521)', () => {
const c = installAndRead('claude');
assert.ok(
c.includes('config-get runtime --default claude --raw'),
'claude default changed in claude install',
);
assert.ok(
c.includes('config-get workflow.use_worktrees --raw 2>/dev/null || echo "true"'),
'claude worktrees default changed (should still be true)',
);
assert.ok(
!c.includes('config-get workflow.use_worktrees --default false --raw'),
'claude install must NOT have use_worktrees=false stamped',
);
});

View File

@@ -8,14 +8,14 @@
"audit-fix.md": 10988,
"audit-milestone.md": 17637,
"audit-uat.md": 7425,
"autonomous.md": 42275,
"autonomous.md": 42778,
"check-todos.md": 9431,
"cleanup.md": 9897,
"code-review-fix.md": 23890,
"code-review.md": 31602,
"complete-milestone.md": 29987,
"debug.md": 13505,
"diagnose-issues.md": 12762,
"diagnose-issues.md": 12820,
"discovery-phase.md": 8651,
"discuss-phase-assumptions.md": 26984,
"discuss-phase-power.md": 11273,
@@ -24,7 +24,7 @@
"docs-update.md": 55662,
"edit-phase.md": 12883,
"eval-review.md": 9923,
"execute-phase.md": 92926,
"execute-phase.md": 93024,
"execute-plan.md": 31365,
"explore.md": 10497,
"extract-learnings.md": 12849,
@@ -39,7 +39,7 @@
"insert-phase.md": 8943,
"list-phase-assumptions.md": 4305,
"list-workspaces.md": 5655,
"manager.md": 25949,
"manager.md": 26265,
"map-codebase.md": 20789,
"milestone-summary.md": 11774,
"mvp-phase.md": 13582,
@@ -57,7 +57,7 @@
"pr-branch.md": 9561,
"profile-user.md": 20650,
"progress.md": 29387,
"quick.md": 48772,
"quick.md": 48830,
"reapply-patches.md": 20393,
"remove-phase.md": 8469,
"remove-workspace.md": 7507,