@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.
@@ -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 sessionId = context.sessionID;
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
- const totalStartMs = Date.now();
69
- const report = [];
70
- const addReport = (phase, msg) => {
71
- report.push(`## ${phase}\n${msg}`);
72
- };
73
- // Notify helper bound to this session
74
- const notify = (message, options) => notifyParent(client, sessionId, agent, message, options);
75
- // ===================================================================
76
- // PHASE 1: IMPLEMENTATION LOOP (build → review → gate, up to 10)
77
- // ===================================================================
78
- lastPhase = 'Implementation';
79
- const implementationStartMs = Date.now();
80
- const implementationIterationDetails = [];
81
- let implGate = { decision: 'REWORK', feedback: requirements };
82
- let lastImplRaw = '';
83
- // Phase start notification
84
- await notify(`# Mini-Loop: Building started\n\nStarting implementation phase...\n`);
85
- for (let implIter = 0; implIter < 10 && implGate.decision === 'REWORK'; implIter++) {
86
- const iteration = implIter + 1;
87
- const buildTitle = `ff-mini-build-${iteration}`;
88
- const reviewTitle = `ff-mini-review-${iteration}`;
89
- const buildInput = implIter === 0
90
- ? requirements
91
- : `${requirements}\n\nPrevious review feedback:\n${implGate.feedback}`;
92
- context.metadata({
93
- title: `⏳ Building (iteration ${iteration}/10)...`,
94
- metadata: {
95
- phase: 'implementation',
96
- step: 'build',
97
- iteration,
98
- maxIterations: 10,
99
- },
100
- });
101
- // Build
102
- const buildStartMs = Date.now();
103
- lastImplRaw = await promptSession(client, sessionId, miniBuildPrompt(buildInput, implIter > 0 ? implGate.feedback : undefined), { model: buildModel, agent: 'building', title: buildTitle });
104
- const buildEndMs = Date.now();
105
- // ---------------------------------------------------------------
106
- // CI validation (deterministic subprocess — not an LLM prompt)
107
- // ---------------------------------------------------------------
108
- const ciDir = process.cwd();
109
- const hasCiScript = await ciScriptExists(ciDir);
110
- if (!hasCiScript) {
111
- await notify(`# Mini-Loop: CI script (ff-ci.sh) not found, skipping CI validation\n`);
112
- }
113
- else {
114
- const maxCiAttempts = 3;
115
- for (let ciAttempt = 1; ciAttempt <= maxCiAttempts; ciAttempt++) {
116
- context.metadata({
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: '❌ CI validation failed (attempts exhausted)',
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 failed\n\nCI checks failed after ${maxCiAttempts} attempts. Aborting.\n`, { noReply: false });
168
- return;
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: `✅ Implementation APPROVED (confidence: ${review.confidence}, iteration: ${iteration})`,
191
+ title: `⏳ Reviewing (iteration ${iteration}/10)...`,
195
192
  metadata: {
196
193
  phase: 'implementation',
197
- step: 'gate',
198
- decision: implGate.decision,
199
- confidence: review.confidence,
194
+ step: 'review',
200
195
  iteration,
201
196
  maxIterations: 10,
202
197
  },
203
198
  });
204
- }
205
- else if (implGate.decision === 'ESCALATE') {
206
- context.metadata({
207
- title: '⚠️ Implementation ESCALATED',
208
- metadata: {
209
- phase: 'implementation',
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 feedback = review.reworkInstructions || implGate.feedback || review.raw;
232
- implementationIterationDetails.push(`### Iteration ${iteration}\n` +
233
- `- **Build**: ${buildTitle} (${formatElapsed(buildStartMs, buildEndMs)})\n` +
234
- `- **Review**: ${reviewTitle} (${formatElapsed(reviewStartMs, reviewEndMs)})\n` +
235
- `- **Gate**: ${implGate.decision} (confidence: ${review.confidence}, change requested: ${review.changeRequested ? 'yes' : 'no'}, unresolved: ${review.unresolvedIssues})\n` +
236
- `- **Feedback**: ${feedback}`);
237
- // Notify each implementation iteration gate decision
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
- // REWORK continues the loop
257
- }
258
- const implementationEndMs = Date.now();
259
- addReport('Implementation', `${implementationIterationDetails.join('\n\n')}\n\n**Outcome**: ${implGate.decision === 'APPROVED'
260
- ? 'APPROVED'
261
- : `REWORK exhausted (10 iterations). Last feedback: ${implGate.feedback}`}\n**Phase time**: ${formatElapsed(implementationStartMs, implementationEndMs)}`);
262
- // Phase end notification
263
- await notify(`# Mini-Loop: Building ended\n\nOutcome: ${implGate.decision}\nIterations: ${implGate.decision === 'APPROVED' ? 'converged' : '10 (exhausted)'}\nPhase time: ${formatElapsed(implementationStartMs, implementationEndMs)}\n`);
264
- if (implGate.decision !== 'APPROVED') {
265
- context.metadata({
266
- title: '⚠️ Mini-loop finished with issues',
267
- metadata: {
268
- phase: 'complete',
269
- outcome: 'issues',
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: `✅ Documentation APPROVED (confidence: ${docReview.confidence}, iteration: ${iteration})`,
310
+ title: `⏳ Writing documentation (iteration ${iteration}/5)...`,
331
311
  metadata: {
332
312
  phase: 'documentation',
333
- step: 'gate',
334
- decision: docGate.decision,
335
- confidence: docReview.confidence,
313
+ step: 'write',
336
314
  iteration,
337
315
  maxIterations: 5,
338
316
  },
339
317
  });
340
- }
341
- else if (docGate.decision === 'ESCALATE') {
342
- context.metadata({
343
- title: '⚠️ Documentation ESCALATED',
344
- metadata: {
345
- phase: 'documentation',
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: `🔄 Documentation rework required (confidence: ${docReview.confidence}, iteration: ${iteration}/5)`,
327
+ title: `⏳ Reviewing documentation (iteration ${iteration}/5)...`,
357
328
  metadata: {
358
329
  phase: 'documentation',
359
- step: 'gate',
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 feedback = docReview.reworkInstructions || docGate.feedback || docReview.raw;
368
- documentationIterationDetails.push(`### Iteration ${iteration}\n` +
369
- `- **Write**: ${writeTitle} (${formatElapsed(writeStartMs, writeEndMs)})\n` +
370
- `- **Review**: ${reviewTitle} (${formatElapsed(reviewStartMs, reviewEndMs)})\n` +
371
- `- **Gate**: ${docGate.decision} (confidence: ${docReview.confidence}, unresolved: ${docReview.unresolvedIssues})\n` +
372
- `- **Feedback**: ${feedback}`);
373
- // Notify each documentation iteration gate decision
374
- 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`);
375
- if (docGate.decision === 'REWORK') {
376
- docInput = `${docInput}\n\nDocumentation review feedback:\n${docGate.feedback}`;
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, sessionId, agent, `# Mini-Loop: Error\n\nPhase: ${lastPhase}\nError: ${message}\n`, { noReply: false });
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
  }