popeye-cli 1.0.1 → 1.2.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.
Files changed (216) 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 +832 -123
  5. package/dist/adapters/claude.d.ts +19 -4
  6. package/dist/adapters/claude.d.ts.map +1 -1
  7. package/dist/adapters/claude.js +908 -42
  8. package/dist/adapters/claude.js.map +1 -1
  9. package/dist/adapters/gemini.d.ts +55 -0
  10. package/dist/adapters/gemini.d.ts.map +1 -0
  11. package/dist/adapters/gemini.js +318 -0
  12. package/dist/adapters/gemini.js.map +1 -0
  13. package/dist/adapters/grok.d.ts +73 -0
  14. package/dist/adapters/grok.d.ts.map +1 -0
  15. package/dist/adapters/grok.js +430 -0
  16. package/dist/adapters/grok.js.map +1 -0
  17. package/dist/adapters/openai.d.ts +1 -1
  18. package/dist/adapters/openai.d.ts.map +1 -1
  19. package/dist/adapters/openai.js +47 -8
  20. package/dist/adapters/openai.js.map +1 -1
  21. package/dist/auth/claude.d.ts +11 -9
  22. package/dist/auth/claude.d.ts.map +1 -1
  23. package/dist/auth/claude.js +107 -71
  24. package/dist/auth/claude.js.map +1 -1
  25. package/dist/auth/gemini.d.ts +58 -0
  26. package/dist/auth/gemini.d.ts.map +1 -0
  27. package/dist/auth/gemini.js +172 -0
  28. package/dist/auth/gemini.js.map +1 -0
  29. package/dist/auth/grok.d.ts +73 -0
  30. package/dist/auth/grok.d.ts.map +1 -0
  31. package/dist/auth/grok.js +211 -0
  32. package/dist/auth/grok.js.map +1 -0
  33. package/dist/auth/index.d.ts +14 -7
  34. package/dist/auth/index.d.ts.map +1 -1
  35. package/dist/auth/index.js +41 -6
  36. package/dist/auth/index.js.map +1 -1
  37. package/dist/auth/keychain.d.ts +20 -7
  38. package/dist/auth/keychain.d.ts.map +1 -1
  39. package/dist/auth/keychain.js +85 -29
  40. package/dist/auth/keychain.js.map +1 -1
  41. package/dist/auth/openai.d.ts +2 -2
  42. package/dist/auth/openai.d.ts.map +1 -1
  43. package/dist/auth/openai.js +30 -32
  44. package/dist/auth/openai.js.map +1 -1
  45. package/dist/cli/commands/auth.d.ts +1 -1
  46. package/dist/cli/commands/auth.d.ts.map +1 -1
  47. package/dist/cli/commands/auth.js +79 -8
  48. package/dist/cli/commands/auth.js.map +1 -1
  49. package/dist/cli/commands/create.d.ts.map +1 -1
  50. package/dist/cli/commands/create.js +15 -4
  51. package/dist/cli/commands/create.js.map +1 -1
  52. package/dist/cli/interactive.d.ts.map +1 -1
  53. package/dist/cli/interactive.js +1494 -114
  54. package/dist/cli/interactive.js.map +1 -1
  55. package/dist/config/defaults.d.ts +9 -1
  56. package/dist/config/defaults.d.ts.map +1 -1
  57. package/dist/config/defaults.js +19 -2
  58. package/dist/config/defaults.js.map +1 -1
  59. package/dist/config/index.d.ts +19 -0
  60. package/dist/config/index.d.ts.map +1 -1
  61. package/dist/config/index.js +33 -1
  62. package/dist/config/index.js.map +1 -1
  63. package/dist/config/schema.d.ts +47 -0
  64. package/dist/config/schema.d.ts.map +1 -1
  65. package/dist/config/schema.js +29 -1
  66. package/dist/config/schema.js.map +1 -1
  67. package/dist/generators/fullstack.d.ts +32 -0
  68. package/dist/generators/fullstack.d.ts.map +1 -0
  69. package/dist/generators/fullstack.js +497 -0
  70. package/dist/generators/fullstack.js.map +1 -0
  71. package/dist/generators/index.d.ts +4 -3
  72. package/dist/generators/index.d.ts.map +1 -1
  73. package/dist/generators/index.js +15 -1
  74. package/dist/generators/index.js.map +1 -1
  75. package/dist/generators/python.d.ts +17 -1
  76. package/dist/generators/python.d.ts.map +1 -1
  77. package/dist/generators/python.js +34 -20
  78. package/dist/generators/python.js.map +1 -1
  79. package/dist/generators/templates/fullstack.d.ts +113 -0
  80. package/dist/generators/templates/fullstack.d.ts.map +1 -0
  81. package/dist/generators/templates/fullstack.js +1004 -0
  82. package/dist/generators/templates/fullstack.js.map +1 -0
  83. package/dist/generators/typescript.d.ts +19 -1
  84. package/dist/generators/typescript.d.ts.map +1 -1
  85. package/dist/generators/typescript.js +37 -20
  86. package/dist/generators/typescript.js.map +1 -1
  87. package/dist/state/index.d.ts +108 -0
  88. package/dist/state/index.d.ts.map +1 -1
  89. package/dist/state/index.js +551 -4
  90. package/dist/state/index.js.map +1 -1
  91. package/dist/state/registry.d.ts +52 -0
  92. package/dist/state/registry.d.ts.map +1 -0
  93. package/dist/state/registry.js +215 -0
  94. package/dist/state/registry.js.map +1 -0
  95. package/dist/types/cli.d.ts +8 -0
  96. package/dist/types/cli.d.ts.map +1 -1
  97. package/dist/types/cli.js.map +1 -1
  98. package/dist/types/consensus.d.ts +186 -4
  99. package/dist/types/consensus.d.ts.map +1 -1
  100. package/dist/types/consensus.js +35 -3
  101. package/dist/types/consensus.js.map +1 -1
  102. package/dist/types/project.d.ts +76 -0
  103. package/dist/types/project.d.ts.map +1 -1
  104. package/dist/types/project.js +1 -1
  105. package/dist/types/project.js.map +1 -1
  106. package/dist/types/workflow.d.ts +217 -16
  107. package/dist/types/workflow.d.ts.map +1 -1
  108. package/dist/types/workflow.js +40 -1
  109. package/dist/types/workflow.js.map +1 -1
  110. package/dist/workflow/auto-fix.d.ts +45 -0
  111. package/dist/workflow/auto-fix.d.ts.map +1 -0
  112. package/dist/workflow/auto-fix.js +274 -0
  113. package/dist/workflow/auto-fix.js.map +1 -0
  114. package/dist/workflow/consensus.d.ts +70 -2
  115. package/dist/workflow/consensus.d.ts.map +1 -1
  116. package/dist/workflow/consensus.js +872 -17
  117. package/dist/workflow/consensus.js.map +1 -1
  118. package/dist/workflow/execution-mode.d.ts +10 -4
  119. package/dist/workflow/execution-mode.d.ts.map +1 -1
  120. package/dist/workflow/execution-mode.js +547 -58
  121. package/dist/workflow/execution-mode.js.map +1 -1
  122. package/dist/workflow/index.d.ts +14 -2
  123. package/dist/workflow/index.d.ts.map +1 -1
  124. package/dist/workflow/index.js +69 -6
  125. package/dist/workflow/index.js.map +1 -1
  126. package/dist/workflow/milestone-workflow.d.ts +34 -0
  127. package/dist/workflow/milestone-workflow.d.ts.map +1 -0
  128. package/dist/workflow/milestone-workflow.js +414 -0
  129. package/dist/workflow/milestone-workflow.js.map +1 -0
  130. package/dist/workflow/plan-mode.d.ts +80 -3
  131. package/dist/workflow/plan-mode.d.ts.map +1 -1
  132. package/dist/workflow/plan-mode.js +767 -49
  133. package/dist/workflow/plan-mode.js.map +1 -1
  134. package/dist/workflow/plan-storage.d.ts +386 -0
  135. package/dist/workflow/plan-storage.d.ts.map +1 -0
  136. package/dist/workflow/plan-storage.js +878 -0
  137. package/dist/workflow/plan-storage.js.map +1 -0
  138. package/dist/workflow/project-verification.d.ts +37 -0
  139. package/dist/workflow/project-verification.d.ts.map +1 -0
  140. package/dist/workflow/project-verification.js +381 -0
  141. package/dist/workflow/project-verification.js.map +1 -0
  142. package/dist/workflow/task-workflow.d.ts +37 -0
  143. package/dist/workflow/task-workflow.d.ts.map +1 -0
  144. package/dist/workflow/task-workflow.js +386 -0
  145. package/dist/workflow/task-workflow.js.map +1 -0
  146. package/dist/workflow/test-runner.d.ts +9 -0
  147. package/dist/workflow/test-runner.d.ts.map +1 -1
  148. package/dist/workflow/test-runner.js +101 -5
  149. package/dist/workflow/test-runner.js.map +1 -1
  150. package/dist/workflow/ui-designer.d.ts +82 -0
  151. package/dist/workflow/ui-designer.d.ts.map +1 -0
  152. package/dist/workflow/ui-designer.js +234 -0
  153. package/dist/workflow/ui-designer.js.map +1 -0
  154. package/dist/workflow/ui-setup.d.ts +58 -0
  155. package/dist/workflow/ui-setup.d.ts.map +1 -0
  156. package/dist/workflow/ui-setup.js +685 -0
  157. package/dist/workflow/ui-setup.js.map +1 -0
  158. package/dist/workflow/ui-verification.d.ts +114 -0
  159. package/dist/workflow/ui-verification.d.ts.map +1 -0
  160. package/dist/workflow/ui-verification.js +258 -0
  161. package/dist/workflow/ui-verification.js.map +1 -0
  162. package/dist/workflow/workflow-logger.d.ts +110 -0
  163. package/dist/workflow/workflow-logger.d.ts.map +1 -0
  164. package/dist/workflow/workflow-logger.js +267 -0
  165. package/dist/workflow/workflow-logger.js.map +1 -0
  166. package/dist/workflow/workspace-manager.d.ts +342 -0
  167. package/dist/workflow/workspace-manager.d.ts.map +1 -0
  168. package/dist/workflow/workspace-manager.js +733 -0
  169. package/dist/workflow/workspace-manager.js.map +1 -0
  170. package/package.json +2 -2
  171. package/src/adapters/claude.ts +1067 -47
  172. package/src/adapters/gemini.ts +373 -0
  173. package/src/adapters/grok.ts +492 -0
  174. package/src/adapters/openai.ts +48 -9
  175. package/src/auth/claude.ts +120 -78
  176. package/src/auth/gemini.ts +207 -0
  177. package/src/auth/grok.ts +255 -0
  178. package/src/auth/index.ts +47 -9
  179. package/src/auth/keychain.ts +95 -28
  180. package/src/auth/openai.ts +29 -36
  181. package/src/cli/commands/auth.ts +89 -10
  182. package/src/cli/commands/create.ts +13 -4
  183. package/src/cli/interactive.ts +1774 -142
  184. package/src/config/defaults.ts +19 -2
  185. package/src/config/index.ts +36 -1
  186. package/src/config/schema.ts +30 -1
  187. package/src/generators/fullstack.ts +551 -0
  188. package/src/generators/index.ts +25 -1
  189. package/src/generators/python.ts +65 -20
  190. package/src/generators/templates/fullstack.ts +1047 -0
  191. package/src/generators/typescript.ts +69 -20
  192. package/src/state/index.ts +713 -4
  193. package/src/state/registry.ts +278 -0
  194. package/src/types/cli.ts +8 -0
  195. package/src/types/consensus.ts +197 -6
  196. package/src/types/project.ts +82 -1
  197. package/src/types/workflow.ts +90 -1
  198. package/src/workflow/auto-fix.ts +340 -0
  199. package/src/workflow/consensus.ts +1180 -16
  200. package/src/workflow/execution-mode.ts +673 -74
  201. package/src/workflow/index.ts +95 -6
  202. package/src/workflow/milestone-workflow.ts +576 -0
  203. package/src/workflow/plan-mode.ts +924 -50
  204. package/src/workflow/plan-storage.ts +1282 -0
  205. package/src/workflow/project-verification.ts +471 -0
  206. package/src/workflow/task-workflow.ts +528 -0
  207. package/src/workflow/test-runner.ts +120 -5
  208. package/src/workflow/ui-designer.ts +337 -0
  209. package/src/workflow/ui-setup.ts +797 -0
  210. package/src/workflow/ui-verification.ts +357 -0
  211. package/src/workflow/workflow-logger.ts +353 -0
  212. package/src/workflow/workspace-manager.ts +912 -0
  213. package/tests/config/config.test.ts +1 -1
  214. package/tests/types/consensus.test.ts +3 -3
  215. package/tests/workflow/plan-mode.test.ts +213 -0
  216. package/tests/workflow/test-runner.test.ts +5 -3
@@ -0,0 +1,1282 @@
1
+ /**
2
+ * Plan Storage System
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/
32
+ */
33
+
34
+ import { promises as fs } from 'node:fs';
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';
47
+
48
+ /**
49
+ * Feedback entry from a reviewer
50
+ */
51
+ export interface ReviewerFeedback {
52
+ reviewer: 'openai' | 'gemini' | 'grok' | 'claude';
53
+ score: number;
54
+ timestamp: string;
55
+ concerns: string[];
56
+ recommendations: string[];
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;
74
+ }
75
+
76
+ /**
77
+ * Plan metadata for tracking
78
+ */
79
+ export interface PlanMetadata {
80
+ id: string;
81
+ type: 'master' | 'milestone' | 'task';
82
+ milestoneId?: string;
83
+ milestoneName?: string;
84
+ taskId?: string;
85
+ taskName?: string;
86
+ version: number;
87
+ createdAt: string;
88
+ updatedAt: string;
89
+ consensusScore?: number;
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[];
106
+ }
107
+
108
+ /**
109
+ * Stored plan with metadata
110
+ */
111
+ export interface StoredPlan {
112
+ metadata: PlanMetadata;
113
+ content: string;
114
+ feedback: ReviewerFeedback[];
115
+ revisionHistory: Array<{
116
+ version: number;
117
+ timestamp: string;
118
+ changes: string;
119
+ score?: number;
120
+ }>;
121
+ }
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
+
140
+ /**
141
+ * Plan Storage Manager
142
+ */
143
+ export class PlanStorage {
144
+ private projectDir: string;
145
+ private plansDir: string;
146
+ private isFullstack: boolean;
147
+
148
+ constructor(projectDir: string, isFullstack: boolean = false) {
149
+ this.projectDir = projectDir;
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;
159
+ }
160
+
161
+ /**
162
+ * Initialize the plans directory structure
163
+ */
164
+ async initialize(): Promise<void> {
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 });
181
+ }
182
+
183
+ /**
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
190
+ */
191
+ private getPlanPath(
192
+ type: 'master' | 'milestone' | 'task',
193
+ milestoneId?: string,
194
+ taskId?: string
195
+ ): string {
196
+ if (type === 'master') {
197
+ if (this.isFullstack) {
198
+ return path.join(this.plansDir, 'master', 'plan.md');
199
+ }
200
+ return path.join(this.projectDir, 'docs', 'PLAN.md');
201
+ }
202
+
203
+ if (type === 'milestone' && milestoneId) {
204
+ const milestoneDir = path.join(this.plansDir, `milestone-${milestoneId}`);
205
+ return path.join(milestoneDir, 'plan.md');
206
+ }
207
+
208
+ if (type === 'task' && milestoneId && taskId) {
209
+ const milestoneDir = path.join(this.plansDir, `milestone-${milestoneId}`);
210
+ if (this.isFullstack) {
211
+ return path.join(milestoneDir, 'tasks', `task-${taskId}`, 'plan.md');
212
+ }
213
+ return path.join(milestoneDir, `task-${taskId}-plan.md`);
214
+ }
215
+
216
+ throw new Error(`Invalid plan type or missing IDs: ${type}`);
217
+ }
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
+
242
+ /**
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
247
+ */
248
+ private getFeedbackPath(
249
+ milestoneId: string,
250
+ taskId?: string,
251
+ appTarget?: FeedbackAppTarget
252
+ ): string {
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
263
+ if (taskId) {
264
+ return path.join(milestoneDir, `task-${taskId}-feedback.md`);
265
+ }
266
+ return path.join(milestoneDir, 'feedback.md');
267
+ }
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
+
279
+ /**
280
+ * Get the path for metadata file
281
+ */
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
+
291
+ const milestoneDir = path.join(this.plansDir, `milestone-${milestoneId}`);
292
+ if (taskId) {
293
+ if (this.isFullstack) {
294
+ return path.join(milestoneDir, 'tasks', `task-${taskId}`, 'metadata.json');
295
+ }
296
+ return path.join(milestoneDir, `task-${taskId}-metadata.json`);
297
+ }
298
+ return path.join(milestoneDir, 'metadata.json');
299
+ }
300
+
301
+ /**
302
+ * Save a plan to file
303
+ */
304
+ async savePlan(
305
+ content: string,
306
+ type: 'master' | 'milestone' | 'task',
307
+ options: {
308
+ milestoneId?: string;
309
+ milestoneName?: string;
310
+ taskId?: string;
311
+ taskName?: string;
312
+ score?: number;
313
+ frontendScore?: number;
314
+ backendScore?: number;
315
+ unifiedScore?: number;
316
+ } = {}
317
+ ): Promise<string> {
318
+ const planPath = this.getPlanPath(type, options.milestoneId, options.taskId);
319
+
320
+ // Ensure directory exists
321
+ await fs.mkdir(path.dirname(planPath), { recursive: true });
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
+
329
+ // Add header with metadata
330
+ const header = this.generatePlanHeader(type, options);
331
+ const fullContent = `${header}\n\n${content}`;
332
+
333
+ await fs.writeFile(planPath, fullContent, 'utf-8');
334
+
335
+ // Save metadata separately for easy parsing
336
+ await this.saveMetadata(type, options);
337
+
338
+ return planPath;
339
+ }
340
+
341
+ /**
342
+ * Generate plan header with tracking info
343
+ */
344
+ private generatePlanHeader(
345
+ type: 'master' | 'milestone' | 'task',
346
+ options: {
347
+ milestoneId?: string;
348
+ milestoneName?: string;
349
+ taskId?: string;
350
+ taskName?: string;
351
+ score?: number;
352
+ frontendScore?: number;
353
+ backendScore?: number;
354
+ unifiedScore?: number;
355
+ }
356
+ ): string {
357
+ const lines: string[] = [];
358
+ lines.push('---');
359
+ lines.push(`type: ${type}`);
360
+ if (options.milestoneId) lines.push(`milestone_id: ${options.milestoneId}`);
361
+ if (options.milestoneName) lines.push(`milestone_name: ${options.milestoneName}`);
362
+ if (options.taskId) lines.push(`task_id: ${options.taskId}`);
363
+ if (options.taskName) lines.push(`task_name: ${options.taskName}`);
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
+
374
+ lines.push(`updated_at: ${new Date().toISOString()}`);
375
+ lines.push('---');
376
+ return lines.join('\n');
377
+ }
378
+
379
+ /**
380
+ * Save metadata to JSON file
381
+ */
382
+ private async saveMetadata(
383
+ type: 'master' | 'milestone' | 'task',
384
+ options: {
385
+ milestoneId?: string;
386
+ milestoneName?: string;
387
+ taskId?: string;
388
+ taskName?: string;
389
+ score?: number;
390
+ frontendScore?: number;
391
+ backendScore?: number;
392
+ unifiedScore?: number;
393
+ }
394
+ ): Promise<void> {
395
+ const metadataPath = this.getMetadataPath(type, options.milestoneId, options.taskId);
396
+
397
+ // Ensure directory exists
398
+ await fs.mkdir(path.dirname(metadataPath), { recursive: true });
399
+
400
+ let metadata: PlanMetadata;
401
+ try {
402
+ const existing = await fs.readFile(metadataPath, 'utf-8');
403
+ metadata = JSON.parse(existing);
404
+ metadata.version += 1;
405
+ metadata.updatedAt = new Date().toISOString();
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
+ }
414
+ } catch {
415
+ metadata = {
416
+ id: options.taskId || options.milestoneId || 'master',
417
+ type,
418
+ milestoneId: options.milestoneId,
419
+ milestoneName: options.milestoneName,
420
+ taskId: options.taskId,
421
+ taskName: options.taskName,
422
+ version: 1,
423
+ createdAt: new Date().toISOString(),
424
+ updatedAt: new Date().toISOString(),
425
+ consensusScore: options.score,
426
+ status: 'draft',
427
+ isFullstack: this.isFullstack,
428
+ frontendScore: options.frontendScore,
429
+ backendScore: options.backendScore,
430
+ unifiedScore: options.unifiedScore,
431
+ totalIterations: 0,
432
+ corrections: [],
433
+ };
434
+ }
435
+
436
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
437
+ }
438
+
439
+ /**
440
+ * Load a plan from file
441
+ */
442
+ async loadPlan(
443
+ type: 'master' | 'milestone' | 'task',
444
+ milestoneId?: string,
445
+ taskId?: string
446
+ ): Promise<string | null> {
447
+ try {
448
+ const planPath = this.getPlanPath(type, milestoneId, taskId);
449
+ const content = await fs.readFile(planPath, 'utf-8');
450
+
451
+ // Strip the header if present
452
+ const headerMatch = content.match(/^---[\s\S]*?---\n\n/);
453
+ if (headerMatch) {
454
+ return content.slice(headerMatch[0].length);
455
+ }
456
+ return content;
457
+ } catch {
458
+ return null;
459
+ }
460
+ }
461
+
462
+ /**
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
469
+ */
470
+ async saveFeedback(
471
+ feedback: ReviewerFeedback,
472
+ milestoneId: string,
473
+ taskId?: string,
474
+ appTarget?: FeedbackAppTarget
475
+ ): Promise<void> {
476
+ const effectiveAppTarget = this.isFullstack ? (appTarget || 'unified') : undefined;
477
+ const feedbackPath = this.getFeedbackPath(milestoneId, taskId, effectiveAppTarget);
478
+
479
+ // Ensure directory exists
480
+ await fs.mkdir(path.dirname(feedbackPath), { recursive: true });
481
+
482
+ // Load existing feedback
483
+ let existingFeedback: ReviewerFeedback[] = [];
484
+ try {
485
+ const content = await fs.readFile(feedbackPath.replace('.md', '.json'), 'utf-8');
486
+ existingFeedback = JSON.parse(content);
487
+ } catch {
488
+ // No existing feedback
489
+ }
490
+
491
+ // Tag feedback with app target
492
+ const taggedFeedback: ReviewerFeedback = {
493
+ ...feedback,
494
+ appTarget: effectiveAppTarget,
495
+ };
496
+
497
+ // Add new feedback
498
+ existingFeedback.push(taggedFeedback);
499
+
500
+ // Save JSON for programmatic access
501
+ await fs.writeFile(
502
+ feedbackPath.replace('.md', '.json'),
503
+ JSON.stringify(existingFeedback, null, 2),
504
+ 'utf-8'
505
+ );
506
+
507
+ // Also save human-readable markdown
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);
604
+ await fs.writeFile(feedbackPath, mdContent, 'utf-8');
605
+ }
606
+
607
+ /**
608
+ * Load all feedback for a plan
609
+ */
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[]> {
654
+ try {
655
+ const effectiveAppTarget = this.isFullstack ? (appTarget || 'unified') : undefined;
656
+ const feedbackPath = this.getMasterFeedbackPath(effectiveAppTarget).replace('.md', '.json');
657
+ const content = await fs.readFile(feedbackPath, 'utf-8');
658
+ return JSON.parse(content);
659
+ } catch {
660
+ return [];
661
+ }
662
+ }
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
+
686
+ /**
687
+ * Clear feedback for a new consensus round
688
+ */
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);
724
+ try {
725
+ await fs.unlink(feedbackPath);
726
+ await fs.unlink(feedbackPath.replace('.md', '.json'));
727
+ } catch {
728
+ // Files don't exist, that's fine
729
+ }
730
+ }
731
+
732
+ /**
733
+ * Format feedback as readable markdown
734
+ */
735
+ private formatFeedbackAsMarkdown(
736
+ feedback: ReviewerFeedback[],
737
+ appTarget?: FeedbackAppTarget
738
+ ): string {
739
+ const lines: string[] = [];
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
+ }
748
+
749
+ for (const fb of feedback) {
750
+ lines.push(`## ${fb.reviewer.toUpperCase()} Review`);
751
+ lines.push(`- **Score:** ${fb.score}%`);
752
+ lines.push(`- **Timestamp:** ${fb.timestamp}`);
753
+ if (fb.appTarget) {
754
+ lines.push(`- **App Target:** ${fb.appTarget}`);
755
+ }
756
+ lines.push('');
757
+
758
+ if (fb.concerns.length > 0) {
759
+ lines.push('### Concerns');
760
+ for (const concern of fb.concerns) {
761
+ lines.push(`- ${concern}`);
762
+ }
763
+ lines.push('');
764
+ }
765
+
766
+ if (fb.recommendations.length > 0) {
767
+ lines.push('### Recommendations');
768
+ for (const rec of fb.recommendations) {
769
+ lines.push(`- ${rec}`);
770
+ }
771
+ lines.push('');
772
+ }
773
+
774
+ if (fb.analysis) {
775
+ lines.push('### Analysis');
776
+ lines.push(fb.analysis);
777
+ lines.push('');
778
+ }
779
+
780
+ lines.push('---\n');
781
+ }
782
+
783
+ return lines.join('\n');
784
+ }
785
+
786
+ /**
787
+ * Get combined feedback summary for revision
788
+ */
789
+ async getCombinedFeedbackForRevision(
790
+ milestoneId: string,
791
+ taskId?: string,
792
+ appTarget?: FeedbackAppTarget
793
+ ): Promise<{
794
+ averageScore: number;
795
+ allConcerns: string[];
796
+ allRecommendations: string[];
797
+ combinedAnalysis: string;
798
+ }> {
799
+ const feedback = await this.loadFeedback(milestoneId, taskId, appTarget);
800
+
801
+ if (feedback.length === 0) {
802
+ return {
803
+ averageScore: 0,
804
+ allConcerns: [],
805
+ allRecommendations: [],
806
+ combinedAnalysis: '',
807
+ };
808
+ }
809
+
810
+ const averageScore = feedback.reduce((sum, f) => sum + f.score, 0) / feedback.length;
811
+
812
+ // Deduplicate concerns and recommendations
813
+ const allConcerns = [...new Set(feedback.flatMap(f => f.concerns))];
814
+ const allRecommendations = [...new Set(feedback.flatMap(f => f.recommendations))];
815
+
816
+ // Combine analysis
817
+ const combinedAnalysis = feedback
818
+ .map(f => `### ${f.reviewer.toUpperCase()} (${f.score}%)\n${f.analysis}`)
819
+ .join('\n\n');
820
+
821
+ return {
822
+ averageScore,
823
+ allConcerns,
824
+ allRecommendations,
825
+ combinedAnalysis,
826
+ };
827
+ }
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
+
876
+ /**
877
+ * Update plan status
878
+ */
879
+ async updateStatus(
880
+ status: 'draft' | 'reviewing' | 'approved' | 'implemented',
881
+ type: 'master' | 'milestone' | 'task',
882
+ milestoneId?: string,
883
+ taskId?: string
884
+ ): Promise<void> {
885
+ const metadataPath = this.getMetadataPath(type, milestoneId, taskId);
886
+
887
+ try {
888
+ const content = await fs.readFile(metadataPath, 'utf-8');
889
+ const metadata: PlanMetadata = JSON.parse(content);
890
+ metadata.status = status;
891
+ metadata.updatedAt = new Date().toISOString();
892
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
893
+ } catch {
894
+ // Metadata doesn't exist yet
895
+ }
896
+ }
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
+
978
+ /**
979
+ * Get plan tracking summary for a milestone
980
+ */
981
+ async getMilestoneTrackingSummary(milestoneId: string): Promise<{
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
+ };
993
+ taskPlans: Array<{
994
+ taskId: string;
995
+ taskName?: string;
996
+ exists: boolean;
997
+ score?: number;
998
+ status?: string;
999
+ frontendScore?: number;
1000
+ backendScore?: number;
1001
+ unifiedScore?: number;
1002
+ }>;
1003
+ }> {
1004
+ const milestoneDir = path.join(this.plansDir, `milestone-${milestoneId}`);
1005
+
1006
+ // Check milestone plan
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
+
1019
+ try {
1020
+ const metadataPath = path.join(milestoneDir, 'metadata.json');
1021
+ const content = await fs.readFile(metadataPath, 'utf-8');
1022
+ const metadata: PlanMetadata = JSON.parse(content);
1023
+ milestonePlan = {
1024
+ exists: true,
1025
+ score: metadata.consensusScore,
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,
1033
+ };
1034
+ } catch {
1035
+ // No milestone plan
1036
+ }
1037
+
1038
+ // Find task plans
1039
+ const taskPlans: Array<{
1040
+ taskId: string;
1041
+ taskName?: string;
1042
+ exists: boolean;
1043
+ score?: number;
1044
+ status?: string;
1045
+ frontendScore?: number;
1046
+ backendScore?: number;
1047
+ unifiedScore?: number;
1048
+ }> = [];
1049
+
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
1083
+ const files = await fs.readdir(milestoneDir);
1084
+ const taskMetadataFiles = files.filter(f => f.startsWith('task-') && f.endsWith('-metadata.json'));
1085
+
1086
+ for (const file of taskMetadataFiles) {
1087
+ try {
1088
+ const content = await fs.readFile(path.join(milestoneDir, file), 'utf-8');
1089
+ const metadata: PlanMetadata = JSON.parse(content);
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
+ }
1103
+ } catch {
1104
+ // Skip invalid files
1105
+ }
1106
+ }
1107
+ } catch {
1108
+ // Directory doesn't exist
1109
+ }
1110
+
1111
+ return { milestonePlan, taskPlans };
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
+ }
1274
+ }
1275
+
1276
+
1277
+ /**
1278
+ * Create a plan storage instance for a project
1279
+ */
1280
+ export function createPlanStorage(projectDir: string, isFullstack: boolean = false): PlanStorage {
1281
+ return new PlanStorage(projectDir, isFullstack);
1282
+ }