@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 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
 
@@ -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;
@@ -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;
@@ -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;
@@ -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
- * Uses `noReply: true` so the message appears in the TUI as a chat message
129
- * without triggering an LLM turn. Errors are swallowed notification
130
- * delivery must never break the pipeline.
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): Promise<void>;
134
+ export declare function notifyParent(client: Client, sessionId: string, agent: string | undefined, message: string, options?: {
135
+ noReply?: boolean;
136
+ }): Promise<void>;
@@ -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
- * Uses `noReply: true` so the message appears in the TUI as a chat message
98
- * without triggering an LLM turn. Errors are swallowed notification
99
- * delivery must never break the pipeline.
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.1",
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",