@syntesseraai/opencode-feature-factory 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -5
- package/agents/building.md +1 -1
- package/agents/documenting.md +1 -1
- package/agents/planning.md +1 -1
- package/agents/reviewing.md +1 -1
- package/dist/tools/mini-loop.d.ts +4 -0
- package/dist/tools/mini-loop.js +242 -202
- package/dist/tools/pipeline.d.ts +4 -0
- package/dist/tools/pipeline.js +449 -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,45 @@ 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 `# Pipeline: Error` or `# 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 notifications use plain-text markdown headers with phase START/END bracketing and per-iteration gate details:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
# Pipeline: Reviewing — Iteration 2/10
|
|
131
|
+
|
|
132
|
+
Status: APPROVED
|
|
133
|
+
Confidence: 97%
|
|
134
|
+
Unresolved Issues: 0
|
|
135
|
+
Duration: 45.3s
|
|
136
|
+
Feedback: N/A
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Mini-loop notifications follow the same pattern:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
# Mini-Loop: Building — Iteration 1/10
|
|
143
|
+
|
|
144
|
+
Status: REWORK
|
|
145
|
+
Confidence: 82%
|
|
146
|
+
Unresolved Issues: 2
|
|
147
|
+
Duration: 23.1s
|
|
148
|
+
Feedback: Fix type errors in handler.ts
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Final reports use `# Pipeline: Complete` or `# Mini-Loop: Complete` headers containing the full markdown report. Errors use `# Pipeline: Error` or `# Mini-Loop: Error`.
|
|
152
|
+
|
|
113
153
|
## Related Docs
|
|
114
154
|
|
|
115
155
|
- `docs/PIPELINE_ORCHESTRATION.md`
|
package/agents/building.md
CHANGED
package/agents/documenting.md
CHANGED
package/agents/planning.md
CHANGED
package/agents/reviewing.md
CHANGED
|
@@ -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,139 @@ 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
|
+
// Phase start notification
|
|
83
|
+
await notify(`# Mini-Loop: Building started\n\nStarting implementation phase...\n`);
|
|
84
|
+
for (let implIter = 0; implIter < 10 && implGate.decision === 'REWORK'; implIter++) {
|
|
85
|
+
const iteration = implIter + 1;
|
|
86
|
+
const buildTitle = `ff-mini-build-${iteration}`;
|
|
87
|
+
const reviewTitle = `ff-mini-review-${iteration}`;
|
|
88
|
+
const buildInput = implIter === 0
|
|
89
|
+
? requirements
|
|
90
|
+
: `${requirements}\n\nPrevious review feedback:\n${implGate.feedback}`;
|
|
112
91
|
context.metadata({
|
|
113
|
-
title:
|
|
92
|
+
title: `⏳ Building (iteration ${iteration}/10)...`,
|
|
114
93
|
metadata: {
|
|
115
94
|
phase: 'implementation',
|
|
116
|
-
step: '
|
|
117
|
-
decision: implGate.decision,
|
|
118
|
-
confidence: review.confidence,
|
|
95
|
+
step: 'build',
|
|
119
96
|
iteration,
|
|
120
97
|
maxIterations: 10,
|
|
121
98
|
},
|
|
122
99
|
});
|
|
123
|
-
|
|
124
|
-
|
|
100
|
+
// Build
|
|
101
|
+
const buildStartMs = Date.now();
|
|
102
|
+
lastImplRaw = await promptSession(client, sessionId, miniBuildPrompt(buildInput, implIter > 0 ? implGate.feedback : undefined), { model: buildModel, agent: 'building', title: buildTitle });
|
|
103
|
+
const buildEndMs = Date.now();
|
|
125
104
|
context.metadata({
|
|
126
|
-
title:
|
|
105
|
+
title: `⏳ Reviewing (iteration ${iteration}/10)...`,
|
|
127
106
|
metadata: {
|
|
128
107
|
phase: 'implementation',
|
|
129
|
-
step: '
|
|
130
|
-
decision: implGate.decision,
|
|
131
|
-
confidence: review.confidence,
|
|
108
|
+
step: 'review',
|
|
132
109
|
iteration,
|
|
133
110
|
maxIterations: 10,
|
|
134
111
|
},
|
|
135
112
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
step: 'gate',
|
|
143
|
-
decision: implGate.decision,
|
|
144
|
-
confidence: review.confidence,
|
|
145
|
-
iteration,
|
|
146
|
-
maxIterations: 10,
|
|
147
|
-
},
|
|
113
|
+
// Review
|
|
114
|
+
const reviewStartMs = Date.now();
|
|
115
|
+
const reviewRaw = await promptSession(client, sessionId, miniReviewPrompt(lastImplRaw), {
|
|
116
|
+
model: reviewModel,
|
|
117
|
+
agent: 'reviewing',
|
|
118
|
+
title: reviewTitle,
|
|
148
119
|
});
|
|
120
|
+
const reviewEndMs = Date.now();
|
|
121
|
+
const review = parseMiniReview(reviewRaw);
|
|
122
|
+
// Gate (deterministic)
|
|
123
|
+
implGate = evaluateMiniLoopImplGate(review, iteration);
|
|
124
|
+
if (implGate.decision === 'APPROVED') {
|
|
125
|
+
context.metadata({
|
|
126
|
+
title: `✅ Implementation APPROVED (confidence: ${review.confidence}, iteration: ${iteration})`,
|
|
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 if (implGate.decision === 'ESCALATE') {
|
|
138
|
+
context.metadata({
|
|
139
|
+
title: '⚠️ Implementation ESCALATED',
|
|
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
|
+
else {
|
|
151
|
+
context.metadata({
|
|
152
|
+
title: `🔄 Rework required (confidence: ${review.confidence}, iteration: ${iteration}/10)`,
|
|
153
|
+
metadata: {
|
|
154
|
+
phase: 'implementation',
|
|
155
|
+
step: 'gate',
|
|
156
|
+
decision: implGate.decision,
|
|
157
|
+
confidence: review.confidence,
|
|
158
|
+
iteration,
|
|
159
|
+
maxIterations: 10,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const feedback = review.reworkInstructions || implGate.feedback || review.raw;
|
|
164
|
+
implementationIterationDetails.push(`### Iteration ${iteration}\n` +
|
|
165
|
+
`- **Build**: ${buildTitle} (${formatElapsed(buildStartMs, buildEndMs)})\n` +
|
|
166
|
+
`- **Review**: ${reviewTitle} (${formatElapsed(reviewStartMs, reviewEndMs)})\n` +
|
|
167
|
+
`- **Gate**: ${implGate.decision} (confidence: ${review.confidence}, change requested: ${review.changeRequested ? 'yes' : 'no'}, unresolved: ${review.unresolvedIssues})\n` +
|
|
168
|
+
`- **Feedback**: ${feedback}`);
|
|
169
|
+
// Notify each implementation iteration gate decision
|
|
170
|
+
await notify(`# Mini-Loop: Building — Iteration ${iteration}/10\n\nStatus: ${implGate.decision}\nConfidence: ${review.confidence}%\nUnresolved Issues: ${review.unresolvedIssues}\nDuration: ${formatElapsed(implementationStartMs, Date.now())}\nFeedback: ${feedback}\n`);
|
|
171
|
+
if (implGate.decision === 'ESCALATE') {
|
|
172
|
+
const implementationEndMs = Date.now();
|
|
173
|
+
addReport('Implementation', `${implementationIterationDetails.join('\n\n')}\n\n**Outcome**: ESCALATE: ${implGate.reason}\n**Phase time**: ${formatElapsed(implementationStartMs, implementationEndMs)}`);
|
|
174
|
+
// Phase end notification
|
|
175
|
+
await notify(`# Mini-Loop: Building ended\n\nOutcome: ESCALATE\nReason: ${implGate.reason}\nIterations: ${iteration}\nPhase time: ${formatElapsed(implementationStartMs, implementationEndMs)}\n`);
|
|
176
|
+
context.metadata({
|
|
177
|
+
title: '⚠️ Mini-loop finished with issues',
|
|
178
|
+
metadata: {
|
|
179
|
+
phase: 'complete',
|
|
180
|
+
outcome: 'issues',
|
|
181
|
+
totalElapsedMs: Date.now() - totalStartMs,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
addReport('Complete', `Mini-loop finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
185
|
+
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// REWORK continues the loop
|
|
149
189
|
}
|
|
150
|
-
const
|
|
151
|
-
implementationIterationDetails.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (implGate.decision
|
|
157
|
-
const implementationEndMs = Date.now();
|
|
158
|
-
addReport('Implementation', `${implementationIterationDetails.join('\n\n')}\n\n**Outcome**: ESCALATE: ${implGate.reason}\n**Phase time**: ${formatElapsed(implementationStartMs, implementationEndMs)}`);
|
|
190
|
+
const implementationEndMs = Date.now();
|
|
191
|
+
addReport('Implementation', `${implementationIterationDetails.join('\n\n')}\n\n**Outcome**: ${implGate.decision === 'APPROVED'
|
|
192
|
+
? 'APPROVED'
|
|
193
|
+
: `REWORK exhausted (10 iterations). Last feedback: ${implGate.feedback}`}\n**Phase time**: ${formatElapsed(implementationStartMs, implementationEndMs)}`);
|
|
194
|
+
// Phase end notification
|
|
195
|
+
await notify(`# Mini-Loop: Building ended\n\nOutcome: ${implGate.decision}\nIterations: ${implGate.decision === 'APPROVED' ? 'converged' : '10 (exhausted)'}\nPhase time: ${formatElapsed(implementationStartMs, implementationEndMs)}\n`);
|
|
196
|
+
if (implGate.decision !== 'APPROVED') {
|
|
159
197
|
context.metadata({
|
|
160
198
|
title: '⚠️ Mini-loop finished with issues',
|
|
161
199
|
metadata: {
|
|
@@ -165,141 +203,143 @@ export function createMiniLoopTool(client) {
|
|
|
165
203
|
},
|
|
166
204
|
});
|
|
167
205
|
addReport('Complete', `Mini-loop finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
168
|
-
|
|
206
|
+
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n`);
|
|
207
|
+
return;
|
|
169
208
|
}
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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') {
|
|
209
|
+
// ===================================================================
|
|
210
|
+
// PHASE 2: DOCUMENTATION LOOP (document → review → gate, up to 5)
|
|
211
|
+
// ===================================================================
|
|
212
|
+
lastPhase = 'Documentation';
|
|
213
|
+
const documentationStartMs = Date.now();
|
|
214
|
+
const documentationIterationDetails = [];
|
|
215
|
+
let docInput = lastImplRaw;
|
|
216
|
+
let docGate = { decision: 'REWORK' };
|
|
217
|
+
// Phase start notification
|
|
218
|
+
await notify(`# Mini-Loop: Documentation started\n\nStarting documentation phase...\n`);
|
|
219
|
+
for (let docIter = 0; docIter < 5 && docGate.decision === 'REWORK'; docIter++) {
|
|
220
|
+
const iteration = docIter + 1;
|
|
221
|
+
const writeTitle = `ff-mini-doc-write-${iteration}`;
|
|
222
|
+
const reviewTitle = `ff-mini-doc-review-${iteration}`;
|
|
237
223
|
context.metadata({
|
|
238
|
-
title:
|
|
224
|
+
title: `⏳ Writing documentation (iteration ${iteration}/5)...`,
|
|
239
225
|
metadata: {
|
|
240
226
|
phase: 'documentation',
|
|
241
|
-
step: '
|
|
242
|
-
decision: docGate.decision,
|
|
243
|
-
confidence: docReview.confidence,
|
|
227
|
+
step: 'write',
|
|
244
228
|
iteration,
|
|
245
229
|
maxIterations: 5,
|
|
246
230
|
},
|
|
247
231
|
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
step: 'gate',
|
|
255
|
-
decision: docGate.decision,
|
|
256
|
-
confidence: docReview.confidence,
|
|
257
|
-
iteration,
|
|
258
|
-
maxIterations: 5,
|
|
259
|
-
},
|
|
232
|
+
// Write docs
|
|
233
|
+
const writeStartMs = Date.now();
|
|
234
|
+
const docRaw = await promptSession(client, sessionId, documentPrompt(docInput), {
|
|
235
|
+
model: docModel,
|
|
236
|
+
agent: 'documenting',
|
|
237
|
+
title: writeTitle,
|
|
260
238
|
});
|
|
261
|
-
|
|
262
|
-
else {
|
|
239
|
+
const writeEndMs = Date.now();
|
|
263
240
|
context.metadata({
|
|
264
|
-
title:
|
|
241
|
+
title: `⏳ Reviewing documentation (iteration ${iteration}/5)...`,
|
|
265
242
|
metadata: {
|
|
266
243
|
phase: 'documentation',
|
|
267
|
-
step: '
|
|
268
|
-
decision: docGate.decision,
|
|
269
|
-
confidence: docReview.confidence,
|
|
244
|
+
step: 'review',
|
|
270
245
|
iteration,
|
|
271
246
|
maxIterations: 5,
|
|
272
247
|
},
|
|
273
248
|
});
|
|
249
|
+
// Review docs
|
|
250
|
+
const reviewStartMs = Date.now();
|
|
251
|
+
const docRevRaw = await promptSession(client, sessionId, docReviewPrompt(docRaw), {
|
|
252
|
+
model: docReviewModel,
|
|
253
|
+
agent: 'reviewing',
|
|
254
|
+
title: reviewTitle,
|
|
255
|
+
});
|
|
256
|
+
const reviewEndMs = Date.now();
|
|
257
|
+
const docReview = parseDocReview(docRevRaw);
|
|
258
|
+
// Gate (deterministic)
|
|
259
|
+
docGate = evaluateMiniLoopDocGate(docReview, iteration);
|
|
260
|
+
if (docGate.decision === 'APPROVED') {
|
|
261
|
+
context.metadata({
|
|
262
|
+
title: `✅ Documentation APPROVED (confidence: ${docReview.confidence}, iteration: ${iteration})`,
|
|
263
|
+
metadata: {
|
|
264
|
+
phase: 'documentation',
|
|
265
|
+
step: 'gate',
|
|
266
|
+
decision: docGate.decision,
|
|
267
|
+
confidence: docReview.confidence,
|
|
268
|
+
iteration,
|
|
269
|
+
maxIterations: 5,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
else if (docGate.decision === 'ESCALATE') {
|
|
274
|
+
context.metadata({
|
|
275
|
+
title: '⚠️ Documentation ESCALATED',
|
|
276
|
+
metadata: {
|
|
277
|
+
phase: 'documentation',
|
|
278
|
+
step: 'gate',
|
|
279
|
+
decision: docGate.decision,
|
|
280
|
+
confidence: docReview.confidence,
|
|
281
|
+
iteration,
|
|
282
|
+
maxIterations: 5,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
context.metadata({
|
|
288
|
+
title: `🔄 Documentation rework required (confidence: ${docReview.confidence}, iteration: ${iteration}/5)`,
|
|
289
|
+
metadata: {
|
|
290
|
+
phase: 'documentation',
|
|
291
|
+
step: 'gate',
|
|
292
|
+
decision: docGate.decision,
|
|
293
|
+
confidence: docReview.confidence,
|
|
294
|
+
iteration,
|
|
295
|
+
maxIterations: 5,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const feedback = docReview.reworkInstructions || docGate.feedback || docReview.raw;
|
|
300
|
+
documentationIterationDetails.push(`### Iteration ${iteration}\n` +
|
|
301
|
+
`- **Write**: ${writeTitle} (${formatElapsed(writeStartMs, writeEndMs)})\n` +
|
|
302
|
+
`- **Review**: ${reviewTitle} (${formatElapsed(reviewStartMs, reviewEndMs)})\n` +
|
|
303
|
+
`- **Gate**: ${docGate.decision} (confidence: ${docReview.confidence}, unresolved: ${docReview.unresolvedIssues})\n` +
|
|
304
|
+
`- **Feedback**: ${feedback}`);
|
|
305
|
+
// Notify each documentation iteration gate decision
|
|
306
|
+
await notify(`# Mini-Loop: Documentation — Iteration ${iteration}/5\n\nStatus: ${docGate.decision}\nConfidence: ${docReview.confidence}%\nUnresolved Issues: ${docReview.unresolvedIssues}\nDuration: ${formatElapsed(documentationStartMs, Date.now())}\nFeedback: ${feedback}\n`);
|
|
307
|
+
if (docGate.decision === 'REWORK') {
|
|
308
|
+
docInput = `${docInput}\n\nDocumentation review feedback:\n${docGate.feedback}`;
|
|
309
|
+
}
|
|
274
310
|
}
|
|
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
|
-
|
|
311
|
+
const documentationEndMs = Date.now();
|
|
312
|
+
addReport('Documentation', `${documentationIterationDetails.join('\n\n')}\n\n**Outcome**: ${docGate.decision === 'APPROVED'
|
|
313
|
+
? 'APPROVED'
|
|
314
|
+
: docGate.decision === 'ESCALATE'
|
|
315
|
+
? `ESCALATE: ${docGate.reason}`
|
|
316
|
+
: `REWORK exhausted (5 iterations). Last feedback: ${docGate.feedback}`}\n**Phase time**: ${formatElapsed(documentationStartMs, documentationEndMs)}`);
|
|
317
|
+
// Phase end notification
|
|
318
|
+
await notify(`# Mini-Loop: Documentation ended\n\nOutcome: ${docGate.decision}\nConfidence: ${docGate.decision === 'APPROVED' ? 'converged' : 'N/A'}\nPhase time: ${formatElapsed(documentationStartMs, documentationEndMs)}\n`);
|
|
319
|
+
const totalEndMs = Date.now();
|
|
320
|
+
const completedWithoutIssues = docGate.decision === 'APPROVED';
|
|
321
|
+
context.metadata({
|
|
322
|
+
title: completedWithoutIssues
|
|
323
|
+
? '✅ Mini-loop complete'
|
|
324
|
+
: '⚠️ Mini-loop finished with issues',
|
|
325
|
+
metadata: {
|
|
326
|
+
phase: 'complete',
|
|
327
|
+
outcome: completedWithoutIssues ? 'success' : 'issues',
|
|
328
|
+
totalElapsedMs: totalEndMs - totalStartMs,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
addReport('Complete', `${completedWithoutIssues ? 'Mini-loop finished successfully.' : 'Mini-loop finished with issues.'}\n**Total time**: ${formatElapsed(totalStartMs, totalEndMs)}`);
|
|
332
|
+
// Send final completion report as notification
|
|
333
|
+
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n`);
|
|
334
|
+
}; // end asyncOrchestration
|
|
335
|
+
// Launch orchestration in background — fire-and-forget
|
|
336
|
+
void asyncOrchestration().catch(async (err) => {
|
|
337
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
338
|
+
await notifyParent(client, sessionId, agent, `# Mini-Loop: Error\n\nPhase: ${lastPhase}\nError: ${message}\n`);
|
|
300
339
|
});
|
|
301
|
-
|
|
302
|
-
|
|
340
|
+
// Return immediately with acknowledgment
|
|
341
|
+
const summary = requirements.length > 120 ? requirements.slice(0, 120) + '...' : requirements;
|
|
342
|
+
return `Mini-loop started for: ${summary}\n\nYou will receive progress updates as each phase completes.`;
|
|
303
343
|
},
|
|
304
344
|
});
|
|
305
345
|
}
|