fix(#1515): make Codex installs resolve their own runtime and fail closed on worktrees (#1519)

* 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013vX5eUtWa2wsZEyeMf5i3r

* chore(#1515): backfill changeset PR number (#1519)

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 11:56:59 -04:00
committed by GitHub
parent eb81faaeab
commit 2436b76980
10 changed files with 173 additions and 14 deletions

View File

@@ -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)

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. |
| `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 |

View File

@@ -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:

View File

@@ -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:**

View File

@@ -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")

View File

@@ -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, …)`:

View File

@@ -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).

View File

@@ -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;

View File

@@ -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;
}));
});

View File

@@ -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,