prjct-cli 0.59.0 → 0.60.1
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/CHANGELOG.md +92 -0
- package/assets/statusline/components/git.sh +32 -2
- package/assets/statusline/lib/theme.sh +8 -8
- package/assets/statusline/themes/default.json +2 -2
- package/core/__tests__/agentic/command-executor.test.ts +659 -0
- package/core/__tests__/services/hierarchical-agent-resolver.test.ts +359 -0
- package/core/__tests__/services/nested-context-resolver.test.ts +443 -0
- package/core/commands/analysis.ts +72 -64
- package/core/domain/agent-loader.ts +197 -3
- package/core/services/hierarchical-agent-resolver.ts +234 -0
- package/core/services/nested-context-resolver.ts +467 -3
- package/dist/bin/prjct.mjs +352 -56
- package/package.json +1 -1
- package/templates/commands/done.md +61 -13
- package/templates/commands/merge.md +11 -18
- package/templates/commands/ship.md +75 -42
- package/templates/commands/task.md +53 -19
- package/templates/global/CLAUDE.md +27 -0
- package/templates/global/STORAGE-SPEC.md +138 -3
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for HierarchicalAgentResolver
|
|
3
|
+
* PRJ-101: Hierarchical scope system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
7
|
+
import fs from 'node:fs/promises'
|
|
8
|
+
import os from 'node:os'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import HierarchicalAgentResolver from '../../services/hierarchical-agent-resolver'
|
|
11
|
+
|
|
12
|
+
let testDir: string
|
|
13
|
+
let resolver: HierarchicalAgentResolver
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
testDir = path.join(os.tmpdir(), `prjct-har-test-${Date.now()}`)
|
|
17
|
+
await fs.mkdir(testDir, { recursive: true })
|
|
18
|
+
resolver = new HierarchicalAgentResolver(testDir)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
try {
|
|
23
|
+
await fs.rm(testDir, { recursive: true, force: true })
|
|
24
|
+
} catch {
|
|
25
|
+
// Ignore cleanup errors
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Basic Resolution Tests
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
describe('HierarchicalAgentResolver - Basic', () => {
|
|
34
|
+
test('resolves agents from root AGENTS.md', async () => {
|
|
35
|
+
await fs.writeFile(
|
|
36
|
+
path.join(testDir, 'AGENTS.md'),
|
|
37
|
+
`## Backend
|
|
38
|
+
|
|
39
|
+
Backend development specialist.
|
|
40
|
+
|
|
41
|
+
### Triggers
|
|
42
|
+
- api
|
|
43
|
+
- endpoint
|
|
44
|
+
- server
|
|
45
|
+
|
|
46
|
+
### Rules
|
|
47
|
+
- Use async/await
|
|
48
|
+
- Validate inputs
|
|
49
|
+
`
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const result = await resolver.resolveAgentsForPath(testDir)
|
|
53
|
+
|
|
54
|
+
expect(result.agents).toHaveLength(1)
|
|
55
|
+
expect(result.agents[0].name).toBe('Backend')
|
|
56
|
+
expect(result.agents[0].description).toBe('Backend development specialist.')
|
|
57
|
+
expect(result.agents[0].triggers).toContain('api')
|
|
58
|
+
expect(result.agents[0].rules).toContain('Use async/await')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('returns empty result when no AGENTS.md exists', async () => {
|
|
62
|
+
const result = await resolver.resolveAgentsForPath(testDir)
|
|
63
|
+
|
|
64
|
+
expect(result.agents).toHaveLength(0)
|
|
65
|
+
expect(result.discoveredFiles).toHaveLength(0)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('tracks discovered files', async () => {
|
|
69
|
+
await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Agent\n\nTest.')
|
|
70
|
+
|
|
71
|
+
const result = await resolver.resolveAgentsForPath(testDir)
|
|
72
|
+
|
|
73
|
+
expect(result.discoveredFiles).toHaveLength(1)
|
|
74
|
+
expect(result.discoveredFiles[0]).toBe(path.join(testDir, 'AGENTS.md'))
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('resolveRootAgents uses root path', async () => {
|
|
78
|
+
await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Root\n\nRoot agent.')
|
|
79
|
+
|
|
80
|
+
const subDir = path.join(testDir, 'sub')
|
|
81
|
+
await fs.mkdir(subDir)
|
|
82
|
+
await fs.writeFile(path.join(subDir, 'AGENTS.md'), '## Sub\n\nSub agent.')
|
|
83
|
+
|
|
84
|
+
// resolveRootAgents should return root agents
|
|
85
|
+
const result = await resolver.resolveRootAgents()
|
|
86
|
+
|
|
87
|
+
// Root has only 1 agent (no inheritance from children)
|
|
88
|
+
expect(result.agents).toHaveLength(1)
|
|
89
|
+
expect(result.agents[0].name).toBe('Root')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// Get Agent By Name Tests
|
|
95
|
+
// =============================================================================
|
|
96
|
+
|
|
97
|
+
describe('HierarchicalAgentResolver - getAgentByName', () => {
|
|
98
|
+
test('finds agent by name', async () => {
|
|
99
|
+
await fs.writeFile(
|
|
100
|
+
path.join(testDir, 'AGENTS.md'),
|
|
101
|
+
`## Frontend
|
|
102
|
+
|
|
103
|
+
Frontend specialist.
|
|
104
|
+
|
|
105
|
+
## Backend
|
|
106
|
+
|
|
107
|
+
Backend specialist.
|
|
108
|
+
`
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const agent = await resolver.getAgentByName('Backend')
|
|
112
|
+
|
|
113
|
+
expect(agent).not.toBeNull()
|
|
114
|
+
expect(agent?.name).toBe('Backend')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('returns null for non-existent agent', async () => {
|
|
118
|
+
await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Frontend\n\nFrontend.')
|
|
119
|
+
|
|
120
|
+
const agent = await resolver.getAgentByName('NonExistent')
|
|
121
|
+
|
|
122
|
+
expect(agent).toBeNull()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('case-insensitive search', async () => {
|
|
126
|
+
await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Backend\n\nBackend.')
|
|
127
|
+
|
|
128
|
+
const agent = await resolver.getAgentByName('BACKEND')
|
|
129
|
+
|
|
130
|
+
expect(agent).not.toBeNull()
|
|
131
|
+
expect(agent?.name).toBe('Backend')
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// =============================================================================
|
|
136
|
+
// Get All Agent Names Tests
|
|
137
|
+
// =============================================================================
|
|
138
|
+
|
|
139
|
+
describe('HierarchicalAgentResolver - getAllAgentNames', () => {
|
|
140
|
+
test('returns all unique agent names', async () => {
|
|
141
|
+
await fs.writeFile(
|
|
142
|
+
path.join(testDir, 'AGENTS.md'),
|
|
143
|
+
`## Alpha
|
|
144
|
+
|
|
145
|
+
Alpha agent.
|
|
146
|
+
|
|
147
|
+
## Beta
|
|
148
|
+
|
|
149
|
+
Beta agent.
|
|
150
|
+
`
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const names = await resolver.getAllAgentNames()
|
|
154
|
+
|
|
155
|
+
expect(names).toEqual(['Alpha', 'Beta'])
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('includes agents from nested files', async () => {
|
|
159
|
+
await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Root\n\nRoot.')
|
|
160
|
+
|
|
161
|
+
const subDir = path.join(testDir, 'sub')
|
|
162
|
+
await fs.mkdir(subDir)
|
|
163
|
+
await fs.writeFile(path.join(subDir, 'AGENTS.md'), '## Sub\n\nSub.')
|
|
164
|
+
|
|
165
|
+
const names = await resolver.getAllAgentNames()
|
|
166
|
+
|
|
167
|
+
expect(names.sort()).toEqual(['Root', 'Sub'])
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('deduplicates agent names', async () => {
|
|
171
|
+
await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Shared\n\nRoot shared.')
|
|
172
|
+
|
|
173
|
+
const subDir = path.join(testDir, 'sub')
|
|
174
|
+
await fs.mkdir(subDir)
|
|
175
|
+
await fs.writeFile(path.join(subDir, 'AGENTS.md'), '## Shared\n\nSub shared.')
|
|
176
|
+
|
|
177
|
+
const names = await resolver.getAllAgentNames()
|
|
178
|
+
|
|
179
|
+
expect(names).toEqual(['Shared'])
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// =============================================================================
|
|
184
|
+
// Agent Existence Check Tests
|
|
185
|
+
// =============================================================================
|
|
186
|
+
|
|
187
|
+
describe('HierarchicalAgentResolver - agentExists', () => {
|
|
188
|
+
test('returns true for existing agent', async () => {
|
|
189
|
+
await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Frontend\n\nFrontend.')
|
|
190
|
+
|
|
191
|
+
const exists = await resolver.agentExists('Frontend')
|
|
192
|
+
|
|
193
|
+
expect(exists).toBe(true)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('returns false for non-existing agent', async () => {
|
|
197
|
+
await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Frontend\n\nFrontend.')
|
|
198
|
+
|
|
199
|
+
const exists = await resolver.agentExists('Backend')
|
|
200
|
+
|
|
201
|
+
expect(exists).toBe(false)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('case-insensitive check', async () => {
|
|
205
|
+
await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Frontend\n\nFrontend.')
|
|
206
|
+
|
|
207
|
+
const exists = await resolver.agentExists('FRONTEND')
|
|
208
|
+
|
|
209
|
+
expect(exists).toBe(true)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// =============================================================================
|
|
214
|
+
// Markdown Generation Tests
|
|
215
|
+
// =============================================================================
|
|
216
|
+
|
|
217
|
+
describe('HierarchicalAgentResolver - generateAgentMarkdown', () => {
|
|
218
|
+
test('generates markdown with all sections', async () => {
|
|
219
|
+
await fs.writeFile(
|
|
220
|
+
path.join(testDir, 'AGENTS.md'),
|
|
221
|
+
`## Backend
|
|
222
|
+
|
|
223
|
+
Backend development specialist.
|
|
224
|
+
|
|
225
|
+
### Domain
|
|
226
|
+
backend
|
|
227
|
+
|
|
228
|
+
### Triggers
|
|
229
|
+
- api
|
|
230
|
+
- server
|
|
231
|
+
|
|
232
|
+
### Rules
|
|
233
|
+
- Use TypeScript
|
|
234
|
+
- Log errors
|
|
235
|
+
|
|
236
|
+
### Patterns
|
|
237
|
+
\`\`\`typescript
|
|
238
|
+
export async function handler() {}
|
|
239
|
+
\`\`\`
|
|
240
|
+
`
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
const result = await resolver.resolveAgentsForPath(testDir)
|
|
244
|
+
const md = resolver.generateAgentMarkdown(result.agents[0])
|
|
245
|
+
|
|
246
|
+
expect(md).toContain('# Backend Agent')
|
|
247
|
+
expect(md).toContain('Backend development specialist.')
|
|
248
|
+
expect(md).toContain('## DOMAIN AUTHORITY')
|
|
249
|
+
expect(md).toContain('backend domain')
|
|
250
|
+
expect(md).toContain('## Triggers')
|
|
251
|
+
expect(md).toContain('- api')
|
|
252
|
+
expect(md).toContain('## Rules')
|
|
253
|
+
expect(md).toContain('- Use TypeScript')
|
|
254
|
+
expect(md).toContain('## Patterns')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test('includes source attribution', async () => {
|
|
258
|
+
await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Agent\n\nTest agent.')
|
|
259
|
+
|
|
260
|
+
const result = await resolver.resolveAgentsForPath(testDir)
|
|
261
|
+
const md = resolver.generateAgentMarkdown(result.agents[0])
|
|
262
|
+
|
|
263
|
+
expect(md).toContain('*Resolved from:')
|
|
264
|
+
expect(md).toContain('AGENTS.md')
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// =============================================================================
|
|
269
|
+
// Hierarchical Resolution Tests
|
|
270
|
+
// =============================================================================
|
|
271
|
+
|
|
272
|
+
describe('HierarchicalAgentResolver - Hierarchy', () => {
|
|
273
|
+
test('merges triggers from parent and child', async () => {
|
|
274
|
+
await fs.writeFile(
|
|
275
|
+
path.join(testDir, 'AGENTS.md'),
|
|
276
|
+
`## Shared
|
|
277
|
+
|
|
278
|
+
Base agent.
|
|
279
|
+
|
|
280
|
+
### Triggers
|
|
281
|
+
- root-trigger
|
|
282
|
+
`
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
const childDir = path.join(testDir, 'child')
|
|
286
|
+
await fs.mkdir(childDir)
|
|
287
|
+
await fs.writeFile(
|
|
288
|
+
path.join(childDir, 'AGENTS.md'),
|
|
289
|
+
`## Shared
|
|
290
|
+
|
|
291
|
+
Extended agent.
|
|
292
|
+
|
|
293
|
+
### Triggers
|
|
294
|
+
- child-trigger
|
|
295
|
+
`
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
const result = await resolver.resolveAgentsForPath(childDir)
|
|
299
|
+
|
|
300
|
+
expect(result.agents[0].triggers).toContain('root-trigger')
|
|
301
|
+
expect(result.agents[0].triggers).toContain('child-trigger')
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('tracks overridden agents', async () => {
|
|
305
|
+
await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Agent\n\nRoot.')
|
|
306
|
+
|
|
307
|
+
const childDir = path.join(testDir, 'child')
|
|
308
|
+
await fs.mkdir(childDir)
|
|
309
|
+
await fs.writeFile(path.join(childDir, 'AGENTS.md'), '## Agent @override\n\nChild override.')
|
|
310
|
+
|
|
311
|
+
const result = await resolver.resolveAgentsForPath(childDir)
|
|
312
|
+
|
|
313
|
+
expect(result.overriddenAgents).toContain('Agent')
|
|
314
|
+
expect(result.agents[0].wasOverridden).toBe(true)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
test('path-specific resolution', async () => {
|
|
318
|
+
await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Global\n\nGlobal.')
|
|
319
|
+
|
|
320
|
+
const apiDir = path.join(testDir, 'packages', 'api')
|
|
321
|
+
await fs.mkdir(apiDir, { recursive: true })
|
|
322
|
+
await fs.writeFile(path.join(apiDir, 'AGENTS.md'), '## API\n\nAPI specific.')
|
|
323
|
+
|
|
324
|
+
const webDir = path.join(testDir, 'packages', 'web')
|
|
325
|
+
await fs.mkdir(webDir, { recursive: true })
|
|
326
|
+
await fs.writeFile(path.join(webDir, 'AGENTS.md'), '## Web\n\nWeb specific.')
|
|
327
|
+
|
|
328
|
+
// Resolve for API path
|
|
329
|
+
const apiResult = await resolver.resolveAgentsForPath(apiDir)
|
|
330
|
+
expect(apiResult.agents.map((a) => a.name).sort()).toEqual(['API', 'Global'])
|
|
331
|
+
|
|
332
|
+
// Resolve for Web path
|
|
333
|
+
const webResult = await resolver.resolveAgentsForPath(webDir)
|
|
334
|
+
expect(webResult.agents.map((a) => a.name).sort()).toEqual(['Global', 'Web'])
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
// =============================================================================
|
|
339
|
+
// Agent File Tree Tests
|
|
340
|
+
// =============================================================================
|
|
341
|
+
|
|
342
|
+
describe('HierarchicalAgentResolver - getAgentFileTree', () => {
|
|
343
|
+
test('returns hierarchical tree structure', async () => {
|
|
344
|
+
await fs.writeFile(path.join(testDir, 'AGENTS.md'), '## Root\n\nRoot.')
|
|
345
|
+
|
|
346
|
+
const childDir = path.join(testDir, 'child')
|
|
347
|
+
await fs.mkdir(childDir)
|
|
348
|
+
await fs.writeFile(path.join(childDir, 'AGENTS.md'), '## Child\n\nChild.')
|
|
349
|
+
|
|
350
|
+
const tree = await resolver.getAgentFileTree()
|
|
351
|
+
|
|
352
|
+
expect(tree).toHaveLength(2)
|
|
353
|
+
const root = tree.find((n) => n.depth === 0)
|
|
354
|
+
const child = tree.find((n) => n.depth > 0)
|
|
355
|
+
|
|
356
|
+
expect(root?.children).toContain(child)
|
|
357
|
+
expect(child?.parent).toBe(root)
|
|
358
|
+
})
|
|
359
|
+
})
|