feat(#2088): migrate Codex onto the Embeddable Orchestration System (ADR-1239)
Drive Codex install/uninstall through the descriptor-driven Host-Integration Interface (declarative embedding adapter → engine surface dispatch) and fold every positive `runtime === 'codex'` / `isCodex` projection into descriptor-driven `runtime.hostBehaviors`. Install/uninstall output stays byte-parity-gated (tests/fixtures/golden-install-parity/codex.json); no other runtime changes. Three Context7-verified upgrades, each with a test on the user-reachable surface: - Skill root → canonical $HOME/.agents/skills via a skills-kind `home` override, with pre-move migration cleanup (stale ~/.codex/skills/gsd-* removed on install and uninstall; user content preserved). Fixes getGlobalSkillsBase, writeManifest, and the skill-manifest inventory to honor the override so --skills-root / sync-skills / the manifest report the real location. - Six new hooks.json lifecycle events (PreToolUse, PermissionRequest, PreCompact, PostCompact, SubagentStop, UserPromptSubmit) shared by install + uninstall; extendedHookEvents reconciled [] -> the schema-valid wired subset. - Explicit `[agents] max_depth = 1` in the managed config.toml block, pinning the negotiated dispatch.maxDepth:1 axis. validateCodexConfigSchema now permits a known-scalar-only bare `[agents]` AgentsToml table (still rejects [[agents]] and unknown-key break-forms, #2760); mergeCodexConfig preserves the user's own AgentsToml scalars (max_threads etc.) instead of dropping them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,19 @@ function withEnv(key, value, fn) {
|
||||
}
|
||||
}
|
||||
|
||||
// #2088: Codex CLI skills install to `$HOME/.agents/skills` (resolved via
|
||||
// os.homedir()), not `$CODEX_HOME/skills`. In-process codex installs must
|
||||
// sandbox HOME (and USERPROFILE, for Windows os.homedir() resolution) to the
|
||||
// test's codexHome dir, or skills get materialized into the real developer
|
||||
// home directory. withEnv saves/restores a single key, so nesting is safe.
|
||||
function withCodexEnv(codexHome, fn) {
|
||||
return withEnv('CODEX_HOME', codexHome, () =>
|
||||
withEnv('HOME', codexHome, () =>
|
||||
withEnv('USERPROFILE', codexHome, fn)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function captureConsole(fn) {
|
||||
const originalLog = console.log;
|
||||
const originalWarn = console.warn;
|
||||
@@ -216,11 +229,20 @@ function assertFreshInstallContract(runtime, targetDir) {
|
||||
}
|
||||
|
||||
if (contract.surface === 'flat-skills') {
|
||||
// Pre-#3562: codex was special-cased to expect zero gsd-* skill dirs
|
||||
// (assumption: Codex auto-discovers from workflows). That assumption
|
||||
// does not hold for Codex CLI 0.130.0 — fresh installs now materialize
|
||||
// the same flat-skills surface as the other runtimes.
|
||||
assertHasGsdDirectory(targetDir, 'skills');
|
||||
if (runtime === 'codex') {
|
||||
// #2088: Codex CLI skills install to the canonical `$HOME/.agents/skills`
|
||||
// root (resolved via os.homedir()), NOT `<config-dir>/skills`. Here
|
||||
// runInstallerCli sandboxes HOME to <dirname(targetDir)>/home, so assert
|
||||
// the skill dir under that sandboxed home instead of under targetDir.
|
||||
const codexSandboxHome = path.join(path.dirname(targetDir), 'home');
|
||||
assertHasGsdDirectory(path.join(codexSandboxHome, '.agents'), 'skills');
|
||||
} else {
|
||||
// Pre-#3562: codex was special-cased to expect zero gsd-* skill dirs
|
||||
// (assumption: Codex auto-discovers from workflows). That assumption
|
||||
// does not hold for Codex CLI 0.130.0 — fresh installs now materialize
|
||||
// the same flat-skills surface as the other runtimes.
|
||||
assertHasGsdDirectory(targetDir, 'skills');
|
||||
}
|
||||
} else if (contract.surface === 'hermes-skills') {
|
||||
// Hermes layout uses prefix: '' — skill dirs have bare stem names (no gsd- prefix).
|
||||
// Assert that the category dir contains at least one skill dir with SKILL.md.
|
||||
@@ -335,7 +357,7 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
});
|
||||
|
||||
const { output } = captureConsole(() =>
|
||||
withEnv('CODEX_HOME', codexHome, () => install(true, 'codex'))
|
||||
withCodexEnv(codexHome, () => install(true, 'codex'))
|
||||
);
|
||||
|
||||
const plainOutput = stripAnsi(output);
|
||||
@@ -353,13 +375,17 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
|
||||
assert.throws(
|
||||
() => captureConsole(() =>
|
||||
withEnv('CODEX_HOME', codexHome, () => install(true, 'codex'))
|
||||
withCodexEnv(codexHome, () => install(true, 'codex'))
|
||||
),
|
||||
/installer migration blocked/
|
||||
);
|
||||
|
||||
assert.equal(fs.readFileSync(path.join(codexHome, 'hooks/gsd-retired-hook.txt'), 'utf8'), 'old gsd hook\n');
|
||||
assert.equal(fs.existsSync(path.join(codexHome, 'skills')), false);
|
||||
// #2088: with HOME sandboxed to codexHome via withCodexEnv, Codex's
|
||||
// canonical skill root ($HOME/.agents/skills) resolves under codexHome
|
||||
// too — assert nothing was materialized there when the install is blocked.
|
||||
assert.equal(fs.existsSync(path.join(codexHome, '.agents', 'skills')), false);
|
||||
assert.equal(fs.existsSync(path.join(codexHome, 'gsd-core', 'VERSION')), false);
|
||||
});
|
||||
|
||||
@@ -431,7 +457,7 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
assert.throws(
|
||||
() => captureConsole(() =>
|
||||
withEnv('CLAUDE_CONFIG_DIR', claudeHome, () =>
|
||||
withEnv('CODEX_HOME', codexHome, () =>
|
||||
withCodexEnv(codexHome, () =>
|
||||
withWriteFailure(path.join(codexHome, 'gsd-core', 'VERSION'), () =>
|
||||
installModule.installAllRuntimes(['claude', 'codex'], true, false)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user