@syntesseraai/opencode-feature-factory 0.10.0 → 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 +23 -22
- package/agents/building.md +1 -1
- package/agents/documenting.md +1 -1
- package/agents/planning.md +1 -1
- package/agents/reviewing.md +1 -1
- package/dist/tools/mini-loop.js +86 -8
- package/dist/tools/pipeline.js +98 -15
- 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,36 +118,37 @@ 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.
|
|
122
|
-
- **Error notifications**: If the background orchestration throws, a
|
|
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
|
+
- **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
|
|
|
125
125
|
### Notification Format
|
|
126
126
|
|
|
127
|
-
Pipeline
|
|
127
|
+
Pipeline notifications use plain-text markdown headers with phase START/END bracketing and per-iteration gate details:
|
|
128
128
|
|
|
129
129
|
```
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
# Pipeline: Reviewing — Iteration 2/10
|
|
131
|
+
|
|
132
132
|
Status: APPROVED
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
Confidence: 97%
|
|
134
|
+
Unresolved Issues: 0
|
|
135
|
+
Duration: 45.3s
|
|
136
|
+
Feedback: N/A
|
|
136
137
|
```
|
|
137
138
|
|
|
138
|
-
Mini-loop
|
|
139
|
+
Mini-loop notifications follow the same pattern:
|
|
139
140
|
|
|
140
141
|
```
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Status:
|
|
144
|
-
Confidence:
|
|
145
|
-
|
|
146
|
-
Duration:
|
|
147
|
-
|
|
142
|
+
# Mini-Loop: Building — Iteration 1/10
|
|
143
|
+
|
|
144
|
+
Status: REWORK
|
|
145
|
+
Confidence: 82%
|
|
146
|
+
Unresolved Issues: 2
|
|
147
|
+
Duration: 23.1s
|
|
148
|
+
Feedback: Fix type errors in handler.ts
|
|
148
149
|
```
|
|
149
150
|
|
|
150
|
-
Final reports
|
|
151
|
+
Final reports use `# Pipeline: Complete` or `# Mini-Loop: Complete` headers containing the full markdown report. Errors use `# Pipeline: Error` or `# Mini-Loop: Error`.
|
|
151
152
|
|
|
152
153
|
## Related Docs
|
|
153
154
|
|
package/agents/building.md
CHANGED
package/agents/documenting.md
CHANGED
package/agents/planning.md
CHANGED
package/agents/reviewing.md
CHANGED
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
|
// ===================================================================
|
|
@@ -79,6 +80,8 @@ export function createMiniLoopTool(client) {
|
|
|
79
80
|
const implementationIterationDetails = [];
|
|
80
81
|
let implGate = { decision: 'REWORK', feedback: requirements };
|
|
81
82
|
let lastImplRaw = '';
|
|
83
|
+
// Phase start notification
|
|
84
|
+
await notify(`# Mini-Loop: Building started\n\nStarting implementation phase...\n`);
|
|
82
85
|
for (let implIter = 0; implIter < 10 && implGate.decision === 'REWORK'; implIter++) {
|
|
83
86
|
const iteration = implIter + 1;
|
|
84
87
|
const buildTitle = `ff-mini-build-${iteration}`;
|
|
@@ -99,6 +102,73 @@ export function createMiniLoopTool(client) {
|
|
|
99
102
|
const buildStartMs = Date.now();
|
|
100
103
|
lastImplRaw = await promptSession(client, sessionId, miniBuildPrompt(buildInput, implIter > 0 ? implGate.feedback : undefined), { model: buildModel, agent: 'building', title: buildTitle });
|
|
101
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
|
+
}
|
|
102
172
|
context.metadata({
|
|
103
173
|
title: `⏳ Reviewing (iteration ${iteration}/10)...`,
|
|
104
174
|
metadata: {
|
|
@@ -165,10 +235,12 @@ export function createMiniLoopTool(client) {
|
|
|
165
235
|
`- **Gate**: ${implGate.decision} (confidence: ${review.confidence}, change requested: ${review.changeRequested ? 'yes' : 'no'}, unresolved: ${review.unresolvedIssues})\n` +
|
|
166
236
|
`- **Feedback**: ${feedback}`);
|
|
167
237
|
// Notify each implementation iteration gate decision
|
|
168
|
-
await notify(
|
|
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`);
|
|
169
239
|
if (implGate.decision === 'ESCALATE') {
|
|
170
240
|
const implementationEndMs = Date.now();
|
|
171
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`);
|
|
172
244
|
context.metadata({
|
|
173
245
|
title: '⚠️ Mini-loop finished with issues',
|
|
174
246
|
metadata: {
|
|
@@ -178,7 +250,7 @@ export function createMiniLoopTool(client) {
|
|
|
178
250
|
},
|
|
179
251
|
});
|
|
180
252
|
addReport('Complete', `Mini-loop finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
181
|
-
await notify(
|
|
253
|
+
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
182
254
|
return;
|
|
183
255
|
}
|
|
184
256
|
// REWORK continues the loop
|
|
@@ -187,6 +259,8 @@ export function createMiniLoopTool(client) {
|
|
|
187
259
|
addReport('Implementation', `${implementationIterationDetails.join('\n\n')}\n\n**Outcome**: ${implGate.decision === 'APPROVED'
|
|
188
260
|
? 'APPROVED'
|
|
189
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`);
|
|
190
264
|
if (implGate.decision !== 'APPROVED') {
|
|
191
265
|
context.metadata({
|
|
192
266
|
title: '⚠️ Mini-loop finished with issues',
|
|
@@ -197,7 +271,7 @@ export function createMiniLoopTool(client) {
|
|
|
197
271
|
},
|
|
198
272
|
});
|
|
199
273
|
addReport('Complete', `Mini-loop finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
200
|
-
await notify(
|
|
274
|
+
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
201
275
|
return;
|
|
202
276
|
}
|
|
203
277
|
// ===================================================================
|
|
@@ -208,6 +282,8 @@ export function createMiniLoopTool(client) {
|
|
|
208
282
|
const documentationIterationDetails = [];
|
|
209
283
|
let docInput = lastImplRaw;
|
|
210
284
|
let docGate = { decision: 'REWORK' };
|
|
285
|
+
// Phase start notification
|
|
286
|
+
await notify(`# Mini-Loop: Documentation started\n\nStarting documentation phase...\n`);
|
|
211
287
|
for (let docIter = 0; docIter < 5 && docGate.decision === 'REWORK'; docIter++) {
|
|
212
288
|
const iteration = docIter + 1;
|
|
213
289
|
const writeTitle = `ff-mini-doc-write-${iteration}`;
|
|
@@ -295,7 +371,7 @@ export function createMiniLoopTool(client) {
|
|
|
295
371
|
`- **Gate**: ${docGate.decision} (confidence: ${docReview.confidence}, unresolved: ${docReview.unresolvedIssues})\n` +
|
|
296
372
|
`- **Feedback**: ${feedback}`);
|
|
297
373
|
// Notify each documentation iteration gate decision
|
|
298
|
-
await notify(
|
|
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`);
|
|
299
375
|
if (docGate.decision === 'REWORK') {
|
|
300
376
|
docInput = `${docInput}\n\nDocumentation review feedback:\n${docGate.feedback}`;
|
|
301
377
|
}
|
|
@@ -306,6 +382,8 @@ export function createMiniLoopTool(client) {
|
|
|
306
382
|
: docGate.decision === 'ESCALATE'
|
|
307
383
|
? `ESCALATE: ${docGate.reason}`
|
|
308
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`);
|
|
309
387
|
const totalEndMs = Date.now();
|
|
310
388
|
const completedWithoutIssues = docGate.decision === 'APPROVED';
|
|
311
389
|
context.metadata({
|
|
@@ -320,12 +398,12 @@ export function createMiniLoopTool(client) {
|
|
|
320
398
|
});
|
|
321
399
|
addReport('Complete', `${completedWithoutIssues ? 'Mini-loop finished successfully.' : 'Mini-loop finished with issues.'}\n**Total time**: ${formatElapsed(totalStartMs, totalEndMs)}`);
|
|
322
400
|
// Send final completion report as notification
|
|
323
|
-
await notify(
|
|
401
|
+
await notify(`# Mini-Loop: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
324
402
|
}; // end asyncOrchestration
|
|
325
403
|
// Launch orchestration in background — fire-and-forget
|
|
326
404
|
void asyncOrchestration().catch(async (err) => {
|
|
327
405
|
const message = err instanceof Error ? err.message : String(err);
|
|
328
|
-
await notifyParent(client, sessionId, agent,
|
|
406
|
+
await notifyParent(client, sessionId, agent, `# Mini-Loop: Error\n\nPhase: ${lastPhase}\nError: ${message}\n`, { noReply: false });
|
|
329
407
|
});
|
|
330
408
|
// Return immediately with acknowledgment
|
|
331
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
|
// ===================================================================
|
|
@@ -108,6 +109,8 @@ export function createPipelineTool(client) {
|
|
|
108
109
|
const planningIterationDetails = [];
|
|
109
110
|
let planningGate = { decision: 'REWORK', feedback: requirements };
|
|
110
111
|
let finalPlan = '';
|
|
112
|
+
// Phase start notification
|
|
113
|
+
await notify(`# Pipeline: Planning started\n\nStarting planning phase with ${planModels.length} models...\n`);
|
|
111
114
|
for (let planIter = 0; planIter < 5 && planningGate.decision === 'REWORK'; planIter++) {
|
|
112
115
|
const iteration = planIter + 1;
|
|
113
116
|
const synthesisTitle = `ff-plan-synthesis-${iteration}`;
|
|
@@ -197,9 +200,13 @@ export function createPipelineTool(client) {
|
|
|
197
200
|
`- **Synthesis**: ${synthesisTitle} (${formatElapsed(synthesisStartMs, synthesisEndMs)})\n` +
|
|
198
201
|
`- **Gate**: ${planningGate.decision} (score: ${consensus.consensusScore})\n` +
|
|
199
202
|
`- **Feedback**: ${feedback}`);
|
|
203
|
+
// Notify each planning iteration gate decision
|
|
204
|
+
await notify(`# Pipeline: Planning — Iteration ${iteration}/5\n\nStatus: ${planningGate.decision}\nConsensus Score: ${consensus.consensusScore}%\nDuration: ${formatElapsed(planningStartMs, Date.now())}\nFeedback: ${feedback}\n`);
|
|
200
205
|
if (planningGate.decision === 'BLOCKED') {
|
|
201
206
|
const planningEndMs = Date.now();
|
|
202
207
|
addReport('Planning', `${planningIterationDetails.join('\n\n')}\n\n**Outcome**: BLOCKED: ${planningGate.reason}\n**Phase time**: ${formatElapsed(planningStartMs, planningEndMs)}`);
|
|
208
|
+
// Phase end notification
|
|
209
|
+
await notify(`# Pipeline: Planning ended\n\nOutcome: BLOCKED\nReason: ${planningGate.reason}\nConsensus Score: ${consensus.consensusScore}%\nIterations: ${iteration}/5\nPhase time: ${formatElapsed(planningStartMs, planningEndMs)}\n`);
|
|
203
210
|
context.metadata({
|
|
204
211
|
title: '⚠️ Pipeline finished with issues',
|
|
205
212
|
metadata: {
|
|
@@ -209,8 +216,7 @@ export function createPipelineTool(client) {
|
|
|
209
216
|
},
|
|
210
217
|
});
|
|
211
218
|
addReport('Complete', `Pipeline finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
212
|
-
await notify(
|
|
213
|
-
await notify(`<ff_pipeline_complete>\n${report.join('\n\n')}\n</ff_pipeline_complete>`);
|
|
219
|
+
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
214
220
|
return;
|
|
215
221
|
}
|
|
216
222
|
// REWORK continues the loop
|
|
@@ -219,8 +225,8 @@ export function createPipelineTool(client) {
|
|
|
219
225
|
addReport('Planning', `${planningIterationDetails.join('\n\n')}\n\n**Outcome**: ${planningGate.decision === 'APPROVED'
|
|
220
226
|
? 'APPROVED'
|
|
221
227
|
: `REWORK exhausted (5 iterations). Last feedback: ${planningGate.feedback}`}\n**Phase time**: ${formatElapsed(planningStartMs, planningEndMs)}`);
|
|
222
|
-
//
|
|
223
|
-
await notify(
|
|
228
|
+
// Phase end notification
|
|
229
|
+
await notify(`# Pipeline: Planning ended\n\nOutcome: ${planningGate.decision}\nDuration: ${formatElapsed(planningStartMs, planningEndMs)}\nNext Phase: ${planningGate.decision === 'APPROVED' ? 'Building' : 'Finished'}\n`);
|
|
224
230
|
if (planningGate.decision !== 'APPROVED') {
|
|
225
231
|
context.metadata({
|
|
226
232
|
title: '⚠️ Pipeline finished with issues',
|
|
@@ -231,7 +237,7 @@ export function createPipelineTool(client) {
|
|
|
231
237
|
},
|
|
232
238
|
});
|
|
233
239
|
addReport('Complete', `Pipeline finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
234
|
-
await notify(
|
|
240
|
+
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
235
241
|
return;
|
|
236
242
|
}
|
|
237
243
|
// ===================================================================
|
|
@@ -239,6 +245,8 @@ export function createPipelineTool(client) {
|
|
|
239
245
|
// ===================================================================
|
|
240
246
|
lastPhase = 'Building';
|
|
241
247
|
const buildingStartMs = Date.now();
|
|
248
|
+
// Phase start notification
|
|
249
|
+
await notify(`# Pipeline: Building started\n\nStarting build phase (breakdown → validate → implement)...\n`);
|
|
242
250
|
context.metadata({
|
|
243
251
|
title: '⏳ Breaking down tasks...',
|
|
244
252
|
metadata: {
|
|
@@ -288,6 +296,71 @@ export function createPipelineTool(client) {
|
|
|
288
296
|
});
|
|
289
297
|
const implementEndMs = Date.now();
|
|
290
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
|
+
}
|
|
291
364
|
const buildingEndMs = Date.now();
|
|
292
365
|
addReport('Building', `### Execution\n` +
|
|
293
366
|
`- **Breakdown**: ${breakdownTitle} (${formatElapsed(breakdownStartMs, breakdownEndMs)})\n` +
|
|
@@ -296,8 +369,8 @@ export function createPipelineTool(client) {
|
|
|
296
369
|
`- **Tests passed**: ${implementation.testsPassed}\n` +
|
|
297
370
|
`- **Open issues**: ${implementation.openIssues.length > 0 ? implementation.openIssues.join('; ') : 'none'}\n\n` +
|
|
298
371
|
`**Phase time**: ${formatElapsed(buildingStartMs, buildingEndMs)}`);
|
|
299
|
-
//
|
|
300
|
-
await notify(
|
|
372
|
+
// Phase end notification
|
|
373
|
+
await notify(`# Pipeline: Building ended\n\nOutcome: COMPLETE\nFiles Changed: ${implementation.filesChanged.length}\nTests Passed: ${implementation.testsPassed}\nOpen Issues: ${implementation.openIssues.length > 0 ? implementation.openIssues.join('; ') : 'none'}\nPhase time: ${formatElapsed(buildingStartMs, buildingEndMs)}\n`);
|
|
301
374
|
// ===================================================================
|
|
302
375
|
// PHASE 3: REVIEWING (triage → fan-out review → synthesize → gate, loop up to 10)
|
|
303
376
|
// ===================================================================
|
|
@@ -306,6 +379,8 @@ export function createPipelineTool(client) {
|
|
|
306
379
|
const reviewIterationDetails = [];
|
|
307
380
|
let reviewInput = implRaw;
|
|
308
381
|
let reviewGate = { decision: 'REWORK' };
|
|
382
|
+
// Phase start notification
|
|
383
|
+
await notify(`# Pipeline: Reviewing started\n\nStarting review phase with ${revModels.length} models...\n`);
|
|
309
384
|
for (let revIter = 0; revIter < 10 && reviewGate.decision === 'REWORK'; revIter++) {
|
|
310
385
|
const iteration = revIter + 1;
|
|
311
386
|
const triageTitle = `ff-review-triage-${iteration}`;
|
|
@@ -435,10 +510,12 @@ export function createPipelineTool(client) {
|
|
|
435
510
|
`${reworkLine}\n` +
|
|
436
511
|
`- **Feedback**: ${feedback}`);
|
|
437
512
|
// Notify each review iteration gate decision
|
|
438
|
-
await notify(
|
|
513
|
+
await notify(`# Pipeline: Reviewing — Iteration ${iteration}/10\n\nStatus: ${reviewGate.decision}\nConfidence: ${synthesis.overallConfidence}%\nUnresolved Issues: ${synthesis.unresolvedIssues}\nDuration: ${formatElapsed(reviewStartMs, Date.now())}\nFeedback: ${feedback}\n`);
|
|
439
514
|
if (reviewGate.decision === 'ESCALATE') {
|
|
440
515
|
const reviewEndMs = Date.now();
|
|
441
516
|
addReport('Reviewing', `${reviewIterationDetails.join('\n\n')}\n\n**Outcome**: ESCALATE: ${reviewGate.reason}\n**Phase time**: ${formatElapsed(reviewStartMs, reviewEndMs)}`);
|
|
517
|
+
// Phase end notification
|
|
518
|
+
await notify(`# Pipeline: Reviewing ended\n\nOutcome: ESCALATE\nReason: ${reviewGate.reason}\nIterations: ${iteration}\nPhase time: ${formatElapsed(reviewStartMs, reviewEndMs)}\n`);
|
|
442
519
|
context.metadata({
|
|
443
520
|
title: '⚠️ Pipeline finished with issues',
|
|
444
521
|
metadata: {
|
|
@@ -448,7 +525,7 @@ export function createPipelineTool(client) {
|
|
|
448
525
|
},
|
|
449
526
|
});
|
|
450
527
|
addReport('Complete', `Pipeline finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
451
|
-
await notify(
|
|
528
|
+
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
452
529
|
return;
|
|
453
530
|
}
|
|
454
531
|
}
|
|
@@ -456,6 +533,8 @@ export function createPipelineTool(client) {
|
|
|
456
533
|
addReport('Reviewing', `${reviewIterationDetails.join('\n\n')}\n\n**Outcome**: ${reviewGate.decision === 'APPROVED'
|
|
457
534
|
? 'APPROVED'
|
|
458
535
|
: `REWORK exhausted (10 iterations). Last feedback: ${reviewGate.feedback}`}\n**Phase time**: ${formatElapsed(reviewStartMs, reviewEndMs)}`);
|
|
536
|
+
// Phase end notification
|
|
537
|
+
await notify(`# Pipeline: Reviewing ended\n\nOutcome: ${reviewGate.decision}\nDuration: ${formatElapsed(reviewStartMs, reviewEndMs)}\n`);
|
|
459
538
|
if (reviewGate.decision !== 'APPROVED') {
|
|
460
539
|
context.metadata({
|
|
461
540
|
title: '⚠️ Pipeline finished with issues',
|
|
@@ -466,7 +545,7 @@ export function createPipelineTool(client) {
|
|
|
466
545
|
},
|
|
467
546
|
});
|
|
468
547
|
addReport('Complete', `Pipeline finished with issues.\n**Total time**: ${formatElapsed(totalStartMs, Date.now())}`);
|
|
469
|
-
await notify(
|
|
548
|
+
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
470
549
|
return;
|
|
471
550
|
}
|
|
472
551
|
// ===================================================================
|
|
@@ -477,6 +556,8 @@ export function createPipelineTool(client) {
|
|
|
477
556
|
const documentationIterationDetails = [];
|
|
478
557
|
let docInput = `Implementation report:\n${implRaw}\n\nReview synthesis:\n${reviewInput}`;
|
|
479
558
|
let docGate = { decision: 'REWORK' };
|
|
559
|
+
// Phase start notification
|
|
560
|
+
await notify(`# Pipeline: Documentation started\n\nStarting documentation phase...\n`);
|
|
480
561
|
for (let docIter = 0; docIter < 5 && docGate.decision === 'REWORK'; docIter++) {
|
|
481
562
|
const iteration = docIter + 1;
|
|
482
563
|
const writeTitle = `ff-doc-write-${iteration}`;
|
|
@@ -567,7 +648,7 @@ export function createPipelineTool(client) {
|
|
|
567
648
|
`- **Gate**: ${docGate.decision} (confidence: ${docReview.confidence}, unresolved: ${docReview.unresolvedIssues})\n` +
|
|
568
649
|
`- **Feedback**: ${feedback}`);
|
|
569
650
|
// Notify each documentation iteration gate decision
|
|
570
|
-
await notify(
|
|
651
|
+
await notify(`# Pipeline: Documentation — Iteration ${iteration}/5\n\nStatus: ${docGate.decision}\nConfidence: ${docReview.confidence}%\nUnresolved Issues: ${docReview.unresolvedIssues}\nDuration: ${formatElapsed(documentationStartMs, Date.now())}\nFeedback: ${feedback}\n`);
|
|
571
652
|
if (docGate.decision === 'REWORK') {
|
|
572
653
|
// Feed feedback into the next iteration
|
|
573
654
|
docInput = `${docInput}\n\nDocumentation review feedback:\n${docGate.feedback}`;
|
|
@@ -579,6 +660,8 @@ export function createPipelineTool(client) {
|
|
|
579
660
|
: docGate.decision === 'ESCALATE'
|
|
580
661
|
? `ESCALATE: ${docGate.reason}`
|
|
581
662
|
: `REWORK exhausted (5 iterations). Last feedback: ${docGate.feedback}`}\n**Phase time**: ${formatElapsed(documentationStartMs, documentationEndMs)}`);
|
|
663
|
+
// Phase end notification
|
|
664
|
+
await notify(`# Pipeline: Documentation ended\n\nOutcome: ${docGate.decision}\nPhase time: ${formatElapsed(documentationStartMs, documentationEndMs)}\n`);
|
|
582
665
|
// ===================================================================
|
|
583
666
|
// FINAL REPORT
|
|
584
667
|
// ===================================================================
|
|
@@ -596,12 +679,12 @@ export function createPipelineTool(client) {
|
|
|
596
679
|
});
|
|
597
680
|
addReport('Complete', `${completedWithoutIssues ? 'Pipeline finished successfully.' : 'Pipeline finished with issues.'}\n**Total time**: ${formatElapsed(totalStartMs, totalEndMs)}`);
|
|
598
681
|
// Send final completion report as notification
|
|
599
|
-
await notify(
|
|
682
|
+
await notify(`# Pipeline: Complete\n\n${report.join('\n\n')}\n`, { noReply: false });
|
|
600
683
|
}; // end asyncOrchestration
|
|
601
684
|
// Launch orchestration in background — fire-and-forget
|
|
602
685
|
void asyncOrchestration().catch(async (err) => {
|
|
603
686
|
const message = err instanceof Error ? err.message : String(err);
|
|
604
|
-
await notifyParent(client, sessionId, agent,
|
|
687
|
+
await notifyParent(client, sessionId, agent, `# Pipeline: Error\n\nPhase: ${lastPhase}\nError: ${message}\n`, { noReply: false });
|
|
605
688
|
});
|
|
606
689
|
// Return immediately with acknowledgment
|
|
607
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",
|