popeye-cli 1.1.0 → 1.2.1

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.
Files changed (150) hide show
  1. package/.env.example +24 -1
  2. package/CONTRIBUTING.md +275 -0
  3. package/OPEN_SOURCE_MANIFESTO.md +172 -0
  4. package/README.md +340 -27
  5. package/dist/adapters/claude.d.ts +28 -2
  6. package/dist/adapters/claude.d.ts.map +1 -1
  7. package/dist/adapters/claude.js +273 -20
  8. package/dist/adapters/claude.js.map +1 -1
  9. package/dist/adapters/grok.d.ts +73 -0
  10. package/dist/adapters/grok.d.ts.map +1 -0
  11. package/dist/adapters/grok.js +430 -0
  12. package/dist/adapters/grok.js.map +1 -0
  13. package/dist/adapters/openai.d.ts +1 -1
  14. package/dist/adapters/openai.d.ts.map +1 -1
  15. package/dist/adapters/openai.js +6 -1
  16. package/dist/adapters/openai.js.map +1 -1
  17. package/dist/auth/grok.d.ts +73 -0
  18. package/dist/auth/grok.d.ts.map +1 -0
  19. package/dist/auth/grok.js +211 -0
  20. package/dist/auth/grok.js.map +1 -0
  21. package/dist/auth/index.d.ts +9 -6
  22. package/dist/auth/index.d.ts.map +1 -1
  23. package/dist/auth/index.js +23 -6
  24. package/dist/auth/index.js.map +1 -1
  25. package/dist/cli/commands/auth.d.ts +1 -1
  26. package/dist/cli/commands/auth.d.ts.map +1 -1
  27. package/dist/cli/commands/auth.js +79 -8
  28. package/dist/cli/commands/auth.js.map +1 -1
  29. package/dist/cli/commands/create.d.ts.map +1 -1
  30. package/dist/cli/commands/create.js +15 -4
  31. package/dist/cli/commands/create.js.map +1 -1
  32. package/dist/cli/interactive.d.ts.map +1 -1
  33. package/dist/cli/interactive.js +406 -35
  34. package/dist/cli/interactive.js.map +1 -1
  35. package/dist/config/defaults.d.ts +3 -0
  36. package/dist/config/defaults.d.ts.map +1 -1
  37. package/dist/config/defaults.js +9 -0
  38. package/dist/config/defaults.js.map +1 -1
  39. package/dist/config/index.d.ts +9 -0
  40. package/dist/config/index.d.ts.map +1 -1
  41. package/dist/config/index.js +16 -3
  42. package/dist/config/index.js.map +1 -1
  43. package/dist/config/schema.d.ts +27 -0
  44. package/dist/config/schema.d.ts.map +1 -1
  45. package/dist/config/schema.js +24 -3
  46. package/dist/config/schema.js.map +1 -1
  47. package/dist/generators/fullstack.d.ts +32 -0
  48. package/dist/generators/fullstack.d.ts.map +1 -0
  49. package/dist/generators/fullstack.js +497 -0
  50. package/dist/generators/fullstack.js.map +1 -0
  51. package/dist/generators/index.d.ts +4 -3
  52. package/dist/generators/index.d.ts.map +1 -1
  53. package/dist/generators/index.js +15 -1
  54. package/dist/generators/index.js.map +1 -1
  55. package/dist/generators/python.d.ts +17 -1
  56. package/dist/generators/python.d.ts.map +1 -1
  57. package/dist/generators/python.js +34 -21
  58. package/dist/generators/python.js.map +1 -1
  59. package/dist/generators/templates/fullstack.d.ts +113 -0
  60. package/dist/generators/templates/fullstack.d.ts.map +1 -0
  61. package/dist/generators/templates/fullstack.js +1004 -0
  62. package/dist/generators/templates/fullstack.js.map +1 -0
  63. package/dist/generators/typescript.d.ts +19 -1
  64. package/dist/generators/typescript.d.ts.map +1 -1
  65. package/dist/generators/typescript.js +37 -21
  66. package/dist/generators/typescript.js.map +1 -1
  67. package/dist/types/cli.d.ts +4 -0
  68. package/dist/types/cli.d.ts.map +1 -1
  69. package/dist/types/cli.js.map +1 -1
  70. package/dist/types/consensus.d.ts +119 -2
  71. package/dist/types/consensus.d.ts.map +1 -1
  72. package/dist/types/consensus.js +12 -1
  73. package/dist/types/consensus.js.map +1 -1
  74. package/dist/types/project.d.ts +76 -0
  75. package/dist/types/project.d.ts.map +1 -1
  76. package/dist/types/project.js +1 -1
  77. package/dist/types/project.js.map +1 -1
  78. package/dist/types/workflow.d.ts +170 -16
  79. package/dist/types/workflow.d.ts.map +1 -1
  80. package/dist/types/workflow.js +26 -3
  81. package/dist/types/workflow.js.map +1 -1
  82. package/dist/workflow/consensus.d.ts +29 -3
  83. package/dist/workflow/consensus.d.ts.map +1 -1
  84. package/dist/workflow/consensus.js +334 -27
  85. package/dist/workflow/consensus.js.map +1 -1
  86. package/dist/workflow/execution-mode.d.ts +2 -0
  87. package/dist/workflow/execution-mode.d.ts.map +1 -1
  88. package/dist/workflow/execution-mode.js +20 -0
  89. package/dist/workflow/execution-mode.js.map +1 -1
  90. package/dist/workflow/index.d.ts +2 -0
  91. package/dist/workflow/index.d.ts.map +1 -1
  92. package/dist/workflow/index.js +11 -0
  93. package/dist/workflow/index.js.map +1 -1
  94. package/dist/workflow/milestone-workflow.d.ts +2 -0
  95. package/dist/workflow/milestone-workflow.d.ts.map +1 -1
  96. package/dist/workflow/milestone-workflow.js +19 -2
  97. package/dist/workflow/milestone-workflow.js.map +1 -1
  98. package/dist/workflow/plan-mode.d.ts +66 -2
  99. package/dist/workflow/plan-mode.d.ts.map +1 -1
  100. package/dist/workflow/plan-mode.js +187 -11
  101. package/dist/workflow/plan-mode.js.map +1 -1
  102. package/dist/workflow/plan-storage.d.ts +252 -8
  103. package/dist/workflow/plan-storage.d.ts.map +1 -1
  104. package/dist/workflow/plan-storage.js +580 -33
  105. package/dist/workflow/plan-storage.js.map +1 -1
  106. package/dist/workflow/project-verification.js +1 -1
  107. package/dist/workflow/project-verification.js.map +1 -1
  108. package/dist/workflow/task-workflow.d.ts +2 -0
  109. package/dist/workflow/task-workflow.d.ts.map +1 -1
  110. package/dist/workflow/task-workflow.js +23 -1
  111. package/dist/workflow/task-workflow.js.map +1 -1
  112. package/dist/workflow/test-runner.d.ts +8 -0
  113. package/dist/workflow/test-runner.d.ts.map +1 -1
  114. package/dist/workflow/test-runner.js +92 -0
  115. package/dist/workflow/test-runner.js.map +1 -1
  116. package/dist/workflow/workspace-manager.d.ts +342 -0
  117. package/dist/workflow/workspace-manager.d.ts.map +1 -0
  118. package/dist/workflow/workspace-manager.js +733 -0
  119. package/dist/workflow/workspace-manager.js.map +1 -0
  120. package/package.json +1 -1
  121. package/src/adapters/claude.ts +322 -25
  122. package/src/adapters/grok.ts +492 -0
  123. package/src/adapters/openai.ts +8 -2
  124. package/src/auth/grok.ts +255 -0
  125. package/src/auth/index.ts +27 -9
  126. package/src/cli/commands/auth.ts +89 -10
  127. package/src/cli/commands/create.ts +13 -4
  128. package/src/cli/interactive.ts +453 -34
  129. package/src/config/defaults.ts +9 -0
  130. package/src/config/index.ts +17 -3
  131. package/src/config/schema.ts +25 -3
  132. package/src/generators/fullstack.ts +551 -0
  133. package/src/generators/index.ts +25 -1
  134. package/src/generators/python.ts +65 -21
  135. package/src/generators/templates/fullstack.ts +1047 -0
  136. package/src/generators/typescript.ts +69 -21
  137. package/src/types/cli.ts +4 -0
  138. package/src/types/consensus.ts +135 -3
  139. package/src/types/project.ts +82 -1
  140. package/src/types/workflow.ts +58 -4
  141. package/src/workflow/consensus.ts +461 -31
  142. package/src/workflow/execution-mode.ts +32 -0
  143. package/src/workflow/index.ts +12 -0
  144. package/src/workflow/milestone-workflow.ts +24 -2
  145. package/src/workflow/plan-mode.ts +238 -10
  146. package/src/workflow/plan-storage.ts +835 -35
  147. package/src/workflow/project-verification.ts +1 -1
  148. package/src/workflow/task-workflow.ts +29 -1
  149. package/src/workflow/test-runner.ts +110 -0
  150. package/src/workflow/workspace-manager.ts +912 -0
@@ -1,21 +1,76 @@
1
1
  /**
2
2
  * Plan Storage System
3
3
  * Manages plans in markdown files to reduce API calls and maintain tracking
4
+ *
5
+ * Directory Structure for Fullstack Projects:
6
+ * docs/plans/
7
+ * ├── master/
8
+ * │ ├── plan.md
9
+ * │ ├── metadata.json
10
+ * │ ├── unified/
11
+ * │ │ ├── feedback.json
12
+ * │ │ └── feedback.md
13
+ * │ ├── frontend/
14
+ * │ │ ├── feedback.json
15
+ * │ │ └── feedback.md
16
+ * │ └── backend/
17
+ * │ ├── feedback.json
18
+ * │ └── feedback.md
19
+ * ├── milestone-1/
20
+ * │ ├── plan.md
21
+ * │ ├── metadata.json
22
+ * │ ├── unified/
23
+ * │ ├── frontend/
24
+ * │ ├── backend/
25
+ * │ └── tasks/
26
+ * │ └── task-1/
27
+ * │ ├── plan.md
28
+ * │ ├── metadata.json
29
+ * │ ├── unified/
30
+ * │ ├── frontend/
31
+ * │ └── backend/
4
32
  */
5
33
 
6
34
  import { promises as fs } from 'node:fs';
7
35
  import path from 'node:path';
36
+ import type {
37
+ ReviewAppTarget,
38
+ TaggedItem,
39
+ AppConsensusScores,
40
+ CorrectionRecord,
41
+ } from '../types/consensus.js';
42
+
43
+ /**
44
+ * App target for feedback storage
45
+ */
46
+ export type FeedbackAppTarget = 'frontend' | 'backend' | 'unified';
8
47
 
9
48
  /**
10
49
  * Feedback entry from a reviewer
11
50
  */
12
51
  export interface ReviewerFeedback {
13
- reviewer: 'openai' | 'gemini' | 'claude';
52
+ reviewer: 'openai' | 'gemini' | 'grok' | 'claude';
14
53
  score: number;
15
54
  timestamp: string;
16
55
  concerns: string[];
17
56
  recommendations: string[];
18
57
  analysis: string;
58
+ /** App target (for fullstack projects) */
59
+ appTarget?: FeedbackAppTarget;
60
+ }
61
+
62
+ /**
63
+ * Fullstack-aware feedback with per-app breakdown
64
+ */
65
+ export interface FullstackReviewerFeedback extends ReviewerFeedback {
66
+ /** Per-app scores */
67
+ appScores: AppConsensusScores;
68
+ /** Tagged concerns by app */
69
+ taggedConcerns: TaggedItem[];
70
+ /** Tagged recommendations by app */
71
+ taggedRecommendations: TaggedItem[];
72
+ /** Whether this is fullstack feedback */
73
+ isFullstack: true;
19
74
  }
20
75
 
21
76
  /**
@@ -33,6 +88,21 @@ export interface PlanMetadata {
33
88
  updatedAt: string;
34
89
  consensusScore?: number;
35
90
  status: 'draft' | 'reviewing' | 'approved' | 'implemented';
91
+
92
+ /** Fullstack-specific tracking */
93
+ isFullstack?: boolean;
94
+ frontendScore?: number;
95
+ backendScore?: number;
96
+ unifiedScore?: number;
97
+ frontendApproved?: boolean;
98
+ backendApproved?: boolean;
99
+ unifiedApproved?: boolean;
100
+
101
+ /** Total iterations for this plan */
102
+ totalIterations?: number;
103
+
104
+ /** Corrections made during consensus */
105
+ corrections?: CorrectionRecord[];
36
106
  }
37
107
 
38
108
  /**
@@ -50,16 +120,42 @@ export interface StoredPlan {
50
120
  }>;
51
121
  }
52
122
 
123
+ /**
124
+ * Fullstack stored plan with per-app feedback
125
+ */
126
+ export interface FullstackStoredPlan extends StoredPlan {
127
+ /** Per-app feedback */
128
+ frontendFeedback: ReviewerFeedback[];
129
+ backendFeedback: ReviewerFeedback[];
130
+ unifiedFeedback: ReviewerFeedback[];
131
+
132
+ /** Per-app revision history */
133
+ appRevisionHistory: {
134
+ frontend: Array<{ version: number; timestamp: string; changes: string; score?: number }>;
135
+ backend: Array<{ version: number; timestamp: string; changes: string; score?: number }>;
136
+ unified: Array<{ version: number; timestamp: string; changes: string; score?: number }>;
137
+ };
138
+ }
139
+
53
140
  /**
54
141
  * Plan Storage Manager
55
142
  */
56
143
  export class PlanStorage {
57
144
  private projectDir: string;
58
145
  private plansDir: string;
146
+ private isFullstack: boolean;
59
147
 
60
- constructor(projectDir: string) {
148
+ constructor(projectDir: string, isFullstack: boolean = false) {
61
149
  this.projectDir = projectDir;
62
150
  this.plansDir = path.join(projectDir, 'docs', 'plans');
151
+ this.isFullstack = isFullstack;
152
+ }
153
+
154
+ /**
155
+ * Set fullstack mode
156
+ */
157
+ setFullstack(isFullstack: boolean): void {
158
+ this.isFullstack = isFullstack;
63
159
  }
64
160
 
65
161
  /**
@@ -67,10 +163,30 @@ export class PlanStorage {
67
163
  */
68
164
  async initialize(): Promise<void> {
69
165
  await fs.mkdir(this.plansDir, { recursive: true });
166
+
167
+ // Create master directory with app subdirectories for fullstack
168
+ if (this.isFullstack) {
169
+ await this.initializeAppDirectories(path.join(this.plansDir, 'master'));
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Initialize app subdirectories (frontend/backend/unified)
175
+ */
176
+ private async initializeAppDirectories(baseDir: string): Promise<void> {
177
+ await fs.mkdir(baseDir, { recursive: true });
178
+ await fs.mkdir(path.join(baseDir, 'unified'), { recursive: true });
179
+ await fs.mkdir(path.join(baseDir, 'frontend'), { recursive: true });
180
+ await fs.mkdir(path.join(baseDir, 'backend'), { recursive: true });
70
181
  }
71
182
 
72
183
  /**
73
184
  * Get the path for a plan file
185
+ *
186
+ * New structure for fullstack:
187
+ * - master: docs/plans/master/plan.md
188
+ * - milestone: docs/plans/milestone-N/plan.md
189
+ * - task: docs/plans/milestone-N/tasks/task-N/plan.md
74
190
  */
75
191
  private getPlanPath(
76
192
  type: 'master' | 'milestone' | 'task',
@@ -78,6 +194,9 @@ export class PlanStorage {
78
194
  taskId?: string
79
195
  ): string {
80
196
  if (type === 'master') {
197
+ if (this.isFullstack) {
198
+ return path.join(this.plansDir, 'master', 'plan.md');
199
+ }
81
200
  return path.join(this.projectDir, 'docs', 'PLAN.md');
82
201
  }
83
202
 
@@ -88,29 +207,92 @@ export class PlanStorage {
88
207
 
89
208
  if (type === 'task' && milestoneId && taskId) {
90
209
  const milestoneDir = path.join(this.plansDir, `milestone-${milestoneId}`);
210
+ if (this.isFullstack) {
211
+ return path.join(milestoneDir, 'tasks', `task-${taskId}`, 'plan.md');
212
+ }
91
213
  return path.join(milestoneDir, `task-${taskId}-plan.md`);
92
214
  }
93
215
 
94
216
  throw new Error(`Invalid plan type or missing IDs: ${type}`);
95
217
  }
96
218
 
219
+ /**
220
+ * Get the base directory for a plan level
221
+ */
222
+ private getPlanBaseDir(
223
+ type: 'master' | 'milestone' | 'task',
224
+ milestoneId?: string,
225
+ taskId?: string
226
+ ): string {
227
+ if (type === 'master') {
228
+ return path.join(this.plansDir, 'master');
229
+ }
230
+
231
+ if (type === 'milestone' && milestoneId) {
232
+ return path.join(this.plansDir, `milestone-${milestoneId}`);
233
+ }
234
+
235
+ if (type === 'task' && milestoneId && taskId) {
236
+ return path.join(this.plansDir, `milestone-${milestoneId}`, 'tasks', `task-${taskId}`);
237
+ }
238
+
239
+ throw new Error(`Invalid plan type or missing IDs: ${type}`);
240
+ }
241
+
97
242
  /**
98
243
  * Get the path for feedback file
244
+ *
245
+ * For fullstack projects, feedback is stored per-app:
246
+ * - unified/feedback.md, frontend/feedback.md, backend/feedback.md
99
247
  */
100
- private getFeedbackPath(milestoneId: string, taskId?: string): string {
248
+ private getFeedbackPath(
249
+ milestoneId: string,
250
+ taskId?: string,
251
+ appTarget?: FeedbackAppTarget
252
+ ): string {
101
253
  const milestoneDir = path.join(this.plansDir, `milestone-${milestoneId}`);
254
+
255
+ if (this.isFullstack && appTarget) {
256
+ if (taskId) {
257
+ return path.join(milestoneDir, 'tasks', `task-${taskId}`, appTarget, 'feedback.md');
258
+ }
259
+ return path.join(milestoneDir, appTarget, 'feedback.md');
260
+ }
261
+
262
+ // Legacy non-fullstack path
102
263
  if (taskId) {
103
264
  return path.join(milestoneDir, `task-${taskId}-feedback.md`);
104
265
  }
105
266
  return path.join(milestoneDir, 'feedback.md');
106
267
  }
107
268
 
269
+ /**
270
+ * Get feedback path for master plan
271
+ */
272
+ private getMasterFeedbackPath(appTarget?: FeedbackAppTarget): string {
273
+ if (this.isFullstack && appTarget) {
274
+ return path.join(this.plansDir, 'master', appTarget, 'feedback.md');
275
+ }
276
+ return path.join(this.plansDir, 'master', 'feedback.md');
277
+ }
278
+
108
279
  /**
109
280
  * Get the path for metadata file
110
281
  */
111
- private getMetadataPath(milestoneId: string, taskId?: string): string {
282
+ private getMetadataPath(
283
+ type: 'master' | 'milestone' | 'task',
284
+ milestoneId?: string,
285
+ taskId?: string
286
+ ): string {
287
+ if (type === 'master') {
288
+ return path.join(this.plansDir, 'master', 'metadata.json');
289
+ }
290
+
112
291
  const milestoneDir = path.join(this.plansDir, `milestone-${milestoneId}`);
113
292
  if (taskId) {
293
+ if (this.isFullstack) {
294
+ return path.join(milestoneDir, 'tasks', `task-${taskId}`, 'metadata.json');
295
+ }
114
296
  return path.join(milestoneDir, `task-${taskId}-metadata.json`);
115
297
  }
116
298
  return path.join(milestoneDir, 'metadata.json');
@@ -128,6 +310,9 @@ export class PlanStorage {
128
310
  taskId?: string;
129
311
  taskName?: string;
130
312
  score?: number;
313
+ frontendScore?: number;
314
+ backendScore?: number;
315
+ unifiedScore?: number;
131
316
  } = {}
132
317
  ): Promise<string> {
133
318
  const planPath = this.getPlanPath(type, options.milestoneId, options.taskId);
@@ -135,6 +320,12 @@ export class PlanStorage {
135
320
  // Ensure directory exists
136
321
  await fs.mkdir(path.dirname(planPath), { recursive: true });
137
322
 
323
+ // For fullstack projects, also create app subdirectories
324
+ if (this.isFullstack) {
325
+ const baseDir = this.getPlanBaseDir(type, options.milestoneId, options.taskId);
326
+ await this.initializeAppDirectories(baseDir);
327
+ }
328
+
138
329
  // Add header with metadata
139
330
  const header = this.generatePlanHeader(type, options);
140
331
  const fullContent = `${header}\n\n${content}`;
@@ -142,9 +333,7 @@ export class PlanStorage {
142
333
  await fs.writeFile(planPath, fullContent, 'utf-8');
143
334
 
144
335
  // Save metadata separately for easy parsing
145
- if (options.milestoneId) {
146
- await this.saveMetadata(type, options);
147
- }
336
+ await this.saveMetadata(type, options);
148
337
 
149
338
  return planPath;
150
339
  }
@@ -160,6 +349,9 @@ export class PlanStorage {
160
349
  taskId?: string;
161
350
  taskName?: string;
162
351
  score?: number;
352
+ frontendScore?: number;
353
+ backendScore?: number;
354
+ unifiedScore?: number;
163
355
  }
164
356
  ): string {
165
357
  const lines: string[] = [];
@@ -170,6 +362,15 @@ export class PlanStorage {
170
362
  if (options.taskId) lines.push(`task_id: ${options.taskId}`);
171
363
  if (options.taskName) lines.push(`task_name: ${options.taskName}`);
172
364
  if (options.score !== undefined) lines.push(`consensus_score: ${options.score}`);
365
+
366
+ // Fullstack-specific scores
367
+ if (this.isFullstack) {
368
+ lines.push(`is_fullstack: true`);
369
+ if (options.frontendScore !== undefined) lines.push(`frontend_score: ${options.frontendScore}`);
370
+ if (options.backendScore !== undefined) lines.push(`backend_score: ${options.backendScore}`);
371
+ if (options.unifiedScore !== undefined) lines.push(`unified_score: ${options.unifiedScore}`);
372
+ }
373
+
173
374
  lines.push(`updated_at: ${new Date().toISOString()}`);
174
375
  lines.push('---');
175
376
  return lines.join('\n');
@@ -186,11 +387,15 @@ export class PlanStorage {
186
387
  taskId?: string;
187
388
  taskName?: string;
188
389
  score?: number;
390
+ frontendScore?: number;
391
+ backendScore?: number;
392
+ unifiedScore?: number;
189
393
  }
190
394
  ): Promise<void> {
191
- if (!options.milestoneId) return;
395
+ const metadataPath = this.getMetadataPath(type, options.milestoneId, options.taskId);
192
396
 
193
- const metadataPath = this.getMetadataPath(options.milestoneId, options.taskId);
397
+ // Ensure directory exists
398
+ await fs.mkdir(path.dirname(metadataPath), { recursive: true });
194
399
 
195
400
  let metadata: PlanMetadata;
196
401
  try {
@@ -199,9 +404,16 @@ export class PlanStorage {
199
404
  metadata.version += 1;
200
405
  metadata.updatedAt = new Date().toISOString();
201
406
  if (options.score !== undefined) metadata.consensusScore = options.score;
407
+
408
+ // Update fullstack scores
409
+ if (this.isFullstack) {
410
+ if (options.frontendScore !== undefined) metadata.frontendScore = options.frontendScore;
411
+ if (options.backendScore !== undefined) metadata.backendScore = options.backendScore;
412
+ if (options.unifiedScore !== undefined) metadata.unifiedScore = options.unifiedScore;
413
+ }
202
414
  } catch {
203
415
  metadata = {
204
- id: options.taskId || options.milestoneId,
416
+ id: options.taskId || options.milestoneId || 'master',
205
417
  type,
206
418
  milestoneId: options.milestoneId,
207
419
  milestoneName: options.milestoneName,
@@ -212,6 +424,12 @@ export class PlanStorage {
212
424
  updatedAt: new Date().toISOString(),
213
425
  consensusScore: options.score,
214
426
  status: 'draft',
427
+ isFullstack: this.isFullstack,
428
+ frontendScore: options.frontendScore,
429
+ backendScore: options.backendScore,
430
+ unifiedScore: options.unifiedScore,
431
+ totalIterations: 0,
432
+ corrections: [],
215
433
  };
216
434
  }
217
435
 
@@ -243,13 +461,20 @@ export class PlanStorage {
243
461
 
244
462
  /**
245
463
  * Save feedback from a reviewer
464
+ *
465
+ * For fullstack projects, appTarget determines which subdirectory:
466
+ * - 'frontend': milestone-N/frontend/feedback.json
467
+ * - 'backend': milestone-N/backend/feedback.json
468
+ * - 'unified': milestone-N/unified/feedback.json
246
469
  */
247
470
  async saveFeedback(
248
471
  feedback: ReviewerFeedback,
249
472
  milestoneId: string,
250
- taskId?: string
473
+ taskId?: string,
474
+ appTarget?: FeedbackAppTarget
251
475
  ): Promise<void> {
252
- const feedbackPath = this.getFeedbackPath(milestoneId, taskId);
476
+ const effectiveAppTarget = this.isFullstack ? (appTarget || 'unified') : undefined;
477
+ const feedbackPath = this.getFeedbackPath(milestoneId, taskId, effectiveAppTarget);
253
478
 
254
479
  // Ensure directory exists
255
480
  await fs.mkdir(path.dirname(feedbackPath), { recursive: true });
@@ -263,8 +488,14 @@ export class PlanStorage {
263
488
  // No existing feedback
264
489
  }
265
490
 
491
+ // Tag feedback with app target
492
+ const taggedFeedback: ReviewerFeedback = {
493
+ ...feedback,
494
+ appTarget: effectiveAppTarget,
495
+ };
496
+
266
497
  // Add new feedback
267
- existingFeedback.push(feedback);
498
+ existingFeedback.push(taggedFeedback);
268
499
 
269
500
  // Save JSON for programmatic access
270
501
  await fs.writeFile(
@@ -274,16 +505,155 @@ export class PlanStorage {
274
505
  );
275
506
 
276
507
  // Also save human-readable markdown
277
- const mdContent = this.formatFeedbackAsMarkdown(existingFeedback);
508
+ const mdContent = this.formatFeedbackAsMarkdown(existingFeedback, effectiveAppTarget);
509
+ await fs.writeFile(feedbackPath, mdContent, 'utf-8');
510
+ }
511
+
512
+ /**
513
+ * Save fullstack feedback with per-app breakdown
514
+ *
515
+ * Saves feedback to all three directories (unified, frontend, backend)
516
+ */
517
+ async saveFullstackFeedback(
518
+ feedback: FullstackReviewerFeedback,
519
+ type: 'master' | 'milestone' | 'task',
520
+ milestoneId?: string,
521
+ taskId?: string
522
+ ): Promise<void> {
523
+ if (!this.isFullstack) {
524
+ // Fall back to unified storage
525
+ await this.saveFeedback(feedback, milestoneId || 'master', taskId);
526
+ return;
527
+ }
528
+
529
+ const apps: FeedbackAppTarget[] = ['unified', 'frontend', 'backend'];
530
+
531
+ for (const app of apps) {
532
+ // Extract app-specific concerns and recommendations
533
+ const appConcerns = feedback.taggedConcerns
534
+ .filter(c => c.app === app)
535
+ .map(c => c.content);
536
+ const appRecommendations = feedback.taggedRecommendations
537
+ .filter(r => r.app === app)
538
+ .map(r => r.content);
539
+
540
+ // Get app-specific score
541
+ const appScore = app === 'frontend'
542
+ ? feedback.appScores.frontend
543
+ : app === 'backend'
544
+ ? feedback.appScores.backend
545
+ : feedback.appScores.unified;
546
+
547
+ const appFeedback: ReviewerFeedback = {
548
+ reviewer: feedback.reviewer,
549
+ score: appScore || feedback.score,
550
+ timestamp: feedback.timestamp,
551
+ concerns: appConcerns.length > 0 ? appConcerns : feedback.concerns,
552
+ recommendations: appRecommendations.length > 0 ? appRecommendations : feedback.recommendations,
553
+ analysis: feedback.analysis,
554
+ appTarget: app,
555
+ };
556
+
557
+ if (type === 'master') {
558
+ await this.saveMasterFeedback(appFeedback, app);
559
+ } else {
560
+ await this.saveFeedback(appFeedback, milestoneId!, taskId, app);
561
+ }
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Save feedback for master plan
567
+ */
568
+ async saveMasterFeedback(
569
+ feedback: ReviewerFeedback,
570
+ appTarget?: FeedbackAppTarget
571
+ ): Promise<void> {
572
+ const effectiveAppTarget = this.isFullstack ? (appTarget || 'unified') : undefined;
573
+ const feedbackPath = this.getMasterFeedbackPath(effectiveAppTarget);
574
+
575
+ // Ensure directory exists
576
+ await fs.mkdir(path.dirname(feedbackPath), { recursive: true });
577
+
578
+ // Load existing feedback
579
+ let existingFeedback: ReviewerFeedback[] = [];
580
+ try {
581
+ const content = await fs.readFile(feedbackPath.replace('.md', '.json'), 'utf-8');
582
+ existingFeedback = JSON.parse(content);
583
+ } catch {
584
+ // No existing feedback
585
+ }
586
+
587
+ // Tag feedback with app target
588
+ const taggedFeedback: ReviewerFeedback = {
589
+ ...feedback,
590
+ appTarget: effectiveAppTarget,
591
+ };
592
+
593
+ existingFeedback.push(taggedFeedback);
594
+
595
+ // Save JSON
596
+ await fs.writeFile(
597
+ feedbackPath.replace('.md', '.json'),
598
+ JSON.stringify(existingFeedback, null, 2),
599
+ 'utf-8'
600
+ );
601
+
602
+ // Save markdown
603
+ const mdContent = this.formatFeedbackAsMarkdown(existingFeedback, effectiveAppTarget);
278
604
  await fs.writeFile(feedbackPath, mdContent, 'utf-8');
279
605
  }
280
606
 
281
607
  /**
282
608
  * Load all feedback for a plan
283
609
  */
284
- async loadFeedback(milestoneId: string, taskId?: string): Promise<ReviewerFeedback[]> {
610
+ async loadFeedback(
611
+ milestoneId: string,
612
+ taskId?: string,
613
+ appTarget?: FeedbackAppTarget
614
+ ): Promise<ReviewerFeedback[]> {
615
+ try {
616
+ const effectiveAppTarget = this.isFullstack ? (appTarget || 'unified') : undefined;
617
+ const feedbackPath = this.getFeedbackPath(milestoneId, taskId, effectiveAppTarget).replace('.md', '.json');
618
+ const content = await fs.readFile(feedbackPath, 'utf-8');
619
+ return JSON.parse(content);
620
+ } catch {
621
+ return [];
622
+ }
623
+ }
624
+
625
+ /**
626
+ * Load all feedback for all apps (fullstack)
627
+ */
628
+ async loadAllAppFeedback(
629
+ milestoneId: string,
630
+ taskId?: string
631
+ ): Promise<{
632
+ unified: ReviewerFeedback[];
633
+ frontend: ReviewerFeedback[];
634
+ backend: ReviewerFeedback[];
635
+ }> {
636
+ if (!this.isFullstack) {
637
+ const unified = await this.loadFeedback(milestoneId, taskId);
638
+ return { unified, frontend: [], backend: [] };
639
+ }
640
+
641
+ const [unified, frontend, backend] = await Promise.all([
642
+ this.loadFeedback(milestoneId, taskId, 'unified'),
643
+ this.loadFeedback(milestoneId, taskId, 'frontend'),
644
+ this.loadFeedback(milestoneId, taskId, 'backend'),
645
+ ]);
646
+
647
+ return { unified, frontend, backend };
648
+ }
649
+
650
+ /**
651
+ * Load master plan feedback
652
+ */
653
+ async loadMasterFeedback(appTarget?: FeedbackAppTarget): Promise<ReviewerFeedback[]> {
285
654
  try {
286
- const feedbackPath = this.getFeedbackPath(milestoneId, taskId).replace('.md', '.json');
655
+ const effectiveAppTarget = this.isFullstack ? (appTarget || 'unified') : undefined;
656
+ const feedbackPath = this.getMasterFeedbackPath(effectiveAppTarget).replace('.md', '.json');
287
657
  const content = await fs.readFile(feedbackPath, 'utf-8');
288
658
  return JSON.parse(content);
289
659
  } catch {
@@ -291,11 +661,66 @@ export class PlanStorage {
291
661
  }
292
662
  }
293
663
 
664
+ /**
665
+ * Load all master plan feedback (fullstack)
666
+ */
667
+ async loadAllMasterFeedback(): Promise<{
668
+ unified: ReviewerFeedback[];
669
+ frontend: ReviewerFeedback[];
670
+ backend: ReviewerFeedback[];
671
+ }> {
672
+ if (!this.isFullstack) {
673
+ const unified = await this.loadMasterFeedback();
674
+ return { unified, frontend: [], backend: [] };
675
+ }
676
+
677
+ const [unified, frontend, backend] = await Promise.all([
678
+ this.loadMasterFeedback('unified'),
679
+ this.loadMasterFeedback('frontend'),
680
+ this.loadMasterFeedback('backend'),
681
+ ]);
682
+
683
+ return { unified, frontend, backend };
684
+ }
685
+
294
686
  /**
295
687
  * Clear feedback for a new consensus round
296
688
  */
297
- async clearFeedback(milestoneId: string, taskId?: string): Promise<void> {
298
- const feedbackPath = this.getFeedbackPath(milestoneId, taskId);
689
+ async clearFeedback(milestoneId: string, taskId?: string, appTarget?: FeedbackAppTarget): Promise<void> {
690
+ if (this.isFullstack && !appTarget) {
691
+ // Clear all app feedback
692
+ await Promise.all([
693
+ this.clearFeedback(milestoneId, taskId, 'unified'),
694
+ this.clearFeedback(milestoneId, taskId, 'frontend'),
695
+ this.clearFeedback(milestoneId, taskId, 'backend'),
696
+ ]);
697
+ return;
698
+ }
699
+
700
+ const feedbackPath = this.getFeedbackPath(milestoneId, taskId, appTarget);
701
+ try {
702
+ await fs.unlink(feedbackPath);
703
+ await fs.unlink(feedbackPath.replace('.md', '.json'));
704
+ } catch {
705
+ // Files don't exist, that's fine
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Clear master plan feedback
711
+ */
712
+ async clearMasterFeedback(appTarget?: FeedbackAppTarget): Promise<void> {
713
+ if (this.isFullstack && !appTarget) {
714
+ // Clear all app feedback
715
+ await Promise.all([
716
+ this.clearMasterFeedback('unified'),
717
+ this.clearMasterFeedback('frontend'),
718
+ this.clearMasterFeedback('backend'),
719
+ ]);
720
+ return;
721
+ }
722
+
723
+ const feedbackPath = this.getMasterFeedbackPath(appTarget);
299
724
  try {
300
725
  await fs.unlink(feedbackPath);
301
726
  await fs.unlink(feedbackPath.replace('.md', '.json'));
@@ -307,14 +732,27 @@ export class PlanStorage {
307
732
  /**
308
733
  * Format feedback as readable markdown
309
734
  */
310
- private formatFeedbackAsMarkdown(feedback: ReviewerFeedback[]): string {
735
+ private formatFeedbackAsMarkdown(
736
+ feedback: ReviewerFeedback[],
737
+ appTarget?: FeedbackAppTarget
738
+ ): string {
311
739
  const lines: string[] = [];
312
- lines.push('# Reviewer Feedback\n');
740
+
741
+ // Header with app target for fullstack
742
+ if (appTarget && this.isFullstack) {
743
+ const appLabel = appTarget.charAt(0).toUpperCase() + appTarget.slice(1);
744
+ lines.push(`# ${appLabel} Reviewer Feedback\n`);
745
+ } else {
746
+ lines.push('# Reviewer Feedback\n');
747
+ }
313
748
 
314
749
  for (const fb of feedback) {
315
750
  lines.push(`## ${fb.reviewer.toUpperCase()} Review`);
316
751
  lines.push(`- **Score:** ${fb.score}%`);
317
752
  lines.push(`- **Timestamp:** ${fb.timestamp}`);
753
+ if (fb.appTarget) {
754
+ lines.push(`- **App Target:** ${fb.appTarget}`);
755
+ }
318
756
  lines.push('');
319
757
 
320
758
  if (fb.concerns.length > 0) {
@@ -350,14 +788,15 @@ export class PlanStorage {
350
788
  */
351
789
  async getCombinedFeedbackForRevision(
352
790
  milestoneId: string,
353
- taskId?: string
791
+ taskId?: string,
792
+ appTarget?: FeedbackAppTarget
354
793
  ): Promise<{
355
794
  averageScore: number;
356
795
  allConcerns: string[];
357
796
  allRecommendations: string[];
358
797
  combinedAnalysis: string;
359
798
  }> {
360
- const feedback = await this.loadFeedback(milestoneId, taskId);
799
+ const feedback = await this.loadFeedback(milestoneId, taskId, appTarget);
361
800
 
362
801
  if (feedback.length === 0) {
363
802
  return {
@@ -387,15 +826,63 @@ export class PlanStorage {
387
826
  };
388
827
  }
389
828
 
829
+ /**
830
+ * Get combined feedback for all apps (fullstack)
831
+ */
832
+ async getFullstackCombinedFeedback(
833
+ milestoneId: string,
834
+ taskId?: string
835
+ ): Promise<{
836
+ unified: { averageScore: number; allConcerns: string[]; allRecommendations: string[]; combinedAnalysis: string };
837
+ frontend: { averageScore: number; allConcerns: string[]; allRecommendations: string[]; combinedAnalysis: string };
838
+ backend: { averageScore: number; allConcerns: string[]; allRecommendations: string[]; combinedAnalysis: string };
839
+ overallScore: number;
840
+ allTaggedConcerns: TaggedItem[];
841
+ allTaggedRecommendations: TaggedItem[];
842
+ }> {
843
+ const [unified, frontend, backend] = await Promise.all([
844
+ this.getCombinedFeedbackForRevision(milestoneId, taskId, 'unified'),
845
+ this.getCombinedFeedbackForRevision(milestoneId, taskId, 'frontend'),
846
+ this.getCombinedFeedbackForRevision(milestoneId, taskId, 'backend'),
847
+ ]);
848
+
849
+ // Calculate overall score (weighted average - unified counts more)
850
+ const scores = [unified.averageScore, frontend.averageScore, backend.averageScore].filter(s => s > 0);
851
+ const overallScore = scores.length > 0 ? scores.reduce((a, b) => a + b, 0) / scores.length : 0;
852
+
853
+ // Create tagged concerns and recommendations
854
+ const allTaggedConcerns: TaggedItem[] = [
855
+ ...unified.allConcerns.map(c => ({ app: 'unified' as ReviewAppTarget, content: c })),
856
+ ...frontend.allConcerns.map(c => ({ app: 'frontend' as ReviewAppTarget, content: c })),
857
+ ...backend.allConcerns.map(c => ({ app: 'backend' as ReviewAppTarget, content: c })),
858
+ ];
859
+
860
+ const allTaggedRecommendations: TaggedItem[] = [
861
+ ...unified.allRecommendations.map(r => ({ app: 'unified' as ReviewAppTarget, content: r })),
862
+ ...frontend.allRecommendations.map(r => ({ app: 'frontend' as ReviewAppTarget, content: r })),
863
+ ...backend.allRecommendations.map(r => ({ app: 'backend' as ReviewAppTarget, content: r })),
864
+ ];
865
+
866
+ return {
867
+ unified,
868
+ frontend,
869
+ backend,
870
+ overallScore,
871
+ allTaggedConcerns,
872
+ allTaggedRecommendations,
873
+ };
874
+ }
875
+
390
876
  /**
391
877
  * Update plan status
392
878
  */
393
879
  async updateStatus(
394
880
  status: 'draft' | 'reviewing' | 'approved' | 'implemented',
395
- milestoneId: string,
881
+ type: 'master' | 'milestone' | 'task',
882
+ milestoneId?: string,
396
883
  taskId?: string
397
884
  ): Promise<void> {
398
- const metadataPath = this.getMetadataPath(milestoneId, taskId);
885
+ const metadataPath = this.getMetadataPath(type, milestoneId, taskId);
399
886
 
400
887
  try {
401
888
  const content = await fs.readFile(metadataPath, 'utf-8');
@@ -408,23 +895,127 @@ export class PlanStorage {
408
895
  }
409
896
  }
410
897
 
898
+ /**
899
+ * Update per-app approval status (fullstack)
900
+ */
901
+ async updateAppApproval(
902
+ type: 'master' | 'milestone' | 'task',
903
+ appTarget: FeedbackAppTarget,
904
+ approved: boolean,
905
+ score: number,
906
+ milestoneId?: string,
907
+ taskId?: string
908
+ ): Promise<void> {
909
+ const metadataPath = this.getMetadataPath(type, milestoneId, taskId);
910
+
911
+ try {
912
+ const content = await fs.readFile(metadataPath, 'utf-8');
913
+ const metadata: PlanMetadata = JSON.parse(content);
914
+
915
+ if (appTarget === 'frontend') {
916
+ metadata.frontendApproved = approved;
917
+ metadata.frontendScore = score;
918
+ } else if (appTarget === 'backend') {
919
+ metadata.backendApproved = approved;
920
+ metadata.backendScore = score;
921
+ } else {
922
+ metadata.unifiedApproved = approved;
923
+ metadata.unifiedScore = score;
924
+ }
925
+
926
+ metadata.updatedAt = new Date().toISOString();
927
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
928
+ } catch {
929
+ // Metadata doesn't exist yet
930
+ }
931
+ }
932
+
933
+ /**
934
+ * Record a correction/revision
935
+ */
936
+ async recordCorrection(
937
+ type: 'master' | 'milestone' | 'task',
938
+ correction: CorrectionRecord,
939
+ milestoneId?: string,
940
+ taskId?: string
941
+ ): Promise<void> {
942
+ const metadataPath = this.getMetadataPath(type, milestoneId, taskId);
943
+
944
+ try {
945
+ const content = await fs.readFile(metadataPath, 'utf-8');
946
+ const metadata: PlanMetadata = JSON.parse(content);
947
+
948
+ if (!metadata.corrections) {
949
+ metadata.corrections = [];
950
+ }
951
+ metadata.corrections.push(correction);
952
+ metadata.totalIterations = (metadata.totalIterations || 0) + 1;
953
+ metadata.updatedAt = new Date().toISOString();
954
+
955
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
956
+ } catch {
957
+ // Metadata doesn't exist yet
958
+ }
959
+ }
960
+
961
+ /**
962
+ * Load metadata for a plan
963
+ */
964
+ async loadMetadata(
965
+ type: 'master' | 'milestone' | 'task',
966
+ milestoneId?: string,
967
+ taskId?: string
968
+ ): Promise<PlanMetadata | null> {
969
+ try {
970
+ const metadataPath = this.getMetadataPath(type, milestoneId, taskId);
971
+ const content = await fs.readFile(metadataPath, 'utf-8');
972
+ return JSON.parse(content);
973
+ } catch {
974
+ return null;
975
+ }
976
+ }
977
+
411
978
  /**
412
979
  * Get plan tracking summary for a milestone
413
980
  */
414
981
  async getMilestoneTrackingSummary(milestoneId: string): Promise<{
415
- milestonePlan: { exists: boolean; score?: number; status?: string };
982
+ milestonePlan: {
983
+ exists: boolean;
984
+ score?: number;
985
+ status?: string;
986
+ frontendScore?: number;
987
+ backendScore?: number;
988
+ unifiedScore?: number;
989
+ frontendApproved?: boolean;
990
+ backendApproved?: boolean;
991
+ unifiedApproved?: boolean;
992
+ };
416
993
  taskPlans: Array<{
417
994
  taskId: string;
418
995
  taskName?: string;
419
996
  exists: boolean;
420
997
  score?: number;
421
998
  status?: string;
999
+ frontendScore?: number;
1000
+ backendScore?: number;
1001
+ unifiedScore?: number;
422
1002
  }>;
423
1003
  }> {
424
1004
  const milestoneDir = path.join(this.plansDir, `milestone-${milestoneId}`);
425
1005
 
426
1006
  // Check milestone plan
427
- let milestonePlan: { exists: boolean; score?: number; status?: string } = { exists: false };
1007
+ let milestonePlan: {
1008
+ exists: boolean;
1009
+ score?: number;
1010
+ status?: string;
1011
+ frontendScore?: number;
1012
+ backendScore?: number;
1013
+ unifiedScore?: number;
1014
+ frontendApproved?: boolean;
1015
+ backendApproved?: boolean;
1016
+ unifiedApproved?: boolean;
1017
+ } = { exists: false };
1018
+
428
1019
  try {
429
1020
  const metadataPath = path.join(milestoneDir, 'metadata.json');
430
1021
  const content = await fs.readFile(metadataPath, 'utf-8');
@@ -433,6 +1024,12 @@ export class PlanStorage {
433
1024
  exists: true,
434
1025
  score: metadata.consensusScore,
435
1026
  status: metadata.status,
1027
+ frontendScore: metadata.frontendScore,
1028
+ backendScore: metadata.backendScore,
1029
+ unifiedScore: metadata.unifiedScore,
1030
+ frontendApproved: metadata.frontendApproved,
1031
+ backendApproved: metadata.backendApproved,
1032
+ unifiedApproved: metadata.unifiedApproved,
436
1033
  };
437
1034
  } catch {
438
1035
  // No milestone plan
@@ -445,9 +1042,44 @@ export class PlanStorage {
445
1042
  exists: boolean;
446
1043
  score?: number;
447
1044
  status?: string;
1045
+ frontendScore?: number;
1046
+ backendScore?: number;
1047
+ unifiedScore?: number;
448
1048
  }> = [];
449
1049
 
450
1050
  try {
1051
+ // Check for new structure (tasks/ subdirectory)
1052
+ if (this.isFullstack) {
1053
+ const tasksDir = path.join(milestoneDir, 'tasks');
1054
+ try {
1055
+ const taskDirs = await fs.readdir(tasksDir);
1056
+ for (const taskDir of taskDirs) {
1057
+ if (taskDir.startsWith('task-')) {
1058
+ const metadataPath = path.join(tasksDir, taskDir, 'metadata.json');
1059
+ try {
1060
+ const content = await fs.readFile(metadataPath, 'utf-8');
1061
+ const metadata: PlanMetadata = JSON.parse(content);
1062
+ taskPlans.push({
1063
+ taskId: metadata.taskId || taskDir.replace('task-', ''),
1064
+ taskName: metadata.taskName,
1065
+ exists: true,
1066
+ score: metadata.consensusScore,
1067
+ status: metadata.status,
1068
+ frontendScore: metadata.frontendScore,
1069
+ backendScore: metadata.backendScore,
1070
+ unifiedScore: metadata.unifiedScore,
1071
+ });
1072
+ } catch {
1073
+ // Skip invalid files
1074
+ }
1075
+ }
1076
+ }
1077
+ } catch {
1078
+ // tasks directory doesn't exist
1079
+ }
1080
+ }
1081
+
1082
+ // Also check legacy structure
451
1083
  const files = await fs.readdir(milestoneDir);
452
1084
  const taskMetadataFiles = files.filter(f => f.startsWith('task-') && f.endsWith('-metadata.json'));
453
1085
 
@@ -455,13 +1087,19 @@ export class PlanStorage {
455
1087
  try {
456
1088
  const content = await fs.readFile(path.join(milestoneDir, file), 'utf-8');
457
1089
  const metadata: PlanMetadata = JSON.parse(content);
458
- taskPlans.push({
459
- taskId: metadata.taskId || '',
460
- taskName: metadata.taskName,
461
- exists: true,
462
- score: metadata.consensusScore,
463
- status: metadata.status,
464
- });
1090
+ // Avoid duplicates
1091
+ if (!taskPlans.find(t => t.taskId === metadata.taskId)) {
1092
+ taskPlans.push({
1093
+ taskId: metadata.taskId || '',
1094
+ taskName: metadata.taskName,
1095
+ exists: true,
1096
+ score: metadata.consensusScore,
1097
+ status: metadata.status,
1098
+ frontendScore: metadata.frontendScore,
1099
+ backendScore: metadata.backendScore,
1100
+ unifiedScore: metadata.unifiedScore,
1101
+ });
1102
+ }
465
1103
  } catch {
466
1104
  // Skip invalid files
467
1105
  }
@@ -472,11 +1110,173 @@ export class PlanStorage {
472
1110
 
473
1111
  return { milestonePlan, taskPlans };
474
1112
  }
1113
+
1114
+ /**
1115
+ * Get comprehensive tracking record for the entire project
1116
+ */
1117
+ async getProjectTrackingRecord(): Promise<{
1118
+ masterPlan: PlanMetadata | null;
1119
+ milestones: Array<{
1120
+ metadata: PlanMetadata | null;
1121
+ tasks: Array<{ metadata: PlanMetadata | null }>;
1122
+ }>;
1123
+ totalCorrections: number;
1124
+ totalIterations: number;
1125
+ }> {
1126
+ // Load master plan metadata
1127
+ const masterPlan = await this.loadMetadata('master');
1128
+
1129
+ // Find all milestone directories
1130
+ const milestones: Array<{
1131
+ metadata: PlanMetadata | null;
1132
+ tasks: Array<{ metadata: PlanMetadata | null }>;
1133
+ }> = [];
1134
+
1135
+ try {
1136
+ const entries = await fs.readdir(this.plansDir, { withFileTypes: true });
1137
+ const milestoneDirs = entries
1138
+ .filter(e => e.isDirectory() && e.name.startsWith('milestone-'))
1139
+ .map(e => e.name);
1140
+
1141
+ for (const milestoneDir of milestoneDirs) {
1142
+ const milestoneId = milestoneDir.replace('milestone-', '');
1143
+ const milestoneMetadata = await this.loadMetadata('milestone', milestoneId);
1144
+
1145
+ // Get tasks for this milestone
1146
+ const { taskPlans } = await this.getMilestoneTrackingSummary(milestoneId);
1147
+ const tasks = await Promise.all(
1148
+ taskPlans.map(async (tp) => ({
1149
+ metadata: await this.loadMetadata('task', milestoneId, tp.taskId),
1150
+ }))
1151
+ );
1152
+
1153
+ milestones.push({ metadata: milestoneMetadata, tasks });
1154
+ }
1155
+ } catch {
1156
+ // Plans directory doesn't exist
1157
+ }
1158
+
1159
+ // Calculate totals
1160
+ let totalCorrections = 0;
1161
+ let totalIterations = 0;
1162
+
1163
+ if (masterPlan) {
1164
+ totalCorrections += masterPlan.corrections?.length || 0;
1165
+ totalIterations += masterPlan.totalIterations || 0;
1166
+ }
1167
+
1168
+ for (const m of milestones) {
1169
+ if (m.metadata) {
1170
+ totalCorrections += m.metadata.corrections?.length || 0;
1171
+ totalIterations += m.metadata.totalIterations || 0;
1172
+ }
1173
+ for (const t of m.tasks) {
1174
+ if (t.metadata) {
1175
+ totalCorrections += t.metadata.corrections?.length || 0;
1176
+ totalIterations += t.metadata.totalIterations || 0;
1177
+ }
1178
+ }
1179
+ }
1180
+
1181
+ return {
1182
+ masterPlan,
1183
+ milestones,
1184
+ totalCorrections,
1185
+ totalIterations,
1186
+ };
1187
+ }
1188
+
1189
+ /**
1190
+ * Get all feedback file paths for the project
1191
+ */
1192
+ async getAllFeedbackPaths(): Promise<{
1193
+ master: { unified?: string; frontend?: string; backend?: string };
1194
+ milestones: Array<{
1195
+ milestoneId: string;
1196
+ paths: { unified?: string; frontend?: string; backend?: string };
1197
+ tasks: Array<{
1198
+ taskId: string;
1199
+ paths: { unified?: string; frontend?: string; backend?: string };
1200
+ }>;
1201
+ }>;
1202
+ }> {
1203
+ const result: {
1204
+ master: { unified?: string; frontend?: string; backend?: string };
1205
+ milestones: Array<{
1206
+ milestoneId: string;
1207
+ paths: { unified?: string; frontend?: string; backend?: string };
1208
+ tasks: Array<{
1209
+ taskId: string;
1210
+ paths: { unified?: string; frontend?: string; backend?: string };
1211
+ }>;
1212
+ }>;
1213
+ } = {
1214
+ master: {},
1215
+ milestones: [],
1216
+ };
1217
+
1218
+ // Master plan paths
1219
+ if (this.isFullstack) {
1220
+ result.master = {
1221
+ unified: this.getMasterFeedbackPath('unified'),
1222
+ frontend: this.getMasterFeedbackPath('frontend'),
1223
+ backend: this.getMasterFeedbackPath('backend'),
1224
+ };
1225
+ } else {
1226
+ result.master = {
1227
+ unified: this.getMasterFeedbackPath(),
1228
+ };
1229
+ }
1230
+
1231
+ // Find milestone directories
1232
+ try {
1233
+ const entries = await fs.readdir(this.plansDir, { withFileTypes: true });
1234
+ const milestoneDirs = entries
1235
+ .filter(e => e.isDirectory() && e.name.startsWith('milestone-'))
1236
+ .map(e => e.name);
1237
+
1238
+ for (const dir of milestoneDirs) {
1239
+ const milestoneId = dir.replace('milestone-', '');
1240
+ const milestonePaths: { unified?: string; frontend?: string; backend?: string } = {};
1241
+
1242
+ if (this.isFullstack) {
1243
+ milestonePaths.unified = this.getFeedbackPath(milestoneId, undefined, 'unified');
1244
+ milestonePaths.frontend = this.getFeedbackPath(milestoneId, undefined, 'frontend');
1245
+ milestonePaths.backend = this.getFeedbackPath(milestoneId, undefined, 'backend');
1246
+ } else {
1247
+ milestonePaths.unified = this.getFeedbackPath(milestoneId);
1248
+ }
1249
+
1250
+ // Get task paths
1251
+ const { taskPlans } = await this.getMilestoneTrackingSummary(milestoneId);
1252
+ const tasks = taskPlans.map(tp => {
1253
+ const taskPaths: { unified?: string; frontend?: string; backend?: string } = {};
1254
+
1255
+ if (this.isFullstack) {
1256
+ taskPaths.unified = this.getFeedbackPath(milestoneId, tp.taskId, 'unified');
1257
+ taskPaths.frontend = this.getFeedbackPath(milestoneId, tp.taskId, 'frontend');
1258
+ taskPaths.backend = this.getFeedbackPath(milestoneId, tp.taskId, 'backend');
1259
+ } else {
1260
+ taskPaths.unified = this.getFeedbackPath(milestoneId, tp.taskId);
1261
+ }
1262
+
1263
+ return { taskId: tp.taskId, paths: taskPaths };
1264
+ });
1265
+
1266
+ result.milestones.push({ milestoneId, paths: milestonePaths, tasks });
1267
+ }
1268
+ } catch {
1269
+ // Plans directory doesn't exist
1270
+ }
1271
+
1272
+ return result;
1273
+ }
475
1274
  }
476
1275
 
1276
+
477
1277
  /**
478
1278
  * Create a plan storage instance for a project
479
1279
  */
480
- export function createPlanStorage(projectDir: string): PlanStorage {
481
- return new PlanStorage(projectDir);
1280
+ export function createPlanStorage(projectDir: string, isFullstack: boolean = false): PlanStorage {
1281
+ return new PlanStorage(projectDir, isFullstack);
482
1282
  }