From 7a10adb41a1c841186dda14f91094fd8c7ff90fd Mon Sep 17 00:00:00 2001 From: Edward Irby Date: Tue, 10 Feb 2026 07:52:41 -0800 Subject: [PATCH 1/3] refactor: simplify scaffold-rules to write AGENTS.md directly - Remove symlink/copy logic and --dirs flag entirely - Write rules inline into AGENTS.md with HTML comment markers - Add @AGENTS.md reference to CLAUDE.md if present - Move .agents/rules/ and .agents/skills/ to project root - Update symlinks (.claude/, .cursor/) to new paths - Update package.json files array, AGENTS.md references - Rewrite tests for new AGENTS.md-based approach (57 passing) --- .claude/rules | 2 +- .claude/skills | 2 +- .cursor/rules | 2 +- .cursor/skills | 2 +- AGENTS.md | 20 +- bin/cli.ts | 4 +- package.json | 3 +- {.agents/rules => rules}/accuracy.md | 2 +- {.agents/rules => rules}/bun.md | 0 {.agents/rules => rules}/core.md | 0 {.agents/rules => rules}/documentation.md | 0 {.agents/rules => rules}/modules.md | 0 rules/skill-activation.md | 27 +++ {.agents/rules => rules}/testing.md | 0 {.agents/rules => rules}/workflow.md | 0 .../code-documentation/SKILL.md | 0 .../references/internal-templates.md | 0 .../references/maintenance.md | 0 .../references/public-api-templates.md | 0 .../references/type-documentation.md | 0 .../code-documentation/references/workflow.md | 0 .../optimize-agents-md/SKILL.md | 0 .../skills => skills}/scaffold-rules/SKILL.md | 32 ++- .../skills => skills}/typescript-lsp/SKILL.md | 0 .../skills => skills}/validate-skill/SKILL.md | 0 src/scaffold-rules.ts | 139 +++++-------- src/tests/scaffold-rules.spec.ts | 183 +++++------------- 27 files changed, 154 insertions(+), 264 deletions(-) rename {.agents/rules => rules}/accuracy.md (94%) rename {.agents/rules => rules}/bun.md (100%) rename {.agents/rules => rules}/core.md (100%) rename {.agents/rules => rules}/documentation.md (100%) rename {.agents/rules => rules}/modules.md (100%) create mode 100644 rules/skill-activation.md rename {.agents/rules => rules}/testing.md (100%) rename {.agents/rules => rules}/workflow.md (100%) rename {.agents/skills => skills}/code-documentation/SKILL.md (100%) rename {.agents/skills => skills}/code-documentation/references/internal-templates.md (100%) rename {.agents/skills => skills}/code-documentation/references/maintenance.md (100%) rename {.agents/skills => skills}/code-documentation/references/public-api-templates.md (100%) rename {.agents/skills => skills}/code-documentation/references/type-documentation.md (100%) rename {.agents/skills => skills}/code-documentation/references/workflow.md (100%) rename {.agents/skills => skills}/optimize-agents-md/SKILL.md (100%) rename {.agents/skills => skills}/scaffold-rules/SKILL.md (56%) rename {.agents/skills => skills}/typescript-lsp/SKILL.md (100%) rename {.agents/skills => skills}/validate-skill/SKILL.md (100%) 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..0aa4598 --- /dev/null +++ b/rules/skill-activation.md @@ -0,0 +1,27 @@ +# Skill Activation + +INSTRUCTION: MANDATORY SKILL ACTIVATION SEQUENCE + +Step 1 - EVALUATE (do this in your response): +For each skill in , state: [skill-name] - YES/NO - [reason] + +Step 2 - ACTIVATE (do this immediately after Step 1): +IF any skills are YES → Use Skill(skill-name) tool for EACH relevant skill NOW +IF no skills are YES → State "No skills needed" and proceed + +Step 3 - IMPLEMENT: +Only after Step 2 is complete, proceed with implementation. + +CRITICAL: You MUST call Skill() tool in Step 2. Do NOT skip to implementation. +The evaluation (Step 1) is WORTHLESS unless you ACTIVATE (Step 2) the skills. + +Example of correct sequence: +- code-patterns: NO - not writing code +- git-workflow: YES - need commit conventions +- documentation: YES - writing README + +[Then IMMEDIATELY use Skill() tool:] +> Skill(git-workflow) +> Skill(documentation) + +[THEN and ONLY THEN start implementation] 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 56% rename from .agents/skills/scaffold-rules/SKILL.md rename to skills/scaffold-rules/SKILL.md index f0a7383..58954ba 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,22 @@ 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 ## 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..695eff6 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) { + 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 (content.includes('@AGENTS.md')) { + 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..1b7e287 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,70 @@ 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 }) + 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 linkTarget = await readlink(join(testDir, '.cursor/rules')) - expect(linkTarget).toBe('../.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('skips symlink if already exists with correct target', async () => { - await mkdir(join(testDir, '.claude'), { 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') - // 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')) + const skipAction = result.actions.find((a) => a.includes('CLAUDE.md') && a.includes('skip')) expect(skipAction).toBeDefined() }) - test('does not create symlink if agent dir does not exist', async () => { - 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) - }) - }) - - 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') - - 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/') - }) - - 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('does not create CLAUDE.md if it does not exist', async () => { await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet() - const content = await Bun.file(join(testDir, 'AGENTS.md')).text() - // Should not have appended rules section (since .agents exists) - expect(content).not.toContain('## Rules') - }) - - test('skips if AGENTS.md already has rules', async () => { - await Bun.write(join(testDir, 'AGENTS.md'), '# AGENTS\n\nSee .agents/rules/ for rules\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() + expect(await Bun.file(join(testDir, 'CLAUDE.md')).exists()).toBe(false) }) }) @@ -203,60 +149,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 +175,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 +183,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') }) From d12cb44ccb4ae4a4970b4f7228f31c032cbb535a Mon Sep 17 00:00:00 2001 From: Edward Irby Date: Tue, 10 Feb 2026 07:57:10 -0800 Subject: [PATCH 2/3] fix: address PR review feedback - Rewrite skill-activation.md to match professional tone of other rules - Add tests for mismatched markers (start-only, end-only) - Add troubleshooting section to scaffold-rules SKILL.md --- rules/skill-activation.md | 27 +++++++++++++-------------- skills/scaffold-rules/SKILL.md | 10 ++++++++++ src/tests/scaffold-rules.spec.ts | 22 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/rules/skill-activation.md b/rules/skill-activation.md index 0aa4598..13a8a33 100644 --- a/rules/skill-activation.md +++ b/rules/skill-activation.md @@ -1,27 +1,26 @@ # Skill Activation -INSTRUCTION: MANDATORY SKILL ACTIVATION SEQUENCE +**Evaluate before implementing** - Check available skills for relevance before starting work -Step 1 - EVALUATE (do this in your response): -For each skill in , state: [skill-name] - YES/NO - [reason] +**Activation sequence:** -Step 2 - ACTIVATE (do this immediately after Step 1): -IF any skills are YES → Use Skill(skill-name) tool for EACH relevant skill NOW -IF no skills are YES → State "No skills needed" and proceed +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 -Step 3 - IMPLEMENT: -Only after Step 2 is complete, proceed with implementation. +*Verify:* Did you check available skills before starting implementation? +*Fix:* Pause, evaluate skills, activate relevant ones, then continue -CRITICAL: You MUST call Skill() tool in Step 2. Do NOT skip to implementation. -The evaluation (Step 1) is WORTHLESS unless you ACTIVATE (Step 2) the skills. - -Example of correct sequence: +**Example:** +``` - code-patterns: NO - not writing code - git-workflow: YES - need commit conventions - documentation: YES - writing README -[Then IMMEDIATELY use Skill() tool:] > Skill(git-workflow) > Skill(documentation) +``` -[THEN and ONLY THEN start implementation] +**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/skills/scaffold-rules/SKILL.md b/skills/scaffold-rules/SKILL.md index 58954ba..9a27a09 100644 --- a/skills/scaffold-rules/SKILL.md +++ b/skills/scaffold-rules/SKILL.md @@ -67,6 +67,16 @@ Rules are written directly into `AGENTS.md` between 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 - **validate-skill** - Validate skill directories against AgentSkills spec diff --git a/src/tests/scaffold-rules.spec.ts b/src/tests/scaffold-rules.spec.ts index 1b7e287..44f0327 100644 --- a/src/tests/scaffold-rules.spec.ts +++ b/src/tests/scaffold-rules.spec.ts @@ -115,6 +115,28 @@ describe('scaffold-rules', () => { expect(content).toContain('# Core Conventions') expect(result.actions).toContain('update: AGENTS.md (rules section)') }) + + 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) + + const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json() + + 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('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() + + 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('CLAUDE.md behavior', () => { From 01051f06373725db4bd2d678095488d6acb9c966 Mon Sep 17 00:00:00 2001 From: Edward Irby Date: Tue, 10 Feb 2026 09:43:12 -0800 Subject: [PATCH 3/3] fix: improve marker and reference detection robustness - Use regex for @AGENTS.md detection (start-of-line only, avoids inline false positives) - Add guard for reversed markers (endIdx > startIdx) - Add tests: reversed markers, inline @AGENTS.md false positive --- src/scaffold-rules.ts | 4 ++-- src/tests/scaffold-rules.spec.ts | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/scaffold-rules.ts b/src/scaffold-rules.ts index 695eff6..34fb3fa 100644 --- a/src/scaffold-rules.ts +++ b/src/scaffold-rules.ts @@ -65,7 +65,7 @@ export const scaffoldRules = async (args: string[]): Promise => { const startIdx = content.indexOf(RULES_START) const endIdx = content.indexOf(RULES_END) - if (startIdx !== -1 && endIdx !== -1) { + if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { const before = content.slice(0, startIdx) const after = content.slice(endIdx + RULES_END.length) if (!dryRun) { @@ -91,7 +91,7 @@ export const scaffoldRules = async (args: string[]): Promise => { if (await claudeMd.exists()) { const content = await claudeMd.text() - if (content.includes('@AGENTS.md')) { + if (/^@AGENTS\.md/m.test(content)) { actions.push('skip: CLAUDE.md (already references @AGENTS.md)') } else { if (!dryRun) { diff --git a/src/tests/scaffold-rules.spec.ts b/src/tests/scaffold-rules.spec.ts index 44f0327..d733351 100644 --- a/src/tests/scaffold-rules.spec.ts +++ b/src/tests/scaffold-rules.spec.ts @@ -127,6 +127,18 @@ describe('scaffold-rules', () => { expect(result.actions).toContain('append: AGENTS.md (rules section)') }) + 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) + + const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json() + + 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('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) @@ -159,6 +171,16 @@ describe('scaffold-rules', () => { expect(skipAction).toBeDefined() }) + 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 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()