@syntesseraai/opencode-feature-factory 0.7.2 → 0.7.3
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/agents/building.md +1 -0
- package/agents/documenting.md +1 -0
- package/agents/planning.md +1 -0
- package/agents/reviewing.md +1 -0
- package/dist/tools/mini-loop.js +187 -15
- package/dist/tools/pipeline.js +361 -22
- package/package.json +1 -1
package/agents/building.md
CHANGED
package/agents/documenting.md
CHANGED
package/agents/planning.md
CHANGED
package/agents/reviewing.md
CHANGED
package/dist/tools/mini-loop.js
CHANGED
|
@@ -11,6 +11,7 @@ import { parseMiniReview, parseDocReview } from './parsers.js';
|
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
// Tool factory
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
|
+
const formatElapsed = (startMs, endMs) => `${((endMs - startMs) / 1000).toFixed(1)}s`;
|
|
14
15
|
export function createMiniLoopTool(client) {
|
|
15
16
|
return tool({
|
|
16
17
|
description: 'Run the Feature Factory mini-loop: build → review (with rework loop) → documentation. ' +
|
|
@@ -38,6 +39,7 @@ export function createMiniLoopTool(client) {
|
|
|
38
39
|
.describe('provider/model for documentation review. Defaults to opencode/gemini-3.1-pro.'),
|
|
39
40
|
},
|
|
40
41
|
async execute(args, context) {
|
|
42
|
+
const totalStartMs = Date.now();
|
|
41
43
|
const sessionId = context.sessionID;
|
|
42
44
|
const { requirements } = args;
|
|
43
45
|
// Resolve models — use provided overrides or fall back to defaults
|
|
@@ -62,71 +64,241 @@ export function createMiniLoopTool(client) {
|
|
|
62
64
|
// ===================================================================
|
|
63
65
|
// PHASE 1: IMPLEMENTATION LOOP (build → review → gate, up to 10)
|
|
64
66
|
// ===================================================================
|
|
67
|
+
const implementationStartMs = Date.now();
|
|
68
|
+
const implementationIterationDetails = [];
|
|
65
69
|
let implGate = { decision: 'REWORK', feedback: requirements };
|
|
66
70
|
let lastImplRaw = '';
|
|
67
71
|
for (let implIter = 0; implIter < 10 && implGate.decision === 'REWORK'; implIter++) {
|
|
72
|
+
const iteration = implIter + 1;
|
|
73
|
+
const buildTitle = `ff-mini-build-${iteration}`;
|
|
74
|
+
const reviewTitle = `ff-mini-review-${iteration}`;
|
|
68
75
|
const buildInput = implIter === 0
|
|
69
76
|
? requirements
|
|
70
77
|
: `${requirements}\n\nPrevious review feedback:\n${implGate.feedback}`;
|
|
78
|
+
context.metadata({
|
|
79
|
+
title: `⏳ Building (iteration ${iteration}/10)...`,
|
|
80
|
+
metadata: {
|
|
81
|
+
phase: 'implementation',
|
|
82
|
+
step: 'build',
|
|
83
|
+
iteration,
|
|
84
|
+
maxIterations: 10,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
71
87
|
// Build
|
|
72
|
-
|
|
88
|
+
const buildStartMs = Date.now();
|
|
89
|
+
lastImplRaw = await promptSession(client, sessionId, miniBuildPrompt(buildInput, implIter > 0 ? implGate.feedback : undefined), { model: buildModel, agent: 'building', title: buildTitle });
|
|
90
|
+
const buildEndMs = Date.now();
|
|
91
|
+
context.metadata({
|
|
92
|
+
title: `⏳ Reviewing (iteration ${iteration}/10)...`,
|
|
93
|
+
metadata: {
|
|
94
|
+
phase: 'implementation',
|
|
95
|
+
step: 'review',
|
|
96
|
+
iteration,
|
|
97
|
+
maxIterations: 10,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
73
100
|
// Review
|
|
101
|
+
const reviewStartMs = Date.now();
|
|
74
102
|
const reviewRaw = await promptSession(client, sessionId, miniReviewPrompt(lastImplRaw), {
|
|
75
103
|
model: reviewModel,
|
|
76
104
|
agent: 'reviewing',
|
|
77
|
-
title:
|
|
105
|
+
title: reviewTitle,
|
|
78
106
|
});
|
|
107
|
+
const reviewEndMs = Date.now();
|
|
79
108
|
const review = parseMiniReview(reviewRaw);
|
|
80
109
|
// Gate (deterministic)
|
|
81
|
-
implGate = evaluateMiniLoopImplGate(review,
|
|
110
|
+
implGate = evaluateMiniLoopImplGate(review, iteration);
|
|
82
111
|
if (implGate.decision === 'APPROVED') {
|
|
83
|
-
|
|
112
|
+
context.metadata({
|
|
113
|
+
title: `✅ Implementation APPROVED (confidence: ${review.confidence}, iteration: ${iteration})`,
|
|
114
|
+
metadata: {
|
|
115
|
+
phase: 'implementation',
|
|
116
|
+
step: 'gate',
|
|
117
|
+
decision: implGate.decision,
|
|
118
|
+
confidence: review.confidence,
|
|
119
|
+
iteration,
|
|
120
|
+
maxIterations: 10,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
84
123
|
}
|
|
85
124
|
else if (implGate.decision === 'ESCALATE') {
|
|
86
|
-
|
|
125
|
+
context.metadata({
|
|
126
|
+
title: '⚠️ Implementation ESCALATED',
|
|
127
|
+
metadata: {
|
|
128
|
+
phase: 'implementation',
|
|
129
|
+
step: 'gate',
|
|
130
|
+
decision: implGate.decision,
|
|
131
|
+
confidence: review.confidence,
|
|
132
|
+
iteration,
|
|
133
|
+
maxIterations: 10,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
context.metadata({
|
|
139
|
+
title: `🔄 Rework required (confidence: ${review.confidence}, iteration: ${iteration}/10)`,
|
|
140
|
+
metadata: {
|
|
141
|
+
phase: 'implementation',
|
|
142
|
+
step: 'gate',
|
|
143
|
+
decision: implGate.decision,
|
|
144
|
+
confidence: review.confidence,
|
|
145
|
+
iteration,
|
|
146
|
+
maxIterations: 10,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const feedback = review.reworkInstructions || implGate.feedback || review.raw;
|
|
151
|
+
implementationIterationDetails.push(`### Iteration ${iteration}\n` +
|
|
152
|
+
`- **Build**: ${buildTitle} (${formatElapsed(buildStartMs, buildEndMs)})\n` +
|
|
153
|
+
`- **Review**: ${reviewTitle} (${formatElapsed(reviewStartMs, reviewEndMs)})\n` +
|
|
154
|
+
`- **Gate**: ${implGate.decision} (confidence: ${review.confidence}, change requested: ${review.changeRequested ? 'yes' : 'no'}, unresolved: ${review.unresolvedIssues})\n` +
|
|
155
|
+
`- **Feedback**: ${feedback}`);
|
|
156
|
+
if (implGate.decision === 'ESCALATE') {
|
|
157
|
+
const implementationEndMs = Date.now();
|
|
158
|
+
addReport('Implementation', `${implementationIterationDetails.join('\n\n')}\n\n**Outcome**: ESCALATE: ${implGate.reason}\n**Phase time**: ${formatElapsed(implementationStartMs, implementationEndMs)}`);
|
|
159
|
+
context.metadata({
|
|
160
|
+
title: '⚠️ Mini-loop finished with issues',
|
|
161
|
+
metadata: {
|
|
162
|
+
phase: 'complete',
|
|
163
|
+
outcome: 'issues',
|
|
164
|
+
totalElapsedMs: Date.now() - totalStartMs,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
addReport('Complete', `Mini-loop finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
87
168
|
return report.join('\n\n');
|
|
88
169
|
}
|
|
89
170
|
// REWORK continues the loop
|
|
90
171
|
}
|
|
172
|
+
const implementationEndMs = Date.now();
|
|
173
|
+
addReport('Implementation', `${implementationIterationDetails.join('\n\n')}\n\n**Outcome**: ${implGate.decision === 'APPROVED'
|
|
174
|
+
? 'APPROVED'
|
|
175
|
+
: `REWORK exhausted (10 iterations). Last feedback: ${implGate.feedback}`}\n**Phase time**: ${formatElapsed(implementationStartMs, implementationEndMs)}`);
|
|
91
176
|
if (implGate.decision !== 'APPROVED') {
|
|
92
|
-
|
|
177
|
+
context.metadata({
|
|
178
|
+
title: '⚠️ Mini-loop finished with issues',
|
|
179
|
+
metadata: {
|
|
180
|
+
phase: 'complete',
|
|
181
|
+
outcome: 'issues',
|
|
182
|
+
totalElapsedMs: Date.now() - totalStartMs,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
addReport('Complete', `Mini-loop finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
93
186
|
return report.join('\n\n');
|
|
94
187
|
}
|
|
95
188
|
// ===================================================================
|
|
96
189
|
// PHASE 2: DOCUMENTATION LOOP (document → review → gate, up to 5)
|
|
97
190
|
// ===================================================================
|
|
191
|
+
const documentationStartMs = Date.now();
|
|
192
|
+
const documentationIterationDetails = [];
|
|
98
193
|
let docInput = lastImplRaw;
|
|
99
194
|
let docGate = { decision: 'REWORK' };
|
|
100
195
|
for (let docIter = 0; docIter < 5 && docGate.decision === 'REWORK'; docIter++) {
|
|
196
|
+
const iteration = docIter + 1;
|
|
197
|
+
const writeTitle = `ff-mini-doc-write-${iteration}`;
|
|
198
|
+
const reviewTitle = `ff-mini-doc-review-${iteration}`;
|
|
199
|
+
context.metadata({
|
|
200
|
+
title: `⏳ Writing documentation (iteration ${iteration}/5)...`,
|
|
201
|
+
metadata: {
|
|
202
|
+
phase: 'documentation',
|
|
203
|
+
step: 'write',
|
|
204
|
+
iteration,
|
|
205
|
+
maxIterations: 5,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
101
208
|
// Write docs
|
|
209
|
+
const writeStartMs = Date.now();
|
|
102
210
|
const docRaw = await promptSession(client, sessionId, documentPrompt(docInput), {
|
|
103
211
|
model: docModel,
|
|
104
212
|
agent: 'documenting',
|
|
105
|
-
title:
|
|
213
|
+
title: writeTitle,
|
|
214
|
+
});
|
|
215
|
+
const writeEndMs = Date.now();
|
|
216
|
+
context.metadata({
|
|
217
|
+
title: `⏳ Reviewing documentation (iteration ${iteration}/5)...`,
|
|
218
|
+
metadata: {
|
|
219
|
+
phase: 'documentation',
|
|
220
|
+
step: 'review',
|
|
221
|
+
iteration,
|
|
222
|
+
maxIterations: 5,
|
|
223
|
+
},
|
|
106
224
|
});
|
|
107
225
|
// Review docs
|
|
226
|
+
const reviewStartMs = Date.now();
|
|
108
227
|
const docRevRaw = await promptSession(client, sessionId, docReviewPrompt(docRaw), {
|
|
109
228
|
model: docReviewModel,
|
|
110
229
|
agent: 'reviewing',
|
|
111
|
-
title:
|
|
230
|
+
title: reviewTitle,
|
|
112
231
|
});
|
|
232
|
+
const reviewEndMs = Date.now();
|
|
113
233
|
const docReview = parseDocReview(docRevRaw);
|
|
114
234
|
// Gate (deterministic)
|
|
115
|
-
docGate = evaluateMiniLoopDocGate(docReview,
|
|
235
|
+
docGate = evaluateMiniLoopDocGate(docReview, iteration);
|
|
116
236
|
if (docGate.decision === 'APPROVED') {
|
|
117
|
-
|
|
237
|
+
context.metadata({
|
|
238
|
+
title: `✅ Documentation APPROVED (confidence: ${docReview.confidence}, iteration: ${iteration})`,
|
|
239
|
+
metadata: {
|
|
240
|
+
phase: 'documentation',
|
|
241
|
+
step: 'gate',
|
|
242
|
+
decision: docGate.decision,
|
|
243
|
+
confidence: docReview.confidence,
|
|
244
|
+
iteration,
|
|
245
|
+
maxIterations: 5,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
118
248
|
}
|
|
119
249
|
else if (docGate.decision === 'ESCALATE') {
|
|
120
|
-
|
|
250
|
+
context.metadata({
|
|
251
|
+
title: '⚠️ Documentation ESCALATED',
|
|
252
|
+
metadata: {
|
|
253
|
+
phase: 'documentation',
|
|
254
|
+
step: 'gate',
|
|
255
|
+
decision: docGate.decision,
|
|
256
|
+
confidence: docReview.confidence,
|
|
257
|
+
iteration,
|
|
258
|
+
maxIterations: 5,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
121
261
|
}
|
|
122
262
|
else {
|
|
263
|
+
context.metadata({
|
|
264
|
+
title: `🔄 Documentation rework required (confidence: ${docReview.confidence}, iteration: ${iteration}/5)`,
|
|
265
|
+
metadata: {
|
|
266
|
+
phase: 'documentation',
|
|
267
|
+
step: 'gate',
|
|
268
|
+
decision: docGate.decision,
|
|
269
|
+
confidence: docReview.confidence,
|
|
270
|
+
iteration,
|
|
271
|
+
maxIterations: 5,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const feedback = docReview.reworkInstructions || docGate.feedback || docReview.raw;
|
|
276
|
+
documentationIterationDetails.push(`### Iteration ${iteration}\n` +
|
|
277
|
+
`- **Write**: ${writeTitle} (${formatElapsed(writeStartMs, writeEndMs)})\n` +
|
|
278
|
+
`- **Review**: ${reviewTitle} (${formatElapsed(reviewStartMs, reviewEndMs)})\n` +
|
|
279
|
+
`- **Gate**: ${docGate.decision} (confidence: ${docReview.confidence}, unresolved: ${docReview.unresolvedIssues})\n` +
|
|
280
|
+
`- **Feedback**: ${feedback}`);
|
|
281
|
+
if (docGate.decision === 'REWORK') {
|
|
123
282
|
docInput = `${docInput}\n\nDocumentation review feedback:\n${docGate.feedback}`;
|
|
124
283
|
}
|
|
125
284
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
285
|
+
const documentationEndMs = Date.now();
|
|
286
|
+
addReport('Documentation', `${documentationIterationDetails.join('\n\n')}\n\n**Outcome**: ${docGate.decision === 'APPROVED'
|
|
287
|
+
? 'APPROVED'
|
|
288
|
+
: docGate.decision === 'ESCALATE'
|
|
289
|
+
? `ESCALATE: ${docGate.reason}`
|
|
290
|
+
: `REWORK exhausted (5 iterations). Last feedback: ${docGate.feedback}`}\n**Phase time**: ${formatElapsed(documentationStartMs, documentationEndMs)}`);
|
|
291
|
+
const totalEndMs = Date.now();
|
|
292
|
+
const completedWithoutIssues = docGate.decision === 'APPROVED';
|
|
293
|
+
context.metadata({
|
|
294
|
+
title: completedWithoutIssues ? '✅ Mini-loop complete' : '⚠️ Mini-loop finished with issues',
|
|
295
|
+
metadata: {
|
|
296
|
+
phase: 'complete',
|
|
297
|
+
outcome: completedWithoutIssues ? 'success' : 'issues',
|
|
298
|
+
totalElapsedMs: totalEndMs - totalStartMs,
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
addReport('Complete', `${completedWithoutIssues ? 'Mini-loop finished successfully.' : 'Mini-loop finished with issues.'}\n**Total time**: ${formatElapsed(totalStartMs, totalEndMs)}`);
|
|
130
302
|
return report.join('\n\n');
|
|
131
303
|
},
|
|
132
304
|
});
|
package/dist/tools/pipeline.js
CHANGED
|
@@ -13,6 +13,7 @@ import { parseConsensusPlan, parseReviewSynthesis, parseImplementationReport, pa
|
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// Tool factory — needs the SDK client from plugin init
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
|
+
const formatElapsed = (startMs, endMs) => `${((endMs - startMs) / 1000).toFixed(1)}s`;
|
|
16
17
|
export function createPipelineTool(client) {
|
|
17
18
|
return tool({
|
|
18
19
|
description: 'Run the full Feature Factory pipeline: multi-model planning → build → review → documentation. ' +
|
|
@@ -54,6 +55,7 @@ export function createPipelineTool(client) {
|
|
|
54
55
|
.describe('provider/model for documentation review. Defaults to the validate model.'),
|
|
55
56
|
},
|
|
56
57
|
async execute(args, context) {
|
|
58
|
+
const totalStartMs = Date.now();
|
|
57
59
|
const sessionId = context.sessionID;
|
|
58
60
|
const { requirements } = args;
|
|
59
61
|
// Resolve models — use provided overrides or fall back to defaults
|
|
@@ -91,136 +93,473 @@ export function createPipelineTool(client) {
|
|
|
91
93
|
// ===================================================================
|
|
92
94
|
// PHASE 1: PLANNING (fan-out → synthesize → gate, loop up to 5)
|
|
93
95
|
// ===================================================================
|
|
96
|
+
const planningStartMs = Date.now();
|
|
97
|
+
const planningIterationDetails = [];
|
|
94
98
|
let planningGate = { decision: 'REWORK', feedback: requirements };
|
|
95
99
|
let finalPlan = '';
|
|
96
100
|
for (let planIter = 0; planIter < 5 && planningGate.decision === 'REWORK'; planIter++) {
|
|
101
|
+
const iteration = planIter + 1;
|
|
102
|
+
const synthesisTitle = `ff-plan-synthesis-${iteration}`;
|
|
103
|
+
const fanOutTitles = planModels.map((model) => `ff-fanout-${model.tag}`);
|
|
97
104
|
const planInput = planIter === 0
|
|
98
105
|
? requirements
|
|
99
106
|
: `${requirements}\n\nPrevious feedback:\n${planningGate.feedback}`;
|
|
107
|
+
context.metadata({
|
|
108
|
+
title: `⏳ Planning fan-out (iteration ${iteration}/5)...`,
|
|
109
|
+
metadata: {
|
|
110
|
+
phase: 'planning',
|
|
111
|
+
step: 'fan_out',
|
|
112
|
+
iteration,
|
|
113
|
+
maxIterations: 5,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
100
116
|
// Fan-out: N models plan in parallel
|
|
117
|
+
const fanOutStartMs = Date.now();
|
|
101
118
|
const fanOutResults = await fanOut(client, sessionId, planModels, (tag) => planningPrompt(planInput, tag), 'planning');
|
|
119
|
+
const fanOutEndMs = Date.now();
|
|
120
|
+
context.metadata({
|
|
121
|
+
title: '⏳ Synthesizing plans...',
|
|
122
|
+
metadata: {
|
|
123
|
+
phase: 'planning',
|
|
124
|
+
step: 'synthesis',
|
|
125
|
+
iteration,
|
|
126
|
+
maxIterations: 5,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
102
129
|
// Synthesize consensus
|
|
130
|
+
const synthesisStartMs = Date.now();
|
|
103
131
|
const synthesisRaw = await promptSession(client, sessionId, synthesisPrompt(fanOutResults), {
|
|
104
132
|
model: orchestratorModel,
|
|
105
133
|
agent: 'planning',
|
|
106
|
-
title:
|
|
134
|
+
title: synthesisTitle,
|
|
107
135
|
});
|
|
136
|
+
const synthesisEndMs = Date.now();
|
|
108
137
|
const consensus = parseConsensusPlan(synthesisRaw);
|
|
109
138
|
// Gate (deterministic)
|
|
110
139
|
planningGate = evaluatePlanningGate(consensus);
|
|
111
140
|
if (planningGate.decision === 'APPROVED') {
|
|
141
|
+
context.metadata({
|
|
142
|
+
title: `✅ Planning APPROVED (score: ${consensus.consensusScore}, iteration: ${iteration})`,
|
|
143
|
+
metadata: {
|
|
144
|
+
phase: 'planning',
|
|
145
|
+
step: 'gate',
|
|
146
|
+
decision: planningGate.decision,
|
|
147
|
+
score: consensus.consensusScore,
|
|
148
|
+
iteration,
|
|
149
|
+
maxIterations: 5,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
112
152
|
finalPlan = consensus.synthesizedPlan || synthesisRaw;
|
|
113
|
-
addReport('Planning', `APPROVED (score: ${consensus.consensusScore}, iteration: ${planIter + 1})`);
|
|
114
153
|
}
|
|
115
154
|
else if (planningGate.decision === 'BLOCKED') {
|
|
116
|
-
|
|
155
|
+
context.metadata({
|
|
156
|
+
title: `⚠️ Planning BLOCKED (score: ${consensus.consensusScore}, iteration: ${iteration})`,
|
|
157
|
+
metadata: {
|
|
158
|
+
phase: 'planning',
|
|
159
|
+
step: 'gate',
|
|
160
|
+
decision: planningGate.decision,
|
|
161
|
+
score: consensus.consensusScore,
|
|
162
|
+
iteration,
|
|
163
|
+
maxIterations: 5,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
context.metadata({
|
|
169
|
+
title: `🔄 Planning rework required (score: ${consensus.consensusScore}, iteration: ${iteration}/5)`,
|
|
170
|
+
metadata: {
|
|
171
|
+
phase: 'planning',
|
|
172
|
+
step: 'gate',
|
|
173
|
+
decision: planningGate.decision,
|
|
174
|
+
score: consensus.consensusScore,
|
|
175
|
+
iteration,
|
|
176
|
+
maxIterations: 5,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const feedback = consensus.openQuestions || planningGate.feedback || consensus.divergentElements || 'N/A';
|
|
181
|
+
planningIterationDetails.push(`### Iteration ${iteration}\n` +
|
|
182
|
+
`- **Fan-out**: ${fanOutTitles.join(', ')} (${formatElapsed(fanOutStartMs, fanOutEndMs)})\n` +
|
|
183
|
+
`- **Synthesis**: ${synthesisTitle} (${formatElapsed(synthesisStartMs, synthesisEndMs)})\n` +
|
|
184
|
+
`- **Gate**: ${planningGate.decision} (score: ${consensus.consensusScore})\n` +
|
|
185
|
+
`- **Feedback**: ${feedback}`);
|
|
186
|
+
if (planningGate.decision === 'BLOCKED') {
|
|
187
|
+
const planningEndMs = Date.now();
|
|
188
|
+
addReport('Planning', `${planningIterationDetails.join('\n\n')}\n\n**Outcome**: BLOCKED: ${planningGate.reason}\n**Phase time**: ${formatElapsed(planningStartMs, planningEndMs)}`);
|
|
189
|
+
context.metadata({
|
|
190
|
+
title: '⚠️ Pipeline finished with issues',
|
|
191
|
+
metadata: {
|
|
192
|
+
phase: 'complete',
|
|
193
|
+
outcome: 'issues',
|
|
194
|
+
totalElapsedMs: Date.now() - totalStartMs,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
addReport('Complete', `Pipeline finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
117
198
|
return report.join('\n\n');
|
|
118
199
|
}
|
|
119
200
|
// REWORK continues the loop
|
|
120
201
|
}
|
|
202
|
+
const planningEndMs = Date.now();
|
|
203
|
+
addReport('Planning', `${planningIterationDetails.join('\n\n')}\n\n**Outcome**: ${planningGate.decision === 'APPROVED'
|
|
204
|
+
? 'APPROVED'
|
|
205
|
+
: `REWORK exhausted (5 iterations). Last feedback: ${planningGate.feedback}`}\n**Phase time**: ${formatElapsed(planningStartMs, planningEndMs)}`);
|
|
121
206
|
if (planningGate.decision !== 'APPROVED') {
|
|
122
|
-
|
|
207
|
+
context.metadata({
|
|
208
|
+
title: '⚠️ Pipeline finished with issues',
|
|
209
|
+
metadata: {
|
|
210
|
+
phase: 'complete',
|
|
211
|
+
outcome: 'issues',
|
|
212
|
+
totalElapsedMs: Date.now() - totalStartMs,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
addReport('Complete', `Pipeline finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
123
216
|
return report.join('\n\n');
|
|
124
217
|
}
|
|
125
218
|
// ===================================================================
|
|
126
219
|
// PHASE 2: BUILDING (breakdown → validate → implement)
|
|
127
220
|
// ===================================================================
|
|
221
|
+
const buildingStartMs = Date.now();
|
|
222
|
+
context.metadata({
|
|
223
|
+
title: '⏳ Breaking down tasks...',
|
|
224
|
+
metadata: {
|
|
225
|
+
phase: 'building',
|
|
226
|
+
step: 'breakdown',
|
|
227
|
+
},
|
|
228
|
+
});
|
|
128
229
|
// Breakdown
|
|
230
|
+
const breakdownTitle = 'ff-build-breakdown';
|
|
231
|
+
const breakdownStartMs = Date.now();
|
|
129
232
|
const tasksRaw = await promptSession(client, sessionId, breakdownPrompt(finalPlan), {
|
|
130
233
|
model: buildModel,
|
|
131
234
|
agent: 'building',
|
|
132
|
-
title:
|
|
235
|
+
title: breakdownTitle,
|
|
236
|
+
});
|
|
237
|
+
const breakdownEndMs = Date.now();
|
|
238
|
+
context.metadata({
|
|
239
|
+
title: '⏳ Validating task batches...',
|
|
240
|
+
metadata: {
|
|
241
|
+
phase: 'building',
|
|
242
|
+
step: 'validate',
|
|
243
|
+
},
|
|
133
244
|
});
|
|
134
245
|
// Validate batches
|
|
246
|
+
const validateTitle = 'ff-build-validate';
|
|
247
|
+
const validateStartMs = Date.now();
|
|
135
248
|
const batchesRaw = await promptSession(client, sessionId, validateBatchPrompt(tasksRaw), {
|
|
136
249
|
model: validateModel,
|
|
137
250
|
agent: 'building',
|
|
138
|
-
title:
|
|
251
|
+
title: validateTitle,
|
|
252
|
+
});
|
|
253
|
+
const validateEndMs = Date.now();
|
|
254
|
+
context.metadata({
|
|
255
|
+
title: '⏳ Implementing validated batches...',
|
|
256
|
+
metadata: {
|
|
257
|
+
phase: 'building',
|
|
258
|
+
step: 'implement',
|
|
259
|
+
},
|
|
139
260
|
});
|
|
140
261
|
// Implement
|
|
262
|
+
const implementTitle = 'ff-build-implement';
|
|
263
|
+
const implementStartMs = Date.now();
|
|
141
264
|
const implRaw = await promptSession(client, sessionId, implementBatchPrompt(batchesRaw), {
|
|
142
265
|
model: buildModel,
|
|
143
266
|
agent: 'building',
|
|
144
|
-
title:
|
|
267
|
+
title: implementTitle,
|
|
145
268
|
});
|
|
269
|
+
const implementEndMs = Date.now();
|
|
146
270
|
const implementation = parseImplementationReport(implRaw);
|
|
147
|
-
|
|
271
|
+
const buildingEndMs = Date.now();
|
|
272
|
+
addReport('Building', `### Execution\n` +
|
|
273
|
+
`- **Breakdown**: ${breakdownTitle} (${formatElapsed(breakdownStartMs, breakdownEndMs)})\n` +
|
|
274
|
+
`- **Validate**: ${validateTitle} (${formatElapsed(validateStartMs, validateEndMs)})\n` +
|
|
275
|
+
`- **Implement**: ${implementTitle} (${formatElapsed(implementStartMs, implementEndMs)})\n` +
|
|
276
|
+
`- **Tests passed**: ${implementation.testsPassed}\n` +
|
|
277
|
+
`- **Open issues**: ${implementation.openIssues.length > 0 ? implementation.openIssues.join('; ') : 'none'}\n\n` +
|
|
278
|
+
`**Phase time**: ${formatElapsed(buildingStartMs, buildingEndMs)}`);
|
|
148
279
|
// ===================================================================
|
|
149
280
|
// PHASE 3: REVIEWING (triage → fan-out review → synthesize → gate, loop up to 10)
|
|
150
281
|
// ===================================================================
|
|
282
|
+
const reviewStartMs = Date.now();
|
|
283
|
+
const reviewIterationDetails = [];
|
|
151
284
|
let reviewInput = implRaw;
|
|
152
285
|
let reviewGate = { decision: 'REWORK' };
|
|
153
286
|
for (let revIter = 0; revIter < 10 && reviewGate.decision === 'REWORK'; revIter++) {
|
|
287
|
+
const iteration = revIter + 1;
|
|
288
|
+
const triageTitle = `ff-review-triage-${iteration}`;
|
|
289
|
+
const synthTitle = `ff-review-synthesis-${iteration}`;
|
|
290
|
+
const reworkTitle = `ff-review-rework-${iteration}`;
|
|
291
|
+
const fanOutTitles = revModels.map((model) => `ff-fanout-${model.tag}`);
|
|
292
|
+
context.metadata({
|
|
293
|
+
title: `⏳ Reviewing triage (iteration ${iteration}/10)...`,
|
|
294
|
+
metadata: {
|
|
295
|
+
phase: 'reviewing',
|
|
296
|
+
step: 'triage',
|
|
297
|
+
iteration,
|
|
298
|
+
maxIterations: 10,
|
|
299
|
+
},
|
|
300
|
+
});
|
|
154
301
|
// Triage
|
|
302
|
+
const triageStartMs = Date.now();
|
|
155
303
|
const brief = await promptSession(client, sessionId, triagePrompt(reviewInput), {
|
|
156
304
|
model: orchestratorModel,
|
|
157
305
|
agent: 'reviewing',
|
|
158
|
-
title:
|
|
306
|
+
title: triageTitle,
|
|
307
|
+
});
|
|
308
|
+
const triageEndMs = Date.now();
|
|
309
|
+
context.metadata({
|
|
310
|
+
title: `⏳ Review fan-out (iteration ${iteration}/10)...`,
|
|
311
|
+
metadata: {
|
|
312
|
+
phase: 'reviewing',
|
|
313
|
+
step: 'fan_out',
|
|
314
|
+
iteration,
|
|
315
|
+
maxIterations: 10,
|
|
316
|
+
},
|
|
159
317
|
});
|
|
160
318
|
// Fan-out review
|
|
319
|
+
const fanOutStartMs = Date.now();
|
|
161
320
|
const reviewResults = await fanOut(client, sessionId, revModels, (tag) => reviewPrompt(brief, tag), 'reviewing');
|
|
321
|
+
const fanOutEndMs = Date.now();
|
|
322
|
+
context.metadata({
|
|
323
|
+
title: '⏳ Synthesizing reviews...',
|
|
324
|
+
metadata: {
|
|
325
|
+
phase: 'reviewing',
|
|
326
|
+
step: 'synthesis',
|
|
327
|
+
iteration,
|
|
328
|
+
maxIterations: 10,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
162
331
|
// Synthesize
|
|
332
|
+
const synthStartMs = Date.now();
|
|
163
333
|
const synthRaw = await promptSession(client, sessionId, reviewSynthesisPrompt(reviewResults), {
|
|
164
334
|
model: orchestratorModel,
|
|
165
335
|
agent: 'reviewing',
|
|
166
|
-
title:
|
|
336
|
+
title: synthTitle,
|
|
167
337
|
});
|
|
338
|
+
const synthEndMs = Date.now();
|
|
168
339
|
const synthesis = parseReviewSynthesis(synthRaw);
|
|
169
340
|
// Gate (deterministic)
|
|
170
|
-
reviewGate = evaluateReviewGate(synthesis,
|
|
341
|
+
reviewGate = evaluateReviewGate(synthesis, iteration);
|
|
171
342
|
if (reviewGate.decision === 'APPROVED') {
|
|
172
|
-
|
|
343
|
+
context.metadata({
|
|
344
|
+
title: `✅ Review APPROVED (confidence: ${synthesis.overallConfidence}, iteration: ${iteration})`,
|
|
345
|
+
metadata: {
|
|
346
|
+
phase: 'reviewing',
|
|
347
|
+
step: 'gate',
|
|
348
|
+
decision: reviewGate.decision,
|
|
349
|
+
confidence: synthesis.overallConfidence,
|
|
350
|
+
unresolvedIssues: synthesis.unresolvedIssues,
|
|
351
|
+
iteration,
|
|
352
|
+
maxIterations: 10,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
173
355
|
}
|
|
174
356
|
else if (reviewGate.decision === 'ESCALATE') {
|
|
175
|
-
|
|
176
|
-
|
|
357
|
+
context.metadata({
|
|
358
|
+
title: '⚠️ Review ESCALATED',
|
|
359
|
+
metadata: {
|
|
360
|
+
phase: 'reviewing',
|
|
361
|
+
step: 'gate',
|
|
362
|
+
decision: reviewGate.decision,
|
|
363
|
+
confidence: synthesis.overallConfidence,
|
|
364
|
+
unresolvedIssues: synthesis.unresolvedIssues,
|
|
365
|
+
iteration,
|
|
366
|
+
maxIterations: 10,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
177
369
|
}
|
|
178
370
|
else {
|
|
371
|
+
context.metadata({
|
|
372
|
+
title: `🔄 Review rework required (confidence: ${synthesis.overallConfidence}, iteration: ${iteration}/10)`,
|
|
373
|
+
metadata: {
|
|
374
|
+
phase: 'reviewing',
|
|
375
|
+
step: 'gate',
|
|
376
|
+
decision: reviewGate.decision,
|
|
377
|
+
confidence: synthesis.overallConfidence,
|
|
378
|
+
unresolvedIssues: synthesis.unresolvedIssues,
|
|
379
|
+
iteration,
|
|
380
|
+
maxIterations: 10,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
let reworkLine = '- **Rework**: not required';
|
|
385
|
+
let feedback = synthesis.reworkInstructions || reviewGate.feedback || synthesis.consolidatedFindings || 'N/A';
|
|
386
|
+
if (reviewGate.decision === 'REWORK') {
|
|
387
|
+
context.metadata({
|
|
388
|
+
title: `⏳ Applying rework (iteration ${iteration}/10)...`,
|
|
389
|
+
metadata: {
|
|
390
|
+
phase: 'reviewing',
|
|
391
|
+
step: 'rework',
|
|
392
|
+
iteration,
|
|
393
|
+
maxIterations: 10,
|
|
394
|
+
},
|
|
395
|
+
});
|
|
179
396
|
// REWORK — apply fixes, then re-review
|
|
180
|
-
const
|
|
397
|
+
const reworkStartMs = Date.now();
|
|
398
|
+
const fixRaw = await promptSession(client, sessionId, implementBatchPrompt(`Rework required:\n${reviewGate.feedback}\n\nOriginal batches:\n${batchesRaw}`), { model: buildModel, agent: 'building', title: reworkTitle });
|
|
399
|
+
const reworkEndMs = Date.now();
|
|
400
|
+
reworkLine = `- **Rework**: ${reworkTitle} (${formatElapsed(reworkStartMs, reworkEndMs)})`;
|
|
181
401
|
reviewInput = fixRaw;
|
|
402
|
+
feedback = reviewGate.feedback || feedback;
|
|
403
|
+
}
|
|
404
|
+
reviewIterationDetails.push(`### Iteration ${iteration}\n` +
|
|
405
|
+
`- **Triage**: ${triageTitle} (${formatElapsed(triageStartMs, triageEndMs)})\n` +
|
|
406
|
+
`- **Fan-out**: ${fanOutTitles.join(', ')} (${formatElapsed(fanOutStartMs, fanOutEndMs)})\n` +
|
|
407
|
+
`- **Synthesis**: ${synthTitle} (${formatElapsed(synthStartMs, synthEndMs)})\n` +
|
|
408
|
+
`- **Gate**: ${reviewGate.decision} (confidence: ${synthesis.overallConfidence}, unresolved: ${synthesis.unresolvedIssues})\n` +
|
|
409
|
+
`${reworkLine}\n` +
|
|
410
|
+
`- **Feedback**: ${feedback}`);
|
|
411
|
+
if (reviewGate.decision === 'ESCALATE') {
|
|
412
|
+
const reviewEndMs = Date.now();
|
|
413
|
+
addReport('Reviewing', `${reviewIterationDetails.join('\n\n')}\n\n**Outcome**: ESCALATE: ${reviewGate.reason}\n**Phase time**: ${formatElapsed(reviewStartMs, reviewEndMs)}`);
|
|
414
|
+
context.metadata({
|
|
415
|
+
title: '⚠️ Pipeline finished with issues',
|
|
416
|
+
metadata: {
|
|
417
|
+
phase: 'complete',
|
|
418
|
+
outcome: 'issues',
|
|
419
|
+
totalElapsedMs: Date.now() - totalStartMs,
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
addReport('Complete', `Pipeline finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
423
|
+
return report.join('\n\n');
|
|
182
424
|
}
|
|
183
425
|
}
|
|
426
|
+
const reviewEndMs = Date.now();
|
|
427
|
+
addReport('Reviewing', `${reviewIterationDetails.join('\n\n')}\n\n**Outcome**: ${reviewGate.decision === 'APPROVED'
|
|
428
|
+
? 'APPROVED'
|
|
429
|
+
: `REWORK exhausted (10 iterations). Last feedback: ${reviewGate.feedback}`}\n**Phase time**: ${formatElapsed(reviewStartMs, reviewEndMs)}`);
|
|
184
430
|
if (reviewGate.decision !== 'APPROVED') {
|
|
185
|
-
|
|
431
|
+
context.metadata({
|
|
432
|
+
title: '⚠️ Pipeline finished with issues',
|
|
433
|
+
metadata: {
|
|
434
|
+
phase: 'complete',
|
|
435
|
+
outcome: 'issues',
|
|
436
|
+
totalElapsedMs: Date.now() - totalStartMs,
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
addReport('Complete', `Pipeline finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
186
440
|
return report.join('\n\n');
|
|
187
441
|
}
|
|
188
442
|
// ===================================================================
|
|
189
443
|
// PHASE 4: DOCUMENTATION (document → review → gate, loop up to 5)
|
|
190
444
|
// ===================================================================
|
|
445
|
+
const documentationStartMs = Date.now();
|
|
446
|
+
const documentationIterationDetails = [];
|
|
191
447
|
let docInput = `Implementation report:\n${implRaw}\n\nReview synthesis:\n${reviewInput}`;
|
|
192
448
|
let docGate = { decision: 'REWORK' };
|
|
193
449
|
for (let docIter = 0; docIter < 5 && docGate.decision === 'REWORK'; docIter++) {
|
|
450
|
+
const iteration = docIter + 1;
|
|
451
|
+
const writeTitle = `ff-doc-write-${iteration}`;
|
|
452
|
+
const reviewTitle = `ff-doc-review-${iteration}`;
|
|
453
|
+
context.metadata({
|
|
454
|
+
title: `⏳ Writing documentation (iteration ${iteration}/5)...`,
|
|
455
|
+
metadata: {
|
|
456
|
+
phase: 'documentation',
|
|
457
|
+
step: 'write',
|
|
458
|
+
iteration,
|
|
459
|
+
maxIterations: 5,
|
|
460
|
+
},
|
|
461
|
+
});
|
|
194
462
|
// Write docs
|
|
463
|
+
const writeStartMs = Date.now();
|
|
195
464
|
const docRaw = await promptSession(client, sessionId, documentPrompt(docInput), {
|
|
196
465
|
model: docModel,
|
|
197
466
|
agent: 'documenting',
|
|
198
|
-
title:
|
|
467
|
+
title: writeTitle,
|
|
468
|
+
});
|
|
469
|
+
const writeEndMs = Date.now();
|
|
470
|
+
context.metadata({
|
|
471
|
+
title: `⏳ Reviewing documentation (iteration ${iteration}/5)...`,
|
|
472
|
+
metadata: {
|
|
473
|
+
phase: 'documentation',
|
|
474
|
+
step: 'review',
|
|
475
|
+
iteration,
|
|
476
|
+
maxIterations: 5,
|
|
477
|
+
},
|
|
199
478
|
});
|
|
200
479
|
// Review docs
|
|
480
|
+
const reviewDocStartMs = Date.now();
|
|
201
481
|
const docRevRaw = await promptSession(client, sessionId, docReviewPrompt(docRaw), {
|
|
202
482
|
model: docReviewModel,
|
|
203
483
|
agent: 'reviewing',
|
|
204
|
-
title:
|
|
484
|
+
title: reviewTitle,
|
|
205
485
|
});
|
|
486
|
+
const reviewDocEndMs = Date.now();
|
|
206
487
|
const docReview = parseDocReview(docRevRaw);
|
|
207
488
|
// Gate (deterministic)
|
|
208
|
-
docGate = evaluateDocGate(docReview,
|
|
489
|
+
docGate = evaluateDocGate(docReview, iteration);
|
|
209
490
|
if (docGate.decision === 'APPROVED') {
|
|
210
|
-
|
|
491
|
+
context.metadata({
|
|
492
|
+
title: `✅ Documentation APPROVED (confidence: ${docReview.confidence}, iteration: ${iteration})`,
|
|
493
|
+
metadata: {
|
|
494
|
+
phase: 'documentation',
|
|
495
|
+
step: 'gate',
|
|
496
|
+
decision: docGate.decision,
|
|
497
|
+
confidence: docReview.confidence,
|
|
498
|
+
unresolvedIssues: docReview.unresolvedIssues,
|
|
499
|
+
iteration,
|
|
500
|
+
maxIterations: 5,
|
|
501
|
+
},
|
|
502
|
+
});
|
|
211
503
|
}
|
|
212
504
|
else if (docGate.decision === 'ESCALATE') {
|
|
213
|
-
|
|
505
|
+
context.metadata({
|
|
506
|
+
title: '⚠️ Documentation ESCALATED',
|
|
507
|
+
metadata: {
|
|
508
|
+
phase: 'documentation',
|
|
509
|
+
step: 'gate',
|
|
510
|
+
decision: docGate.decision,
|
|
511
|
+
confidence: docReview.confidence,
|
|
512
|
+
unresolvedIssues: docReview.unresolvedIssues,
|
|
513
|
+
iteration,
|
|
514
|
+
maxIterations: 5,
|
|
515
|
+
},
|
|
516
|
+
});
|
|
214
517
|
}
|
|
215
518
|
else {
|
|
519
|
+
context.metadata({
|
|
520
|
+
title: `🔄 Documentation rework required (confidence: ${docReview.confidence}, iteration: ${iteration}/5)`,
|
|
521
|
+
metadata: {
|
|
522
|
+
phase: 'documentation',
|
|
523
|
+
step: 'gate',
|
|
524
|
+
decision: docGate.decision,
|
|
525
|
+
confidence: docReview.confidence,
|
|
526
|
+
unresolvedIssues: docReview.unresolvedIssues,
|
|
527
|
+
iteration,
|
|
528
|
+
maxIterations: 5,
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
const feedback = docReview.reworkInstructions || docGate.feedback || docReview.raw;
|
|
533
|
+
documentationIterationDetails.push(`### Iteration ${iteration}\n` +
|
|
534
|
+
`- **Write**: ${writeTitle} (${formatElapsed(writeStartMs, writeEndMs)})\n` +
|
|
535
|
+
`- **Review**: ${reviewTitle} (${formatElapsed(reviewDocStartMs, reviewDocEndMs)})\n` +
|
|
536
|
+
`- **Gate**: ${docGate.decision} (confidence: ${docReview.confidence}, unresolved: ${docReview.unresolvedIssues})\n` +
|
|
537
|
+
`- **Feedback**: ${feedback}`);
|
|
538
|
+
if (docGate.decision === 'REWORK') {
|
|
216
539
|
// Feed feedback into the next iteration
|
|
217
540
|
docInput = `${docInput}\n\nDocumentation review feedback:\n${docGate.feedback}`;
|
|
218
541
|
}
|
|
219
542
|
}
|
|
543
|
+
const documentationEndMs = Date.now();
|
|
544
|
+
addReport('Documentation', `${documentationIterationDetails.join('\n\n')}\n\n**Outcome**: ${docGate.decision === 'APPROVED'
|
|
545
|
+
? 'APPROVED'
|
|
546
|
+
: docGate.decision === 'ESCALATE'
|
|
547
|
+
? `ESCALATE: ${docGate.reason}`
|
|
548
|
+
: `REWORK exhausted (5 iterations). Last feedback: ${docGate.feedback}`}\n**Phase time**: ${formatElapsed(documentationStartMs, documentationEndMs)}`);
|
|
220
549
|
// ===================================================================
|
|
221
550
|
// FINAL REPORT
|
|
222
551
|
// ===================================================================
|
|
223
|
-
|
|
552
|
+
const totalEndMs = Date.now();
|
|
553
|
+
const completedWithoutIssues = docGate.decision === 'APPROVED';
|
|
554
|
+
context.metadata({
|
|
555
|
+
title: completedWithoutIssues ? '✅ Pipeline complete' : '⚠️ Pipeline finished with issues',
|
|
556
|
+
metadata: {
|
|
557
|
+
phase: 'complete',
|
|
558
|
+
outcome: completedWithoutIssues ? 'success' : 'issues',
|
|
559
|
+
totalElapsedMs: totalEndMs - totalStartMs,
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
addReport('Complete', `${completedWithoutIssues ? 'Pipeline finished successfully.' : 'Pipeline finished with issues.'}\n**Total time**: ${formatElapsed(totalStartMs, totalEndMs)}`);
|
|
224
563
|
return report.join('\n\n');
|
|
225
564
|
},
|
|
226
565
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@syntesseraai/opencode-feature-factory",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "OpenCode plugin for Feature Factory agents - provides sub-agents and skills for validation, review, security, and architecture assessment",
|
|
7
7
|
"license": "MIT",
|