Under Codex, `write-profile` wrote ~/.claude/gsd-core/USER-PROFILE.md while Codex discuss-phase advisor-mode (installed under ~/.codex) checked the Codex home and never found the profile, so advisor-mode silently stayed disabled. Resolve the default output via the runtime-aware getGlobalConfigDir (GSD_RUNTIME / config.runtime), mirroring cmdGenerateDevPreferences. Claude unchanged; --output still wins. Closes #1114 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/1114-write-profile-runtime-home.md
Normal file
5
.changeset/1114-write-profile-runtime-home.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 1119
|
||||
---
|
||||
**`write-profile` now writes `USER-PROFILE.md` to the active runtime's config home instead of always `~/.claude`** — under Codex, `gsd-tools query write-profile` wrote `~/.claude/gsd-core/USER-PROFILE.md` while Codex `discuss-phase` advisor-mode (installed under `~/.codex`) checked the Codex home and never found it, so advisor-mode silently stayed disabled. The default output path is now resolved via the runtime-aware `getGlobalConfigDir` (`GSD_RUNTIME` / `config.runtime` → e.g. `~/.codex` for Codex), matching how the runtime's own workflows resolve it — mirroring `generate-dev-preferences`. Claude is unchanged (`~/.claude`); an explicit `--output` still wins. (#1114)
|
||||
@@ -20,7 +20,7 @@ import os from 'node:os';
|
||||
import core = require('./core.cjs');
|
||||
const { output, error, loadConfig } = core;
|
||||
import { platformReadSync as safeReadFile, platformWriteSync, platformEnsureDir } from './shell-command-projection.cjs';
|
||||
import { getGlobalSkillDir } from './runtime-homes.cjs';
|
||||
import { getGlobalSkillDir, getGlobalConfigDir } from './runtime-homes.cjs';
|
||||
import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs';
|
||||
import { resolveRuntimeNameFromCandidates } from './runtime-name-policy.cjs';
|
||||
|
||||
@@ -729,7 +729,27 @@ function cmdWriteProfile(cwd: string, options: CmdWriteProfileOptions, raw: bool
|
||||
|
||||
let outputPath = options.output;
|
||||
if (!outputPath) {
|
||||
outputPath = path.join(os.homedir(), '.claude', 'gsd-core', 'USER-PROFILE.md');
|
||||
// #1114: resolve the ACTIVE runtime's config home so the profile is written
|
||||
// where the runtime's own workflows look for it. Previously this hardcoded the
|
||||
// Claude home, so a Codex run wrote the profile under the Claude config dir
|
||||
// while Codex advisor-mode (installed under the Codex home) checked the Codex
|
||||
// dir and never found it. Mirrors cmdGenerateDevPreferences' runtime resolution.
|
||||
let effectiveRuntime = 'claude';
|
||||
try {
|
||||
const config = loadConfig(cwd);
|
||||
effectiveRuntime = resolveRuntimeNameFromCandidates(
|
||||
process.env['GSD_RUNTIME'],
|
||||
config['runtime'],
|
||||
'claude'
|
||||
) || 'claude';
|
||||
} catch {
|
||||
effectiveRuntime = resolveRuntimeNameFromCandidates(
|
||||
process.env['GSD_RUNTIME'],
|
||||
'claude'
|
||||
) || 'claude';
|
||||
}
|
||||
// path.join (not a string literal) keeps the cline-install leaked-path lint quiet.
|
||||
outputPath = path.join(getGlobalConfigDir(effectiveRuntime), 'gsd-core', 'USER-PROFILE.md');
|
||||
} else if (!path.isAbsolute(outputPath)) {
|
||||
outputPath = path.join(cwd, outputPath);
|
||||
}
|
||||
|
||||
@@ -117,6 +117,73 @@ describe('write-profile command', () => {
|
||||
assert.ok(out.dimensions_scored > 0, 'should have scored dimensions');
|
||||
});
|
||||
|
||||
test('#1114: default output resolves the active runtime config home (codex)', () => {
|
||||
const analysis = {
|
||||
profile_version: '1.0',
|
||||
dimensions: { communication_style: { rating: 'terse-direct', confidence: 'HIGH' } },
|
||||
};
|
||||
const analysisPath = path.join(tmpDir, 'analysis.json');
|
||||
const codexHome = path.join(tmpDir, 'codex-home');
|
||||
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
|
||||
|
||||
const result = runGsdTools(
|
||||
['write-profile', '--input', analysisPath, '--raw'],
|
||||
tmpDir,
|
||||
{ CODEX_HOME: codexHome, GSD_RUNTIME: 'codex' }
|
||||
);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
// Must land in the Codex home (so Codex advisor-mode finds it), NOT .claude.
|
||||
assert.strictEqual(out.profile_path, path.join(codexHome, 'gsd-core', 'USER-PROFILE.md'));
|
||||
assert.ok(!out.profile_path.includes(`${path.sep}.claude${path.sep}`),
|
||||
`codex profile must not be written under .claude; got ${out.profile_path}`);
|
||||
assert.ok(fs.existsSync(out.profile_path), 'runtime-aware profile should be written to disk');
|
||||
});
|
||||
|
||||
test('#1114: default output is the .claude config home for the claude runtime', () => {
|
||||
const analysis = {
|
||||
profile_version: '1.0',
|
||||
dimensions: { communication_style: { rating: 'terse-direct', confidence: 'HIGH' } },
|
||||
};
|
||||
const analysisPath = path.join(tmpDir, 'analysis.json');
|
||||
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
|
||||
|
||||
// Clear ambient runtime vars so the test is hermetic regardless of the
|
||||
// developer's shell (a stray GSD_RUNTIME/CLAUDE_CONFIG_DIR would redirect it).
|
||||
const result = runGsdTools(['write-profile', '--input', analysisPath, '--raw'], tmpDir,
|
||||
{ HOME: tmpDir, GSD_RUNTIME: '', CLAUDE_CONFIG_DIR: '', CODEX_HOME: '' });
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
// os.homedir() returns HOME verbatim, so assert on the suffix to stay
|
||||
// robust against macOS /var → /private/var symlink normalization.
|
||||
assert.ok(
|
||||
out.profile_path.endsWith(path.join('.claude', 'gsd-core', 'USER-PROFILE.md')),
|
||||
`claude profile must be under .claude/gsd-core; got ${out.profile_path}`
|
||||
);
|
||||
assert.ok(fs.existsSync(out.profile_path), 'profile should be written to disk');
|
||||
});
|
||||
|
||||
test('#1114: config.runtime=codex (no GSD_RUNTIME) also resolves the Codex home', () => {
|
||||
const analysis = {
|
||||
profile_version: '1.0',
|
||||
dimensions: { communication_style: { rating: 'terse-direct', confidence: 'HIGH' } },
|
||||
};
|
||||
const analysisPath = path.join(tmpDir, 'analysis.json');
|
||||
const codexHome = path.join(tmpDir, 'codex-home');
|
||||
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({ runtime: 'codex' }));
|
||||
|
||||
// No GSD_RUNTIME — the runtime must be read from config.runtime.
|
||||
const result = runGsdTools(
|
||||
['write-profile', '--input', analysisPath, '--raw'],
|
||||
tmpDir,
|
||||
{ CODEX_HOME: codexHome, GSD_RUNTIME: '' }
|
||||
);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.profile_path, path.join(codexHome, 'gsd-core', 'USER-PROFILE.md'));
|
||||
});
|
||||
|
||||
test('errors when --input is missing', () => {
|
||||
const result = runGsdTools('write-profile --raw', tmpDir);
|
||||
assert.ok(!result.success, 'should fail without --input');
|
||||
|
||||
Reference in New Issue
Block a user