fix(#4667): rewrite codex @ includes to the codex install root (#4858)

* test(#4667): add failing-first coverage for the codex @-include rewrite

Behavioral end-to-end: a real in-process install(true,'codex') into a temp
CODEX_HOME must leave zero @~/.claude includes in GSD-owned .md artifacts,
rewrite the issue's own example include to @~/.codex/, keep the deliberate
_GSD_RUNTIME_ROOT .claude fallback chains byte-identical, and stay
idempotent across a reinstall (no doubled prefix). All four are RED until
the installer grows the manifest-scoped rewrite pass.

* fix(#4667): rewrite codex @ includes to the codex install root

Codex-installed agents and commands kept @~/.claude/gsd-core/... (and
@/Users/trekkie/.claude/gsd-core/...) include references pointing into the Claude
install: silent wrong-copy reads on dual-runtime machines at divergent
versions, missing files on codex-only ones. Several emitters bypass the
per-runtime converters, so the per-emitter fixes since #570 rotted.

Adds a manifest-scoped rewrite pass in install() beside the leak scanner:
for codex, every manifest-tracked .md/.toml artifact has the @-include
forms rewritten to the codex root. The pass matches the exact include
literal only — the _GSD_RUNTIME_ROOT/$PREFERRED_CONFIG_DIR fallback
chains, prose .claude mentions, and CHANGELOG.md are untouched — and runs
before the scanner, which remains the verification backstop for anything
a future emitter introduces.

* test(#4667): cover the HOME-anchored include form and sync the pass comment

Adds behavioral coverage for the second rewrite literal
(@$HOME/.claude/gsd-core/ -> @$HOME/.codex/gsd-core/) via the
plan-review-convergence command, and corrects the pass's comment: the
agent .tomls are generated after it and prefix themselves, so the .toml
branch of the rewrite is inert by design.

* test(#4667): baseline the offline upgrade against the deployed tree (sanctioned)

* chore(#4667): backfill changeset PR number (4858)

* test(#4667): sandbox the install home in the include-rewrite tests (#3712 guard)

* test(#4667): install via subprocess with an isolated env in the include-rewrite tests

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-18 11:36:36 -04:00
committed by GitHub
parent e1f72cd324
commit 58c7bbb16a
4 changed files with 181 additions and 2 deletions

View File

@@ -12354,6 +12354,42 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
manifestFiles = null;
}
if (manifestFiles !== null) {
// #4667: codex-installed artifacts must not keep `@~/.claude/gsd-core/…`
// include references — the `@` form resolves into the CLAUDE install
// (wrong copy on dual-runtime machines at divergent versions, nothing at
// all on codex-only ones; #570 cause 2 residue). Every target ships in
// the codex install, so rewriting the `@~/` include form to the codex
// root is mechanical and correct. This runs after all .md emitters
// (several bypass the per-runtime converters — that is how the leak
// survived the per-emitter fixes; the agent .tomls are generated later
// and prefix themselves), and before the scan below, which stays as the
// verification backstop. The `_GSD_RUNTIME_ROOT`/`$PREFERRED_CONFIG_DIR`
// fallback chains and prose `.claude` mentions carry no `@~/` prefix and
// are deliberately untouched, as is CHANGELOG.md.
if (runtime === 'codex') {
for (const relPath of manifestFiles) {
const fileName = path.basename(relPath);
if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
if (fileName === 'CHANGELOG.md') continue;
const rewritePath = path.join(targetDir, relPath);
let rewriteContent;
try {
rewriteContent = fs.readFileSync(rewritePath, 'utf8');
} catch (rewriteErr) {
continue; // inaccessible or missing — the scan below reports or skips it
}
const rewritten = rewriteContent
.split('@~/.claude/gsd-core/').join('@~/.codex/gsd-core/')
.split('@$HOME/.claude/gsd-core/').join('@$HOME/.codex/gsd-core/');
if (rewritten !== rewriteContent) {
try {
fs.writeFileSync(rewritePath, rewritten);
} catch (writeErr) {
continue; // never fail the install over the rewrite; the scan still warns
}
}
}
}
for (const relPath of manifestFiles) {
const fileName = path.basename(relPath);
if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;