diff --git a/.claude/rules b/.claude/rules index 2d5c9a9..4c6c243 120000 --- a/.claude/rules +++ b/.claude/rules @@ -1 +1 @@ -../.agents/rules \ No newline at end of file +../rules \ No newline at end of file diff --git a/.claude/skills b/.claude/skills index 2b7a412..42c5394 120000 --- a/.claude/skills +++ b/.claude/skills @@ -1 +1 @@ -../.agents/skills \ No newline at end of file +../skills \ No newline at end of file diff --git a/.cursor/rules b/.cursor/rules index 2d5c9a9..4c6c243 120000 --- a/.cursor/rules +++ b/.cursor/rules @@ -1 +1 @@ -../.agents/rules \ No newline at end of file +../rules \ No newline at end of file diff --git a/.cursor/skills b/.cursor/skills index 2b7a412..42c5394 120000 --- a/.cursor/skills +++ b/.cursor/skills @@ -1 +1 @@ -../.agents/skills \ No newline at end of file +../skills \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 3d38ea7..f3f52db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,8 +16,8 @@ ``` bin/cli.ts # CLI entry point src/ # TypeScript source -.agents/skills/ # Agent skills -.agents/rules/ # Shared rules (bundled) +skills/ # Agent skills +rules/ # Shared rules (bundled) ``` **Test CLI:** `bun bin/cli.ts ` @@ -38,7 +38,7 @@ After code changes: 1. `bun run check` - Must pass 2. `bun test` - Must pass 3. `bun bin/cli.ts ` - Test CLI changes -4. For skills: `bunx @plaited/development-skills validate-skill .agents/skills` +4. For skills: `bunx @plaited/development-skills validate-skill skills` **Rule compliance:** Read rules below, use verification patterns to self-check @@ -59,13 +59,13 @@ After code changes: Compressed rules with embedded verification patterns: -- @.agents/rules/core.md - TypeScript conventions (type>interface, no any, arrow fns) -- @.agents/rules/testing.md - Test patterns (test>it, no conditional assertions) -- @.agents/rules/modules.md - Module organization (no index.ts, explicit .ts) -- @.agents/rules/workflow.md - Git + GitHub CLI patterns -- @.agents/rules/bun.md - Bun APIs over Node.js -- @.agents/rules/accuracy.md - 95% confidence, verify before stating -- @.agents/rules/documentation.md - TSDoc standards +- @rules/core.md - TypeScript conventions (type>interface, no any, arrow fns) +- @rules/testing.md - Test patterns (test>it, no conditional assertions) +- @rules/modules.md - Module organization (no index.ts, explicit .ts) +- @rules/workflow.md - Git + GitHub CLI patterns +- @rules/bun.md - Bun APIs over Node.js +- @rules/accuracy.md - 95% confidence, verify before stating +- @rules/documentation.md - TSDoc standards diff --git a/bin/cli.ts b/bin/cli.ts index 7b7ad84..dc3e617 100755 --- a/bin/cli.ts +++ b/bin/cli.ts @@ -20,7 +20,7 @@ * bunx @plaited/development-skills lsp-hover src/index.ts 10 5 * bunx @plaited/development-skills lsp-find parseConfig * bunx @plaited/development-skills validate-skill .claude/skills/my-skill - * bunx @plaited/development-skills scaffold-rules --agent=claude --format=json + * bunx @plaited/development-skills scaffold-rules */ import { lspAnalyze } from '../src/lsp-analyze.ts' @@ -64,7 +64,7 @@ Examples: bunx @plaited/development-skills lsp-symbols src/app.ts bunx @plaited/development-skills lsp-analyze src/app.ts bunx @plaited/development-skills validate-skill .claude/skills/my-skill - bunx @plaited/development-skills scaffold-rules --agent=claude --format=json + bunx @plaited/development-skills scaffold-rules Options: -h, --help Show this help diff --git a/package.json b/package.json index dd490f4..f0fd62a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "files": [ "bin/", "src/", - ".agents/" + "rules/", + "skills/" ], "publishConfig": { "access": "public" diff --git a/.agents/rules/accuracy.md b/rules/accuracy.md similarity index 94% rename from .agents/rules/accuracy.md rename to rules/accuracy.md index e22bc33..56567f6 100644 --- a/.agents/rules/accuracy.md +++ b/rules/accuracy.md @@ -28,4 +28,4 @@ - Code review: Read files before commenting - Patterns: Confirm examples reflect actual usage -See .agents/rules/testing.md for verification in test contexts. +See rules/testing.md for verification in test contexts. diff --git a/.agents/rules/bun.md b/rules/bun.md similarity index 100% rename from .agents/rules/bun.md rename to rules/bun.md diff --git a/.agents/rules/core.md b/rules/core.md similarity index 100% rename from .agents/rules/core.md rename to rules/core.md diff --git a/.agents/rules/documentation.md b/rules/documentation.md similarity index 100% rename from .agents/rules/documentation.md rename to rules/documentation.md diff --git a/.agents/rules/modules.md b/rules/modules.md similarity index 100% rename from .agents/rules/modules.md rename to rules/modules.md diff --git a/rules/skill-activation.md b/rules/skill-activation.md new file mode 100644 index 0000000..13a8a33 --- /dev/null +++ b/rules/skill-activation.md @@ -0,0 +1,26 @@ +# Skill Activation + +**Evaluate before implementing** - Check available skills for relevance before starting work + +**Activation sequence:** + +1. **Evaluate** - For each skill in ``, assess: `[skill-name] - YES/NO - [reason]` +2. **Activate** - Call `Skill(skill-name)` for each relevant skill before proceeding +3. **Implement** - Begin work only after activation is complete + +*Verify:* Did you check available skills before starting implementation? +*Fix:* Pause, evaluate skills, activate relevant ones, then continue + +**Example:** +``` +- code-patterns: NO - not writing code +- git-workflow: YES - need commit conventions +- documentation: YES - writing README + +> Skill(git-workflow) +> Skill(documentation) +``` + +**Activation before implementation** - Evaluating skills without calling `Skill()` provides no benefit +*Verify:* Check that `Skill()` was called for each YES evaluation +*Fix:* Call `Skill(skill-name)` for skipped activations diff --git a/.agents/rules/testing.md b/rules/testing.md similarity index 100% rename from .agents/rules/testing.md rename to rules/testing.md diff --git a/.agents/rules/workflow.md b/rules/workflow.md similarity index 100% rename from .agents/rules/workflow.md rename to rules/workflow.md diff --git a/.agents/skills/code-documentation/SKILL.md b/skills/code-documentation/SKILL.md similarity index 100% rename from .agents/skills/code-documentation/SKILL.md rename to skills/code-documentation/SKILL.md diff --git a/.agents/skills/code-documentation/references/internal-templates.md b/skills/code-documentation/references/internal-templates.md similarity index 100% rename from .agents/skills/code-documentation/references/internal-templates.md rename to skills/code-documentation/references/internal-templates.md diff --git a/.agents/skills/code-documentation/references/maintenance.md b/skills/code-documentation/references/maintenance.md similarity index 100% rename from .agents/skills/code-documentation/references/maintenance.md rename to skills/code-documentation/references/maintenance.md diff --git a/.agents/skills/code-documentation/references/public-api-templates.md b/skills/code-documentation/references/public-api-templates.md similarity index 100% rename from .agents/skills/code-documentation/references/public-api-templates.md rename to skills/code-documentation/references/public-api-templates.md diff --git a/.agents/skills/code-documentation/references/type-documentation.md b/skills/code-documentation/references/type-documentation.md similarity index 100% rename from .agents/skills/code-documentation/references/type-documentation.md rename to skills/code-documentation/references/type-documentation.md diff --git a/.agents/skills/code-documentation/references/workflow.md b/skills/code-documentation/references/workflow.md similarity index 100% rename from .agents/skills/code-documentation/references/workflow.md rename to skills/code-documentation/references/workflow.md diff --git a/.agents/skills/optimize-agents-md/SKILL.md b/skills/optimize-agents-md/SKILL.md similarity index 100% rename from .agents/skills/optimize-agents-md/SKILL.md rename to skills/optimize-agents-md/SKILL.md diff --git a/.agents/skills/scaffold-rules/SKILL.md b/skills/scaffold-rules/SKILL.md similarity index 50% rename from .agents/skills/scaffold-rules/SKILL.md rename to skills/scaffold-rules/SKILL.md index f0a7383..9a27a09 100644 --- a/.agents/skills/scaffold-rules/SKILL.md +++ b/skills/scaffold-rules/SKILL.md @@ -34,9 +34,8 @@ bunx @plaited/development-skills scaffold-rules ``` This will: -1. Copy rules to `.agents/rules/` (canonical location) -2. Create symlinks in `.claude/rules` and `.cursor/rules` (if those directories exist) -3. Fallback: append links to `AGENTS.md` if no agent directories found +1. Write rules into `AGENTS.md` (creates if missing, updates between markers if present) +2. Add `@AGENTS.md` reference to `CLAUDE.md` if it exists without one ### Step 3: Report to User @@ -51,25 +50,32 @@ Tell the user what was created based on the `actions` output. ## How It Works +Rules are written directly into `AGENTS.md` between markers: + ``` -.agents/rules/ ← Canonical location (files copied here) - ├── testing.md - ├── bun.md - └── ... + + +## Rules + +(rule content inlined here) -.claude/rules -> ../.agents/rules ← Symlink (if .claude/ exists) -.cursor/rules -> ../.agents/rules ← Symlink (if .cursor/ exists) + ``` -| Project has... | Copy | Symlinks | AGENTS.md | -|----------------|------|----------|-----------| -| `.agents/` only | ✓ | None | No | -| `.claude/` only | ✓ | `.claude/rules` | No | -| `.cursor/` only | ✓ | `.cursor/rules` | No | -| `.agents/` + `.claude/` | ✓ | `.claude/rules` | No | -| `.agents/` + `.cursor/` | ✓ | `.cursor/rules` | No | -| `.agents/` + `.claude/` + `.cursor/` | ✓ | Both | No | -| None of the above | ✓ | None | ✓ Append links | +- **No AGENTS.md**: Creates one with rules section +- **AGENTS.md without markers**: Appends rules section with markers +- **AGENTS.md with markers**: Replaces content between markers (preserves user content outside) +- **CLAUDE.md exists**: Adds `@AGENTS.md` reference if not already present + +## Troubleshooting + +| Issue | Cause | Fix | +|-------|-------|-----| +| Rules duplicated in AGENTS.md | Markers were manually deleted | Remove duplicate section, re-run `scaffold-rules` | +| Update didn't apply | Only one marker present (start or end) | Ensure both `` and `` exist, or delete both to get a fresh append | +| `@AGENTS.md` not added to CLAUDE.md | CLAUDE.md doesn't exist | Create CLAUDE.md first, then re-run | + +**Do not** manually edit content between the `PLAITED-RULES-START` and `PLAITED-RULES-END` markers — it will be overwritten on next run. ## Related Skills diff --git a/.agents/skills/typescript-lsp/SKILL.md b/skills/typescript-lsp/SKILL.md similarity index 100% rename from .agents/skills/typescript-lsp/SKILL.md rename to skills/typescript-lsp/SKILL.md diff --git a/.agents/skills/validate-skill/SKILL.md b/skills/validate-skill/SKILL.md similarity index 100% rename from .agents/skills/validate-skill/SKILL.md rename to skills/validate-skill/SKILL.md diff --git a/src/scaffold-rules.ts b/src/scaffold-rules.ts index 38c992f..34fb3fa 100644 --- a/src/scaffold-rules.ts +++ b/src/scaffold-rules.ts @@ -1,46 +1,20 @@ #!/usr/bin/env bun /** - * Scaffold development rules - Copy bundled rules and create symlinks + * Scaffold development rules into AGENTS.md * - * Copies rules from the package to `.agents/rules/` (canonical location), - * creates symlinks for `.claude/` and `.cursor/` agent directories, - * and falls back to appending links in `AGENTS.md` if no agent dirs exist. + * Writes rules into AGENTS.md (creates if missing, uses markers for updates). + * Adds `@AGENTS.md` reference to CLAUDE.md if it exists without one. * * @throws When source rules directory cannot be read - * @throws When target directory cannot be created (permissions) - * @throws When symlink creation fails (existing file, not directory) */ -import { mkdir, readdir, readlink, stat, symlink } from 'node:fs/promises' +import { readdir } from 'node:fs/promises' import { join } from 'node:path' import { parseArgs } from 'node:util' -/** Agents that get symlinks to .agents/rules (not .agents itself) */ -const SYMLINK_AGENTS = ['.claude', '.cursor'] as const - -/** All supported agent directories (including .agents which gets direct copy) */ -const ALL_AGENTS = ['.agents', ...SYMLINK_AGENTS] as const - -/** Canonical rules location */ -const TARGET_RULES = '.agents/rules' as const - -/** - * NOTE: This tool only scaffolds RULES, not skills. - * Skills symlinks (.claude/skills -> ../.agents/skills) are managed separately - * via the skills-installer or manual setup. - */ - -/** - * Check if path is a directory - */ -const isDirectory = async (path: string): Promise => { - try { - const s = await stat(path) - return s.isDirectory() - } catch { - return false - } -} +/** Markers for the rules section in AGENTS.md */ +const RULES_START = '' +const RULES_END = '' /** * Main scaffold-rules function @@ -59,7 +33,7 @@ export const scaffoldRules = async (args: string[]): Promise => { const dryRun = values['dry-run'] as boolean | undefined const listOnly = values.list as boolean | undefined - const sourceRules = join(import.meta.dir, '../.agents/rules') + const sourceRules = join(import.meta.dir, '../rules') const cwd = process.cwd() // Get available rules @@ -74,77 +48,60 @@ export const scaffoldRules = async (args: string[]): Promise => { const actions: string[] = [] - // Check for agent directories BEFORE copying (since copy creates .agents/) - // This determines whether to fall back to AGENTS.md append - let hadAgentDirBeforeScaffold = false - for (const agent of ALL_AGENTS) { - if (await isDirectory(join(cwd, agent))) { - hadAgentDirBeforeScaffold = true - break - } - } - - // 1. Copy rules to .agents/rules/ (canonical location, serves .agents agent) - const targetDir = join(cwd, TARGET_RULES) - if (!dryRun) { - await mkdir(targetDir, { recursive: true }) - } + // 1. Write rules into AGENTS.md + const agentsMdPath = join(cwd, 'AGENTS.md') + const agentsMd = Bun.file(agentsMdPath) + const ruleEntries: string[] = [] for (const file of rules) { - const src = join(sourceRules, file) - const dest = join(targetDir, file) - if (!dryRun) { - await Bun.write(dest, await Bun.file(src).text()) - } - actions.push(`copy: ${TARGET_RULES}/${file}`) + const content = await Bun.file(join(sourceRules, file)).text() + ruleEntries.push(content) } + const rulesContent = ruleEntries.join('\n\n') + const rulesSection = `${RULES_START}\n\n## Rules\n\n${rulesContent}\n\n${RULES_END}` - // 2. Symlink for other agents (.claude, .cursor) - for (const agent of SYMLINK_AGENTS) { - const agentDir = join(cwd, agent) - if (await isDirectory(agentDir)) { - const rulesLink = join(agentDir, 'rules') - - // Check if symlink already exists and points to right place - try { - const existing = await readlink(rulesLink) - if (existing === '../.agents/rules') { - actions.push(`skip: ${agent}/rules (symlink exists)`) - continue - } - } catch { - // Doesn't exist or not a symlink - proceed to create - } + if (await agentsMd.exists()) { + const content = await agentsMd.text() + const startIdx = content.indexOf(RULES_START) + const endIdx = content.indexOf(RULES_END) + if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { + const before = content.slice(0, startIdx) + const after = content.slice(endIdx + RULES_END.length) if (!dryRun) { - await symlink('../.agents/rules', rulesLink) + await Bun.write(agentsMdPath, `${before}${rulesSection}${after}`) } - actions.push(`symlink: ${agent}/rules -> ../.agents/rules`) + actions.push('update: AGENTS.md (rules section)') + } else { + if (!dryRun) { + await Bun.write(agentsMdPath, `${content}\n${rulesSection}\n`) + } + actions.push('append: AGENTS.md (rules section)') + } + } else { + if (!dryRun) { + await Bun.write(agentsMdPath, `# AGENTS\n\n${rulesSection}\n`) } + actions.push('create: AGENTS.md (rules section)') } - // 3. Fallback: append to AGENTS.md only if NO agent directories existed before copy - if (!hadAgentDirBeforeScaffold) { - const agentsMdPath = join(cwd, 'AGENTS.md') - const agentsMd = Bun.file(agentsMdPath) - - if (await agentsMd.exists()) { - const content = await agentsMd.text() - if (content.includes('.agents/rules')) { - actions.push('skip: AGENTS.md (already has rules)') - } else { - const links = rules.map((f) => `- [${f.replace('.md', '')}](${TARGET_RULES}/${f})`).join('\n') - const section = `\n## Rules\n\n${links}\n` - - if (!dryRun) { - await Bun.write(agentsMdPath, content + section) - } - actions.push('append: AGENTS.md (rules section)') + // 2. Add @AGENTS.md reference to CLAUDE.md if it exists without one + const claudeMdPath = join(cwd, 'CLAUDE.md') + const claudeMd = Bun.file(claudeMdPath) + + if (await claudeMd.exists()) { + const content = await claudeMd.text() + if (/^@AGENTS\.md/m.test(content)) { + actions.push('skip: CLAUDE.md (already references @AGENTS.md)') + } else { + if (!dryRun) { + await Bun.write(claudeMdPath, `@AGENTS.md\n\n${content}`) } + actions.push('update: CLAUDE.md (added @AGENTS.md reference)') } } - console.log(JSON.stringify({ dryRun: !!dryRun, targetRules: TARGET_RULES, actions }, null, 2)) + console.log(JSON.stringify({ dryRun: !!dryRun, actions }, null, 2)) } // CLI entry point diff --git a/src/tests/scaffold-rules.spec.ts b/src/tests/scaffold-rules.spec.ts index f8a3843..d733351 100644 --- a/src/tests/scaffold-rules.spec.ts +++ b/src/tests/scaffold-rules.spec.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test' -import { mkdir, readlink, rm } from 'node:fs/promises' +import { mkdir, rm } from 'node:fs/promises' import { join } from 'node:path' import { $ } from 'bun' @@ -9,7 +9,6 @@ type ListOutput = { type ScaffoldOutput = { dryRun: boolean - targetRules: string actions: string[] } @@ -19,13 +18,11 @@ describe('scaffold-rules', () => { let testDir: string beforeEach(async () => { - // Create a temp directory for each test testDir = join(import.meta.dir, `test-scaffold-${Date.now()}`) await mkdir(testDir, { recursive: true }) }) afterEach(async () => { - // Clean up temp directory await rm(testDir, { recursive: true, force: true }) }) @@ -63,26 +60,14 @@ describe('scaffold-rules', () => { const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules --dry-run`.json() expect(result.dryRun).toBe(true) - expect(result.targetRules).toBe('.agents/rules') expect(result.actions).toBeArray() - }) - - test('shows copy actions for each rule', async () => { - const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules --dry-run`.json() - - const copyActions = result.actions.filter((a) => a.startsWith('copy:')) - expect(copyActions.length).toBeGreaterThan(0) - - // Should include our compressed rules - expect(copyActions.some((a) => a.includes('core.md'))).toBe(true) - expect(copyActions.some((a) => a.includes('testing.md'))).toBe(true) + expect(result.actions).toContain('create: AGENTS.md (rules section)') }) test('does not create files in dry-run mode', async () => { await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules --dry-run`.json() - const rulesDir = Bun.file(join(testDir, '.agents/rules')) - expect(await rulesDir.exists()).toBe(false) + expect(await Bun.file(join(testDir, 'AGENTS.md')).exists()).toBe(false) }) test('short flag -n works', async () => { @@ -92,109 +77,114 @@ describe('scaffold-rules', () => { }) }) - describe('copy behavior', () => { - test('copies rules to .agents/rules/', async () => { + describe('AGENTS.md behavior', () => { + test('creates AGENTS.md if it does not exist', async () => { await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() - const coreRule = Bun.file(join(testDir, '.agents/rules/core.md')) - expect(await coreRule.exists()).toBe(true) - - const content = await coreRule.text() + const content = await Bun.file(join(testDir, 'AGENTS.md')).text() + expect(content).toContain('# AGENTS') + expect(content).toContain('## Rules') + expect(content).toContain('') + expect(content).toContain('') expect(content).toContain('# Core Conventions') }) - test('copies all compressed rules', async () => { - await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() - - const expectedRules = ['accuracy', 'bun', 'core', 'documentation', 'modules', 'testing', 'workflow'] - - for (const rule of expectedRules) { - const ruleFile = Bun.file(join(testDir, `.agents/rules/${rule}.md`)) - expect(await ruleFile.exists()).toBe(true) - } - }) + test('appends rules with markers to existing AGENTS.md', async () => { + await Bun.write(join(testDir, 'AGENTS.md'), '# My Project\n\nCustom content\n') - test('creates .agents/rules directory if missing', async () => { await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() - const rulesDir = await Bun.file(join(testDir, '.agents/rules/core.md')).exists() - expect(rulesDir).toBe(true) + const content = await Bun.file(join(testDir, 'AGENTS.md')).text() + expect(content).toStartWith('# My Project\n\nCustom content\n') + expect(content).toContain('') + expect(content).toContain('## Rules') + expect(content).toContain('') }) - }) - describe('symlink behavior', () => { - test('creates symlink for .claude/rules when .claude exists', async () => { - // Create .claude directory - await mkdir(join(testDir, '.claude'), { recursive: true }) + test('updates existing markers without overwriting user content', async () => { + const initial = + '# Project\n\nUser notes\n\n\n\n## Rules\n\n- old rule\n\n\n\nMore user content\n' + await Bun.write(join(testDir, 'AGENTS.md'), initial) - await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() + const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json() - // Check symlink exists and points to right place - const linkTarget = await readlink(join(testDir, '.claude/rules')) - expect(linkTarget).toBe('../.agents/rules') + const content = await Bun.file(join(testDir, 'AGENTS.md')).text() + expect(content).toContain('User notes') + expect(content).toContain('More user content') + expect(content).not.toContain('old rule') + expect(content).toContain('# Core Conventions') + expect(result.actions).toContain('update: AGENTS.md (rules section)') }) - test('creates symlink for .cursor/rules when .cursor exists', async () => { - // Create .cursor directory - await mkdir(join(testDir, '.cursor'), { recursive: true }) + test('appends when only start marker exists (no end marker)', async () => { + const malformed = '# Project\n\n\n\nOrphan content\n' + await Bun.write(join(testDir, 'AGENTS.md'), malformed) - await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() + const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json() - const linkTarget = await readlink(join(testDir, '.cursor/rules')) - expect(linkTarget).toBe('../.agents/rules') + const content = await Bun.file(join(testDir, 'AGENTS.md')).text() + expect(content).toContain('# Core Conventions') + expect(result.actions).toContain('append: AGENTS.md (rules section)') }) - test('skips symlink if already exists with correct target', async () => { - await mkdir(join(testDir, '.claude'), { recursive: true }) + test('appends when markers are reversed (end before start)', async () => { + const reversed = + '# Project\n\n\n\nMiddle\n\n\n\nOld rules\n' + await Bun.write(join(testDir, 'AGENTS.md'), reversed) - // Run twice - await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json() - // Second run should skip the symlink - const skipAction = result.actions.find((a) => a.includes('.claude/rules') && a.includes('skip')) - expect(skipAction).toBeDefined() + const content = await Bun.file(join(testDir, 'AGENTS.md')).text() + expect(content).toContain('# Core Conventions') + expect(result.actions).toContain('append: AGENTS.md (rules section)') }) - test('does not create symlink if agent dir does not exist', async () => { + test('appends when only end marker exists (no start marker)', async () => { + const malformed = '# Project\n\nSome content\n\n\n' + await Bun.write(join(testDir, 'AGENTS.md'), malformed) + const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json() - // Should not have any symlink actions - const symlinkActions = result.actions.filter((a) => a.startsWith('symlink:')) - expect(symlinkActions.length).toBe(0) + const content = await Bun.file(join(testDir, 'AGENTS.md')).text() + expect(content).toContain('# Core Conventions') + expect(result.actions).toContain('append: AGENTS.md (rules section)') }) }) - describe('AGENTS.md fallback', () => { - test('appends rules to AGENTS.md when no agent dirs exist', async () => { - // Create AGENTS.md without any agent directories - await Bun.write(join(testDir, 'AGENTS.md'), '# AGENTS\n\nSome content\n') + describe('CLAUDE.md behavior', () => { + test('adds @AGENTS.md reference to existing CLAUDE.md', async () => { + await Bun.write(join(testDir, 'CLAUDE.md'), '# Claude Config\n\nSome settings\n') await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() - const content = await Bun.file(join(testDir, 'AGENTS.md')).text() - expect(content).toContain('## Rules') - expect(content).toContain('.agents/rules/') + const content = await Bun.file(join(testDir, 'CLAUDE.md')).text() + expect(content).toStartWith('@AGENTS.md\n\n') + expect(content).toContain('# Claude Config') }) - test('does not append if agent dir exists', async () => { - await Bun.write(join(testDir, 'AGENTS.md'), '# AGENTS\n\nSome content\n') - await mkdir(join(testDir, '.agents'), { recursive: true }) + test('skips CLAUDE.md if @AGENTS.md reference already exists', async () => { + await Bun.write(join(testDir, 'CLAUDE.md'), '@AGENTS.md\n\n# Claude Config\n') - await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() + const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json() - const content = await Bun.file(join(testDir, 'AGENTS.md')).text() - // Should not have appended rules section (since .agents exists) - expect(content).not.toContain('## Rules') + const skipAction = result.actions.find((a) => a.includes('CLAUDE.md') && a.includes('skip')) + expect(skipAction).toBeDefined() }) - test('skips if AGENTS.md already has rules', async () => { - await Bun.write(join(testDir, 'AGENTS.md'), '# AGENTS\n\nSee .agents/rules/ for rules\n') + test('adds reference when @AGENTS.md only appears inline (not at start of line)', async () => { + await Bun.write(join(testDir, 'CLAUDE.md'), '# Config\n\nSee `@AGENTS.md` for details\n') const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json() - const skipAction = result.actions.find((a) => a.includes('AGENTS.md') && a.includes('skip')) - expect(skipAction).toBeDefined() + const content = await Bun.file(join(testDir, 'CLAUDE.md')).text() + expect(content).toStartWith('@AGENTS.md\n\n') + expect(result.actions).toContain('update: CLAUDE.md (added @AGENTS.md reference)') + }) + + test('does not create CLAUDE.md if it does not exist', async () => { + await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() + + expect(await Bun.file(join(testDir, 'CLAUDE.md')).exists()).toBe(false) }) }) @@ -203,60 +193,25 @@ describe('scaffold-rules', () => { const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json() expect(result).toHaveProperty('dryRun') - expect(result).toHaveProperty('targetRules') expect(result).toHaveProperty('actions') - expect(result.dryRun).toBe(false) - expect(result.targetRules).toBe('.agents/rules') expect(result.actions).toBeArray() }) }) - describe('symlink readability', () => { - test('rules are readable through .claude symlink', async () => { - await mkdir(join(testDir, '.claude'), { recursive: true }) - await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() - - // Read through symlink path - const content = await Bun.file(join(testDir, '.claude/rules/core.md')).text() - expect(content).toContain('# Core Conventions') - }) - - test('rules are readable through .cursor symlink', async () => { - await mkdir(join(testDir, '.cursor'), { recursive: true }) - await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() - - // Read through symlink path - const content = await Bun.file(join(testDir, '.cursor/rules/core.md')).text() - expect(content).toContain('# Core Conventions') - }) - }) - - describe('error handling', () => { - test('fails when existing file blocks symlink creation', async () => { - await mkdir(join(testDir, '.claude'), { recursive: true }) - // Create a file where symlink should go - await Bun.write(join(testDir, '.claude/rules'), 'not a directory') - - // Should fail when trying to create symlink over existing file - const result = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules 2>&1`.nothrow().text() - expect(result).toContain('EEXIST') - }) - }) - describe('rule content', () => { - test('core.md contains TypeScript conventions', async () => { + test('AGENTS.md contains TypeScript conventions', async () => { await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() - const content = await Bun.file(join(testDir, '.agents/rules/core.md')).text() + const content = await Bun.file(join(testDir, 'AGENTS.md')).text() expect(content).toContain('Type over interface') expect(content).toContain('Arrow functions') }) - test('testing.md contains test conventions', async () => { + test('AGENTS.md contains test conventions', async () => { await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() - const content = await Bun.file(join(testDir, '.agents/rules/testing.md')).text() + const content = await Bun.file(join(testDir, 'AGENTS.md')).text() expect(content).toContain('test not it') expect(content).toContain('No conditional assertions') }) @@ -264,7 +219,7 @@ describe('scaffold-rules', () => { test('rules include verification patterns', async () => { await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() - const content = await Bun.file(join(testDir, '.agents/rules/core.md')).text() + const content = await Bun.file(join(testDir, 'AGENTS.md')).text() expect(content).toContain('*Verify:*') expect(content).toContain('*Fix:*') }) @@ -272,9 +227,7 @@ describe('scaffold-rules', () => { test('rules are compressed (no verbose examples)', async () => { await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() - const content = await Bun.file(join(testDir, '.agents/rules/core.md')).text() - - // Should not have verbose code blocks with Good/Avoid patterns + const content = await Bun.file(join(testDir, 'AGENTS.md')).text() expect(content).not.toContain('// ✅ Good') expect(content).not.toContain('// ❌ Avoid') })