fix(#2602): discover project-local Codex agents (#2623)

* fix(#2602): discover project-local Codex agents

- Select an existing local Codex agents directory before global fallback
- Prove init reports the canonical local installation through compiled CJS

* test(#2602): lock Codex agent precedence

- Cover override, local authority, global fallback, and runtime compatibility
- Exercise installed state through the compiled resolver

* fix(#2602): resolve local Codex agent skills

- Pass the canonical project root to the non-Claude persona fallback
- Cover nested-Codex fallback and Claude compatibility through the CLI

* test(#2602): cover local Codex validation status

- Assert emitted validate and health commands use the project-local install
- Preserve empty local-directory authority beside complete global agents

* fix(#2602): align validation with local Codex discovery

- Pass the resolved runtime and project root to health W010
- Resolve the validate-agents runtime before checking installation status

* test(#2602): cover local Codex docs status

- Assert docs-init reports an authoritative empty local install as unhealthy

* fix(#2602): align docs with local Codex discovery

- Pass the resolved runtime and canonical project root to the shared agent checker

* fix(#2602): honor agent-skills runtime override

- Resolve agent-skills fallback runtime through the canonical project resolver
- Cover conflicting config and GSD_RUNTIME values through the emitted CLI

* fix(#2602): ignore non-directory local agents paths

- Treat only a local Codex agents directory as authoritative
- Cover regular-file fallback through the emitted install checker

* chore(#2602): add changelog fragment

- record the user-visible local Codex agent discovery fix for PR #2623

* fix(#2602): align local agent discovery with runtime policy

- Resolve Codex's local config directory through the canonical runtime policy
- Use test-managed cleanup for local-agent discovery coverage

* fix(#2602): discover local agents across runtimes

- Prefer manifest-backed project-local installs for non-Claude runtimes
- Respect runtime-specific local install roots and preserve global fallback behavior
- Cover native, partial, cross-runtime, and project-root local discovery

* fix(#2602): preserve agent discovery fallback

- Fall back globally when local-install probes fail
- Document and test symlink rejection
- Align the changeset with repository format

* fix(#2602): reuse local directory policy

- Resolve runtimes without local config through the canonical sentinel
- Document the manifest gate and refresh the context index

---------

Co-authored-by: Daniel E. <daniel.e@teachingstrategies.com>
Co-authored-by: Rezolv <dave@sienkowski.com>
This commit is contained in:
Daniel Einspanjer
2026-07-31 21:20:46 -04:00
committed by GitHub
parent 7b204ad2ac
commit f0ff23635e
10 changed files with 495 additions and 12 deletions

View File

@@ -31,6 +31,7 @@ const RUNTIME_HOMES_PATH = path.join(
const agentInstallCheck = require(AGENT_INSTALL_CHECK_PATH);
const { getGlobalConfigDir } = require(RUNTIME_HOMES_PATH);
const { getDirName } = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'runtime-name-policy.cjs'));
// Get EXPECTED_AGENTS from model-profiles (same source of truth)
const MODEL_PROFILES = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'model-profiles.cjs')).MODEL_PROFILES;
@@ -40,12 +41,15 @@ const EXPECTED_AGENTS = Object.keys(MODEL_PROFILES);
let savedAgentsDir;
let savedRuntime;
let savedCodexHome;
beforeEach(() => {
savedAgentsDir = process.env['GSD_AGENTS_DIR'];
savedRuntime = process.env['GSD_RUNTIME'];
savedCodexHome = process.env['CODEX_HOME'];
delete process.env['GSD_AGENTS_DIR'];
delete process.env['GSD_RUNTIME'];
delete process.env['CODEX_HOME'];
});
afterEach(() => {
@@ -59,8 +63,34 @@ afterEach(() => {
} else {
process.env['GSD_RUNTIME'] = savedRuntime;
}
if (savedCodexHome === undefined) {
delete process.env['CODEX_HOME'];
} else {
process.env['CODEX_HOME'] = savedCodexHome;
}
});
function createCompleteAgents(agentsDir) {
fs.mkdirSync(agentsDir, { recursive: true });
for (const agent of EXPECTED_AGENTS) {
fs.writeFileSync(path.join(agentsDir, `${agent}.toml`), `name = "${agent}"\n`);
}
}
function markLocalGsdInstall(configDir) {
fs.writeFileSync(
path.join(configDir, 'gsd-file-manifest.json'),
JSON.stringify({ files: {} }),
);
}
function createCompleteLocalGsdInstall(configDir) {
const agentsDir = path.join(configDir, 'agents');
createCompleteAgents(agentsDir);
markLocalGsdInstall(configDir);
return agentsDir;
}
// ─── 1. getAgentsDir behaviour ────────────────────────────────────────────────
describe('getAgentsDir', () => {
@@ -94,6 +124,198 @@ describe('getAgentsDir', () => {
const fromClaude = agentInstallCheck.getAgentsDir('claude');
assert.strictEqual(fromModule, fromClaude);
});
test('a manifest-backed local runtime installation wins over global agents', (t) => {
const projectRoot = createTempDir('gsd-local-codex-');
const globalHome = createTempDir('gsd-global-codex-');
const localAgentsDir = createCompleteLocalGsdInstall(path.join(projectRoot, '.codex'));
t.after(() => cleanup(projectRoot));
t.after(() => cleanup(globalHome));
createCompleteAgents(path.join(globalHome, 'agents'));
process.env['CODEX_HOME'] = globalHome;
assert.strictEqual(agentInstallCheck.getAgentsDir('codex', projectRoot), localAgentsDir);
assert.strictEqual(agentInstallCheck.checkAgentsInstalled('codex', projectRoot).agents_installed, true);
});
test('GSD_AGENTS_DIR remains terminal when a local Codex installation exists', (t) => {
const projectRoot = createTempDir('gsd-local-codex-');
const overrideDir = path.join(projectRoot, 'override-agents');
createCompleteLocalGsdInstall(path.join(projectRoot, '.codex'));
t.after(() => cleanup(projectRoot));
fs.mkdirSync(overrideDir, { recursive: true });
process.env['GSD_AGENTS_DIR'] = overrideDir;
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
assert.strictEqual(result.agents_dir, overrideDir);
assert.strictEqual(result.agents_installed, false);
assert.deepStrictEqual(result.missing_agents, EXPECTED_AGENTS);
});
test('a manifest-backed empty local directory is authoritative over complete global agents', (t) => {
const projectRoot = createTempDir('gsd-local-codex-');
const globalHome = createTempDir('gsd-global-codex-');
const localAgentsDir = path.join(projectRoot, '.codex', 'agents');
t.after(() => cleanup(projectRoot));
t.after(() => cleanup(globalHome));
fs.mkdirSync(localAgentsDir, { recursive: true });
markLocalGsdInstall(path.dirname(localAgentsDir));
createCompleteAgents(path.join(globalHome, 'agents'));
process.env['CODEX_HOME'] = globalHome;
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
assert.strictEqual(result.agents_dir, localAgentsDir);
assert.strictEqual(result.agents_installed, false);
assert.deepStrictEqual(result.missing_agents, EXPECTED_AGENTS);
});
test('Codex falls back to global agents when no local directory exists', (t) => {
const projectRoot = createTempDir('gsd-local-codex-');
const globalHome = createTempDir('gsd-global-codex-');
t.after(() => cleanup(projectRoot));
t.after(() => cleanup(globalHome));
createCompleteAgents(path.join(globalHome, 'agents'));
process.env['CODEX_HOME'] = globalHome;
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
assert.strictEqual(result.agents_dir, path.join(globalHome, 'agents'));
assert.strictEqual(result.agents_installed, true);
});
test('Codex falls back to global agents when the local candidate is a regular file', (t) => {
const projectRoot = createTempDir('gsd-local-codex-');
const globalHome = createTempDir('gsd-global-codex-');
const localCandidate = path.join(projectRoot, '.codex', 'agents');
t.after(() => cleanup(projectRoot));
t.after(() => cleanup(globalHome));
fs.mkdirSync(path.dirname(localCandidate), { recursive: true });
fs.writeFileSync(localCandidate, 'not an agents directory\n');
createCompleteAgents(path.join(globalHome, 'agents'));
process.env['CODEX_HOME'] = globalHome;
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
assert.strictEqual(result.agents_dir, path.join(globalHome, 'agents'));
assert.strictEqual(result.agents_installed, true);
});
test('Codex falls back to global agents when the local candidate cannot be inspected', (t) => {
const projectRoot = createTempDir('gsd-local-codex-');
const globalHome = createTempDir('gsd-global-codex-');
const localAgentsDir = path.join(projectRoot, '.codex', 'agents');
const realLstatSync = fs.lstatSync;
t.after(() => cleanup(projectRoot));
t.after(() => cleanup(globalHome));
fs.mkdirSync(localAgentsDir, { recursive: true });
markLocalGsdInstall(path.dirname(localAgentsDir));
createCompleteAgents(path.join(globalHome, 'agents'));
process.env['CODEX_HOME'] = globalHome;
t.mock.method(fs, 'lstatSync', function injectedLocalProbeFailure(target, ...args) {
if (target === localAgentsDir) {
throw Object.assign(new Error('injected EACCES'), { code: 'EACCES' });
}
return realLstatSync.call(fs, target, ...args);
});
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
assert.strictEqual(result.agents_dir, path.join(globalHome, 'agents'));
assert.strictEqual(result.agents_installed, true);
});
test('Codex does not follow a symlinked local agents directory', (t) => {
const projectRoot = createTempDir('gsd-local-codex-');
const globalHome = createTempDir('gsd-global-codex-');
const localConfigDir = path.join(projectRoot, '.codex');
const localAgentsDir = path.join(localConfigDir, 'agents');
const symlinkTarget = path.join(projectRoot, 'shared-agents');
t.after(() => cleanup(projectRoot));
t.after(() => cleanup(globalHome));
createCompleteAgents(symlinkTarget);
fs.mkdirSync(localConfigDir, { recursive: true });
markLocalGsdInstall(localConfigDir);
createCompleteAgents(path.join(globalHome, 'agents'));
process.env['CODEX_HOME'] = globalHome;
try {
fs.symlinkSync(symlinkTarget, localAgentsDir, process.platform === 'win32' ? 'junction' : 'dir');
} catch (error) {
if (error && ['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) {
t.skip('symlink creation is not available on this platform');
return;
}
throw error;
}
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
assert.strictEqual(result.agents_dir, path.join(globalHome, 'agents'));
assert.strictEqual(result.agents_installed, true);
});
test('a project-native agents directory without a GSD manifest does not override global agents', (t) => {
const projectRoot = createTempDir('gsd-local-codex-');
const globalHome = createTempDir('gsd-global-codex-');
const localAgentsDir = path.join(projectRoot, '.codex', 'agents');
t.after(() => cleanup(projectRoot));
t.after(() => cleanup(globalHome));
createCompleteAgents(localAgentsDir);
createCompleteAgents(path.join(globalHome, 'agents'));
process.env['CODEX_HOME'] = globalHome;
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
assert.strictEqual(result.agents_dir, path.join(globalHome, 'agents'));
assert.strictEqual(result.agents_installed, true);
});
test('a manifest-backed Cursor installation resolves from the project root', (t) => {
const projectRoot = createTempDir('gsd-local-cursor-');
const localAgentsDir = createCompleteLocalGsdInstall(path.join(projectRoot, getDirName('cursor')));
t.after(() => cleanup(projectRoot));
assert.strictEqual(agentInstallCheck.getAgentsDir('cursor', projectRoot), localAgentsDir);
assert.strictEqual(agentInstallCheck.checkAgentsInstalled('cursor', projectRoot).agents_installed, true);
});
test('a manifest-backed Cline installation resolves from the project root', (t) => {
const projectRoot = createTempDir('gsd-local-cline-');
const localAgentsDir = createCompleteLocalGsdInstall(projectRoot);
t.after(() => cleanup(projectRoot));
assert.strictEqual(agentInstallCheck.getAgentsDir('cline', projectRoot), localAgentsDir);
assert.strictEqual(agentInstallCheck.checkAgentsInstalled('cline', projectRoot).agents_installed, true);
});
test('a partial manifest-backed local installation remains selected and incomplete', (t) => {
const projectRoot = createTempDir('gsd-local-codex-');
const localConfigDir = path.join(projectRoot, '.codex');
const localAgentsDir = createCompleteLocalGsdInstall(localConfigDir);
const partialAgent = EXPECTED_AGENTS[0];
t.after(() => cleanup(projectRoot));
fs.writeFileSync(path.join(localAgentsDir, `${partialAgent}.md`), `# ${partialAgent}\n`);
fs.unlinkSync(path.join(localAgentsDir, `${partialAgent}.toml`));
fs.writeFileSync(
path.join(localConfigDir, 'gsd-file-manifest.json'),
JSON.stringify({ files: { [`agents/${partialAgent}.md`]: {}, [`agents/${partialAgent}.toml`]: {} } }),
);
const result = agentInstallCheck.checkAgentsInstalled('codex', projectRoot);
assert.strictEqual(result.agents_dir, localAgentsDir);
assert.strictEqual(result.agents_installed, false);
assert.deepStrictEqual(result.missing_agents, []);
assert.deepStrictEqual(result.incomplete_agents, [partialAgent]);
});
test('Claude and other runtimes ignore a supplied Codex-local candidate', (t) => {
const projectRoot = createTempDir('gsd-local-codex-');
t.after(() => cleanup(projectRoot));
createCompleteLocalGsdInstall(path.join(projectRoot, '.codex'));
assert.strictEqual(
agentInstallCheck.getAgentsDir('claude', projectRoot),
agentInstallCheck.getAgentsDir('claude'),
);
assert.strictEqual(
agentInstallCheck.getAgentsDir('cursor', projectRoot),
path.join(getGlobalConfigDir('cursor'), 'agents'),
);
});
});
// ─── 2. checkAgentsInstalled behaviour ───────────────────────────────────────

View File

@@ -42,6 +42,23 @@ function _createAgentsDir(configDir, agentNames = []) {
return agentsDir;
}
function createCompleteCodexAgents(configDir) {
const agentsDir = path.join(configDir, '.codex', AGENTS_DIR_NAME);
fs.mkdirSync(agentsDir, { recursive: true });
const files = {};
for (const name of EXPECTED_AGENTS) {
fs.writeFileSync(path.join(agentsDir, `${name}.md`), `# ${name}\n`);
fs.writeFileSync(path.join(agentsDir, `${name}.toml`), `name = "${name}"\n`);
files[`agents/${name}.md`] = {};
files[`agents/${name}.toml`] = {};
}
fs.writeFileSync(
path.join(configDir, '.codex', 'gsd-file-manifest.json'),
JSON.stringify({ files }),
);
return agentsDir;
}
// ─── Init command agent validation ──────────────────────────────────────────
describe('init commands: agents_installed field (#1371)', () => {
@@ -92,6 +109,30 @@ describe('init commands: agents_installed field (#1371)', () => {
assert.strictEqual(output.agents_installed, true);
});
test('init plan-phase reports the complete project-local Codex installation', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
const globalHome = path.join(tmpDir, 'global-codex');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({ runtime: 'codex' }),
);
createCompleteCodexAgents(tmpDir);
const canonicalRoot = fs.realpathSync(tmpDir);
const localAgentsDir = path.join(canonicalRoot, '.codex', AGENTS_DIR_NAME);
_createAgentsDir(globalHome, EXPECTED_AGENTS);
const result = runGsdTools('init plan-phase 1 --raw', tmpDir, { CODEX_HOME: globalHome });
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.project_root, canonicalRoot);
assert.strictEqual(output.agent_runtime, 'codex');
assert.strictEqual(output.agents_dir, localAgentsDir);
assert.strictEqual(output.agents_installed, true);
assert.deepStrictEqual(output.missing_agents, []);
});
test('init execute-phase includes missing_agents list when agents are missing', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
fs.mkdirSync(phaseDir, { recursive: true });
@@ -162,6 +203,67 @@ describe('validate health: agent installation check W010 (#1371)', () => {
});
});
// ─── Codex project-local validation status ──────────────────────────────────
describe('Codex project-local validation status', () => {
let tmpDir;
let globalHome;
beforeEach(() => {
tmpDir = createTempProject();
globalHome = path.join(tmpDir, 'global-codex');
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({ runtime: 'codex' }),
);
_createAgentsDir(globalHome, EXPECTED_AGENTS);
});
afterEach(() => {
cleanup(tmpDir);
});
test('validate agents and health use a complete project-local Codex installation', () => {
createCompleteCodexAgents(tmpDir);
const localAgentsDir = path.join(fs.realpathSync(tmpDir), '.codex', AGENTS_DIR_NAME);
const env = { CODEX_HOME: globalHome };
const validateResult = runGsdTools('validate agents --raw', tmpDir, env);
assert.ok(validateResult.success, `validate agents failed: ${validateResult.error}`);
const validateOutput = JSON.parse(validateResult.output);
assert.strictEqual(validateOutput.agents_dir, localAgentsDir);
assert.strictEqual(validateOutput.agents_found, true);
assert.deepStrictEqual(validateOutput.missing, []);
assert.deepStrictEqual(validateOutput.incomplete, []);
const healthResult = runGsdTools('validate health --raw', tmpDir, env);
assert.ok(healthResult.success, `validate health failed: ${healthResult.error}`);
const healthOutput = JSON.parse(healthResult.output);
assert.ok(!(healthOutput.warnings || []).some(warning => warning.code === 'W010'));
});
test('an empty project-local Codex directory remains authoritative for validate agents and health', () => {
fs.mkdirSync(path.join(tmpDir, '.codex', AGENTS_DIR_NAME), { recursive: true });
fs.writeFileSync(path.join(tmpDir, '.codex', 'gsd-file-manifest.json'), JSON.stringify({ files: {} }));
const localAgentsDir = path.join(fs.realpathSync(tmpDir), '.codex', AGENTS_DIR_NAME);
const env = { CODEX_HOME: globalHome };
const validateResult = runGsdTools('validate agents --raw', tmpDir, env);
assert.ok(validateResult.success, `validate agents failed: ${validateResult.error}`);
const validateOutput = JSON.parse(validateResult.output);
assert.strictEqual(validateOutput.agents_dir, localAgentsDir);
assert.strictEqual(validateOutput.agents_found, false);
assert.deepStrictEqual(validateOutput.installed, []);
assert.deepStrictEqual(validateOutput.incomplete, []);
assert.deepStrictEqual(validateOutput.missing, EXPECTED_AGENTS);
const healthResult = runGsdTools('validate health --raw', tmpDir, env);
assert.ok(healthResult.success, `validate health failed: ${healthResult.error}`);
const healthOutput = JSON.parse(healthResult.output);
assert.ok((healthOutput.warnings || []).some(warning => warning.code === 'W010'));
});
});
// ─── Copilot .agent.md detection (#1512) ────────────────────────────────────
describe('checkAgentsInstalled: Copilot .agent.md format (#1512)', () => {

View File

@@ -30,6 +30,7 @@ const TEST_ENV_BASE = {
WT_SESSION: '',
TMUX_PANE: '',
ZELLIJ_SESSION_NAME: '',
GSD_WORKSTREAM: '',
TTY: '',
SSH_TTY: '',
};
@@ -71,6 +72,13 @@ function readConfig(tmpDir) {
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
function markLocalGsdInstall(tmpDir) {
fs.writeFileSync(
path.join(tmpDir, '.codex', 'gsd-file-manifest.json'),
JSON.stringify({ files: {} }),
);
}
// Run agent-skills with --json for typed IR assertions
function runAgentSkillsJson(args, tmpDir, env) {
// Insert --json after 'agent-skills' subcommand
@@ -128,6 +136,59 @@ describe('agent-skills command', () => {
assert.strictEqual(r.ir.block, '');
});
test('unconfigured Codex reads its local companion agent from a descendant cwd', () => {
const agentsDir = path.join(tmpDir, '.codex', 'agents');
const descendant = path.join(tmpDir, 'src', 'feature');
const localPersona = '# Local Codex executor\nUse the project-local agent.\n';
fs.mkdirSync(agentsDir, { recursive: true });
fs.mkdirSync(descendant, { recursive: true });
fs.writeFileSync(path.join(agentsDir, 'gsd-executor.md'), localPersona);
markLocalGsdInstall(tmpDir);
writeConfig(tmpDir, { runtime: 'codex' });
const r = runAgentSkillsJson(['agent-skills', 'gsd-executor'], descendant, {
HOME: tmpDir,
USERPROFILE: tmpDir,
CODEX_HOME: path.join(tmpDir, 'global-codex'),
GSD_RUNTIME: '',
});
assert.ok(r.success, `Command failed: ${r.error}`);
assert.strictEqual(r.ir.block, localPersona);
});
test('workstream runtime selects the local Codex companion when root config differs', () => {
const agentsDir = path.join(tmpDir, '.codex', 'agents');
const localPersona = '# Local Codex executor\nUse the overridden runtime.\n';
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(path.join(agentsDir, 'gsd-executor.md'), localPersona);
markLocalGsdInstall(tmpDir);
writeConfig(tmpDir, { runtime: 'claude' });
const workstreamDir = path.join(tmpDir, '.planning', 'workstreams', 'feature-x');
fs.mkdirSync(workstreamDir, { recursive: true });
fs.writeFileSync(path.join(workstreamDir, 'config.json'), JSON.stringify({ runtime: 'codex' }));
const r = runAgentSkillsJson(['agent-skills', 'gsd-executor'], tmpDir, {
HOME: tmpDir,
USERPROFILE: tmpDir,
CODEX_HOME: path.join(tmpDir, 'global-codex'),
GSD_RUNTIME: '',
GSD_WORKSTREAM: 'feature-x',
});
assert.ok(r.success, `Command failed: ${r.error}`);
assert.strictEqual(r.ir.block, localPersona);
});
test('unconfigured Claude remains empty when a local Codex companion exists', () => {
const agentsDir = path.join(tmpDir, '.codex', 'agents');
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(path.join(agentsDir, 'gsd-executor.md'), '# Local Codex executor\n');
writeConfig(tmpDir, { runtime: 'claude' });
const r = runAgentSkillsJson(['agent-skills', 'gsd-executor'], tmpDir, { GSD_RUNTIME: 'claude' });
assert.ok(r.success, `Command failed: ${r.error}`);
assert.strictEqual(r.ir.block, '');
});
test('returns block containing agent_skills XML for configured agent', () => {
const skillDir = path.join(tmpDir, 'skills', 'test-skill');
fs.mkdirSync(skillDir, { recursive: true });

View File

@@ -14,6 +14,22 @@ const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
const MODEL_PROFILES = require('../gsd-core/bin/lib/model-profiles.cjs').MODEL_PROFILES;
const EXPECTED_AGENTS = Object.keys(MODEL_PROFILES);
function createCompleteCodexAgents(projectRoot) {
const agentsDir = path.join(projectRoot, '.codex', 'agents');
const files = {};
fs.mkdirSync(agentsDir, { recursive: true });
for (const name of EXPECTED_AGENTS) {
fs.writeFileSync(path.join(agentsDir, `${name}.md`), `# ${name}\n`);
fs.writeFileSync(path.join(agentsDir, `${name}.toml`), `name = "${name}"\n`);
files[`agents/${name}.md`] = {};
files[`agents/${name}.toml`] = {};
}
fs.writeFileSync(path.join(projectRoot, '.codex', 'gsd-file-manifest.json'), JSON.stringify({ files }));
}
// ─── JSON output shape ────────────────────────────────────────────────────────
describe('docs-init command', () => {
@@ -91,6 +107,49 @@ describe('docs-init command', () => {
});
});
describe('docs-init Codex project-local agent status', () => {
let tmpDir;
let globalHome;
beforeEach(() => {
tmpDir = createTempProject();
globalHome = path.join(tmpDir, 'global-codex');
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({ runtime: 'codex' }));
fs.mkdirSync(path.join(globalHome, 'agents'), { recursive: true });
for (const name of EXPECTED_AGENTS) {
fs.writeFileSync(path.join(globalHome, 'agents', `${name}.md`), `# global-${name}\n`);
}
});
afterEach(() => {
cleanup(tmpDir);
});
test('reports a complete local Codex installation instead of a global fixture', () => {
createCompleteCodexAgents(tmpDir);
const result = runGsdTools('docs-init --raw', tmpDir, { CODEX_HOME: globalHome });
assert.ok(result.success, `docs-init failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.project_root, fs.realpathSync(tmpDir));
assert.strictEqual(output.agents_installed, true);
assert.deepStrictEqual(output.missing_agents, []);
});
test('reports an empty local Codex directory as unhealthy even with global agents', () => {
fs.mkdirSync(path.join(tmpDir, '.codex', 'agents'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, '.codex', 'gsd-file-manifest.json'), JSON.stringify({ files: {} }));
const result = runGsdTools('docs-init --raw', tmpDir, { CODEX_HOME: globalHome });
assert.ok(result.success, `docs-init failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.agents_installed, false);
assert.deepStrictEqual(output.missing_agents, EXPECTED_AGENTS);
});
});
// ─── response_language wiring (#2402) ─────────────────────────────────────────
//
// cmdDocsInit predates withProjectRoot (init.cts) and never picked up