@syntesseraai/opencode-feature-factory 0.10.1 → 0.10.2
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 +6 -6
- package/dist/tools/mini-loop.js +74 -6
- package/dist/tools/pipeline.js +74 -8
- package/dist/tools/prompts.d.ts +1 -0
- package/dist/tools/prompts.js +16 -0
- package/dist/workflow/ci-runner.d.ts +22 -0
- package/dist/workflow/ci-runner.js +91 -0
- package/dist/workflow/fan-out.d.ts +8 -4
- package/dist/workflow/fan-out.js +7 -5
- package/dist/workflow/orchestrator.d.ts +1 -0
- package/dist/workflow/orchestrator.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,11 +61,11 @@ These colors are intentionally unique to avoid collisions in OpenCode agent UIs
|
|
|
61
61
|
|
|
62
62
|
The plugin exposes three MCP tools via the `feature-factory` agent:
|
|
63
63
|
|
|
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.
|
|
64
|
+
| Tool | Description |
|
|
65
|
+
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| `ff_pipeline` | Full multi-model pipeline: planning → build → CI → review → documentation. Uses hardcoded per-role model defaults (see Model Routing below). |
|
|
67
|
+
| `ff_mini_loop` | Lightweight build → CI → review → documentation loop. **Does not hardcode model defaults** — all roles inherit the current session model when omitted. CI validation runs `ff-ci.sh` if present (up to 3 attempts with auto-fix). |
|
|
68
|
+
| `ff_list_models` | Read-only discovery tool. Queries the OpenCode SDK to list all available providers, models, capability badges, connected status, and defaults. |
|
|
69
69
|
|
|
70
70
|
### Mini-Loop Model Inheritance
|
|
71
71
|
|
|
@@ -118,7 +118,7 @@ Both `ff_pipeline` and `ff_mini_loop` tools run asynchronously with real-time pr
|
|
|
118
118
|
- **Immediate return**: Tools return instantly with a brief acknowledgment (e.g. `Pipeline started for: <summary>`), so the LLM can continue the conversation.
|
|
119
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
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.
|
|
121
|
+
- **Phase-by-phase visibility**: Users see updates for planning, building, CI validation (when `ff-ci.sh` present), each review iteration gate decision, each documentation iteration, and the final completion report.
|
|
122
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
123
|
- **`context.metadata()` retained**: All existing metadata calls remain in place for future-proofing (when OpenCode's TUI renders tool metadata natively).
|
|
124
124
|
|
package/dist/tools/mini-loop.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { tool } from '@opencode-ai/plugin/tool';
|
|
12
12
|
import { promptSession, notifyParent, evaluateMiniLoopImplGate, evaluateMiniLoopDocGate, parseModelString, } from '../workflow/orchestrator.js';
|
|
13
|
-
import { miniBuildPrompt, miniReviewPrompt, documentPrompt, docReviewPrompt } from './prompts.js';
|
|
13
|
+
import { miniBuildPrompt, miniReviewPrompt, documentPrompt, docReviewPrompt, ciFixPrompt, } from './prompts.js';
|
|
14
14
|
import { parseMiniReview, parseDocReview } from './parsers.js';
|
|
15
|
+
import { ciScriptExists, runCI } from '../workflow/ci-runner.js';
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
// Tool factory
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
@@ -70,7 +71,7 @@ export function createMiniLoopTool(client) {
|
|
|
70
71
|
report.push(`## ${phase}\n${msg}`);
|
|
71
72
|
};
|
|
72
73
|
// Notify helper bound to this session
|
|
73
|
-
const notify = (message) => notifyParent(client, sessionId, agent, message);
|
|
74
|
+
const notify = (message, options) => notifyParent(client, sessionId, agent, message, options);
|
|
74
75
|
// ===================================================================
|
|
75
76
|
// PHASE 1: IMPLEMENTATION LOOP (build → review → gate, up to 10)
|
|
76
77
|
// ===================================================================
|
|
@@ -101,6 +102,73 @@ export function createMiniLoopTool(client) {
|
|
|
101
102
|
const buildStartMs = Date.now();
|
|
102
103
|
lastImplRaw = await promptSession(client, sessionId, miniBuildPrompt(buildInput, implIter > 0 ? implGate.feedback : undefined), { model: buildModel, agent: 'building', title: buildTitle });
|
|
103
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`);
|
|
156
|
+
context.metadata({
|
|
157
|
+
title: '❌ CI validation failed (attempts exhausted)',
|
|
158
|
+
metadata: {
|
|
159
|
+
phase: 'implementation',
|
|
160
|
+
step: 'ci',
|
|
161
|
+
iteration,
|
|
162
|
+
ciAttempt,
|
|
163
|
+
maxCiAttempts,
|
|
164
|
+
outcome: 'failed',
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
await notify(`# Mini-Loop: CI validation failed\n\nCI checks failed after ${maxCiAttempts} attempts. Aborting.\n`, { noReply: false });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
104
172
|
context.metadata({
|
|
105
173
|
title: `⏳ Reviewing (iteration ${iteration}/10)...`,
|
|
106
174
|
metadata: {
|
|
@@ -182,7 +250,7 @@ export function createMiniLoopTool(client) {
|
|
|
182
250
|
},
|
|
183
251
|
});
|
|
184
252
|
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
|
|
253
|
+
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
186
254
|
return;
|
|
187
255
|
}
|
|
188
256
|
// REWORK continues the loop
|
|
@@ -203,7 +271,7 @@ export function createMiniLoopTool(client) {
|
|
|
203
271
|
},
|
|
204
272
|
});
|
|
205
273
|
addReport('Complete', `Mini-loop finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
206
|
-
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n
|
|
274
|
+
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
207
275
|
return;
|
|
208
276
|
}
|
|
209
277
|
// ===================================================================
|
|
@@ -330,12 +398,12 @@ export function createMiniLoopTool(client) {
|
|
|
330
398
|
});
|
|
331
399
|
addReport('Complete', `${completedWithoutIssues ? 'Mini-loop finished successfully.' : 'Mini-loop finished with issues.'}\n**Total time**: ${formatElapsed(totalStartMs, totalEndMs)}`);
|
|
332
400
|
// Send final completion report as notification
|
|
333
|
-
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n
|
|
401
|
+
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
334
402
|
}; // end asyncOrchestration
|
|
335
403
|
// Launch orchestration in background — fire-and-forget
|
|
336
404
|
void asyncOrchestration().catch(async (err) => {
|
|
337
405
|
const message = err instanceof Error ? err.message : String(err);
|
|
338
|
-
await notifyParent(client, sessionId, agent, `# Mini-Loop: Error\n\nPhase: ${lastPhase}\nError: ${message}\n
|
|
406
|
+
await notifyParent(client, sessionId, agent, `# Mini-Loop: Error\n\nPhase: ${lastPhase}\nError: ${message}\n`, { noReply: false });
|
|
339
407
|
});
|
|
340
408
|
// Return immediately with acknowledgment
|
|
341
409
|
const summary = requirements.length > 120 ? requirements.slice(0, 120) + '...' : requirements;
|
package/dist/tools/pipeline.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { tool } from '@opencode-ai/plugin/tool';
|
|
14
14
|
import { fanOut, promptSession, notifyParent, evaluatePlanningGate, evaluateReviewGate, evaluateDocGate, PLANNING_MODELS, REVIEW_MODELS, ORCHESTRATOR_MODEL, BUILD_MODEL, DOC_MODEL, VALIDATE_MODEL, DOC_REVIEW_MODEL, parseModelString, parseNamedModels, } from '../workflow/orchestrator.js';
|
|
15
|
-
import { planningPrompt, synthesisPrompt, breakdownPrompt, validateBatchPrompt, implementBatchPrompt, triagePrompt, reviewPrompt, reviewSynthesisPrompt, documentPrompt, docReviewPrompt, } from './prompts.js';
|
|
15
|
+
import { planningPrompt, synthesisPrompt, breakdownPrompt, validateBatchPrompt, implementBatchPrompt, triagePrompt, reviewPrompt, reviewSynthesisPrompt, documentPrompt, docReviewPrompt, ciFixPrompt, } from './prompts.js';
|
|
16
|
+
import { ciScriptExists, runCI } from '../workflow/ci-runner.js';
|
|
16
17
|
import { parseConsensusPlan, parseReviewSynthesis, parseImplementationReport, parseDocReview, } from './parsers.js';
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Tool factory — needs the SDK client from plugin init
|
|
@@ -99,7 +100,7 @@ export function createPipelineTool(client) {
|
|
|
99
100
|
report.push(`## ${phase}\n${msg}`);
|
|
100
101
|
};
|
|
101
102
|
// Notify helper bound to this session
|
|
102
|
-
const notify = (message) => notifyParent(client, sessionId, agent, message);
|
|
103
|
+
const notify = (message, options) => notifyParent(client, sessionId, agent, message, options);
|
|
103
104
|
// ===================================================================
|
|
104
105
|
// PHASE 1: PLANNING (fan-out → synthesize → gate, loop up to 5)
|
|
105
106
|
// ===================================================================
|
|
@@ -215,7 +216,7 @@ export function createPipelineTool(client) {
|
|
|
215
216
|
},
|
|
216
217
|
});
|
|
217
218
|
addReport('Complete', `Pipeline finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
218
|
-
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n
|
|
219
|
+
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
219
220
|
return;
|
|
220
221
|
}
|
|
221
222
|
// REWORK continues the loop
|
|
@@ -236,7 +237,7 @@ export function createPipelineTool(client) {
|
|
|
236
237
|
},
|
|
237
238
|
});
|
|
238
239
|
addReport('Complete', `Pipeline finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
239
|
-
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n
|
|
240
|
+
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
240
241
|
return;
|
|
241
242
|
}
|
|
242
243
|
// ===================================================================
|
|
@@ -295,6 +296,71 @@ export function createPipelineTool(client) {
|
|
|
295
296
|
});
|
|
296
297
|
const implementEndMs = Date.now();
|
|
297
298
|
const implementation = parseImplementationReport(implRaw);
|
|
299
|
+
// ---------------------------------------------------------------
|
|
300
|
+
// CI validation (deterministic subprocess — not an LLM prompt)
|
|
301
|
+
// ---------------------------------------------------------------
|
|
302
|
+
const ciDir = process.cwd();
|
|
303
|
+
const hasCiScript = await ciScriptExists(ciDir);
|
|
304
|
+
if (!hasCiScript) {
|
|
305
|
+
await notify(`# Pipeline: CI script (ff-ci.sh) not found, skipping CI validation\n`);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
const maxCiAttempts = 3;
|
|
309
|
+
for (let ciAttempt = 1; ciAttempt <= maxCiAttempts; ciAttempt++) {
|
|
310
|
+
context.metadata({
|
|
311
|
+
title: `⏳ Running CI (attempt ${ciAttempt}/${maxCiAttempts})...`,
|
|
312
|
+
metadata: {
|
|
313
|
+
phase: 'building',
|
|
314
|
+
step: 'ci',
|
|
315
|
+
ciAttempt,
|
|
316
|
+
maxCiAttempts,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
await notify(`# Pipeline: CI validation — attempt ${ciAttempt}/${maxCiAttempts}\n`);
|
|
320
|
+
const ciResult = await runCI(ciDir);
|
|
321
|
+
if (ciResult.passed) {
|
|
322
|
+
await notify(`# Pipeline: CI passed (attempt ${ciAttempt}/${maxCiAttempts})\n`);
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
// CI failed
|
|
326
|
+
await notify(`# Pipeline: CI failed (attempt ${ciAttempt}/${maxCiAttempts})\n\n\`\`\`\n${ciResult.output}\n\`\`\`\n`);
|
|
327
|
+
if (ciAttempt < maxCiAttempts) {
|
|
328
|
+
// Ask build model to fix the CI failures
|
|
329
|
+
context.metadata({
|
|
330
|
+
title: `⏳ CI rework (attempt ${ciAttempt}/${maxCiAttempts})...`,
|
|
331
|
+
metadata: {
|
|
332
|
+
phase: 'building',
|
|
333
|
+
step: 'ci',
|
|
334
|
+
ciAttempt,
|
|
335
|
+
maxCiAttempts,
|
|
336
|
+
action: 'rework',
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
await promptSession(client, sessionId, ciFixPrompt(requirements, ciResult.output), {
|
|
340
|
+
model: buildModel,
|
|
341
|
+
agent: 'building',
|
|
342
|
+
title: `ff-pipeline-ci-fix-${ciAttempt}`,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
// Terminal CI failure — exhausted all attempts
|
|
347
|
+
addReport('CI Validation', `CI failed after ${maxCiAttempts} attempts.\n\n**Last output**:\n\`\`\`\n${ciResult.output}\n\`\`\`\n`);
|
|
348
|
+
context.metadata({
|
|
349
|
+
title: '❌ CI validation failed (attempts exhausted)',
|
|
350
|
+
metadata: {
|
|
351
|
+
phase: 'building',
|
|
352
|
+
step: 'ci',
|
|
353
|
+
ciAttempt,
|
|
354
|
+
maxCiAttempts,
|
|
355
|
+
outcome: 'failed',
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
addReport('Complete', `Pipeline finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
359
|
+
await notify(`# Pipeline: CI validation failed\n\nCI checks failed after ${maxCiAttempts} attempts. Aborting.\n`, { noReply: false });
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
298
364
|
const buildingEndMs = Date.now();
|
|
299
365
|
addReport('Building', `### Execution\n` +
|
|
300
366
|
`- **Breakdown**: ${breakdownTitle} (${formatElapsed(breakdownStartMs, breakdownEndMs)})\n` +
|
|
@@ -459,7 +525,7 @@ export function createPipelineTool(client) {
|
|
|
459
525
|
},
|
|
460
526
|
});
|
|
461
527
|
addReport('Complete', `Pipeline finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
462
|
-
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n
|
|
528
|
+
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
463
529
|
return;
|
|
464
530
|
}
|
|
465
531
|
}
|
|
@@ -479,7 +545,7 @@ export function createPipelineTool(client) {
|
|
|
479
545
|
},
|
|
480
546
|
});
|
|
481
547
|
addReport('Complete', `Pipeline finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
482
|
-
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n
|
|
548
|
+
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
483
549
|
return;
|
|
484
550
|
}
|
|
485
551
|
// ===================================================================
|
|
@@ -613,12 +679,12 @@ export function createPipelineTool(client) {
|
|
|
613
679
|
});
|
|
614
680
|
addReport('Complete', `${completedWithoutIssues ? 'Pipeline finished successfully.' : 'Pipeline finished with issues.'}\n**Total time**: ${formatElapsed(totalStartMs, totalEndMs)}`);
|
|
615
681
|
// Send final completion report as notification
|
|
616
|
-
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n
|
|
682
|
+
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
617
683
|
}; // end asyncOrchestration
|
|
618
684
|
// Launch orchestration in background — fire-and-forget
|
|
619
685
|
void asyncOrchestration().catch(async (err) => {
|
|
620
686
|
const message = err instanceof Error ? err.message : String(err);
|
|
621
|
-
await notifyParent(client, sessionId, agent, `# Pipeline: Error\n\nPhase: ${lastPhase}\nError: ${message}\n
|
|
687
|
+
await notifyParent(client, sessionId, agent, `# Pipeline: Error\n\nPhase: ${lastPhase}\nError: ${message}\n`, { noReply: false });
|
|
622
688
|
});
|
|
623
689
|
// Return immediately with acknowledgment
|
|
624
690
|
const summary = requirements.length > 120 ? requirements.slice(0, 120) + '...' : requirements;
|
package/dist/tools/prompts.d.ts
CHANGED
|
@@ -21,4 +21,5 @@ export declare function reviewSynthesisPrompt(reviews: Array<{
|
|
|
21
21
|
export declare function documentPrompt(input: string): string;
|
|
22
22
|
export declare function docReviewPrompt(docUpdate: string): string;
|
|
23
23
|
export declare function miniBuildPrompt(requirements: string, reworkFeedback?: string): string;
|
|
24
|
+
export declare function ciFixPrompt(requirements: string, ciOutput: string): string;
|
|
24
25
|
export declare function miniReviewPrompt(implementationReport: string): string;
|
package/dist/tools/prompts.js
CHANGED
|
@@ -169,6 +169,22 @@ Requirements:
|
|
|
169
169
|
3. Run lint/typecheck/tests only for impacted scope.
|
|
170
170
|
4. Return a concise implementation report with changed files, tests run, and known open issues.`;
|
|
171
171
|
}
|
|
172
|
+
export function ciFixPrompt(requirements, ciOutput) {
|
|
173
|
+
return `The CI checks (ff-ci.sh) failed after your implementation. Fix the issues identified below.
|
|
174
|
+
|
|
175
|
+
Original requirements:
|
|
176
|
+
${requirements}
|
|
177
|
+
|
|
178
|
+
CI failure output:
|
|
179
|
+
\`\`\`
|
|
180
|
+
${ciOutput}
|
|
181
|
+
\`\`\`
|
|
182
|
+
|
|
183
|
+
Requirements:
|
|
184
|
+
1. Fix all failing checks (lint, typecheck, build, tests).
|
|
185
|
+
2. Do not remove or skip tests — fix the underlying issues.
|
|
186
|
+
3. Return a concise implementation report with changed files and what was fixed.`;
|
|
187
|
+
}
|
|
172
188
|
export function miniReviewPrompt(implementationReport) {
|
|
173
189
|
return `Review the latest mini-loop implementation output below.
|
|
174
190
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI runner utility for workflow tools.
|
|
3
|
+
*
|
|
4
|
+
* Executes `ff-ci.sh` in a subprocess and returns a structured result.
|
|
5
|
+
* Reuses sanitizeOutput / truncateOutput from stop-quality-gate.ts for
|
|
6
|
+
* consistent secret redaction and output trimming.
|
|
7
|
+
*/
|
|
8
|
+
export interface CIResult {
|
|
9
|
+
passed: boolean;
|
|
10
|
+
output: string;
|
|
11
|
+
timedOut: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Check whether `ff-ci.sh` exists at the given directory root.
|
|
15
|
+
* Never throws — returns `false` on any error.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ciScriptExists(directory: string): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Execute `ff-ci.sh` from the given directory and return a structured result.
|
|
20
|
+
* Never throws — returns a failure result on any error.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runCI(directory: string): Promise<CIResult>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI runner utility for workflow tools.
|
|
3
|
+
*
|
|
4
|
+
* Executes `ff-ci.sh` in a subprocess and returns a structured result.
|
|
5
|
+
* Reuses sanitizeOutput / truncateOutput from stop-quality-gate.ts for
|
|
6
|
+
* consistent secret redaction and output trimming.
|
|
7
|
+
*/
|
|
8
|
+
import { sanitizeOutput, truncateOutput } from '../stop-quality-gate.js';
|
|
9
|
+
const CI_TIMEOUT_MS = 300_000; // 5 minutes
|
|
10
|
+
/**
|
|
11
|
+
* Check whether `ff-ci.sh` exists at the given directory root.
|
|
12
|
+
* Never throws — returns `false` on any error.
|
|
13
|
+
*/
|
|
14
|
+
export async function ciScriptExists(directory) {
|
|
15
|
+
try {
|
|
16
|
+
const ciPath = `${directory}/ff-ci.sh`;
|
|
17
|
+
// eslint-disable-next-line no-undef
|
|
18
|
+
const proc = Bun.spawn(['test', '-f', ciPath], {
|
|
19
|
+
cwd: directory,
|
|
20
|
+
stdout: 'pipe',
|
|
21
|
+
stderr: 'pipe',
|
|
22
|
+
});
|
|
23
|
+
await proc.exited;
|
|
24
|
+
return proc.exitCode === 0;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Execute `ff-ci.sh` from the given directory and return a structured result.
|
|
32
|
+
* Never throws — returns a failure result on any error.
|
|
33
|
+
*/
|
|
34
|
+
export async function runCI(directory) {
|
|
35
|
+
try {
|
|
36
|
+
const ciPath = `${directory}/ff-ci.sh`;
|
|
37
|
+
// eslint-disable-next-line no-undef
|
|
38
|
+
const proc = Bun.spawn(['bash', ciPath], {
|
|
39
|
+
cwd: directory,
|
|
40
|
+
stdout: 'pipe',
|
|
41
|
+
stderr: 'pipe',
|
|
42
|
+
});
|
|
43
|
+
let timedOut = false;
|
|
44
|
+
let timeoutId = null;
|
|
45
|
+
let forceKillTimeoutId = null;
|
|
46
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
47
|
+
timeoutId = setTimeout(() => {
|
|
48
|
+
timedOut = true;
|
|
49
|
+
proc.kill('SIGTERM');
|
|
50
|
+
forceKillTimeoutId = setTimeout(() => {
|
|
51
|
+
try {
|
|
52
|
+
proc.kill('SIGKILL');
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Process already terminated
|
|
56
|
+
}
|
|
57
|
+
}, 5000);
|
|
58
|
+
resolve();
|
|
59
|
+
}, CI_TIMEOUT_MS);
|
|
60
|
+
});
|
|
61
|
+
await Promise.race([proc.exited, timeoutPromise]);
|
|
62
|
+
if (timeoutId)
|
|
63
|
+
clearTimeout(timeoutId);
|
|
64
|
+
if (forceKillTimeoutId)
|
|
65
|
+
clearTimeout(forceKillTimeoutId);
|
|
66
|
+
const stdout = await new Response(proc.stdout).text();
|
|
67
|
+
const stderr = await new Response(proc.stderr).text();
|
|
68
|
+
let output;
|
|
69
|
+
let passed;
|
|
70
|
+
if (timedOut) {
|
|
71
|
+
output =
|
|
72
|
+
`CI execution timed out after ${CI_TIMEOUT_MS / 1000} seconds\n\n${stdout}\n${stderr}`.trim();
|
|
73
|
+
passed = false;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
output = stdout + (stderr ? `\n${stderr}` : '');
|
|
77
|
+
passed = proc.exitCode === 0;
|
|
78
|
+
}
|
|
79
|
+
// Sanitize secrets and truncate for prompt safety
|
|
80
|
+
output = truncateOutput(sanitizeOutput(output));
|
|
81
|
+
return { passed, output, timedOut };
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
85
|
+
return {
|
|
86
|
+
passed: false,
|
|
87
|
+
output: `CI runner error: ${message}`,
|
|
88
|
+
timedOut: false,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -125,8 +125,12 @@ export declare function promptSession(client: Client, parentSessionId: string, p
|
|
|
125
125
|
/**
|
|
126
126
|
* Send a visible notification message to the parent session via `promptAsync`.
|
|
127
127
|
*
|
|
128
|
-
*
|
|
129
|
-
* without triggering an LLM turn.
|
|
130
|
-
*
|
|
128
|
+
* By default uses `noReply: true` so the message appears in the TUI as a chat
|
|
129
|
+
* message without triggering an LLM turn. Pass `noReply: false` for terminal
|
|
130
|
+
* messages (completion reports, errors) so the LLM can validate the outcome.
|
|
131
|
+
*
|
|
132
|
+
* Errors are swallowed — notification delivery must never break the pipeline.
|
|
131
133
|
*/
|
|
132
|
-
export declare function notifyParent(client: Client, sessionId: string, agent: string | undefined, message: string
|
|
134
|
+
export declare function notifyParent(client: Client, sessionId: string, agent: string | undefined, message: string, options?: {
|
|
135
|
+
noReply?: boolean;
|
|
136
|
+
}): Promise<void>;
|
package/dist/workflow/fan-out.js
CHANGED
|
@@ -94,14 +94,16 @@ export async function promptSession(client, parentSessionId, prompt, options) {
|
|
|
94
94
|
/**
|
|
95
95
|
* Send a visible notification message to the parent session via `promptAsync`.
|
|
96
96
|
*
|
|
97
|
-
*
|
|
98
|
-
* without triggering an LLM turn.
|
|
99
|
-
*
|
|
97
|
+
* By default uses `noReply: true` so the message appears in the TUI as a chat
|
|
98
|
+
* message without triggering an LLM turn. Pass `noReply: false` for terminal
|
|
99
|
+
* messages (completion reports, errors) so the LLM can validate the outcome.
|
|
100
|
+
*
|
|
101
|
+
* Errors are swallowed — notification delivery must never break the pipeline.
|
|
100
102
|
*/
|
|
101
|
-
export async function notifyParent(client, sessionId, agent, message) {
|
|
103
|
+
export async function notifyParent(client, sessionId, agent, message, options) {
|
|
102
104
|
try {
|
|
103
105
|
const body = {
|
|
104
|
-
noReply: true,
|
|
106
|
+
noReply: options?.noReply ?? true,
|
|
105
107
|
parts: [{ type: 'text', text: message }],
|
|
106
108
|
};
|
|
107
109
|
if (agent) {
|
|
@@ -6,4 +6,5 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { fanOut, promptSession, extractText, notifyParent, type Client } from './fan-out.js';
|
|
8
8
|
export { evaluatePlanningGate, evaluateReviewGate, evaluateDocGate, evaluateMiniLoopImplGate, evaluateMiniLoopDocGate, } from './gate-evaluator.js';
|
|
9
|
+
export { runCI, ciScriptExists } from './ci-runner.js';
|
|
9
10
|
export * from './types.js';
|
|
@@ -6,4 +6,5 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { fanOut, promptSession, extractText, notifyParent } from './fan-out.js';
|
|
8
8
|
export { evaluatePlanningGate, evaluateReviewGate, evaluateDocGate, evaluateMiniLoopImplGate, evaluateMiniLoopDocGate, } from './gate-evaluator.js';
|
|
9
|
+
export { runCI, ciScriptExists } from './ci-runner.js';
|
|
9
10
|
export * from './types.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@syntesseraai/opencode-feature-factory",
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "OpenCode plugin for Feature Factory agents - provides sub-agents and skills for validation, review, security, and architecture assessment",
|
|
7
7
|
"license": "MIT",
|