@syntesseraai/opencode-feature-factory 0.10.5 ā 0.10.7
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/AGENTS.md +37 -0
- package/README.md +6 -1
- package/bin/ff-deploy.js +22 -1
- 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 +2 -7
- package/dist/workflow/gate-evaluator.js +21 -3
- package/dist/workflow/types.d.ts +23 -7
- package/package.json +3 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Feature Factory AGENTS Guide
|
|
2
|
+
|
|
3
|
+
This file is installed to `~/.config/opencode/AGENTS.md` by `@syntesseraai/opencode-feature-factory`.
|
|
4
|
+
|
|
5
|
+
## What Feature Factory Provides
|
|
6
|
+
|
|
7
|
+
- Workflow tools:
|
|
8
|
+
- `ff_pipeline`: full multi-model planning/build/review/documentation workflow.
|
|
9
|
+
- `ff_mini_loop`: lightweight build/review/documentation loop for focused changes.
|
|
10
|
+
- `ff_list_models`: list available provider/model IDs for workflow overrides.
|
|
11
|
+
- Specialized agents: `feature-factory`, `planning`, `building`, `reviewing`, `documenting`, and `ff-research`.
|
|
12
|
+
- Skills for planning, review quality/security/architecture/documentation, reporting templates, and todo management.
|
|
13
|
+
|
|
14
|
+
## Preferred Workflow Pattern
|
|
15
|
+
|
|
16
|
+
1. Clarify requirements and acceptance criteria before coding.
|
|
17
|
+
2. Choose `ff_mini_loop` for small, well-scoped changes.
|
|
18
|
+
3. Choose `ff_pipeline` for complex, high-risk, or architectural changes.
|
|
19
|
+
4. Confirm model overrides only when needed; otherwise use defaults.
|
|
20
|
+
5. Use worktree isolation when running concurrent workflows.
|
|
21
|
+
|
|
22
|
+
## Preferred Tooling Pattern
|
|
23
|
+
|
|
24
|
+
- Use `cocoindex-code_search` first for semantic codebase discovery.
|
|
25
|
+
- Use `read`, `glob`, and `grep` for targeted file inspection.
|
|
26
|
+
- Use `morph-mcp_edit_file` for precise code/document edits.
|
|
27
|
+
- Use `bash` for non-interactive command execution (`git`, `npm`, `tsc`, tests, builds).
|
|
28
|
+
- Use PTY only when interactive terminal control is actually required.
|
|
29
|
+
- Use `todowrite` for multi-step tasks to keep progress visible.
|
|
30
|
+
|
|
31
|
+
## Implementation Expectations
|
|
32
|
+
|
|
33
|
+
- Keep edits scoped and consistent with existing architecture.
|
|
34
|
+
- Prefer non-destructive merges and preserve user configuration.
|
|
35
|
+
- Update docs when behavior, defaults, or operational steps change.
|
|
36
|
+
- Run relevant validation for touched areas (lint/typecheck/tests as needed).
|
|
37
|
+
- Avoid destructive git operations unless explicitly requested.
|
package/README.md
CHANGED
|
@@ -23,16 +23,21 @@ The installer deploys to `~/.config/opencode/`:
|
|
|
23
23
|
|
|
24
24
|
- `agents/`
|
|
25
25
|
- `skills/`
|
|
26
|
+
- `AGENTS.md`
|
|
26
27
|
|
|
27
28
|
It also updates `~/.config/opencode/opencode.json` non-destructively by merging missing Feature Factory MCP entries and plugins without deleting existing user configuration.
|
|
28
29
|
|
|
29
30
|
## Install Behavior
|
|
30
31
|
|
|
31
|
-
- **Always overwrites packaged assets**: installer unconditionally overwrites Feature Factory `agents` and `
|
|
32
|
+
- **Always overwrites packaged assets**: installer unconditionally overwrites Feature Factory `agents`, `skills`, and `AGENTS.md` files on every install.
|
|
32
33
|
- **`opencode.json` is non-destructive**: existing keys/values are preserved; only missing required plugin/MCP entries are added.
|
|
33
34
|
- **No automatic plugin removals**: existing plugin entries are preserved as-is, including entries no longer in current defaults.
|
|
34
35
|
- **Global scope**: assets are installed to `~/.config/opencode/` and shared across projects.
|
|
35
36
|
|
|
37
|
+
## Global AGENTS Guide
|
|
38
|
+
|
|
39
|
+
`~/.config/opencode/AGENTS.md` is installed from this package and provides Feature Factory-specific operating guidance, including workflow selection and preferred tool patterns (semantic search, edit workflow, and `bash`-first command execution).
|
|
40
|
+
|
|
36
41
|
## Tools
|
|
37
42
|
|
|
38
43
|
The plugin exposes three MCP tools via the `feature-factory` agent:
|
package/bin/ff-deploy.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Feature Factory Deployment Script
|
|
5
5
|
*
|
|
6
|
-
* Deploys skills and
|
|
6
|
+
* Deploys skills, agents, and AGENTS.md guidance to the global OpenCode
|
|
7
|
+
* configuration directory.
|
|
7
8
|
* Run manually with: npx @syntesseraai/opencode-feature-factory
|
|
8
9
|
*/
|
|
9
10
|
|
|
@@ -19,10 +20,12 @@ const GLOBAL_CONFIG_DIR = join(homedir(), '.config', 'opencode');
|
|
|
19
20
|
const SKILLS_DIR = join(GLOBAL_CONFIG_DIR, 'skills');
|
|
20
21
|
const AGENTS_DIR = join(GLOBAL_CONFIG_DIR, 'agents');
|
|
21
22
|
const GLOBAL_CONFIG_FILE = join(GLOBAL_CONFIG_DIR, 'opencode.json');
|
|
23
|
+
const GLOBAL_AGENTS_GUIDE_FILE = join(GLOBAL_CONFIG_DIR, 'AGENTS.md');
|
|
22
24
|
|
|
23
25
|
const PACKAGE_ROOT = join(__dirname, '..');
|
|
24
26
|
const SOURCE_SKILLS_DIR = join(PACKAGE_ROOT, 'skills');
|
|
25
27
|
const SOURCE_AGENTS_DIR = join(PACKAGE_ROOT, 'agents');
|
|
28
|
+
const SOURCE_AGENTS_GUIDE_FILE = join(PACKAGE_ROOT, 'AGENTS.md');
|
|
26
29
|
|
|
27
30
|
// Check if running in interactive mode (has TTY)
|
|
28
31
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
@@ -135,6 +138,15 @@ async function getFileNames(dir) {
|
|
|
135
138
|
}
|
|
136
139
|
}
|
|
137
140
|
|
|
141
|
+
async function fileExists(filePath) {
|
|
142
|
+
try {
|
|
143
|
+
await fs.access(filePath);
|
|
144
|
+
return true;
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
138
150
|
async function updateMCPConfig() {
|
|
139
151
|
if (isInteractive) {
|
|
140
152
|
console.log('\nš§ MCP Configuration Update');
|
|
@@ -279,6 +291,7 @@ async function deploy() {
|
|
|
279
291
|
// Get existing skills/agents for reporting
|
|
280
292
|
const existingSkills = await getDirectoryNames(SKILLS_DIR);
|
|
281
293
|
const existingAgents = await getFileNames(AGENTS_DIR);
|
|
294
|
+
const hadExistingAgentsGuide = await fileExists(GLOBAL_AGENTS_GUIDE_FILE);
|
|
282
295
|
|
|
283
296
|
// Deploy skills
|
|
284
297
|
if (isInteractive) {
|
|
@@ -315,11 +328,19 @@ async function deploy() {
|
|
|
315
328
|
}
|
|
316
329
|
}
|
|
317
330
|
|
|
331
|
+
// Deploy global AGENTS guide
|
|
332
|
+
await fs.copyFile(SOURCE_AGENTS_GUIDE_FILE, GLOBAL_AGENTS_GUIDE_FILE);
|
|
333
|
+
if (isInteractive) {
|
|
334
|
+
console.log('\nš Deploying AGENTS Guide...');
|
|
335
|
+
console.log(` ${hadExistingAgentsGuide ? 'š' : 'ā
'} AGENTS.md ${hadExistingAgentsGuide ? '(updated)' : '(new)'}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
318
338
|
// Summary
|
|
319
339
|
if (isInteractive) {
|
|
320
340
|
console.log('\n⨠Deployment Complete!');
|
|
321
341
|
console.log(` Skills: ${skills.length} deployed`);
|
|
322
342
|
console.log(` Agents: ${agents.length} deployed`);
|
|
343
|
+
console.log(' AGENTS guide: deployed');
|
|
323
344
|
console.log(` Location: ${GLOBAL_CONFIG_DIR}`);
|
|
324
345
|
console.log('\nš Next Steps:');
|
|
325
346
|
console.log(' - Restart OpenCode or run /reload to load new agents');
|
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;
|