Skip to content

Commit 8091d1a

Browse files
committed
sdk: Fall back to .AGENTS.md or CLAUDE.md
1 parent 650598d commit 8091d1a

File tree

2 files changed

+259
-9
lines changed

2 files changed

+259
-9
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { describe, expect, test } from 'bun:test'
2+
3+
import { selectKnowledgeFilePaths } from '../run-state'
4+
5+
describe('selectKnowledgeFilePaths', () => {
6+
test('selects knowledge.md when it exists alone', () => {
7+
const files = ['src/knowledge.md', 'lib/utils.ts']
8+
const result = selectKnowledgeFilePaths(files)
9+
10+
expect(result).toEqual(['src/knowledge.md'])
11+
})
12+
13+
test('selects AGENTS.md when knowledge.md does not exist', () => {
14+
const files = ['src/AGENTS.md', 'lib/utils.ts']
15+
const result = selectKnowledgeFilePaths(files)
16+
17+
expect(result).toEqual(['src/AGENTS.md'])
18+
})
19+
20+
test('selects CLAUDE.md when neither knowledge.md nor AGENTS.md exist', () => {
21+
const files = ['src/CLAUDE.md', 'lib/utils.ts']
22+
const result = selectKnowledgeFilePaths(files)
23+
24+
expect(result).toEqual(['src/CLAUDE.md'])
25+
})
26+
27+
test('prefers knowledge.md over AGENTS.md when both exist in same directory', () => {
28+
const files = ['src/knowledge.md', 'src/AGENTS.md', 'lib/utils.ts']
29+
const result = selectKnowledgeFilePaths(files)
30+
31+
expect(result).toEqual(['src/knowledge.md'])
32+
})
33+
34+
test('prefers knowledge.md over CLAUDE.md when both exist in same directory', () => {
35+
const files = ['src/knowledge.md', 'src/CLAUDE.md', 'lib/utils.ts']
36+
const result = selectKnowledgeFilePaths(files)
37+
38+
expect(result).toEqual(['src/knowledge.md'])
39+
})
40+
41+
test('prefers AGENTS.md over CLAUDE.md when both exist in same directory', () => {
42+
const files = ['src/AGENTS.md', 'src/CLAUDE.md', 'lib/utils.ts']
43+
const result = selectKnowledgeFilePaths(files)
44+
45+
expect(result).toEqual(['src/AGENTS.md'])
46+
})
47+
48+
test('prefers knowledge.md when all three exist in same directory', () => {
49+
const files = [
50+
'src/knowledge.md',
51+
'src/AGENTS.md',
52+
'src/CLAUDE.md',
53+
'lib/utils.ts',
54+
]
55+
const result = selectKnowledgeFilePaths(files)
56+
57+
expect(result).toEqual(['src/knowledge.md'])
58+
})
59+
60+
test('handles case-insensitive matching for knowledge.md', () => {
61+
const files = ['src/Knowledge.md', 'lib/KNOWLEDGE.MD', 'root/knowledge.MD']
62+
const result = selectKnowledgeFilePaths(files)
63+
64+
expect(result).toHaveLength(3)
65+
expect(result).toContain('src/Knowledge.md')
66+
expect(result).toContain('lib/KNOWLEDGE.MD')
67+
expect(result).toContain('root/knowledge.MD')
68+
})
69+
70+
test('handles case-insensitive matching for AGENTS.md', () => {
71+
const files = ['src/agents.md', 'lib/Agents.MD', 'root/AGENTS.md']
72+
const result = selectKnowledgeFilePaths(files)
73+
74+
expect(result).toHaveLength(3)
75+
expect(result).toContain('src/agents.md')
76+
expect(result).toContain('lib/Agents.MD')
77+
expect(result).toContain('root/AGENTS.md')
78+
})
79+
80+
test('handles case-insensitive matching for CLAUDE.md', () => {
81+
const files = ['src/claude.md', 'lib/Claude.MD', 'root/CLAUDE.md']
82+
const result = selectKnowledgeFilePaths(files)
83+
84+
expect(result).toHaveLength(3)
85+
expect(result).toContain('src/claude.md')
86+
expect(result).toContain('lib/Claude.MD')
87+
expect(result).toContain('root/CLAUDE.md')
88+
})
89+
90+
test('selects one knowledge file per directory when multiple directories have files', () => {
91+
const files = [
92+
'src/knowledge.md',
93+
'src/AGENTS.md',
94+
'lib/AGENTS.md',
95+
'lib/CLAUDE.md',
96+
'docs/CLAUDE.md',
97+
]
98+
const result = selectKnowledgeFilePaths(files)
99+
100+
expect(result).toHaveLength(3)
101+
expect(result).toContain('src/knowledge.md')
102+
expect(result).toContain('lib/AGENTS.md')
103+
expect(result).toContain('docs/CLAUDE.md')
104+
})
105+
106+
test('handles nested directory structures', () => {
107+
const files = [
108+
'src/components/knowledge.md',
109+
'src/components/AGENTS.md',
110+
'src/utils/AGENTS.md',
111+
'src/utils/CLAUDE.md',
112+
]
113+
const result = selectKnowledgeFilePaths(files)
114+
115+
expect(result).toHaveLength(2)
116+
expect(result).toContain('src/components/knowledge.md')
117+
expect(result).toContain('src/utils/AGENTS.md')
118+
})
119+
120+
test('returns empty array when no knowledge files exist', () => {
121+
const files = ['src/utils.ts', 'lib/helper.js', 'README.md']
122+
const result = selectKnowledgeFilePaths(files)
123+
124+
expect(result).toEqual([])
125+
})
126+
127+
test('handles root directory knowledge files', () => {
128+
const files = ['knowledge.md', 'AGENTS.md', 'CLAUDE.md']
129+
const result = selectKnowledgeFilePaths(files)
130+
131+
expect(result).toEqual(['knowledge.md'])
132+
})
133+
134+
test('handles deeply nested directory structures', () => {
135+
const files = [
136+
'a/b/c/d/knowledge.md',
137+
'a/b/c/d/AGENTS.md',
138+
'a/b/c/CLAUDE.md',
139+
'a/b/AGENTS.md',
140+
]
141+
const result = selectKnowledgeFilePaths(files)
142+
143+
expect(result).toHaveLength(3)
144+
expect(result).toContain('a/b/c/d/knowledge.md')
145+
expect(result).toContain('a/b/c/CLAUDE.md')
146+
expect(result).toContain('a/b/AGENTS.md')
147+
})
148+
149+
test('handles files with similar names but different extensions', () => {
150+
const files = [
151+
'src/knowledge.md',
152+
'src/knowledge.txt',
153+
'src/AGENTS.md',
154+
'src/agents.txt',
155+
]
156+
const result = selectKnowledgeFilePaths(files)
157+
158+
expect(result).toEqual(['src/knowledge.md'])
159+
})
160+
161+
test('handles empty file list', () => {
162+
const files: string[] = []
163+
const result = selectKnowledgeFilePaths(files)
164+
165+
expect(result).toEqual([])
166+
})
167+
168+
test('handles file paths with special characters', () => {
169+
const files = [
170+
'my-project/knowledge.md',
171+
'my_project/AGENTS.md',
172+
'my.project/CLAUDE.md',
173+
]
174+
const result = selectKnowledgeFilePaths(files)
175+
176+
expect(result).toHaveLength(3)
177+
expect(result).toContain('my-project/knowledge.md')
178+
expect(result).toContain('my_project/AGENTS.md')
179+
expect(result).toContain('my.project/CLAUDE.md')
180+
})
181+
182+
test('prioritizes correctly with all variations in same directory', () => {
183+
const files = [
184+
'dir/knowledge.md',
185+
'dir/Knowledge.MD',
186+
'dir/AGENTS.md',
187+
'dir/agents.MD',
188+
'dir/CLAUDE.md',
189+
'dir/claude.MD',
190+
]
191+
const result = selectKnowledgeFilePaths(files)
192+
193+
expect(result).toHaveLength(1)
194+
expect(result[0].toLowerCase()).toBe('dir/knowledge.md')
195+
})
196+
197+
test('handles paths correctly regardless of separator', () => {
198+
const files = [
199+
'src/components/knowledge.md',
200+
'src/components/AGENTS.md',
201+
'lib/CLAUDE.md',
202+
]
203+
const result = selectKnowledgeFilePaths(files)
204+
205+
expect(result).toHaveLength(2)
206+
expect(result).toContain('src/components/knowledge.md')
207+
expect(result).toContain('lib/CLAUDE.md')
208+
})
209+
})

sdk/src/run-state.ts

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,20 +160,61 @@ async function discoverProjectFiles(params: {
160160
}
161161

162162
/**
163-
* Auto-derives knowledge files from project files if knowledgeFiles is undefined
163+
* Selects knowledge files from a list of file paths with fallback logic.
164+
* For each directory, checks for knowledge.md first, then AGENTS.md, then CLAUDE.md.
165+
* @internal Exported for testing
166+
*/
167+
export function selectKnowledgeFilePaths(allFilePaths: string[]): string[] {
168+
const knowledgeCandidates = allFilePaths.filter((filePath) => {
169+
const lowercaseFilePath = filePath.toLowerCase()
170+
return (
171+
lowercaseFilePath.endsWith('knowledge.md') ||
172+
lowercaseFilePath.endsWith('agents.md') ||
173+
lowercaseFilePath.endsWith('claude.md')
174+
)
175+
})
176+
177+
// Group candidates by directory
178+
const byDirectory = new Map<string, string[]>()
179+
for (const filePath of knowledgeCandidates) {
180+
const dir = path.dirname(filePath)
181+
if (!byDirectory.has(dir)) {
182+
byDirectory.set(dir, [])
183+
}
184+
byDirectory.get(dir)!.push(filePath)
185+
}
186+
187+
const selectedFiles: string[] = []
188+
189+
// For each directory, select one knowledge file using fallback priority
190+
for (const [_dir, files] of byDirectory.entries()) {
191+
const knowledgeMd = files.find((f) => f.toLowerCase().endsWith('knowledge.md'))
192+
const agentsMd = files.find((f) => f.toLowerCase().endsWith('agents.md'))
193+
const claudeMd = files.find((f) => f.toLowerCase().endsWith('claude.md'))
194+
195+
// Priority: knowledge.md > AGENTS.md > CLAUDE.md
196+
const selectedKnowledgeFile = knowledgeMd || agentsMd || claudeMd
197+
if (selectedKnowledgeFile) {
198+
selectedFiles.push(selectedKnowledgeFile)
199+
}
200+
}
201+
202+
return selectedFiles
203+
}
204+
205+
/**
206+
* Auto-derives knowledge files from project files if knowledgeFiles is undefined.
207+
* Implements fallback priority: knowledge.md > AGENTS.md > CLAUDE.md per directory.
164208
*/
165209
function deriveKnowledgeFiles(
166210
projectFiles: Record<string, string>,
167211
): Record<string, string> {
212+
const allFilePaths = Object.keys(projectFiles)
213+
const selectedFilePaths = selectKnowledgeFilePaths(allFilePaths)
214+
168215
const knowledgeFiles: Record<string, string> = {}
169-
for (const [filePath, fileContents] of Object.entries(projectFiles)) {
170-
const lowercasePathName = filePath.toLowerCase()
171-
if (
172-
lowercasePathName.endsWith('knowledge.md') ||
173-
lowercasePathName.endsWith('claude.md')
174-
) {
175-
knowledgeFiles[filePath] = fileContents
176-
}
216+
for (const filePath of selectedFilePaths) {
217+
knowledgeFiles[filePath] = projectFiles[filePath]
177218
}
178219
return knowledgeFiles
179220
}

0 commit comments

Comments
 (0)