prjct-cli 1.21.0 → 1.22.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,469 @@
1
+ /**
2
+ * State Storage Task History Tests
3
+ *
4
+ * Tests for task history functionality in StateStorage:
5
+ * - Task history push on completion
6
+ * - FIFO eviction (max 20 entries)
7
+ * - Backward compatibility (undefined taskHistory)
8
+ * - Accessor methods (getTaskHistory, getMostRecentTask, getTaskHistoryByType)
9
+ * - Context injection (markdown generation with filtering)
10
+ */
11
+
12
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
13
+ import fs from 'node:fs/promises'
14
+ import os from 'node:os'
15
+ import path from 'node:path'
16
+ import pathManager from '../../infrastructure/path-manager'
17
+ import type { CurrentTask, StateJson } from '../../schemas/state'
18
+ import { prjctDb } from '../../storage/database'
19
+ import { stateStorage } from '../../storage/state-storage'
20
+
21
+ // =============================================================================
22
+ // Test Setup
23
+ // =============================================================================
24
+
25
+ let tmpRoot: string | null = null
26
+ let testProjectId: string
27
+
28
+ // Mock pathManager to use temp directory
29
+ const originalGetGlobalProjectPath = pathManager.getGlobalProjectPath.bind(pathManager)
30
+ const originalGetStoragePath = pathManager.getStoragePath.bind(pathManager)
31
+ const originalGetFilePath = pathManager.getFilePath.bind(pathManager)
32
+
33
+ beforeEach(async () => {
34
+ // Create temp directory for test isolation
35
+ tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'prjct-history-test-'))
36
+ testProjectId = `test-history-${Date.now()}`
37
+
38
+ // Mock pathManager to use temp directory
39
+ pathManager.getGlobalProjectPath = (projectId: string) => {
40
+ return path.join(tmpRoot!, projectId)
41
+ }
42
+
43
+ pathManager.getStoragePath = (projectId: string, filename: string) => {
44
+ return path.join(tmpRoot!, projectId, 'storage', filename)
45
+ }
46
+
47
+ pathManager.getFilePath = (projectId: string, layer: string, filename: string) => {
48
+ return path.join(tmpRoot!, projectId, layer, filename)
49
+ }
50
+
51
+ // Create storage and sync directories
52
+ const storagePath = pathManager.getStoragePath(testProjectId, '')
53
+ await fs.mkdir(storagePath, { recursive: true })
54
+
55
+ const syncPath = path.join(tmpRoot!, testProjectId, 'sync')
56
+ await fs.mkdir(syncPath, { recursive: true })
57
+ })
58
+
59
+ afterEach(async () => {
60
+ // Close SQLite connections before cleanup
61
+ prjctDb.close()
62
+
63
+ // Restore original pathManager methods
64
+ pathManager.getGlobalProjectPath = originalGetGlobalProjectPath
65
+ pathManager.getStoragePath = originalGetStoragePath
66
+ pathManager.getFilePath = originalGetFilePath
67
+
68
+ // Clean up temp directory
69
+ if (tmpRoot) {
70
+ await fs.rm(tmpRoot, { recursive: true, force: true })
71
+ tmpRoot = null
72
+ }
73
+ })
74
+
75
+ // =============================================================================
76
+ // Helper Functions
77
+ // =============================================================================
78
+
79
+ /**
80
+ * Create a mock task for testing
81
+ */
82
+ function createMockTask(
83
+ overrides: Partial<CurrentTask> & Record<string, unknown> = {}
84
+ ): CurrentTask {
85
+ return {
86
+ id: `task-${Date.now()}`,
87
+ description: 'Test task',
88
+ startedAt: new Date().toISOString(),
89
+ sessionId: `session-${Date.now()}`,
90
+ ...overrides,
91
+ } as CurrentTask
92
+ }
93
+
94
+ /**
95
+ * Complete a task and return the state
96
+ */
97
+ async function completeTaskAndGetState(projectId: string, task: CurrentTask): Promise<StateJson> {
98
+ // Start the task
99
+ await stateStorage.startTask(projectId, task)
100
+
101
+ // Complete the task
102
+ await stateStorage.completeTask(projectId)
103
+
104
+ // Return the state
105
+ return await stateStorage.read(projectId)
106
+ }
107
+
108
+ // =============================================================================
109
+ // Tests: Task History Push on Completion
110
+ // =============================================================================
111
+
112
+ describe('Task History - Push on Completion', () => {
113
+ it('should add completed task to taskHistory array', async () => {
114
+ const task = createMockTask({
115
+ description: 'Test task 1',
116
+ })
117
+
118
+ const state = await completeTaskAndGetState(testProjectId, task)
119
+
120
+ expect(state.taskHistory).toBeDefined()
121
+ expect(state.taskHistory?.length).toBe(1)
122
+ expect(state.taskHistory![0].title).toBe('Test task 1')
123
+ })
124
+
125
+ it('should include all required metadata in history entry', async () => {
126
+ const task = createMockTask({
127
+ description: 'Test task with metadata',
128
+ linearId: 'PRJ-123',
129
+ linearUuid: 'uuid-123',
130
+ })
131
+
132
+ const state = await completeTaskAndGetState(testProjectId, task)
133
+
134
+ const entry = state.taskHistory![0]
135
+ expect(entry.taskId).toBe(task.id)
136
+ expect(entry.title).toBe('Test task with metadata')
137
+ expect(entry.startedAt).toBeDefined() // startedAt is set by startTask()
138
+ expect(entry.completedAt).toBeDefined()
139
+ expect(entry.subtaskCount).toBe(0)
140
+ expect(entry.subtaskSummaries).toEqual([])
141
+ expect(entry.outcome).toBe('Task completed')
142
+ expect(entry.linearId).toBe('PRJ-123')
143
+ expect(entry.linearUuid).toBe('uuid-123')
144
+ })
145
+
146
+ it('should extract subtask summaries from completed task', async () => {
147
+ const task = createMockTask({
148
+ description: 'Task with subtasks',
149
+ subtasks: [
150
+ {
151
+ id: 'subtask-1',
152
+ description: 'First subtask',
153
+ domain: 'backend',
154
+ agent: 'backend.md',
155
+ status: 'completed',
156
+ dependsOn: [],
157
+ summary: {
158
+ title: 'Subtask 1 Complete',
159
+ description: 'Did some work',
160
+ filesChanged: [{ path: 'file1.ts', action: 'modified' }],
161
+ whatWasDone: ['Made changes'],
162
+ outputForNextAgent: 'Ready for next step',
163
+ },
164
+ },
165
+ {
166
+ id: 'subtask-2',
167
+ description: 'Second subtask',
168
+ domain: 'backend',
169
+ agent: 'backend.md',
170
+ status: 'pending',
171
+ dependsOn: [],
172
+ },
173
+ ],
174
+ })
175
+
176
+ const state = await completeTaskAndGetState(testProjectId, task)
177
+
178
+ const entry = state.taskHistory![0]
179
+ expect(entry.subtaskCount).toBe(2)
180
+ expect(entry.subtaskSummaries.length).toBe(1) // Only completed with summary
181
+ expect(entry.subtaskSummaries[0].title).toBe('Subtask 1 Complete')
182
+ })
183
+
184
+ it('should preserve order - newest entries first', async () => {
185
+ // Complete 3 tasks
186
+ for (let i = 1; i <= 3; i++) {
187
+ const task = createMockTask({
188
+ description: `Task ${i}`,
189
+ })
190
+ await completeTaskAndGetState(testProjectId, task)
191
+ }
192
+
193
+ const state = await stateStorage.read(testProjectId)
194
+
195
+ expect(state.taskHistory?.length).toBe(3)
196
+ expect(state.taskHistory![0].title).toBe('Task 3') // Newest first
197
+ expect(state.taskHistory![1].title).toBe('Task 2')
198
+ expect(state.taskHistory![2].title).toBe('Task 1') // Oldest last
199
+ })
200
+ })
201
+
202
+ // =============================================================================
203
+ // Tests: FIFO Eviction
204
+ // =============================================================================
205
+
206
+ describe('Task History - FIFO Eviction', () => {
207
+ it('should enforce max 20 entries', async () => {
208
+ // Complete 25 tasks
209
+ for (let i = 1; i <= 25; i++) {
210
+ const task = createMockTask({
211
+ description: `Task ${i}`,
212
+ })
213
+ await completeTaskAndGetState(testProjectId, task)
214
+ }
215
+
216
+ const state = await stateStorage.read(testProjectId)
217
+
218
+ expect(state.taskHistory?.length).toBe(20) // Max 20
219
+ })
220
+
221
+ it('should drop oldest entries when exceeding limit', async () => {
222
+ // Complete 22 tasks
223
+ for (let i = 1; i <= 22; i++) {
224
+ const task = createMockTask({
225
+ description: `Task ${i}`,
226
+ })
227
+ await completeTaskAndGetState(testProjectId, task)
228
+ }
229
+
230
+ const state = await stateStorage.read(testProjectId)
231
+
232
+ expect(state.taskHistory?.length).toBe(20)
233
+ expect(state.taskHistory![0].title).toBe('Task 22') // Newest
234
+ expect(state.taskHistory![19].title).toBe('Task 3') // Oldest kept
235
+ // Task 1 and Task 2 should be dropped
236
+ })
237
+ })
238
+
239
+ // =============================================================================
240
+ // Tests: Backward Compatibility
241
+ // =============================================================================
242
+
243
+ describe('Task History - Backward Compatibility', () => {
244
+ it('should initialize taskHistory as empty array when undefined', async () => {
245
+ // Read state before any tasks (should use default)
246
+ const state = await stateStorage.read(testProjectId)
247
+
248
+ expect(state.taskHistory).toBeDefined()
249
+ expect(Array.isArray(state.taskHistory)).toBe(true)
250
+ expect(state.taskHistory?.length).toBe(0)
251
+ })
252
+
253
+ it('should handle missing taskHistory field in existing state', async () => {
254
+ // Manually write state without taskHistory field
255
+ const stateFile = pathManager.getStoragePath(testProjectId, 'state.json')
256
+
257
+ await fs.writeFile(
258
+ stateFile,
259
+ JSON.stringify({
260
+ currentTask: null,
261
+ pausedTasks: [],
262
+ lastUpdated: new Date().toISOString(),
263
+ // No taskHistory field
264
+ })
265
+ )
266
+
267
+ // Complete a task - should work without error
268
+ const task = createMockTask({ description: 'New task' })
269
+ const state = await completeTaskAndGetState(testProjectId, task)
270
+
271
+ expect(state.taskHistory).toBeDefined()
272
+ expect(state.taskHistory?.length).toBe(1)
273
+ })
274
+ })
275
+
276
+ // =============================================================================
277
+ // Tests: Accessor Methods
278
+ // =============================================================================
279
+
280
+ describe('Task History - Accessor Methods', () => {
281
+ beforeEach(async () => {
282
+ // Setup: Complete 3 tasks with different classifications
283
+ const tasks = [
284
+ { description: 'Feature task 1', type: 'feature' },
285
+ { description: 'Bug fix', type: 'bug' },
286
+ { description: 'Feature task 2', type: 'feature' },
287
+ ]
288
+
289
+ for (const taskData of tasks) {
290
+ const task = createMockTask(taskData)
291
+ await completeTaskAndGetState(testProjectId, task)
292
+ }
293
+ })
294
+
295
+ it('getTaskHistory() should return full history', async () => {
296
+ const history = await stateStorage.getTaskHistory(testProjectId)
297
+
298
+ expect(history.length).toBe(3)
299
+ expect(history[0].title).toBe('Feature task 2') // Newest first
300
+ })
301
+
302
+ it('getMostRecentTask() should return latest entry', async () => {
303
+ const recent = await stateStorage.getMostRecentTask(testProjectId)
304
+
305
+ expect(recent).not.toBeNull()
306
+ expect(recent!.title).toBe('Feature task 2')
307
+ })
308
+
309
+ it('getMostRecentTask() should return null when no history', async () => {
310
+ const emptyProjectId = `empty-${Date.now()}`
311
+
312
+ // Create storage directory for empty project
313
+ const storagePath = pathManager.getStoragePath(emptyProjectId, '')
314
+ await fs.mkdir(storagePath, { recursive: true })
315
+
316
+ const recent = await stateStorage.getMostRecentTask(emptyProjectId)
317
+
318
+ expect(recent).toBeNull()
319
+ })
320
+
321
+ it('getTaskHistoryByType() should filter by classification', async () => {
322
+ const featureHistory = await stateStorage.getTaskHistoryByType(testProjectId, 'feature')
323
+
324
+ expect(featureHistory.length).toBe(2)
325
+ expect(featureHistory.every((h) => h.classification === 'feature')).toBe(true)
326
+ })
327
+
328
+ it('getTaskHistoryByType() should return empty array for no matches', async () => {
329
+ const choreHistory = await stateStorage.getTaskHistoryByType(testProjectId, 'chore')
330
+
331
+ expect(choreHistory.length).toBe(0)
332
+ })
333
+ })
334
+
335
+ // =============================================================================
336
+ // Tests: Context Injection (Markdown Generation)
337
+ // =============================================================================
338
+
339
+ describe('Task History - Context Injection', () => {
340
+ it('should include task history section in markdown when history exists', async () => {
341
+ // Complete a task
342
+ const task = createMockTask({ description: 'Completed task' })
343
+ await completeTaskAndGetState(testProjectId, task)
344
+
345
+ // Generate markdown
346
+ const state = await stateStorage.read(testProjectId)
347
+ const markdown = (stateStorage as any).toMarkdown(state)
348
+
349
+ expect(markdown).toContain('Recent tasks')
350
+ expect(markdown).toContain('Completed task')
351
+ expect(markdown).toContain('Task history helps identify patterns')
352
+ })
353
+
354
+ it('should filter by current task classification when task is active', async () => {
355
+ // Complete several tasks
356
+ const tasks = [
357
+ { description: 'Feature 1', type: 'feature' },
358
+ { description: 'Bug 1', type: 'bug' },
359
+ { description: 'Feature 2', type: 'feature' },
360
+ ]
361
+
362
+ for (const taskData of tasks) {
363
+ const task = createMockTask(taskData)
364
+ await completeTaskAndGetState(testProjectId, task)
365
+ }
366
+
367
+ // Start a new bug task (should filter history to bugs only)
368
+ const currentTask = createMockTask({ description: 'Current bug', type: 'bug' })
369
+ await stateStorage.startTask(testProjectId, currentTask)
370
+
371
+ const state = await stateStorage.read(testProjectId)
372
+ const markdown = (stateStorage as any).toMarkdown(state)
373
+
374
+ expect(markdown).toContain('Recent bug tasks')
375
+ expect(markdown).toContain('Bug 1')
376
+ expect(markdown).not.toContain('Feature 1')
377
+ })
378
+
379
+ it('should show max 5 recent tasks when no current task', async () => {
380
+ // Complete 7 tasks
381
+ for (let i = 1; i <= 7; i++) {
382
+ const task = createMockTask({ description: `Task ${i}` })
383
+ await completeTaskAndGetState(testProjectId, task)
384
+ }
385
+
386
+ const state = await stateStorage.read(testProjectId)
387
+ const markdown = (stateStorage as any).toMarkdown(state)
388
+
389
+ expect(markdown).toContain('Recent tasks (5)')
390
+ expect(markdown).toContain('Task 7')
391
+ expect(markdown).toContain('Task 3')
392
+ expect(markdown).not.toContain('Task 2') // Too old
393
+ })
394
+
395
+ it('should show max 3 entries of same type when task is active', async () => {
396
+ // Complete 5 feature tasks
397
+ for (let i = 1; i <= 5; i++) {
398
+ const task = createMockTask({ description: `Feature ${i}`, type: 'feature' })
399
+ await completeTaskAndGetState(testProjectId, task)
400
+ }
401
+
402
+ // Start a new feature task
403
+ const currentTask = createMockTask({ description: 'Current feature', type: 'feature' })
404
+ await stateStorage.startTask(testProjectId, currentTask)
405
+
406
+ const state = await stateStorage.read(testProjectId)
407
+ const markdown = (stateStorage as any).toMarkdown(state)
408
+
409
+ expect(markdown).toContain('Recent feature tasks (3)')
410
+ expect(markdown).toContain('Feature 5')
411
+ expect(markdown).toContain('Feature 4')
412
+ expect(markdown).toContain('Feature 3')
413
+ expect(markdown).not.toContain('Feature 2') // Too old (beyond 3)
414
+ })
415
+
416
+ it('should not show history section when taskHistory is empty', async () => {
417
+ const state = await stateStorage.read(testProjectId)
418
+ const markdown = (stateStorage as any).toMarkdown(state)
419
+
420
+ expect(markdown).not.toContain('Recent tasks')
421
+ expect(markdown).not.toContain('Task history helps identify patterns')
422
+ })
423
+
424
+ it('should include completion time and subtask count', async () => {
425
+ const task = createMockTask({
426
+ description: 'Task with details',
427
+ subtasks: [
428
+ {
429
+ id: 'st-1',
430
+ description: 'Subtask 1',
431
+ domain: 'backend',
432
+ agent: 'backend.md',
433
+ status: 'completed',
434
+ dependsOn: [],
435
+ },
436
+ {
437
+ id: 'st-2',
438
+ description: 'Subtask 2',
439
+ domain: 'backend',
440
+ agent: 'backend.md',
441
+ status: 'completed',
442
+ dependsOn: [],
443
+ },
444
+ ],
445
+ })
446
+
447
+ await completeTaskAndGetState(testProjectId, task)
448
+
449
+ const state = await stateStorage.read(testProjectId)
450
+ const markdown = (stateStorage as any).toMarkdown(state)
451
+
452
+ expect(markdown).toContain('2 subtasks')
453
+ expect(markdown).toContain('Completed:')
454
+ })
455
+
456
+ it('should include Linear ID when present', async () => {
457
+ const task = createMockTask({
458
+ description: 'Task with Linear',
459
+ linearId: 'PRJ-456',
460
+ })
461
+
462
+ await completeTaskAndGetState(testProjectId, task)
463
+
464
+ const state = await stateStorage.read(testProjectId)
465
+ const markdown = (stateStorage as any).toMarkdown(state)
466
+
467
+ expect(markdown).toContain('Linear: PRJ-456')
468
+ })
469
+ })
@@ -19,6 +19,7 @@ import {
19
19
  import { linearService } from '../integrations/linear'
20
20
  import outcomeRecorder from '../outcomes/recorder'
21
21
  import { generateUUID } from '../schemas'
22
+ import type { TaskFeedback } from '../schemas/state'
22
23
  import { queueStorage, stateStorage } from '../storage'
23
24
  import type { CommandResult } from '../types'
24
25
  import { getErrorMessage } from '../types/fs'
@@ -174,10 +175,11 @@ export class WorkflowCommands extends PrjctCommandsBase {
174
175
 
175
176
  /**
176
177
  * /p:done - Complete current task
178
+ * Optionally accepts structured feedback for the task-to-analysis feedback loop (PRJ-272)
177
179
  */
178
180
  async done(
179
181
  projectPath: string = process.cwd(),
180
- options: { skipHooks?: boolean } = {}
182
+ options: { skipHooks?: boolean; feedback?: TaskFeedback } = {}
181
183
  ): Promise<CommandResult> {
182
184
  try {
183
185
  const initResult = await this.ensureProjectInit(projectPath)
@@ -249,7 +251,8 @@ export class WorkflowCommands extends PrjctCommandsBase {
249
251
  }
250
252
 
251
253
  // Write-through: Complete task (JSON → MD → Event)
252
- await stateStorage.completeTask(projectId)
254
+ // Pass feedback for the task-to-analysis feedback loop (PRJ-272)
255
+ await stateStorage.completeTask(projectId, options.feedback)
253
256
 
254
257
  // Sync to Linear if task has linearId
255
258
  const linearId = (currentTask as { linearId?: string }).linearId
@@ -111,10 +111,50 @@ export const PreviousTaskSchema = z.object({
111
111
  pauseReason: z.string().optional(),
112
112
  })
113
113
 
114
+ // Task feedback captured during completion (PRJ-272)
115
+ // Enables the task-to-analysis feedback loop: tasks report discoveries back to analysis
116
+ export const TaskFeedbackSchema = z.object({
117
+ // Stack confirmations - tech confirmed/used during the task
118
+ stackConfirmed: z.array(z.string()).optional(), // ["React 18", "TypeScript strict mode"]
119
+ // Patterns discovered during the task
120
+ patternsDiscovered: z.array(z.string()).optional(), // ["API routes follow /api/v1/{resource}"]
121
+ // Agent accuracy - how well domain agents performed
122
+ agentAccuracy: z
123
+ .array(
124
+ z.object({
125
+ agent: z.string(), // "backend.md"
126
+ rating: z.enum(['helpful', 'neutral', 'inaccurate']),
127
+ note: z.string().optional(), // "Missing Tailwind context"
128
+ })
129
+ )
130
+ .optional(),
131
+ // Issues encountered during the task
132
+ issuesEncountered: z.array(z.string()).optional(), // ["ESLint conflicts with Prettier"]
133
+ })
134
+
135
+ // Task history entry for completed tasks
136
+ // Stores historical context to enable pattern learning and cross-task correlation
137
+ export const TaskHistoryEntrySchema = z.object({
138
+ taskId: z.string(), // task UUID
139
+ title: z.string(), // parent task description
140
+ classification: TaskTypeSchema, // feature, bug, improvement, chore
141
+ startedAt: z.string(), // ISO8601
142
+ completedAt: z.string(), // ISO8601
143
+ subtaskCount: z.number(), // total number of subtasks
144
+ subtaskSummaries: z.array(SubtaskSummarySchema), // summary of each subtask
145
+ outcome: z.string(), // brief description of what was accomplished
146
+ branchName: z.string(), // git branch used
147
+ linearId: z.string().optional(), // Linear issue ID if linked
148
+ linearUuid: z.string().optional(), // Linear internal UUID
149
+ prUrl: z.string().optional(), // PR URL if shipped
150
+ feedback: TaskFeedbackSchema.optional(), // Task-to-analysis feedback (PRJ-272)
151
+ })
152
+
114
153
  export const StateJsonSchema = z.object({
115
154
  currentTask: CurrentTaskSchema.nullable(),
116
155
  previousTask: PreviousTaskSchema.nullable().optional(), // deprecated: use pausedTasks
117
156
  pausedTasks: z.array(PreviousTaskSchema).optional(), // replaces previousTask
157
+ taskHistory: z.array(TaskHistoryEntrySchema).optional(), // completed tasks history (max 20)
118
158
  lastUpdated: z.string(),
119
159
  })
120
160
 
@@ -181,6 +221,8 @@ export type SubtaskProgress = z.infer<typeof SubtaskProgressSchema>
181
221
 
182
222
  export type CurrentTask = z.infer<typeof CurrentTaskSchema>
183
223
  export type PreviousTask = z.infer<typeof PreviousTaskSchema>
224
+ export type TaskFeedback = z.infer<typeof TaskFeedbackSchema>
225
+ export type TaskHistoryEntry = z.infer<typeof TaskHistoryEntrySchema>
184
226
  export type StateJson = z.infer<typeof StateJsonSchema>
185
227
  export type QueueTask = z.infer<typeof QueueTaskSchema>
186
228
  export type QueueJson = z.infer<typeof QueueJsonSchema>
@@ -224,6 +266,7 @@ export const validateSubtaskCompletion = (
224
266
  export const DEFAULT_STATE: StateJson = {
225
267
  currentTask: null,
226
268
  pausedTasks: [],
269
+ taskHistory: [],
227
270
  lastUpdated: '',
228
271
  }
229
272
 
@@ -37,6 +37,13 @@ export interface ProjectStats {
37
37
  frameworks: string[]
38
38
  }
39
39
 
40
+ /** Aggregated task feedback for agent generation (PRJ-272) */
41
+ export interface TaskFeedbackContext {
42
+ patternsDiscovered: string[]
43
+ knownGotchas: string[]
44
+ agentAccuracy: Array<{ agent: string; rating: string; note?: string }>
45
+ }
46
+
40
47
  // ============================================================================
41
48
  // AGENT GENERATOR CLASS
42
49
  // ============================================================================
@@ -50,11 +57,20 @@ export class AgentGenerator {
50
57
  this.templatesPath = templatesPath || path.join(__dirname, '..', '..', 'templates', 'subagents')
51
58
  }
52
59
 
60
+ /** Task feedback context for agent generation (PRJ-272) */
61
+ private feedbackContext?: TaskFeedbackContext
62
+
53
63
  /**
54
64
  * Generate all agents based on stack detection
65
+ * Optionally accepts task feedback to influence agent content (PRJ-272)
55
66
  */
56
- async generate(stack: StackDetection, stats: ProjectStats): Promise<AgentInfo[]> {
67
+ async generate(
68
+ stack: StackDetection,
69
+ stats: ProjectStats,
70
+ feedbackContext?: TaskFeedbackContext
71
+ ): Promise<AgentInfo[]> {
57
72
  const agents: AgentInfo[] = []
73
+ this.feedbackContext = feedbackContext
58
74
 
59
75
  // Purge old agents
60
76
  await this.purgeOldAgents()
@@ -227,6 +243,7 @@ export class AgentGenerator {
227
243
 
228
244
  /**
229
245
  * Generate a single domain agent
246
+ * Injects task feedback learnings when available (PRJ-272)
230
247
  */
231
248
  private async generateDomainAgent(
232
249
  name: string,
@@ -248,9 +265,61 @@ export class AgentGenerator {
248
265
  content = this.generateMinimalDomainAgent(name, stats, stack)
249
266
  }
250
267
 
268
+ // Inject task feedback learnings (PRJ-272)
269
+ content = this.injectFeedbackSection(content, name)
270
+
251
271
  await this.writeAgentWithPreservation(`${name}.md`, content)
252
272
  }
253
273
 
274
+ /**
275
+ * Inject a "Recent Learnings" section into agent content from task feedback (PRJ-272)
276
+ * Only injects if there are relevant patterns, gotchas, or agent accuracy notes
277
+ */
278
+ private injectFeedbackSection(content: string, agentName: string): string {
279
+ if (!this.feedbackContext) return content
280
+
281
+ const { patternsDiscovered, knownGotchas, agentAccuracy } = this.feedbackContext
282
+
283
+ // Filter agent accuracy notes relevant to this agent
284
+ const agentNotes = agentAccuracy.filter(
285
+ (a) => a.agent === `${agentName}.md` || a.agent === agentName
286
+ )
287
+
288
+ const hasContent =
289
+ patternsDiscovered.length > 0 || knownGotchas.length > 0 || agentNotes.length > 0
290
+
291
+ if (!hasContent) return content
292
+
293
+ const lines: string[] = ['\n## Recent Learnings (from completed tasks)\n']
294
+
295
+ if (patternsDiscovered.length > 0) {
296
+ lines.push('### Discovered Patterns')
297
+ for (const pattern of patternsDiscovered) {
298
+ lines.push(`- ${pattern}`)
299
+ }
300
+ lines.push('')
301
+ }
302
+
303
+ if (knownGotchas.length > 0) {
304
+ lines.push('### Known Gotchas')
305
+ for (const gotcha of knownGotchas) {
306
+ lines.push(`- ${gotcha}`)
307
+ }
308
+ lines.push('')
309
+ }
310
+
311
+ if (agentNotes.length > 0) {
312
+ lines.push('### Agent Accuracy Notes')
313
+ for (const note of agentNotes) {
314
+ const desc = note.note ? ` — ${note.note}` : ''
315
+ lines.push(`- ${note.rating}${desc}`)
316
+ }
317
+ lines.push('')
318
+ }
319
+
320
+ return content + lines.join('\n')
321
+ }
322
+
254
323
  /**
255
324
  * Generate minimal workflow agent content
256
325
  */