@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.
- package/dist/tools/mini-loop.js +118 -19
- package/dist/tools/parsers.d.ts +5 -8
- package/dist/tools/parsers.js +193 -13
- package/dist/tools/pipeline.js +163 -34
- package/dist/tools/prompts.d.ts +11 -0
- package/dist/tools/prompts.js +163 -23
- package/dist/workflow/gate-evaluator.d.ts +5 -7
- package/dist/workflow/gate-evaluator.js +50 -3
- package/dist/workflow/types.d.ts +23 -7
- package/package.json +1 -1
package/dist/tools/mini-loop.js
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
339
|
+
`${formatStructuredStage(implementationStageOutput, implGate.decision === 'REWORK')}`);
|
|
255
340
|
// Notify each implementation iteration gate decision
|
|
256
|
-
await notify(
|
|
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
|
|
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
|
-
|
|
482
|
+
`${formatStructuredStage(docStageOutput, docGate.decision === 'REWORK')}`);
|
|
391
483
|
// Notify each documentation iteration gate decision
|
|
392
|
-
await notify(
|
|
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 =
|
|
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:
|
|
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
|
}
|
package/dist/tools/parsers.d.ts
CHANGED
|
@@ -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
|
|
23
|
-
|
|
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;
|
package/dist/tools/parsers.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
136
|
-
changeRequested
|
|
137
|
-
unresolvedIssues
|
|
138
|
-
|
|
139
|
-
|
|
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
|
}
|