fix(#2406): stop Codex config.toml from double-registering agent roles (#2432)

* fix(#2406): stop Codex config.toml from double-registering agent roles

generateCodexConfigBlock emitted an [agents.<name>] role table per agent
pointing config_file back at the standalone agents/<name>.toml Codex
already auto-discovers, so every install declared each role twice in
one config layer and Codex logged a duplicate-role warning per agent.
Remove the redundant role-table loop; the standalone per-agent TOML is
now the sole canonical registration source. The existing marker-truncate
and leaked-section stripping in mergeCodexConfig already clean up legacy
[agents.gsd-*] tables from prior installs, so updates converge to zero
duplicates without any new migration path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#2406): regenerate fixtures + lint gate-prep

* fix(#2406): repair failing tests after gate verification

* docs(#2406): add changeset for Codex duplicate agent-role fix

Adds the missing .changeset/*.md fragment for the Codex config.toml
double-registration fix, closing the PR-gate finding from review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#2406): backfill changeset pr (#2432)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-19 12:53:22 -04:00
committed by GitHub
parent 88d6b392af
commit cd6665d73b
9 changed files with 214 additions and 114 deletions

View File

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

View File

@@ -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/<name>.toml`) are auto-discovered by Codex and are the
* SOLE canonical registration source for each role. This block therefore no
* longer emits `[agents.<name>]` 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.<name>]` 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.<name>] 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');
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.<name>] 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.<name>] 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.<name>] struct format (Codex 0.120.0+, replaces #2645 [[agents]])', () => {
test('#2406: does not emit [agents.<name>] role tables — the standalone agents/<name>.toml is the sole canonical source', () => {
const result = generateCodexConfigBlock(agents);
// One [agents.<name>] 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.<name>] 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.<name>] 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.<name>] struct header per agent');
assert.strictEqual(structHeaders, 0, 'zero [agents.<name>] 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.<name>] struct format by default (Codex 0.124.0+)', () => {
const result = generateCodexConfigBlock(agents);
// Codex 0.124.0 expects [agents.<name>] 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.<name>] 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/<name>.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/<name>.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 = "<absolute agents dir>/<name>.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.<name>] 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.<name>] 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.<name>] 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.<name>] 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.<name>] 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.<name>] 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.

View File

@@ -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/<name>.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);

View File

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