@syntesseraai/opencode-feature-factory 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,6 +47,7 @@ All shipped Feature Factory agent manifests under `agents/*.md` include a `color
47
47
  | `ff-research` | `#6366f1` |
48
48
 
49
49
  These colors are intentionally unique to avoid collisions in OpenCode agent UIs and logs.
50
+
50
51
  ## Pipeline Entrypoint
51
52
 
52
53
  - Invoke `/pipeline/start <requirements-brief>` directly from any agent (e.g. `@building`).
@@ -60,11 +61,11 @@ These colors are intentionally unique to avoid collisions in OpenCode agent UIs
60
61
 
61
62
  The plugin exposes three MCP tools via the `feature-factory` agent:
62
63
 
63
- | Tool | Description |
64
- |------|-------------|
65
- | `ff_pipeline` | Full multi-model pipeline: planning → build → review → documentation. Uses hardcoded per-role model defaults (see Model Routing below). |
66
- | `ff_mini_loop` | Lightweight build → review → documentation loop. **Does not hardcode model defaults** — all roles inherit the current session model when omitted. |
67
- | `ff_list_models` | Read-only discovery tool. Queries the OpenCode SDK to list all available providers, models, capability badges, connected status, and defaults. |
64
+ | Tool | Description |
65
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
66
+ | `ff_pipeline` | Full multi-model pipeline: planning → build → review → documentation. Uses hardcoded per-role model defaults (see Model Routing below). |
67
+ | `ff_mini_loop` | Lightweight build → review → documentation loop. **Does not hardcode model defaults** — all roles inherit the current session model when omitted. |
68
+ | `ff_list_models` | Read-only discovery tool. Queries the OpenCode SDK to list all available providers, models, capability badges, connected status, and defaults. |
68
69
 
69
70
  ### Mini-Loop Model Inheritance
70
71
 
@@ -110,6 +111,44 @@ Models are declared in each command's frontmatter (`model:` field). Multi-model
110
111
  - Documentation approval: documentation reviewer verdict `APPROVED` with zero unresolved documentation issues.
111
112
  - Planning loop confirmation: after 5 unsuccessful planning iterations, pipeline asks user whether to continue.
112
113
 
114
+ ## Async Progress Notifications
115
+
116
+ Both `ff_pipeline` and `ff_mini_loop` tools run asynchronously with real-time progress notifications:
117
+
118
+ - **Immediate return**: Tools return instantly with a brief acknowledgment (e.g. `Pipeline started for: <summary>`), so the LLM can continue the conversation.
119
+ - **Background orchestration**: The full pipeline or mini-loop runs in a detached `Promise`. All child session orchestration (fan-out, gates, loops) remains identical — it just executes after the tool returns.
120
+ - **Progress updates via `promptAsync(noReply: true)`**: After each major phase completes, a structured notification is injected into the parent session as a visible chat message. These appear in the OpenCode TUI without triggering an LLM turn.
121
+ - **Phase-by-phase visibility**: Users see updates for planning, building, each review iteration gate decision, each documentation iteration, and the final completion report.
122
+ - **Error notifications**: If the background orchestration throws, a `<ff_pipeline_error>` or `<ff_mini_loop_error>` notification is sent with the last phase and error message.
123
+ - **`context.metadata()` retained**: All existing metadata calls remain in place for future-proofing (when OpenCode's TUI renders tool metadata natively).
124
+
125
+ ### Notification Format
126
+
127
+ Pipeline updates use XML-style tags for structured parsing:
128
+
129
+ ```
130
+ <ff_pipeline_update>
131
+ Phase: Planning
132
+ Status: APPROVED
133
+ Duration: 45.2s
134
+ Next Phase: Building
135
+ </ff_pipeline_update>
136
+ ```
137
+
138
+ Mini-loop updates follow the same pattern:
139
+
140
+ ```
141
+ <ff_mini_loop_update>
142
+ Phase: Implementation
143
+ Status: APPROVED
144
+ Confidence: 97%
145
+ Iteration: 2/10
146
+ Duration: 32.1s
147
+ </ff_mini_loop_update>
148
+ ```
149
+
150
+ Final reports are wrapped in `<ff_pipeline_complete>` or `<ff_mini_loop_complete>` tags containing the full markdown report.
151
+
113
152
  ## Related Docs
114
153
 
115
154
  - `docs/PIPELINE_ORCHESTRATION.md`
@@ -3,6 +3,10 @@
3
3
  *
4
4
  * A simpler two-stage workflow (no multi-model fan-out for planning).
5
5
  * Implements a build/review loop, then a documentation loop.
6
+ *
7
+ * The tool returns immediately with an acknowledgment. Orchestration
8
+ * runs asynchronously in the background, sending progress notifications
9
+ * to the parent session via `promptAsync(noReply: true)`.
6
10
  */
7
11
  import { type Client } from '../workflow/orchestrator.js';
8
12
  export declare function createMiniLoopTool(client: Client): {
@@ -3,9 +3,13 @@
3
3
  *
4
4
  * A simpler two-stage workflow (no multi-model fan-out for planning).
5
5
  * Implements a build/review loop, then a documentation loop.
6
+ *
7
+ * The tool returns immediately with an acknowledgment. Orchestration
8
+ * runs asynchronously in the background, sending progress notifications
9
+ * to the parent session via `promptAsync(noReply: true)`.
6
10
  */
7
11
  import { tool } from '@opencode-ai/plugin/tool';
8
- import { promptSession, evaluateMiniLoopImplGate, evaluateMiniLoopDocGate, parseModelString, } from '../workflow/orchestrator.js';
12
+ import { promptSession, notifyParent, evaluateMiniLoopImplGate, evaluateMiniLoopDocGate, parseModelString, } from '../workflow/orchestrator.js';
9
13
  import { miniBuildPrompt, miniReviewPrompt, documentPrompt, docReviewPrompt } from './prompts.js';
10
14
  import { parseMiniReview, parseDocReview } from './parsers.js';
11
15
  // ---------------------------------------------------------------------------
@@ -39,8 +43,8 @@ export function createMiniLoopTool(client) {
39
43
  .describe('provider/model for documentation review. When omitted, inherits the session model.'),
40
44
  },
41
45
  async execute(args, context) {
42
- const totalStartMs = Date.now();
43
46
  const sessionId = context.sessionID;
47
+ const agent = context.agent;
44
48
  const { requirements } = args;
45
49
  // Resolve models — use provided overrides or undefined (inherit session model)
46
50
  const buildModel = args.build_model
@@ -57,105 +61,133 @@ export function createMiniLoopTool(client) {
57
61
  const docReviewModel = args.doc_review_model
58
62
  ? parseModelString(args.doc_review_model)
59
63
  : undefined;
60
- const report = [];
61
- const addReport = (phase, msg) => {
62
- report.push(`## ${phase}\n${msg}`);
63
- };
64
- // ===================================================================
65
- // PHASE 1: IMPLEMENTATION LOOP (build → review → gate, up to 10)
66
- // ===================================================================
67
- const implementationStartMs = Date.now();
68
- const implementationIterationDetails = [];
69
- let implGate = { decision: 'REWORK', feedback: requirements };
70
- let lastImplRaw = '';
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}`;
75
- const buildInput = implIter === 0
76
- ? requirements
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
- });
87
- // Build
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
- });
100
- // Review
101
- const reviewStartMs = Date.now();
102
- const reviewRaw = await promptSession(client, sessionId, miniReviewPrompt(lastImplRaw), {
103
- model: reviewModel,
104
- agent: 'reviewing',
105
- title: reviewTitle,
106
- });
107
- const reviewEndMs = Date.now();
108
- const review = parseMiniReview(reviewRaw);
109
- // Gate (deterministic)
110
- implGate = evaluateMiniLoopImplGate(review, iteration);
111
- if (implGate.decision === 'APPROVED') {
64
+ // Fire-and-forget: run orchestration in background
65
+ let lastPhase = 'init';
66
+ const asyncOrchestration = async () => {
67
+ const totalStartMs = Date.now();
68
+ const report = [];
69
+ const addReport = (phase, msg) => {
70
+ report.push(`## ${phase}\n${msg}`);
71
+ };
72
+ // Notify helper bound to this session
73
+ const notify = (message) => notifyParent(client, sessionId, agent, message);
74
+ // ===================================================================
75
+ // PHASE 1: IMPLEMENTATION LOOP (build review gate, up to 10)
76
+ // ===================================================================
77
+ lastPhase = 'Implementation';
78
+ const implementationStartMs = Date.now();
79
+ const implementationIterationDetails = [];
80
+ let implGate = { decision: 'REWORK', feedback: requirements };
81
+ let lastImplRaw = '';
82
+ for (let implIter = 0; implIter < 10 && implGate.decision === 'REWORK'; implIter++) {
83
+ const iteration = implIter + 1;
84
+ const buildTitle = `ff-mini-build-${iteration}`;
85
+ const reviewTitle = `ff-mini-review-${iteration}`;
86
+ const buildInput = implIter === 0
87
+ ? requirements
88
+ : `${requirements}\n\nPrevious review feedback:\n${implGate.feedback}`;
112
89
  context.metadata({
113
- title: `✅ Implementation APPROVED (confidence: ${review.confidence}, iteration: ${iteration})`,
90
+ title: `⏳ Building (iteration ${iteration}/10)...`,
114
91
  metadata: {
115
92
  phase: 'implementation',
116
- step: 'gate',
117
- decision: implGate.decision,
118
- confidence: review.confidence,
93
+ step: 'build',
119
94
  iteration,
120
95
  maxIterations: 10,
121
96
  },
122
97
  });
123
- }
124
- else if (implGate.decision === 'ESCALATE') {
98
+ // Build
99
+ const buildStartMs = Date.now();
100
+ lastImplRaw = await promptSession(client, sessionId, miniBuildPrompt(buildInput, implIter > 0 ? implGate.feedback : undefined), { model: buildModel, agent: 'building', title: buildTitle });
101
+ const buildEndMs = Date.now();
125
102
  context.metadata({
126
- title: '⚠️ Implementation ESCALATED',
103
+ title: `⏳ Reviewing (iteration ${iteration}/10)...`,
127
104
  metadata: {
128
105
  phase: 'implementation',
129
- step: 'gate',
130
- decision: implGate.decision,
131
- confidence: review.confidence,
106
+ step: 'review',
132
107
  iteration,
133
108
  maxIterations: 10,
134
109
  },
135
110
  });
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
- },
111
+ // Review
112
+ const reviewStartMs = Date.now();
113
+ const reviewRaw = await promptSession(client, sessionId, miniReviewPrompt(lastImplRaw), {
114
+ model: reviewModel,
115
+ agent: 'reviewing',
116
+ title: reviewTitle,
148
117
  });
118
+ const reviewEndMs = Date.now();
119
+ const review = parseMiniReview(reviewRaw);
120
+ // Gate (deterministic)
121
+ implGate = evaluateMiniLoopImplGate(review, iteration);
122
+ if (implGate.decision === 'APPROVED') {
123
+ context.metadata({
124
+ title: `✅ Implementation APPROVED (confidence: ${review.confidence}, iteration: ${iteration})`,
125
+ metadata: {
126
+ phase: 'implementation',
127
+ step: 'gate',
128
+ decision: implGate.decision,
129
+ confidence: review.confidence,
130
+ iteration,
131
+ maxIterations: 10,
132
+ },
133
+ });
134
+ }
135
+ else if (implGate.decision === 'ESCALATE') {
136
+ context.metadata({
137
+ title: '⚠️ Implementation ESCALATED',
138
+ metadata: {
139
+ phase: 'implementation',
140
+ step: 'gate',
141
+ decision: implGate.decision,
142
+ confidence: review.confidence,
143
+ iteration,
144
+ maxIterations: 10,
145
+ },
146
+ });
147
+ }
148
+ else {
149
+ context.metadata({
150
+ title: `🔄 Rework required (confidence: ${review.confidence}, iteration: ${iteration}/10)`,
151
+ metadata: {
152
+ phase: 'implementation',
153
+ step: 'gate',
154
+ decision: implGate.decision,
155
+ confidence: review.confidence,
156
+ iteration,
157
+ maxIterations: 10,
158
+ },
159
+ });
160
+ }
161
+ const feedback = review.reworkInstructions || implGate.feedback || review.raw;
162
+ implementationIterationDetails.push(`### Iteration ${iteration}\n` +
163
+ `- **Build**: ${buildTitle} (${formatElapsed(buildStartMs, buildEndMs)})\n` +
164
+ `- **Review**: ${reviewTitle} (${formatElapsed(reviewStartMs, reviewEndMs)})\n` +
165
+ `- **Gate**: ${implGate.decision} (confidence: ${review.confidence}, change requested: ${review.changeRequested ? 'yes' : 'no'}, unresolved: ${review.unresolvedIssues})\n` +
166
+ `- **Feedback**: ${feedback}`);
167
+ // Notify each implementation iteration gate decision
168
+ await notify(`<ff_mini_loop_update>\nPhase: Implementation\nStatus: ${implGate.decision}\nConfidence: ${review.confidence}%\nIteration: ${iteration}/10\nDuration: ${formatElapsed(implementationStartMs, Date.now())}\n</ff_mini_loop_update>`);
169
+ if (implGate.decision === 'ESCALATE') {
170
+ const implementationEndMs = Date.now();
171
+ addReport('Implementation', `${implementationIterationDetails.join('\n\n')}\n\n**Outcome**: ESCALATE: ${implGate.reason}\n**Phase time**: ${formatElapsed(implementationStartMs, implementationEndMs)}`);
172
+ context.metadata({
173
+ title: '⚠️ Mini-loop finished with issues',
174
+ metadata: {
175
+ phase: 'complete',
176
+ outcome: 'issues',
177
+ totalElapsedMs: Date.now() - totalStartMs,
178
+ },
179
+ });
180
+ addReport('Complete', `Mini-loop finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
181
+ await notify(`<ff_mini_loop_complete>\n${report.join('\n\n')}\n</ff_mini_loop_complete>`);
182
+ return;
183
+ }
184
+ // REWORK continues the loop
149
185
  }
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)}`);
186
+ const implementationEndMs = Date.now();
187
+ addReport('Implementation', `${implementationIterationDetails.join('\n\n')}\n\n**Outcome**: ${implGate.decision === 'APPROVED'
188
+ ? 'APPROVED'
189
+ : `REWORK exhausted (10 iterations). Last feedback: ${implGate.feedback}`}\n**Phase time**: ${formatElapsed(implementationStartMs, implementationEndMs)}`);
190
+ if (implGate.decision !== 'APPROVED') {
159
191
  context.metadata({
160
192
  title: '⚠️ Mini-loop finished with issues',
161
193
  metadata: {
@@ -165,141 +197,139 @@ export function createMiniLoopTool(client) {
165
197
  },
166
198
  });
167
199
  addReport('Complete', `Mini-loop finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
168
- return report.join('\n\n');
200
+ await notify(`<ff_mini_loop_complete>\n${report.join('\n\n')}\n</ff_mini_loop_complete>`);
201
+ return;
169
202
  }
170
- // REWORK continues the loop
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)}`);
176
- if (implGate.decision !== 'APPROVED') {
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())}`);
186
- return report.join('\n\n');
187
- }
188
- // ===================================================================
189
- // PHASE 2: DOCUMENTATION LOOP (document → review → gate, up to 5)
190
- // ===================================================================
191
- const documentationStartMs = Date.now();
192
- const documentationIterationDetails = [];
193
- let docInput = lastImplRaw;
194
- let docGate = { decision: 'REWORK' };
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
- });
208
- // Write docs
209
- const writeStartMs = Date.now();
210
- const docRaw = await promptSession(client, sessionId, documentPrompt(docInput), {
211
- model: docModel,
212
- agent: 'documenting',
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
- },
224
- });
225
- // Review docs
226
- const reviewStartMs = Date.now();
227
- const docRevRaw = await promptSession(client, sessionId, docReviewPrompt(docRaw), {
228
- model: docReviewModel,
229
- agent: 'reviewing',
230
- title: reviewTitle,
231
- });
232
- const reviewEndMs = Date.now();
233
- const docReview = parseDocReview(docRevRaw);
234
- // Gate (deterministic)
235
- docGate = evaluateMiniLoopDocGate(docReview, iteration);
236
- if (docGate.decision === 'APPROVED') {
203
+ // ===================================================================
204
+ // PHASE 2: DOCUMENTATION LOOP (document → review → gate, up to 5)
205
+ // ===================================================================
206
+ lastPhase = 'Documentation';
207
+ const documentationStartMs = Date.now();
208
+ const documentationIterationDetails = [];
209
+ let docInput = lastImplRaw;
210
+ let docGate = { decision: 'REWORK' };
211
+ for (let docIter = 0; docIter < 5 && docGate.decision === 'REWORK'; docIter++) {
212
+ const iteration = docIter + 1;
213
+ const writeTitle = `ff-mini-doc-write-${iteration}`;
214
+ const reviewTitle = `ff-mini-doc-review-${iteration}`;
237
215
  context.metadata({
238
- title: `✅ Documentation APPROVED (confidence: ${docReview.confidence}, iteration: ${iteration})`,
216
+ title: `⏳ Writing documentation (iteration ${iteration}/5)...`,
239
217
  metadata: {
240
218
  phase: 'documentation',
241
- step: 'gate',
242
- decision: docGate.decision,
243
- confidence: docReview.confidence,
219
+ step: 'write',
244
220
  iteration,
245
221
  maxIterations: 5,
246
222
  },
247
223
  });
248
- }
249
- else if (docGate.decision === 'ESCALATE') {
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
- },
224
+ // Write docs
225
+ const writeStartMs = Date.now();
226
+ const docRaw = await promptSession(client, sessionId, documentPrompt(docInput), {
227
+ model: docModel,
228
+ agent: 'documenting',
229
+ title: writeTitle,
260
230
  });
261
- }
262
- else {
231
+ const writeEndMs = Date.now();
263
232
  context.metadata({
264
- title: `🔄 Documentation rework required (confidence: ${docReview.confidence}, iteration: ${iteration}/5)`,
233
+ title: `⏳ Reviewing documentation (iteration ${iteration}/5)...`,
265
234
  metadata: {
266
235
  phase: 'documentation',
267
- step: 'gate',
268
- decision: docGate.decision,
269
- confidence: docReview.confidence,
236
+ step: 'review',
270
237
  iteration,
271
238
  maxIterations: 5,
272
239
  },
273
240
  });
241
+ // Review docs
242
+ const reviewStartMs = Date.now();
243
+ const docRevRaw = await promptSession(client, sessionId, docReviewPrompt(docRaw), {
244
+ model: docReviewModel,
245
+ agent: 'reviewing',
246
+ title: reviewTitle,
247
+ });
248
+ const reviewEndMs = Date.now();
249
+ const docReview = parseDocReview(docRevRaw);
250
+ // Gate (deterministic)
251
+ docGate = evaluateMiniLoopDocGate(docReview, iteration);
252
+ if (docGate.decision === 'APPROVED') {
253
+ context.metadata({
254
+ title: `✅ Documentation APPROVED (confidence: ${docReview.confidence}, iteration: ${iteration})`,
255
+ metadata: {
256
+ phase: 'documentation',
257
+ step: 'gate',
258
+ decision: docGate.decision,
259
+ confidence: docReview.confidence,
260
+ iteration,
261
+ maxIterations: 5,
262
+ },
263
+ });
264
+ }
265
+ else if (docGate.decision === 'ESCALATE') {
266
+ context.metadata({
267
+ title: '⚠️ Documentation ESCALATED',
268
+ metadata: {
269
+ phase: 'documentation',
270
+ step: 'gate',
271
+ decision: docGate.decision,
272
+ confidence: docReview.confidence,
273
+ iteration,
274
+ maxIterations: 5,
275
+ },
276
+ });
277
+ }
278
+ else {
279
+ context.metadata({
280
+ title: `🔄 Documentation rework required (confidence: ${docReview.confidence}, iteration: ${iteration}/5)`,
281
+ metadata: {
282
+ phase: 'documentation',
283
+ step: 'gate',
284
+ decision: docGate.decision,
285
+ confidence: docReview.confidence,
286
+ iteration,
287
+ maxIterations: 5,
288
+ },
289
+ });
290
+ }
291
+ const feedback = docReview.reworkInstructions || docGate.feedback || docReview.raw;
292
+ documentationIterationDetails.push(`### Iteration ${iteration}\n` +
293
+ `- **Write**: ${writeTitle} (${formatElapsed(writeStartMs, writeEndMs)})\n` +
294
+ `- **Review**: ${reviewTitle} (${formatElapsed(reviewStartMs, reviewEndMs)})\n` +
295
+ `- **Gate**: ${docGate.decision} (confidence: ${docReview.confidence}, unresolved: ${docReview.unresolvedIssues})\n` +
296
+ `- **Feedback**: ${feedback}`);
297
+ // Notify each documentation iteration gate decision
298
+ await notify(`<ff_mini_loop_update>\nPhase: Documentation\nStatus: ${docGate.decision}\nConfidence: ${docReview.confidence}%\nIteration: ${iteration}/5\nDuration: ${formatElapsed(documentationStartMs, Date.now())}\n</ff_mini_loop_update>`);
299
+ if (docGate.decision === 'REWORK') {
300
+ docInput = `${docInput}\n\nDocumentation review feedback:\n${docGate.feedback}`;
301
+ }
274
302
  }
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') {
282
- docInput = `${docInput}\n\nDocumentation review feedback:\n${docGate.feedback}`;
283
- }
284
- }
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
- },
303
+ const documentationEndMs = Date.now();
304
+ addReport('Documentation', `${documentationIterationDetails.join('\n\n')}\n\n**Outcome**: ${docGate.decision === 'APPROVED'
305
+ ? 'APPROVED'
306
+ : docGate.decision === 'ESCALATE'
307
+ ? `ESCALATE: ${docGate.reason}`
308
+ : `REWORK exhausted (5 iterations). Last feedback: ${docGate.feedback}`}\n**Phase time**: ${formatElapsed(documentationStartMs, documentationEndMs)}`);
309
+ const totalEndMs = Date.now();
310
+ const completedWithoutIssues = docGate.decision === 'APPROVED';
311
+ context.metadata({
312
+ title: completedWithoutIssues
313
+ ? '✅ Mini-loop complete'
314
+ : '⚠️ Mini-loop finished with issues',
315
+ metadata: {
316
+ phase: 'complete',
317
+ outcome: completedWithoutIssues ? 'success' : 'issues',
318
+ totalElapsedMs: totalEndMs - totalStartMs,
319
+ },
320
+ });
321
+ addReport('Complete', `${completedWithoutIssues ? 'Mini-loop finished successfully.' : 'Mini-loop finished with issues.'}\n**Total time**: ${formatElapsed(totalStartMs, totalEndMs)}`);
322
+ // Send final completion report as notification
323
+ await notify(`<ff_mini_loop_complete>\n${report.join('\n\n')}\n</ff_mini_loop_complete>`);
324
+ }; // end asyncOrchestration
325
+ // Launch orchestration in background — fire-and-forget
326
+ void asyncOrchestration().catch(async (err) => {
327
+ const message = err instanceof Error ? err.message : String(err);
328
+ await notifyParent(client, sessionId, agent, `<ff_mini_loop_error>\nPhase: ${lastPhase}\nError: ${message}\nDuration: N/A\n</ff_mini_loop_error>`);
300
329
  });
301
- addReport('Complete', `${completedWithoutIssues ? 'Mini-loop finished successfully.' : 'Mini-loop finished with issues.'}\n**Total time**: ${formatElapsed(totalStartMs, totalEndMs)}`);
302
- return report.join('\n\n');
330
+ // Return immediately with acknowledgment
331
+ const summary = requirements.length > 120 ? requirements.slice(0, 120) + '...' : requirements;
332
+ return `Mini-loop started for: ${summary}\n\nYou will receive progress updates as each phase completes.`;
303
333
  },
304
334
  });
305
335
  }
@@ -5,6 +5,10 @@
5
5
  * complete Feature Factory pipeline using deterministic control flow
6
6
  * (TypeScript loops and gates) while delegating creative work to
7
7
  * model-specific prompts via the SDK client.
8
+ *
9
+ * The tool returns immediately with an acknowledgment. Orchestration
10
+ * runs asynchronously in the background, sending progress notifications
11
+ * to the parent session via `promptAsync(noReply: true)`.
8
12
  */
9
13
  import { type Client } from '../workflow/orchestrator.js';
10
14
  export declare function createPipelineTool(client: Client): {