@syntesseraai/opencode-feature-factory 0.6.21 → 0.7.0
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/command-config.d.ts +38 -0
- package/dist/command-config.js +128 -0
- package/dist/index.js +18 -2
- package/dist/tools/index.d.ts +16 -0
- package/dist/tools/index.js +21 -0
- package/dist/tools/mini-loop.d.ts +24 -0
- package/dist/tools/mini-loop.js +133 -0
- package/dist/tools/parsers.d.ts +28 -0
- package/dist/tools/parsers.js +142 -0
- package/dist/tools/pipeline.d.ts +32 -0
- package/dist/tools/pipeline.js +227 -0
- package/dist/tools/prompts.d.ts +24 -0
- package/dist/tools/prompts.js +182 -0
- package/dist/workflow/fan-out.d.ts +84 -0
- package/dist/workflow/fan-out.js +83 -0
- package/dist/workflow/gate-evaluator.d.ts +50 -0
- package/dist/workflow/gate-evaluator.js +118 -0
- package/dist/workflow/orchestrator.d.ts +9 -0
- package/dist/workflow/orchestrator.js +9 -0
- package/dist/workflow/types.d.ts +148 -0
- package/dist/workflow/types.js +62 -0
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type BunShell = any;
|
|
2
|
+
/**
|
|
3
|
+
* Command configuration type matching the OpenCode `command` config schema.
|
|
4
|
+
*/
|
|
5
|
+
export interface CommandConfig {
|
|
6
|
+
template: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
agent?: string;
|
|
9
|
+
model?: string;
|
|
10
|
+
subtask?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface CommandConfigs {
|
|
13
|
+
[commandName: string]: CommandConfig;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Default commands to register in the global OpenCode config.
|
|
17
|
+
*
|
|
18
|
+
* "Feature-Factory" is a Socratic entrypoint that guides the user through
|
|
19
|
+
* clarifying requirements, choosing between pipeline and mini-loop, confirming
|
|
20
|
+
* model selections, and then invoking the appropriate workflow tool.
|
|
21
|
+
*/
|
|
22
|
+
export declare const DEFAULT_COMMANDS: Record<string, CommandConfig>;
|
|
23
|
+
/**
|
|
24
|
+
* Merge command configs, preserving existing settings and adding new ones.
|
|
25
|
+
* Existing command configs take precedence (we never overwrite them).
|
|
26
|
+
*/
|
|
27
|
+
export declare function mergeCommandConfigs(existing: CommandConfigs | undefined, defaults: Record<string, CommandConfig>): CommandConfigs;
|
|
28
|
+
/**
|
|
29
|
+
* Update the command configuration in global opencode.json.
|
|
30
|
+
*
|
|
31
|
+
* This function:
|
|
32
|
+
* 1. Reads existing config from ~/.config/opencode/opencode.json
|
|
33
|
+
* 2. Preserves existing command settings
|
|
34
|
+
* 3. Adds default Feature Factory commands that don't exist yet
|
|
35
|
+
* 4. Writes updated config back to ~/.config/opencode/opencode.json
|
|
36
|
+
*/
|
|
37
|
+
export declare function updateCommandConfig($: BunShell): Promise<void>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { isRecord, updateGlobalOpenCodeConfigBlock } from './opencode-global-config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default commands to register in the global OpenCode config.
|
|
4
|
+
*
|
|
5
|
+
* "Feature-Factory" is a Socratic entrypoint that guides the user through
|
|
6
|
+
* clarifying requirements, choosing between pipeline and mini-loop, confirming
|
|
7
|
+
* model selections, and then invoking the appropriate workflow tool.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_COMMANDS = {
|
|
10
|
+
'feature-factory': {
|
|
11
|
+
description: 'Feature Factory — guided workflow for planning and building features',
|
|
12
|
+
agent: 'general',
|
|
13
|
+
subtask: false,
|
|
14
|
+
template: `You are the Feature Factory workflow assistant. Your job is to guide the user through a structured process before launching either the full pipeline or the mini-loop.
|
|
15
|
+
|
|
16
|
+
Work through this process step by step. Do NOT skip steps or launch a tool until all steps are complete.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Step 1: Understand the request
|
|
21
|
+
|
|
22
|
+
The user said: $ARGUMENTS
|
|
23
|
+
|
|
24
|
+
Work through their request in a Socratic manner:
|
|
25
|
+
- Ask clarifying questions about scope, constraints, and acceptance criteria.
|
|
26
|
+
- Summarise your understanding back to them and ask them to confirm or correct it.
|
|
27
|
+
- Continue until you have a clear, unambiguous set of requirements.
|
|
28
|
+
|
|
29
|
+
If $ARGUMENTS is empty, ask the user what they would like to build.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Step 2: Choose workflow
|
|
34
|
+
|
|
35
|
+
Once requirements are agreed, present the two workflow options:
|
|
36
|
+
|
|
37
|
+
**Pipeline** (full multi-model workflow)
|
|
38
|
+
- Multi-model fan-out planning (3 models propose plans, then consensus synthesis)
|
|
39
|
+
- Build phase with task breakdown and batch validation
|
|
40
|
+
- Multi-model fan-out code review
|
|
41
|
+
- Documentation with review loop
|
|
42
|
+
- Best for: complex features, architectural changes, high-risk work
|
|
43
|
+
|
|
44
|
+
**Mini-loop** (lightweight workflow)
|
|
45
|
+
- Direct build → review loop (single model per step)
|
|
46
|
+
- Documentation with review loop
|
|
47
|
+
- Best for: small features, bug fixes, incremental improvements, well-understood changes
|
|
48
|
+
|
|
49
|
+
Ask the user which workflow they prefer. If they are unsure, recommend one based on the complexity of the requirements.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Step 3: Confirm models
|
|
54
|
+
|
|
55
|
+
Present the default models that will be used for the chosen workflow:
|
|
56
|
+
|
|
57
|
+
**Pipeline defaults:**
|
|
58
|
+
- Planning fan-out: opus (anthropic/claude-opus-4-6), gemini (opencode/gemini-3.1-pro), codex (openai/gpt-5.3-codex)
|
|
59
|
+
- Review fan-out: same as planning
|
|
60
|
+
- Orchestrator (synthesis/triage): openai/gpt-5.4
|
|
61
|
+
- Build: openai/gpt-5.3-codex
|
|
62
|
+
- Validate: opencode/gemini-3.1-pro
|
|
63
|
+
- Documentation: openai/gpt-5.3-codex
|
|
64
|
+
- Doc review: opencode/gemini-3.1-pro
|
|
65
|
+
|
|
66
|
+
**Mini-loop defaults:**
|
|
67
|
+
- Build: openai/gpt-5.3-codex
|
|
68
|
+
- Review: openai/gpt-5.4
|
|
69
|
+
- Documentation: openai/gpt-5.3-codex
|
|
70
|
+
- Doc review: opencode/gemini-3.1-pro
|
|
71
|
+
|
|
72
|
+
Ask: "Would you like to use these default models, or would you like to override any of them?"
|
|
73
|
+
|
|
74
|
+
If they want to override, collect the provider/model strings for each role they want to change.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Step 4: Launch
|
|
79
|
+
|
|
80
|
+
Once all three steps are confirmed, call the appropriate tool:
|
|
81
|
+
|
|
82
|
+
- If they chose **Pipeline**: call the \`ff_pipeline\` tool with the agreed requirements and any model overrides.
|
|
83
|
+
- If they chose **Mini-loop**: call the \`ff_mini_loop\` tool with the agreed requirements and any model overrides.
|
|
84
|
+
|
|
85
|
+
Pass only the model parameters that were explicitly overridden; omit any that should use defaults.`,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Merge command configs, preserving existing settings and adding new ones.
|
|
90
|
+
* Existing command configs take precedence (we never overwrite them).
|
|
91
|
+
*/
|
|
92
|
+
export function mergeCommandConfigs(existing, defaults) {
|
|
93
|
+
const existingCommands = existing ?? {};
|
|
94
|
+
const result = { ...existingCommands };
|
|
95
|
+
for (const [commandName, commandConfig] of Object.entries(defaults)) {
|
|
96
|
+
if (!result[commandName]) {
|
|
97
|
+
result[commandName] = commandConfig;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Update the command configuration in global opencode.json.
|
|
104
|
+
*
|
|
105
|
+
* This function:
|
|
106
|
+
* 1. Reads existing config from ~/.config/opencode/opencode.json
|
|
107
|
+
* 2. Preserves existing command settings
|
|
108
|
+
* 3. Adds default Feature Factory commands that don't exist yet
|
|
109
|
+
* 4. Writes updated config back to ~/.config/opencode/opencode.json
|
|
110
|
+
*/
|
|
111
|
+
export async function updateCommandConfig($) {
|
|
112
|
+
void $;
|
|
113
|
+
await updateGlobalOpenCodeConfigBlock({
|
|
114
|
+
blockName: 'command',
|
|
115
|
+
warningLabel: 'command config',
|
|
116
|
+
update: (existingBlock) => {
|
|
117
|
+
const existingCommandConfigs = isRecord(existingBlock)
|
|
118
|
+
? existingBlock
|
|
119
|
+
: undefined;
|
|
120
|
+
const updatedCommandConfigs = mergeCommandConfigs(existingCommandConfigs, DEFAULT_COMMANDS);
|
|
121
|
+
const hasChanges = Object.keys(DEFAULT_COMMANDS).some((commandName) => !existingCommandConfigs?.[commandName]);
|
|
122
|
+
return {
|
|
123
|
+
nextBlock: updatedCommandConfigs,
|
|
124
|
+
changed: hasChanges,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import { StopQualityGateHooksPlugin } from './stop-quality-gate.js';
|
|
|
2
2
|
import { updateMCPConfig } from './mcp-config.js';
|
|
3
3
|
import { updateAgentConfig } from './agent-config.js';
|
|
4
4
|
import { updatePluginConfig } from './plugin-config.js';
|
|
5
|
+
import { updateCommandConfig } from './command-config.js';
|
|
6
|
+
import { createWorkflowTools } from './tools/index.js';
|
|
5
7
|
import { $ } from 'bun';
|
|
6
8
|
/**
|
|
7
9
|
* Feature Factory Plugin
|
|
@@ -40,10 +42,24 @@ export const FeatureFactoryPlugin = async (input) => {
|
|
|
40
42
|
catch {
|
|
41
43
|
console.error('Failed to update plugin config in OpenCode plugin');
|
|
42
44
|
}
|
|
45
|
+
// Update command configuration in global OpenCode config
|
|
46
|
+
// This registers the Feature-Factory Socratic entrypoint command
|
|
47
|
+
try {
|
|
48
|
+
await updateCommandConfig($);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
console.error('Failed to update command config in OpenCode plugin');
|
|
52
|
+
}
|
|
43
53
|
// Load hooks from the quality gate plugin
|
|
44
54
|
const qualityGateHooks = await StopQualityGateHooksPlugin(input).catch(() => ({}));
|
|
45
|
-
// Tool registry
|
|
46
|
-
|
|
55
|
+
// Tool registry: workflow tools (ff_pipeline, ff_mini_loop) powered by
|
|
56
|
+
// the SDK client for programmatic session orchestration.
|
|
57
|
+
// The old ff-* artifact tools were removed; these replace the subtask2
|
|
58
|
+
// command-based workflow with TypeScript-driven orchestration.
|
|
59
|
+
const workflowTools = createWorkflowTools(input.client);
|
|
60
|
+
// Merge workflow tools with any existing tools from quality gate hooks
|
|
61
|
+
const existingTools = qualityGateHooks.tool ?? {};
|
|
62
|
+
const tools = { ...existingTools, ...workflowTools };
|
|
47
63
|
// Return combined hooks and tools
|
|
48
64
|
return {
|
|
49
65
|
...qualityGateHooks,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry for Feature Factory.
|
|
3
|
+
*
|
|
4
|
+
* Exports a function that creates all FF tools given the SDK client.
|
|
5
|
+
* Only entrypoint tools (pipeline, mini-loop) are exposed to the LLM.
|
|
6
|
+
* Internal workflow steps are orchestrated programmatically, not as
|
|
7
|
+
* individually callable tools.
|
|
8
|
+
*/
|
|
9
|
+
import type { ToolDefinition } from '@opencode-ai/plugin/tool';
|
|
10
|
+
import type { Client } from '../workflow/fan-out.js';
|
|
11
|
+
/**
|
|
12
|
+
* Build the tool map to be merged into the plugin's `Hooks.tool` output.
|
|
13
|
+
*
|
|
14
|
+
* @param client - The OpenCode SDK client from PluginInput
|
|
15
|
+
*/
|
|
16
|
+
export declare function createWorkflowTools(client: Client): Record<string, ToolDefinition>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry for Feature Factory.
|
|
3
|
+
*
|
|
4
|
+
* Exports a function that creates all FF tools given the SDK client.
|
|
5
|
+
* Only entrypoint tools (pipeline, mini-loop) are exposed to the LLM.
|
|
6
|
+
* Internal workflow steps are orchestrated programmatically, not as
|
|
7
|
+
* individually callable tools.
|
|
8
|
+
*/
|
|
9
|
+
import { createPipelineTool } from './pipeline.js';
|
|
10
|
+
import { createMiniLoopTool } from './mini-loop.js';
|
|
11
|
+
/**
|
|
12
|
+
* Build the tool map to be merged into the plugin's `Hooks.tool` output.
|
|
13
|
+
*
|
|
14
|
+
* @param client - The OpenCode SDK client from PluginInput
|
|
15
|
+
*/
|
|
16
|
+
export function createWorkflowTools(client) {
|
|
17
|
+
return {
|
|
18
|
+
ff_pipeline: createPipelineTool(client),
|
|
19
|
+
ff_mini_loop: createMiniLoopTool(client),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ff_mini_loop — Lightweight build → review → document workflow tool.
|
|
3
|
+
*
|
|
4
|
+
* A simpler two-stage workflow (no multi-model fan-out for planning).
|
|
5
|
+
* Implements a build/review loop, then a documentation loop.
|
|
6
|
+
*/
|
|
7
|
+
import { type Client } from '../workflow/orchestrator.js';
|
|
8
|
+
export declare function createMiniLoopTool(client: Client): {
|
|
9
|
+
description: string;
|
|
10
|
+
args: {
|
|
11
|
+
requirements: import("zod").ZodString;
|
|
12
|
+
build_model: import("zod").ZodOptional<import("zod").ZodString>;
|
|
13
|
+
review_model: import("zod").ZodOptional<import("zod").ZodString>;
|
|
14
|
+
doc_model: import("zod").ZodOptional<import("zod").ZodString>;
|
|
15
|
+
doc_review_model: import("zod").ZodOptional<import("zod").ZodString>;
|
|
16
|
+
};
|
|
17
|
+
execute(args: {
|
|
18
|
+
requirements: string;
|
|
19
|
+
build_model?: string | undefined;
|
|
20
|
+
review_model?: string | undefined;
|
|
21
|
+
doc_model?: string | undefined;
|
|
22
|
+
doc_review_model?: string | undefined;
|
|
23
|
+
}, context: import("@opencode-ai/plugin/tool").ToolContext): Promise<string>;
|
|
24
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ff_mini_loop — Lightweight build → review → document workflow tool.
|
|
3
|
+
*
|
|
4
|
+
* A simpler two-stage workflow (no multi-model fan-out for planning).
|
|
5
|
+
* Implements a build/review loop, then a documentation loop.
|
|
6
|
+
*/
|
|
7
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
8
|
+
import { promptSession, evaluateMiniLoopImplGate, evaluateMiniLoopDocGate, BUILD_MODEL, DOC_MODEL, DOC_REVIEW_MODEL, ORCHESTRATOR_MODEL, parseModelString, } from '../workflow/orchestrator.js';
|
|
9
|
+
import { miniBuildPrompt, miniReviewPrompt, documentPrompt, docReviewPrompt } from './prompts.js';
|
|
10
|
+
import { parseMiniReview, parseDocReview } from './parsers.js';
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Tool factory
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
export function createMiniLoopTool(client) {
|
|
15
|
+
return tool({
|
|
16
|
+
description: 'Run the Feature Factory mini-loop: build → review (with rework loop) → documentation. ' +
|
|
17
|
+
'Lighter weight than the full pipeline — no multi-model planning phase. ' +
|
|
18
|
+
'All model parameters are optional and use sensible defaults.',
|
|
19
|
+
args: {
|
|
20
|
+
requirements: tool.schema
|
|
21
|
+
.string()
|
|
22
|
+
.describe('The feature requirements or task description to implement'),
|
|
23
|
+
build_model: tool.schema
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('provider/model for building and implementation. Defaults to openai/gpt-5.3-codex.'),
|
|
27
|
+
review_model: tool.schema
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('provider/model for implementation review. Defaults to openai/gpt-5.4.'),
|
|
31
|
+
doc_model: tool.schema
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('provider/model for documentation writing. Defaults to the build model.'),
|
|
35
|
+
doc_review_model: tool.schema
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('provider/model for documentation review. Defaults to opencode/gemini-3.1-pro.'),
|
|
39
|
+
},
|
|
40
|
+
async execute(args, context) {
|
|
41
|
+
const sessionId = context.sessionID;
|
|
42
|
+
const { requirements } = args;
|
|
43
|
+
// Resolve models — use provided overrides or fall back to defaults
|
|
44
|
+
const buildModel = args.build_model
|
|
45
|
+
? parseModelString(args.build_model)
|
|
46
|
+
: BUILD_MODEL;
|
|
47
|
+
const reviewModel = args.review_model
|
|
48
|
+
? parseModelString(args.review_model)
|
|
49
|
+
: ORCHESTRATOR_MODEL;
|
|
50
|
+
const docModel = args.doc_model
|
|
51
|
+
? parseModelString(args.doc_model)
|
|
52
|
+
: args.build_model
|
|
53
|
+
? buildModel
|
|
54
|
+
: DOC_MODEL;
|
|
55
|
+
const docReviewModel = args.doc_review_model
|
|
56
|
+
? parseModelString(args.doc_review_model)
|
|
57
|
+
: DOC_REVIEW_MODEL;
|
|
58
|
+
const report = [];
|
|
59
|
+
const addReport = (phase, msg) => {
|
|
60
|
+
report.push(`## ${phase}\n${msg}`);
|
|
61
|
+
};
|
|
62
|
+
// ===================================================================
|
|
63
|
+
// PHASE 1: IMPLEMENTATION LOOP (build → review → gate, up to 10)
|
|
64
|
+
// ===================================================================
|
|
65
|
+
let implGate = { decision: 'REWORK', feedback: requirements };
|
|
66
|
+
let lastImplRaw = '';
|
|
67
|
+
for (let implIter = 0; implIter < 10 && implGate.decision === 'REWORK'; implIter++) {
|
|
68
|
+
const buildInput = implIter === 0
|
|
69
|
+
? requirements
|
|
70
|
+
: `${requirements}\n\nPrevious review feedback:\n${implGate.feedback}`;
|
|
71
|
+
// Build
|
|
72
|
+
lastImplRaw = await promptSession(client, sessionId, miniBuildPrompt(buildInput, implIter > 0 ? implGate.feedback : undefined), { model: buildModel, agent: 'building', title: `ff-mini-build-${implIter + 1}` });
|
|
73
|
+
// Review
|
|
74
|
+
const reviewRaw = await promptSession(client, sessionId, miniReviewPrompt(lastImplRaw), {
|
|
75
|
+
model: reviewModel,
|
|
76
|
+
agent: 'reviewing',
|
|
77
|
+
title: `ff-mini-review-${implIter + 1}`,
|
|
78
|
+
});
|
|
79
|
+
const review = parseMiniReview(reviewRaw);
|
|
80
|
+
// Gate (deterministic)
|
|
81
|
+
implGate = evaluateMiniLoopImplGate(review, implIter + 1);
|
|
82
|
+
if (implGate.decision === 'APPROVED') {
|
|
83
|
+
addReport('Implementation', `APPROVED (confidence: ${review.confidence}, iteration: ${implIter + 1})`);
|
|
84
|
+
}
|
|
85
|
+
else if (implGate.decision === 'ESCALATE') {
|
|
86
|
+
addReport('Implementation', `ESCALATE: ${implGate.reason}`);
|
|
87
|
+
return report.join('\n\n');
|
|
88
|
+
}
|
|
89
|
+
// REWORK continues the loop
|
|
90
|
+
}
|
|
91
|
+
if (implGate.decision !== 'APPROVED') {
|
|
92
|
+
addReport('Implementation', `REWORK exhausted (10 iterations). Last feedback:\n${implGate.feedback}`);
|
|
93
|
+
return report.join('\n\n');
|
|
94
|
+
}
|
|
95
|
+
// ===================================================================
|
|
96
|
+
// PHASE 2: DOCUMENTATION LOOP (document → review → gate, up to 5)
|
|
97
|
+
// ===================================================================
|
|
98
|
+
let docInput = lastImplRaw;
|
|
99
|
+
let docGate = { decision: 'REWORK' };
|
|
100
|
+
for (let docIter = 0; docIter < 5 && docGate.decision === 'REWORK'; docIter++) {
|
|
101
|
+
// Write docs
|
|
102
|
+
const docRaw = await promptSession(client, sessionId, documentPrompt(docInput), {
|
|
103
|
+
model: docModel,
|
|
104
|
+
agent: 'documenting',
|
|
105
|
+
title: `ff-mini-doc-write-${docIter + 1}`,
|
|
106
|
+
});
|
|
107
|
+
// Review docs
|
|
108
|
+
const docRevRaw = await promptSession(client, sessionId, docReviewPrompt(docRaw), {
|
|
109
|
+
model: docReviewModel,
|
|
110
|
+
agent: 'reviewing',
|
|
111
|
+
title: `ff-mini-doc-review-${docIter + 1}`,
|
|
112
|
+
});
|
|
113
|
+
const docReview = parseDocReview(docRevRaw);
|
|
114
|
+
// Gate (deterministic)
|
|
115
|
+
docGate = evaluateMiniLoopDocGate(docReview, docIter + 1);
|
|
116
|
+
if (docGate.decision === 'APPROVED') {
|
|
117
|
+
addReport('Documentation', `APPROVED (confidence: ${docReview.confidence}, iteration: ${docIter + 1})`);
|
|
118
|
+
}
|
|
119
|
+
else if (docGate.decision === 'ESCALATE') {
|
|
120
|
+
addReport('Documentation', `ESCALATE: ${docGate.reason}`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
docInput = `${docInput}\n\nDocumentation review feedback:\n${docGate.feedback}`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// ===================================================================
|
|
127
|
+
// FINAL REPORT
|
|
128
|
+
// ===================================================================
|
|
129
|
+
addReport('Complete', 'Mini-loop finished.');
|
|
130
|
+
return report.join('\n\n');
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response parsers that extract structured data from LLM text output.
|
|
3
|
+
*
|
|
4
|
+
* These are intentionally tolerant — they fall back to raw text when
|
|
5
|
+
* structured sections cannot be found. The gate evaluators work on
|
|
6
|
+
* the parsed numbers/booleans, not on freeform text.
|
|
7
|
+
*/
|
|
8
|
+
import type { PlanProposal, ConsensusPlan, ReviewReport, ReviewSynthesis, DocReview, ImplementationReport, DocUpdate } from '../workflow/types.js';
|
|
9
|
+
/** Extract the first integer found after a label in the text. */
|
|
10
|
+
export declare function extractNumber(text: string, label: string, fallback?: number): number;
|
|
11
|
+
/** Extract text between two section headers (markdown-style). */
|
|
12
|
+
export declare function extractSection(text: string, heading: string): string;
|
|
13
|
+
/** Check if a YES/NO field is YES. */
|
|
14
|
+
export declare function isYes(text: string, label: string): boolean;
|
|
15
|
+
export declare function parsePlanProposal(tag: string, raw: string): PlanProposal;
|
|
16
|
+
export declare function parseConsensusPlan(raw: string): ConsensusPlan;
|
|
17
|
+
export declare function parseReviewReport(tag: string, raw: string): ReviewReport;
|
|
18
|
+
export declare function parseReviewSynthesis(raw: string): ReviewSynthesis;
|
|
19
|
+
export declare function parseDocUpdate(raw: string): DocUpdate;
|
|
20
|
+
export declare function parseDocReview(raw: string): DocReview;
|
|
21
|
+
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
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response parsers that extract structured data from LLM text output.
|
|
3
|
+
*
|
|
4
|
+
* These are intentionally tolerant — they fall back to raw text when
|
|
5
|
+
* structured sections cannot be found. The gate evaluators work on
|
|
6
|
+
* the parsed numbers/booleans, not on freeform text.
|
|
7
|
+
*/
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Generic helpers
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/** Extract the first integer found after a label in the text. */
|
|
12
|
+
export function extractNumber(text, label, fallback = 0) {
|
|
13
|
+
// Try patterns like "CONSENSUS_SCORE: 85", "confidence: 92", "CONFIDENCE=95"
|
|
14
|
+
const patterns = [
|
|
15
|
+
new RegExp(`${label}[=:\\s]+([0-9]+)`, 'i'),
|
|
16
|
+
new RegExp(`${label}[^0-9]*([0-9]+)`, 'i'),
|
|
17
|
+
];
|
|
18
|
+
for (const re of patterns) {
|
|
19
|
+
const m = text.match(re);
|
|
20
|
+
if (m)
|
|
21
|
+
return parseInt(m[1], 10);
|
|
22
|
+
}
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
/** Extract text between two section headers (markdown-style). */
|
|
26
|
+
export function extractSection(text, heading) {
|
|
27
|
+
// Try numbered list style: "1. HEADING" or "## HEADING" or "**HEADING**"
|
|
28
|
+
const patterns = [
|
|
29
|
+
new RegExp(`(?:^|\\n)(?:\\d+\\.\\s*)?\\*{0,2}${heading}\\*{0,2}[:\\s]*\\n([\\s\\S]*?)(?=\\n(?:\\d+\\.\\s*)?\\*{0,2}[A-Z_]+|$)`, 'i'),
|
|
30
|
+
new RegExp(`(?:^|\\n)#{1,3}\\s*${heading}[:\\s]*\\n([\\s\\S]*?)(?=\\n#{1,3}\\s|$)`, 'i'),
|
|
31
|
+
];
|
|
32
|
+
for (const re of patterns) {
|
|
33
|
+
const m = text.match(re);
|
|
34
|
+
if (m)
|
|
35
|
+
return m[1].trim();
|
|
36
|
+
}
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
/** Check if a YES/NO field is YES. */
|
|
40
|
+
export function isYes(text, label) {
|
|
41
|
+
const m = text.match(new RegExp(`${label}[=:\\s]*(YES|NO)`, 'i'));
|
|
42
|
+
return m ? m[1].toUpperCase() === 'YES' : false;
|
|
43
|
+
}
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Planning parsers
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
export function parsePlanProposal(tag, raw) {
|
|
48
|
+
return {
|
|
49
|
+
tag,
|
|
50
|
+
requirementsSummary: extractSection(raw, 'REQUIREMENTS_SUMMARY') || raw.slice(0, 200),
|
|
51
|
+
architectureValidation: extractSection(raw, 'ARCHITECTURE_VALIDATION'),
|
|
52
|
+
implementationSteps: extractSection(raw, 'IMPLEMENTATION_STEPS'),
|
|
53
|
+
risksAndMitigations: extractSection(raw, 'RISKS_AND_MITIGATIONS'),
|
|
54
|
+
testingStrategy: extractSection(raw, 'TESTING_AND_VALIDATION_STRATEGY'),
|
|
55
|
+
raw,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function parseConsensusPlan(raw) {
|
|
59
|
+
return {
|
|
60
|
+
consensusScore: extractNumber(raw, 'CONSENSUS_SCORE'),
|
|
61
|
+
agreedElements: extractSection(raw, 'AGREED_ELEMENTS'),
|
|
62
|
+
divergentElements: extractSection(raw, 'DIVERGENT_ELEMENTS'),
|
|
63
|
+
synthesizedPlan: extractSection(raw, 'SYNTHESIZED_PLAN'),
|
|
64
|
+
openQuestions: extractSection(raw, 'OPEN_QUESTIONS'),
|
|
65
|
+
raw,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Review parsers
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
export function parseReviewReport(tag, raw) {
|
|
72
|
+
return {
|
|
73
|
+
tag,
|
|
74
|
+
findings: raw,
|
|
75
|
+
confidence: extractNumber(raw, 'confidence'),
|
|
76
|
+
raw,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function parseReviewSynthesis(raw) {
|
|
80
|
+
const confidence = extractNumber(raw, 'confidence');
|
|
81
|
+
const unresolvedIssues = extractNumber(raw, 'unresolved');
|
|
82
|
+
const verdictMatch = raw.match(/verdict[=:\s]*(APPROVED|REWORK_REQUIRED)/i);
|
|
83
|
+
const verdict = (verdictMatch?.[1]?.toUpperCase() === 'APPROVED' ? 'APPROVED' : 'REWORK_REQUIRED');
|
|
84
|
+
const reworkSection = extractSection(raw, 'rework');
|
|
85
|
+
return {
|
|
86
|
+
overallConfidence: confidence,
|
|
87
|
+
consolidatedFindings: raw,
|
|
88
|
+
unresolvedIssues,
|
|
89
|
+
verdict,
|
|
90
|
+
reworkInstructions: reworkSection || undefined,
|
|
91
|
+
raw,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Doc parsers
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
export function parseDocUpdate(raw) {
|
|
98
|
+
return {
|
|
99
|
+
filesChanged: [], // The LLM lists these in the raw text; we keep the raw for the gate
|
|
100
|
+
rationale: raw,
|
|
101
|
+
raw,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function parseDocReview(raw) {
|
|
105
|
+
const confidence = extractNumber(raw, 'confidence');
|
|
106
|
+
const unresolvedIssues = extractNumber(raw, 'unresolved');
|
|
107
|
+
const verdictMatch = raw.match(/verdict[=:\s]*(APPROVED|REWORK_REQUIRED)/i);
|
|
108
|
+
const verdict = (verdictMatch?.[1]?.toUpperCase() === 'APPROVED' ? 'APPROVED' : 'REWORK_REQUIRED');
|
|
109
|
+
const reworkSection = extractSection(raw, 'rework');
|
|
110
|
+
return {
|
|
111
|
+
verdict,
|
|
112
|
+
unresolvedIssues,
|
|
113
|
+
confidence,
|
|
114
|
+
reworkInstructions: reworkSection || undefined,
|
|
115
|
+
raw,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Implementation parsers
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
export function parseImplementationReport(raw) {
|
|
122
|
+
return {
|
|
123
|
+
filesChanged: [],
|
|
124
|
+
testsRun: [],
|
|
125
|
+
testsPassed: !raw.toLowerCase().includes('failed'),
|
|
126
|
+
openIssues: [],
|
|
127
|
+
raw,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Mini-loop review parser
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
export function parseMiniReview(raw) {
|
|
134
|
+
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,
|
|
140
|
+
raw,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ff_pipeline — Full plan → build → review → document workflow tool.
|
|
3
|
+
*
|
|
4
|
+
* This is an entrypoint tool exposed to the LLM. It orchestrates the
|
|
5
|
+
* complete Feature Factory pipeline using deterministic control flow
|
|
6
|
+
* (TypeScript loops and gates) while delegating creative work to
|
|
7
|
+
* model-specific prompts via the SDK client.
|
|
8
|
+
*/
|
|
9
|
+
import { type Client } from '../workflow/orchestrator.js';
|
|
10
|
+
export declare function createPipelineTool(client: Client): {
|
|
11
|
+
description: string;
|
|
12
|
+
args: {
|
|
13
|
+
requirements: import("zod").ZodString;
|
|
14
|
+
planning_models: import("zod").ZodOptional<import("zod").ZodString>;
|
|
15
|
+
review_models: import("zod").ZodOptional<import("zod").ZodString>;
|
|
16
|
+
orchestrator_model: import("zod").ZodOptional<import("zod").ZodString>;
|
|
17
|
+
build_model: import("zod").ZodOptional<import("zod").ZodString>;
|
|
18
|
+
validate_model: import("zod").ZodOptional<import("zod").ZodString>;
|
|
19
|
+
doc_model: import("zod").ZodOptional<import("zod").ZodString>;
|
|
20
|
+
doc_review_model: import("zod").ZodOptional<import("zod").ZodString>;
|
|
21
|
+
};
|
|
22
|
+
execute(args: {
|
|
23
|
+
requirements: string;
|
|
24
|
+
planning_models?: string | undefined;
|
|
25
|
+
review_models?: string | undefined;
|
|
26
|
+
orchestrator_model?: string | undefined;
|
|
27
|
+
build_model?: string | undefined;
|
|
28
|
+
validate_model?: string | undefined;
|
|
29
|
+
doc_model?: string | undefined;
|
|
30
|
+
doc_review_model?: string | undefined;
|
|
31
|
+
}, context: import("@opencode-ai/plugin/tool").ToolContext): Promise<string>;
|
|
32
|
+
};
|