@syntesseraai/opencode-feature-factory 0.7.1 → 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/README.md CHANGED
@@ -33,6 +33,20 @@ It also updates `~/.config/opencode/opencode.json` non-destructively by merging
33
33
  - **`opencode.json` is non-destructive**: existing keys/values are preserved; only missing required plugin/MCP entries are added.
34
34
  - **Global scope**: assets are installed to `~/.config/opencode/` and shared across projects.
35
35
 
36
+ ## Agent Frontmatter Metadata
37
+
38
+ All shipped Feature Factory agent manifests under `agents/*.md` include a `color` frontmatter field using a valid hex value.
39
+
40
+ | Agent | Color |
41
+ | ----------------- | --------- |
42
+ | `feature-factory` | `#10b981` |
43
+ | `planning` | `#3b82f6` |
44
+ | `building` | `#22c55e` |
45
+ | `reviewing` | `#8b5cf6` |
46
+ | `documenting` | `#f97316` |
47
+ | `ff-research` | `#6366f1` |
48
+
49
+ These colors are intentionally unique to avoid collisions in OpenCode agent UIs and logs.
36
50
  ## Pipeline Entrypoint
37
51
 
38
52
  - Invoke `/pipeline/start <requirements-brief>` directly from any agent (e.g. `@building`).
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Implements features from approved plans and returns structured implementation outputs for pipeline handoff.
3
- color: '#16b910'
3
+ mode: subagent
4
+ color: '#22c55e'
4
5
  tools:
5
6
  read: true
6
7
  write: true
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Documentation implementation specialist for pipeline documentation stage.
3
- color: '#c522a2'
3
+ mode: subagent
4
+ color: '#f97316'
4
5
  tools:
5
6
  read: true
6
7
  write: true
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Feature Factory — guided workflow for planning and building features. Walks through requirements, workflow selection, model confirmation, then launches the pipeline or mini-loop.
3
3
  mode: primary
4
- color: '#f59e0b'
4
+ color: '#10b981'
5
5
  tools:
6
6
  read: true
7
7
  write: false
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Research sub-agent for external APIs, libraries, and best practices. Produces concise, evidence-backed findings.
3
3
  mode: subagent
4
+ color: '#6366f1'
4
5
  tools:
5
6
  read: true
6
7
  write: false
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Creates implementation plans and planning gates for pipeline and ad-hoc work. Uses result-based handoff instead of file artifacts.
3
+ mode: subagent
3
4
  color: '#3b82f6'
4
5
  tools:
5
6
  read: true
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Unified validation agent for code and documentation. Performs acceptance, quality, security, and architecture review with context-driven scope.
3
- color: '#f59e0b'
3
+ mode: subagent
4
+ color: '#8b5cf6'
4
5
  tools:
5
6
  read: true
6
7
  write: false
@@ -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
- lastImplRaw = await promptSession(client, sessionId, miniBuildPrompt(buildInput, implIter > 0 ? implGate.feedback : undefined), { model: buildModel, agent: 'building', title: `ff-mini-build-${implIter + 1}` });
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: `ff-mini-review-${implIter + 1}`,
105
+ title: reviewTitle,
78
106
  });
107
+ const reviewEndMs = Date.now();
79
108
  const review = parseMiniReview(reviewRaw);
80
109
  // Gate (deterministic)
81
- implGate = evaluateMiniLoopImplGate(review, implIter + 1);
110
+ implGate = evaluateMiniLoopImplGate(review, iteration);
82
111
  if (implGate.decision === 'APPROVED') {
83
- addReport('Implementation', `APPROVED (confidence: ${review.confidence}, iteration: ${implIter + 1})`);
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
- addReport('Implementation', `ESCALATE: ${implGate.reason}`);
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
- addReport('Implementation', `REWORK exhausted (10 iterations). Last feedback:\n${implGate.feedback}`);
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: `ff-mini-doc-write-${docIter + 1}`,
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: `ff-mini-doc-review-${docIter + 1}`,
230
+ title: reviewTitle,
112
231
  });
232
+ const reviewEndMs = Date.now();
113
233
  const docReview = parseDocReview(docRevRaw);
114
234
  // Gate (deterministic)
115
- docGate = evaluateMiniLoopDocGate(docReview, docIter + 1);
235
+ docGate = evaluateMiniLoopDocGate(docReview, iteration);
116
236
  if (docGate.decision === 'APPROVED') {
117
- addReport('Documentation', `APPROVED (confidence: ${docReview.confidence}, iteration: ${docIter + 1})`);
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
- addReport('Documentation', `ESCALATE: ${docGate.reason}`);
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
- // FINAL REPORT
128
- // ===================================================================
129
- addReport('Complete', 'Mini-loop finished.');
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
  });
@@ -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: `ff-plan-synthesis-${planIter + 1}`,
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
- addReport('Planning', `BLOCKED: ${planningGate.reason}`);
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
- addReport('Planning', `REWORK exhausted (5 iterations). Last feedback:\n${planningGate.feedback}`);
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: 'ff-build-breakdown',
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: 'ff-build-validate',
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: 'ff-build-implement',
267
+ title: implementTitle,
145
268
  });
269
+ const implementEndMs = Date.now();
146
270
  const implementation = parseImplementationReport(implRaw);
147
- addReport('Building', `Implementation completed. Tests passed: ${implementation.testsPassed}`);
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: `ff-review-triage-${revIter + 1}`,
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: `ff-review-synthesis-${revIter + 1}`,
336
+ title: synthTitle,
167
337
  });
338
+ const synthEndMs = Date.now();
168
339
  const synthesis = parseReviewSynthesis(synthRaw);
169
340
  // Gate (deterministic)
170
- reviewGate = evaluateReviewGate(synthesis, revIter + 1);
341
+ reviewGate = evaluateReviewGate(synthesis, iteration);
171
342
  if (reviewGate.decision === 'APPROVED') {
172
- addReport('Reviewing', `APPROVED (confidence: ${synthesis.overallConfidence}, iteration: ${revIter + 1})`);
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
- addReport('Reviewing', `ESCALATE: ${reviewGate.reason}`);
176
- return report.join('\n\n');
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 fixRaw = await promptSession(client, sessionId, implementBatchPrompt(`Rework required:\n${reviewGate.feedback}\n\nOriginal batches:\n${batchesRaw}`), { model: buildModel, agent: 'building', title: `ff-review-rework-${revIter + 1}` });
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
- addReport('Reviewing', `REWORK exhausted (10 iterations). Last feedback:\n${reviewGate.feedback}`);
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: `ff-doc-write-${docIter + 1}`,
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: `ff-doc-review-${docIter + 1}`,
484
+ title: reviewTitle,
205
485
  });
486
+ const reviewDocEndMs = Date.now();
206
487
  const docReview = parseDocReview(docRevRaw);
207
488
  // Gate (deterministic)
208
- docGate = evaluateDocGate(docReview, docIter + 1);
489
+ docGate = evaluateDocGate(docReview, iteration);
209
490
  if (docGate.decision === 'APPROVED') {
210
- addReport('Documentation', `APPROVED (confidence: ${docReview.confidence}, iteration: ${docIter + 1})`);
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
- addReport('Documentation', `ESCALATE: ${docGate.reason}`);
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
- addReport('Complete', 'Pipeline finished.');
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.1",
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",