From 2436b769804c1a75468e42d00881e88d6e4eeba5 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 21 Jun 2026 11:56:59 -0400 Subject: [PATCH] fix(#1515): make Codex installs resolve their own runtime and fail closed on worktrees (#1519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#1515): make Codex installs resolve their own runtime and fail closed on worktrees A Codex install with a runtime-neutral .planning/config.json resolved RUNTIME=claude and enabled git worktree isolation, which Codex's spawn_agent cannot honor. Two root causes: 1. Workflows read `config-get runtime` / `config-get workflow.use_worktrees` without `--raw`, so config-get's JSON-quoted output ("codex") was captured verbatim into the bash var and broke every `[ "$RUNTIME" = ... ]` check — the Codex fail-closed guard was dead even when runtime:codex was explicit, and Claude's own worktree degrade-check was dead too. Add `--raw` to those reads across execute-phase, autonomous, manager, diagnose-issues, quick. 2. The conversion engine emitted `--default claude` for every runtime. Stamp the codex-emitted workflows to `--default codex` (runtime) and `--default false` (use_worktrees) in _applyRuntimeRewrites case 'codex', so a neutral config on a Codex install resolves runtime=codex / worktrees off. Also extend the Codex fail-closed worktree guard to quick.md and diagnose-issues.md (they spawned isolation="worktree" with no runtime guard). Regression test asserts source<->engine parity across all five workflows (DEFECT.GENERATIVE-FIX) plus fast-check property coverage of the stamping. Closes #1515 Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_013vX5eUtWa2wsZEyeMf5i3r * chore(#1515): backfill changeset PR number (#1519) Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_013vX5eUtWa2wsZEyeMf5i3r --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .changeset/humble-wasps-click.md | 5 + docs/CONFIGURATION.md | 2 +- gsd-core/workflows/autonomous.md | 4 +- gsd-core/workflows/diagnose-issues.md | 7 +- gsd-core/workflows/execute-phase.md | 4 +- gsd-core/workflows/manager.md | 4 +- gsd-core/workflows/quick.md | 7 +- src/runtime-artifact-conversion.cts | 14 ++ tests/fix-1515-codex-runtime-default.test.cjs | 130 ++++++++++++++++++ tests/workflow-size-baseline.json | 10 +- 10 files changed, 173 insertions(+), 14 deletions(-) create mode 100644 .changeset/humble-wasps-click.md create mode 100644 tests/fix-1515-codex-runtime-default.test.cjs diff --git a/.changeset/humble-wasps-click.md b/.changeset/humble-wasps-click.md new file mode 100644 index 000000000..3e1670dc4 --- /dev/null +++ b/.changeset/humble-wasps-click.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1519 +--- +**Codex installs no longer run with unsafe Claude-style worktree isolation** — a Codex install with a runtime-neutral `.planning/config.json` was resolving its runtime as Claude and enabling git worktree isolation, which Codex's `spawn_agent` cannot honor; the Codex fail-closed guard was also silently dead because runtime/worktree config was read JSON-quoted and broke shell equality checks. Codex-emitted workflows now resolve `runtime=codex`, default `workflow.use_worktrees` to `false`, and fail closed when worktrees are forced on. (#1515) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 71d37d540..1931a41a5 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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. | +| `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.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 | diff --git a/gsd-core/workflows/autonomous.md b/gsd-core/workflows/autonomous.md index 56cc9079e..c73ebd079 100644 --- a/gsd-core/workflows/autonomous.md +++ b/gsd-core/workflows/autonomous.md @@ -360,7 +360,7 @@ UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1) **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: ```bash -RUNTIME=$(gsd_run query config-get runtime --default claude 2>/dev/null || echo "claude") +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. @@ -422,7 +422,7 @@ Verify plan produced output — re-run `init phase-op` and check `has_plans`. If **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: ```bash -RUNTIME=$(gsd_run query config-get runtime --default claude 2>/dev/null || echo "claude") +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: diff --git a/gsd-core/workflows/diagnose-issues.md b/gsd-core/workflows/diagnose-issues.md index 6267579a8..4a1b86d10 100644 --- a/gsd-core/workflows/diagnose-issues.md +++ b/gsd-core/workflows/diagnose-issues.md @@ -59,7 +59,12 @@ gaps = [ ```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 "$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 2>/dev/null || echo "true") +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 + exit 1 +fi ``` **Report diagnosis plan to user:** diff --git a/gsd-core/workflows/execute-phase.md b/gsd-core/workflows/execute-phase.md index 07af88c7d..8f0c96a1b 100644 --- a/gsd-core/workflows/execute-phase.md +++ b/gsd-core/workflows/execute-phase.md @@ -91,8 +91,8 @@ Parse JSON for: `executor_model`, `verifier_model`, `commit_docs`, `parallelizat Read runtime/worktree config and fail closed before any executor dispatch: ```bash -RUNTIME=$(gsd_run query config-get runtime --default claude 2>/dev/null || echo "claude") -USE_WORKTREES=$(gsd_run query config-get workflow.use_worktrees 2>/dev/null || echo "true") +RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude") +USE_WORKTREES=$(gsd_run query config-get workflow.use_worktrees --raw 2>/dev/null || echo "true") 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") diff --git a/gsd-core/workflows/manager.md b/gsd-core/workflows/manager.md index e14f5bede..5e4414ded 100644 --- a/gsd-core/workflows/manager.md +++ b/gsd-core/workflows/manager.md @@ -247,7 +247,7 @@ After discuss completes, loop back to dashboard step. 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. ```bash -RUNTIME=$(gsd_run query config-get runtime --default claude 2>/dev/null || echo "claude") +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, …)`: @@ -301,7 +301,7 @@ Loop back to dashboard step. 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 2>/dev/null || echo "claude") +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, …)`: diff --git a/gsd-core/workflows/quick.md b/gsd-core/workflows/quick.md index c63254641..a2146f6e0 100644 --- a/gsd-core/workflows/quick.md +++ b/gsd-core/workflows/quick.md @@ -137,7 +137,12 @@ AGENT_SKILLS_VERIFIER=$(gsd_run query agent-skills gsd-verifier) Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `branch_name`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`. ```bash -USE_WORKTREES=$(gsd_run query config-get workflow.use_worktrees 2>/dev/null || echo "true") +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 + exit 1 +fi ``` If `USE_WORKTREES` is not `"false"`, run a startup orphan sweep before spawning any executors. This reaps locked worktrees whose lock-owner process is dead, whose branch is merged into the default branch, and whose lock file mtime is older than 5 minutes. Running it at startup prevents accumulation of orphaned worktrees from prior sessions that exited without cleanup (#3707). diff --git a/src/runtime-artifact-conversion.cts b/src/runtime-artifact-conversion.cts index ac5c01181..c5c6be2de 100644 --- a/src/runtime-artifact-conversion.cts +++ b/src/runtime-artifact-conversion.cts @@ -2139,6 +2139,20 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a 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"', + ); content = processAttribution(content, attribution); break; diff --git a/tests/fix-1515-codex-runtime-default.test.cjs b/tests/fix-1515-codex-runtime-default.test.cjs new file mode 100644 index 000000000..fe551e3c2 --- /dev/null +++ b/tests/fix-1515-codex-runtime-default.test.cjs @@ -0,0 +1,130 @@ +'use strict'; +/** + * Regression tests for bug #1515: Codex install with runtime-neutral + * .planning/config.json resolves runtime as 'claude' and enables worktree + * isolation (unsafe for Codex). + * + * Root causes: + * A) config-get reads in workflows lacked --raw → output JSON-quoted → + * every comparison like [ "$RUNTIME" = "codex" ] failed silently. + * B) The conversion engine emitted --default claude for every runtime → + * neutral Codex config fell back to claude default. + * + * All tests assert on the SUT's RETURN VALUE (engine output), not raw file reads, + * except the integration test (test 4) which is explicitly the source↔engine + * parity guard and 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'); + +// --------------------------------------------------------------------------- +// Unit tests: engine stamps codex-specific defaults into emitted workflows +// --------------------------------------------------------------------------- + +test('codex emit stamps its own runtime default into the runtime-resolution line', () => { + const line = + 'RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")\n'; + const out = conversion._applyRuntimeRewrites(line, 'codex', '$HOME/.codex/', true, undefined); + assert.ok( + out.includes('config-get runtime --default codex --raw'), + `Expected 'config-get runtime --default codex --raw' in output; got:\n${out}`, + ); + assert.ok( + out.includes('|| echo "codex")'), + `Expected '|| echo "codex")' in output; got:\n${out}`, + ); + assert.ok( + !out.includes('--default claude'), + `Expected '--default claude' to be fully rewritten; got:\n${out}`, + ); + assert.ok( + !out.includes('echo "claude"'), + `Expected 'echo "claude"' to be fully rewritten; got:\n${out}`, + ); +}); + +test('codex emit defaults workflow.use_worktrees to false', () => { + const line = + 'USE_WORKTREES=$(gsd_run query config-get workflow.use_worktrees --raw 2>/dev/null || echo "true")\n'; + const out = conversion._applyRuntimeRewrites(line, 'codex', '$HOME/.codex/', true, undefined); + assert.ok( + out.includes('config-get workflow.use_worktrees --default false --raw'), + `Expected 'config-get workflow.use_worktrees --default false --raw' in output; got:\n${out}`, + ); + assert.ok( + out.includes('|| echo "false")'), + `Expected '|| echo "false")' in output; got:\n${out}`, + ); + assert.ok( + !out.includes('|| echo "true")'), + `Expected '|| echo "true")' to be fully rewritten; got:\n${out}`, + ); +}); + +test('non-codex runtime (cursor) does NOT rewrite the runtime default — stamping is codex-scoped', () => { + 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); + assert.ok( + out.includes('--default claude --raw'), + `Expected cursor 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}`, + ); +}); + +// --------------------------------------------------------------------------- +// Integration / parity guard: real source ↔ engine output for codex (all surfaces) +// --------------------------------------------------------------------------- + +test('regression: every edited workflow gets codex-stamped (source↔engine parity, all surfaces) (#1515)', () => { + // allow-test-rule: emitted workflow runtime-resolution shell block is the runtime contract surface (#1515) — asserts on engine-transformed output of the real source + const WORKFLOWS = ['execute-phase.md', 'autonomous.md', 'manager.md', 'diagnose-issues.md', 'quick.md']; + const CLAUDE_RUNTIME = 'config-get runtime --default claude --raw 2>/dev/null || echo "claude"'; + const CODEX_RUNTIME = 'config-get runtime --default codex --raw 2>/dev/null || echo "codex"'; + const TRUE_WT = 'config-get workflow.use_worktrees --raw 2>/dev/null || echo "true"'; + const FALSE_WT = 'config-get workflow.use_worktrees --default false --raw 2>/dev/null || echo "false"'; + for (const wf of WORKFLOWS) { + const src = fs.readFileSync(path.join(__dirname, '..', 'gsd-core', 'workflows', wf), 'utf8'); + const out = conversion._applyRuntimeRewrites(src, 'codex', '$HOME/.codex/', true, undefined); + // No un-stamped claude/true resolution line may survive codex emit on ANY surface. + assert.ok(!out.includes(CLAUDE_RUNTIME), `${wf}: residual un-stamped runtime read — engine regex no longer matches source line (parity drift)`); + assert.ok(!out.includes(TRUE_WT), `${wf}: residual un-stamped use_worktrees read — parity drift`); + // If the source HAS such a read, the codex form must be present. + if (src.includes(CLAUDE_RUNTIME)) assert.ok(out.includes(CODEX_RUNTIME), `${wf}: runtime read not stamped to codex`); + if (src.includes(TRUE_WT)) assert.ok(out.includes(FALSE_WT), `${wf}: use_worktrees read not defaulted to false`); + } +}); + +// --------------------------------------------------------------------------- +// 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']; + 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'); + })); +}); + +test('property: codex stamping is idempotent on resolution lines (#1515)', () => { + fc.assert(fc.property(fc.constantFrom('runtime', 'use_worktrees'), (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, 'codex', '$HOME/.codex/', true, undefined); + const twice = conversion._applyRuntimeRewrites(once, 'codex', '$HOME/.codex/', true, undefined); + return once === twice; + })); +}); diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index 3f880b5cd..b32c48fab 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -8,14 +8,14 @@ "audit-fix.md": 10988, "audit-milestone.md": 17637, "audit-uat.md": 7425, - "autonomous.md": 42263, + "autonomous.md": 42275, "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": 12425, + "diagnose-issues.md": 12762, "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": 92914, + "execute-phase.md": 92926, "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": 25937, + "manager.md": 25949, "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": 48435, + "quick.md": 48772, "reapply-patches.md": 20393, "remove-phase.md": 8469, "remove-workspace.md": 7507,