prjct-cli 1.11.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 +47 -2354
- package/core/__tests__/storage/subtask-handoff.test.ts +237 -0
- package/core/agentic/prompt-builder.ts +18 -0
- package/core/schemas/state.ts +22 -2
- package/core/storage/state-storage.ts +40 -8
- package/core/types/agentic.ts +7 -0
- package/dist/bin/prjct.mjs +59 -7
- package/package.json +1 -1
- package/templates/commands/done.md +86 -18
|
@@ -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
|
}
|
package/core/schemas/state.ts
CHANGED
|
@@ -44,11 +44,18 @@ export const SubtaskSummarySchema = z.object({
|
|
|
44
44
|
action: z.enum(['created', 'modified', 'deleted']),
|
|
45
45
|
})
|
|
46
46
|
),
|
|
47
|
-
whatWasDone: z.array(z.string()),
|
|
48
|
-
outputForNextAgent: z.string().
|
|
47
|
+
whatWasDone: z.array(z.string()).min(1),
|
|
48
|
+
outputForNextAgent: z.string().min(1),
|
|
49
49
|
notes: z.string().optional(),
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
+
// Schema for validating completion data before persisting
|
|
53
|
+
// Used by completeSubtask() to enforce mandatory handoff
|
|
54
|
+
export const SubtaskCompletionDataSchema = z.object({
|
|
55
|
+
output: z.string().min(1, 'Subtask output is required'),
|
|
56
|
+
summary: SubtaskSummarySchema,
|
|
57
|
+
})
|
|
58
|
+
|
|
52
59
|
// Subtask schema for task fragmentation
|
|
53
60
|
export const SubtaskSchema = z.object({
|
|
54
61
|
id: z.string(), // subtask-xxx
|
|
@@ -169,6 +176,7 @@ export type ActivityType = z.infer<typeof ActivityTypeSchema>
|
|
|
169
176
|
|
|
170
177
|
export type Subtask = z.infer<typeof SubtaskSchema>
|
|
171
178
|
export type SubtaskSummary = z.infer<typeof SubtaskSummarySchema>
|
|
179
|
+
export type SubtaskCompletionData = z.infer<typeof SubtaskCompletionDataSchema>
|
|
172
180
|
export type SubtaskProgress = z.infer<typeof SubtaskProgressSchema>
|
|
173
181
|
|
|
174
182
|
export type CurrentTask = z.infer<typeof CurrentTaskSchema>
|
|
@@ -197,6 +205,18 @@ export const parseQueue = (data: unknown): QueueJson => QueueJsonSchema.parse(da
|
|
|
197
205
|
export const safeParseState = (data: unknown) => StateJsonSchema.safeParse(data)
|
|
198
206
|
export const safeParseQueue = (data: unknown) => QueueJsonSchema.safeParse(data)
|
|
199
207
|
|
|
208
|
+
/** Validate subtask completion data — returns errors or null */
|
|
209
|
+
export const validateSubtaskCompletion = (
|
|
210
|
+
data: unknown
|
|
211
|
+
): { success: true; data: SubtaskCompletionData } | { success: false; errors: string[] } => {
|
|
212
|
+
const result = SubtaskCompletionDataSchema.safeParse(data)
|
|
213
|
+
if (result.success) return { success: true, data: result.data }
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
errors: result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`),
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
200
220
|
// =============================================================================
|
|
201
221
|
// Defaults
|
|
202
222
|
// =============================================================================
|
|
@@ -14,9 +14,9 @@ import type {
|
|
|
14
14
|
PreviousTask,
|
|
15
15
|
StateJson,
|
|
16
16
|
Subtask,
|
|
17
|
-
|
|
17
|
+
SubtaskCompletionData,
|
|
18
18
|
} from '../schemas/state'
|
|
19
|
-
import { StateJsonSchema } from '../schemas/state'
|
|
19
|
+
import { StateJsonSchema, SubtaskCompletionDataSchema } from '../schemas/state'
|
|
20
20
|
import { getTimestamp, toRelative } from '../utils/date-helper'
|
|
21
21
|
import { md } from '../utils/markdown-builder'
|
|
22
22
|
import type { WorkflowCommand } from '../workflow/state-machine'
|
|
@@ -497,14 +497,23 @@ class StateStorage extends StorageManager<StateJson> {
|
|
|
497
497
|
}
|
|
498
498
|
|
|
499
499
|
/**
|
|
500
|
-
* Complete current subtask and advance to next
|
|
501
|
-
*
|
|
500
|
+
* Complete current subtask and advance to next.
|
|
501
|
+
* Requires output and summary for mandatory handoff (PRJ-262).
|
|
502
|
+
* Returns the next subtask (or null if all complete).
|
|
502
503
|
*/
|
|
503
504
|
async completeSubtask(
|
|
504
505
|
projectId: string,
|
|
505
|
-
|
|
506
|
-
summary?: SubtaskSummary
|
|
506
|
+
completionData: SubtaskCompletionData
|
|
507
507
|
): Promise<Subtask | null> {
|
|
508
|
+
// Validate handoff data with Zod before persisting
|
|
509
|
+
const validation = SubtaskCompletionDataSchema.safeParse(completionData)
|
|
510
|
+
if (!validation.success) {
|
|
511
|
+
const errors = validation.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`)
|
|
512
|
+
throw new Error(`Subtask completion requires handoff data:\n${errors.join('\n')}`)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const { output, summary } = validation.data
|
|
516
|
+
|
|
508
517
|
const state = await this.read(projectId)
|
|
509
518
|
if (!state.currentTask?.subtasks) return null
|
|
510
519
|
|
|
@@ -512,7 +521,7 @@ class StateStorage extends StorageManager<StateJson> {
|
|
|
512
521
|
const current = state.currentTask.subtasks[currentIndex]
|
|
513
522
|
if (!current) return null
|
|
514
523
|
|
|
515
|
-
// Mark current as completed
|
|
524
|
+
// Mark current as completed with mandatory handoff
|
|
516
525
|
const updatedSubtasks = [...state.currentTask.subtasks]
|
|
517
526
|
updatedSubtasks[currentIndex] = {
|
|
518
527
|
...current,
|
|
@@ -548,12 +557,14 @@ class StateStorage extends StorageManager<StateJson> {
|
|
|
548
557
|
lastUpdated: getTimestamp(),
|
|
549
558
|
}))
|
|
550
559
|
|
|
551
|
-
// Publish event
|
|
560
|
+
// Publish event with handoff data
|
|
552
561
|
await this.publishEvent(projectId, 'subtask.completed', {
|
|
553
562
|
taskId: state.currentTask.id,
|
|
554
563
|
subtaskId: current.id,
|
|
555
564
|
description: current.description,
|
|
556
565
|
output,
|
|
566
|
+
handoff: summary.outputForNextAgent,
|
|
567
|
+
filesChanged: summary.filesChanged.length,
|
|
557
568
|
progress: { completed, total, percentage },
|
|
558
569
|
})
|
|
559
570
|
|
|
@@ -592,6 +603,27 @@ class StateStorage extends StorageManager<StateJson> {
|
|
|
592
603
|
return state.currentTask.subtasks[prevIndex] || null
|
|
593
604
|
}
|
|
594
605
|
|
|
606
|
+
/**
|
|
607
|
+
* Get handoff from the most recently completed subtask (PRJ-262).
|
|
608
|
+
* Returns the summary.outputForNextAgent and related context,
|
|
609
|
+
* or null if no previous subtask or no handoff data.
|
|
610
|
+
*/
|
|
611
|
+
async getPreviousHandoff(projectId: string): Promise<{
|
|
612
|
+
fromSubtask: string
|
|
613
|
+
outputForNextAgent: string
|
|
614
|
+
filesChanged: Array<{ path: string; action: string }>
|
|
615
|
+
whatWasDone: string[]
|
|
616
|
+
} | null> {
|
|
617
|
+
const prev = await this.getPreviousSubtask(projectId)
|
|
618
|
+
if (!prev?.summary?.outputForNextAgent) return null
|
|
619
|
+
return {
|
|
620
|
+
fromSubtask: prev.description,
|
|
621
|
+
outputForNextAgent: prev.summary.outputForNextAgent,
|
|
622
|
+
filesChanged: prev.summary.filesChanged,
|
|
623
|
+
whatWasDone: prev.summary.whatWasDone,
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
595
627
|
/**
|
|
596
628
|
* Get all subtasks
|
|
597
629
|
*/
|
package/core/types/agentic.ts
CHANGED
|
@@ -673,6 +673,13 @@ export interface OrchestratorSubtask {
|
|
|
673
673
|
status: 'pending' | 'in_progress' | 'completed' | 'failed'
|
|
674
674
|
dependsOn: string[]
|
|
675
675
|
order: number
|
|
676
|
+
/** Handoff from previous subtask — injected into prompt for context continuity (PRJ-262) */
|
|
677
|
+
handoff?: {
|
|
678
|
+
fromSubtask: string
|
|
679
|
+
outputForNextAgent: string
|
|
680
|
+
filesChanged: Array<{ path: string; action: string }>
|
|
681
|
+
whatWasDone: string[]
|
|
682
|
+
}
|
|
676
683
|
}
|
|
677
684
|
|
|
678
685
|
/**
|
package/dist/bin/prjct.mjs
CHANGED
|
@@ -10212,7 +10212,7 @@ var init_shipped = __esm({
|
|
|
10212
10212
|
|
|
10213
10213
|
// core/schemas/state.ts
|
|
10214
10214
|
import { z as z14 } from "zod";
|
|
10215
|
-
var PrioritySchema, TaskTypeSchema, TaskSectionSchema, TaskStatusSchema, ActivityTypeSchema, SubtaskSummarySchema, SubtaskSchema, SubtaskProgressSchema, CurrentTaskSchema, PreviousTaskSchema, StateJsonSchema, QueueTaskSchema, QueueJsonSchema, StatsSchema, RecentActivitySchema, StateSchemaFull;
|
|
10215
|
+
var PrioritySchema, TaskTypeSchema, TaskSectionSchema, TaskStatusSchema, ActivityTypeSchema, SubtaskSummarySchema, SubtaskCompletionDataSchema, SubtaskSchema, SubtaskProgressSchema, CurrentTaskSchema, PreviousTaskSchema, StateJsonSchema, QueueTaskSchema, QueueJsonSchema, StatsSchema, RecentActivitySchema, StateSchemaFull;
|
|
10216
10216
|
var init_state = __esm({
|
|
10217
10217
|
"core/schemas/state.ts"() {
|
|
10218
10218
|
"use strict";
|
|
@@ -10244,10 +10244,14 @@ var init_state = __esm({
|
|
|
10244
10244
|
action: z14.enum(["created", "modified", "deleted"])
|
|
10245
10245
|
})
|
|
10246
10246
|
),
|
|
10247
|
-
whatWasDone: z14.array(z14.string()),
|
|
10248
|
-
outputForNextAgent: z14.string().
|
|
10247
|
+
whatWasDone: z14.array(z14.string()).min(1),
|
|
10248
|
+
outputForNextAgent: z14.string().min(1),
|
|
10249
10249
|
notes: z14.string().optional()
|
|
10250
10250
|
});
|
|
10251
|
+
SubtaskCompletionDataSchema = z14.object({
|
|
10252
|
+
output: z14.string().min(1, "Subtask output is required"),
|
|
10253
|
+
summary: SubtaskSummarySchema
|
|
10254
|
+
});
|
|
10251
10255
|
SubtaskSchema = z14.object({
|
|
10252
10256
|
id: z14.string(),
|
|
10253
10257
|
// subtask-xxx
|
|
@@ -13259,10 +13263,18 @@ var init_state_storage = __esm({
|
|
|
13259
13263
|
});
|
|
13260
13264
|
}
|
|
13261
13265
|
/**
|
|
13262
|
-
* Complete current subtask and advance to next
|
|
13263
|
-
*
|
|
13266
|
+
* Complete current subtask and advance to next.
|
|
13267
|
+
* Requires output and summary for mandatory handoff (PRJ-262).
|
|
13268
|
+
* Returns the next subtask (or null if all complete).
|
|
13264
13269
|
*/
|
|
13265
|
-
async completeSubtask(projectId,
|
|
13270
|
+
async completeSubtask(projectId, completionData) {
|
|
13271
|
+
const validation = SubtaskCompletionDataSchema.safeParse(completionData);
|
|
13272
|
+
if (!validation.success) {
|
|
13273
|
+
const errors = validation.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
|
|
13274
|
+
throw new Error(`Subtask completion requires handoff data:
|
|
13275
|
+
${errors.join("\n")}`);
|
|
13276
|
+
}
|
|
13277
|
+
const { output, summary } = validation.data;
|
|
13266
13278
|
const state = await this.read(projectId);
|
|
13267
13279
|
if (!state.currentTask?.subtasks) return null;
|
|
13268
13280
|
const currentIndex = state.currentTask.currentSubtaskIndex || 0;
|
|
@@ -13302,6 +13314,8 @@ var init_state_storage = __esm({
|
|
|
13302
13314
|
subtaskId: current.id,
|
|
13303
13315
|
description: current.description,
|
|
13304
13316
|
output,
|
|
13317
|
+
handoff: summary.outputForNextAgent,
|
|
13318
|
+
filesChanged: summary.filesChanged.length,
|
|
13305
13319
|
progress: { completed, total, percentage }
|
|
13306
13320
|
});
|
|
13307
13321
|
return nextIndex < total ? updatedSubtasks[nextIndex] : null;
|
|
@@ -13334,6 +13348,21 @@ var init_state_storage = __esm({
|
|
|
13334
13348
|
if (prevIndex < 0) return null;
|
|
13335
13349
|
return state.currentTask.subtasks[prevIndex] || null;
|
|
13336
13350
|
}
|
|
13351
|
+
/**
|
|
13352
|
+
* Get handoff from the most recently completed subtask (PRJ-262).
|
|
13353
|
+
* Returns the summary.outputForNextAgent and related context,
|
|
13354
|
+
* or null if no previous subtask or no handoff data.
|
|
13355
|
+
*/
|
|
13356
|
+
async getPreviousHandoff(projectId) {
|
|
13357
|
+
const prev = await this.getPreviousSubtask(projectId);
|
|
13358
|
+
if (!prev?.summary?.outputForNextAgent) return null;
|
|
13359
|
+
return {
|
|
13360
|
+
fromSubtask: prev.description,
|
|
13361
|
+
outputForNextAgent: prev.summary.outputForNextAgent,
|
|
13362
|
+
filesChanged: prev.summary.filesChanged,
|
|
13363
|
+
whatWasDone: prev.summary.whatWasDone
|
|
13364
|
+
};
|
|
13365
|
+
}
|
|
13337
13366
|
/**
|
|
13338
13367
|
* Get all subtasks
|
|
13339
13368
|
*/
|
|
@@ -16386,6 +16415,29 @@ Read files before modifying.
|
|
|
16386
16415
|
`);
|
|
16387
16416
|
if (currentSubtask.dependsOn.length > 0) {
|
|
16388
16417
|
parts.push(`Dependencies: ${currentSubtask.dependsOn.join(", ")}
|
|
16418
|
+
`);
|
|
16419
|
+
}
|
|
16420
|
+
if (currentSubtask.handoff) {
|
|
16421
|
+
const h = currentSubtask.handoff;
|
|
16422
|
+
parts.push("\n### Previous Subtask Handoff\n\n");
|
|
16423
|
+
parts.push(`**From:** ${h.fromSubtask}
|
|
16424
|
+
|
|
16425
|
+
`);
|
|
16426
|
+
parts.push("**What was done:**\n");
|
|
16427
|
+
for (const item of h.whatWasDone) {
|
|
16428
|
+
parts.push(`- ${item}
|
|
16429
|
+
`);
|
|
16430
|
+
}
|
|
16431
|
+
if (h.filesChanged.length > 0) {
|
|
16432
|
+
parts.push("\n**Files changed:**\n");
|
|
16433
|
+
for (const f of h.filesChanged) {
|
|
16434
|
+
parts.push(`- \`${f.path}\` (${f.action})
|
|
16435
|
+
`);
|
|
16436
|
+
}
|
|
16437
|
+
}
|
|
16438
|
+
parts.push(`
|
|
16439
|
+
**Context for this subtask:**
|
|
16440
|
+
${h.outputForNextAgent}
|
|
16389
16441
|
`);
|
|
16390
16442
|
}
|
|
16391
16443
|
}
|
|
@@ -31050,7 +31102,7 @@ var require_package = __commonJS({
|
|
|
31050
31102
|
"package.json"(exports, module) {
|
|
31051
31103
|
module.exports = {
|
|
31052
31104
|
name: "prjct-cli",
|
|
31053
|
-
version: "1.
|
|
31105
|
+
version: "1.12.0",
|
|
31054
31106
|
description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
|
|
31055
31107
|
main: "core/index.ts",
|
|
31056
31108
|
bin: {
|
package/package.json
CHANGED
|
@@ -43,34 +43,102 @@ IF currentTask.subtasks exists AND has items:
|
|
|
43
43
|
- label: "Continue current"
|
|
44
44
|
description: "Keep working on this subtask"
|
|
45
45
|
|
|
46
|
-
IF "
|
|
47
|
-
|
|
46
|
+
IF "Continue current":
|
|
47
|
+
OUTPUT: "Continuing: {current subtask}"
|
|
48
|
+
STOP
|
|
49
|
+
|
|
50
|
+
IF "Next subtask" OR "Complete all remaining":
|
|
51
|
+
# ═══════════════════════════════════════════════════════════════
|
|
52
|
+
# MANDATORY HANDOFF COLLECTION (PRJ-262)
|
|
53
|
+
# Every subtask MUST provide handoff data before completing.
|
|
54
|
+
# This enables the next subtask to start with full context.
|
|
55
|
+
# ═══════════════════════════════════════════════════════════════
|
|
56
|
+
|
|
57
|
+
GOTO: Step 3.5 (Collect Handoff)
|
|
58
|
+
|
|
59
|
+
# After collecting handoff, mark current subtask as completed:
|
|
48
60
|
currentTask.subtasks[currentSubtaskIndex].status = "completed"
|
|
49
|
-
currentTask.currentSubtaskIndex
|
|
50
|
-
currentTask.subtasks[currentSubtaskIndex].
|
|
51
|
-
|
|
61
|
+
currentTask.subtasks[currentSubtaskIndex].output = "{handoff.output}"
|
|
62
|
+
currentTask.subtasks[currentSubtaskIndex].summary = {
|
|
63
|
+
"title": "{current subtask description}",
|
|
64
|
+
"description": "{what was accomplished}",
|
|
65
|
+
"filesChanged": [{path, action}...],
|
|
66
|
+
"whatWasDone": ["item1", "item2", ...],
|
|
67
|
+
"outputForNextAgent": "{context for next subtask}",
|
|
68
|
+
"notes": "{optional notes}"
|
|
69
|
+
}
|
|
52
70
|
|
|
53
|
-
|
|
71
|
+
IF "Next subtask":
|
|
72
|
+
currentTask.currentSubtaskIndex++
|
|
73
|
+
currentTask.subtasks[currentSubtaskIndex].status = "active"
|
|
74
|
+
currentTask.description = currentTask.subtasks[currentSubtaskIndex].description
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
"""
|
|
57
|
-
✅ Subtask complete: {completed subtask}
|
|
76
|
+
WRITE state.json
|
|
58
77
|
|
|
59
|
-
|
|
78
|
+
# Show previous subtask handoff to establish context
|
|
79
|
+
OUTPUT:
|
|
80
|
+
"""
|
|
81
|
+
✅ Subtask complete: {completed subtask}
|
|
60
82
|
|
|
61
|
-
|
|
83
|
+
Progress: {completed}/{total} subtasks
|
|
62
84
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
STOP
|
|
85
|
+
### Handoff
|
|
86
|
+
{outputForNextAgent}
|
|
66
87
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
Current: {next subtask description}
|
|
89
|
+
|
|
90
|
+
Next: Continue working, then `p. done`
|
|
91
|
+
"""
|
|
92
|
+
STOP
|
|
93
|
+
|
|
94
|
+
# If "Complete all" - fall through to complete task (handoff still collected)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Step 3.5: Collect Handoff (MANDATORY for subtask completion)
|
|
98
|
+
|
|
99
|
+
**⛔ DO NOT skip this step. Every subtask completion MUST include handoff data.**
|
|
70
100
|
|
|
71
|
-
|
|
101
|
+
The LLM should analyze the work done during this subtask and produce:
|
|
102
|
+
|
|
103
|
+
### 1. Get Files Changed
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Files changed during this subtask (uncommitted + recent commits on branch)
|
|
107
|
+
git diff --name-only HEAD 2>/dev/null
|
|
108
|
+
git diff --name-only --cached 2>/dev/null
|
|
72
109
|
```
|
|
73
110
|
|
|
111
|
+
Categorize each file as `created`, `modified`, or `deleted`.
|
|
112
|
+
|
|
113
|
+
### 2. Summarize Work Done
|
|
114
|
+
|
|
115
|
+
Based on the code changes and task context, produce:
|
|
116
|
+
- **whatWasDone**: Array of 1-5 bullet points describing key accomplishments
|
|
117
|
+
- **outputForNextAgent**: A paragraph explaining context the next subtask needs:
|
|
118
|
+
- What was built/changed and why
|
|
119
|
+
- Key decisions made and their rationale
|
|
120
|
+
- Any patterns established that subsequent work should follow
|
|
121
|
+
- Known issues or edge cases to watch for
|
|
122
|
+
|
|
123
|
+
### 3. Validation
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
IF whatWasDone is empty:
|
|
127
|
+
⛔ STOP. At least one item is required.
|
|
128
|
+
Re-analyze the work and provide at minimum 1 bullet point.
|
|
129
|
+
|
|
130
|
+
IF outputForNextAgent is empty:
|
|
131
|
+
⛔ STOP. Context for next subtask is required.
|
|
132
|
+
Even if this is the last subtask, provide a summary for the done/ship step.
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 4. Store Handoff
|
|
136
|
+
|
|
137
|
+
The handoff data is stored in the subtask's `summary` field in state.json.
|
|
138
|
+
This data persists across sessions and feeds into the next subtask's prompt context.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
74
142
|
## Step 4: Complete Task
|
|
75
143
|
|
|
76
144
|
Generate timestamp:
|