@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 +44 -5
- package/dist/tools/mini-loop.d.ts +4 -0
- package/dist/tools/mini-loop.js +232 -202
- package/dist/tools/pipeline.d.ts +4 -0
- package/dist/tools/pipeline.js +432 -387
- package/dist/workflow/fan-out.d.ts +21 -0
- package/dist/workflow/fan-out.js +28 -0
- package/dist/workflow/orchestrator.d.ts +1 -1
- package/dist/workflow/orchestrator.js +1 -1
- package/package.json +1 -1
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
|
|
64
|
-
|
|
65
|
-
| `ff_pipeline`
|
|
66
|
-
| `ff_mini_loop`
|
|
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): {
|
package/dist/tools/mini-loop.js
CHANGED
|
@@ -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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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:
|
|
90
|
+
title: `⏳ Building (iteration ${iteration}/10)...`,
|
|
114
91
|
metadata: {
|
|
115
92
|
phase: 'implementation',
|
|
116
|
-
step: '
|
|
117
|
-
decision: implGate.decision,
|
|
118
|
-
confidence: review.confidence,
|
|
93
|
+
step: 'build',
|
|
119
94
|
iteration,
|
|
120
95
|
maxIterations: 10,
|
|
121
96
|
},
|
|
122
97
|
});
|
|
123
|
-
|
|
124
|
-
|
|
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:
|
|
103
|
+
title: `⏳ Reviewing (iteration ${iteration}/10)...`,
|
|
127
104
|
metadata: {
|
|
128
105
|
phase: 'implementation',
|
|
129
|
-
step: '
|
|
130
|
-
decision: implGate.decision,
|
|
131
|
-
confidence: review.confidence,
|
|
106
|
+
step: 'review',
|
|
132
107
|
iteration,
|
|
133
108
|
maxIterations: 10,
|
|
134
109
|
},
|
|
135
110
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
151
|
-
implementationIterationDetails.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
200
|
+
await notify(`<ff_mini_loop_complete>\n${report.join('\n\n')}\n</ff_mini_loop_complete>`);
|
|
201
|
+
return;
|
|
169
202
|
}
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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:
|
|
216
|
+
title: `⏳ Writing documentation (iteration ${iteration}/5)...`,
|
|
239
217
|
metadata: {
|
|
240
218
|
phase: 'documentation',
|
|
241
|
-
step: '
|
|
242
|
-
decision: docGate.decision,
|
|
243
|
-
confidence: docReview.confidence,
|
|
219
|
+
step: 'write',
|
|
244
220
|
iteration,
|
|
245
221
|
maxIterations: 5,
|
|
246
222
|
},
|
|
247
223
|
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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:
|
|
233
|
+
title: `⏳ Reviewing documentation (iteration ${iteration}/5)...`,
|
|
265
234
|
metadata: {
|
|
266
235
|
phase: 'documentation',
|
|
267
|
-
step: '
|
|
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
|
|
276
|
-
documentationIterationDetails.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
302
|
-
|
|
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
|
}
|
package/dist/tools/pipeline.d.ts
CHANGED
|
@@ -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): {
|