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.
@@ -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
  }
@@ -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().optional(),
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
- SubtaskSummary,
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
- * Returns the next subtask (or null if all complete)
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
- output?: string,
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
  */
@@ -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
  /**
@@ -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().optional(),
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
- * Returns the next subtask (or null if all complete)
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, output, summary) {
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.11.0",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
5
5
  "main": "core/index.ts",
6
6
  "bin": {
@@ -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 "Next subtask":
47
- # Mark current subtask as completed
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].status = "active"
51
- currentTask.description = currentTask.subtasks[currentSubtaskIndex].description
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
- WRITE state.json
71
+ IF "Next subtask":
72
+ currentTask.currentSubtaskIndex++
73
+ currentTask.subtasks[currentSubtaskIndex].status = "active"
74
+ currentTask.description = currentTask.subtasks[currentSubtaskIndex].description
54
75
 
55
- OUTPUT:
56
- """
57
- ✅ Subtask complete: {completed subtask}
76
+ WRITE state.json
58
77
 
59
- Progress: {completed}/{total} subtasks
78
+ # Show previous subtask handoff to establish context
79
+ OUTPUT:
80
+ """
81
+ ✅ Subtask complete: {completed subtask}
60
82
 
61
- Current: {next subtask description}
83
+ Progress: {completed}/{total} subtasks
62
84
 
63
- Next: Continue working, then `p. done`
64
- """
65
- STOP
85
+ ### Handoff
86
+ {outputForNextAgent}
66
87
 
67
- IF "Continue current":
68
- OUTPUT: "Continuing: {current subtask}"
69
- STOP
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
- # If "Complete all" - fall through to complete task
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: