diff --git a/.changeset/2406-codex-agent-role-double-registration.md b/.changeset/2406-codex-agent-role-double-registration.md new file mode 100644 index 000000000..90ffdc442 --- /dev/null +++ b/.changeset/2406-codex-agent-role-double-registration.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2432 +--- +**Codex installer no longer double-registers each agent role in `config.toml`, eliminating one duplicate-role startup warning per agent** — `generateCodexConfigBlock` stopped emitting `[agents.gsd-*]` tables whose `config_file` pointed back at the same standalone TOMLs Codex already auto-discovers under `$CODEX_HOME/agents/`; reinstalling over an existing config also drops any legacy managed role tables left by a prior install while preserving unrelated user config and the user's own AgentsToml scalars. (#2406) diff --git a/bin/install.js b/bin/install.js index 9fde53ced..8d94508c1 100755 --- a/bin/install.js +++ b/bin/install.js @@ -4365,14 +4365,26 @@ function _resolveMovedSkillsOldDir(runtime, targetDir, scope) { /** * Generate the GSD config block for Codex config.toml. - * @param {Array<{name: string, description: string}>} agents + * + * #2406 — standalone per-agent TOMLs (written by installCodexConfig to + * `$CODEX_HOME/agents/.toml`) are auto-discovered by Codex and are the + * SOLE canonical registration source for each role. This block therefore no + * longer emits `[agents.]` role tables that point `config_file` back at + * those same standalone TOMLs — that was a second, redundant declaration of + * the same role in one config layer, and Codex logged "Ignoring malformed + * agent role definition: duplicate agent role name" once per agent as a + * result. Only the bare `[agents]` dispatch-tuning scalar table is emitted + * here; role name/description/model/reasoning-effort/sandbox settings remain + * fully discoverable through the standalone TOML alone. + * @param {Array<{name: string, description: string}>} _agents unused — kept + * in the signature for call-site compatibility (installCodexConfig and + * existing tests still pass it positionally); per-agent role tables are no + * longer generated from it. + * @param {string} [_targetDir] unused — the standalone-TOML `config_file` + * path it used to resolve is no longer emitted here; kept for the same + * call-site-compatibility reason as `_agents`. */ -function generateCodexConfigBlock(agents, targetDir) { - // Use absolute paths when targetDir is provided — Codex ≥0.116 requires - // AbsolutePathBuf for config_file and cannot resolve relative paths. - const agentsPrefix = targetDir - ? path.join(targetDir, 'agents').replace(/\\/g, '/') - : 'agents'; +function generateCodexConfigBlock(_agents, _targetDir) { const lines = [ GSD_CODEX_MARKER, '', @@ -4381,24 +4393,12 @@ function generateCodexConfigBlock(agents, targetDir) { // ADR-1239 upgrade 2 / #2088 — explicit dispatch tuning. Pin `max_depth` on the // `[agents]` (AgentsToml) table rather than relying on codex-cli's implicit // default, realizing the negotiated `dispatch.maxDepth: 1` axis. This bare - // `[agents]` scalar table coexists with the flattened `[agents.]` role - // sub-tables below (validated by validateCodexConfigSchema, which permits a - // known-scalar-only `[agents]`). Emitted before the role tables so the parent - // table is opened first. + // `[agents]` scalar table is validated by validateCodexConfigSchema, which + // permits a known-scalar-only `[agents]`. lines.push('[agents]'); lines.push(`max_depth = ${GSD_CODEX_AGENTS_MAX_DEPTH}`); lines.push(''); - for (const { name, description } of agents) { - // #2727 — Codex 0.124.0 requires [agents.] struct format, not [[agents]] sequence. - // [[agents]] (introduced in #2645) is rejected by codex-cli 0.124.0 with - // "invalid type: sequence, expected struct AgentsToml in `agents`". - lines.push(`[agents.${name}]`); - lines.push(`description = ${JSON.stringify(description)}`); - lines.push(`config_file = "${agentsPrefix}/${name}.toml"`); - lines.push(''); - } - return lines.join('\n'); } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cd80f98af..0c1bbe34f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -837,7 +837,7 @@ The migration-specific ownership and source snapshots live in | OpenCode | `~/.config/opencode` | `./.opencode` | `commands/gsd-*.md` | `agents/gsd-*.md` | `opencode.json` or `opencode.jsonc`; no GSD hooks | | Kilo | `~/.config/kilo` | `./.kilo` | `command/gsd-*.md` | `agents/gsd-*.md` | `kilo.json` or `kilo.jsonc`; no GSD hooks | | Kimi CLI | First-existing generic root: `~/.config/agents` recommended, then `~/.agents` when `~/.agents/skills` exists and `~/.config/agents/skills` does not | Deferred and guarded | `skills/gsd-*/SKILL.md` (flat) invoked as `/skill:gsd-*` | `agents/gsd.yaml`, `agents/gsd.md`, and `agents/subagents/gsd-*` YAML/prompt pairs | Explicit `kimi --agent-file /agents/gsd.yaml`; no GSD hooks or statusline | -| Codex | `~/.codex` | `./.codex` | `skills/gsd-*/SKILL.md` (flat) | `agents/` source markdown plus per-agent TOML | `config.toml` `[agents.gsd-*]`, `[features].hooks` (canonical; legacy alias `codex_hooks` is recognized and migrated forward on reinstall, #3566), and hook tables | +| Codex | `~/.codex` | `./.codex` | `skills/gsd-*/SKILL.md` (flat) | `agents/` source markdown plus per-agent TOML (Codex auto-discovers each `agents/gsd-*.toml`; this is the sole canonical role registration, #2406) | `config.toml` bare `[agents]` dispatch-tuning scalar (`max_depth`, no per-role `[agents.gsd-*]` tables), `[features].hooks` (canonical; legacy alias `codex_hooks` is recognized and migrated forward on reinstall, #3566), and hook tables | | GitHub Copilot | `~/.copilot` | `./.github` | `skills/gsd-*/SKILL.md` (flat), `copilot-instructions.md`, and `AGENTS.md` (repo root, local) | `.agent.md` files | Self-contained `sessionStart` hook (`hooks/gsd-session.json`, inline `command` type); no statusline | | Antigravity | auto-detected: `~/.gemini/antigravity`, `~/.gemini/antigravity-ide`, or `~/.gemini/antigravity-cli` | `./.agent` | `skills/gsd-*/SKILL.md` (flat, #1614) | `agents/gsd-*.md` | Gemini-style `settings.json` hook entries when installed by GSD | | Cursor | `~/.cursor` | `./.cursor` | `skills/gsd-*/SKILL.md` (flat) | `agents/gsd-*.md` | Rule references under `rules/`; `hooks.json` with sessionStart context injection and postToolUse STATE.md monitor (#777) | diff --git a/docs/how-to/install-on-your-runtime.md b/docs/how-to/install-on-your-runtime.md index 75409f259..4b77719ed 100644 --- a/docs/how-to/install-on-your-runtime.md +++ b/docs/how-to/install-on-your-runtime.md @@ -155,7 +155,7 @@ KILO_CONFIG_DIR=~/.config/kilo-alt npx @opengsd/gsd-core@latest --kilo --global npx @opengsd/gsd-core@latest --codex --global ``` -Skills land in `~/.codex/skills/gsd-*/SKILL.md`. Agents are written with per-agent TOML entries in `config.toml`. Restart Codex (or run `codex --reload`) after install. +Skills land in `~/.codex/skills/gsd-*/SKILL.md`. Agents are written as standalone `~/.codex/agents/gsd-*.toml` files, which Codex auto-discovers — that is the sole registration source for each role; `config.toml` only carries the shared `[agents]` dispatch-tuning scalar (`max_depth`), not a per-role table (#2406). Restart Codex (or run `codex --reload`) after install. **Minimum supported version:** Codex CLI 0.130.0. Earlier versions had additional skill-root scanning that can produce duplicate listings. diff --git a/docs/installer-migrations.md b/docs/installer-migrations.md index 0978ec094..6fdae1b39 100644 --- a/docs/installer-migrations.md +++ b/docs/installer-migrations.md @@ -368,7 +368,7 @@ for the new shape before changing migration behavior. | OpenCode | Flat markdown commands in `commands/gsd-*.md` (plural — OpenCode discovers slash commands from `commands/`, not the legacy singular `command/`, #2329); agents in `agents/gsd-*.md`; config updates in `opencode.json` or `opencode.jsonc` | Global `OPENCODE_CONFIG_DIR`, `dirname(OPENCODE_CONFIG)`, `XDG_CONFIG_HOME/opencode`, or `~/.config/opencode`; local `./.opencode` | GSD owns generated command/agent files and GSD entries in structured config only | [Config](https://opencode.ai/docs/config/), [Commands](https://opencode.ai/docs/commands/); docs published 2026-05, checked 2026-07-16 | | Kilo | OpenCode-style flat markdown commands in `command/gsd-*.md`; agents in `agents/gsd-*.md`; config updates in `kilo.json` or `kilo.jsonc` | Global `KILO_CONFIG_DIR`, `dirname(KILO_CONFIG)`, `XDG_CONFIG_HOME/kilo`, or `~/.config/kilo`; local `./.kilo` | GSD owns generated command/agent files and GSD entries in structured config only | [Custom subagents](https://docs.kilo.ai/docs/customize/custom-subagents); docs not versioned, checked 2026-05-11 | | Kimi CLI | Agent Skills in `skills/gsd-*/SKILL.md`; explicit custom agent YAML/prompt artifacts in `agents/gsd.yaml`, `agents/gsd.md`, and `agents/subagents/gsd-*`; `gsd-core/` payload files referenced by generated skills; manifest, pristine, local-patch, and migration journal files from the normal installer safety pipeline | Global `KIMI_CONFIG_DIR`, explicit `--config-dir`, or first-existing generic skills root: `~/.config/agents` when `~/.config/agents/skills` exists or no generic skills root exists yet, otherwise `~/.agents` when `~/.agents/skills` exists and `~/.config/agents/skills` does not; `KIMI_CONFIG_DIR` and `--config-dir` are GSD write-location overrides and arbitrary roots require Kimi-side `--skills-dir` or `extra_skill_dirs` configuration for skill discovery; local `--kimi --local` is guarded and writes no project-level artifacts | GSD owns only generated `skills/gsd-*`, `agents/gsd.*`, `agents/subagents/gsd-*`, installed `gsd-core/` payload files, and manifest/preservation/migration records. GSD does not own Kimi config files, hooks, settings, rules, statusline, update-banner registration, or non-GSD Kimi skills/agents. Reinstall/update must preserve locally modified generated Kimi artifacts through manifest-backed `gsd-local-patches/`; uninstall removes only GSD-owned Kimi artifacts and preserves non-GSD user content. | [Agent Skills](https://moonshotai.github.io/kimi-cli/en/customization/skills.html), [Agents and Subagents](https://moonshotai.github.io/kimi-cli/en/customization/agents.html), [Tools](https://moonshotai.github.io/kimi-code/en/reference/tools.html); docs checked 2026-06-07 | -| Codex | Skills in `skills/gsd-*/SKILL.md`; agents as source markdown plus per-agent TOML in `agents/`; `[agents.gsd-*]` and hooks in `config.toml` | Global `CODEX_HOME` or `~/.codex`; local `./.codex` | GSD owns generated skills, generated agent TOML, `agents.gsd-*` config sections, `[features].hooks` when added by GSD (canonical; legacy alias `codex_hooks` is recognized and migrated forward, #3566), and GSD hook entries | [Codex config schema](https://developers.openai.com/codex/config-schema.json), [Codex developer docs](https://developers.openai.com/codex/); docs not versioned, checked 2026-05-15; installer compatibility sentinel: Codex 0.130.0 features.hooks key (legacy `codex_hooks` recognized) | +| Codex | Skills in `skills/gsd-*/SKILL.md`; agents as source markdown plus per-agent TOML in `agents/` (Codex auto-discovers each standalone `agents/gsd-*.toml` — that is the sole role-registration source, #2406); bare `[agents]` dispatch-tuning scalar and hooks in `config.toml` | Global `CODEX_HOME` or `~/.codex`; local `./.codex` | GSD owns generated skills, generated agent TOML, the managed bare `[agents]` scalar table (`max_depth`; no `[agents.gsd-*]` role sections — those were a duplicate registration removed in #2406), `[features].hooks` when added by GSD (canonical; legacy alias `codex_hooks` is recognized and migrated forward, #3566), and GSD hook entries | [Codex config schema](https://developers.openai.com/codex/config-schema.json), [Codex developer docs](https://developers.openai.com/codex/); docs not versioned, checked 2026-05-15; installer compatibility sentinel: Codex 0.130.0 features.hooks key (legacy `codex_hooks` recognized) | | GitHub Copilot | Skills in `skills/gsd-*/SKILL.md`; agents as `.agent.md`; repository instructions in `copilot-instructions.md` | Global `COPILOT_CONFIG_DIR`, `COPILOT_HOME`, or `~/.copilot`; local `./.github` | GSD owns generated skill/agent files and GSD-authored instruction files; no hook/statusline ownership | [Repository custom instructions](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions), [Copilot CLI custom instructions](https://docs.github.com/en/copilot/how-tos/copilot-cli/add-custom-instructions); GitHub Docs product docs, checked 2026-05-11 | | Antigravity | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; Gemini-style `settings.json` hooks when installed by GSD | Global `ANTIGRAVITY_CONFIG_DIR` or `~/.gemini/antigravity`; local `./.agents` (canonical, #791) or `./.agent` (legacy, recognized for backward-compat) | GSD owns generated skills/agents/hooks and GSD settings entries only | Public Antigravity install/config docs for this file layout were not stable or complete as of 2026-05-11; installer compatibility therefore uses GSD's Gemini-compatible settings policy, documented shim baseline. Fresh installs write to `.agents/` (the Google-Codelabs-documented form); existing `.agent/` installs continue to be detected and served. | | Cursor | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; rule references under `rules/`; lifecycle hooks via `hooks.json` (sessionStart + postToolUse, #777) | Global `CURSOR_CONFIG_DIR` or `~/.cursor`; local `./.cursor` | GSD owns generated skills/agents, GSD rule files or references, and GSD-managed `hooks.json` entries (sentinel `gsd-managed:true`); no statusline ownership | [Cursor rules](https://docs.cursor.com/context/rules); [Cursor hooks](https://docs.cursor.com/context/hooks); docs not versioned, checked 2026-06-07 | diff --git a/docs/reference/host-integration-capability-matrix.md b/docs/reference/host-integration-capability-matrix.md index d9052207f..819e1fead 100644 --- a/docs/reference/host-integration-capability-matrix.md +++ b/docs/reference/host-integration-capability-matrix.md @@ -105,7 +105,7 @@ Sources consulted: - **Skill root** — skills install to the canonical `$HOME/.agents/skills` (Codex core-skills `loader.rs` user-scope root), not the deprecated `$CODEX_HOME/skills` fallback. Declared via the skills-kind `home: ".agents"` override; pre-move installs are migrated (stale `~/.codex/skills/gsd-*` cleaned on both install and uninstall). - **Hook events** — GSD registers all documented `hooks.json` lifecycle events beyond `SessionStart`: `SubagentStart`, `Stop`, `PostToolUse` (#772), plus the six added in #2088 — `PreToolUse`, `PermissionRequest`, `PreCompact`, `PostCompact`, `SubagentStop`, `UserPromptSubmit` — all routed through `gsd-context-monitor.js`. (The descriptor `extendedHookEvents` field reflects the schema-valid cross-runtime subset `SubagentStop`/`Stop`/`PreCompact`; Codex's full event set is codex-hooks-json-native, registered directly in `hooks.json`.) -- **Dispatch tuning** — `[agents] max_depth = 1` is written explicitly into the managed `config.toml` block, pinning the `dispatch.maxDepth: 1` axis instead of relying on codex-cli's implicit default. Because `maxDepth === 1`, `degradationFor` flattens GSD-hosted wave dispatch to single-level even though `dispatch.nested`/`background`/`backgroundDispatch` are all `true`. The block is a bare `[agents]` AgentsToml scalar table (coexisting with the flattened `[agents.gsd-*]` role sub-tables); `validateCodexConfigSchema` permits a known-scalar-only `[agents]` while still rejecting `[[agents]]` and unknown-key forms. +- **Dispatch tuning** — `[agents] max_depth = 1` is written explicitly into the managed `config.toml` block, pinning the `dispatch.maxDepth: 1` axis instead of relying on codex-cli's implicit default. Because `maxDepth === 1`, `degradationFor` flattens GSD-hosted wave dispatch to single-level even though `dispatch.nested`/`background`/`backgroundDispatch` are all `true`. The block is a bare `[agents]` AgentsToml scalar table; it does **not** carry per-role `[agents.gsd-*]` sub-tables — those pointed `config_file` back at the standalone `agents/gsd-*.toml` files Codex already auto-discovers, so emitting them was a duplicate role registration (Codex logged "Ignoring malformed agent role definition: duplicate agent role name" once per agent) removed in #2406. `validateCodexConfigSchema` permits a known-scalar-only `[agents]` while still rejecting `[[agents]]` and unknown-key forms. Sources consulted: - https://github.com/openai/codex (repo via gh CLI) diff --git a/tests/codex-config.test.cjs b/tests/codex-config.test.cjs index e72da282e..b1d0e9d8a 100644 --- a/tests/codex-config.test.cjs +++ b/tests/codex-config.test.cjs @@ -56,6 +56,7 @@ const { CODEX_AGENT_SANDBOX, parseTomlToObject, resolveNodeRunner, + validateCodexConfigSchema, } = require('../bin/install.js'); const { resolveInstallPlan } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs'); @@ -833,8 +834,9 @@ describe('generateCodexConfigBlock', () => { assert.ok(!result.includes('[features]'), 'no features table'); assert.ok(!result.includes('multi_agent'), 'no multi_agent'); assert.ok(!result.includes('default_mode_request_user_input'), 'no request_user_input'); - // #2088: the managed block DOES pin dispatch depth via a bare [agents] - // AgentsToml scalar table (coexisting with the [agents.] role structs). + // #2088: the managed block pins dispatch depth via a bare [agents] + // AgentsToml scalar table. #2406: this is now the ONLY [agents]-namespaced + // content the block emits — no [agents.] role structs. assert.match(result, /^\[agents\]$/m, 'emits the [agents] tuning table'); assert.match(result, /^max_depth = 1$/m, 'pins max_depth = 1'); // Should not emit [[agents]] sequence format (rejected by Codex 0.124.0). @@ -843,52 +845,39 @@ describe('generateCodexConfigBlock', () => { assert.ok(!result.includes('max_threads'), 'no max_threads (only max_depth is GSD-managed)'); }); - test('#2727: emits [agents.] struct format (Codex 0.120.0+, replaces #2645 [[agents]])', () => { + test('#2406: does not emit [agents.] role tables — the standalone agents/.toml is the sole canonical source', () => { const result = generateCodexConfigBlock(agents); - // One [agents.] header per agent — no [[agents]] sequence. - assert.ok(result.includes('[agents.gsd-executor]'), 'executor has struct header'); - assert.ok(result.includes('[agents.gsd-planner]'), 'planner has struct header'); - // Struct format uses the key as the name; no name = field. - assert.ok(!result.includes('name = "gsd-executor"'), 'no name field in struct format'); - assert.ok(!result.includes('name = "gsd-planner"'), 'no name field in struct format'); - assert.ok(!result.includes('[[agents]]'), 'no sequence format headers'); + // Codex auto-discovers standalone TOMLs under $CODEX_HOME/agents/. A + // config.toml [agents.] table pointing config_file back at that + // same file is a SECOND registration of the same role and made Codex log + // "Ignoring malformed agent role definition: duplicate agent role name" + // once per agent. Zero role headers and zero config_file lines proves + // the duplication is gone. + assert.ok(!result.includes('[agents.gsd-executor]'), 'no executor role header'); + assert.ok(!result.includes('[agents.gsd-planner]'), 'no planner role header'); + assert.ok(!result.includes('config_file'), 'no config_file line at all'); + assert.ok(!result.includes('description = "Executes plans"'), 'no per-agent description leaks into config.toml'); + assert.ok(!result.includes('[[agents]]'), 'no [[agents]] sequence format either'); }); - test('#2727: block is a valid TOML struct shape (no [[agents]] sequence headers)', () => { + test('#2406: block is a valid TOML shape with exactly one [agents] table and zero [agents.*] sub-tables', () => { const result = generateCodexConfigBlock(agents); - // Must not contain [[agents]] array-of-tables syntax (rejected by Codex 0.124.0). assert.ok(!result.includes('[[agents]]'), 'no [[agents]] sequence format present'); - // Must contain [agents.] struct headers. + const bareAgentsHeaders = (result.match(/^\[agents\]\s*$/gm) || []).length; + assert.strictEqual(bareAgentsHeaders, 1, 'exactly one bare [agents] dispatch-tuning table'); const structHeaders = (result.match(/^\[agents\.[^\]]+\]\s*$/gm) || []).length; - assert.strictEqual(structHeaders, 2, 'one [agents.] struct header per agent'); + assert.strictEqual(structHeaders, 0, 'zero [agents.] struct headers — role tables removed (#2406)'); }); - test('includes per-agent sections with relative paths (no targetDir)', () => { - const result = generateCodexConfigBlock(agents); - assert.ok(result.includes('[agents.gsd-executor]'), 'has executor entry'); - assert.ok(result.includes('[agents.gsd-planner]'), 'has planner entry'); - assert.ok(result.includes('config_file = "agents/gsd-executor.toml"'), 'relative config_file without targetDir'); - assert.ok(result.includes('"Executes plans"'), 'has executor description'); - }); + test('#2406: output is unaffected by agents/targetDir — no per-agent content is derived from either', () => { + const withAgents = generateCodexConfigBlock(agents); + const withoutAgents = generateCodexConfigBlock([]); + assert.strictEqual(withAgents, withoutAgents, 'agents list no longer influences the emitted block'); - test('uses absolute config_file paths when targetDir is provided', () => { - const result = generateCodexConfigBlock(agents, '/home/user/.codex'); - assert.ok(result.includes('config_file = "/home/user/.codex/agents/gsd-executor.toml"'), 'absolute executor path'); - assert.ok(result.includes('config_file = "/home/user/.codex/agents/gsd-planner.toml"'), 'absolute planner path'); - assert.ok(!result.includes('config_file = "agents/'), 'no relative paths when targetDir given'); - }); - - test('#2727: emits [agents.] struct format by default (Codex 0.124.0+)', () => { - const result = generateCodexConfigBlock(agents); - // Codex 0.124.0 expects [agents.] struct format, not [[agents]] sequence format. - // [[agents]] was introduced in #2645 but is rejected by codex-cli 0.124.0 with - // "invalid type: sequence, expected struct AgentsToml". - assert.ok(!result.includes('[[agents]]'), 'should not emit [[agents]] sequence format'); - assert.ok(result.includes('[agents.'), 'should emit [agents.] struct format'); - assert.ok(result.includes('[agents.gsd-executor]'), 'executor uses struct header'); - assert.ok(result.includes('[agents.gsd-planner]'), 'planner uses struct header'); - // Struct format must NOT have a name = field (name is the key, not a value) - assert.ok(!result.includes('name = "gsd-executor"'), 'no name field in struct format'); + const withTargetDir = generateCodexConfigBlock(agents, '/home/user/.codex'); + assert.strictEqual(withTargetDir, withAgents, 'targetDir no longer influences the emitted block'); + assert.ok(!withTargetDir.includes('config_file'), 'no config_file even when targetDir is provided'); + assert.ok(!withTargetDir.includes('/home/user/.codex'), 'targetDir path does not leak into the block'); }); }); @@ -1404,7 +1393,10 @@ describe('mergeCodexConfig', () => { assert.ok(fs.existsSync(configPath), 'file created'); const content = fs.readFileSync(configPath, 'utf8'); assert.ok(content.includes(GSD_CODEX_MARKER), 'has marker'); - assert.ok(content.includes('[agents.gsd-executor]'), 'has agent in struct format'); + // #2406: config.toml never gets an [agents.gsd-*] role table — the + // standalone agents/.toml written by installCodexConfig is the + // sole canonical registration Codex auto-discovers. + assert.ok(!content.includes('[agents.gsd-executor]'), 'no agent role table (canonical source is the standalone TOML)'); assert.ok(!content.includes('[features]'), 'no features section'); assert.ok(!content.includes('multi_agent'), 'no multi_agent'); }); @@ -1423,8 +1415,10 @@ describe('mergeCodexConfig', () => { const content = fs.readFileSync(configPath, 'utf8'); assert.ok(content.includes('[model]'), 'preserves user content'); - assert.ok(content.includes('Updated description'), 'has new description'); - assert.ok(content.includes('[agents.gsd-planner]'), 'has new agent in struct format'); + // #2406: description text is per-agent metadata carried only by the + // standalone TOML now — it no longer leaks into config.toml. + assert.ok(!content.includes('Updated description'), 'no per-agent description in config.toml'); + assert.ok(!content.includes('[agents.gsd-planner]'), 'no agent role table (canonical source is the standalone TOML)'); // Verify no duplicate markers const markerCount = (content.match(new RegExp(GSD_CODEX_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length; assert.strictEqual(markerCount, 1, 'exactly one marker'); @@ -1439,7 +1433,7 @@ describe('mergeCodexConfig', () => { const content = fs.readFileSync(configPath, 'utf8'); assert.ok(content.includes('[model]'), 'preserves user content'); assert.ok(content.includes(GSD_CODEX_MARKER), 'adds marker'); - assert.ok(content.includes('[agents.gsd-executor]'), 'has agent in struct format'); + assert.ok(!content.includes('[agents.gsd-executor]'), 'no agent role table (canonical source is the standalone TOML)'); }); test('case 3 with existing [features]: preserves user features, does not inject GSD keys', () => { @@ -1453,10 +1447,10 @@ describe('mergeCodexConfig', () => { assert.ok(!content.includes('multi_agent'), 'does not inject multi_agent'); assert.ok(!content.includes('default_mode_request_user_input'), 'does not inject request_user_input'); assert.ok(content.includes(GSD_CODEX_MARKER), 'adds marker for agents block'); - assert.ok(content.includes('[agents.gsd-executor]'), 'has agent in struct format'); + assert.ok(!content.includes('[agents.gsd-executor]'), 'no agent role table (canonical source is the standalone TOML)'); }); - test('case 3 strips existing [agents.gsd-*] sections before appending fresh block', () => { + test('case 3 strips existing [agents.gsd-*] sections before appending fresh block (#2406: fresh block never re-adds them)', () => { const configPath = path.join(tmpDir, 'config.toml'); const existing = [ '[model]', @@ -1476,16 +1470,15 @@ describe('mergeCodexConfig', () => { mergeCodexConfig(configPath, sampleBlock); const content = fs.readFileSync(configPath, 'utf8'); - // After merge, GSD block is after the marker. Count [agents.gsd-executor] headers: - // exactly one should exist (the one in the freshly-written GSD block). + // The pre-existing legacy [agents.gsd-executor] role table is a leaked + // GSD section (stripLeakedGsdCodexSections) and the fresh block never + // re-adds one (#2406) — zero role tables should remain anywhere. const gsdStructCount = (content.match(/^\[agents\.gsd-executor\]\s*$/gm) || []).length; const markerCount = (content.match(new RegExp(GSD_CODEX_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length; - // Struct format does not use name = field - assert.ok(!content.match(/^name = "gsd-executor"/m), 'no name = field in struct format'); assert.ok(content.includes('[model]'), 'preserves user content'); assert.ok(content.includes('[agents.custom-agent]'), 'preserves non-GSD agent section'); - assert.strictEqual(gsdStructCount, 1, 'keeps exactly one [agents.gsd-executor] struct entry'); + assert.strictEqual(gsdStructCount, 0, 'legacy [agents.gsd-executor] struct entry is removed and not regrown'); assert.strictEqual(markerCount, 1, 'adds exactly one marker block'); assert.ok(!/\r?\n{3,}# GSD Agent Configuration/.test(content), 'does not leave extra blank lines before marker block'); }); @@ -1512,13 +1505,13 @@ describe('mergeCodexConfig', () => { const featuresCount = (content.match(/^\[features\]\s*$/gm) || []).length; assert.strictEqual(featuresCount, 1, 'exactly one [features] section'); assert.ok(content.includes('other_feature = true'), 'preserves user feature keys'); - assert.ok(content.includes('[agents.gsd-executor]'), 'has agent in struct format'); + assert.ok(!content.includes('[agents.gsd-executor]'), 'no agent role table (canonical source is the standalone TOML)'); // Verify no duplicate markers const markerCount = (content.match(new RegExp(GSD_CODEX_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length; assert.strictEqual(markerCount, 1, 'exactly one marker'); }); - test('case 2 does not inject feature keys', () => { + test('case 2 does not inject feature keys, and drops a legacy [agents.gsd-old] role table on reinstall (#2406)', () => { const configPath = path.join(tmpDir, 'config.toml'); const manualContent = '[features]\nother_feature = true\n\n' + GSD_CODEX_MARKER + '\n[agents.gsd-old]\ndescription = "old"\n'; fs.writeFileSync(configPath, manualContent); @@ -1529,10 +1522,64 @@ describe('mergeCodexConfig', () => { assert.ok(!content.includes('multi_agent'), 'does not inject multi_agent'); assert.ok(!content.includes('default_mode_request_user_input'), 'does not inject request_user_input'); assert.ok(content.includes('other_feature = true'), 'preserves user feature'); - assert.ok(content.includes('[agents.gsd-executor]'), 'has agent from fresh block in struct format'); + // #2406: the pre-existing managed [agents.gsd-old] role table (below the + // marker, from a pre-fix install) is truncated away by Case 2's + // marker-truncate, and the fresh block never re-adds a role table. + assert.ok(!content.includes('[agents.gsd-old]'), 'legacy managed role table removed'); + assert.ok(!content.includes('[agents.gsd-executor]'), 'no agent role table (canonical source is the standalone TOML)'); }); - test('case 2 strips leaked [agents] and [agents.gsd-*] from before content', () => { + test('#2406: update over a legacy 1.7.0-shape install removes duplicate [agents.gsd-*] role registrations, preserves unrelated user config, and is idempotent on rerun', () => { + const configPath = path.join(tmpDir, 'config.toml'); + // Simulate a config.toml produced by the pre-fix installer: a managed + // block with BOTH the bare [agents] dispatch-tuning table (holding the + // user's own max_threads = 4 alongside GSD's stale max_depth = 2) AND + // [agents.gsd-*] role tables duplicating what the standalone TOMLs + // already register — plus an unrelated user [model] table above the + // marker that must survive untouched. + const legacyInstall = [ + '[model]', + 'name = "o3"', + '', + GSD_CODEX_MARKER, + '', + '[agents]', + 'max_threads = 4', + 'max_depth = 2', + '', + '[agents.gsd-executor]', + 'description = "Executes plans"', + 'config_file = "agents/gsd-executor.toml"', + '', + '[agents.gsd-planner]', + 'description = "Creates plans"', + 'config_file = "agents/gsd-planner.toml"', + '', + ].join('\n'); + fs.writeFileSync(configPath, legacyInstall); + + mergeCodexConfig(configPath, sampleBlock); + const first = fs.readFileSync(configPath, 'utf8'); + + // Legacy duplicate role registrations removed. + assert.strictEqual((first.match(/^\[agents\.gsd-/gm) || []).length, 0, 'zero [agents.gsd-*] role tables remain'); + assert.ok(!first.includes('config_file'), 'no config_file line remains'); + // Unrelated user config preserved. + assert.ok(first.includes('[model]') && first.includes('name = "o3"'), 'preserves unrelated user [model] table'); + // User's own AgentsToml scalar tuning preserved; GSD's max_depth re-pinned to 1. + assert.ok(first.includes('max_threads = 4'), 'preserves user max_threads scalar'); + assert.match(first, /max_depth = 1/, 're-pins GSD-managed max_depth to 1'); + assert.doesNotMatch(first, /max_depth = 2/, 'stale legacy max_depth value is gone'); + assert.strictEqual((first.match(/^\[agents\]\s*$/gm) || []).length, 1, 'exactly one [agents] table (no duplicate)'); + assert.equal(validateCodexConfigSchema(first).ok, true, 'the migrated config still validates'); + + // Idempotent: running install/merge again produces byte-identical output — no regrowth. + mergeCodexConfig(configPath, sampleBlock); + const second = fs.readFileSync(configPath, 'utf8'); + assert.strictEqual(first, second, 'second merge is byte-identical — removed registrations do not regrow'); + }); + + test('case 2 strips leaked [agents] and [agents.gsd-*] from before content, and does not regrow a role table (#2406)', () => { const configPath = path.join(tmpDir, 'config.toml'); const brokenContent = [ '[features]', @@ -1559,7 +1606,9 @@ describe('mergeCodexConfig', () => { const content = fs.readFileSync(configPath, 'utf8'); assert.ok(content.includes('child_agents_md = false'), 'preserves user feature keys'); - assert.ok(content.includes('[agents.gsd-executor]'), 'has agent from fresh block in struct format'); + // #2406: neither the leaked pre-marker role table nor the legacy + // post-marker managed one survives — the fresh block emits none. + assert.ok(!content.includes('[agents.gsd-executor]'), 'no agent role table anywhere in the file'); // Verify the leaked [agents] table header above marker was stripped const markerIndex = content.indexOf(GSD_CODEX_MARKER); const beforeMarker = content.substring(0, markerIndex); @@ -1567,7 +1616,7 @@ describe('mergeCodexConfig', () => { assert.ok(!beforeMarker.includes('[agents.gsd-'), 'no leaked [agents.gsd-*] above marker'); }); - test('case 2 strips leaked GSD-managed sections above marker in CRLF files', () => { + test('case 2 strips leaked GSD-managed sections above marker in CRLF files, and does not regrow a role table (#2406)', () => { const configPath = path.join(tmpDir, 'config.toml'); const brokenContent = [ '[features]', @@ -1599,13 +1648,14 @@ describe('mergeCodexConfig', () => { assert.ok(content.includes('child_agents_md = false'), 'preserves user feature keys'); assert.strictEqual(countMatches(beforeMarker, /^\[agents\]\s*$/gm), 0, 'removes leaked [agents] above marker'); assert.strictEqual(countMatches(beforeMarker, /^\[agents\.gsd-executor\]\s*$/gm), 0, 'removes leaked GSD agent section above marker'); - // New struct format: exactly one [agents.gsd-executor] header in the GSD block (after marker) - assert.strictEqual(countMatches(content, /^\[agents\.gsd-executor\]\s*$/gm), 1, 'exactly one struct agent header in GSD block'); + // #2406: the fresh block never emits a role table, so zero remain + // anywhere in the file — not just above the marker. + assert.strictEqual(countMatches(content, /^\[agents\.gsd-executor\]\s*$/gm), 0, 'zero role tables anywhere'); assert.strictEqual(countMatches(content, /name = "gsd-executor"/g), 0, 'no name = field in struct format'); assertUsesOnlyEol(content, '\r\n'); }); - test('case 2 strips bare [agents] tables (invalid in current Codex schema, #2760) and removes leaked GSD sections in CRLF files', () => { + test('case 2 strips bare [agents] tables (invalid in current Codex schema, #2760) and removes leaked GSD sections in CRLF files, without regrowing a role table (#2406)', () => { const configPath = path.join(tmpDir, 'config.toml'); const brokenContent = [ '[features]', @@ -1652,8 +1702,9 @@ describe('mergeCodexConfig', () => { parsedBefore.features && parsedBefore.features.child_agents_md === false, 'preserves user feature keys above marker', ); - // New struct format: exactly one [agents.gsd-executor] in the GSD block (after marker) - assert.strictEqual(countMatches(content, /^\[agents\.gsd-executor\]\s*$/gm), 1, 'exactly one struct agent header in GSD block'); + // #2406: the fresh block never emits a role table, so zero remain + // anywhere in the file — not just above the marker. + assert.strictEqual(countMatches(content, /^\[agents\.gsd-executor\]\s*$/gm), 0, 'zero role tables anywhere'); assert.strictEqual(countMatches(content, /name = "gsd-executor"/g), 0, 'no name = field in struct format'); assertUsesOnlyEol(content, '\r\n'); }); @@ -1726,7 +1777,13 @@ describe('installCodexConfig (integration)', () => { assert.ok(fs.existsSync(configPath), 'config.toml exists'); const config = fs.readFileSync(configPath, 'utf8'); assert.ok(config.includes(GSD_CODEX_MARKER), 'has GSD marker'); - assert.ok(config.includes('[agents.gsd-executor]'), 'has executor agent in struct format'); + // #2406: config.toml must NOT register agent roles — the standalone + // agents/.toml (verified below) is the sole canonical source + // Codex auto-discovers. A role table here would be a second, + // duplicate registration of the same role. + assert.ok(!config.includes('[agents.gsd-executor]'), 'no executor role table in config.toml'); + assert.strictEqual((config.match(/^\[agents\.gsd-/gm) || []).length, 0, 'zero [agents.gsd-*] role tables of any kind'); + assert.strictEqual((config.match(/^config_file = /gm) || []).length, 0, 'zero config_file lines'); assert.ok(!config.includes('multi_agent'), 'no feature flags'); // Verify per-agent .toml files @@ -1895,18 +1952,26 @@ describe('Codex install hook configuration (e2e)', () => { assertUsesOnlyEol(content, '\n'); }); - test('config_file paths are absolute using CODEX_HOME', () => { + test('#2406: config.toml carries no config_file entries — standalone agents/*.toml under CODEX_HOME are the sole canonical source', () => { runCodexInstall(codexHome); const content = readCodexConfig(codexHome); - const agentsDir = path.join(codexHome, 'agents').replace(/\\/g, '/'); - // All config_file values should use absolute paths + // config.toml previously carried a `config_file = "/.toml"` + // line per role, pointing back at the standalone TOML Codex already + // auto-discovers under $CODEX_HOME/agents/ — a second, duplicate + // registration of the same role that produced one + // "Ignoring malformed agent role definition: duplicate agent role name" + // warning per agent. That line is gone entirely now. const configFileLines = content.split(/\r?\n/).filter(l => l.startsWith('config_file = ')); - assert.ok(configFileLines.length > 0, 'has config_file entries'); - for (const line of configFileLines) { - assert.ok(line.includes(agentsDir), `absolute path in: ${line}`); - } - assert.ok(!content.includes('config_file = "agents/'), 'no relative config_file paths'); + assert.deepStrictEqual(configFileLines, [], 'config.toml has zero config_file entries'); + + // The standalone per-agent TOMLs are still written under CODEX_HOME/agents/ + // and are what Codex auto-discovers. + const agentsDir = path.join(codexHome, 'agents'); + const tomlFiles = fs.existsSync(agentsDir) + ? fs.readdirSync(agentsDir).filter((f) => f.startsWith('gsd-') && f.endsWith('.toml')) + : []; + assert.ok(tomlFiles.length > 0, 'standalone gsd-*.toml files exist under CODEX_HOME/agents/'); }); test('re-install repairs non-boolean keys trapped under [features] by previous install (#1379)', () => { @@ -2085,7 +2150,17 @@ describe('Codex install hook configuration (e2e)', () => { assert.strictEqual(countMatches(content, /^\[features\]\s*$/gm), 1, 'keeps one [features] section'); assert.strictEqual(countMatches(content, /^hooks = true$/gm), 1, 'adds one codex_hooks key'); assert.ok(content.indexOf('hooks = true') > content.indexOf('[features]'), 'adds codex_hooks after the existing EOF features header'); - assert.ok(content.indexOf('hooks = true') < content.indexOf('[agents.'), 'keeps codex_hooks before the first managed [agents.] struct entry'); + // In this EOF-without-trailing-newline edge case, the pre-existing + // [features] header has no blank-line boundary to close it, so the + // appended GSD marker/ownership comment textually falls *inside* what + // reads as the [features] section body, and `hooks = true` is inserted + // at the end of that body — after the marker, not before it. That + // ordering is unrelated to #2406 (verified unchanged against + // origin/next's install.js) and #2406 removed the [agents.] role + // tables that used to anchor this assertion, so anchor on the bare + // [agents] dispatch-tuning table instead — codex_hooks always lands + // before it. + assert.ok(content.indexOf('hooks = true') < content.indexOf('[agents]'), 'keeps codex_hooks before the [agents] dispatch-tuning table'); assertNoDraftRootKeys(content); }); @@ -2223,7 +2298,10 @@ describe('Codex install hook configuration (e2e)', () => { assert.strictEqual(countMatches(content, /^features\.codex_hooks = true$/gm), 0, 'does not append an invalid dotted codex_hooks key'); assert.strictEqual(countMatches(content, /^\[features\]\s*$/gm), 0, 'does not prepend a features table'); assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 0, 'does not add the GSD hook block when codex_hooks cannot be enabled safely'); - assert.ok(content.includes('[agents.gsd-executor]'), 'still installs the managed agent block in struct format'); + // #2406: config.toml no longer carries an [agents.] role table — + // it still installs the managed [agents] dispatch-tuning block. + assert.ok(content.includes(GSD_CODEX_MARKER), 'still installs the managed GSD block'); + assert.ok(!content.includes('[agents.gsd-executor]'), 'no agent role table (canonical source is the standalone TOML)'); assertNoDraftRootKeys(content); }); @@ -2244,7 +2322,10 @@ describe('Codex install hook configuration (e2e)', () => { assert.strictEqual(countMatches(content, /^features\.codex_hooks = true$/gm), 0, 'does not append an invalid dotted codex_hooks key'); assert.strictEqual(countMatches(content, /^\[features\]\s*$/gm), 0, 'does not prepend a features table'); assert.strictEqual(countMatches(content, /gsd-check-update\.js/g), 0, 'does not add the GSD hook block when codex_hooks cannot be enabled safely'); - assert.ok(content.includes('[agents.gsd-executor]'), 'still installs the managed agent block in struct format'); + // #2406: config.toml no longer carries an [agents.] role table — + // it still installs the managed [agents] dispatch-tuning block. + assert.ok(content.includes(GSD_CODEX_MARKER), 'still installs the managed GSD block'); + assert.ok(!content.includes('[agents.gsd-executor]'), 'no agent role table (canonical source is the standalone TOML)'); assertNoDraftRootKeys(content); }); @@ -3344,18 +3425,21 @@ describe('#2760 fix 2 — Strip purges invalid legacy [agents] / [[agents]] rega const parsed = parseTomlToObject(content); // Bare [agents] would have left { default, extra_key } as scalar leaves - // on parsed.agents. After strip + struct emit, every key under agents - // must itself be a table (the gsd-* struct form). + // on parsed.agents. After strip + re-emit, only GSD's own managed + // AgentsToml scalar (max_depth) remains — #2406 stopped emitting + // [agents.] role sub-tables entirely, so `agents` stays a flat + // scalar-only object, not a table-of-tables. assert.ok( parsed.agents && typeof parsed.agents === 'object' && !Array.isArray(parsed.agents), - 'agents must be a table-of-tables in parsed structure, got: ' + typeof parsed.agents + 'agents must be an object in parsed structure, got: ' + typeof parsed.agents ); assert.equal(parsed.agents.default, undefined, 'bare [agents] default key must be stripped'); assert.equal(parsed.agents.extra_key, undefined, 'bare [agents] extra_key must be stripped'); + assert.equal(parsed.agents.max_depth, 1, 'GSD-managed max_depth is the only surviving [agents] key'); const gsdAgents = Object.keys(parsed.agents).filter((k) => k.startsWith('gsd-')); - assert.ok( - gsdAgents.length > 0 && gsdAgents.every((k) => typeof parsed.agents[k] === 'object'), - 'agents.gsd-* struct form must be present: ' + JSON.stringify(Object.keys(parsed.agents)) + assert.deepStrictEqual( + gsdAgents, [], + 'no [agents.gsd-*] role sub-tables (#2406) — canonical registration lives only in the standalone TOMLs: ' + JSON.stringify(Object.keys(parsed.agents)) ); // User's unrelated [model] section preserved structurally. @@ -3385,16 +3469,18 @@ describe('#2760 fix 2 — Strip purges invalid legacy [agents] / [[agents]] rega const parsed = parseTomlToObject(content); // [[agents]] sequence form would parse to Array — after strip it must be - // a table-of-tables with gsd-* struct keys. + // a plain object holding only GSD's own managed max_depth scalar (#2406 + // stopped emitting [agents.] role sub-tables entirely). assert.ok( parsed.agents && typeof parsed.agents === 'object' && !Array.isArray(parsed.agents), - 'agents must be a table-of-tables in parsed structure (sequence form must be stripped), got: ' + 'agents must be an object in parsed structure (sequence form must be stripped), got: ' + (Array.isArray(parsed.agents) ? 'array' : typeof parsed.agents) ); + assert.equal(parsed.agents.max_depth, 1, 'GSD-managed max_depth is the only surviving [agents] key'); const gsdAgents = Object.keys(parsed.agents).filter((k) => k.startsWith('gsd-')); - assert.ok( - gsdAgents.length > 0, - 'agents.gsd-* struct form must be present: ' + JSON.stringify(Object.keys(parsed.agents)) + assert.deepStrictEqual( + gsdAgents, [], + 'no [agents.gsd-*] role sub-tables (#2406) — canonical registration lives only in the standalone TOMLs: ' + JSON.stringify(Object.keys(parsed.agents)) ); // User's unrelated [projects."/tmp/x"] section preserved structurally. diff --git a/tests/codex-declarative-reference.test.cjs b/tests/codex-declarative-reference.test.cjs index 378ace06a..619b1e113 100644 --- a/tests/codex-declarative-reference.test.cjs +++ b/tests/codex-declarative-reference.test.cjs @@ -228,13 +228,18 @@ test('upgrade 2 — the managed config block writes [agents] max_depth = 1', () path.join(os.homedir(), '.codex'), ); assert.match(block, /\[agents\]\nmax_depth = 1\n/, 'the block pins max_depth = 1 on a bare [agents] table'); - // The bare [agents] scalar table coexists with the [agents.gsd-*] role tables. - assert.match(block, /\[agents\.gsd-foo\]/); + // #2406: the bare [agents] scalar table is the ONLY [agents]-namespaced + // content the block emits — no [agents.gsd-*] role sub-tables. Codex + // auto-discovers roles from the standalone agents/.toml files, so a + // config.toml role table pointing config_file back at that same file was a + // second, duplicate registration of the same role. + assert.doesNotMatch(block, /\[agents\.gsd-foo\]/, 'no [agents.gsd-foo] role table'); + assert.doesNotMatch(block, /config_file/, 'no config_file line'); }); test('upgrade 2 — validateCodexConfigSchema accepts the managed [agents] block but still rejects break-forms', () => { const block = install.generateCodexConfigBlock([{ name: 'gsd-foo', description: 'Foo' }], path.join(os.homedir(), '.codex')); - assert.equal(install.validateCodexConfigSchema(block).ok, true, 'known-scalar [agents] + role tables must validate'); + assert.equal(install.validateCodexConfigSchema(block).ok, true, 'known-scalar [agents] table must validate'); // Still rejects the actual #2760 break-forms. assert.equal(install.validateCodexConfigSchema('[[agents]]\nname = "x"\n').ok, false, '[[agents]] sequence still rejected'); @@ -275,13 +280,17 @@ test('upgrade 2 — install preserves the user\'s own AgentsToml scalars (max_th assert.doesNotMatch(merged, /max_depth = 9/, 'user max_depth is overridden by the GSD-managed value'); assert.equal((merged.match(/^\[agents\]$/mg) || []).length, 1, 'exactly one managed [agents] table (no duplicate)'); assert.equal(install.validateCodexConfigSchema(merged).ok, true, 'the merged config still validates'); + // #2406: the fresh block never emitted an [agents.gsd-foo] role table in + // the first place — canonical registration lives only in the standalone + // TOML, so there is nothing gsd-foo-shaped in config.toml to begin with. + assert.doesNotMatch(merged, /gsd-foo/, 'no [agents.gsd-foo] role table in the merged config'); // Symmetric: uninstall restores the user's scalars and drops GSD's max_depth. const uninstalled = install.stripGsdFromCodexConfig(merged); assert.match(uninstalled, /max_threads = 4/, 'uninstall restores the user\'s max_threads'); assert.match(uninstalled, /interrupt_message = false/, 'uninstall restores interrupt_message'); assert.doesNotMatch(uninstalled, /max_depth/, 'uninstall drops the GSD-managed max_depth'); - assert.doesNotMatch(uninstalled, /gsd-foo/, 'uninstall removes the gsd role table'); + assert.doesNotMatch(uninstalled, /gsd-foo/, 'no [agents.gsd-foo] role table remains after uninstall either'); assert.equal(install.validateCodexConfigSchema(uninstalled).ok, true, 'the restored config validates'); } finally { cleanup(tmp); diff --git a/tests/fixtures/golden-install-parity/codex.json b/tests/fixtures/golden-install-parity/codex.json index 0409958de..fb14c0690 100644 --- a/tests/fixtures/golden-install-parity/codex.json +++ b/tests/fixtures/golden-install-parity/codex.json @@ -140,7 +140,7 @@ "agents/gsd-user-profiler.toml": "b9c244bb8fbf8140", "agents/gsd-verifier.md": "4ac4b860e2504374", "agents/gsd-verifier.toml": "8ed9fb961409e894", - "config.toml": "b5f627b42f060910", + "config.toml": "aa0b484a070f603c", "gsd-core/.gsd-runtime": "243b0dc9b847e66c", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",