diff --git a/CHANGELOG.md b/CHANGELOG.md index af60aed..e8ec3a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.4.0 — 2026-10-11 + +### Added + +- Claude Code support. Skills are now installed for the `claude-code` agent too, so the Skills CLI links each one from `.agents/skills/` into `.claude/skills/`, where Claude Code finds it as `/name`. aginit writes a one-line `CLAUDE.md` (`@AGENTS.md`) when none exists, and wires Graft for Claude Code (`graft init --agents ... claude`). See `docs/adr/0002-claude-code-skills-and-instructions.md`. +- New `agents.claudeCode` config flag, `true` by default. Configs without the flag keep loading and count as enabled. Set it to `false` to opt out. +- `aginit update` links already-installed skills for Claude Code when they're missing from `.claude/skills/`, and adds the `CLAUDE.md` import. +- `aginit doctor` reports skills missing from `.claude/skills/` and a `CLAUDE.md` that doesn't import `AGENTS.md`. + ## 0.3.0 — 2026-10-10 ### Fixed diff --git a/README.md b/README.md index ac24c44..ed24a6d 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ aginit sets all of this up the same way every time, and you can re-run it whenev | | | |---|---| -| **Instructions** | `AGENTS.md` with the project's real commands, invariants and a first-session playbook, shared by Antigravity, Codex and any agent that reads `AGENTS.md` | -| **Skills** | All 27 stable [Matt Pocock skills](https://github.com/mattpocock/skills) (`tdd`, `code-review`, `diagnosing-bugs`, `to-spec`, `to-tickets`, `implement-spec`, `pr`...) plus [humanizer](https://github.com/blader/humanizer) and [ponytail](https://github.com/DietrichGebert/ponytail), installed in `.agents/skills/` via the [Skills CLI](https://github.com/vercel-labs/skills) | +| **Instructions** | `AGENTS.md` with the project's real commands, invariants and a first-session playbook, shared by Antigravity, Codex and any agent that reads `AGENTS.md`, plus a one-line `CLAUDE.md` (`@AGENTS.md`) so Claude Code loads it too | +| **Skills** | All 27 stable [Matt Pocock skills](https://github.com/mattpocock/skills) (`tdd`, `code-review`, `diagnosing-bugs`, `to-spec`, `to-tickets`, `implement-spec`, `pr`...) plus [humanizer](https://github.com/blader/humanizer) and [ponytail](https://github.com/DietrichGebert/ponytail), installed in `.agents/skills/` via the [Skills CLI](https://github.com/vercel-labs/skills) and linked into `.claude/skills/` for Claude Code | | **Codebase graph** | [Graft](https://www.npmjs.com/package/@nanonets/graft): `graft map` and `graft ask` give agents the relevant code spans before they grep | | **Test tooling** | Vitest for TypeScript projects, plus Playwright when a web framework is selected | | **Web extras** | [Impeccable](https://github.com/pbakaus/impeccable) for UI design and [agent-browser](https://github.com/vercel-labs/agent-browser) for visual QA, with its CLI and browser binaries installed so it works out of the box | @@ -67,7 +67,7 @@ npm install -g @nanonets/graft ``` aginit new [name] Create a new project aginit init Add or update agent setup in the current directory -aginit doctor Check Node.js, package manager, Git, Graft, Codex, Skills CLI, agent-browser and config +aginit doctor Check Node.js, package manager, Git, Graft, Codex, Skills CLI, agent-browser, Claude Code wiring and config aginit update Update installed skills and rebuild the Graft index ``` diff --git a/docs/adr/0002-claude-code-skills-and-instructions.md b/docs/adr/0002-claude-code-skills-and-instructions.md new file mode 100644 index 0000000..309ee7c --- /dev/null +++ b/docs/adr/0002-claude-code-skills-and-instructions.md @@ -0,0 +1,51 @@ +# 2. Make installed skills and AGENTS.md visible to Claude Code + +Date: 2026-10-11 + +## Status +Accepted + +## Context +aginit installed skills only for Antigravity and Codex, which both read +`.agents/skills/`. Claude Code discovers project skills only in +`.claude/skills//SKILL.md`, so a project set up by aginit showed none of its +skills in Claude Code. Claude Code also loads instructions from `CLAUDE.md`. Since +v2.1.277 it reads `AGENTS.md` too, but only when no `CLAUDE.md` or +`CLAUDE.local.md` exists, and not in every session (older versions, the first +session after an upgrade, or with the built-in AGENTS.md plugin disabled). + +Options considered: +1. Add `claude-code` to the Skills CLI `--agent` list. +2. Symlink `.claude/skills` to `../.agents/skills` from aginit. +3. Replace `agents.primary`/`secondary` with an agent array. + +## Decision +- **Option 1.** The Skills CLI (`vercel-labs/skills`) already knows the + `claude-code` agent and links each skill from the canonical + `.agents/skills/` into `.claude/skills/`. There is one copy on + disk, `skills update` flows through, and the CLI handles platforms where + symlinks don't work (`--copy`). aginit keeps orchestrating instead of + managing links itself. +- **Config:** add an optional `agents.claudeCode: boolean`, written as `true` + by new configs. If the field is missing (configs written before 0.4.0), it + counts as enabled, so existing projects get Claude Code support on the next + `aginit init` or `aginit update` without editing their config. Set it to + `false` to opt out. `primary`/`secondary` keep their shape. +- **Instructions:** when Claude Code is enabled and no `CLAUDE.md` exists, + write a one-line `CLAUDE.md` containing `@AGENTS.md`. Claude Code documents + this import as the way to share AGENTS.md. It works in every session, and + Claude never reads AGENTS.md twice because of it. An existing `CLAUDE.md` is + never modified; `aginit doctor` reports when it lacks the import. +- **Graft:** pass `claude` to `graft init --agents` so Graft wires its + in-repo Claude Code integration (`.claude/skills/graft`, hooks, statusline, + `.mcp.json`). `--no-global` still keeps it from writing outside the repo. +- **Update path:** `aginit update` relinks the configured sources with the full + agent list when an installed skill is missing from `.claude/skills/`. + +## Consequences +- Fresh and re-initialized projects expose every skill to Claude Code as + `/name`, including skills with `disable-model-invocation: true`. +- Projects get `.claude/` and `CLAUDE.md` files to commit. +- `aginit doctor` gains two checks while Claude Code is enabled: skills + missing from `.claude/skills/`, and a `CLAUDE.md` that doesn't import + AGENTS.md. diff --git a/package.json b/package.json index e9da12d..c14a546 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@antomanc/aginit", - "version": "0.3.0", + "version": "0.4.0", "description": "Set up a new or existing project for AI coding agents in one command: AGENTS.md, Matt Pocock skills, Graft codebase graph, Vitest/Playwright. Orchestrates upstream tools, idempotent.", "type": "module", "main": "dist/index.js", diff --git a/src/adapters/agents-md.ts b/src/adapters/agents-md.ts index 9598f24..fd7b39b 100644 --- a/src/adapters/agents-md.ts +++ b/src/adapters/agents-md.ts @@ -111,7 +111,9 @@ export function generateAgentsMarkdown( '', '## AI Capabilities & Skills', hasSkills - ? 'Configured skills install into `.agents/skills/` (shared by Antigravity and Codex):' + ? config.agents.claudeCode === false + ? 'Configured skills install into `.agents/skills/` (shared by Antigravity and Codex):' + : 'Configured skills install into `.agents/skills/` (shared by Antigravity and Codex, linked into `.claude/skills/` for Claude Code):' : 'Skills installation is disabled in this configuration.', engineeringCapability || undefined, proseCapability || undefined, @@ -175,3 +177,30 @@ export function setupAgentsMarkdown( const content = generateAgentsMarkdown(config, readPackageManifest(targetDir)?.scripts); return safeWriteFile(filePath, content, { overwrite: true, dryRun, silent }); } + +export const CLAUDE_AGENTS_IMPORT = '@AGENTS.md'; + +/** + * Point Claude Code at AGENTS.md through a one-line `@AGENTS.md` import. Recent + * Claude Code reads AGENTS.md on its own, but only without a CLAUDE.md and not + * in every session; the import works everywhere and never loads it twice. + * An existing CLAUDE.md is left alone (doctor reports a missing import). + */ +export function setupClaudeMarkdown( + targetDir: string, + options: { dryRun?: boolean; silent?: boolean } = {} +): boolean { + const { dryRun = false, silent = false } = options; + const filePath = path.join(targetDir, 'CLAUDE.md'); + if (fileExists(filePath)) { + if (!silent) logger.dim('CLAUDE.md already exists, preserving content.'); + return true; + } + return safeWriteFile(filePath, CLAUDE_AGENTS_IMPORT + '\n', { overwrite: false, dryRun, silent }); +} + +export function claudeMarkdownImportsAgents(content: string): boolean { + return content + .split(/\r?\n/) + .some((line) => ['@AGENTS.md', '@./AGENTS.md'].includes(line.trim())); +} diff --git a/src/adapters/graft.ts b/src/adapters/graft.ts index 2401757..65caac6 100644 --- a/src/adapters/graft.ts +++ b/src/adapters/graft.ts @@ -9,9 +9,9 @@ export async function isGraftInstalled(): Promise { export async function initGraft( targetDir: string, - options: { dryRun?: boolean; silent?: boolean } = {} + options: { dryRun?: boolean; silent?: boolean; claude?: boolean } = {} ): Promise { - const { dryRun = false, silent = false } = options; + const { dryRun = false, silent = false, claude = true } = options; const installed = dryRun || (await isGraftInstalled()); if (!installed) { @@ -25,10 +25,12 @@ export async function initGraft( if (!silent) logger.dim('Initializing Graft context graph...'); - // Run graft init with antigravity and agents, without touching global system files (--no-global) + // Wire Antigravity, AGENTS.md readers and (optionally) Claude Code's in-repo + // .claude/ + .mcp.json, without touching global system files (--no-global) + const agents = ['antigravity', 'agents', ...(claude ? ['claude'] : [])]; const res = await runExecutable( 'graft', - ['init', '--no-global', '--agents', 'antigravity', 'agents', '-y'], + ['init', '--no-global', '--agents', ...agents, '-y'], { cwd: targetDir, dryRun, diff --git a/src/adapters/skills.ts b/src/adapters/skills.ts index 2fe8be3..24d745d 100644 --- a/src/adapters/skills.ts +++ b/src/adapters/skills.ts @@ -4,7 +4,21 @@ import { fileExists, readJsonFile } from '../utils/fs.js'; import { runExecutable } from '../utils/shell.js'; import { logger } from '../utils/logger.js'; import { validateUpstreamArgument } from '../config/validation.js'; -import { SkillSourceConfig } from '../config/schema.js'; +import { AginitConfig, SkillSourceConfig } from '../config/schema.js'; + +/** The skills CLI id for Claude Code; it links skills into `.claude/skills/`. */ +export const CLAUDE_CODE_AGENT = 'claude-code'; + +export function claudeCodeEnabled(config: Pick): boolean { + return config.agents.claudeCode !== false; +} + +/** Agents the skills CLI installs into, deduplicated. */ +export function skillAgents(config: Pick): string[] { + const agents = [config.agents.primary, config.agents.secondary]; + if (claudeCodeEnabled(config)) agents.push(CLAUDE_CODE_AGENT); + return [...new Set(agents)]; +} export interface SkillsLockFile { version: number; @@ -30,7 +44,7 @@ export async function installSkillSource( source: SkillSourceConfig, options: SkillInstallOptions = {} ): Promise<{ ok: boolean; message?: string }> { - const { agents = ['antigravity', 'codex'], dryRun = false, silent = false } = options; + const { agents = ['antigravity', 'codex', CLAUDE_CODE_AGENT], dryRun = false, silent = false } = options; validateUpstreamArgument(source.package, 'skill package'); for (const skill of source.skills) validateUpstreamArgument(skill, 'skill name'); @@ -105,6 +119,36 @@ export function getInstalledSkills(targetDir: string): string[] { return [...installed]; } +/** + * Installed skills Claude Code cannot see because `.claude/skills//SKILL.md` + * is missing (the skills CLI links them there when `claude-code` is an agent). + */ +export function getSkillsMissingForClaude(targetDir: string): string[] { + return getInstalledSkills(targetDir).filter( + (name) => !fileExists(path.join(targetDir, '.claude', 'skills', name, 'SKILL.md')) + ); +} + +/** + * Bring projects set up before Claude Code support up to date: when Claude Code + * is enabled and some installed skill is not linked into `.claude/skills/`, + * reinstall the configured sources with the full agent list (idempotent). + */ +export async function ensureClaudeSkillLinks( + targetDir: string, + config: AginitConfig, + options: { dryRun?: boolean; silent?: boolean } = {} +): Promise { + if (!claudeCodeEnabled(config) || config.skills.sources.length === 0) return true; + if (getSkillsMissingForClaude(targetDir).length === 0) return true; + if (!options.silent) logger.dim('Linking skills into .claude/skills/ for Claude Code...'); + const { failed } = await installAllSkills(targetDir, config.skills.sources, { + ...options, + agents: skillAgents(config) + }); + return failed.length === 0; +} + export async function updateSkills( targetDir: string, options: { dryRun?: boolean; silent?: boolean } = {} diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index 6b4702c..291eb62 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -9,7 +9,13 @@ import { PackageManager } from '../config/schema.js'; import { detectPackageManager } from '../adapters/package-manager.js'; -import { getInstalledSkills } from '../adapters/skills.js'; +import fs from 'node:fs'; +import { + claudeCodeEnabled, + getInstalledSkills, + getSkillsMissingForClaude +} from '../adapters/skills.js'; +import { claudeMarkdownImportsAgents } from '../adapters/agents-md.js'; import { agentBrowserConfigured, agentBrowserStubInstalled, @@ -169,7 +175,7 @@ export async function runDoctor(targetDir: string = process.cwd()): Promise 0) { + const missing = getSkillsMissingForClaude(targetDir); + checks.push({ + name: 'Claude Code Skills (.claude/skills/)', + category: 'Project Config', + ok: missing.length === 0, + detail: + missing.length === 0 + ? `All ${installedSkills.length} skills visible to Claude Code` + : `${missing.length} skills not linked for Claude Code: ${missing.join(', ')}`, + suggestion: + missing.length === 0 + ? undefined + : 'Run `aginit update` or `aginit init` to link skills into .claude/skills/ (or set agents.claudeCode: false)' + }); + } + + const claudePath = path.join(targetDir, 'CLAUDE.md'); + const claudeExists = fileExists(claudePath); + // A CLAUDE.md symlinked to AGENTS.md is as good as the import. + const claudeIsAgentsLink = + claudeExists && + agentsExists && + fs.realpathSync(claudePath) === fs.realpathSync(agentsPath); + const claudeOk = + !claudeExists || + claudeIsAgentsLink || + claudeMarkdownImportsAgents(readTextFile(claudePath) || ''); + checks.push({ + name: 'CLAUDE.md → AGENTS.md', + category: 'Project Config', + ok: claudeExists && claudeOk, + detail: !claudeExists + ? 'No CLAUDE.md; only Claude Code v2.1.277+ reads AGENTS.md on its own' + : claudeOk + ? 'CLAUDE.md imports AGENTS.md' + : 'CLAUDE.md exists without `@AGENTS.md`, so Claude Code ignores AGENTS.md', + suggestion: !claudeExists + ? 'Run `aginit update` or `aginit init` to add a CLAUDE.md that imports AGENTS.md' + : claudeOk + ? undefined + : 'Add a line `@AGENTS.md` to CLAUDE.md' + }); + } + // 10. agent-browser runtime (only when the project ships the skill) const agentBrowserWanted = projectConfig !== null && diff --git a/src/cli/main.ts b/src/cli/main.ts index 9ad447a..a7ad9a8 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -3,7 +3,8 @@ import { readFileSync } from 'node:fs'; import { createProject } from './new.js'; import { initCurrentDirectory } from './init.js'; import { runDoctor } from './doctor.js'; -import { updateSkills } from '../adapters/skills.js'; +import { claudeCodeEnabled, ensureClaudeSkillLinks, updateSkills } from '../adapters/skills.js'; +import { setupClaudeMarkdown } from '../adapters/agents-md.js'; import { buildGraft } from '../adapters/graft.js'; import { readProjectConfig } from '../config/validation.js'; import { logger } from '../utils/logger.js'; @@ -161,9 +162,12 @@ export function createCliProgram(): Command { const targetDir = process.cwd(); const config = readProjectConfig(targetDir); const skillsOk = - config?.skills.sources.length === 0 + (config?.skills.sources.length === 0 ? true - : await updateSkills(targetDir, { dryRun: options.dryRun }); + : await updateSkills(targetDir, { dryRun: options.dryRun })) && + (config ? await ensureClaudeSkillLinks(targetDir, config, { dryRun: options.dryRun }) : true); + if (config && claudeCodeEnabled(config)) + setupClaudeMarkdown(targetDir, { dryRun: options.dryRun }); const graftOk = config?.codebase.graft === false ? true diff --git a/src/cli/new.ts b/src/cli/new.ts index a8fa4f7..336c266 100644 --- a/src/cli/new.ts +++ b/src/cli/new.ts @@ -9,8 +9,8 @@ import { } from '../config/schema.js'; import { getPreset } from '../presets/registry.js'; import { setupGit } from '../adapters/git.js'; -import { setupAgentsMarkdown } from '../adapters/agents-md.js'; -import { installAllSkills } from '../adapters/skills.js'; +import { setupAgentsMarkdown, setupClaudeMarkdown } from '../adapters/agents-md.js'; +import { claudeCodeEnabled, installAllSkills, skillAgents } from '../adapters/skills.js'; import { initGraft } from '../adapters/graft.js'; import { agentBrowserConfigured, @@ -137,13 +137,14 @@ export async function createProject( // Step 4: Write or update AGENTS.md intake contract if (!silent) logger.step(currentStep++, totalSteps, 'Generating AGENTS.md intake guidance'); setupAgentsMarkdown(targetDir, config, { dryRun, silent }); + if (claudeCodeEnabled(config)) setupClaudeMarkdown(targetDir, { dryRun, silent }); if (!silent) logger.success('Configured AGENTS.md'); // Step 5: Install skills via skills.sh engine if (!silent) logger.step(currentStep++, totalSteps, 'Installing upstream skills via skills.sh'); if (options.skills !== false && config.skills.sources.length > 0) { const { installed, failed } = await installAllSkills(targetDir, config.skills.sources, { - agents: [config.agents.primary, config.agents.secondary], + agents: skillAgents(config), dryRun, silent }); @@ -171,7 +172,11 @@ export async function createProject( // Codebase intelligence (Graft) if (!silent) logger.step(currentStep++, totalSteps, 'Configuring codebase intelligence'); if (config.codebase.graft && options.graft !== false) { - const graftOk = await initGraft(targetDir, { dryRun, silent }); + const graftOk = await initGraft(targetDir, { + dryRun, + silent, + claude: claudeCodeEnabled(config) + }); complete &&= graftOk; if (graftOk && !silent) { logger.success('Graft context graph initialized'); diff --git a/src/config/defaults.ts b/src/config/defaults.ts index 2875349..35bfc89 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -70,7 +70,8 @@ export function getDefaultConfig( packageManager, agents: { primary: 'antigravity', - secondary: 'codex' + secondary: 'codex', + claudeCode: true }, skills: { sources: [ @@ -120,7 +121,8 @@ export function getDefaultConfig( packageManager, agents: { primary: 'antigravity', - secondary: 'codex' + secondary: 'codex', + claudeCode: true }, skills: { sources: [ @@ -162,7 +164,8 @@ export function getDefaultConfig( packageManager, agents: { primary: 'antigravity', - secondary: 'codex' + secondary: 'codex', + claudeCode: true }, skills: { sources: [ diff --git a/src/config/schema.ts b/src/config/schema.ts index 24fd686..820c0bf 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -16,6 +16,11 @@ export interface AginitConfig { agents: { primary: string; secondary: string; + /** + * Also install skills, Graft wiring and a CLAUDE.md import for Claude Code. + * Missing in configs written before 0.4.0; treated as enabled. + */ + claudeCode?: boolean; }; skills: { /** diff --git a/src/config/validation.ts b/src/config/validation.ts index 253a708..e672e5a 100644 --- a/src/config/validation.ts +++ b/src/config/validation.ts @@ -45,6 +45,8 @@ export function validateConfig(value: unknown): AginitConfig { if (!isRecord(value.agents)) fail('agents'); for (const key of ['primary', 'secondary']) validateUpstreamArgument(value.agents[key], `agent ${key}`); + if (value.agents.claudeCode !== undefined && typeof value.agents.claudeCode !== 'boolean') + fail('agents.claudeCode'); if (!isRecord(value.skills) || !Array.isArray(value.skills.sources)) fail('skills.sources'); if (value.skills.workflow !== undefined && !['minimal', 'spec'].includes(value.skills.workflow)) fail('skills.workflow'); diff --git a/tests/claude-code.test.ts b/tests/claude-code.test.ts new file mode 100644 index 0000000..2be42b3 --- /dev/null +++ b/tests/claude-code.test.ts @@ -0,0 +1,171 @@ +import { it, expect, vi, beforeEach, afterEach } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; +import { createProject } from '../src/cli/new.js'; +import { createCliProgram } from '../src/cli/main.js'; +import { runDoctor } from '../src/cli/doctor.js'; +import { + ensureClaudeSkillLinks, + getSkillsMissingForClaude, + skillAgents +} from '../src/adapters/skills.js'; +import { initGraft } from '../src/adapters/graft.js'; +import { claudeMarkdownImportsAgents } from '../src/adapters/agents-md.js'; +import { getDefaultConfig } from '../src/config/defaults.js'; +import { readProjectConfig, validateConfig } from '../src/config/validation.js'; + +const shell = vi.hoisted(() => ({ runExecutable: vi.fn(), commandExists: vi.fn() })); +vi.mock('../src/utils/shell.js', async (original) => ({ + ...(await original()), + ...shell +})); + +const success = { ok: true, exitCode: 0, stdout: '1.0.0', stderr: '' }; +let dir: string; +let cwd: string; +let log: ReturnType; +beforeEach(() => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'aginit-claude-')); + cwd = process.cwd(); + log = vi.spyOn(console, 'log').mockImplementation(() => {}); + shell.commandExists.mockResolvedValue(true); + shell.runExecutable.mockReset().mockResolvedValue(success); +}); +afterEach(() => { + process.chdir(cwd); + vi.restoreAllMocks(); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +const legacyConfig = () => { + const config: any = getDefaultConfig('app', 'generic'); + delete config.agents.claudeCode; + config.codebase.graft = false; + return config; +}; +const skillCalls = () => + shell.runExecutable.mock.calls.filter(([bin, args]) => bin === 'npx' && args[2] === 'add'); +const addSkill = (base: string, name: string) => { + fs.mkdirSync(path.join(dir, base, 'skills', name), { recursive: true }); + fs.writeFileSync(path.join(dir, base, 'skills', name, 'SKILL.md'), `# ${name}\n`); +}; + +it('enables Claude Code in every default preset', () => { + for (const preset of ['web', 'cli', 'generic'] as const) { + const config = getDefaultConfig('app', preset); + expect(config.agents.claudeCode).toBe(true); + expect(skillAgents(config)).toEqual(['antigravity', 'codex', 'claude-code']); + } +}); + +it('loads legacy configs without the claudeCode flag and treats them as enabled', () => { + fs.writeFileSync(path.join(dir, 'aginit.config.json'), JSON.stringify(legacyConfig())); + const config = readProjectConfig(dir)!; + expect(config.agents.claudeCode).toBeUndefined(); + expect(skillAgents(config)).toContain('claude-code'); +}); + +it('rejects a non-boolean claudeCode flag', () => { + const config: any = getDefaultConfig('app', 'generic'); + config.agents.claudeCode = 'yes'; + expect(() => validateConfig(config)).toThrow(/agents\.claudeCode/); +}); + +it('does not duplicate claude-code when it is already a configured agent', () => { + const config = getDefaultConfig('app', 'generic'); + config.agents.secondary = 'claude-code'; + expect(skillAgents(config)).toEqual(['antigravity', 'claude-code']); +}); + +it('installs skills for Claude Code and writes a CLAUDE.md that imports AGENTS.md', async () => { + await createProject('app', { targetDir: dir, git: false, graft: false, silent: true }); + expect(skillCalls().length).toBeGreaterThan(0); + for (const [, args] of skillCalls()) { + const agents = args.slice(args.indexOf('--agent') + 1, -1); + expect(agents).toEqual(['antigravity', 'codex', 'claude-code']); + } + expect(fs.readFileSync(path.join(dir, 'CLAUDE.md'), 'utf8')).toBe('@AGENTS.md\n'); +}); + +it('leaves Claude Code out when agents.claudeCode is false', async () => { + const config = legacyConfig(); + config.agents.claudeCode = false; + fs.writeFileSync(path.join(dir, 'aginit.config.json'), JSON.stringify(config)); + await createProject('app', { targetDir: dir, git: false, silent: true }); + for (const [, args] of skillCalls()) expect(args).not.toContain('claude-code'); + expect(fs.existsSync(path.join(dir, 'CLAUDE.md'))).toBe(false); +}); + +it('preserves an existing CLAUDE.md', async () => { + fs.writeFileSync(path.join(dir, 'CLAUDE.md'), '# Mine\n'); + await createProject('app', { + targetDir: dir, + git: false, + graft: false, + skills: false, + silent: true + }); + expect(fs.readFileSync(path.join(dir, 'CLAUDE.md'), 'utf8')).toBe('# Mine\n'); +}); + +it('wires Graft for Claude Code only when enabled', async () => { + await initGraft(dir, { silent: true }); + expect(shell.runExecutable).toHaveBeenLastCalledWith( + 'graft', + ['init', '--no-global', '--agents', 'antigravity', 'agents', 'claude', '-y'], + expect.anything() + ); + await initGraft(dir, { silent: true, claude: false }); + expect(shell.runExecutable).toHaveBeenLastCalledWith( + 'graft', + ['init', '--no-global', '--agents', 'antigravity', 'agents', '-y'], + expect.anything() + ); +}); + +it('detects skills not linked into .claude/skills/', () => { + addSkill('.agents', 'tdd'); + addSkill('.agents', 'wayfinder'); + addSkill('.claude', 'tdd'); + expect(getSkillsMissingForClaude(dir)).toEqual(['wayfinder']); +}); + +it('relinks a legacy project for Claude Code only when skills are missing', async () => { + const config = legacyConfig(); + addSkill('.agents', 'wayfinder'); + await ensureClaudeSkillLinks(dir, config, { silent: true }); + expect(skillCalls()).toHaveLength(config.skills.sources.length); + expect(skillCalls()[0][1]).toContain('claude-code'); + + shell.runExecutable.mockClear(); + addSkill('.claude', 'wayfinder'); + await ensureClaudeSkillLinks(dir, config, { silent: true }); + expect(skillCalls()).toHaveLength(0); +}); + +it('links skills and adds CLAUDE.md during aginit update on a legacy project', async () => { + fs.writeFileSync(path.join(dir, 'aginit.config.json'), JSON.stringify(legacyConfig())); + addSkill('.agents', 'wayfinder'); + process.chdir(dir); + await createCliProgram().parseAsync(['node', 'aginit', 'update']); + expect(skillCalls().some(([, args]) => args.includes('claude-code'))).toBe(true); + expect(fs.readFileSync(path.join(dir, 'CLAUDE.md'), 'utf8')).toBe('@AGENTS.md\n'); +}); + +it('doctor flags unlinked skills and a CLAUDE.md without the AGENTS.md import', async () => { + fs.writeFileSync(path.join(dir, 'aginit.config.json'), JSON.stringify(legacyConfig())); + fs.writeFileSync(path.join(dir, 'AGENTS.md'), '# app\n'); + fs.writeFileSync(path.join(dir, 'CLAUDE.md'), '# Mine\n'); + addSkill('.agents', 'wayfinder'); + await runDoctor(dir); + const output = log.mock.calls.flat().join('\n'); + expect(output).toContain('not linked for Claude Code: wayfinder'); + expect(output).toContain('Claude Code ignores AGENTS.md'); +}); + +it('recognizes the AGENTS.md import forms', () => { + expect(claudeMarkdownImportsAgents('@AGENTS.md\n')).toBe(true); + expect(claudeMarkdownImportsAgents('# Notes\n\n@./AGENTS.md\n')).toBe(true); + expect(claudeMarkdownImportsAgents('Read AGENTS.md first.\n')).toBe(false); +});