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.
@@ -77,6 +77,12 @@ class SyncService {
77
77
  private projectId: string | null = null
78
78
  private globalPath: string = ''
79
79
  private cliVersion: string = '0.0.0'
80
+ /** Task feedback context for agent generation (PRJ-272) */
81
+ private taskFeedbackContext?: {
82
+ patternsDiscovered: string[]
83
+ knownGotchas: string[]
84
+ agentAccuracy: Array<{ agent: string; rating: string; note?: string }>
85
+ }
80
86
 
81
87
  constructor() {
82
88
  this.projectPath = process.cwd()
@@ -246,9 +252,32 @@ class SyncService {
246
252
  }
247
253
 
248
254
  // 4. Generate all files (depends on gathered data)
255
+ // Load task feedback for agent generation (PRJ-272)
256
+ let taskFeedbackContext:
257
+ | {
258
+ patternsDiscovered: string[]
259
+ knownGotchas: string[]
260
+ agentAccuracy: Array<{ agent: string; rating: string; note?: string }>
261
+ }
262
+ | undefined
263
+ if (shouldRegenerateAgents) {
264
+ try {
265
+ const feedback = await stateStorage.getAggregatedFeedback(this.projectId!)
266
+ if (
267
+ feedback.patternsDiscovered.length > 0 ||
268
+ feedback.knownGotchas.length > 0 ||
269
+ feedback.agentAccuracy.length > 0
270
+ ) {
271
+ taskFeedbackContext = feedback
272
+ }
273
+ } catch {
274
+ // Feedback loading failure should not block agent generation
275
+ }
276
+ }
277
+
249
278
  // Skip agent regeneration if nothing structural changed
250
279
  const agents = shouldRegenerateAgents
251
- ? await this.generateAgents(stack, stats)
280
+ ? await this.generateAgents(stack, stats, taskFeedbackContext)
252
281
  : await this.loadExistingAgents()
253
282
  const skills = this.configureSkills(agents)
254
283
  const skillsInstalled = shouldRegenerateAgents ? await this.autoInstallSkills(agents) : []
@@ -658,8 +687,14 @@ class SyncService {
658
687
 
659
688
  private async generateAgents(
660
689
  stack: StackDetection,
661
- stats: ProjectStats
690
+ stats: ProjectStats,
691
+ feedbackContext?: {
692
+ patternsDiscovered: string[]
693
+ knownGotchas: string[]
694
+ agentAccuracy: Array<{ agent: string; rating: string; note?: string }>
695
+ }
662
696
  ): Promise<SyncAgentInfo[]> {
697
+ this.taskFeedbackContext = feedbackContext
663
698
  const agents: SyncAgentInfo[] = []
664
699
  const agentsPath = path.join(this.globalPath, 'agents')
665
700
 
@@ -837,9 +872,58 @@ class SyncService {
837
872
  content = this.generateMinimalDomainAgent(name, stats, stack)
838
873
  }
839
874
 
875
+ // Inject task feedback learnings (PRJ-272)
876
+ content = this.injectFeedbackSection(content, name)
877
+
840
878
  await fs.writeFile(path.join(agentsPath, `${name}.md`), content, 'utf-8')
841
879
  }
842
880
 
881
+ /**
882
+ * Inject a "Recent Learnings" section into agent content from task feedback (PRJ-272)
883
+ */
884
+ private injectFeedbackSection(content: string, agentName: string): string {
885
+ if (!this.taskFeedbackContext) return content
886
+
887
+ const { patternsDiscovered, knownGotchas, agentAccuracy } = this.taskFeedbackContext
888
+
889
+ const agentNotes = agentAccuracy.filter(
890
+ (a) => a.agent === `${agentName}.md` || a.agent === agentName
891
+ )
892
+
893
+ const hasContent =
894
+ patternsDiscovered.length > 0 || knownGotchas.length > 0 || agentNotes.length > 0
895
+ if (!hasContent) return content
896
+
897
+ const lines: string[] = ['\n## Recent Learnings (from completed tasks)\n']
898
+
899
+ if (patternsDiscovered.length > 0) {
900
+ lines.push('### Discovered Patterns')
901
+ for (const pattern of patternsDiscovered) {
902
+ lines.push(`- ${pattern}`)
903
+ }
904
+ lines.push('')
905
+ }
906
+
907
+ if (knownGotchas.length > 0) {
908
+ lines.push('### Known Gotchas')
909
+ for (const gotcha of knownGotchas) {
910
+ lines.push(`- ${gotcha}`)
911
+ }
912
+ lines.push('')
913
+ }
914
+
915
+ if (agentNotes.length > 0) {
916
+ lines.push('### Agent Accuracy Notes')
917
+ for (const note of agentNotes) {
918
+ const desc = note.note ? ` — ${note.note}` : ''
919
+ lines.push(`- ${note.rating}${desc}`)
920
+ }
921
+ lines.push('')
922
+ }
923
+
924
+ return content + lines.join('\n')
925
+ }
926
+
843
927
  private generateMinimalWorkflowAgent(name: string): string {
844
928
  const descriptions: Record<string, string> = {
845
929
  'prjct-workflow': 'Task lifecycle: now, done, pause, resume',
@@ -1253,6 +1337,7 @@ You are the ${name} expert for this project. Apply best practices for the detect
1253
1337
  /**
1254
1338
  * Save sync results as a draft analysis.
1255
1339
  * Preserves existing sealed analysis — only the draft is overwritten.
1340
+ * Incorporates task feedback from completed tasks (PRJ-272).
1256
1341
  */
1257
1342
  private async saveDraftAnalysis(
1258
1343
  git: GitData,
@@ -1262,14 +1347,40 @@ You are the ${name} expert for this project. Apply best practices for the detect
1262
1347
  try {
1263
1348
  const commitHash = git.recentCommits[0]?.hash || null
1264
1349
 
1350
+ // Load aggregated feedback from completed tasks (PRJ-272)
1351
+ let patterns: Array<{ name: string; description: string; location?: string }> = []
1352
+ let antiPatterns: Array<{ issue: string; file: string; suggestion: string }> = []
1353
+ try {
1354
+ const feedback = await stateStorage.getAggregatedFeedback(this.projectId!)
1355
+
1356
+ // Convert discovered patterns to CodePattern objects
1357
+ if (feedback.patternsDiscovered.length > 0) {
1358
+ patterns = feedback.patternsDiscovered.map((p) => ({
1359
+ name: p,
1360
+ description: `Discovered during task execution: ${p}`,
1361
+ }))
1362
+ }
1363
+
1364
+ // Convert known gotchas (recurring issues) to AntiPattern objects
1365
+ if (feedback.knownGotchas.length > 0) {
1366
+ antiPatterns = feedback.knownGotchas.map((g) => ({
1367
+ issue: g,
1368
+ file: 'multiple',
1369
+ suggestion: `Recurring issue reported across tasks: ${g}`,
1370
+ }))
1371
+ }
1372
+ } catch {
1373
+ // Feedback aggregation failure should not block analysis
1374
+ }
1375
+
1265
1376
  await analysisStorage.saveDraft(this.projectId!, {
1266
1377
  projectId: this.projectId!,
1267
1378
  languages: stats.languages,
1268
1379
  frameworks: stats.frameworks,
1269
1380
  configFiles: [],
1270
1381
  fileCount: stats.fileCount,
1271
- patterns: [],
1272
- antiPatterns: [],
1382
+ patterns,
1383
+ antiPatterns,
1273
1384
  analyzedAt: dateHelper.getTimestamp(),
1274
1385
  status: 'draft',
1275
1386
  commitHash: commitHash ?? undefined,
@@ -15,6 +15,8 @@ import type {
15
15
  StateJson,
16
16
  Subtask,
17
17
  SubtaskCompletionData,
18
+ TaskFeedback,
19
+ TaskHistoryEntry,
18
20
  } from '../schemas/state'
19
21
  import { StateJsonSchema, SubtaskCompletionDataSchema } from '../schemas/state'
20
22
  import { getTimestamp, toRelative } from '../utils/date-helper'
@@ -33,6 +35,8 @@ class StateStorage extends StorageManager<StateJson> {
33
35
  return {
34
36
  currentTask: null,
35
37
  previousTask: null,
38
+ pausedTasks: [],
39
+ taskHistory: [],
36
40
  lastUpdated: '',
37
41
  }
38
42
  }
@@ -140,6 +144,39 @@ class StateStorage extends StorageManager<StateJson> {
140
144
  })
141
145
  m.blank().italic('Use /p:resume to continue')
142
146
  })
147
+ .when((data.taskHistory?.length || 0) > 0, (m) => {
148
+ const history = this.getTaskHistoryFromState(data)
149
+ if (history.length === 0) return
150
+
151
+ // Filter by current task classification if available
152
+ const currentTaskType = (data.currentTask as any)?.type
153
+ const relevantHistory = currentTaskType
154
+ ? history.filter((h) => h.classification === currentTaskType).slice(0, 3)
155
+ : history.slice(0, 5)
156
+
157
+ if (relevantHistory.length === 0) return
158
+
159
+ m.hr().h2(
160
+ currentTaskType
161
+ ? `Recent ${currentTaskType} tasks (${relevantHistory.length})`
162
+ : `Recent tasks (${relevantHistory.length})`
163
+ )
164
+ relevantHistory.forEach((entry, i) => {
165
+ m.raw(`${i + 1}. **${entry.title}** (${entry.classification})`)
166
+ .raw(
167
+ ` Completed: ${toRelative(entry.completedAt)} | ${entry.subtaskCount} subtask${entry.subtaskCount > 1 ? 's' : ''}`
168
+ )
169
+ .raw(` Outcome: ${entry.outcome}`)
170
+ if (entry.linearId) m.raw(` Linear: ${entry.linearId}`)
171
+ if (entry.feedback?.patternsDiscovered?.length) {
172
+ m.raw(` Patterns: ${entry.feedback.patternsDiscovered.join(', ')}`)
173
+ }
174
+ if (entry.feedback?.issuesEncountered?.length) {
175
+ m.raw(` Gotchas: ${entry.feedback.issuesEncountered.join(', ')}`)
176
+ }
177
+ })
178
+ m.blank().italic('Task history helps identify patterns and improve decisions')
179
+ })
143
180
  .blank()
144
181
  .build()
145
182
  }
@@ -220,8 +257,10 @@ class StateStorage extends StorageManager<StateJson> {
220
257
 
221
258
  /**
222
259
  * Complete current task
260
+ * Creates a TaskHistoryEntry and adds it to taskHistory with FIFO eviction
261
+ * Optionally accepts structured feedback for the task-to-analysis feedback loop (PRJ-272)
223
262
  */
224
- async completeTask(projectId: string): Promise<CurrentTask | null> {
263
+ async completeTask(projectId: string, feedback?: TaskFeedback): Promise<CurrentTask | null> {
225
264
  const state = await this.read(projectId)
226
265
  const completedTask = state.currentTask
227
266
 
@@ -231,10 +270,22 @@ class StateStorage extends StorageManager<StateJson> {
231
270
 
232
271
  this.validateTransition(state, 'done')
233
272
 
273
+ const completedAt = getTimestamp()
274
+
275
+ // Create task history entry for completed task (with optional feedback)
276
+ const historyEntry = this.createTaskHistoryEntry(completedTask, completedAt, feedback)
277
+
278
+ // Get existing task history with backward compatibility
279
+ const existingHistory = this.getTaskHistoryFromState(state)
280
+
281
+ // Add new entry to beginning, enforce max limit with FIFO eviction
282
+ const taskHistory = [historyEntry, ...existingHistory].slice(0, this.maxTaskHistory)
283
+
234
284
  await this.update(projectId, () => ({
235
285
  currentTask: null,
236
286
  previousTask: null,
237
- lastUpdated: getTimestamp(),
287
+ taskHistory,
288
+ lastUpdated: completedAt,
238
289
  }))
239
290
 
240
291
  // Publish incremental event
@@ -242,15 +293,64 @@ class StateStorage extends StorageManager<StateJson> {
242
293
  taskId: completedTask.id,
243
294
  description: completedTask.description,
244
295
  startedAt: completedTask.startedAt,
245
- completedAt: getTimestamp(),
296
+ completedAt,
246
297
  })
247
298
 
248
299
  return completedTask
249
300
  }
250
301
 
302
+ /**
303
+ * Create a TaskHistoryEntry from a completed task
304
+ * Optionally includes structured feedback for the feedback loop (PRJ-272)
305
+ */
306
+ private createTaskHistoryEntry(
307
+ task: CurrentTask,
308
+ completedAt: string,
309
+ feedback?: TaskFeedback
310
+ ): TaskHistoryEntry {
311
+ // Extended task properties (may be present in storage but not in schema)
312
+ const taskAny = task as any
313
+
314
+ // Extract subtask summaries (only completed subtasks with summaries)
315
+ const subtaskSummaries = (task.subtasks || [])
316
+ .filter((st) => st.status === 'completed' && st.summary)
317
+ .map((st) => st.summary!)
318
+
319
+ // Calculate outcome description from subtask summaries
320
+ const outcome =
321
+ subtaskSummaries.length > 0
322
+ ? subtaskSummaries.map((s) => s.title).join(', ')
323
+ : 'Task completed'
324
+
325
+ const entry: TaskHistoryEntry = {
326
+ taskId: task.id,
327
+ title: taskAny.parentDescription || task.description,
328
+ classification: taskAny.type || 'improvement',
329
+ startedAt: task.startedAt,
330
+ completedAt,
331
+ subtaskCount: task.subtasks?.length || 0,
332
+ subtaskSummaries,
333
+ outcome,
334
+ branchName: taskAny.branch || 'unknown',
335
+ linearId: task.linearId,
336
+ linearUuid: task.linearUuid,
337
+ prUrl: taskAny.prUrl,
338
+ }
339
+
340
+ // Attach feedback if provided (PRJ-272)
341
+ if (feedback) {
342
+ entry.feedback = feedback
343
+ }
344
+
345
+ return entry
346
+ }
347
+
251
348
  /** Max number of paused tasks (configurable) */
252
349
  private maxPausedTasks = 5
253
350
 
351
+ /** Max number of task history entries (configurable) */
352
+ private maxTaskHistory = 20
353
+
254
354
  /** Staleness threshold in days */
255
355
  private stalenessThresholdDays = 30
256
356
 
@@ -367,6 +467,14 @@ class StateStorage extends StorageManager<StateJson> {
367
467
  return paused
368
468
  }
369
469
 
470
+ /**
471
+ * Get task history from state with backward compatibility
472
+ * Ensures taskHistory is always an array (never undefined)
473
+ */
474
+ private getTaskHistoryFromState(state: StateJson): TaskHistoryEntry[] {
475
+ return state.taskHistory || []
476
+ }
477
+
370
478
  /**
371
479
  * Get stale paused tasks (older than threshold)
372
480
  */
@@ -462,6 +570,85 @@ class StateStorage extends StorageManager<StateJson> {
462
570
  return this.getPausedTasksFromState(state)
463
571
  }
464
572
 
573
+ /**
574
+ * Get full task history (completed tasks)
575
+ */
576
+ async getTaskHistory(projectId: string): Promise<TaskHistoryEntry[]> {
577
+ const state = await this.read(projectId)
578
+ return this.getTaskHistoryFromState(state)
579
+ }
580
+
581
+ /**
582
+ * Get most recent task from history
583
+ */
584
+ async getMostRecentTask(projectId: string): Promise<TaskHistoryEntry | null> {
585
+ const state = await this.read(projectId)
586
+ const history = this.getTaskHistoryFromState(state)
587
+ return history[0] || null
588
+ }
589
+
590
+ /**
591
+ * Get task history filtered by classification
592
+ */
593
+ async getTaskHistoryByType(
594
+ projectId: string,
595
+ classification: TaskHistoryEntry['classification']
596
+ ): Promise<TaskHistoryEntry[]> {
597
+ const state = await this.read(projectId)
598
+ const history = this.getTaskHistoryFromState(state)
599
+ return history.filter((t) => t.classification === classification)
600
+ }
601
+
602
+ /**
603
+ * Aggregate feedback from all task history entries (PRJ-272)
604
+ * Used by sync to feed task discoveries back into analysis and agent generation.
605
+ * Returns consolidated patterns, stack confirmations, issues, and agent accuracy.
606
+ */
607
+ async getAggregatedFeedback(projectId: string): Promise<{
608
+ stackConfirmed: string[]
609
+ patternsDiscovered: string[]
610
+ agentAccuracy: Array<{ agent: string; rating: string; note?: string }>
611
+ issuesEncountered: string[]
612
+ knownGotchas: string[]
613
+ }> {
614
+ const history = await this.getTaskHistory(projectId)
615
+ const entriesWithFeedback = history.filter((h) => h.feedback)
616
+
617
+ const stackConfirmed: string[] = []
618
+ const patternsDiscovered: string[] = []
619
+ const agentAccuracy: Array<{ agent: string; rating: string; note?: string }> = []
620
+ const allIssues: string[] = []
621
+
622
+ for (const entry of entriesWithFeedback) {
623
+ const fb = entry.feedback!
624
+ if (fb.stackConfirmed) stackConfirmed.push(...fb.stackConfirmed)
625
+ if (fb.patternsDiscovered) patternsDiscovered.push(...fb.patternsDiscovered)
626
+ if (fb.agentAccuracy) agentAccuracy.push(...fb.agentAccuracy)
627
+ if (fb.issuesEncountered) allIssues.push(...fb.issuesEncountered)
628
+ }
629
+
630
+ // Deduplicate patterns and stack confirmations
631
+ const uniqueStack = [...new Set(stackConfirmed)]
632
+ const uniquePatterns = [...new Set(patternsDiscovered)]
633
+
634
+ // Promote recurring issues (2+) to known gotchas
635
+ const issueCounts = new Map<string, number>()
636
+ for (const issue of allIssues) {
637
+ issueCounts.set(issue, (issueCounts.get(issue) || 0) + 1)
638
+ }
639
+ const knownGotchas = [...issueCounts.entries()]
640
+ .filter(([_, count]) => count >= 2)
641
+ .map(([issue]) => issue)
642
+
643
+ return {
644
+ stackConfirmed: uniqueStack,
645
+ patternsDiscovered: uniquePatterns,
646
+ agentAccuracy,
647
+ issuesEncountered: [...new Set(allIssues)],
648
+ knownGotchas,
649
+ }
650
+ }
651
+
465
652
  // =========== Subtask Methods ===========
466
653
 
467
654
  /**