prjct-cli 1.11.0 → 1.13.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/CHANGELOG.md +87 -2354
- package/core/__tests__/agentic/analysis-injection.test.ts +377 -0
- package/core/__tests__/storage/subtask-handoff.test.ts +237 -0
- package/core/agentic/anti-hallucination.ts +23 -1
- package/core/agentic/orchestrator-executor.ts +36 -3
- package/core/agentic/prompt-builder.ts +64 -1
- package/core/schemas/state.ts +22 -2
- package/core/storage/state-storage.ts +40 -8
- package/core/types/agentic.ts +38 -0
- package/core/types/index.ts +2 -0
- package/dist/bin/prjct.mjs +162 -12
- package/package.json +1 -1
- package/templates/commands/done.md +86 -18
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analysis Injection Tests (PRJ-260)
|
|
3
|
+
*
|
|
4
|
+
* Tests for injecting sealed analysis data into task context:
|
|
5
|
+
* - Prompt builder renders analysis in ground_truth section
|
|
6
|
+
* - Anti-hallucination block enriched with analysis data
|
|
7
|
+
* - Graceful degradation when no analysis available
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { beforeEach, describe, expect, it } from 'bun:test'
|
|
11
|
+
import {
|
|
12
|
+
buildAntiHallucinationBlock,
|
|
13
|
+
type ProjectGroundTruth,
|
|
14
|
+
} from '../../agentic/anti-hallucination'
|
|
15
|
+
import promptBuilder from '../../agentic/prompt-builder'
|
|
16
|
+
import type { OrchestratorContext, SealedAnalysisContext } from '../../types'
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Test Fixtures
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
const mockSealedAnalysis: SealedAnalysisContext = {
|
|
23
|
+
languages: ['TypeScript', 'JavaScript'],
|
|
24
|
+
frameworks: ['Hono', 'Zod'],
|
|
25
|
+
packageManager: 'bun',
|
|
26
|
+
sourceDir: 'core/',
|
|
27
|
+
testDir: 'core/__tests__/',
|
|
28
|
+
fileCount: 295,
|
|
29
|
+
patterns: [
|
|
30
|
+
{
|
|
31
|
+
name: 'StorageManager pattern',
|
|
32
|
+
description: 'All storage uses StorageManager base class with read/write/update',
|
|
33
|
+
location: 'core/storage/',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'Zod schemas',
|
|
37
|
+
description: 'Runtime validation with Zod for all data structures',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
antiPatterns: [
|
|
41
|
+
{
|
|
42
|
+
issue: 'Direct fs.writeFile without StorageManager',
|
|
43
|
+
file: 'core/storage/legacy.ts',
|
|
44
|
+
suggestion: 'Use StorageManager.write() instead',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
status: 'sealed',
|
|
48
|
+
commitHash: 'abc123def456',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeOrchestratorContext(
|
|
52
|
+
sealedAnalysis: SealedAnalysisContext | null = mockSealedAnalysis
|
|
53
|
+
): OrchestratorContext {
|
|
54
|
+
return {
|
|
55
|
+
detectedDomains: ['backend', 'testing'],
|
|
56
|
+
primaryDomain: 'backend',
|
|
57
|
+
agents: [],
|
|
58
|
+
skills: [],
|
|
59
|
+
requiresFragmentation: false,
|
|
60
|
+
subtasks: null,
|
|
61
|
+
project: {
|
|
62
|
+
id: 'test-project',
|
|
63
|
+
ecosystem: 'TypeScript',
|
|
64
|
+
conventions: ['Hono', 'Zod'],
|
|
65
|
+
},
|
|
66
|
+
sealedAnalysis,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// Prompt Builder — Ground Truth Section
|
|
72
|
+
// =============================================================================
|
|
73
|
+
|
|
74
|
+
describe('Analysis Injection in Prompt Builder (PRJ-260)', () => {
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
promptBuilder.resetContext()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should render sealed analysis in ground truth section', async () => {
|
|
80
|
+
const template = {
|
|
81
|
+
frontmatter: { description: 'Test task' },
|
|
82
|
+
content: '## Instructions\nDo the work',
|
|
83
|
+
}
|
|
84
|
+
const context = { projectPath: '/test/project', files: ['a.ts'] }
|
|
85
|
+
const orcCtx = makeOrchestratorContext()
|
|
86
|
+
|
|
87
|
+
const prompt = await promptBuilder.build(
|
|
88
|
+
template,
|
|
89
|
+
context,
|
|
90
|
+
{},
|
|
91
|
+
null,
|
|
92
|
+
null,
|
|
93
|
+
null,
|
|
94
|
+
null,
|
|
95
|
+
null,
|
|
96
|
+
orcCtx
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
// Should contain analysis data
|
|
100
|
+
expect(prompt).toContain('Languages**: TypeScript, JavaScript')
|
|
101
|
+
expect(prompt).toContain('Frameworks**: Hono, Zod')
|
|
102
|
+
expect(prompt).toContain('Package Manager**: bun')
|
|
103
|
+
expect(prompt).toContain('Source Dir**: core/')
|
|
104
|
+
expect(prompt).toContain('Test Dir**: core/__tests__/')
|
|
105
|
+
expect(prompt).toContain('Files Analyzed**: 295')
|
|
106
|
+
expect(prompt).toContain('Analysis Status**: sealed')
|
|
107
|
+
expect(prompt).toContain('abc123de') // truncated commit hash
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should render code patterns from sealed analysis', async () => {
|
|
111
|
+
const template = {
|
|
112
|
+
frontmatter: { description: 'Test' },
|
|
113
|
+
content: '## Do',
|
|
114
|
+
}
|
|
115
|
+
const context = { projectPath: '/test', files: [] }
|
|
116
|
+
const orcCtx = makeOrchestratorContext()
|
|
117
|
+
|
|
118
|
+
const prompt = await promptBuilder.build(
|
|
119
|
+
template,
|
|
120
|
+
context,
|
|
121
|
+
{},
|
|
122
|
+
null,
|
|
123
|
+
null,
|
|
124
|
+
null,
|
|
125
|
+
null,
|
|
126
|
+
null,
|
|
127
|
+
orcCtx
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
expect(prompt).toContain('Code Patterns (Follow These)')
|
|
131
|
+
expect(prompt).toContain('StorageManager pattern')
|
|
132
|
+
expect(prompt).toContain('All storage uses StorageManager base class')
|
|
133
|
+
expect(prompt).toContain('core/storage/')
|
|
134
|
+
expect(prompt).toContain('Zod schemas')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should render anti-patterns from sealed analysis', async () => {
|
|
138
|
+
const template = {
|
|
139
|
+
frontmatter: { description: 'Test' },
|
|
140
|
+
content: '## Do',
|
|
141
|
+
}
|
|
142
|
+
const context = { projectPath: '/test', files: [] }
|
|
143
|
+
const orcCtx = makeOrchestratorContext()
|
|
144
|
+
|
|
145
|
+
const prompt = await promptBuilder.build(
|
|
146
|
+
template,
|
|
147
|
+
context,
|
|
148
|
+
{},
|
|
149
|
+
null,
|
|
150
|
+
null,
|
|
151
|
+
null,
|
|
152
|
+
null,
|
|
153
|
+
null,
|
|
154
|
+
orcCtx
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
expect(prompt).toContain('Anti-Patterns (Avoid These)')
|
|
158
|
+
expect(prompt).toContain('Direct fs.writeFile without StorageManager')
|
|
159
|
+
expect(prompt).toContain('core/storage/legacy.ts')
|
|
160
|
+
expect(prompt).toContain('Use StorageManager.write() instead')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should gracefully handle null sealed analysis', async () => {
|
|
164
|
+
const template = {
|
|
165
|
+
frontmatter: { description: 'Test' },
|
|
166
|
+
content: '## Do',
|
|
167
|
+
}
|
|
168
|
+
const context = { projectPath: '/test', files: [] }
|
|
169
|
+
const orcCtx = makeOrchestratorContext(null)
|
|
170
|
+
|
|
171
|
+
const prompt = await promptBuilder.build(
|
|
172
|
+
template,
|
|
173
|
+
context,
|
|
174
|
+
{},
|
|
175
|
+
null,
|
|
176
|
+
null,
|
|
177
|
+
null,
|
|
178
|
+
null,
|
|
179
|
+
null,
|
|
180
|
+
orcCtx
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
// Should still have basic project analysis
|
|
184
|
+
expect(prompt).toContain('PROJECT ANALYSIS')
|
|
185
|
+
expect(prompt).toContain('Ecosystem**: TypeScript')
|
|
186
|
+
// Should NOT have analysis-specific fields
|
|
187
|
+
expect(prompt).not.toContain('Languages**:')
|
|
188
|
+
expect(prompt).not.toContain('Code Patterns (Follow These)')
|
|
189
|
+
expect(prompt).not.toContain('Anti-Patterns (Avoid These)')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should handle empty patterns and anti-patterns', async () => {
|
|
193
|
+
const template = {
|
|
194
|
+
frontmatter: { description: 'Test' },
|
|
195
|
+
content: '## Do',
|
|
196
|
+
}
|
|
197
|
+
const context = { projectPath: '/test', files: [] }
|
|
198
|
+
const emptyAnalysis: SealedAnalysisContext = {
|
|
199
|
+
...mockSealedAnalysis,
|
|
200
|
+
patterns: [],
|
|
201
|
+
antiPatterns: [],
|
|
202
|
+
}
|
|
203
|
+
const orcCtx = makeOrchestratorContext(emptyAnalysis)
|
|
204
|
+
|
|
205
|
+
const prompt = await promptBuilder.build(
|
|
206
|
+
template,
|
|
207
|
+
context,
|
|
208
|
+
{},
|
|
209
|
+
null,
|
|
210
|
+
null,
|
|
211
|
+
null,
|
|
212
|
+
null,
|
|
213
|
+
null,
|
|
214
|
+
orcCtx
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
expect(prompt).toContain('Languages**: TypeScript, JavaScript')
|
|
218
|
+
expect(prompt).not.toContain('Code Patterns (Follow These)')
|
|
219
|
+
expect(prompt).not.toContain('Anti-Patterns (Avoid These)')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('should handle no orchestrator context at all', async () => {
|
|
223
|
+
const template = {
|
|
224
|
+
frontmatter: { description: 'Test' },
|
|
225
|
+
content: '## Do',
|
|
226
|
+
}
|
|
227
|
+
const context = { projectPath: '/test', files: [] }
|
|
228
|
+
|
|
229
|
+
const prompt = await promptBuilder.build(
|
|
230
|
+
template,
|
|
231
|
+
context,
|
|
232
|
+
{},
|
|
233
|
+
null,
|
|
234
|
+
null,
|
|
235
|
+
null,
|
|
236
|
+
null,
|
|
237
|
+
null,
|
|
238
|
+
null
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
// Should not crash, should have fallback rules
|
|
242
|
+
expect(prompt).toContain('CONSTRAINTS')
|
|
243
|
+
expect(prompt).not.toContain('PROJECT ANALYSIS')
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// =============================================================================
|
|
248
|
+
// Anti-Hallucination Block — Analysis Enrichment
|
|
249
|
+
// =============================================================================
|
|
250
|
+
|
|
251
|
+
describe('Anti-Hallucination Block with Analysis (PRJ-260)', () => {
|
|
252
|
+
it('should include analysis languages in AVAILABLE list', () => {
|
|
253
|
+
const truth: ProjectGroundTruth = {
|
|
254
|
+
projectPath: '/test',
|
|
255
|
+
language: 'TypeScript',
|
|
256
|
+
techStack: ['Hono'],
|
|
257
|
+
analysisLanguages: ['TypeScript', 'JavaScript', 'Shell'],
|
|
258
|
+
analysisFrameworks: ['Hono', 'Vitest'],
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const block = buildAntiHallucinationBlock(truth)
|
|
262
|
+
|
|
263
|
+
expect(block).toContain('AVAILABLE in this project:')
|
|
264
|
+
// TypeScript and Hono should not be duplicated
|
|
265
|
+
expect(block).toContain('JavaScript')
|
|
266
|
+
expect(block).toContain('Shell')
|
|
267
|
+
expect(block).toContain('Vitest')
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should deduplicate analysis data with existing tech stack (case-insensitive)', () => {
|
|
271
|
+
const truth: ProjectGroundTruth = {
|
|
272
|
+
projectPath: '/test',
|
|
273
|
+
language: 'TypeScript',
|
|
274
|
+
framework: 'Hono',
|
|
275
|
+
techStack: ['Zod'],
|
|
276
|
+
analysisLanguages: ['typescript'], // lowercase duplicate
|
|
277
|
+
analysisFrameworks: ['hono', 'Zod'], // lowercase duplicates
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const block = buildAntiHallucinationBlock(truth)
|
|
281
|
+
|
|
282
|
+
// Count occurrences — each should appear exactly once
|
|
283
|
+
const typescriptMatches = block.match(/typescript/gi)
|
|
284
|
+
expect(typescriptMatches?.length).toBe(1)
|
|
285
|
+
|
|
286
|
+
const honoMatches = block.match(/hono/gi)
|
|
287
|
+
expect(honoMatches?.length).toBe(1)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('should show package manager from analysis', () => {
|
|
291
|
+
const truth: ProjectGroundTruth = {
|
|
292
|
+
projectPath: '/test',
|
|
293
|
+
analysisPackageManager: 'bun',
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const block = buildAntiHallucinationBlock(truth)
|
|
297
|
+
|
|
298
|
+
expect(block).toContain('PACKAGE MANAGER: bun')
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('should work without any analysis data', () => {
|
|
302
|
+
const truth: ProjectGroundTruth = {
|
|
303
|
+
projectPath: '/test',
|
|
304
|
+
language: 'Python',
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const block = buildAntiHallucinationBlock(truth)
|
|
308
|
+
|
|
309
|
+
expect(block).toContain('AVAILABLE in this project: Python')
|
|
310
|
+
expect(block).not.toContain('PACKAGE MANAGER:')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('should handle empty analysis arrays gracefully', () => {
|
|
314
|
+
const truth: ProjectGroundTruth = {
|
|
315
|
+
projectPath: '/test',
|
|
316
|
+
analysisLanguages: [],
|
|
317
|
+
analysisFrameworks: [],
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const block = buildAntiHallucinationBlock(truth)
|
|
321
|
+
|
|
322
|
+
// Should not contain AVAILABLE line (no tech to show)
|
|
323
|
+
expect(block).not.toContain('AVAILABLE in this project:')
|
|
324
|
+
expect(block).toContain('CONSTRAINTS')
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
// =============================================================================
|
|
329
|
+
// SealedAnalysisContext Type
|
|
330
|
+
// =============================================================================
|
|
331
|
+
|
|
332
|
+
describe('SealedAnalysisContext type (PRJ-260)', () => {
|
|
333
|
+
it('should accept valid sealed analysis data', () => {
|
|
334
|
+
const analysis: SealedAnalysisContext = {
|
|
335
|
+
languages: ['TypeScript'],
|
|
336
|
+
frameworks: ['Hono'],
|
|
337
|
+
fileCount: 100,
|
|
338
|
+
patterns: [{ name: 'test', description: 'test pattern' }],
|
|
339
|
+
antiPatterns: [{ issue: 'test', file: 'test.ts', suggestion: 'fix it' }],
|
|
340
|
+
status: 'sealed',
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
expect(analysis.languages).toEqual(['TypeScript'])
|
|
344
|
+
expect(analysis.status).toBe('sealed')
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('should accept draft status', () => {
|
|
348
|
+
const analysis: SealedAnalysisContext = {
|
|
349
|
+
languages: [],
|
|
350
|
+
frameworks: [],
|
|
351
|
+
fileCount: 0,
|
|
352
|
+
patterns: [],
|
|
353
|
+
antiPatterns: [],
|
|
354
|
+
status: 'draft',
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
expect(analysis.status).toBe('draft')
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('should accept optional fields', () => {
|
|
361
|
+
const analysis: SealedAnalysisContext = {
|
|
362
|
+
languages: ['Python'],
|
|
363
|
+
frameworks: [],
|
|
364
|
+
fileCount: 50,
|
|
365
|
+
patterns: [],
|
|
366
|
+
antiPatterns: [],
|
|
367
|
+
status: 'sealed',
|
|
368
|
+
packageManager: 'pip',
|
|
369
|
+
sourceDir: 'src/',
|
|
370
|
+
testDir: 'tests/',
|
|
371
|
+
commitHash: 'abc123',
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
expect(analysis.packageManager).toBe('pip')
|
|
375
|
+
expect(analysis.commitHash).toBe('abc123')
|
|
376
|
+
})
|
|
377
|
+
})
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subtask Handoff Tests (PRJ-262)
|
|
3
|
+
*
|
|
4
|
+
* Tests for mandatory subtask output and handoff:
|
|
5
|
+
* - SubtaskCompletionDataSchema validation
|
|
6
|
+
* - SubtaskSummarySchema required fields
|
|
7
|
+
* - validateSubtaskCompletion helper
|
|
8
|
+
* - Backward compatibility with old state.json
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from 'bun:test'
|
|
12
|
+
import {
|
|
13
|
+
SubtaskCompletionDataSchema,
|
|
14
|
+
SubtaskSchema,
|
|
15
|
+
SubtaskSummarySchema,
|
|
16
|
+
validateSubtaskCompletion,
|
|
17
|
+
} from '../../schemas/state'
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// SubtaskSummarySchema — outputForNextAgent is now required
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
describe('SubtaskSummarySchema', () => {
|
|
24
|
+
const validSummary = {
|
|
25
|
+
title: 'Implement auth middleware',
|
|
26
|
+
description: 'Added JWT verification to all protected routes',
|
|
27
|
+
filesChanged: [
|
|
28
|
+
{ path: 'src/middleware/auth.ts', action: 'created' as const },
|
|
29
|
+
{ path: 'src/routes/api.ts', action: 'modified' as const },
|
|
30
|
+
],
|
|
31
|
+
whatWasDone: ['Created JWT middleware', 'Applied to API routes'],
|
|
32
|
+
outputForNextAgent:
|
|
33
|
+
'Auth middleware is in place. Next subtask should add role-based access control.',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it('should parse a valid summary with all required fields', () => {
|
|
37
|
+
const result = SubtaskSummarySchema.parse(validSummary)
|
|
38
|
+
expect(result.outputForNextAgent).toBe(validSummary.outputForNextAgent)
|
|
39
|
+
expect(result.whatWasDone).toHaveLength(2)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should reject missing outputForNextAgent', () => {
|
|
43
|
+
const { outputForNextAgent, ...withoutOutput } = validSummary
|
|
44
|
+
expect(() => SubtaskSummarySchema.parse(withoutOutput)).toThrow()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should reject empty outputForNextAgent', () => {
|
|
48
|
+
expect(() => SubtaskSummarySchema.parse({ ...validSummary, outputForNextAgent: '' })).toThrow()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should reject empty whatWasDone array', () => {
|
|
52
|
+
expect(() => SubtaskSummarySchema.parse({ ...validSummary, whatWasDone: [] })).toThrow()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should allow optional notes', () => {
|
|
56
|
+
const result = SubtaskSummarySchema.parse({ ...validSummary, notes: 'Watch for rate limits' })
|
|
57
|
+
expect(result.notes).toBe('Watch for rate limits')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should allow missing notes', () => {
|
|
61
|
+
const result = SubtaskSummarySchema.parse(validSummary)
|
|
62
|
+
expect(result.notes).toBeUndefined()
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// =============================================================================
|
|
67
|
+
// SubtaskCompletionDataSchema — validates completion requirements
|
|
68
|
+
// =============================================================================
|
|
69
|
+
|
|
70
|
+
describe('SubtaskCompletionDataSchema', () => {
|
|
71
|
+
const validCompletion = {
|
|
72
|
+
output: 'Implemented auth middleware with JWT verification',
|
|
73
|
+
summary: {
|
|
74
|
+
title: 'Auth middleware',
|
|
75
|
+
description: 'JWT verification for protected routes',
|
|
76
|
+
filesChanged: [{ path: 'src/auth.ts', action: 'created' as const }],
|
|
77
|
+
whatWasDone: ['Created JWT middleware'],
|
|
78
|
+
outputForNextAgent: 'Middleware ready, add RBAC next.',
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
it('should parse valid completion data', () => {
|
|
83
|
+
const result = SubtaskCompletionDataSchema.parse(validCompletion)
|
|
84
|
+
expect(result.output).toBe(validCompletion.output)
|
|
85
|
+
expect(result.summary.outputForNextAgent).toBe('Middleware ready, add RBAC next.')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should reject missing output', () => {
|
|
89
|
+
const { output, ...withoutOutput } = validCompletion
|
|
90
|
+
expect(() => SubtaskCompletionDataSchema.parse(withoutOutput)).toThrow()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should reject empty output', () => {
|
|
94
|
+
expect(() => SubtaskCompletionDataSchema.parse({ ...validCompletion, output: '' })).toThrow()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should reject missing summary', () => {
|
|
98
|
+
expect(() => SubtaskCompletionDataSchema.parse({ output: 'some output' })).toThrow()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should reject summary without outputForNextAgent', () => {
|
|
102
|
+
const badSummary = { ...validCompletion.summary }
|
|
103
|
+
// @ts-expect-error - intentionally testing invalid data
|
|
104
|
+
delete badSummary.outputForNextAgent
|
|
105
|
+
expect(() => SubtaskCompletionDataSchema.parse({ output: 'ok', summary: badSummary })).toThrow()
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// validateSubtaskCompletion helper
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
113
|
+
describe('validateSubtaskCompletion', () => {
|
|
114
|
+
const validData = {
|
|
115
|
+
output: 'Task done',
|
|
116
|
+
summary: {
|
|
117
|
+
title: 'Test',
|
|
118
|
+
description: 'Testing',
|
|
119
|
+
filesChanged: [],
|
|
120
|
+
whatWasDone: ['Did the thing'],
|
|
121
|
+
outputForNextAgent: 'Context for next.',
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
it('should return success for valid data', () => {
|
|
126
|
+
const result = validateSubtaskCompletion(validData)
|
|
127
|
+
expect(result.success).toBe(true)
|
|
128
|
+
if (result.success) {
|
|
129
|
+
expect(result.data.output).toBe('Task done')
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should return errors for missing output', () => {
|
|
134
|
+
const result = validateSubtaskCompletion({ summary: validData.summary })
|
|
135
|
+
expect(result.success).toBe(false)
|
|
136
|
+
if (!result.success) {
|
|
137
|
+
expect(result.errors.length).toBeGreaterThan(0)
|
|
138
|
+
expect(result.errors.some((e) => e.includes('output'))).toBe(true)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should return errors for empty whatWasDone', () => {
|
|
143
|
+
const result = validateSubtaskCompletion({
|
|
144
|
+
output: 'ok',
|
|
145
|
+
summary: { ...validData.summary, whatWasDone: [] },
|
|
146
|
+
})
|
|
147
|
+
expect(result.success).toBe(false)
|
|
148
|
+
if (!result.success) {
|
|
149
|
+
expect(result.errors.some((e) => e.includes('whatWasDone'))).toBe(true)
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should return errors for completely invalid data', () => {
|
|
154
|
+
const result = validateSubtaskCompletion({})
|
|
155
|
+
expect(result.success).toBe(false)
|
|
156
|
+
if (!result.success) {
|
|
157
|
+
expect(result.errors.length).toBeGreaterThan(0)
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// Backward Compatibility
|
|
164
|
+
// =============================================================================
|
|
165
|
+
|
|
166
|
+
describe('backward compatibility', () => {
|
|
167
|
+
it('should parse old SubtaskSchema without summary or output', () => {
|
|
168
|
+
const oldSubtask = {
|
|
169
|
+
id: 'subtask-1',
|
|
170
|
+
description: 'Old subtask without handoff',
|
|
171
|
+
domain: 'backend',
|
|
172
|
+
agent: 'backend.md',
|
|
173
|
+
status: 'completed' as const,
|
|
174
|
+
dependsOn: [],
|
|
175
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
176
|
+
completedAt: '2026-01-01T01:00:00.000Z',
|
|
177
|
+
// No output, no summary — old format
|
|
178
|
+
}
|
|
179
|
+
const result = SubtaskSchema.parse(oldSubtask)
|
|
180
|
+
expect(result.output).toBeUndefined()
|
|
181
|
+
expect(result.summary).toBeUndefined()
|
|
182
|
+
expect(result.status).toBe('completed')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should parse pending subtask without completion fields', () => {
|
|
186
|
+
const pending = {
|
|
187
|
+
id: 'subtask-2',
|
|
188
|
+
description: 'Pending subtask',
|
|
189
|
+
domain: 'frontend',
|
|
190
|
+
agent: 'frontend.md',
|
|
191
|
+
status: 'pending' as const,
|
|
192
|
+
dependsOn: ['subtask-1'],
|
|
193
|
+
}
|
|
194
|
+
const result = SubtaskSchema.parse(pending)
|
|
195
|
+
expect(result.status).toBe('pending')
|
|
196
|
+
expect(result.output).toBeUndefined()
|
|
197
|
+
expect(result.summary).toBeUndefined()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should parse subtask with output but no summary (transition format)', () => {
|
|
201
|
+
const transitional = {
|
|
202
|
+
id: 'subtask-1',
|
|
203
|
+
description: 'Transitional format',
|
|
204
|
+
domain: 'backend',
|
|
205
|
+
agent: 'backend.md',
|
|
206
|
+
status: 'completed' as const,
|
|
207
|
+
dependsOn: [],
|
|
208
|
+
output: 'Done with basic output',
|
|
209
|
+
// No summary yet
|
|
210
|
+
}
|
|
211
|
+
const result = SubtaskSchema.parse(transitional)
|
|
212
|
+
expect(result.output).toBe('Done with basic output')
|
|
213
|
+
expect(result.summary).toBeUndefined()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should parse subtask with full handoff data', () => {
|
|
217
|
+
const withHandoff = {
|
|
218
|
+
id: 'subtask-1',
|
|
219
|
+
description: 'Full handoff format',
|
|
220
|
+
domain: 'backend',
|
|
221
|
+
agent: 'backend.md',
|
|
222
|
+
status: 'completed' as const,
|
|
223
|
+
dependsOn: [],
|
|
224
|
+
output: 'Implemented the feature',
|
|
225
|
+
summary: {
|
|
226
|
+
title: 'Backend API',
|
|
227
|
+
description: 'Created REST endpoints',
|
|
228
|
+
filesChanged: [{ path: 'src/api.ts', action: 'created' as const }],
|
|
229
|
+
whatWasDone: ['Created endpoints', 'Added validation'],
|
|
230
|
+
outputForNextAgent: 'API is ready at /api/v1. Add auth next.',
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
const result = SubtaskSchema.parse(withHandoff)
|
|
234
|
+
expect(result.summary?.outputForNextAgent).toBe('API is ready at /api/v1. Add auth next.')
|
|
235
|
+
expect(result.summary?.whatWasDone).toHaveLength(2)
|
|
236
|
+
})
|
|
237
|
+
})
|
|
@@ -47,6 +47,12 @@ export const ProjectGroundTruthSchema = z.object({
|
|
|
47
47
|
fileCount: z.number().optional(),
|
|
48
48
|
/** Available agent names (e.g., ['backend', 'testing']) */
|
|
49
49
|
availableAgents: z.array(z.string()).default([]),
|
|
50
|
+
/** Sealed analysis languages — used to ground available tech (PRJ-260) */
|
|
51
|
+
analysisLanguages: z.array(z.string()).default([]),
|
|
52
|
+
/** Sealed analysis frameworks — used to ground available tech (PRJ-260) */
|
|
53
|
+
analysisFrameworks: z.array(z.string()).default([]),
|
|
54
|
+
/** Package manager from sealed analysis (PRJ-260) */
|
|
55
|
+
analysisPackageManager: z.string().optional(),
|
|
50
56
|
})
|
|
51
57
|
|
|
52
58
|
export type ProjectGroundTruth = z.input<typeof ProjectGroundTruthSchema>
|
|
@@ -79,16 +85,32 @@ export function buildAntiHallucinationBlock(truth: ProjectGroundTruth): string {
|
|
|
79
85
|
|
|
80
86
|
parts.push('## CONSTRAINTS (Read Before Acting)\n')
|
|
81
87
|
|
|
82
|
-
// 1. Explicit availability
|
|
88
|
+
// 1. Explicit availability (enriched by sealed analysis — PRJ-260)
|
|
83
89
|
const available: string[] = []
|
|
84
90
|
if (truth.language) available.push(truth.language)
|
|
85
91
|
if (truth.framework) available.push(truth.framework)
|
|
86
92
|
const techStack = truth.techStack ?? []
|
|
87
93
|
available.push(...techStack.filter((t) => t !== truth.framework))
|
|
94
|
+
// Merge languages/frameworks from sealed analysis (deduped)
|
|
95
|
+
const analysisLangs = truth.analysisLanguages ?? []
|
|
96
|
+
const analysisFrameworks = truth.analysisFrameworks ?? []
|
|
97
|
+
for (const lang of analysisLangs) {
|
|
98
|
+
if (!available.some((a) => a.toLowerCase() === lang.toLowerCase())) {
|
|
99
|
+
available.push(lang)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const fw of analysisFrameworks) {
|
|
103
|
+
if (!available.some((a) => a.toLowerCase() === fw.toLowerCase())) {
|
|
104
|
+
available.push(fw)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
88
107
|
|
|
89
108
|
if (available.length > 0) {
|
|
90
109
|
parts.push(`AVAILABLE in this project: ${available.join(', ')}`)
|
|
91
110
|
}
|
|
111
|
+
if (truth.analysisPackageManager) {
|
|
112
|
+
parts.push(`PACKAGE MANAGER: ${truth.analysisPackageManager}`)
|
|
113
|
+
}
|
|
92
114
|
|
|
93
115
|
// 2. Explicit unavailability from domain flags
|
|
94
116
|
if (truth.domains) {
|