prjct-cli 1.10.0 → 1.12.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 +76 -2344
- package/core/__tests__/storage/analysis-storage.test.ts +277 -0
- package/core/__tests__/storage/subtask-handoff.test.ts +237 -0
- package/core/agentic/prompt-builder.ts +18 -0
- package/core/commands/analysis.ts +117 -2
- package/core/commands/command-data.ts +29 -0
- package/core/commands/commands.ts +14 -0
- package/core/commands/register.ts +2 -0
- package/core/index.ts +2 -0
- package/core/schemas/analysis.ts +69 -25
- package/core/schemas/state.ts +22 -2
- package/core/services/sync-service.ts +35 -0
- package/core/storage/analysis-storage.ts +328 -0
- package/core/storage/index.ts +2 -0
- package/core/storage/state-storage.ts +40 -8
- package/core/types/agentic.ts +7 -0
- package/dist/bin/prjct.mjs +1012 -514
- package/package.json +1 -1
- package/templates/commands/done.md +86 -18
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analysis Storage Tests (PRJ-263)
|
|
3
|
+
*
|
|
4
|
+
* Tests for sealable analysis lifecycle:
|
|
5
|
+
* - Schema validation (draft/verified/sealed)
|
|
6
|
+
* - Signature computation and verification
|
|
7
|
+
* - Staleness detection
|
|
8
|
+
* - Draft preservation on re-sync
|
|
9
|
+
* - Backward compatibility
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from 'bun:test'
|
|
13
|
+
import {
|
|
14
|
+
AnalysisItemSchema,
|
|
15
|
+
AnalysisStatusSchema,
|
|
16
|
+
parseAnalysis,
|
|
17
|
+
safeParseAnalysis,
|
|
18
|
+
} from '../../schemas/analysis'
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Schema Validation
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
describe('AnalysisStatusSchema', () => {
|
|
25
|
+
it('should accept valid statuses', () => {
|
|
26
|
+
expect(AnalysisStatusSchema.parse('draft')).toBe('draft')
|
|
27
|
+
expect(AnalysisStatusSchema.parse('verified')).toBe('verified')
|
|
28
|
+
expect(AnalysisStatusSchema.parse('sealed')).toBe('sealed')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should reject invalid statuses', () => {
|
|
32
|
+
expect(() => AnalysisStatusSchema.parse('pending')).toThrow()
|
|
33
|
+
expect(() => AnalysisStatusSchema.parse('active')).toThrow()
|
|
34
|
+
expect(() => AnalysisStatusSchema.parse('')).toThrow()
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('AnalysisItemSchema', () => {
|
|
39
|
+
const validDraft = {
|
|
40
|
+
projectId: 'test-project-123',
|
|
41
|
+
languages: ['TypeScript'],
|
|
42
|
+
frameworks: ['Hono'],
|
|
43
|
+
configFiles: ['tsconfig.json'],
|
|
44
|
+
fileCount: 295,
|
|
45
|
+
patterns: [{ name: 'Service pattern', description: 'Classes with dependency injection' }],
|
|
46
|
+
antiPatterns: [],
|
|
47
|
+
analyzedAt: '2026-02-08T20:00:00.000Z',
|
|
48
|
+
status: 'draft' as const,
|
|
49
|
+
commitHash: 'abc1234',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
it('should parse a valid draft analysis', () => {
|
|
53
|
+
const result = AnalysisItemSchema.parse(validDraft)
|
|
54
|
+
expect(result.status).toBe('draft')
|
|
55
|
+
expect(result.commitHash).toBe('abc1234')
|
|
56
|
+
expect(result.projectId).toBe('test-project-123')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should default status to draft when not provided', () => {
|
|
60
|
+
const { status, ...withoutStatus } = validDraft
|
|
61
|
+
const result = AnalysisItemSchema.parse(withoutStatus)
|
|
62
|
+
expect(result.status).toBe('draft')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should parse a sealed analysis with signature', () => {
|
|
66
|
+
const sealed = {
|
|
67
|
+
...validDraft,
|
|
68
|
+
status: 'sealed' as const,
|
|
69
|
+
signature: 'sha256-abc123def456',
|
|
70
|
+
sealedAt: '2026-02-08T21:00:00.000Z',
|
|
71
|
+
}
|
|
72
|
+
const result = AnalysisItemSchema.parse(sealed)
|
|
73
|
+
expect(result.status).toBe('sealed')
|
|
74
|
+
expect(result.signature).toBe('sha256-abc123def456')
|
|
75
|
+
expect(result.sealedAt).toBe('2026-02-08T21:00:00.000Z')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should accept optional fields as undefined', () => {
|
|
79
|
+
const minimal = {
|
|
80
|
+
projectId: 'test',
|
|
81
|
+
languages: [],
|
|
82
|
+
frameworks: [],
|
|
83
|
+
configFiles: [],
|
|
84
|
+
fileCount: 0,
|
|
85
|
+
patterns: [],
|
|
86
|
+
antiPatterns: [],
|
|
87
|
+
analyzedAt: '2026-02-08T20:00:00.000Z',
|
|
88
|
+
}
|
|
89
|
+
const result = AnalysisItemSchema.parse(minimal)
|
|
90
|
+
expect(result.status).toBe('draft')
|
|
91
|
+
expect(result.commitHash).toBeUndefined()
|
|
92
|
+
expect(result.signature).toBeUndefined()
|
|
93
|
+
expect(result.sealedAt).toBeUndefined()
|
|
94
|
+
expect(result.modelMetadata).toBeUndefined()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should reject missing required fields', () => {
|
|
98
|
+
expect(() => AnalysisItemSchema.parse({})).toThrow()
|
|
99
|
+
expect(() => AnalysisItemSchema.parse({ projectId: 'test' })).toThrow()
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('parseAnalysis / safeParseAnalysis', () => {
|
|
104
|
+
it('should parse valid data', () => {
|
|
105
|
+
const data = {
|
|
106
|
+
projectId: 'test',
|
|
107
|
+
languages: ['TypeScript'],
|
|
108
|
+
frameworks: [],
|
|
109
|
+
configFiles: [],
|
|
110
|
+
fileCount: 10,
|
|
111
|
+
patterns: [],
|
|
112
|
+
antiPatterns: [],
|
|
113
|
+
analyzedAt: '2026-02-08T20:00:00.000Z',
|
|
114
|
+
}
|
|
115
|
+
const result = parseAnalysis(data)
|
|
116
|
+
expect(result.projectId).toBe('test')
|
|
117
|
+
expect(result.status).toBe('draft')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should return success for safeParseAnalysis with valid data', () => {
|
|
121
|
+
const data = {
|
|
122
|
+
projectId: 'test',
|
|
123
|
+
languages: [],
|
|
124
|
+
frameworks: [],
|
|
125
|
+
configFiles: [],
|
|
126
|
+
fileCount: 0,
|
|
127
|
+
patterns: [],
|
|
128
|
+
antiPatterns: [],
|
|
129
|
+
analyzedAt: '2026-02-08T20:00:00.000Z',
|
|
130
|
+
}
|
|
131
|
+
const result = safeParseAnalysis(data)
|
|
132
|
+
expect(result.success).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should return failure for safeParseAnalysis with invalid data', () => {
|
|
136
|
+
const result = safeParseAnalysis({ invalid: true })
|
|
137
|
+
expect(result.success).toBe(false)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// =============================================================================
|
|
142
|
+
// Staleness Detection (pure function tests)
|
|
143
|
+
// =============================================================================
|
|
144
|
+
|
|
145
|
+
describe('staleness detection', () => {
|
|
146
|
+
// Test checkStaleness method from AnalysisStorage
|
|
147
|
+
it('should detect stale analysis when commits differ', () => {
|
|
148
|
+
const { analysisStorage } = require('../../storage/analysis-storage')
|
|
149
|
+
const result = analysisStorage.checkStaleness('abc1234', 'def5678')
|
|
150
|
+
expect(result.isStale).toBe(true)
|
|
151
|
+
expect(result.sealedCommit).toBe('abc1234')
|
|
152
|
+
expect(result.currentCommit).toBe('def5678')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should detect fresh analysis when commits match', () => {
|
|
156
|
+
const { analysisStorage } = require('../../storage/analysis-storage')
|
|
157
|
+
const result = analysisStorage.checkStaleness('abc1234', 'abc1234')
|
|
158
|
+
expect(result.isStale).toBe(false)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should handle null sealed commit', () => {
|
|
162
|
+
const { analysisStorage } = require('../../storage/analysis-storage')
|
|
163
|
+
const result = analysisStorage.checkStaleness(null, 'abc1234')
|
|
164
|
+
expect(result.isStale).toBe(false)
|
|
165
|
+
expect(result.message).toContain('No sealed analysis')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('should handle null current commit', () => {
|
|
169
|
+
const { analysisStorage } = require('../../storage/analysis-storage')
|
|
170
|
+
const result = analysisStorage.checkStaleness('abc1234', null)
|
|
171
|
+
expect(result.isStale).toBe(true)
|
|
172
|
+
expect(result.message).toContain('Cannot determine')
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// Signature Computation (determinism test)
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
describe('signature computation', () => {
|
|
181
|
+
it('should produce deterministic signatures for same input', () => {
|
|
182
|
+
const { createHash } = require('node:crypto')
|
|
183
|
+
|
|
184
|
+
const analysis = {
|
|
185
|
+
projectId: 'test',
|
|
186
|
+
languages: ['TypeScript'],
|
|
187
|
+
frameworks: ['Hono'],
|
|
188
|
+
configFiles: [],
|
|
189
|
+
fileCount: 100,
|
|
190
|
+
patterns: [],
|
|
191
|
+
antiPatterns: [],
|
|
192
|
+
analyzedAt: '2026-02-08T20:00:00.000Z',
|
|
193
|
+
commitHash: 'abc1234',
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const canonical = {
|
|
197
|
+
projectId: analysis.projectId,
|
|
198
|
+
languages: analysis.languages,
|
|
199
|
+
frameworks: analysis.frameworks,
|
|
200
|
+
packageManager: undefined,
|
|
201
|
+
sourceDir: undefined,
|
|
202
|
+
testDir: undefined,
|
|
203
|
+
configFiles: analysis.configFiles,
|
|
204
|
+
fileCount: analysis.fileCount,
|
|
205
|
+
patterns: analysis.patterns,
|
|
206
|
+
antiPatterns: analysis.antiPatterns,
|
|
207
|
+
analyzedAt: analysis.analyzedAt,
|
|
208
|
+
commitHash: analysis.commitHash,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const sig1 = createHash('sha256').update(JSON.stringify(canonical)).digest('hex')
|
|
212
|
+
const sig2 = createHash('sha256').update(JSON.stringify(canonical)).digest('hex')
|
|
213
|
+
|
|
214
|
+
expect(sig1).toBe(sig2)
|
|
215
|
+
expect(sig1).toHaveLength(64) // SHA-256 hex = 64 chars
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('should produce different signatures for different inputs', () => {
|
|
219
|
+
const { createHash } = require('node:crypto')
|
|
220
|
+
|
|
221
|
+
const data1 = JSON.stringify({ projectId: 'a', fileCount: 1 })
|
|
222
|
+
const data2 = JSON.stringify({ projectId: 'b', fileCount: 2 })
|
|
223
|
+
|
|
224
|
+
const sig1 = createHash('sha256').update(data1).digest('hex')
|
|
225
|
+
const sig2 = createHash('sha256').update(data2).digest('hex')
|
|
226
|
+
|
|
227
|
+
expect(sig1).not.toBe(sig2)
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// =============================================================================
|
|
232
|
+
// Backward Compatibility
|
|
233
|
+
// =============================================================================
|
|
234
|
+
|
|
235
|
+
describe('backward compatibility', () => {
|
|
236
|
+
it('should parse old analysis.json without seal fields', () => {
|
|
237
|
+
const oldFormat = {
|
|
238
|
+
projectId: 'old-project',
|
|
239
|
+
languages: ['JavaScript'],
|
|
240
|
+
frameworks: ['Express'],
|
|
241
|
+
configFiles: ['package.json'],
|
|
242
|
+
fileCount: 50,
|
|
243
|
+
patterns: [],
|
|
244
|
+
antiPatterns: [],
|
|
245
|
+
analyzedAt: '2025-12-01T00:00:00.000Z',
|
|
246
|
+
// No status, commitHash, signature, sealedAt
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const result = AnalysisItemSchema.parse(oldFormat)
|
|
250
|
+
expect(result.status).toBe('draft') // Default
|
|
251
|
+
expect(result.commitHash).toBeUndefined()
|
|
252
|
+
expect(result.signature).toBeUndefined()
|
|
253
|
+
expect(result.sealedAt).toBeUndefined()
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should parse old analysis with modelMetadata but no seal fields', () => {
|
|
257
|
+
const oldWithModel = {
|
|
258
|
+
projectId: 'old-project',
|
|
259
|
+
languages: ['TypeScript'],
|
|
260
|
+
frameworks: [],
|
|
261
|
+
configFiles: [],
|
|
262
|
+
fileCount: 10,
|
|
263
|
+
patterns: [],
|
|
264
|
+
antiPatterns: [],
|
|
265
|
+
analyzedAt: '2026-01-15T00:00:00.000Z',
|
|
266
|
+
modelMetadata: {
|
|
267
|
+
provider: 'claude',
|
|
268
|
+
model: 'sonnet',
|
|
269
|
+
recordedAt: '2026-01-15T00:00:00.000Z',
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const result = AnalysisItemSchema.parse(oldWithModel)
|
|
274
|
+
expect(result.status).toBe('draft')
|
|
275
|
+
expect(result.modelMetadata?.provider).toBe('claude')
|
|
276
|
+
})
|
|
277
|
+
})
|
|
@@ -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
|
+
})
|
|
@@ -800,6 +800,24 @@ class PromptBuilder {
|
|
|
800
800
|
if (currentSubtask.dependsOn.length > 0) {
|
|
801
801
|
parts.push(`Dependencies: ${currentSubtask.dependsOn.join(', ')}\n`)
|
|
802
802
|
}
|
|
803
|
+
|
|
804
|
+
// Inject previous subtask handoff for context continuity (PRJ-262)
|
|
805
|
+
if (currentSubtask.handoff) {
|
|
806
|
+
const h = currentSubtask.handoff
|
|
807
|
+
parts.push('\n### Previous Subtask Handoff\n\n')
|
|
808
|
+
parts.push(`**From:** ${h.fromSubtask}\n\n`)
|
|
809
|
+
parts.push('**What was done:**\n')
|
|
810
|
+
for (const item of h.whatWasDone) {
|
|
811
|
+
parts.push(`- ${item}\n`)
|
|
812
|
+
}
|
|
813
|
+
if (h.filesChanged.length > 0) {
|
|
814
|
+
parts.push('\n**Files changed:**\n')
|
|
815
|
+
for (const f of h.filesChanged) {
|
|
816
|
+
parts.push(`- \`${f.path}\` (${f.action})\n`)
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
parts.push(`\n**Context for this subtask:**\n${h.outputForNextAgent}\n`)
|
|
820
|
+
}
|
|
803
821
|
}
|
|
804
822
|
parts.push('\n')
|
|
805
823
|
}
|
|
@@ -12,6 +12,7 @@ import commandInstaller from '../infrastructure/command-installer'
|
|
|
12
12
|
import { formatCost } from '../schemas/metrics'
|
|
13
13
|
import { createStalenessChecker, memoryService, syncService } from '../services'
|
|
14
14
|
import { formatDiffPreview, formatFullDiff, generateSyncDiff } from '../services/diff-generator'
|
|
15
|
+
import { analysisStorage } from '../storage/analysis-storage'
|
|
15
16
|
import { metricsStorage } from '../storage/metrics-storage'
|
|
16
17
|
import type { AnalyzeOptions, CommandResult, ProjectContext } from '../types'
|
|
17
18
|
import { getErrorMessage } from '../types/fs'
|
|
@@ -805,6 +806,9 @@ export class AnalysisCommands extends PrjctCommandsBase {
|
|
|
805
806
|
// Get session info
|
|
806
807
|
const sessionInfo = await checker.getSessionInfo(projectId)
|
|
807
808
|
|
|
809
|
+
// Get analysis status (PRJ-263)
|
|
810
|
+
const analysisStatus = await analysisStorage.getStatus(projectId)
|
|
811
|
+
|
|
808
812
|
// JSON output mode
|
|
809
813
|
if (options.json) {
|
|
810
814
|
console.log(
|
|
@@ -812,9 +816,13 @@ export class AnalysisCommands extends PrjctCommandsBase {
|
|
|
812
816
|
success: true,
|
|
813
817
|
...status,
|
|
814
818
|
session: sessionInfo,
|
|
819
|
+
analysis: analysisStatus,
|
|
815
820
|
})
|
|
816
821
|
)
|
|
817
|
-
return {
|
|
822
|
+
return {
|
|
823
|
+
success: true,
|
|
824
|
+
data: { ...status, session: sessionInfo, analysis: analysisStatus },
|
|
825
|
+
}
|
|
818
826
|
}
|
|
819
827
|
|
|
820
828
|
// Human-readable output
|
|
@@ -822,9 +830,22 @@ export class AnalysisCommands extends PrjctCommandsBase {
|
|
|
822
830
|
console.log(checker.formatStatus(status))
|
|
823
831
|
console.log('')
|
|
824
832
|
console.log(checker.formatSessionInfo(sessionInfo))
|
|
833
|
+
|
|
834
|
+
// Show analysis status (PRJ-263)
|
|
835
|
+
if (analysisStatus.hasSealed || analysisStatus.hasDraft) {
|
|
836
|
+
console.log('')
|
|
837
|
+
console.log('Analysis:')
|
|
838
|
+
if (analysisStatus.hasSealed) {
|
|
839
|
+
console.log(` Sealed: ${analysisStatus.sealedCommit} (${analysisStatus.sealedAt})`)
|
|
840
|
+
}
|
|
841
|
+
if (analysisStatus.hasDraft) {
|
|
842
|
+
console.log(` Draft: ${analysisStatus.draftCommit} (pending seal)`)
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
825
846
|
console.log('')
|
|
826
847
|
|
|
827
|
-
return { success: true, data: { ...status, session: sessionInfo } }
|
|
848
|
+
return { success: true, data: { ...status, session: sessionInfo, analysis: analysisStatus } }
|
|
828
849
|
} catch (error) {
|
|
829
850
|
const errMsg = getErrorMessage(error)
|
|
830
851
|
if (options.json) {
|
|
@@ -836,6 +857,100 @@ export class AnalysisCommands extends PrjctCommandsBase {
|
|
|
836
857
|
}
|
|
837
858
|
}
|
|
838
859
|
|
|
860
|
+
/**
|
|
861
|
+
* prjct seal - Seal the current draft analysis (PRJ-263)
|
|
862
|
+
*
|
|
863
|
+
* Locks the current draft with a SHA-256 signature.
|
|
864
|
+
* Only sealed analysis feeds task context.
|
|
865
|
+
*/
|
|
866
|
+
async seal(
|
|
867
|
+
projectPath: string = process.cwd(),
|
|
868
|
+
options: { json?: boolean } = {}
|
|
869
|
+
): Promise<CommandResult> {
|
|
870
|
+
try {
|
|
871
|
+
const initResult = await this.ensureProjectInit(projectPath)
|
|
872
|
+
if (!initResult.success) return initResult
|
|
873
|
+
|
|
874
|
+
const projectId = await configManager.getProjectId(projectPath)
|
|
875
|
+
if (!projectId) {
|
|
876
|
+
if (options.json) {
|
|
877
|
+
console.log(JSON.stringify({ success: false, error: 'No project ID found' }))
|
|
878
|
+
}
|
|
879
|
+
return { success: false, error: 'No project ID found' }
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const result = await analysisStorage.seal(projectId)
|
|
883
|
+
|
|
884
|
+
if (options.json) {
|
|
885
|
+
console.log(
|
|
886
|
+
JSON.stringify({
|
|
887
|
+
success: result.success,
|
|
888
|
+
signature: result.signature,
|
|
889
|
+
error: result.error,
|
|
890
|
+
})
|
|
891
|
+
)
|
|
892
|
+
return { success: result.success, error: result.error }
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (!result.success) {
|
|
896
|
+
out.fail(result.error || 'Seal failed')
|
|
897
|
+
return { success: false, error: result.error }
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
out.done('Analysis sealed')
|
|
901
|
+
console.log(` Signature: ${result.signature?.substring(0, 16)}...`)
|
|
902
|
+
console.log('')
|
|
903
|
+
|
|
904
|
+
return { success: true, data: { signature: result.signature } }
|
|
905
|
+
} catch (error) {
|
|
906
|
+
const errMsg = getErrorMessage(error)
|
|
907
|
+
if (options.json) {
|
|
908
|
+
console.log(JSON.stringify({ success: false, error: errMsg }))
|
|
909
|
+
} else {
|
|
910
|
+
out.fail(errMsg)
|
|
911
|
+
}
|
|
912
|
+
return { success: false, error: errMsg }
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* prjct verify - Verify integrity of sealed analysis (PRJ-263)
|
|
918
|
+
*/
|
|
919
|
+
async verify(
|
|
920
|
+
projectPath: string = process.cwd(),
|
|
921
|
+
options: { json?: boolean } = {}
|
|
922
|
+
): Promise<CommandResult> {
|
|
923
|
+
try {
|
|
924
|
+
const initResult = await this.ensureProjectInit(projectPath)
|
|
925
|
+
if (!initResult.success) return initResult
|
|
926
|
+
|
|
927
|
+
const projectId = await configManager.getProjectId(projectPath)
|
|
928
|
+
if (!projectId) {
|
|
929
|
+
return { success: false, error: 'No project ID found' }
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const result = await analysisStorage.verify(projectId)
|
|
933
|
+
|
|
934
|
+
if (options.json) {
|
|
935
|
+
console.log(JSON.stringify(result))
|
|
936
|
+
return { success: result.valid }
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (result.valid) {
|
|
940
|
+
out.done(result.message)
|
|
941
|
+
} else {
|
|
942
|
+
out.fail(result.message)
|
|
943
|
+
}
|
|
944
|
+
console.log('')
|
|
945
|
+
|
|
946
|
+
return { success: result.valid, data: result }
|
|
947
|
+
} catch (error) {
|
|
948
|
+
const errMsg = getErrorMessage(error)
|
|
949
|
+
out.fail(errMsg)
|
|
950
|
+
return { success: false, error: errMsg }
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
839
954
|
/**
|
|
840
955
|
* Get session activity stats from today's events
|
|
841
956
|
* @see PRJ-89
|