@plaited/development-skills 0.6.5 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,11 +42,11 @@ LSP understands re-exports, aliases, and type relationships.
42
42
  ## Install for AI Agents
43
43
 
44
44
  ```bash
45
- curl -fsSL https://raw.githubusercontent.com/plaited/skills-installer/main/install.sh | bash -s -- --agents <agent-name> --project development-skills
45
+ npx skills add plaited/development-skills
46
+ # or
47
+ bunx skills add plaited/development-skills
46
48
  ```
47
49
 
48
- **Agents:** `claude` · `cursor` · `copilot` · `codex` · `gemini` · `windsurf` · `opencode` · `amp` · `goose` · `factory`
49
-
50
50
  ## Commands
51
51
 
52
52
  | Command | What it does |
package/bin/cli.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  * bunx @plaited/development-skills lsp-hover src/index.ts 10 5
21
21
  * bunx @plaited/development-skills lsp-find parseConfig
22
22
  * bunx @plaited/development-skills validate-skill .claude/skills/my-skill
23
- * bunx @plaited/development-skills scaffold-rules --agent=claude --format=json
23
+ * bunx @plaited/development-skills scaffold-rules
24
24
  */
25
25
 
26
26
  import { lspAnalyze } from '../src/lsp-analyze.ts'
@@ -64,7 +64,7 @@ Examples:
64
64
  bunx @plaited/development-skills lsp-symbols src/app.ts
65
65
  bunx @plaited/development-skills lsp-analyze src/app.ts
66
66
  bunx @plaited/development-skills validate-skill .claude/skills/my-skill
67
- bunx @plaited/development-skills scaffold-rules --agent=claude --format=json
67
+ bunx @plaited/development-skills scaffold-rules
68
68
 
69
69
  Options:
70
70
  -h, --help Show this help
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plaited/development-skills",
3
- "version": "0.6.5",
3
+ "version": "0.8.0",
4
4
  "description": "Development skills for Claude Code - TypeScript LSP, code documentation, and validation tools",
5
5
  "license": "ISC",
6
6
  "engines": {
@@ -21,7 +21,8 @@
21
21
  "files": [
22
22
  "bin/",
23
23
  "src/",
24
- ".plaited/"
24
+ "rules/",
25
+ "skills/"
25
26
  ],
26
27
  "publishConfig": {
27
28
  "access": "public"
@@ -48,7 +49,6 @@
48
49
  },
49
50
  "devDependencies": {
50
51
  "@biomejs/biome": "2.3.11",
51
- "@modelcontextprotocol/sdk": "1.22.0",
52
52
  "@types/bun": "1.3.6",
53
53
  "format-package": "7.0.0",
54
54
  "lint-staged": "16.2.7",
@@ -28,4 +28,4 @@
28
28
  - Code review: Read files before commenting
29
29
  - Patterns: Confirm examples reflect actual usage
30
30
 
31
- See .plaited/rules/testing.md for verification in test contexts.
31
+ See rules/testing.md for verification in test contexts.
@@ -9,11 +9,16 @@
9
9
  *Fix:* Add `.ts` extension
10
10
 
11
11
  **Re-export at boundaries** - Parent `feature.ts` re-exports from `feature/feature.ts`
12
- ```
13
- src/
14
- ├── acp/ # Feature module
15
- │ └── acp.ts # Implementation
16
- └── acp.ts # Re-exports public API
12
+
13
+ ```mermaid
14
+ graph TD
15
+ A[src/] --> B[feature/]
16
+ A --> C[feature.ts]
17
+ B --> D[feature.ts]
18
+ B --> E[tests/]
19
+ E --> F[feature.spec.ts]
20
+
21
+ C -.Re-exports.-> D
17
22
  ```
18
23
 
19
24
  **File organization within modules:**
@@ -0,0 +1,26 @@
1
+ # Skill Activation
2
+
3
+ **Evaluate before implementing** - Check available skills for relevance before starting work
4
+
5
+ **Activation sequence:**
6
+
7
+ 1. **Evaluate** - For each skill in `<available_skills>`, assess: `[skill-name] - YES/NO - [reason]`
8
+ 2. **Activate** - Call `Skill(skill-name)` for each relevant skill before proceeding
9
+ 3. **Implement** - Begin work only after activation is complete
10
+
11
+ *Verify:* Did you check available skills before starting implementation?
12
+ *Fix:* Pause, evaluate skills, activate relevant ones, then continue
13
+
14
+ **Example:**
15
+ ```
16
+ - code-patterns: NO - not writing code
17
+ - git-workflow: YES - need commit conventions
18
+ - documentation: YES - writing README
19
+
20
+ > Skill(git-workflow)
21
+ > Skill(documentation)
22
+ ```
23
+
24
+ **Activation before implementation** - Evaluating skills without calling `Skill()` provides no benefit
25
+ *Verify:* Check that `Skill()` was called for each YES evaluation
26
+ *Fix:* Call `Skill(skill-name)` for skipped activations
@@ -14,7 +14,7 @@
14
14
 
15
15
  **Use real dependencies** - Prefer installed packages over mocks when testing module resolution
16
16
  *Verify:* Review test imports for fake paths
17
- *Fix:* Use actual package like `@modelcontextprotocol/sdk/client`
17
+ *Fix:* Use actual package like `typescript`
18
18
 
19
19
  **Organize with describe** - Group related tests in `describe('feature', () => {...})`
20
20
  *Verify:* Check for flat test structure
@@ -34,9 +34,8 @@ bunx @plaited/development-skills scaffold-rules
34
34
  ```
35
35
 
36
36
  This will:
37
- 1. Copy rules to `.plaited/rules/` (canonical location)
38
- 2. Create symlinks in `.claude/rules` and `.cursor/rules` (if those directories exist)
39
- 3. Fallback: append links to `AGENTS.md` if no agent directories found
37
+ 1. Write rules into `AGENTS.md` (creates if missing, updates between markers if present)
38
+ 2. Add `@AGENTS.md` reference to `CLAUDE.md` if it exists without one
40
39
 
41
40
  ### Step 3: Report to User
42
41
 
@@ -51,25 +50,32 @@ Tell the user what was created based on the `actions` output.
51
50
 
52
51
  ## How It Works
53
52
 
53
+ Rules are written directly into `AGENTS.md` between markers:
54
+
54
55
  ```
55
- .plaited/rules/ ← Canonical location (files copied here)
56
- ├── testing.md
57
- ├── bun.md
58
- └── ...
56
+ <!-- PLAITED-RULES-START -->
57
+
58
+ ## Rules
59
+
60
+ (rule content inlined here)
59
61
 
60
- .claude/rules -> ../.plaited/rules ← Symlink (if .claude/ exists)
61
- .cursor/rules -> ../.plaited/rules ← Symlink (if .cursor/ exists)
62
+ <!-- PLAITED-RULES-END -->
62
63
  ```
63
64
 
64
- | Project has... | Copy | Symlinks | AGENTS.md |
65
- |----------------|------|----------|-----------|
66
- | `.plaited/` only | | None | No |
67
- | `.claude/` only | | `.claude/rules` | No |
68
- | `.cursor/` only | ✓ | `.cursor/rules` | No |
69
- | `.plaited/` + `.claude/` | ✓ | `.claude/rules` | No |
70
- | `.plaited/` + `.cursor/` | ✓ | `.cursor/rules` | No |
71
- | `.plaited/` + `.claude/` + `.cursor/` | | Both | No |
72
- | None of the above | ✓ | None | ✓ Append links |
65
+ - **No AGENTS.md**: Creates one with rules section
66
+ - **AGENTS.md without markers**: Appends rules section with markers
67
+ - **AGENTS.md with markers**: Replaces content between markers (preserves user content outside)
68
+ - **CLAUDE.md exists**: Adds `@AGENTS.md` reference if not already present
69
+
70
+ ## Troubleshooting
71
+
72
+ | Issue | Cause | Fix |
73
+ |-------|-------|-----|
74
+ | Rules duplicated in AGENTS.md | Markers were manually deleted | Remove duplicate section, re-run `scaffold-rules` |
75
+ | Update didn't apply | Only one marker present (start or end) | Ensure both `<!-- PLAITED-RULES-START -->` and `<!-- PLAITED-RULES-END -->` exist, or delete both to get a fresh append |
76
+ | `@AGENTS.md` not added to CLAUDE.md | CLAUDE.md doesn't exist | Create CLAUDE.md first, then re-run |
77
+
78
+ **Do not** manually edit content between the `PLAITED-RULES-START` and `PLAITED-RULES-END` markers — it will be overwritten on next run.
73
79
 
74
80
  ## Related Skills
75
81
 
@@ -1,46 +1,20 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Scaffold development rules - Copy bundled rules and create symlinks
3
+ * Scaffold development rules into AGENTS.md
4
4
  *
5
- * Copies rules from the package to `.plaited/rules/` (canonical location),
6
- * creates symlinks for `.claude/` and `.cursor/` agent directories,
7
- * and falls back to appending links in `AGENTS.md` if no agent dirs exist.
5
+ * Writes rules into AGENTS.md (creates if missing, uses markers for updates).
6
+ * Adds `@AGENTS.md` reference to CLAUDE.md if it exists without one.
8
7
  *
9
8
  * @throws When source rules directory cannot be read
10
- * @throws When target directory cannot be created (permissions)
11
- * @throws When symlink creation fails (existing file, not directory)
12
9
  */
13
10
 
14
- import { mkdir, readdir, readlink, stat, symlink } from 'node:fs/promises'
11
+ import { readdir } from 'node:fs/promises'
15
12
  import { join } from 'node:path'
16
13
  import { parseArgs } from 'node:util'
17
14
 
18
- /** Agents that get symlinks to .plaited/rules (not .plaited itself) */
19
- const SYMLINK_AGENTS = ['.claude', '.cursor'] as const
20
-
21
- /** All supported agent directories (including .plaited which gets direct copy) */
22
- const ALL_AGENTS = ['.plaited', ...SYMLINK_AGENTS] as const
23
-
24
- /** Canonical rules location */
25
- const TARGET_RULES = '.plaited/rules' as const
26
-
27
- /**
28
- * NOTE: This tool only scaffolds RULES, not skills.
29
- * Skills symlinks (.claude/skills -> ../.plaited/skills) are managed separately
30
- * via the skills-installer or manual setup.
31
- */
32
-
33
- /**
34
- * Check if path is a directory
35
- */
36
- const isDirectory = async (path: string): Promise<boolean> => {
37
- try {
38
- const s = await stat(path)
39
- return s.isDirectory()
40
- } catch {
41
- return false
42
- }
43
- }
15
+ /** Markers for the rules section in AGENTS.md */
16
+ const RULES_START = '<!-- PLAITED-RULES-START -->'
17
+ const RULES_END = '<!-- PLAITED-RULES-END -->'
44
18
 
45
19
  /**
46
20
  * Main scaffold-rules function
@@ -59,7 +33,7 @@ export const scaffoldRules = async (args: string[]): Promise<void> => {
59
33
  const dryRun = values['dry-run'] as boolean | undefined
60
34
  const listOnly = values.list as boolean | undefined
61
35
 
62
- const sourceRules = join(import.meta.dir, '../.plaited/rules')
36
+ const sourceRules = join(import.meta.dir, '../rules')
63
37
  const cwd = process.cwd()
64
38
 
65
39
  // Get available rules
@@ -74,77 +48,60 @@ export const scaffoldRules = async (args: string[]): Promise<void> => {
74
48
 
75
49
  const actions: string[] = []
76
50
 
77
- // Check for agent directories BEFORE copying (since copy creates .plaited/)
78
- // This determines whether to fall back to AGENTS.md append
79
- let hadAgentDirBeforeScaffold = false
80
- for (const agent of ALL_AGENTS) {
81
- if (await isDirectory(join(cwd, agent))) {
82
- hadAgentDirBeforeScaffold = true
83
- break
84
- }
85
- }
86
-
87
- // 1. Copy rules to .plaited/rules/ (canonical location, serves .plaited agent)
88
- const targetDir = join(cwd, TARGET_RULES)
89
- if (!dryRun) {
90
- await mkdir(targetDir, { recursive: true })
91
- }
51
+ // 1. Write rules into AGENTS.md
52
+ const agentsMdPath = join(cwd, 'AGENTS.md')
53
+ const agentsMd = Bun.file(agentsMdPath)
92
54
 
55
+ const ruleEntries: string[] = []
93
56
  for (const file of rules) {
94
- const src = join(sourceRules, file)
95
- const dest = join(targetDir, file)
96
- if (!dryRun) {
97
- await Bun.write(dest, await Bun.file(src).text())
98
- }
99
- actions.push(`copy: ${TARGET_RULES}/${file}`)
57
+ const content = await Bun.file(join(sourceRules, file)).text()
58
+ ruleEntries.push(content)
100
59
  }
60
+ const rulesContent = ruleEntries.join('\n\n')
61
+ const rulesSection = `${RULES_START}\n\n## Rules\n\n${rulesContent}\n\n${RULES_END}`
101
62
 
102
- // 2. Symlink for other agents (.claude, .cursor)
103
- for (const agent of SYMLINK_AGENTS) {
104
- const agentDir = join(cwd, agent)
105
- if (await isDirectory(agentDir)) {
106
- const rulesLink = join(agentDir, 'rules')
107
-
108
- // Check if symlink already exists and points to right place
109
- try {
110
- const existing = await readlink(rulesLink)
111
- if (existing === '../.plaited/rules') {
112
- actions.push(`skip: ${agent}/rules (symlink exists)`)
113
- continue
114
- }
115
- } catch {
116
- // Doesn't exist or not a symlink - proceed to create
117
- }
63
+ if (await agentsMd.exists()) {
64
+ const content = await agentsMd.text()
65
+ const startIdx = content.indexOf(RULES_START)
66
+ const endIdx = content.indexOf(RULES_END)
118
67
 
68
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
69
+ const before = content.slice(0, startIdx)
70
+ const after = content.slice(endIdx + RULES_END.length)
119
71
  if (!dryRun) {
120
- await symlink('../.plaited/rules', rulesLink)
72
+ await Bun.write(agentsMdPath, `${before}${rulesSection}${after}`)
121
73
  }
122
- actions.push(`symlink: ${agent}/rules -> ../.plaited/rules`)
74
+ actions.push('update: AGENTS.md (rules section)')
75
+ } else {
76
+ if (!dryRun) {
77
+ await Bun.write(agentsMdPath, `${content}\n${rulesSection}\n`)
78
+ }
79
+ actions.push('append: AGENTS.md (rules section)')
80
+ }
81
+ } else {
82
+ if (!dryRun) {
83
+ await Bun.write(agentsMdPath, `# AGENTS\n\n${rulesSection}\n`)
123
84
  }
85
+ actions.push('create: AGENTS.md (rules section)')
124
86
  }
125
87
 
126
- // 3. Fallback: append to AGENTS.md only if NO agent directories existed before copy
127
- if (!hadAgentDirBeforeScaffold) {
128
- const agentsMdPath = join(cwd, 'AGENTS.md')
129
- const agentsMd = Bun.file(agentsMdPath)
130
-
131
- if (await agentsMd.exists()) {
132
- const content = await agentsMd.text()
133
- if (content.includes('.plaited/rules')) {
134
- actions.push('skip: AGENTS.md (already has rules)')
135
- } else {
136
- const links = rules.map((f) => `- [${f.replace('.md', '')}](${TARGET_RULES}/${f})`).join('\n')
137
- const section = `\n## Rules\n\n${links}\n`
138
-
139
- if (!dryRun) {
140
- await Bun.write(agentsMdPath, content + section)
141
- }
142
- actions.push('append: AGENTS.md (rules section)')
88
+ // 2. Add @AGENTS.md reference to CLAUDE.md if it exists without one
89
+ const claudeMdPath = join(cwd, 'CLAUDE.md')
90
+ const claudeMd = Bun.file(claudeMdPath)
91
+
92
+ if (await claudeMd.exists()) {
93
+ const content = await claudeMd.text()
94
+ if (/^@AGENTS\.md/m.test(content)) {
95
+ actions.push('skip: CLAUDE.md (already references @AGENTS.md)')
96
+ } else {
97
+ if (!dryRun) {
98
+ await Bun.write(claudeMdPath, `@AGENTS.md\n\n${content}`)
143
99
  }
100
+ actions.push('update: CLAUDE.md (added @AGENTS.md reference)')
144
101
  }
145
102
  }
146
103
 
147
- console.log(JSON.stringify({ dryRun: !!dryRun, targetRules: TARGET_RULES, actions }, null, 2))
104
+ console.log(JSON.stringify({ dryRun: !!dryRun, actions }, null, 2))
148
105
  }
149
106
 
150
107
  // CLI entry point
@@ -75,27 +75,6 @@ describe('resolveFilePath', () => {
75
75
  })
76
76
 
77
77
  describe('scoped package specifiers', () => {
78
- test('resolves scoped package subpath export', () => {
79
- // @modelcontextprotocol/sdk/client is a defined export
80
- const result = resolveFilePath('@modelcontextprotocol/sdk/client')
81
- expect(result).toContain('node_modules/@modelcontextprotocol/sdk')
82
- expect(result).toContain('client')
83
- })
84
-
85
- test('resolves scoped package server subpath', () => {
86
- const result = resolveFilePath('@modelcontextprotocol/sdk/server')
87
- expect(result).toContain('node_modules/@modelcontextprotocol/sdk')
88
- expect(result).toContain('server')
89
- })
90
-
91
- test('resolves scoped package deep subpath with extension', () => {
92
- // Deep subpath with .js extension via wildcard export "./*"
93
- const path = '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js'
94
- const result = resolveFilePath(path)
95
- expect(result).toContain('node_modules/@modelcontextprotocol/sdk')
96
- expect(result).toContain('bearerAuth')
97
- })
98
-
99
78
  test('falls back to cwd for non-existent scoped package', () => {
100
79
  const scopedPkg = '@nonexistent/pkg/src/file.ts'
101
80
  const result = resolveFilePath(scopedPkg)
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
- import { mkdir, readlink, rm } from 'node:fs/promises'
2
+ import { mkdir, rm } from 'node:fs/promises'
3
3
  import { join } from 'node:path'
4
4
  import { $ } from 'bun'
5
5
 
@@ -9,7 +9,6 @@ type ListOutput = {
9
9
 
10
10
  type ScaffoldOutput = {
11
11
  dryRun: boolean
12
- targetRules: string
13
12
  actions: string[]
14
13
  }
15
14
 
@@ -19,13 +18,11 @@ describe('scaffold-rules', () => {
19
18
  let testDir: string
20
19
 
21
20
  beforeEach(async () => {
22
- // Create a temp directory for each test
23
21
  testDir = join(import.meta.dir, `test-scaffold-${Date.now()}`)
24
22
  await mkdir(testDir, { recursive: true })
25
23
  })
26
24
 
27
25
  afterEach(async () => {
28
- // Clean up temp directory
29
26
  await rm(testDir, { recursive: true, force: true })
30
27
  })
31
28
 
@@ -63,26 +60,14 @@ describe('scaffold-rules', () => {
63
60
  const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules --dry-run`.json()
64
61
 
65
62
  expect(result.dryRun).toBe(true)
66
- expect(result.targetRules).toBe('.plaited/rules')
67
63
  expect(result.actions).toBeArray()
68
- })
69
-
70
- test('shows copy actions for each rule', async () => {
71
- const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules --dry-run`.json()
72
-
73
- const copyActions = result.actions.filter((a) => a.startsWith('copy:'))
74
- expect(copyActions.length).toBeGreaterThan(0)
75
-
76
- // Should include our compressed rules
77
- expect(copyActions.some((a) => a.includes('core.md'))).toBe(true)
78
- expect(copyActions.some((a) => a.includes('testing.md'))).toBe(true)
64
+ expect(result.actions).toContain('create: AGENTS.md (rules section)')
79
65
  })
80
66
 
81
67
  test('does not create files in dry-run mode', async () => {
82
68
  await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules --dry-run`.json()
83
69
 
84
- const rulesDir = Bun.file(join(testDir, '.plaited/rules'))
85
- expect(await rulesDir.exists()).toBe(false)
70
+ expect(await Bun.file(join(testDir, 'AGENTS.md')).exists()).toBe(false)
86
71
  })
87
72
 
88
73
  test('short flag -n works', async () => {
@@ -92,109 +77,114 @@ describe('scaffold-rules', () => {
92
77
  })
93
78
  })
94
79
 
95
- describe('copy behavior', () => {
96
- test('copies rules to .plaited/rules/', async () => {
80
+ describe('AGENTS.md behavior', () => {
81
+ test('creates AGENTS.md if it does not exist', async () => {
97
82
  await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
98
83
 
99
- const coreRule = Bun.file(join(testDir, '.plaited/rules/core.md'))
100
- expect(await coreRule.exists()).toBe(true)
101
-
102
- const content = await coreRule.text()
84
+ const content = await Bun.file(join(testDir, 'AGENTS.md')).text()
85
+ expect(content).toContain('# AGENTS')
86
+ expect(content).toContain('## Rules')
87
+ expect(content).toContain('<!-- PLAITED-RULES-START -->')
88
+ expect(content).toContain('<!-- PLAITED-RULES-END -->')
103
89
  expect(content).toContain('# Core Conventions')
104
90
  })
105
91
 
106
- test('copies all compressed rules', async () => {
107
- await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
108
-
109
- const expectedRules = ['accuracy', 'bun', 'core', 'documentation', 'modules', 'testing', 'workflow']
110
-
111
- for (const rule of expectedRules) {
112
- const ruleFile = Bun.file(join(testDir, `.plaited/rules/${rule}.md`))
113
- expect(await ruleFile.exists()).toBe(true)
114
- }
115
- })
92
+ test('appends rules with markers to existing AGENTS.md', async () => {
93
+ await Bun.write(join(testDir, 'AGENTS.md'), '# My Project\n\nCustom content\n')
116
94
 
117
- test('creates .plaited/rules directory if missing', async () => {
118
95
  await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
119
96
 
120
- const rulesDir = await Bun.file(join(testDir, '.plaited/rules/core.md')).exists()
121
- expect(rulesDir).toBe(true)
97
+ const content = await Bun.file(join(testDir, 'AGENTS.md')).text()
98
+ expect(content).toStartWith('# My Project\n\nCustom content\n')
99
+ expect(content).toContain('<!-- PLAITED-RULES-START -->')
100
+ expect(content).toContain('## Rules')
101
+ expect(content).toContain('<!-- PLAITED-RULES-END -->')
122
102
  })
123
- })
124
103
 
125
- describe('symlink behavior', () => {
126
- test('creates symlink for .claude/rules when .claude exists', async () => {
127
- // Create .claude directory
128
- await mkdir(join(testDir, '.claude'), { recursive: true })
104
+ test('updates existing markers without overwriting user content', async () => {
105
+ const initial =
106
+ '# Project\n\nUser notes\n\n<!-- PLAITED-RULES-START -->\n\n## Rules\n\n- old rule\n\n<!-- PLAITED-RULES-END -->\n\nMore user content\n'
107
+ await Bun.write(join(testDir, 'AGENTS.md'), initial)
129
108
 
130
- await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
109
+ const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json()
131
110
 
132
- // Check symlink exists and points to right place
133
- const linkTarget = await readlink(join(testDir, '.claude/rules'))
134
- expect(linkTarget).toBe('../.plaited/rules')
111
+ const content = await Bun.file(join(testDir, 'AGENTS.md')).text()
112
+ expect(content).toContain('User notes')
113
+ expect(content).toContain('More user content')
114
+ expect(content).not.toContain('old rule')
115
+ expect(content).toContain('# Core Conventions')
116
+ expect(result.actions).toContain('update: AGENTS.md (rules section)')
135
117
  })
136
118
 
137
- test('creates symlink for .cursor/rules when .cursor exists', async () => {
138
- // Create .cursor directory
139
- await mkdir(join(testDir, '.cursor'), { recursive: true })
119
+ test('appends when only start marker exists (no end marker)', async () => {
120
+ const malformed = '# Project\n\n<!-- PLAITED-RULES-START -->\n\nOrphan content\n'
121
+ await Bun.write(join(testDir, 'AGENTS.md'), malformed)
140
122
 
141
- await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
123
+ const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json()
142
124
 
143
- const linkTarget = await readlink(join(testDir, '.cursor/rules'))
144
- expect(linkTarget).toBe('../.plaited/rules')
125
+ const content = await Bun.file(join(testDir, 'AGENTS.md')).text()
126
+ expect(content).toContain('# Core Conventions')
127
+ expect(result.actions).toContain('append: AGENTS.md (rules section)')
145
128
  })
146
129
 
147
- test('skips symlink if already exists with correct target', async () => {
148
- await mkdir(join(testDir, '.claude'), { recursive: true })
130
+ test('appends when markers are reversed (end before start)', async () => {
131
+ const reversed =
132
+ '# Project\n\n<!-- PLAITED-RULES-END -->\n\nMiddle\n\n<!-- PLAITED-RULES-START -->\n\nOld rules\n'
133
+ await Bun.write(join(testDir, 'AGENTS.md'), reversed)
149
134
 
150
- // Run twice
151
- await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
152
135
  const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json()
153
136
 
154
- // Second run should skip the symlink
155
- const skipAction = result.actions.find((a) => a.includes('.claude/rules') && a.includes('skip'))
156
- expect(skipAction).toBeDefined()
137
+ const content = await Bun.file(join(testDir, 'AGENTS.md')).text()
138
+ expect(content).toContain('# Core Conventions')
139
+ expect(result.actions).toContain('append: AGENTS.md (rules section)')
157
140
  })
158
141
 
159
- test('does not create symlink if agent dir does not exist', async () => {
142
+ test('appends when only end marker exists (no start marker)', async () => {
143
+ const malformed = '# Project\n\nSome content\n\n<!-- PLAITED-RULES-END -->\n'
144
+ await Bun.write(join(testDir, 'AGENTS.md'), malformed)
145
+
160
146
  const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json()
161
147
 
162
- // Should not have any symlink actions
163
- const symlinkActions = result.actions.filter((a) => a.startsWith('symlink:'))
164
- expect(symlinkActions.length).toBe(0)
148
+ const content = await Bun.file(join(testDir, 'AGENTS.md')).text()
149
+ expect(content).toContain('# Core Conventions')
150
+ expect(result.actions).toContain('append: AGENTS.md (rules section)')
165
151
  })
166
152
  })
167
153
 
168
- describe('AGENTS.md fallback', () => {
169
- test('appends rules to AGENTS.md when no agent dirs exist', async () => {
170
- // Create AGENTS.md without any agent directories
171
- await Bun.write(join(testDir, 'AGENTS.md'), '# AGENTS\n\nSome content\n')
154
+ describe('CLAUDE.md behavior', () => {
155
+ test('adds @AGENTS.md reference to existing CLAUDE.md', async () => {
156
+ await Bun.write(join(testDir, 'CLAUDE.md'), '# Claude Config\n\nSome settings\n')
172
157
 
173
158
  await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
174
159
 
175
- const content = await Bun.file(join(testDir, 'AGENTS.md')).text()
176
- expect(content).toContain('## Rules')
177
- expect(content).toContain('.plaited/rules/')
160
+ const content = await Bun.file(join(testDir, 'CLAUDE.md')).text()
161
+ expect(content).toStartWith('@AGENTS.md\n\n')
162
+ expect(content).toContain('# Claude Config')
178
163
  })
179
164
 
180
- test('does not append if agent dir exists', async () => {
181
- await Bun.write(join(testDir, 'AGENTS.md'), '# AGENTS\n\nSome content\n')
182
- await mkdir(join(testDir, '.plaited'), { recursive: true })
165
+ test('skips CLAUDE.md if @AGENTS.md reference already exists', async () => {
166
+ await Bun.write(join(testDir, 'CLAUDE.md'), '@AGENTS.md\n\n# Claude Config\n')
183
167
 
184
- await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
168
+ const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json()
185
169
 
186
- const content = await Bun.file(join(testDir, 'AGENTS.md')).text()
187
- // Should not have appended rules section (since .plaited exists)
188
- expect(content).not.toContain('## Rules')
170
+ const skipAction = result.actions.find((a) => a.includes('CLAUDE.md') && a.includes('skip'))
171
+ expect(skipAction).toBeDefined()
189
172
  })
190
173
 
191
- test('skips if AGENTS.md already has rules', async () => {
192
- await Bun.write(join(testDir, 'AGENTS.md'), '# AGENTS\n\nSee .plaited/rules/ for rules\n')
174
+ test('adds reference when @AGENTS.md only appears inline (not at start of line)', async () => {
175
+ await Bun.write(join(testDir, 'CLAUDE.md'), '# Config\n\nSee `@AGENTS.md` for details\n')
193
176
 
194
177
  const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json()
195
178
 
196
- const skipAction = result.actions.find((a) => a.includes('AGENTS.md') && a.includes('skip'))
197
- expect(skipAction).toBeDefined()
179
+ const content = await Bun.file(join(testDir, 'CLAUDE.md')).text()
180
+ expect(content).toStartWith('@AGENTS.md\n\n')
181
+ expect(result.actions).toContain('update: CLAUDE.md (added @AGENTS.md reference)')
182
+ })
183
+
184
+ test('does not create CLAUDE.md if it does not exist', async () => {
185
+ await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
186
+
187
+ expect(await Bun.file(join(testDir, 'CLAUDE.md')).exists()).toBe(false)
198
188
  })
199
189
  })
200
190
 
@@ -203,60 +193,25 @@ describe('scaffold-rules', () => {
203
193
  const result: ScaffoldOutput = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.json()
204
194
 
205
195
  expect(result).toHaveProperty('dryRun')
206
- expect(result).toHaveProperty('targetRules')
207
196
  expect(result).toHaveProperty('actions')
208
-
209
197
  expect(result.dryRun).toBe(false)
210
- expect(result.targetRules).toBe('.plaited/rules')
211
198
  expect(result.actions).toBeArray()
212
199
  })
213
200
  })
214
201
 
215
- describe('symlink readability', () => {
216
- test('rules are readable through .claude symlink', async () => {
217
- await mkdir(join(testDir, '.claude'), { recursive: true })
218
- await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
219
-
220
- // Read through symlink path
221
- const content = await Bun.file(join(testDir, '.claude/rules/core.md')).text()
222
- expect(content).toContain('# Core Conventions')
223
- })
224
-
225
- test('rules are readable through .cursor symlink', async () => {
226
- await mkdir(join(testDir, '.cursor'), { recursive: true })
227
- await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
228
-
229
- // Read through symlink path
230
- const content = await Bun.file(join(testDir, '.cursor/rules/core.md')).text()
231
- expect(content).toContain('# Core Conventions')
232
- })
233
- })
234
-
235
- describe('error handling', () => {
236
- test('fails when existing file blocks symlink creation', async () => {
237
- await mkdir(join(testDir, '.claude'), { recursive: true })
238
- // Create a file where symlink should go
239
- await Bun.write(join(testDir, '.claude/rules'), 'not a directory')
240
-
241
- // Should fail when trying to create symlink over existing file
242
- const result = await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules 2>&1`.nothrow().text()
243
- expect(result).toContain('EEXIST')
244
- })
245
- })
246
-
247
202
  describe('rule content', () => {
248
- test('core.md contains TypeScript conventions', async () => {
203
+ test('AGENTS.md contains TypeScript conventions', async () => {
249
204
  await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
250
205
 
251
- const content = await Bun.file(join(testDir, '.plaited/rules/core.md')).text()
206
+ const content = await Bun.file(join(testDir, 'AGENTS.md')).text()
252
207
  expect(content).toContain('Type over interface')
253
208
  expect(content).toContain('Arrow functions')
254
209
  })
255
210
 
256
- test('testing.md contains test conventions', async () => {
211
+ test('AGENTS.md contains test conventions', async () => {
257
212
  await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
258
213
 
259
- const content = await Bun.file(join(testDir, '.plaited/rules/testing.md')).text()
214
+ const content = await Bun.file(join(testDir, 'AGENTS.md')).text()
260
215
  expect(content).toContain('test not it')
261
216
  expect(content).toContain('No conditional assertions')
262
217
  })
@@ -264,7 +219,7 @@ describe('scaffold-rules', () => {
264
219
  test('rules include verification patterns', async () => {
265
220
  await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
266
221
 
267
- const content = await Bun.file(join(testDir, '.plaited/rules/core.md')).text()
222
+ const content = await Bun.file(join(testDir, 'AGENTS.md')).text()
268
223
  expect(content).toContain('*Verify:*')
269
224
  expect(content).toContain('*Fix:*')
270
225
  })
@@ -272,9 +227,7 @@ describe('scaffold-rules', () => {
272
227
  test('rules are compressed (no verbose examples)', async () => {
273
228
  await $`cd ${testDir} && bun ${binDir}/cli.ts scaffold-rules`.quiet()
274
229
 
275
- const content = await Bun.file(join(testDir, '.plaited/rules/core.md')).text()
276
-
277
- // Should not have verbose code blocks with Good/Avoid patterns
230
+ const content = await Bun.file(join(testDir, 'AGENTS.md')).text()
278
231
  expect(content).not.toContain('// ✅ Good')
279
232
  expect(content).not.toContain('// ❌ Avoid')
280
233
  })
File without changes
File without changes
File without changes
File without changes