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.
- package/.env.example +24 -1
- package/CONTRIBUTING.md +275 -0
- package/OPEN_SOURCE_MANIFESTO.md +172 -0
- package/README.md +340 -27
- package/dist/adapters/claude.d.ts +28 -2
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +273 -20
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/grok.d.ts +73 -0
- package/dist/adapters/grok.d.ts.map +1 -0
- package/dist/adapters/grok.js +430 -0
- package/dist/adapters/grok.js.map +1 -0
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +6 -1
- package/dist/adapters/openai.js.map +1 -1
- package/dist/auth/grok.d.ts +73 -0
- package/dist/auth/grok.d.ts.map +1 -0
- package/dist/auth/grok.js +211 -0
- package/dist/auth/grok.js.map +1 -0
- package/dist/auth/index.d.ts +9 -6
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +23 -6
- package/dist/auth/index.js.map +1 -1
- package/dist/cli/commands/auth.d.ts +1 -1
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +79 -8
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +15 -4
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +406 -35
- package/dist/cli/interactive.js.map +1 -1
- package/dist/config/defaults.d.ts +3 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +9 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +16 -3
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +27 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +24 -3
- package/dist/config/schema.js.map +1 -1
- package/dist/generators/fullstack.d.ts +32 -0
- package/dist/generators/fullstack.d.ts.map +1 -0
- package/dist/generators/fullstack.js +497 -0
- package/dist/generators/fullstack.js.map +1 -0
- package/dist/generators/index.d.ts +4 -3
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +15 -1
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/python.d.ts +17 -1
- package/dist/generators/python.d.ts.map +1 -1
- package/dist/generators/python.js +34 -21
- package/dist/generators/python.js.map +1 -1
- package/dist/generators/templates/fullstack.d.ts +113 -0
- package/dist/generators/templates/fullstack.d.ts.map +1 -0
- package/dist/generators/templates/fullstack.js +1004 -0
- package/dist/generators/templates/fullstack.js.map +1 -0
- package/dist/generators/typescript.d.ts +19 -1
- package/dist/generators/typescript.d.ts.map +1 -1
- package/dist/generators/typescript.js +37 -21
- package/dist/generators/typescript.js.map +1 -1
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/cli.js.map +1 -1
- package/dist/types/consensus.d.ts +119 -2
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +12 -1
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/project.d.ts +76 -0
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +1 -1
- package/dist/types/project.js.map +1 -1
- package/dist/types/workflow.d.ts +170 -16
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +26 -3
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/consensus.d.ts +29 -3
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +334 -27
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts +2 -0
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +20 -0
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +2 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +11 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/milestone-workflow.d.ts +2 -0
- package/dist/workflow/milestone-workflow.d.ts.map +1 -1
- package/dist/workflow/milestone-workflow.js +19 -2
- package/dist/workflow/milestone-workflow.js.map +1 -1
- package/dist/workflow/plan-mode.d.ts +66 -2
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +187 -11
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/plan-storage.d.ts +252 -8
- package/dist/workflow/plan-storage.d.ts.map +1 -1
- package/dist/workflow/plan-storage.js +580 -33
- package/dist/workflow/plan-storage.js.map +1 -1
- package/dist/workflow/project-verification.js +1 -1
- package/dist/workflow/project-verification.js.map +1 -1
- package/dist/workflow/task-workflow.d.ts +2 -0
- package/dist/workflow/task-workflow.d.ts.map +1 -1
- package/dist/workflow/task-workflow.js +23 -1
- package/dist/workflow/task-workflow.js.map +1 -1
- package/dist/workflow/test-runner.d.ts +8 -0
- package/dist/workflow/test-runner.d.ts.map +1 -1
- package/dist/workflow/test-runner.js +92 -0
- package/dist/workflow/test-runner.js.map +1 -1
- package/dist/workflow/workspace-manager.d.ts +342 -0
- package/dist/workflow/workspace-manager.d.ts.map +1 -0
- package/dist/workflow/workspace-manager.js +733 -0
- package/dist/workflow/workspace-manager.js.map +1 -0
- package/package.json +1 -1
- package/src/adapters/claude.ts +322 -25
- package/src/adapters/grok.ts +492 -0
- package/src/adapters/openai.ts +8 -2
- package/src/auth/grok.ts +255 -0
- package/src/auth/index.ts +27 -9
- package/src/cli/commands/auth.ts +89 -10
- package/src/cli/commands/create.ts +13 -4
- package/src/cli/interactive.ts +453 -34
- package/src/config/defaults.ts +9 -0
- package/src/config/index.ts +17 -3
- package/src/config/schema.ts +25 -3
- package/src/generators/fullstack.ts +551 -0
- package/src/generators/index.ts +25 -1
- package/src/generators/python.ts +65 -21
- package/src/generators/templates/fullstack.ts +1047 -0
- package/src/generators/typescript.ts +69 -21
- package/src/types/cli.ts +4 -0
- package/src/types/consensus.ts +135 -3
- package/src/types/project.ts +82 -1
- package/src/types/workflow.ts +58 -4
- package/src/workflow/consensus.ts +461 -31
- package/src/workflow/execution-mode.ts +32 -0
- package/src/workflow/index.ts +12 -0
- package/src/workflow/milestone-workflow.ts +24 -2
- package/src/workflow/plan-mode.ts +238 -10
- package/src/workflow/plan-storage.ts +835 -35
- package/src/workflow/project-verification.ts +1 -1
- package/src/workflow/task-workflow.ts +29 -1
- package/src/workflow/test-runner.ts +110 -0
- 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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
395
|
+
const metadataPath = this.getMetadataPath(type, options.milestoneId, options.taskId);
|
|
192
396
|
|
|
193
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
735
|
+
private formatFeedbackAsMarkdown(
|
|
736
|
+
feedback: ReviewerFeedback[],
|
|
737
|
+
appTarget?: FeedbackAppTarget
|
|
738
|
+
): string {
|
|
311
739
|
const lines: string[] = [];
|
|
312
|
-
|
|
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
|
-
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
}
|