fix(#1529): emit runtime-native instruction file from new-project

This commit is contained in:
Tom Boucher
2026-06-22 09:30:11 -04:00
parent 91d29be124
commit bf9bd1f4e0
8 changed files with 252 additions and 13 deletions

View File

@@ -25,7 +25,7 @@ const { loadConfig } = configLoader;
import { platformReadSync as safeReadFile, platformWriteSync, platformEnsureDir } from './shell-command-projection.cjs';
import { getGlobalSkillDir, getGlobalConfigDir } from './runtime-homes.cjs';
import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs';
import { resolveRuntimeNameFromCandidates } from './runtime-name-policy.cjs';
import { resolveRuntimeNameFromCandidates, getProjectInstructionFile } from './runtime-name-policy.cjs';
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -1120,20 +1120,32 @@ function cmdGenerateClaudeMd(cwd: string, options: CmdGenerateClaudeMdOptions, r
// repo-root `CLAUDE.md`, so generated GSD content does not land next to — or
// pollute — a hand-crafted repo-root CLAUDE.md. An explicit `claude_md_path`
// config value or `--output` still wins.
let configClaudeMdPath = './.claude/CLAUDE.md';
let configClaudeMdPath = '.claude/CLAUDE.md';
try {
const config = loadConfig(cwd);
if (config['claude_md_path']) configClaudeMdPath = config['claude_md_path'] as string;
if (config['claude_md_assembly']) assemblyConfig = config['claude_md_assembly'] as Record<string, unknown>;
// #3163: When runtime is codex, override the output target to AGENTS.md
// regardless of claude_md_path, so Codex projects never write to CLAUDE.md.
// GSD_RUNTIME env var takes precedence over config.runtime, mirroring detectRuntime().
// #1529: When no explicit --output is provided, derive the instruction
// file from the runtime via the shared `getProjectInstructionFile` policy
// (single source of truth in runtime-name-policy.cjs, shared with the
// new-project.md bash workflow via `gsd-tools query
// project-instruction-file`). Previously this was a codex-only override
// (#3163) that left AGENTS-native runtimes (opencode/kilo/kimi) emitting
// CLAUDE.md; copilot now resolves to copilot-instructions.md, and
// antigravity/gemini to GEMINI.md. GSD_RUNTIME env var takes precedence
// over config.runtime, mirroring detectRuntime().
//
// Non-claude runtimes always win over a stale `claude_md_path` (the #3163
// rationale: a Codex/AGENTS-native project must never write to CLAUDE.md
// even if a prior Claude setup left a `claude_md_path` behind). For the
// claude runtime, `claude_md_path` config is honored — it IS the
// Claude-specific output setting (per #1098 and the #3163 non-codex test).
const effectiveRuntime = resolveRuntimeNameFromCandidates(
process.env['GSD_RUNTIME'],
config['runtime']
);
if (!options.output && effectiveRuntime === 'codex') {
configClaudeMdPath = './AGENTS.md';
if (!options.output && effectiveRuntime && effectiveRuntime !== 'claude') {
configClaudeMdPath = getProjectInstructionFile(effectiveRuntime);
}
} catch { /* use default */ }

View File

@@ -89,6 +89,37 @@ export function resolveRuntimeNameFromCandidates(...candidates: unknown[]): stri
return null;
}
/**
* Map a runtime id to its project instruction file path (relative to project
* root). Bug #1529: this is the SINGLE source of truth shared by both
* consumption surfaces —
* (A) the Node surface: profile-output.cjs (generate-claude-md handler)
* (B) the bash surface: `gsd-tools query project-instruction-file --runtime <r>`,
* consumed by gsd-core/workflows/new-project.md to set $INSTRUCTION_FILE
*
* Mapping table (per the #1529 issue contract):
*
* claude → .claude/CLAUDE.md
* codex, opencode, kilo, kimi → AGENTS.md
* copilot → copilot-instructions.md
* antigravity, gemini → GEMINI.md
* unknown / future runtimes → AGENTS.md (safe cross-agent default)
*
* Aliases are normalized via `canonicalizeRuntimeName` first, so inputs like
* `codex-cli` resolve to `codex` → `AGENTS.md`. Replaces the prior codex-only
* override in profile-output.cjs (#3163) which left AGENTS-native runtimes
* (opencode/kilo/kimi) incorrectly emitting `.claude/CLAUDE.md`. Pure: no I/O.
*/
export function getProjectInstructionFile(runtime: unknown): string {
const canonical = canonicalizeRuntimeName(runtime);
if (canonical === 'claude') return '.claude/CLAUDE.md';
if (canonical === 'copilot') return 'copilot-instructions.md';
if (canonical === 'antigravity' || canonical === 'gemini') return 'GEMINI.md';
// codex, opencode, kilo, kimi, AND unknown/future runtimes all default to
// root AGENTS.md (the safe cross-agent instruction file).
return 'AGENTS.md';
}
/**
* Map a canonical runtime id to its on-disk local config directory name
* (e.g. `cursor` -> `.cursor`, `windsurf` -> `.devin`). Unknown/empty inputs