@syntesseraai/opencode-feature-factory 0.10.13 → 0.10.14

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 CHANGED
@@ -5,11 +5,11 @@ This file is installed to `~/.config/opencode/AGENTS.md` by `@syntesseraai/openc
5
5
  ## What Feature Factory Provides
6
6
 
7
7
  - Native workflow orchestration through the `feature-factory` agent.
8
- - Default orchestrator model: `feature-factory` -> `openai/gpt-5.4`
8
+ - Default orchestrator model: `feature-factory` -> `github-copilot/gpt-5.4-mini`
9
9
  - Stage sub-agents with default model routing:
10
10
  - `planning` -> `openai/gpt-5.4`
11
11
  - `building` -> `openai/gpt-5.3-codex`
12
- - `reviewing` -> `opencode/claude-opus-4-6`
12
+ - `reviewing` -> `opencode/glm-5.1`
13
13
  - `documenting` -> `opencode/gemini-3.1-pro`
14
14
  - Specialized agents: `feature-factory`, `planning`, `building`, `reviewing`, `documenting`, and `ff-research`.
15
15
  - Quick commands: `/ff-review [optional prompt]`, `/ff-document [optional prompt]`, and `/ff-rework [optional prompt]` (all run as subtasks on the relevant stage agent).
@@ -36,7 +36,7 @@ When work changes behavior, workflows, configuration, operational guidance, or r
36
36
 
37
37
  ## Preferred Tooling Pattern
38
38
 
39
- - Use `morph-mcp_codebase_search` first for semantic codebase discovery.
39
+ - Use `morph-mcp_codebase_search` first for semantic codebase discovery in stage agents; the orchestration-only `feature-factory` agent should delegate discovery work instead of calling it directly.
40
40
  - Use `read`, `glob`, and `grep` for targeted file inspection.
41
41
  - Writable agents should prefer `morph-mcp` `edit_file`, then fallback to native `edit` when needed.
42
42
  - Keep `edit` restricted on read-only agents (`planning`, `reviewing`, `ff-research`).
package/README.md CHANGED
@@ -56,10 +56,10 @@ The plugin no longer exposes `ff_pipeline`, `ff_mini_loop`, or `ff_list_models`
56
56
 
57
57
  Instead, the `feature-factory` primary agent orchestrates workflows natively by delegating to stage sub-agents:
58
58
 
59
- - `feature-factory` (orchestrator) -> default model `openai/gpt-5.4`
59
+ - `feature-factory` (orchestrator) -> default model `github-copilot/gpt-5.4-mini`
60
60
  - `planning` -> default model `openai/gpt-5.4`
61
61
  - `building` -> default model `openai/gpt-5.3-codex`
62
- - `reviewing` -> default model `opencode/claude-opus-4-6`
62
+ - `reviewing` -> default model `opencode/glm-5.1`
63
63
  - `documenting` -> default model `opencode/gemini-3.1-pro`
64
64
 
65
65
  ### Fixed execution path
@@ -76,6 +76,15 @@ Each transition carries forward prior-stage context (summary, gate/verdict, acti
76
76
 
77
77
  Writable stage agents (`building`, `documenting`) prefer Morph MCP `edit_file` and can fallback to native `edit` (and `write` for new files) when needed. Read-only agents keep `edit` disabled.
78
78
 
79
+ ### Plugin auto-handoff safety net
80
+
81
+ The plugin now includes an auto-handoff hook that can continue deterministic next steps by reading explicit machine-readable fields from assistant output and dispatching `client.session.prompt(...)`.
82
+
83
+ - The orchestrator (`@feature-factory`) remains the source of truth for workflow progression.
84
+ - Auto-handoff is a continuation safety net, not a replacement for orchestrator logic.
85
+ - Multi-stage continuation should target `feature-factory`; one-stage follow-up can target a stage agent.
86
+ - Dispatch is performed through direct API prompt calls (not slash-command execution).
87
+
79
88
  ## Quick Commands
80
89
 
81
90
  The plugin installs global custom slash commands in `~/.config/opencode/commands/`:
@@ -83,14 +92,17 @@ The plugin installs global custom slash commands in `~/.config/opencode/commands
83
92
  - `/ff-review [optional prompt]`
84
93
  - Agent: `reviewing`
85
94
  - Subtask: `true` (always runs as a subtask)
95
+ - Mode: manual standalone helper (does not continue the full pipeline automatically)
86
96
  - Default prompt when no argument is provided: `Review the changes so far`
87
97
  - `/ff-document [optional prompt]`
88
98
  - Agent: `documenting`
89
99
  - Subtask: `true` (always runs as a subtask)
100
+ - Mode: manual standalone helper (does not continue the full pipeline automatically)
90
101
  - Default prompt when no argument is provided: `Document all the changes so far`
91
102
  - `/ff-rework [optional prompt]`
92
103
  - Agent: `building`
93
104
  - Subtask: `true` (always runs as a subtask)
105
+ - Mode: manual standalone helper (does not continue the full pipeline automatically)
94
106
  - Default prompt when no argument is provided: `apply the rework recommendations`
95
107
 
96
108
  Examples:
@@ -43,13 +43,19 @@ You are the building specialist.
43
43
 
44
44
  ## Editing Workflow (Required)
45
45
 
46
- Prefer Morph MCP `edit_file` for all implementation edits.
46
+ Prefer the OpenCode `edit_file` tool (Fast Apply) for all implementation edits.
47
+
48
+ Fallback policy:
49
+
50
+ - **Codex models:** use `patch` or `apply_patch`.
51
+ - **Non-Codex models:** use `edit`.
52
+ - **New files (all models):** `write` or `patch` are acceptable.
47
53
 
48
54
  1. **Read before editing** to confirm current behavior and scope.
49
55
  2. **Apply changes with `edit_file`** for targeted replacements/insertions whenever available.
50
56
  3. **Re-read after edits** to verify the applied diff and avoid stale assumptions.
51
- 4. If `edit_file` is unavailable or cannot express the required change, fallback to `edit`.
52
- 5. Use `write` when creating brand-new files.
57
+ 4. If `edit_file` is unavailable or cannot express the required change, choose fallback by model family: Codex → `patch`/`apply_patch`; non-Codex → `edit`.
58
+ 5. For brand-new files, use `write` or `patch`.
53
59
 
54
60
  ## Semantic Code Search
55
61
 
@@ -47,15 +47,21 @@ Always load `ff-documentation-rules` before scoping or editing documentation. Tr
47
47
 
48
48
  ## Editing Workflow (Required)
49
49
 
50
- Prefer Morph MCP `edit_file` for documentation updates.
50
+ Prefer the OpenCode `edit_file` tool (Fast Apply) for documentation updates.
51
+
52
+ Fallback policy:
53
+
54
+ - **Codex models:** use `patch` or `apply_patch`.
55
+ - **Non-Codex models:** use `edit`.
56
+ - **New files (all models):** `write` or `patch` are acceptable.
51
57
 
52
58
  1. **Read the current navigation chain first** — inspect the root `README.md`, `docs/INDEX.md`, relevant nested `docs/**/INDEX.md` files, and affected leaf docs before editing.
53
59
  2. **Verify behavior from code** — use semantic search and targeted reads so the documentation matches what actually ships.
54
60
  3. **Update canonical docs and navigation together** — change the affected leaf docs, every impacted `INDEX.md`, and the root `README.md` when primary documentation entry points or doc structure change.
55
61
  4. **Apply changes with `edit_file`** for precise doc updates whenever available.
56
62
  5. **Re-read after edits** to validate wording, relative links, and cross-doc consistency.
57
- 6. If `edit_file` is unavailable or cannot express the required change, fallback to `edit`.
58
- 7. Use `write` when creating brand-new documentation files.
63
+ 6. If `edit_file` is unavailable or cannot express the required change, choose fallback by model family: Codex → `patch`/`apply_patch`; non-Codex → `edit`.
64
+ 7. For brand-new documentation files, use `write` or `patch`.
59
65
 
60
66
  ## Semantic Code Search
61
67
 
@@ -191,6 +191,13 @@ For each iteration `n`:
191
191
  7. If either gate is not `APPROVED`, route back to Build with consolidated action items from both review outputs.
192
192
  8. If 10 iterations are exhausted, stop and escalate to the user.
193
193
 
194
+ ### Autonomous continuation rule
195
+
196
+ - After the required user confirmation checkpoint is satisfied, do not pause between required stages.
197
+ - Do not ask the user what to do next while required workflow stages remain unfinished.
198
+ - Do not use optional phrasing like "If you want, I can..." for unfinished required workflow steps.
199
+ - If a stage reports partial progress (for example "waiting for tests"), treat it as non-terminal and issue a focused same-stage follow-up immediately.
200
+
194
201
  ### Direct-execution prohibition
195
202
 
196
203
  - Do not substitute manual/orchestrator-only answers for work that should be performed by stage agents.
@@ -208,3 +215,30 @@ When a workflow ends (success or escalation), return:
208
215
  4. `TEST_RESULTS` (if reported)
209
216
  5. `OPEN_ISSUES`
210
217
  6. `RECOMMENDED_NEXT_STEP`
218
+
219
+ `RECOMMENDED_NEXT_STEP` is reserved for true end-of-workflow recommendations only; do not use it to defer unfinished required stages.
220
+
221
+ ## Auto-handoff contract (plugin-facing)
222
+
223
+ When the next step is deterministic and does not require user input, append this exact block at the end of the response:
224
+
225
+ ```text
226
+ RECOMMENDED_NEXT_STEP=<ACTION> "<human-readable next step>"
227
+
228
+ AUTO_HANDOFF=YES|NO
229
+ AUTO_HANDOFF_TARGET=feature-factory|building|documenting|reviewing
230
+ AUTO_HANDOFF_REASON=CONTINUE_WORKFLOW|REWORK|DOCUMENT|REVIEW
231
+ AUTO_HANDOFF_PROMPT<<FF_PROMPT
232
+ <exact prompt text for next turn>
233
+ FF_PROMPT
234
+ ```
235
+
236
+ Rules:
237
+
238
+ - Use `AUTO_HANDOFF=YES` only when the next step is deterministic and should run automatically.
239
+ - Use `AUTO_HANDOFF_TARGET=feature-factory` when the workflow should continue across multiple stages.
240
+ - Use stage targets (`building`, `documenting`, `reviewing`) only for focused one-stage follow-up.
241
+ - Never use `AUTO_HANDOFF=YES` before the required user confirmation checkpoint.
242
+ - If complete, blocked, escalated, or waiting on the user, output `AUTO_HANDOFF=NO`.
243
+ - `RECOMMENDED_NEXT_STEP` must match the machine-readable handoff block semantically.
244
+ - For normal workflow progression, continue by delegating to stage agents directly (`@building`, `@documenting`, `@reviewing`), not by invoking slash commands such as `/ff-review`, `/ff-document`, or `/ff-rework`.
@@ -2,7 +2,7 @@
2
2
  description: Unified validation agent for code and documentation. Performs acceptance, quality, security, and architecture review with context-driven scope.
3
3
  mode: primary
4
4
  color: '#8b5cf6'
5
- model: opencode/claude-opus-4-6
5
+ model: opencode/glm-5.1
6
6
  tools:
7
7
  read: true
8
8
  write: false
@@ -96,6 +96,28 @@ When acting as gate reviewer, output status line exactly:
96
96
  - `REVIEW_GATE=APPROVED|REWORK|ESCALATE`
97
97
  - `DOCUMENTATION_GATE=APPROVED|REWORK|ESCALATE`
98
98
 
99
+ ## Auto-handoff output contract (for standalone review runs)
100
+
101
+ When a standalone review clearly requires rework and the next action is deterministic, append:
102
+
103
+ ```text
104
+ RECOMMENDED_NEXT_STEP=REWORK "<specific rework summary>"
105
+ AUTO_HANDOFF=YES
106
+ AUTO_HANDOFF_TARGET=building
107
+ AUTO_HANDOFF_REASON=REWORK
108
+ AUTO_HANDOFF_PROMPT<<FF_PROMPT
109
+ apply the rework recommendations
110
+
111
+ - <specific action item>
112
+ FF_PROMPT
113
+ ```
114
+
115
+ Otherwise emit:
116
+
117
+ ```text
118
+ AUTO_HANDOFF=NO
119
+ ```
120
+
99
121
  ## Operating Mode
100
122
 
101
123
  - Use result-based handoff (`$RESULT[...]`) rather than file-based artifacts.
@@ -0,0 +1,22 @@
1
+ import { type Plugin, type PluginInput } from '@opencode-ai/plugin';
2
+ type AutoHandoffTarget = 'feature-factory' | 'building' | 'documenting' | 'reviewing';
3
+ type AutoHandoffReason = 'CONTINUE_WORKFLOW' | 'REWORK' | 'DOCUMENT' | 'REVIEW';
4
+ export type ParsedAutoHandoff = {
5
+ enabled: false;
6
+ } | {
7
+ enabled: true;
8
+ target: AutoHandoffTarget;
9
+ reason: AutoHandoffReason;
10
+ prompt: string;
11
+ };
12
+ type Client = PluginInput['client'];
13
+ type SessionMetadata = {
14
+ parentID?: string;
15
+ };
16
+ export declare function parseAutoHandoff(text: string): ParsedAutoHandoff | null;
17
+ export declare function fingerprint(sessionId: string, messageId: string, handoff: Extract<ParsedAutoHandoff, {
18
+ enabled: true;
19
+ }>): string;
20
+ export declare function getSessionMetadata(client: Client, sessionId: string): Promise<SessionMetadata>;
21
+ export declare const AutoHandoffHooksPlugin: Plugin;
22
+ export {};
@@ -0,0 +1,195 @@
1
+ const SERVICE_NAME = 'feature-factory';
2
+ const MAX_AUTO_HANDOFFS_PER_SESSION = 5;
3
+ const AUTO_HANDOFF_FLAG_RE = /^AUTO_HANDOFF=(YES|NO)$/m;
4
+ const AUTO_HANDOFF_TARGET_RE = /^AUTO_HANDOFF_TARGET=(feature-factory|building|documenting|reviewing)$/m;
5
+ const AUTO_HANDOFF_REASON_RE = /^AUTO_HANDOFF_REASON=(CONTINUE_WORKFLOW|REWORK|DOCUMENT|REVIEW)$/m;
6
+ const AUTO_HANDOFF_PROMPT_RE = /AUTO_HANDOFF_PROMPT<<FF_PROMPT\r?\n([\s\S]*?)\r?\nFF_PROMPT/m;
7
+ async function log(client, level, message, extra) {
8
+ try {
9
+ await client.app.log({
10
+ body: {
11
+ service: SERVICE_NAME,
12
+ level,
13
+ message,
14
+ extra,
15
+ },
16
+ });
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ function extractMessageText(info) {
23
+ if (!info || typeof info !== 'object') {
24
+ return '';
25
+ }
26
+ const parts = info.parts;
27
+ if (!Array.isArray(parts)) {
28
+ return '';
29
+ }
30
+ return parts
31
+ .map((part) => {
32
+ if (!part || typeof part !== 'object') {
33
+ return '';
34
+ }
35
+ const typedPart = part;
36
+ return typedPart.type === 'text' && typeof typedPart.text === 'string' ? typedPart.text : '';
37
+ })
38
+ .join('');
39
+ }
40
+ export function parseAutoHandoff(text) {
41
+ const enabledMatch = text.match(AUTO_HANDOFF_FLAG_RE);
42
+ if (!enabledMatch) {
43
+ return null;
44
+ }
45
+ const enabled = enabledMatch[1];
46
+ if (enabled === 'NO') {
47
+ return { enabled: false };
48
+ }
49
+ const targetMatch = text.match(AUTO_HANDOFF_TARGET_RE);
50
+ const reasonMatch = text.match(AUTO_HANDOFF_REASON_RE);
51
+ const promptMatch = text.match(AUTO_HANDOFF_PROMPT_RE);
52
+ const target = targetMatch?.[1];
53
+ const reason = reasonMatch?.[1];
54
+ const prompt = promptMatch?.[1]?.trim();
55
+ if (!target || !reason || !prompt) {
56
+ return null;
57
+ }
58
+ return {
59
+ enabled: true,
60
+ target: target,
61
+ reason: reason,
62
+ prompt,
63
+ };
64
+ }
65
+ export function fingerprint(sessionId, messageId, handoff) {
66
+ return [sessionId, messageId, handoff.target, handoff.reason, handoff.prompt].join('::');
67
+ }
68
+ export async function getSessionMetadata(client, sessionId) {
69
+ try {
70
+ const response = await client.session.get({ path: { id: sessionId } });
71
+ return {
72
+ parentID: response.data?.parentID,
73
+ };
74
+ }
75
+ catch {
76
+ await log(client, 'warn', 'auto-handoff.session-metadata-fetch-failed', { sessionId });
77
+ return {};
78
+ }
79
+ }
80
+ export const AutoHandoffHooksPlugin = async ({ client }) => {
81
+ const buffers = new Map();
82
+ const sessionState = new Map();
83
+ return {
84
+ event: async ({ event }) => {
85
+ const properties = event
86
+ .properties;
87
+ const sessionId = properties?.sessionID ?? properties?.sessionId;
88
+ if (!sessionId) {
89
+ return;
90
+ }
91
+ if (event.type === 'session.deleted') {
92
+ buffers.delete(sessionId);
93
+ sessionState.delete(sessionId);
94
+ return;
95
+ }
96
+ if (event.type === 'message.updated') {
97
+ const info = event
98
+ .properties?.info;
99
+ if (!info || info.role !== 'assistant' || !info.id) {
100
+ return;
101
+ }
102
+ buffers.set(sessionId, {
103
+ messageId: info.id,
104
+ text: extractMessageText(info),
105
+ });
106
+ return;
107
+ }
108
+ if (event.type === 'message.part.updated') {
109
+ const state = buffers.get(sessionId);
110
+ if (!state) {
111
+ return;
112
+ }
113
+ const props = event.properties;
114
+ const part = props?.part;
115
+ if (part?.type !== 'text') {
116
+ return;
117
+ }
118
+ if (typeof props?.delta === 'string') {
119
+ state.text += props.delta;
120
+ }
121
+ else if (typeof part.text === 'string') {
122
+ state.text = part.text;
123
+ }
124
+ buffers.set(sessionId, state);
125
+ return;
126
+ }
127
+ if (event.type !== 'session.idle') {
128
+ return;
129
+ }
130
+ const buffer = buffers.get(sessionId);
131
+ if (!buffer?.text) {
132
+ return;
133
+ }
134
+ const parsed = parseAutoHandoff(buffer.text);
135
+ if (!parsed || !parsed.enabled) {
136
+ return;
137
+ }
138
+ const meta = await getSessionMetadata(client, sessionId);
139
+ if (meta.parentID) {
140
+ await log(client, 'debug', 'auto-handoff.skipped-sub-agent-session', {
141
+ sessionId,
142
+ parentID: meta.parentID,
143
+ });
144
+ return;
145
+ }
146
+ const state = sessionState.get(sessionId) ?? {
147
+ count: 0,
148
+ fingerprints: new Set(),
149
+ };
150
+ if (state.count >= MAX_AUTO_HANDOFFS_PER_SESSION) {
151
+ await log(client, 'warn', 'auto-handoff.skipped-cap-reached', {
152
+ sessionId,
153
+ max: MAX_AUTO_HANDOFFS_PER_SESSION,
154
+ });
155
+ sessionState.set(sessionId, state);
156
+ return;
157
+ }
158
+ const key = fingerprint(sessionId, buffer.messageId, parsed);
159
+ if (state.fingerprints.has(key)) {
160
+ await log(client, 'debug', 'auto-handoff.skipped-duplicate', { sessionId });
161
+ return;
162
+ }
163
+ state.fingerprints.add(key);
164
+ state.count += 1;
165
+ sessionState.set(sessionId, state);
166
+ await log(client, 'info', 'auto-handoff.dispatching', {
167
+ sessionId,
168
+ target: parsed.target,
169
+ reason: parsed.reason,
170
+ });
171
+ try {
172
+ await client.session.prompt({
173
+ path: { id: sessionId },
174
+ body: {
175
+ agent: parsed.target,
176
+ parts: [
177
+ {
178
+ type: 'text',
179
+ text: parsed.prompt,
180
+ },
181
+ ],
182
+ },
183
+ });
184
+ }
185
+ catch (error) {
186
+ await log(client, 'error', 'auto-handoff.dispatch-failed', {
187
+ sessionId,
188
+ target: parsed.target,
189
+ reason: parsed.reason,
190
+ error: String(error),
191
+ });
192
+ }
193
+ },
194
+ };
195
+ };
package/dist/index.js CHANGED
@@ -1,8 +1,20 @@
1
1
  import { StopQualityGateHooksPlugin } from './stop-quality-gate.js';
2
+ import { AutoHandoffHooksPlugin } from './auto-handoff.js';
2
3
  import { updateMCPConfig } from './mcp-config.js';
3
4
  import { updateAgentConfig } from './agent-config.js';
4
5
  import { updatePluginConfig } from './plugin-config.js';
5
6
  import { $ } from 'bun';
7
+ function composeAsyncHandlers(...handlers) {
8
+ const activeHandlers = handlers.filter((handler) => Boolean(handler));
9
+ if (activeHandlers.length === 0) {
10
+ return undefined;
11
+ }
12
+ return (async (...args) => {
13
+ for (const handler of activeHandlers) {
14
+ await handler(...args);
15
+ }
16
+ });
17
+ }
6
18
  /**
7
19
  * Feature Factory Plugin
8
20
  *
@@ -42,10 +54,13 @@ export const FeatureFactoryPlugin = async (input) => {
42
54
  }
43
55
  // Load hooks from the quality gate plugin
44
56
  const qualityGateHooks = await StopQualityGateHooksPlugin(input).catch(() => ({}));
57
+ const autoHandoffHooks = await AutoHandoffHooksPlugin(input).catch(() => ({}));
45
58
  // Feature Factory orchestration now runs via native sub-agent handoff in
46
59
  // the feature-factory agent prompt (LLM-driven workflow). ff_* workflow MCP
47
60
  // tools are no longer registered from this plugin.
48
61
  return {
49
- ...qualityGateHooks,
62
+ event: composeAsyncHandlers(qualityGateHooks.event, autoHandoffHooks.event),
63
+ 'tool.execute.before': composeAsyncHandlers(qualityGateHooks['tool.execute.before'], autoHandoffHooks['tool.execute.before']),
64
+ 'tool.execute.after': composeAsyncHandlers(qualityGateHooks['tool.execute.after'], autoHandoffHooks['tool.execute.after']),
50
65
  };
51
66
  };
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.10.13",
4
+ "version": "0.10.14",
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",
@@ -34,9 +34,7 @@
34
34
  ],
35
35
  "scripts": {},
36
36
  "dependencies": {
37
- "@modelcontextprotocol/sdk": "^1.0.0",
38
- "@opencode-ai/plugin": "^1.1.48",
39
- "glob": "^10.0.0"
37
+ "@opencode-ai/plugin": "^1.1.48"
40
38
  },
41
39
  "devDependencies": {
42
40
  "@types/bun": "^1.2.6",