@syntesseraai/opencode-feature-factory 0.10.6 → 0.10.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,15 +9,40 @@
9
9
  * to the parent session via `promptAsync(noReply: true)`.
10
10
  */
11
11
  import { tool } from '@opencode-ai/plugin/tool';
12
+ import { randomUUID } from 'node:crypto';
12
13
  import { promptSession, notifyParent, evaluateMiniLoopImplGate, evaluateMiniLoopDocGate, parseModelString, } from '../workflow/orchestrator.js';
13
- import { miniBuildPrompt, miniReviewPrompt, documentPrompt, docReviewPrompt, ciFixPrompt, } from './prompts.js';
14
- import { parseMiniReview, parseDocReview } from './parsers.js';
14
+ import { miniBuildPrompt, miniReviewPrompt, documentPrompt, docReviewPrompt, ciFixPrompt, runNamePrompt, actionFirstReworkPrompt, } from './prompts.js';
15
+ import { parseMiniReview, parseDocReview, parseImplementationReport, formatImplementationReportForHandoff } from './parsers.js';
15
16
  import { ciScriptExists, runCI } from '../workflow/ci-runner.js';
16
17
  import { cleanupWorktree, resolveRunDirectory } from '../workflow/run-isolation.js';
17
18
  // ---------------------------------------------------------------------------
18
19
  // Tool factory
19
20
  // ---------------------------------------------------------------------------
20
21
  const formatElapsed = (startMs, endMs) => `${((endMs - startMs) / 1000).toFixed(1)}s`;
22
+ const normalizeRunName = (raw, fallback) => {
23
+ const normalized = raw
24
+ .trim()
25
+ .replace(/^[-#*\s]+/, '')
26
+ .replace(/\s+/g, ' ')
27
+ .slice(0, 80);
28
+ return normalized.length > 0 ? normalized : fallback;
29
+ };
30
+ const asBulletList = (items, empty) => {
31
+ if (items.length === 0) {
32
+ return `- ${empty}`;
33
+ }
34
+ return items.map((item) => `- ${item}`).join('\n');
35
+ };
36
+ const formatStructuredStage = (stage, includeActionItems) => {
37
+ const base = `STATUS: ${stage.status}\n` +
38
+ `SUMMARY: ${stage.summary}\n` +
39
+ `CHECKLIST:\n${asBulletList(stage.checklist, 'No checklist provided.')}\n` +
40
+ `ISSUES:\n${asBulletList(stage.issues, 'No issues reported.')}`;
41
+ if (!includeActionItems) {
42
+ return base;
43
+ }
44
+ return `${base}\nACTION_ITEMS:\n${asBulletList(stage.actionItems, 'No action items required.')}`;
45
+ };
21
46
  export function createMiniLoopTool(client) {
22
47
  return tool({
23
48
  description: 'Run the Feature Factory mini-loop: build → review (with rework loop) → documentation. ' +
@@ -56,6 +81,8 @@ export function createMiniLoopTool(client) {
56
81
  const callerSessionId = context.sessionID;
57
82
  const agent = context.agent;
58
83
  const { requirements } = args;
84
+ const runId = randomUUID();
85
+ let runName = normalizeRunName(requirements.split('\n')[0] || '', 'Mini-Loop Workflow');
59
86
  // Resolve models — use provided overrides or undefined (inherit session model)
60
87
  const buildModel = args.build_model
61
88
  ? parseModelString(args.build_model)
@@ -79,6 +106,18 @@ export function createMiniLoopTool(client) {
79
106
  sessionId: callerSessionId,
80
107
  directory: runDirectoryContext.runDirectory,
81
108
  };
109
+ try {
110
+ const runNameRaw = await promptSession(client, runContext, runNamePrompt('Mini-Loop', requirements), {
111
+ model: buildModel,
112
+ agent: 'building',
113
+ title: 'ff-mini-run-name',
114
+ });
115
+ runName = normalizeRunName(runNameRaw, runName);
116
+ }
117
+ catch {
118
+ // Keep fallback name derived from requirements when name generation fails.
119
+ }
120
+ const runHeader = `Mini-Loop: ${runName} (${runId})`;
82
121
  // Fire-and-forget: run orchestration in background
83
122
  let lastPhase = 'init';
84
123
  const asyncOrchestration = async () => {
@@ -89,7 +128,7 @@ export function createMiniLoopTool(client) {
89
128
  report.push(`## ${phase}\n${msg}`);
90
129
  };
91
130
  // Notify helper bound to this session
92
- const notify = (message, options) => notifyParent(client, runContext, agent, message, options);
131
+ const notify = (message, options) => notifyParent(client, runContext, agent, `# ${runHeader}\n\n${message}`, options);
93
132
  // ===================================================================
94
133
  // PHASE 1: IMPLEMENTATION LOOP (build → review → gate, up to 10)
95
134
  // ===================================================================
@@ -98,6 +137,8 @@ export function createMiniLoopTool(client) {
98
137
  const implementationIterationDetails = [];
99
138
  let implGate = { decision: 'REWORK', feedback: requirements };
100
139
  let lastImplRaw = '';
140
+ let lastImplementationHandoff = '';
141
+ let nextImplementationDirective;
101
142
  // Phase start notification
102
143
  await notify(`# Mini-Loop: Building started\n\nStarting implementation phase...\n`);
103
144
  for (let implIter = 0; implIter < 10 && implGate.decision === 'REWORK'; implIter++) {
@@ -106,7 +147,7 @@ export function createMiniLoopTool(client) {
106
147
  const reviewTitle = `ff-mini-review-${iteration}`;
107
148
  const buildInput = implIter === 0
108
149
  ? requirements
109
- : `${requirements}\n\nPrevious review feedback:\n${implGate.feedback}`;
150
+ : `${requirements}\n\nPrevious implementation handoff for context:\n${lastImplementationHandoff || lastImplRaw}`;
110
151
  context.metadata({
111
152
  title: `⏳ Building (iteration ${iteration}/10)...`,
112
153
  metadata: {
@@ -118,7 +159,7 @@ export function createMiniLoopTool(client) {
118
159
  });
119
160
  // Build
120
161
  const buildStartMs = Date.now();
121
- lastImplRaw = await promptSession(client, runContext, miniBuildPrompt(buildInput, implIter > 0 ? implGate.feedback : undefined), { model: buildModel, agent: 'building', title: buildTitle });
162
+ lastImplRaw = await promptSession(client, runContext, miniBuildPrompt(buildInput, nextImplementationDirective), { model: buildModel, agent: 'building', title: buildTitle });
122
163
  const buildEndMs = Date.now();
123
164
  // ---------------------------------------------------------------
124
165
  // CI validation (deterministic subprocess — not an LLM prompt)
@@ -141,14 +182,32 @@ export function createMiniLoopTool(client) {
141
182
  maxCiAttempts,
142
183
  },
143
184
  });
144
- await notify(`# Mini-Loop: CI validation — attempt ${ciAttempt}/${maxCiAttempts}\n`);
145
185
  const ciResult = await runCI(ciDir);
146
186
  if (ciResult.passed) {
147
- await notify(`# Mini-Loop: CI passed (attempt ${ciAttempt}/${maxCiAttempts})\n`);
187
+ const ciStageOutput = {
188
+ status: 'APPROVED',
189
+ summary: `CI validation passed on attempt ${ciAttempt}/${maxCiAttempts}.`,
190
+ checklist: ['ff-ci.sh executed in run directory', 'No failing lint/typecheck/test/build checks'],
191
+ issues: [],
192
+ actionItems: [],
193
+ raw: 'CI passed',
194
+ };
195
+ await notify(`Stage: CI Validation — attempt ${ciAttempt}/${maxCiAttempts}\n` +
196
+ `${formatStructuredStage(ciStageOutput, false)}`);
148
197
  break;
149
198
  }
150
199
  // CI failed
151
- await notify(`# Mini-Loop: CI failed (attempt ${ciAttempt}/${maxCiAttempts})\n\n\`\`\`\n${ciResult.output}\n\`\`\`\n`);
200
+ const ciFailureStage = {
201
+ status: 'REWORK',
202
+ summary: `CI failed on attempt ${ciAttempt}/${maxCiAttempts}.`,
203
+ checklist: ['ff-ci.sh executed in run directory'],
204
+ issues: ['CI reported failing checks.'],
205
+ actionItems: ['Fix CI failures before continuing to the next attempt.'],
206
+ raw: ciResult.output,
207
+ };
208
+ await notify(`Stage: CI Validation — attempt ${ciAttempt}/${maxCiAttempts}\n` +
209
+ `${formatStructuredStage(ciFailureStage, true)}\n\n` +
210
+ `\`\`\`\n${ciResult.output}\n\`\`\`\n`);
152
211
  if (ciAttempt < maxCiAttempts) {
153
212
  // Ask build model to fix the CI failures
154
213
  context.metadata({
@@ -187,6 +246,20 @@ export function createMiniLoopTool(client) {
187
246
  }
188
247
  }
189
248
  }
249
+ const implementation = parseImplementationReport(lastImplRaw);
250
+ const implementationHandoff = formatImplementationReportForHandoff(implementation);
251
+ lastImplementationHandoff = implementationHandoff;
252
+ const buildStageOutput = {
253
+ ...implementation.stageOutput,
254
+ status: implementation.stageOutput.status ||
255
+ (implementation.testsPassed && implementation.openIssues.length === 0
256
+ ? 'APPROVED'
257
+ : 'REWORK_REQUIRED'),
258
+ };
259
+ await notify(`Stage: Build Output — iteration ${iteration}/10\n` +
260
+ `${formatStructuredStage(buildStageOutput, buildStageOutput.actionItems.length > 0)}\n` +
261
+ `Files Changed: ${implementation.filesChanged.length}\n` +
262
+ `Tests Passed: ${implementation.testsPassed}`);
190
263
  context.metadata({
191
264
  title: `⏳ Reviewing (iteration ${iteration}/10)...`,
192
265
  metadata: {
@@ -198,7 +271,7 @@ export function createMiniLoopTool(client) {
198
271
  });
199
272
  // Review
200
273
  const reviewStartMs = Date.now();
201
- const reviewRaw = await promptSession(client, runContext, miniReviewPrompt(lastImplRaw), {
274
+ const reviewRaw = await promptSession(client, runContext, miniReviewPrompt(implementationHandoff), {
202
275
  model: reviewModel,
203
276
  agent: 'reviewing',
204
277
  title: reviewTitle,
@@ -207,6 +280,15 @@ export function createMiniLoopTool(client) {
207
280
  const review = parseMiniReview(reviewRaw);
208
281
  // Gate (deterministic)
209
282
  implGate = evaluateMiniLoopImplGate(review, iteration);
283
+ if (implGate.decision === 'REWORK') {
284
+ nextImplementationDirective = actionFirstReworkPrompt({
285
+ ...review.stageOutput,
286
+ status: implGate.decision,
287
+ }, `${requirements}\n\nPrevious implementation handoff for context:\n${implementationHandoff}\n\nReview feedback:\n${implGate.feedback || review.reworkInstructions || review.raw}`, implGate.feedback || review.reworkInstructions || review.raw);
288
+ }
289
+ else {
290
+ nextImplementationDirective = undefined;
291
+ }
210
292
  if (implGate.decision === 'APPROVED') {
211
293
  context.metadata({
212
294
  title: `✅ Implementation APPROVED (confidence: ${review.confidence}, iteration: ${iteration})`,
@@ -246,14 +328,21 @@ export function createMiniLoopTool(client) {
246
328
  },
247
329
  });
248
330
  }
249
- const feedback = review.reworkInstructions || implGate.feedback || review.raw;
331
+ const implementationStageOutput = {
332
+ ...review.stageOutput,
333
+ status: implGate.decision,
334
+ };
250
335
  implementationIterationDetails.push(`### Iteration ${iteration}\n` +
251
336
  `- **Build**: ${buildTitle} (${formatElapsed(buildStartMs, buildEndMs)})\n` +
252
337
  `- **Review**: ${reviewTitle} (${formatElapsed(reviewStartMs, reviewEndMs)})\n` +
253
338
  `- **Gate**: ${implGate.decision} (confidence: ${review.confidence}, change requested: ${review.changeRequested ? 'yes' : 'no'}, unresolved: ${review.unresolvedIssues})\n` +
254
- `- **Feedback**: ${feedback}`);
339
+ `${formatStructuredStage(implementationStageOutput, implGate.decision === 'REWORK')}`);
255
340
  // Notify each implementation iteration gate decision
256
- await notify(`# Mini-Loop: Building — Iteration ${iteration}/10\n\nStatus: ${implGate.decision}\nConfidence: ${review.confidence}%\nUnresolved Issues: ${review.unresolvedIssues}\nDuration: ${formatElapsed(implementationStartMs, Date.now())}\nFeedback: ${feedback}\n`);
341
+ await notify(`Stage: Building — iteration ${iteration}/10\n` +
342
+ `${formatStructuredStage(implementationStageOutput, implGate.decision === 'REWORK')}\n` +
343
+ `Confidence: ${review.confidence}%\n` +
344
+ `Unresolved Issues: ${review.unresolvedIssues}\n` +
345
+ `Duration: ${formatElapsed(implementationStartMs, Date.now())}`);
257
346
  if (implGate.decision === 'ESCALATE') {
258
347
  const implementationEndMs = Date.now();
259
348
  addReport('Implementation', `${implementationIterationDetails.join('\n\n')}\n\n**Outcome**: ESCALATE: ${implGate.reason}\n**Phase time**: ${formatElapsed(implementationStartMs, implementationEndMs)}`);
@@ -298,7 +387,7 @@ export function createMiniLoopTool(client) {
298
387
  lastPhase = 'Documentation';
299
388
  const documentationStartMs = Date.now();
300
389
  const documentationIterationDetails = [];
301
- let docInput = lastImplRaw;
390
+ let docInput = lastImplementationHandoff || lastImplRaw;
302
391
  let docGate = { decision: 'REWORK' };
303
392
  // Phase start notification
304
393
  await notify(`# Mini-Loop: Documentation started\n\nStarting documentation phase...\n`);
@@ -382,16 +471,26 @@ export function createMiniLoopTool(client) {
382
471
  },
383
472
  });
384
473
  }
385
- const feedback = docReview.reworkInstructions || docGate.feedback || docReview.raw;
474
+ const docStageOutput = {
475
+ ...docReview.stageOutput,
476
+ status: docGate.decision,
477
+ };
386
478
  documentationIterationDetails.push(`### Iteration ${iteration}\n` +
387
479
  `- **Write**: ${writeTitle} (${formatElapsed(writeStartMs, writeEndMs)})\n` +
388
480
  `- **Review**: ${reviewTitle} (${formatElapsed(reviewStartMs, reviewEndMs)})\n` +
389
481
  `- **Gate**: ${docGate.decision} (confidence: ${docReview.confidence}, unresolved: ${docReview.unresolvedIssues})\n` +
390
- `- **Feedback**: ${feedback}`);
482
+ `${formatStructuredStage(docStageOutput, docGate.decision === 'REWORK')}`);
391
483
  // Notify each documentation iteration gate decision
392
- await notify(`# Mini-Loop: Documentation — Iteration ${iteration}/5\n\nStatus: ${docGate.decision}\nConfidence: ${docReview.confidence}%\nUnresolved Issues: ${docReview.unresolvedIssues}\nDuration: ${formatElapsed(documentationStartMs, Date.now())}\nFeedback: ${feedback}\n`);
484
+ await notify(`Stage: Documentation — iteration ${iteration}/5\n` +
485
+ `${formatStructuredStage(docStageOutput, docGate.decision === 'REWORK')}\n` +
486
+ `Confidence: ${docReview.confidence}%\n` +
487
+ `Unresolved Issues: ${docReview.unresolvedIssues}\n` +
488
+ `Duration: ${formatElapsed(documentationStartMs, Date.now())}`);
393
489
  if (docGate.decision === 'REWORK') {
394
- docInput = `${docInput}\n\nDocumentation review feedback:\n${docGate.feedback}`;
490
+ docInput = actionFirstReworkPrompt({
491
+ ...docReview.stageOutput,
492
+ status: docGate.decision,
493
+ }, `${docInput}\n\nDocumentation review feedback:\n${docGate.feedback || docReview.reworkInstructions || docReview.raw}`, docGate.feedback || docReview.reworkInstructions || docReview.raw);
395
494
  }
396
495
  }
397
496
  const documentationEndMs = Date.now();
@@ -425,11 +524,11 @@ export function createMiniLoopTool(client) {
425
524
  // Launch orchestration in background — fire-and-forget
426
525
  void asyncOrchestration().catch(async (err) => {
427
526
  const message = err instanceof Error ? err.message : String(err);
428
- await notifyParent(client, runContext, agent, `# Mini-Loop: Error\n\nPhase: ${lastPhase}\nError: ${message}\n`, { noReply: false });
527
+ await notifyParent(client, runContext, agent, `# Mini-Loop: ${runName} (${runId})\n\nStage: Error\nSTATUS: FAILED\nSUMMARY: Workflow terminated during ${lastPhase}.\nCHECKLIST:\n- Captured terminal error\nISSUES:\n- ${message}\n`, { noReply: false });
429
528
  });
430
529
  // Return immediately with acknowledgment
431
530
  const summary = requirements.length > 120 ? requirements.slice(0, 120) + '...' : requirements;
432
- return `Mini-loop started for: ${summary}\n\nRun directory: ${runDirectoryContext.runDirectory}\nWorktree isolation: ${runDirectoryContext.worktreeEnabled ? 'enabled' : 'disabled'}\nYou will receive progress updates as each phase completes.`;
531
+ return `Mini-loop started for: ${summary}\n\nRun: ${runName} (${runId})\nRun directory: ${runDirectoryContext.runDirectory}\nWorktree isolation: ${runDirectoryContext.worktreeEnabled ? 'enabled' : 'disabled'}\nYou will receive progress updates as each phase completes.`;
433
532
  },
434
533
  });
435
534
  }
@@ -5,13 +5,15 @@
5
5
  * structured sections cannot be found. The gate evaluators work on
6
6
  * the parsed numbers/booleans, not on freeform text.
7
7
  */
8
- import type { PlanProposal, ConsensusPlan, ReviewReport, ReviewSynthesis, DocReview, ImplementationReport, DocUpdate } from '../workflow/types.js';
8
+ import type { PlanProposal, ConsensusPlan, ReviewReport, ReviewSynthesis, DocReview, ImplementationReport, DocUpdate, StageOutput, MiniLoopReview } from '../workflow/types.js';
9
9
  /** Extract the first integer found after a label in the text. */
10
10
  export declare function extractNumber(text: string, label: string, fallback?: number): number;
11
11
  /** Extract text between two section headers (markdown-style). */
12
12
  export declare function extractSection(text: string, heading: string): string;
13
13
  /** Check if a YES/NO field is YES. */
14
14
  export declare function isYes(text: string, label: string): boolean;
15
+ export declare function parseStageOutput(raw: string, defaults?: Partial<Pick<StageOutput, 'status' | 'summary'>>): StageOutput;
16
+ export declare function formatActionItems(actionItems: string[]): string;
15
17
  export declare function parsePlanProposal(tag: string, raw: string): PlanProposal;
16
18
  export declare function parseConsensusPlan(raw: string): ConsensusPlan;
17
19
  export declare function parseReviewReport(tag: string, raw: string): ReviewReport;
@@ -19,10 +21,5 @@ export declare function parseReviewSynthesis(raw: string): ReviewSynthesis;
19
21
  export declare function parseDocUpdate(raw: string): DocUpdate;
20
22
  export declare function parseDocReview(raw: string): DocReview;
21
23
  export declare function parseImplementationReport(raw: string): ImplementationReport;
22
- export declare function parseMiniReview(raw: string): {
23
- confidence: number;
24
- changeRequested: boolean;
25
- unresolvedIssues: number;
26
- reworkInstructions?: string;
27
- raw: string;
28
- };
24
+ export declare function formatImplementationReportForHandoff(report: ImplementationReport): string;
25
+ export declare function parseMiniReview(raw: string): MiniLoopReview;
@@ -41,6 +41,100 @@ export function isYes(text, label) {
41
41
  const m = text.match(new RegExp(`${label}[=:\\s]*(YES|NO)`, 'i'));
42
42
  return m ? m[1].toUpperCase() === 'YES' : false;
43
43
  }
44
+ function fallbackSummary(raw) {
45
+ const firstNonEmpty = raw
46
+ .split('\n')
47
+ .map((line) => line.trim())
48
+ .find((line) => line.length > 0);
49
+ return firstNonEmpty || 'No summary provided.';
50
+ }
51
+ function extractDelimitedField(text, label) {
52
+ const pattern = new RegExp(`(?:^|\\n)(?:\\d+\\.\\s*)?\\*{0,2}${label}\\*{0,2}[=:]?[\\t ]*\\n?([\\s\\S]*?)(?=\\n(?:\\d+\\.\\s*)?\\*{0,2}(?:STATUS|SUMMARY|CHECKLIST|ISSUES|ACTION_ITEMS|VERDICT|CONFIDENCE|CHANGE_REQUESTED|UNRESOLVED(?:_BLOCKING_ISSUES|_DOCUMENTATION_ISSUES)?|UNRESOLVED_ISSUES)\\*{0,2}[=:]?|$)`, 'i');
53
+ const match = text.match(pattern);
54
+ return match ? match[1].trim() : '';
55
+ }
56
+ function parseListField(value) {
57
+ return value
58
+ .split('\n')
59
+ .map((line) => line.trim())
60
+ .map((line) => line.replace(/^[-*\d.)\s]+/, '').trim())
61
+ .filter((line) => line.length > 0);
62
+ }
63
+ function fallbackBulletLines(raw) {
64
+ return raw
65
+ .split('\n')
66
+ .map((line) => line.trim())
67
+ .filter((line) => line.length > 0)
68
+ .filter((line) => !/^(STATUS|SUMMARY|CHECKLIST|ISSUES|ACTION_ITEMS|VERDICT|CONFIDENCE|CHANGE_REQUESTED|UNRESOLVED)/i.test(line))
69
+ .slice(0, 4)
70
+ .map((line) => line.replace(/^[-*\d.)\s]+/, '').trim())
71
+ .filter((line) => line.length > 0);
72
+ }
73
+ function fallbackIssueLines(raw) {
74
+ const issueLike = raw
75
+ .split('\n')
76
+ .map((line) => line.trim())
77
+ .filter((line) => /\b(issue|error|fail|missing|regression|risk|bug|broken)\b/i.test(line))
78
+ .map((line) => line.replace(/^[-*\d.)\s]+/, '').trim())
79
+ .filter((line) => line.length > 0)
80
+ .slice(0, 4);
81
+ return issueLike;
82
+ }
83
+ function isNoneLike(value) {
84
+ return /^(none|none\.|no\b.*|n\/a|not applicable|nil)$/i.test(value.trim());
85
+ }
86
+ function normalizeOptionalList(items) {
87
+ return items.filter((item) => !isNoneLike(item));
88
+ }
89
+ function asBullets(items, empty) {
90
+ if (items.length === 0) {
91
+ return `- ${empty}`;
92
+ }
93
+ return items.map((item) => `- ${item}`).join('\n');
94
+ }
95
+ export function parseStageOutput(raw, defaults) {
96
+ const status = extractDelimitedField(raw, 'STATUS') ||
97
+ extractDelimitedField(raw, 'VERDICT') ||
98
+ defaults?.status ||
99
+ 'UNKNOWN';
100
+ const summary = extractDelimitedField(raw, 'SUMMARY') || defaults?.summary || fallbackSummary(raw);
101
+ const checklistField = parseListField(extractDelimitedField(raw, 'CHECKLIST'));
102
+ const issuesField = parseListField(extractDelimitedField(raw, 'ISSUES'));
103
+ const actionItemsField = parseListField(extractDelimitedField(raw, 'ACTION_ITEMS'));
104
+ const fallbackChecklist = fallbackBulletLines(raw);
105
+ const fallbackIssues = fallbackIssueLines(raw);
106
+ const normalizedStatus = status.toUpperCase();
107
+ const isReworkStatus = normalizedStatus.includes('REWORK') || normalizedStatus === 'ESCALATE';
108
+ const checklist = checklistField.length > 0
109
+ ? checklistField
110
+ : fallbackChecklist.length > 0
111
+ ? fallbackChecklist
112
+ : ['No checklist provided.'];
113
+ const issues = issuesField.length > 0
114
+ ? issuesField
115
+ : fallbackIssues.length > 0
116
+ ? fallbackIssues
117
+ : [];
118
+ const actionItems = actionItemsField.length > 0
119
+ ? actionItemsField
120
+ : isReworkStatus && issues.length > 0
121
+ ? issues.map((issue) => `Resolve: ${issue}`)
122
+ : [];
123
+ return {
124
+ status,
125
+ summary,
126
+ checklist,
127
+ issues,
128
+ actionItems,
129
+ raw,
130
+ };
131
+ }
132
+ export function formatActionItems(actionItems) {
133
+ if (actionItems.length === 0) {
134
+ return 'No explicit ACTION_ITEMS were provided.';
135
+ }
136
+ return actionItems.map((item, idx) => `${idx + 1}. ${item}`).join('\n');
137
+ }
44
138
  // ---------------------------------------------------------------------------
45
139
  // Planning parsers
46
140
  // ---------------------------------------------------------------------------
@@ -81,13 +175,19 @@ export function parseReviewSynthesis(raw) {
81
175
  const unresolvedIssues = extractNumber(raw, 'unresolved');
82
176
  const verdictMatch = raw.match(/verdict[=:\s]*(APPROVED|REWORK_REQUIRED)/i);
83
177
  const verdict = (verdictMatch?.[1]?.toUpperCase() === 'APPROVED' ? 'APPROVED' : 'REWORK_REQUIRED');
84
- const reworkSection = extractSection(raw, 'rework');
178
+ const stageOutput = parseStageOutput(raw, {
179
+ status: verdict,
180
+ summary: `Review confidence ${confidence}/100 with ${unresolvedIssues} unresolved issues.`,
181
+ });
85
182
  return {
86
183
  overallConfidence: confidence,
87
184
  consolidatedFindings: raw,
88
185
  unresolvedIssues,
89
186
  verdict,
90
- reworkInstructions: reworkSection || undefined,
187
+ reworkInstructions: stageOutput.actionItems.length > 0
188
+ ? formatActionItems(stageOutput.actionItems)
189
+ : extractSection(raw, 'rework') || undefined,
190
+ stageOutput,
91
191
  raw,
92
192
  };
93
193
  }
@@ -106,12 +206,18 @@ export function parseDocReview(raw) {
106
206
  const unresolvedIssues = extractNumber(raw, 'unresolved');
107
207
  const verdictMatch = raw.match(/verdict[=:\s]*(APPROVED|REWORK_REQUIRED)/i);
108
208
  const verdict = (verdictMatch?.[1]?.toUpperCase() === 'APPROVED' ? 'APPROVED' : 'REWORK_REQUIRED');
109
- const reworkSection = extractSection(raw, 'rework');
209
+ const stageOutput = parseStageOutput(raw, {
210
+ status: verdict,
211
+ summary: `Documentation confidence ${confidence}/100 with ${unresolvedIssues} unresolved issues.`,
212
+ });
110
213
  return {
111
214
  verdict,
112
215
  unresolvedIssues,
113
216
  confidence,
114
- reworkInstructions: reworkSection || undefined,
217
+ reworkInstructions: stageOutput.actionItems.length > 0
218
+ ? formatActionItems(stageOutput.actionItems)
219
+ : extractSection(raw, 'rework') || undefined,
220
+ stageOutput,
115
221
  raw,
116
222
  };
117
223
  }
@@ -119,24 +225,98 @@ export function parseDocReview(raw) {
119
225
  // Implementation parsers
120
226
  // ---------------------------------------------------------------------------
121
227
  export function parseImplementationReport(raw) {
228
+ const implementedTasks = parseListField(extractSection(raw, 'IMPLEMENTED_TASKS'));
229
+ const filesChanged = normalizeOptionalList(parseListField(extractSection(raw, 'FILES_CHANGED')));
230
+ const testResults = normalizeOptionalList(parseListField(extractSection(raw, 'TEST_RESULTS')));
231
+ const openIssues = normalizeOptionalList(parseListField(extractSection(raw, 'OPEN_ISSUES')));
232
+ const assumptionsMade = normalizeOptionalList(parseListField(extractSection(raw, 'ASSUMPTIONS_MADE')));
233
+ const testsRun = testResults;
234
+ const testsPassed = testResults.length > 0
235
+ ? testResults.every((result) => !/\b(fail|failed|failing|error|errors|broken|regression)\b/i.test(result) ||
236
+ /\b(no issues|no errors|not fail|resolved)\b/i.test(result))
237
+ : !/\b(test|lint|typecheck|build)\b[\s\S]{0,60}\b(fail|failed|error|errors)\b/i.test(raw);
238
+ const explicitStatus = extractDelimitedField(raw, 'STATUS').toUpperCase();
239
+ const fallbackStatus = testsPassed && openIssues.length === 0 ? 'APPROVED' : 'REWORK_REQUIRED';
240
+ const status = explicitStatus || fallbackStatus;
241
+ const explicitSummary = extractDelimitedField(raw, 'SUMMARY');
242
+ const summary = explicitSummary || implementedTasks[0] || fallbackSummary(raw);
243
+ const checklistField = parseListField(extractDelimitedField(raw, 'CHECKLIST'));
244
+ const issuesField = parseListField(extractDelimitedField(raw, 'ISSUES'));
245
+ const actionItemsField = parseListField(extractDelimitedField(raw, 'ACTION_ITEMS'));
246
+ const checklist = checklistField.length > 0
247
+ ? checklistField
248
+ : [
249
+ `Implemented tasks recorded: ${implementedTasks.length}`,
250
+ `Files changed recorded: ${filesChanged.length}`,
251
+ testResults.length > 0
252
+ ? `Test results recorded: ${testResults.length}`
253
+ : 'No explicit test results were reported.',
254
+ ];
255
+ const issues = issuesField.length > 0 ? issuesField : openIssues;
256
+ const actionItems = actionItemsField.length > 0
257
+ ? actionItemsField
258
+ : issues.length > 0
259
+ ? issues.map((issue) => `Resolve: ${issue}`)
260
+ : [];
261
+ const stageOutput = {
262
+ status,
263
+ summary,
264
+ checklist,
265
+ issues,
266
+ actionItems,
267
+ raw,
268
+ };
122
269
  return {
123
- filesChanged: [],
124
- testsRun: [],
125
- testsPassed: !raw.toLowerCase().includes('failed'),
126
- openIssues: [],
270
+ implementedTasks,
271
+ filesChanged,
272
+ testResults,
273
+ testsRun,
274
+ testsPassed,
275
+ openIssues,
276
+ assumptionsMade,
277
+ stageOutput,
127
278
  raw,
128
279
  };
129
280
  }
281
+ export function formatImplementationReportForHandoff(report) {
282
+ return [
283
+ `STATUS: ${report.stageOutput.status}`,
284
+ `SUMMARY: ${report.stageOutput.summary}`,
285
+ `CHECKLIST:\n${asBullets(report.stageOutput.checklist, 'No checklist provided.')}`,
286
+ `ISSUES:\n${asBullets(report.stageOutput.issues, 'No issues reported.')}`,
287
+ `ACTION_ITEMS:\n${asBullets(report.stageOutput.actionItems, 'No action items required.')}`,
288
+ '',
289
+ `IMPLEMENTED_TASKS\n${asBullets(report.implementedTasks, 'No tasks recorded.')}`,
290
+ `FILES_CHANGED\n${asBullets(report.filesChanged, 'No file list provided.')}`,
291
+ `TEST_RESULTS\n${asBullets(report.testResults, 'No test results provided.')}`,
292
+ `OPEN_ISSUES\n${asBullets(report.openIssues, 'No open issues.')}`,
293
+ `ASSUMPTIONS_MADE\n${asBullets(report.assumptionsMade, 'No assumptions listed.')}`,
294
+ '',
295
+ `RAW_IMPLEMENTATION_CONTEXT\n${report.raw.trim()}`,
296
+ ].join('\n');
297
+ }
130
298
  // ---------------------------------------------------------------------------
131
299
  // Mini-loop review parser
132
300
  // ---------------------------------------------------------------------------
133
301
  export function parseMiniReview(raw) {
302
+ const confidence = extractNumber(raw, 'CONFIDENCE');
303
+ const changeRequested = isYes(raw, 'CHANGE_REQUESTED') || /verdict[=:\s]*REWORK_REQUIRED/i.test(raw);
304
+ const unresolvedIssues = extractNumber(raw, 'UNRESOLVED_BLOCKING_ISSUES') ||
305
+ extractNumber(raw, 'UNRESOLVED_DOCUMENTATION_ISSUES') ||
306
+ extractNumber(raw, 'UNRESOLVED_ISSUES');
307
+ const status = changeRequested ? 'REWORK_REQUIRED' : 'APPROVED';
308
+ const stageOutput = parseStageOutput(raw, {
309
+ status,
310
+ summary: `Mini-loop review confidence ${confidence}/100 with ${unresolvedIssues} unresolved issues.`,
311
+ });
134
312
  return {
135
- confidence: extractNumber(raw, 'CONFIDENCE'),
136
- changeRequested: isYes(raw, 'CHANGE_REQUESTED'),
137
- unresolvedIssues: extractNumber(raw, 'UNRESOLVED_BLOCKING_ISSUES') ||
138
- extractNumber(raw, 'UNRESOLVED_DOCUMENTATION_ISSUES'),
139
- reworkInstructions: extractSection(raw, 'rework') || undefined,
313
+ confidence,
314
+ changeRequested,
315
+ unresolvedIssues,
316
+ reworkInstructions: stageOutput.actionItems.length > 0
317
+ ? formatActionItems(stageOutput.actionItems)
318
+ : extractSection(raw, 'rework') || undefined,
319
+ stageOutput,
140
320
  raw,
141
321
  };
142
322
  }