@syntesseraai/opencode-feature-factory 0.10.2 → 0.10.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 +13 -5
- package/agents/feature-factory.md +15 -3
- package/dist/tools/mini-loop.d.ts +4 -0
- package/dist/tools/mini-loop.js +311 -289
- package/dist/tools/pipeline.d.ts +4 -0
- package/dist/tools/pipeline.js +516 -494
- package/dist/workflow/fan-out.d.ts +20 -3
- package/dist/workflow/fan-out.js +34 -8
- package/dist/workflow/orchestrator.d.ts +1 -1
- package/dist/workflow/orchestrator.js +1 -1
- package/dist/workflow/run-isolation.d.ts +13 -0
- package/dist/workflow/run-isolation.js +77 -0
- package/package.json +1 -1
package/dist/tools/mini-loop.js
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* to the parent session via `promptAsync(noReply: true)`.
|
|
10
10
|
*/
|
|
11
11
|
import { tool } from '@opencode-ai/plugin/tool';
|
|
12
|
-
import { promptSession, notifyParent, evaluateMiniLoopImplGate, evaluateMiniLoopDocGate, parseModelString, } from '../workflow/orchestrator.js';
|
|
12
|
+
import { promptSession, notifyParent, createRunParentSession, evaluateMiniLoopImplGate, evaluateMiniLoopDocGate, parseModelString, } from '../workflow/orchestrator.js';
|
|
13
13
|
import { miniBuildPrompt, miniReviewPrompt, documentPrompt, docReviewPrompt, ciFixPrompt, } from './prompts.js';
|
|
14
14
|
import { parseMiniReview, parseDocReview } from './parsers.js';
|
|
15
15
|
import { ciScriptExists, runCI } from '../workflow/ci-runner.js';
|
|
16
|
+
import { cleanupWorktree, resolveRunDirectory } from '../workflow/run-isolation.js';
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Tool factory
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
@@ -42,9 +43,17 @@ export function createMiniLoopTool(client) {
|
|
|
42
43
|
.string()
|
|
43
44
|
.optional()
|
|
44
45
|
.describe('provider/model for documentation review. When omitted, inherits the session model.'),
|
|
46
|
+
worktree_isolation: tool.schema
|
|
47
|
+
.boolean()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('When true, run this workflow in a dedicated detached git worktree to avoid collisions with concurrent runs.'),
|
|
50
|
+
worktree_parent_dir: tool.schema
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Optional parent directory for isolated worktrees. Defaults to .feature-factory/worktrees under the repository root.'),
|
|
45
54
|
},
|
|
46
55
|
async execute(args, context) {
|
|
47
|
-
const
|
|
56
|
+
const callerSessionId = context.sessionID;
|
|
48
57
|
const agent = context.agent;
|
|
49
58
|
const { requirements } = args;
|
|
50
59
|
// Resolve models — use provided overrides or undefined (inherit session model)
|
|
@@ -62,185 +71,215 @@ export function createMiniLoopTool(client) {
|
|
|
62
71
|
const docReviewModel = args.doc_review_model
|
|
63
72
|
? parseModelString(args.doc_review_model)
|
|
64
73
|
: undefined;
|
|
74
|
+
const runDirectoryContext = await resolveRunDirectory('mini-loop', {
|
|
75
|
+
enabled: args.worktree_isolation,
|
|
76
|
+
parentDirectory: args.worktree_parent_dir,
|
|
77
|
+
});
|
|
78
|
+
const runContext = await createRunParentSession(client, callerSessionId, {
|
|
79
|
+
title: `ff-mini-loop-run-${runDirectoryContext.runId}`,
|
|
80
|
+
directory: runDirectoryContext.runDirectory,
|
|
81
|
+
});
|
|
65
82
|
// Fire-and-forget: run orchestration in background
|
|
66
83
|
let lastPhase = 'init';
|
|
67
84
|
const asyncOrchestration = async () => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
title: `⏳ Running CI (attempt ${ciAttempt}/${maxCiAttempts})...`,
|
|
118
|
-
metadata: {
|
|
119
|
-
phase: 'implementation',
|
|
120
|
-
step: 'ci',
|
|
121
|
-
iteration,
|
|
122
|
-
ciAttempt,
|
|
123
|
-
maxCiAttempts,
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
await notify(`# Mini-Loop: CI validation — attempt ${ciAttempt}/${maxCiAttempts}\n`);
|
|
127
|
-
const ciResult = await runCI(ciDir);
|
|
128
|
-
if (ciResult.passed) {
|
|
129
|
-
await notify(`# Mini-Loop: CI passed (attempt ${ciAttempt}/${maxCiAttempts})\n`);
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
// CI failed
|
|
133
|
-
await notify(`# Mini-Loop: CI failed (attempt ${ciAttempt}/${maxCiAttempts})\n\n\`\`\`\n${ciResult.output}\n\`\`\`\n`);
|
|
134
|
-
if (ciAttempt < maxCiAttempts) {
|
|
135
|
-
// Ask build model to fix the CI failures
|
|
136
|
-
context.metadata({
|
|
137
|
-
title: `⏳ CI rework (attempt ${ciAttempt}/${maxCiAttempts})...`,
|
|
138
|
-
metadata: {
|
|
139
|
-
phase: 'implementation',
|
|
140
|
-
step: 'ci',
|
|
141
|
-
iteration,
|
|
142
|
-
ciAttempt,
|
|
143
|
-
maxCiAttempts,
|
|
144
|
-
action: 'rework',
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
lastImplRaw = await promptSession(client, sessionId, ciFixPrompt(requirements, ciResult.output), {
|
|
148
|
-
model: buildModel,
|
|
149
|
-
agent: 'building',
|
|
150
|
-
title: `ff-mini-ci-fix-${iteration}-${ciAttempt}`,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
// Terminal CI failure — exhausted all attempts
|
|
155
|
-
addReport('CI Validation', `CI failed after ${maxCiAttempts} attempts.\n\n**Last output**:\n\`\`\`\n${ciResult.output}\n\`\`\`\n`);
|
|
85
|
+
try {
|
|
86
|
+
const totalStartMs = Date.now();
|
|
87
|
+
const report = [];
|
|
88
|
+
const addReport = (phase, msg) => {
|
|
89
|
+
report.push(`## ${phase}\n${msg}`);
|
|
90
|
+
};
|
|
91
|
+
// Notify helper bound to this session
|
|
92
|
+
const notify = (message, options) => notifyParent(client, runContext, agent, message, options);
|
|
93
|
+
// ===================================================================
|
|
94
|
+
// PHASE 1: IMPLEMENTATION LOOP (build → review → gate, up to 10)
|
|
95
|
+
// ===================================================================
|
|
96
|
+
lastPhase = 'Implementation';
|
|
97
|
+
const implementationStartMs = Date.now();
|
|
98
|
+
const implementationIterationDetails = [];
|
|
99
|
+
let implGate = { decision: 'REWORK', feedback: requirements };
|
|
100
|
+
let lastImplRaw = '';
|
|
101
|
+
// Phase start notification
|
|
102
|
+
await notify(`# Mini-Loop: Building started\n\nStarting implementation phase...\n`);
|
|
103
|
+
for (let implIter = 0; implIter < 10 && implGate.decision === 'REWORK'; implIter++) {
|
|
104
|
+
const iteration = implIter + 1;
|
|
105
|
+
const buildTitle = `ff-mini-build-${iteration}`;
|
|
106
|
+
const reviewTitle = `ff-mini-review-${iteration}`;
|
|
107
|
+
const buildInput = implIter === 0
|
|
108
|
+
? requirements
|
|
109
|
+
: `${requirements}\n\nPrevious review feedback:\n${implGate.feedback}`;
|
|
110
|
+
context.metadata({
|
|
111
|
+
title: `⏳ Building (iteration ${iteration}/10)...`,
|
|
112
|
+
metadata: {
|
|
113
|
+
phase: 'implementation',
|
|
114
|
+
step: 'build',
|
|
115
|
+
iteration,
|
|
116
|
+
maxIterations: 10,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
// Build
|
|
120
|
+
const buildStartMs = Date.now();
|
|
121
|
+
lastImplRaw = await promptSession(client, runContext, miniBuildPrompt(buildInput, implIter > 0 ? implGate.feedback : undefined), { model: buildModel, agent: 'building', title: buildTitle });
|
|
122
|
+
const buildEndMs = Date.now();
|
|
123
|
+
// ---------------------------------------------------------------
|
|
124
|
+
// CI validation (deterministic subprocess — not an LLM prompt)
|
|
125
|
+
// ---------------------------------------------------------------
|
|
126
|
+
const ciDir = runDirectoryContext.runDirectory;
|
|
127
|
+
const hasCiScript = await ciScriptExists(ciDir);
|
|
128
|
+
if (!hasCiScript) {
|
|
129
|
+
await notify(`# Mini-Loop: CI script (ff-ci.sh) not found, skipping CI validation\n`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
const maxCiAttempts = 3;
|
|
133
|
+
for (let ciAttempt = 1; ciAttempt <= maxCiAttempts; ciAttempt++) {
|
|
156
134
|
context.metadata({
|
|
157
|
-
title:
|
|
135
|
+
title: `⏳ Running CI (attempt ${ciAttempt}/${maxCiAttempts})...`,
|
|
158
136
|
metadata: {
|
|
159
137
|
phase: 'implementation',
|
|
160
138
|
step: 'ci',
|
|
161
139
|
iteration,
|
|
162
140
|
ciAttempt,
|
|
163
141
|
maxCiAttempts,
|
|
164
|
-
outcome: 'failed',
|
|
165
142
|
},
|
|
166
143
|
});
|
|
167
|
-
await notify(`# Mini-Loop: CI validation
|
|
168
|
-
|
|
144
|
+
await notify(`# Mini-Loop: CI validation — attempt ${ciAttempt}/${maxCiAttempts}\n`);
|
|
145
|
+
const ciResult = await runCI(ciDir);
|
|
146
|
+
if (ciResult.passed) {
|
|
147
|
+
await notify(`# Mini-Loop: CI passed (attempt ${ciAttempt}/${maxCiAttempts})\n`);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
// CI failed
|
|
151
|
+
await notify(`# Mini-Loop: CI failed (attempt ${ciAttempt}/${maxCiAttempts})\n\n\`\`\`\n${ciResult.output}\n\`\`\`\n`);
|
|
152
|
+
if (ciAttempt < maxCiAttempts) {
|
|
153
|
+
// Ask build model to fix the CI failures
|
|
154
|
+
context.metadata({
|
|
155
|
+
title: `⏳ CI rework (attempt ${ciAttempt}/${maxCiAttempts})...`,
|
|
156
|
+
metadata: {
|
|
157
|
+
phase: 'implementation',
|
|
158
|
+
step: 'ci',
|
|
159
|
+
iteration,
|
|
160
|
+
ciAttempt,
|
|
161
|
+
maxCiAttempts,
|
|
162
|
+
action: 'rework',
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
lastImplRaw = await promptSession(client, runContext, ciFixPrompt(requirements, ciResult.output), {
|
|
166
|
+
model: buildModel,
|
|
167
|
+
agent: 'building',
|
|
168
|
+
title: `ff-mini-ci-fix-${iteration}-${ciAttempt}`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// Terminal CI failure — exhausted all attempts
|
|
173
|
+
addReport('CI Validation', `CI failed after ${maxCiAttempts} attempts.\n\n**Last output**:\n\`\`\`\n${ciResult.output}\n\`\`\`\n`);
|
|
174
|
+
context.metadata({
|
|
175
|
+
title: '❌ CI validation failed (attempts exhausted)',
|
|
176
|
+
metadata: {
|
|
177
|
+
phase: 'implementation',
|
|
178
|
+
step: 'ci',
|
|
179
|
+
iteration,
|
|
180
|
+
ciAttempt,
|
|
181
|
+
maxCiAttempts,
|
|
182
|
+
outcome: 'failed',
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
await notify(`# Mini-Loop: CI validation failed\n\nCI checks failed after ${maxCiAttempts} attempts. Aborting.\n`, { noReply: false });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
169
188
|
}
|
|
170
189
|
}
|
|
171
|
-
}
|
|
172
|
-
context.metadata({
|
|
173
|
-
title: `⏳ Reviewing (iteration ${iteration}/10)...`,
|
|
174
|
-
metadata: {
|
|
175
|
-
phase: 'implementation',
|
|
176
|
-
step: 'review',
|
|
177
|
-
iteration,
|
|
178
|
-
maxIterations: 10,
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
// Review
|
|
182
|
-
const reviewStartMs = Date.now();
|
|
183
|
-
const reviewRaw = await promptSession(client, sessionId, miniReviewPrompt(lastImplRaw), {
|
|
184
|
-
model: reviewModel,
|
|
185
|
-
agent: 'reviewing',
|
|
186
|
-
title: reviewTitle,
|
|
187
|
-
});
|
|
188
|
-
const reviewEndMs = Date.now();
|
|
189
|
-
const review = parseMiniReview(reviewRaw);
|
|
190
|
-
// Gate (deterministic)
|
|
191
|
-
implGate = evaluateMiniLoopImplGate(review, iteration);
|
|
192
|
-
if (implGate.decision === 'APPROVED') {
|
|
193
190
|
context.metadata({
|
|
194
|
-
title:
|
|
191
|
+
title: `⏳ Reviewing (iteration ${iteration}/10)...`,
|
|
195
192
|
metadata: {
|
|
196
193
|
phase: 'implementation',
|
|
197
|
-
step: '
|
|
198
|
-
decision: implGate.decision,
|
|
199
|
-
confidence: review.confidence,
|
|
194
|
+
step: 'review',
|
|
200
195
|
iteration,
|
|
201
196
|
maxIterations: 10,
|
|
202
197
|
},
|
|
203
198
|
});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
step: 'gate',
|
|
211
|
-
decision: implGate.decision,
|
|
212
|
-
confidence: review.confidence,
|
|
213
|
-
iteration,
|
|
214
|
-
maxIterations: 10,
|
|
215
|
-
},
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
context.metadata({
|
|
220
|
-
title: `🔄 Rework required (confidence: ${review.confidence}, iteration: ${iteration}/10)`,
|
|
221
|
-
metadata: {
|
|
222
|
-
phase: 'implementation',
|
|
223
|
-
step: 'gate',
|
|
224
|
-
decision: implGate.decision,
|
|
225
|
-
confidence: review.confidence,
|
|
226
|
-
iteration,
|
|
227
|
-
maxIterations: 10,
|
|
228
|
-
},
|
|
199
|
+
// Review
|
|
200
|
+
const reviewStartMs = Date.now();
|
|
201
|
+
const reviewRaw = await promptSession(client, runContext, miniReviewPrompt(lastImplRaw), {
|
|
202
|
+
model: reviewModel,
|
|
203
|
+
agent: 'reviewing',
|
|
204
|
+
title: reviewTitle,
|
|
229
205
|
});
|
|
206
|
+
const reviewEndMs = Date.now();
|
|
207
|
+
const review = parseMiniReview(reviewRaw);
|
|
208
|
+
// Gate (deterministic)
|
|
209
|
+
implGate = evaluateMiniLoopImplGate(review, iteration);
|
|
210
|
+
if (implGate.decision === 'APPROVED') {
|
|
211
|
+
context.metadata({
|
|
212
|
+
title: `✅ Implementation APPROVED (confidence: ${review.confidence}, iteration: ${iteration})`,
|
|
213
|
+
metadata: {
|
|
214
|
+
phase: 'implementation',
|
|
215
|
+
step: 'gate',
|
|
216
|
+
decision: implGate.decision,
|
|
217
|
+
confidence: review.confidence,
|
|
218
|
+
iteration,
|
|
219
|
+
maxIterations: 10,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
else if (implGate.decision === 'ESCALATE') {
|
|
224
|
+
context.metadata({
|
|
225
|
+
title: '⚠️ Implementation ESCALATED',
|
|
226
|
+
metadata: {
|
|
227
|
+
phase: 'implementation',
|
|
228
|
+
step: 'gate',
|
|
229
|
+
decision: implGate.decision,
|
|
230
|
+
confidence: review.confidence,
|
|
231
|
+
iteration,
|
|
232
|
+
maxIterations: 10,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
context.metadata({
|
|
238
|
+
title: `🔄 Rework required (confidence: ${review.confidence}, iteration: ${iteration}/10)`,
|
|
239
|
+
metadata: {
|
|
240
|
+
phase: 'implementation',
|
|
241
|
+
step: 'gate',
|
|
242
|
+
decision: implGate.decision,
|
|
243
|
+
confidence: review.confidence,
|
|
244
|
+
iteration,
|
|
245
|
+
maxIterations: 10,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const feedback = review.reworkInstructions || implGate.feedback || review.raw;
|
|
250
|
+
implementationIterationDetails.push(`### Iteration ${iteration}\n` +
|
|
251
|
+
`- **Build**: ${buildTitle} (${formatElapsed(buildStartMs, buildEndMs)})\n` +
|
|
252
|
+
`- **Review**: ${reviewTitle} (${formatElapsed(reviewStartMs, reviewEndMs)})\n` +
|
|
253
|
+
`- **Gate**: ${implGate.decision} (confidence: ${review.confidence}, change requested: ${review.changeRequested ? 'yes' : 'no'}, unresolved: ${review.unresolvedIssues})\n` +
|
|
254
|
+
`- **Feedback**: ${feedback}`);
|
|
255
|
+
// Notify each implementation iteration gate decision
|
|
256
|
+
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`);
|
|
257
|
+
if (implGate.decision === 'ESCALATE') {
|
|
258
|
+
const implementationEndMs = Date.now();
|
|
259
|
+
addReport('Implementation', `${implementationIterationDetails.join('\n\n')}\n\n**Outcome**: ESCALATE: ${implGate.reason}\n**Phase time**: ${formatElapsed(implementationStartMs, implementationEndMs)}`);
|
|
260
|
+
// Phase end notification
|
|
261
|
+
await notify(`# Mini-Loop: Building ended\n\nOutcome: ESCALATE\nReason: ${implGate.reason}\nIterations: ${iteration}\nPhase time: ${formatElapsed(implementationStartMs, implementationEndMs)}\n`);
|
|
262
|
+
context.metadata({
|
|
263
|
+
title: '⚠️ Mini-loop finished with issues',
|
|
264
|
+
metadata: {
|
|
265
|
+
phase: 'complete',
|
|
266
|
+
outcome: 'issues',
|
|
267
|
+
totalElapsedMs: Date.now() - totalStartMs,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
addReport('Complete', `Mini-loop finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
271
|
+
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// REWORK continues the loop
|
|
230
275
|
}
|
|
231
|
-
const
|
|
232
|
-
implementationIterationDetails.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
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`);
|
|
239
|
-
if (implGate.decision === 'ESCALATE') {
|
|
240
|
-
const implementationEndMs = Date.now();
|
|
241
|
-
addReport('Implementation', `${implementationIterationDetails.join('\n\n')}\n\n**Outcome**: ESCALATE: ${implGate.reason}\n**Phase time**: ${formatElapsed(implementationStartMs, implementationEndMs)}`);
|
|
242
|
-
// Phase end notification
|
|
243
|
-
await notify(`# Mini-Loop: Building ended\n\nOutcome: ESCALATE\nReason: ${implGate.reason}\nIterations: ${iteration}\nPhase time: ${formatElapsed(implementationStartMs, implementationEndMs)}\n`);
|
|
276
|
+
const implementationEndMs = Date.now();
|
|
277
|
+
addReport('Implementation', `${implementationIterationDetails.join('\n\n')}\n\n**Outcome**: ${implGate.decision === 'APPROVED'
|
|
278
|
+
? 'APPROVED'
|
|
279
|
+
: `REWORK exhausted (10 iterations). Last feedback: ${implGate.feedback}`}\n**Phase time**: ${formatElapsed(implementationStartMs, implementationEndMs)}`);
|
|
280
|
+
// Phase end notification
|
|
281
|
+
await notify(`# Mini-Loop: Building ended\n\nOutcome: ${implGate.decision}\nIterations: ${implGate.decision === 'APPROVED' ? 'converged' : '10 (exhausted)'}\nPhase time: ${formatElapsed(implementationStartMs, implementationEndMs)}\n`);
|
|
282
|
+
if (implGate.decision !== 'APPROVED') {
|
|
244
283
|
context.metadata({
|
|
245
284
|
title: '⚠️ Mini-loop finished with issues',
|
|
246
285
|
metadata: {
|
|
@@ -253,161 +292,144 @@ export function createMiniLoopTool(client) {
|
|
|
253
292
|
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
254
293
|
return;
|
|
255
294
|
}
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
totalElapsedMs: Date.now() - totalStartMs,
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
addReport('Complete', `Mini-loop finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
274
|
-
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
// ===================================================================
|
|
278
|
-
// PHASE 2: DOCUMENTATION LOOP (document → review → gate, up to 5)
|
|
279
|
-
// ===================================================================
|
|
280
|
-
lastPhase = 'Documentation';
|
|
281
|
-
const documentationStartMs = Date.now();
|
|
282
|
-
const documentationIterationDetails = [];
|
|
283
|
-
let docInput = lastImplRaw;
|
|
284
|
-
let docGate = { decision: 'REWORK' };
|
|
285
|
-
// Phase start notification
|
|
286
|
-
await notify(`# Mini-Loop: Documentation started\n\nStarting documentation phase...\n`);
|
|
287
|
-
for (let docIter = 0; docIter < 5 && docGate.decision === 'REWORK'; docIter++) {
|
|
288
|
-
const iteration = docIter + 1;
|
|
289
|
-
const writeTitle = `ff-mini-doc-write-${iteration}`;
|
|
290
|
-
const reviewTitle = `ff-mini-doc-review-${iteration}`;
|
|
291
|
-
context.metadata({
|
|
292
|
-
title: `⏳ Writing documentation (iteration ${iteration}/5)...`,
|
|
293
|
-
metadata: {
|
|
294
|
-
phase: 'documentation',
|
|
295
|
-
step: 'write',
|
|
296
|
-
iteration,
|
|
297
|
-
maxIterations: 5,
|
|
298
|
-
},
|
|
299
|
-
});
|
|
300
|
-
// Write docs
|
|
301
|
-
const writeStartMs = Date.now();
|
|
302
|
-
const docRaw = await promptSession(client, sessionId, documentPrompt(docInput), {
|
|
303
|
-
model: docModel,
|
|
304
|
-
agent: 'documenting',
|
|
305
|
-
title: writeTitle,
|
|
306
|
-
});
|
|
307
|
-
const writeEndMs = Date.now();
|
|
308
|
-
context.metadata({
|
|
309
|
-
title: `⏳ Reviewing documentation (iteration ${iteration}/5)...`,
|
|
310
|
-
metadata: {
|
|
311
|
-
phase: 'documentation',
|
|
312
|
-
step: 'review',
|
|
313
|
-
iteration,
|
|
314
|
-
maxIterations: 5,
|
|
315
|
-
},
|
|
316
|
-
});
|
|
317
|
-
// Review docs
|
|
318
|
-
const reviewStartMs = Date.now();
|
|
319
|
-
const docRevRaw = await promptSession(client, sessionId, docReviewPrompt(docRaw), {
|
|
320
|
-
model: docReviewModel,
|
|
321
|
-
agent: 'reviewing',
|
|
322
|
-
title: reviewTitle,
|
|
323
|
-
});
|
|
324
|
-
const reviewEndMs = Date.now();
|
|
325
|
-
const docReview = parseDocReview(docRevRaw);
|
|
326
|
-
// Gate (deterministic)
|
|
327
|
-
docGate = evaluateMiniLoopDocGate(docReview, iteration);
|
|
328
|
-
if (docGate.decision === 'APPROVED') {
|
|
295
|
+
// ===================================================================
|
|
296
|
+
// PHASE 2: DOCUMENTATION LOOP (document → review → gate, up to 5)
|
|
297
|
+
// ===================================================================
|
|
298
|
+
lastPhase = 'Documentation';
|
|
299
|
+
const documentationStartMs = Date.now();
|
|
300
|
+
const documentationIterationDetails = [];
|
|
301
|
+
let docInput = lastImplRaw;
|
|
302
|
+
let docGate = { decision: 'REWORK' };
|
|
303
|
+
// Phase start notification
|
|
304
|
+
await notify(`# Mini-Loop: Documentation started\n\nStarting documentation phase...\n`);
|
|
305
|
+
for (let docIter = 0; docIter < 5 && docGate.decision === 'REWORK'; docIter++) {
|
|
306
|
+
const iteration = docIter + 1;
|
|
307
|
+
const writeTitle = `ff-mini-doc-write-${iteration}`;
|
|
308
|
+
const reviewTitle = `ff-mini-doc-review-${iteration}`;
|
|
329
309
|
context.metadata({
|
|
330
|
-
title:
|
|
310
|
+
title: `⏳ Writing documentation (iteration ${iteration}/5)...`,
|
|
331
311
|
metadata: {
|
|
332
312
|
phase: 'documentation',
|
|
333
|
-
step: '
|
|
334
|
-
decision: docGate.decision,
|
|
335
|
-
confidence: docReview.confidence,
|
|
313
|
+
step: 'write',
|
|
336
314
|
iteration,
|
|
337
315
|
maxIterations: 5,
|
|
338
316
|
},
|
|
339
317
|
});
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
step: 'gate',
|
|
347
|
-
decision: docGate.decision,
|
|
348
|
-
confidence: docReview.confidence,
|
|
349
|
-
iteration,
|
|
350
|
-
maxIterations: 5,
|
|
351
|
-
},
|
|
318
|
+
// Write docs
|
|
319
|
+
const writeStartMs = Date.now();
|
|
320
|
+
const docRaw = await promptSession(client, runContext, documentPrompt(docInput), {
|
|
321
|
+
model: docModel,
|
|
322
|
+
agent: 'documenting',
|
|
323
|
+
title: writeTitle,
|
|
352
324
|
});
|
|
353
|
-
|
|
354
|
-
else {
|
|
325
|
+
const writeEndMs = Date.now();
|
|
355
326
|
context.metadata({
|
|
356
|
-
title:
|
|
327
|
+
title: `⏳ Reviewing documentation (iteration ${iteration}/5)...`,
|
|
357
328
|
metadata: {
|
|
358
329
|
phase: 'documentation',
|
|
359
|
-
step: '
|
|
360
|
-
decision: docGate.decision,
|
|
361
|
-
confidence: docReview.confidence,
|
|
330
|
+
step: 'review',
|
|
362
331
|
iteration,
|
|
363
332
|
maxIterations: 5,
|
|
364
333
|
},
|
|
365
334
|
});
|
|
335
|
+
// Review docs
|
|
336
|
+
const reviewStartMs = Date.now();
|
|
337
|
+
const docRevRaw = await promptSession(client, runContext, docReviewPrompt(docRaw), {
|
|
338
|
+
model: docReviewModel,
|
|
339
|
+
agent: 'reviewing',
|
|
340
|
+
title: reviewTitle,
|
|
341
|
+
});
|
|
342
|
+
const reviewEndMs = Date.now();
|
|
343
|
+
const docReview = parseDocReview(docRevRaw);
|
|
344
|
+
// Gate (deterministic)
|
|
345
|
+
docGate = evaluateMiniLoopDocGate(docReview, iteration);
|
|
346
|
+
if (docGate.decision === 'APPROVED') {
|
|
347
|
+
context.metadata({
|
|
348
|
+
title: `✅ Documentation APPROVED (confidence: ${docReview.confidence}, iteration: ${iteration})`,
|
|
349
|
+
metadata: {
|
|
350
|
+
phase: 'documentation',
|
|
351
|
+
step: 'gate',
|
|
352
|
+
decision: docGate.decision,
|
|
353
|
+
confidence: docReview.confidence,
|
|
354
|
+
iteration,
|
|
355
|
+
maxIterations: 5,
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
else if (docGate.decision === 'ESCALATE') {
|
|
360
|
+
context.metadata({
|
|
361
|
+
title: '⚠️ Documentation ESCALATED',
|
|
362
|
+
metadata: {
|
|
363
|
+
phase: 'documentation',
|
|
364
|
+
step: 'gate',
|
|
365
|
+
decision: docGate.decision,
|
|
366
|
+
confidence: docReview.confidence,
|
|
367
|
+
iteration,
|
|
368
|
+
maxIterations: 5,
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
context.metadata({
|
|
374
|
+
title: `🔄 Documentation rework required (confidence: ${docReview.confidence}, iteration: ${iteration}/5)`,
|
|
375
|
+
metadata: {
|
|
376
|
+
phase: 'documentation',
|
|
377
|
+
step: 'gate',
|
|
378
|
+
decision: docGate.decision,
|
|
379
|
+
confidence: docReview.confidence,
|
|
380
|
+
iteration,
|
|
381
|
+
maxIterations: 5,
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
const feedback = docReview.reworkInstructions || docGate.feedback || docReview.raw;
|
|
386
|
+
documentationIterationDetails.push(`### Iteration ${iteration}\n` +
|
|
387
|
+
`- **Write**: ${writeTitle} (${formatElapsed(writeStartMs, writeEndMs)})\n` +
|
|
388
|
+
`- **Review**: ${reviewTitle} (${formatElapsed(reviewStartMs, reviewEndMs)})\n` +
|
|
389
|
+
`- **Gate**: ${docGate.decision} (confidence: ${docReview.confidence}, unresolved: ${docReview.unresolvedIssues})\n` +
|
|
390
|
+
`- **Feedback**: ${feedback}`);
|
|
391
|
+
// Notify each documentation iteration gate decision
|
|
392
|
+
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`);
|
|
393
|
+
if (docGate.decision === 'REWORK') {
|
|
394
|
+
docInput = `${docInput}\n\nDocumentation review feedback:\n${docGate.feedback}`;
|
|
395
|
+
}
|
|
366
396
|
}
|
|
367
|
-
const
|
|
368
|
-
documentationIterationDetails.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
//
|
|
374
|
-
await notify(`# Mini-Loop: Documentation
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
397
|
+
const documentationEndMs = Date.now();
|
|
398
|
+
addReport('Documentation', `${documentationIterationDetails.join('\n\n')}\n\n**Outcome**: ${docGate.decision === 'APPROVED'
|
|
399
|
+
? 'APPROVED'
|
|
400
|
+
: docGate.decision === 'ESCALATE'
|
|
401
|
+
? `ESCALATE: ${docGate.reason}`
|
|
402
|
+
: `REWORK exhausted (5 iterations). Last feedback: ${docGate.feedback}`}\n**Phase time**: ${formatElapsed(documentationStartMs, documentationEndMs)}`);
|
|
403
|
+
// Phase end notification
|
|
404
|
+
await notify(`# Mini-Loop: Documentation ended\n\nOutcome: ${docGate.decision}\nConfidence: ${docGate.decision === 'APPROVED' ? 'converged' : 'N/A'}\nPhase time: ${formatElapsed(documentationStartMs, documentationEndMs)}\n`);
|
|
405
|
+
const totalEndMs = Date.now();
|
|
406
|
+
const completedWithoutIssues = docGate.decision === 'APPROVED';
|
|
407
|
+
context.metadata({
|
|
408
|
+
title: completedWithoutIssues
|
|
409
|
+
? '✅ Mini-loop complete'
|
|
410
|
+
: '⚠️ Mini-loop finished with issues',
|
|
411
|
+
metadata: {
|
|
412
|
+
phase: 'complete',
|
|
413
|
+
outcome: completedWithoutIssues ? 'success' : 'issues',
|
|
414
|
+
totalElapsedMs: totalEndMs - totalStartMs,
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
addReport('Complete', `${completedWithoutIssues ? 'Mini-loop finished successfully.' : 'Mini-loop finished with issues.'}\n**Total time**: ${formatElapsed(totalStartMs, totalEndMs)}`);
|
|
418
|
+
// Send final completion report as notification
|
|
419
|
+
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
await cleanupWorktree(runDirectoryContext);
|
|
378
423
|
}
|
|
379
|
-
const documentationEndMs = Date.now();
|
|
380
|
-
addReport('Documentation', `${documentationIterationDetails.join('\n\n')}\n\n**Outcome**: ${docGate.decision === 'APPROVED'
|
|
381
|
-
? 'APPROVED'
|
|
382
|
-
: docGate.decision === 'ESCALATE'
|
|
383
|
-
? `ESCALATE: ${docGate.reason}`
|
|
384
|
-
: `REWORK exhausted (5 iterations). Last feedback: ${docGate.feedback}`}\n**Phase time**: ${formatElapsed(documentationStartMs, documentationEndMs)}`);
|
|
385
|
-
// Phase end notification
|
|
386
|
-
await notify(`# Mini-Loop: Documentation ended\n\nOutcome: ${docGate.decision}\nConfidence: ${docGate.decision === 'APPROVED' ? 'converged' : 'N/A'}\nPhase time: ${formatElapsed(documentationStartMs, documentationEndMs)}\n`);
|
|
387
|
-
const totalEndMs = Date.now();
|
|
388
|
-
const completedWithoutIssues = docGate.decision === 'APPROVED';
|
|
389
|
-
context.metadata({
|
|
390
|
-
title: completedWithoutIssues
|
|
391
|
-
? '✅ Mini-loop complete'
|
|
392
|
-
: '⚠️ Mini-loop finished with issues',
|
|
393
|
-
metadata: {
|
|
394
|
-
phase: 'complete',
|
|
395
|
-
outcome: completedWithoutIssues ? 'success' : 'issues',
|
|
396
|
-
totalElapsedMs: totalEndMs - totalStartMs,
|
|
397
|
-
},
|
|
398
|
-
});
|
|
399
|
-
addReport('Complete', `${completedWithoutIssues ? 'Mini-loop finished successfully.' : 'Mini-loop finished with issues.'}\n**Total time**: ${formatElapsed(totalStartMs, totalEndMs)}`);
|
|
400
|
-
// Send final completion report as notification
|
|
401
|
-
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
402
424
|
}; // end asyncOrchestration
|
|
403
425
|
// Launch orchestration in background — fire-and-forget
|
|
404
426
|
void asyncOrchestration().catch(async (err) => {
|
|
405
427
|
const message = err instanceof Error ? err.message : String(err);
|
|
406
|
-
await notifyParent(client,
|
|
428
|
+
await notifyParent(client, runContext, agent, `# Mini-Loop: Error\n\nPhase: ${lastPhase}\nError: ${message}\n`, { noReply: false });
|
|
407
429
|
});
|
|
408
430
|
// Return immediately with acknowledgment
|
|
409
431
|
const summary = requirements.length > 120 ? requirements.slice(0, 120) + '...' : requirements;
|
|
410
|
-
return `Mini-loop started for: ${summary}\n\nYou will receive progress updates as each phase completes.`;
|
|
432
|
+
return `Mini-loop started for: ${summary}\n\nRun session: ${runContext.sessionId}\nRun directory: ${runDirectoryContext.runDirectory}\nWorktree isolation: ${runDirectoryContext.worktreeEnabled ? 'enabled' : 'disabled'}\nYou will receive progress updates as each phase completes.`;
|
|
411
433
|
},
|
|
412
434
|
});
|
|
413
435
|
}
|