@syntesseraai/opencode-feature-factory 0.6.20 → 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.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Fan-out helpers for multi-model parallel invocation.
3
+ *
4
+ * Every sub-step runs in an **isolated child session** so that
5
+ * intermediate prompts and responses stay off the main context window.
6
+ * Only the final tool return value surfaces in the parent conversation.
7
+ *
8
+ * Because tools execute synchronously in OpenCode (the tool blocks until
9
+ * `execute()` resolves), we can safely `Promise.all` the SDK calls inside
10
+ * a single tool invocation.
11
+ */
12
+ import type { NamedModel, ModelId } from './types.js';
13
+ /**
14
+ * Opaque client type — the plugin receives `ReturnType<typeof createOpencodeClient>`
15
+ * but we only need `.session.create` and `.session.prompt`.
16
+ * We keep it loosely typed so this module doesn't import the full SDK.
17
+ */
18
+ export type Client = {
19
+ session: {
20
+ create(options: {
21
+ body?: {
22
+ parentID?: string;
23
+ title?: string;
24
+ };
25
+ }): Promise<{
26
+ data?: {
27
+ id: string;
28
+ };
29
+ }>;
30
+ prompt(options: {
31
+ path: {
32
+ id: string;
33
+ };
34
+ body: {
35
+ model?: {
36
+ providerID: string;
37
+ modelID: string;
38
+ };
39
+ agent?: string;
40
+ parts: Array<{
41
+ type: 'text';
42
+ text: string;
43
+ }>;
44
+ };
45
+ }): Promise<{
46
+ data?: {
47
+ info: unknown;
48
+ parts: Array<{
49
+ type: string;
50
+ text?: string;
51
+ [k: string]: unknown;
52
+ }>;
53
+ };
54
+ }>;
55
+ };
56
+ };
57
+ /** Extract all text parts from an SDK prompt response. */
58
+ export declare function extractText(parts: Array<{
59
+ type: string;
60
+ text?: string;
61
+ [k: string]: unknown;
62
+ }>): string;
63
+ export interface FanOutResult {
64
+ tag: string;
65
+ raw: string;
66
+ }
67
+ /**
68
+ * Create one isolated child session per model (in parallel), prompt each,
69
+ * and return the collected text outputs tagged by model.
70
+ *
71
+ * Each model's work runs in its own child session so none of the
72
+ * intermediate outputs pollute the parent context window.
73
+ */
74
+ export declare function fanOut(client: Client, parentSessionId: string, models: readonly NamedModel[], buildPrompt: (tag: string) => string, agent?: string): Promise<FanOutResult[]>;
75
+ /**
76
+ * Prompt in an isolated child session and return the raw text response.
77
+ *
78
+ * This keeps the sub-step off the parent's context window.
79
+ */
80
+ export declare function promptSession(client: Client, parentSessionId: string, prompt: string, options?: {
81
+ model?: ModelId;
82
+ agent?: string;
83
+ title?: string;
84
+ }): Promise<string>;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Fan-out helpers for multi-model parallel invocation.
3
+ *
4
+ * Every sub-step runs in an **isolated child session** so that
5
+ * intermediate prompts and responses stay off the main context window.
6
+ * Only the final tool return value surfaces in the parent conversation.
7
+ *
8
+ * Because tools execute synchronously in OpenCode (the tool blocks until
9
+ * `execute()` resolves), we can safely `Promise.all` the SDK calls inside
10
+ * a single tool invocation.
11
+ */
12
+ // ---------------------------------------------------------------------------
13
+ // Text extraction helper
14
+ // ---------------------------------------------------------------------------
15
+ /** Extract all text parts from an SDK prompt response. */
16
+ export function extractText(parts) {
17
+ return parts
18
+ .filter((p) => p.type === 'text' && typeof p.text === 'string')
19
+ .map((p) => p.text)
20
+ .join('\n');
21
+ }
22
+ // ---------------------------------------------------------------------------
23
+ // Isolated session helper
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * Create an isolated child session, send a prompt, and return the text.
27
+ *
28
+ * The child session is parented to `parentSessionId` so it appears in the
29
+ * session tree but its messages do **not** pollute the parent context window.
30
+ */
31
+ async function promptInChildSession(client, parentSessionId, prompt, options) {
32
+ // 1. Create an isolated child session
33
+ const session = await client.session.create({
34
+ body: {
35
+ parentID: parentSessionId,
36
+ title: options?.title,
37
+ },
38
+ });
39
+ const childId = session.data?.id;
40
+ if (!childId) {
41
+ throw new Error('Failed to create child session');
42
+ }
43
+ // 2. Send the prompt to the child session
44
+ const response = await client.session.prompt({
45
+ path: { id: childId },
46
+ body: {
47
+ model: options?.model,
48
+ agent: options?.agent,
49
+ parts: [{ type: 'text', text: prompt }],
50
+ },
51
+ });
52
+ // 3. Extract and return the text
53
+ return extractText(response.data?.parts ?? []);
54
+ }
55
+ /**
56
+ * Create one isolated child session per model (in parallel), prompt each,
57
+ * and return the collected text outputs tagged by model.
58
+ *
59
+ * Each model's work runs in its own child session so none of the
60
+ * intermediate outputs pollute the parent context window.
61
+ */
62
+ export async function fanOut(client, parentSessionId, models, buildPrompt, agent) {
63
+ const results = await Promise.all(models.map(async (nm) => {
64
+ const raw = await promptInChildSession(client, parentSessionId, buildPrompt(nm.tag), {
65
+ model: nm.model,
66
+ agent,
67
+ title: `ff-fanout-${nm.tag}`,
68
+ });
69
+ return { tag: nm.tag, raw };
70
+ }));
71
+ return results;
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // Single prompt helper (isolated)
75
+ // ---------------------------------------------------------------------------
76
+ /**
77
+ * Prompt in an isolated child session and return the raw text response.
78
+ *
79
+ * This keeps the sub-step off the parent's context window.
80
+ */
81
+ export async function promptSession(client, parentSessionId, prompt, options) {
82
+ return promptInChildSession(client, parentSessionId, prompt, options);
83
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Deterministic gate evaluation functions.
3
+ *
4
+ * These replace the LLM-evaluated gate commands with code that applies
5
+ * threshold logic directly. The LLM still produces the synthesis; the
6
+ * gate decision is computed here.
7
+ */
8
+ import type { GateResult, ConsensusPlan, ReviewSynthesis, DocReview } from './types.js';
9
+ /**
10
+ * Evaluate the planning consensus gate.
11
+ *
12
+ * - `>=75` consensus score → APPROVED
13
+ * - `50-74` → REWORK
14
+ * - `<50` → BLOCKED
15
+ */
16
+ export declare function evaluatePlanningGate(consensus: ConsensusPlan): GateResult;
17
+ /**
18
+ * Evaluate the review approval gate.
19
+ *
20
+ * - confidence >=95 AND unresolvedIssues == 0 → APPROVED
21
+ * - iteration >= maxIterations → ESCALATE
22
+ * - otherwise → REWORK
23
+ */
24
+ export declare function evaluateReviewGate(synthesis: ReviewSynthesis, iteration: number, maxIterations?: number): GateResult;
25
+ /**
26
+ * Evaluate the documentation approval gate.
27
+ *
28
+ * - confidence >95, no change requested, 0 unresolved → APPROVED
29
+ * - iteration >= maxIterations → ESCALATE
30
+ * - otherwise → REWORK
31
+ */
32
+ export declare function evaluateDocGate(review: DocReview, iteration: number, maxIterations?: number): GateResult;
33
+ /**
34
+ * Evaluate the mini-loop implementation gate.
35
+ *
36
+ * - confidence >95, no change requested, 0 blocking issues → APPROVED
37
+ * - iteration >= maxIterations → ESCALATE
38
+ * - otherwise → REWORK
39
+ */
40
+ export declare function evaluateMiniLoopImplGate(review: {
41
+ confidence: number;
42
+ changeRequested: boolean;
43
+ unresolvedIssues: number;
44
+ reworkInstructions?: string;
45
+ }, iteration: number, maxIterations?: number): GateResult;
46
+ /**
47
+ * Evaluate the mini-loop documentation gate.
48
+ * Same thresholds as the pipeline doc gate.
49
+ */
50
+ export declare const evaluateMiniLoopDocGate: typeof evaluateDocGate;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Deterministic gate evaluation functions.
3
+ *
4
+ * These replace the LLM-evaluated gate commands with code that applies
5
+ * threshold logic directly. The LLM still produces the synthesis; the
6
+ * gate decision is computed here.
7
+ */
8
+ // ---------------------------------------------------------------------------
9
+ // Planning gate
10
+ // ---------------------------------------------------------------------------
11
+ /**
12
+ * Evaluate the planning consensus gate.
13
+ *
14
+ * - `>=75` consensus score → APPROVED
15
+ * - `50-74` → REWORK
16
+ * - `<50` → BLOCKED
17
+ */
18
+ export function evaluatePlanningGate(consensus) {
19
+ if (consensus.consensusScore >= 75) {
20
+ return { decision: 'APPROVED' };
21
+ }
22
+ if (consensus.consensusScore >= 50) {
23
+ return {
24
+ decision: 'REWORK',
25
+ feedback: `Consensus score ${consensus.consensusScore}/100. Divergent elements:\n${consensus.divergentElements}\nOpen questions:\n${consensus.openQuestions}`,
26
+ };
27
+ }
28
+ return {
29
+ decision: 'BLOCKED',
30
+ reason: `Consensus score too low (${consensus.consensusScore}/100). Open questions:\n${consensus.openQuestions}`,
31
+ };
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // Review gate
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * Evaluate the review approval gate.
38
+ *
39
+ * - confidence >=95 AND unresolvedIssues == 0 → APPROVED
40
+ * - iteration >= maxIterations → ESCALATE
41
+ * - otherwise → REWORK
42
+ */
43
+ export function evaluateReviewGate(synthesis, iteration, maxIterations = 10) {
44
+ if (synthesis.overallConfidence >= 95 && synthesis.unresolvedIssues === 0) {
45
+ return { decision: 'APPROVED' };
46
+ }
47
+ if (iteration >= maxIterations) {
48
+ return {
49
+ decision: 'ESCALATE',
50
+ reason: `Max review iterations reached (${maxIterations}). Confidence: ${synthesis.overallConfidence}, unresolved: ${synthesis.unresolvedIssues}`,
51
+ };
52
+ }
53
+ return {
54
+ decision: 'REWORK',
55
+ feedback: synthesis.reworkInstructions ??
56
+ `Confidence ${synthesis.overallConfidence}/100 with ${synthesis.unresolvedIssues} unresolved issues.`,
57
+ };
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // Documentation gate
61
+ // ---------------------------------------------------------------------------
62
+ /**
63
+ * Evaluate the documentation approval gate.
64
+ *
65
+ * - confidence >95, no change requested, 0 unresolved → APPROVED
66
+ * - iteration >= maxIterations → ESCALATE
67
+ * - otherwise → REWORK
68
+ */
69
+ export function evaluateDocGate(review, iteration, maxIterations = 5) {
70
+ if (review.verdict === 'APPROVED' && review.unresolvedIssues === 0 && review.confidence > 95) {
71
+ return { decision: 'APPROVED' };
72
+ }
73
+ if (iteration >= maxIterations) {
74
+ return {
75
+ decision: 'ESCALATE',
76
+ reason: `Max doc iterations reached (${maxIterations}). Confidence: ${review.confidence}, unresolved: ${review.unresolvedIssues}`,
77
+ };
78
+ }
79
+ return {
80
+ decision: 'REWORK',
81
+ feedback: review.reworkInstructions ??
82
+ `Documentation review verdict: ${review.verdict}. ${review.unresolvedIssues} unresolved issues.`,
83
+ };
84
+ }
85
+ // ---------------------------------------------------------------------------
86
+ // Mini-loop implementation gate
87
+ // ---------------------------------------------------------------------------
88
+ /**
89
+ * Evaluate the mini-loop implementation gate.
90
+ *
91
+ * - confidence >95, no change requested, 0 blocking issues → APPROVED
92
+ * - iteration >= maxIterations → ESCALATE
93
+ * - otherwise → REWORK
94
+ */
95
+ export function evaluateMiniLoopImplGate(review, iteration, maxIterations = 10) {
96
+ if (review.confidence > 95 && !review.changeRequested && review.unresolvedIssues === 0) {
97
+ return { decision: 'APPROVED' };
98
+ }
99
+ if (iteration >= maxIterations) {
100
+ return {
101
+ decision: 'ESCALATE',
102
+ reason: `Max mini-loop iterations reached (${maxIterations}). Confidence: ${review.confidence}, unresolved: ${review.unresolvedIssues}`,
103
+ };
104
+ }
105
+ return {
106
+ decision: 'REWORK',
107
+ feedback: review.reworkInstructions ??
108
+ `Change requested: ${review.changeRequested}, confidence: ${review.confidence}, unresolved: ${review.unresolvedIssues}`,
109
+ };
110
+ }
111
+ // ---------------------------------------------------------------------------
112
+ // Mini-loop documentation gate
113
+ // ---------------------------------------------------------------------------
114
+ /**
115
+ * Evaluate the mini-loop documentation gate.
116
+ * Same thresholds as the pipeline doc gate.
117
+ */
118
+ export const evaluateMiniLoopDocGate = evaluateDocGate;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared workflow orchestration utilities.
3
+ *
4
+ * Re-exports the fan-out, gate, and type modules as a single convenient
5
+ * entry-point for tool implementations.
6
+ */
7
+ export { fanOut, promptSession, extractText, type Client } from './fan-out.js';
8
+ export { evaluatePlanningGate, evaluateReviewGate, evaluateDocGate, evaluateMiniLoopImplGate, evaluateMiniLoopDocGate, } from './gate-evaluator.js';
9
+ export * from './types.js';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared workflow orchestration utilities.
3
+ *
4
+ * Re-exports the fan-out, gate, and type modules as a single convenient
5
+ * entry-point for tool implementations.
6
+ */
7
+ export { fanOut, promptSession, extractText } from './fan-out.js';
8
+ export { evaluatePlanningGate, evaluateReviewGate, evaluateDocGate, evaluateMiniLoopImplGate, evaluateMiniLoopDocGate, } from './gate-evaluator.js';
9
+ export * from './types.js';
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Workflow type definitions for the Feature Factory pipeline and mini-loop.
3
+ *
4
+ * These types represent the structured data that flows between phases
5
+ * of the workflow, replacing the untyped $RESULT[name] string interpolation
6
+ * used by the subtask2 command approach.
7
+ */
8
+ /** A model identifier split into provider and model parts. */
9
+ export interface ModelId {
10
+ providerID: string;
11
+ modelID: string;
12
+ }
13
+ /** Named model configuration used for fan-out. */
14
+ export interface NamedModel {
15
+ /** Human label used in prompts and logs (e.g. "opus", "gemini", "codex") */
16
+ tag: string;
17
+ /** Provider/model split consumed by the SDK. */
18
+ model: ModelId;
19
+ }
20
+ export declare const PLANNING_MODELS: readonly NamedModel[];
21
+ export declare const REVIEW_MODELS: readonly NamedModel[];
22
+ export declare const ORCHESTRATOR_MODEL: ModelId;
23
+ export declare const BUILD_MODEL: ModelId;
24
+ export declare const DOC_MODEL: ModelId;
25
+ export declare const VALIDATE_MODEL: ModelId;
26
+ export declare const DOC_REVIEW_MODEL: ModelId;
27
+ export type GateDecision = 'APPROVED' | 'REWORK' | 'BLOCKED' | 'ESCALATE';
28
+ export interface GateResult {
29
+ decision: GateDecision;
30
+ /** Feedback for the next loop iteration when decision is REWORK. */
31
+ feedback?: string;
32
+ /** Reason the gate blocked or escalated. */
33
+ reason?: string;
34
+ }
35
+ export interface PlanProposal {
36
+ tag: string;
37
+ requirementsSummary: string;
38
+ architectureValidation: string;
39
+ implementationSteps: string;
40
+ risksAndMitigations: string;
41
+ testingStrategy: string;
42
+ /** The full raw text returned by the model (kept for transparency). */
43
+ raw: string;
44
+ }
45
+ export interface ConsensusPlan {
46
+ consensusScore: number;
47
+ agreedElements: string;
48
+ divergentElements: string;
49
+ synthesizedPlan: string;
50
+ openQuestions: string;
51
+ raw: string;
52
+ }
53
+ export interface PlanningPhaseResult {
54
+ proposals: PlanProposal[];
55
+ consensus: ConsensusPlan;
56
+ gate: GateResult;
57
+ /** The final plan text accepted by the gate (only set when APPROVED). */
58
+ finalPlan?: string;
59
+ }
60
+ export interface TaskBreakdown {
61
+ taskId: string;
62
+ title: string;
63
+ description: string;
64
+ targetFiles: string[];
65
+ dependencies: string[];
66
+ acceptanceCriteria: string;
67
+ }
68
+ export interface ValidatedBatch {
69
+ batchId: string;
70
+ tasks: TaskBreakdown[];
71
+ parallelizable: boolean;
72
+ architecturalRisks: string[];
73
+ }
74
+ export interface ImplementationReport {
75
+ filesChanged: string[];
76
+ testsRun: string[];
77
+ testsPassed: boolean;
78
+ openIssues: string[];
79
+ raw: string;
80
+ }
81
+ export interface BuildPhaseResult {
82
+ tasks: TaskBreakdown[];
83
+ batches: ValidatedBatch[];
84
+ implementation: ImplementationReport;
85
+ }
86
+ export interface ReviewReport {
87
+ tag: string;
88
+ findings: string;
89
+ confidence: number;
90
+ raw: string;
91
+ }
92
+ export interface ReviewSynthesis {
93
+ overallConfidence: number;
94
+ consolidatedFindings: string;
95
+ unresolvedIssues: number;
96
+ verdict: 'APPROVED' | 'REWORK_REQUIRED';
97
+ reworkInstructions?: string;
98
+ raw: string;
99
+ }
100
+ export interface ReviewPhaseResult {
101
+ reviews: ReviewReport[];
102
+ synthesis: ReviewSynthesis;
103
+ gate: GateResult;
104
+ }
105
+ export interface DocUpdate {
106
+ filesChanged: string[];
107
+ rationale: string;
108
+ raw: string;
109
+ }
110
+ export interface DocReview {
111
+ verdict: 'APPROVED' | 'REWORK_REQUIRED';
112
+ unresolvedIssues: number;
113
+ confidence: number;
114
+ reworkInstructions?: string;
115
+ raw: string;
116
+ }
117
+ export interface DocPhaseResult {
118
+ update: DocUpdate;
119
+ review: DocReview;
120
+ gate: GateResult;
121
+ }
122
+ export interface PipelineState {
123
+ requirements: string;
124
+ planning?: PlanningPhaseResult;
125
+ build?: BuildPhaseResult;
126
+ review?: ReviewPhaseResult;
127
+ documentation?: DocPhaseResult;
128
+ }
129
+ export interface MiniLoopState {
130
+ requirements: string;
131
+ implementation?: ImplementationReport;
132
+ review?: {
133
+ confidence: number;
134
+ changeRequested: boolean;
135
+ unresolvedIssues: number;
136
+ reworkInstructions?: string;
137
+ raw: string;
138
+ };
139
+ documentation?: DocPhaseResult;
140
+ }
141
+ /** Parse "provider/model" string into a ModelId. */
142
+ export declare function parseModelString(s: string): ModelId;
143
+ /**
144
+ * Parse a comma-separated list of `tag:provider/model` entries into NamedModel[].
145
+ *
146
+ * Example input: `"opus:anthropic/claude-opus-4-6, gemini:google/gemini-2.5-pro"`
147
+ */
148
+ export declare function parseNamedModels(s: string): NamedModel[];
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Workflow type definitions for the Feature Factory pipeline and mini-loop.
3
+ *
4
+ * These types represent the structured data that flows between phases
5
+ * of the workflow, replacing the untyped $RESULT[name] string interpolation
6
+ * used by the subtask2 command approach.
7
+ */
8
+ // ---------------------------------------------------------------------------
9
+ // Default model roster
10
+ // ---------------------------------------------------------------------------
11
+ export const PLANNING_MODELS = [
12
+ { tag: 'opus', model: { providerID: 'anthropic', modelID: 'claude-opus-4-6' } },
13
+ { tag: 'gemini', model: { providerID: 'opencode', modelID: 'gemini-3.1-pro' } },
14
+ { tag: 'codex', model: { providerID: 'openai', modelID: 'gpt-5.3-codex' } },
15
+ ];
16
+ export const REVIEW_MODELS = PLANNING_MODELS;
17
+ export const ORCHESTRATOR_MODEL = {
18
+ providerID: 'openai',
19
+ modelID: 'gpt-5.4',
20
+ };
21
+ export const BUILD_MODEL = {
22
+ providerID: 'openai',
23
+ modelID: 'gpt-5.3-codex',
24
+ };
25
+ export const DOC_MODEL = BUILD_MODEL;
26
+ export const VALIDATE_MODEL = {
27
+ providerID: 'opencode',
28
+ modelID: 'gemini-3.1-pro',
29
+ };
30
+ export const DOC_REVIEW_MODEL = VALIDATE_MODEL;
31
+ // ---------------------------------------------------------------------------
32
+ // Helpers
33
+ // ---------------------------------------------------------------------------
34
+ /** Parse "provider/model" string into a ModelId. */
35
+ export function parseModelString(s) {
36
+ const slash = s.indexOf('/');
37
+ if (slash === -1)
38
+ return { providerID: s, modelID: s };
39
+ return { providerID: s.slice(0, slash), modelID: s.slice(slash + 1) };
40
+ }
41
+ /**
42
+ * Parse a comma-separated list of `tag:provider/model` entries into NamedModel[].
43
+ *
44
+ * Example input: `"opus:anthropic/claude-opus-4-6, gemini:google/gemini-2.5-pro"`
45
+ */
46
+ export function parseNamedModels(s) {
47
+ return s
48
+ .split(',')
49
+ .map((entry) => entry.trim())
50
+ .filter((entry) => entry.length > 0)
51
+ .map((entry) => {
52
+ const colon = entry.indexOf(':');
53
+ if (colon === -1) {
54
+ // No tag — use the model id as the tag
55
+ const model = parseModelString(entry);
56
+ return { tag: model.modelID, model };
57
+ }
58
+ const tag = entry.slice(0, colon).trim();
59
+ const model = parseModelString(entry.slice(colon + 1).trim());
60
+ return { tag, model };
61
+ });
62
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@syntesseraai/opencode-feature-factory",
4
- "version": "0.6.20",
4
+ "version": "0.7.0",
5
5
  "type": "module",
6
6
  "description": "OpenCode plugin for Feature Factory agents - provides sub-agents and skills for validation, review, security, and architecture assessment",
7
7
  "license": "MIT",