@syntesseraai/opencode-feature-factory 0.11.10 → 0.11.12

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/README.md CHANGED
@@ -90,12 +90,18 @@ Feature Factory agent assets follow OpenCode granular `permissions` blocks (incl
90
90
 
91
91
  ### Plugin auto-handoff safety net
92
92
 
93
- The plugin includes an auto-handoff hook that continues deterministic next steps by reading explicit machine-readable fields from assistant output and dispatching the extracted prompt as a normal text prompt via `client.session.prompt(...)` while setting the target agent.
93
+ The plugin includes an auto-handoff hook that continues deterministic next steps by reading `RECOMMENDED_NEXT_STEP` from assistant output and dispatching the extracted text as a normal prompt via `client.session.prompt(...)`.
94
94
 
95
95
  - The orchestrator (`@feature-factory`) remains the source of truth for workflow progression.
96
96
  - Auto-handoff is a continuation safety net, not a replacement for orchestrator logic.
97
- - Multi-stage continuation should target `feature-factory`; one-stage follow-up can target a stage agent.
98
- - Dispatch is performed through direct API prompt calls (not slash-command execution), and handoffs are injected as the next user turn for the targeted agent.
97
+ - On `session.idle`, the hook queries `client.session.messages({ path: { id: sessionId } })`, scans messages in reverse order, and parses only the latest assistant message.
98
+ - The hook extracts the next prompt only from these exact machine-readable formats:
99
+ - `RECOMMENDED_NEXT_STEP: "<human-readable next step>"`
100
+ - `RECOMMENDED_NEXT_STEP="<human-readable next step>"`
101
+
102
+ - If the latest assistant message does not include one of those formats, the hook does nothing.
103
+ - The hook does not buffer streaming assistant chunks and does not enforce a fixed per-session handoff cap.
104
+ - Dispatch is performed through direct API prompt calls (not slash-command execution), and the extracted next-step text is injected as the next user turn to `feature-factory`.
99
105
 
100
106
  ## Quick Commands
101
107
 
@@ -2,7 +2,7 @@
2
2
  description: Feature Factory — native stage orchestrator for planning, building, reviewing, and documenting through sub-agents.
3
3
  mode: primary
4
4
  color: '#0fc24e'
5
- model: github-copilot/gpt-5.4-mini
5
+ model: openai/gpt-5.4
6
6
  permissions:
7
7
  write: deny
8
8
  edit: deny
@@ -212,8 +212,8 @@ For each iteration `n`:
212
212
  - After the required user confirmation checkpoint, successful Build always proceeds to Document.
213
213
  - After the required user confirmation checkpoint, successful Document always proceeds to Review.
214
214
  - Build and Document are stage-complete states, not workflow-complete states.
215
- - Therefore, successful Build and successful Document must emit `AUTO_HANDOFF=YES`.
216
- - Successful termination with `AUTO_HANDOFF=NO` is allowed only when Review is approved with no further required work (or when blocked/escalated/waiting on required user input).
215
+ - Therefore, successful Build and successful Document must include a deterministic `RECOMMENDED_NEXT_STEP` so continuation can proceed automatically.
216
+ - Omit `RECOMMENDED_NEXT_STEP` only when the workflow is fully complete, blocked, escalated, or waiting on required user input.
217
217
 
218
218
  ### Direct-execution prohibition
219
219
 
@@ -233,40 +233,36 @@ When a workflow ends (success or escalation), return:
233
233
  5. `OPEN_ISSUES`
234
234
  6. `RECOMMENDED_NEXT_STEP`
235
235
 
236
- `RECOMMENDED_NEXT_STEP` is reserved for true end-of-workflow recommendations only; do not use it to defer unfinished required stages.
236
+ `RECOMMENDED_NEXT_STEP` should provide the exact next action when deterministic continuation is appropriate.
237
237
 
238
- ## Auto-handoff contract (plugin-facing)
238
+ ## Plugin continuation contract
239
239
 
240
- When the next step is deterministic and does not require user input, append this exact block at the end of the response:
240
+ When the next step is deterministic and does not require user input, append exactly one of these lines at the end of the response:
241
241
 
242
242
  ```text
243
- RECOMMENDED_NEXT_STEP=<ACTION> "<human-readable next step>"
244
-
245
- AUTO_HANDOFF=YES|NO
246
- AUTO_HANDOFF_TARGET=feature-factory|building|documenting|reviewing
247
- AUTO_HANDOFF_REASON=CONTINUE_WORKFLOW|REWORK|DOCUMENT|REVIEW
248
- AUTO_HANDOFF_PROMPT<<FF_PROMPT
249
- <exact prompt text for next turn>
250
- FF_PROMPT
243
+ RECOMMENDED_NEXT_STEP: "<human-readable next step>"
244
+ ```
245
+
246
+ or
247
+
248
+ ```text
249
+ RECOMMENDED_NEXT_STEP="<human-readable next step>"
251
250
  ```
252
251
 
253
252
  Rules:
254
253
 
255
- - After the required user confirmation checkpoint, use `AUTO_HANDOFF=YES` whenever the next workflow step is predetermined by the fixed workflow.
254
+ - After the required user confirmation checkpoint, emit `RECOMMENDED_NEXT_STEP` whenever the next workflow step is predetermined by the fixed workflow.
256
255
  - In the fixed workflow, successful Build must always continue to Document.
257
256
  - In the fixed workflow, successful Document must always continue to Review.
258
- - Successful Build and successful Document are stage-complete but not workflow-complete; they must not output `AUTO_HANDOFF=NO`.
259
- - Use `AUTO_HANDOFF_TARGET=feature-factory` when the workflow should continue across multiple stages.
260
- - Use stage targets (`building`, `documenting`, `reviewing`) only for focused one-stage follow-up.
261
- - Never use `AUTO_HANDOFF=YES` before the required user confirmation checkpoint.
262
- - Use `AUTO_HANDOFF=NO` only when the workflow is fully complete, blocked, escalated, or waiting on required user input.
263
- - `RECOMMENDED_NEXT_STEP` must match the machine-readable handoff block semantically.
257
+ - Successful Build and successful Document are stage-complete but not workflow-complete; do not omit `RECOMMENDED_NEXT_STEP` for these outcomes.
258
+ - Never emit `RECOMMENDED_NEXT_STEP` before the required user confirmation checkpoint.
259
+ - Omit `RECOMMENDED_NEXT_STEP` when the workflow is fully complete, blocked, escalated, or waiting on required user input.
264
260
  - 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`.
265
261
 
266
262
  Expected mappings:
267
263
 
268
- - Planning approved + user confirmed -> `AUTO_HANDOFF=YES`, `AUTO_HANDOFF_TARGET=feature-factory`, `AUTO_HANDOFF_REASON=CONTINUE_WORKFLOW`
269
- - Build succeeded -> `AUTO_HANDOFF=YES`, `AUTO_HANDOFF_TARGET=feature-factory`, `AUTO_HANDOFF_REASON=DOCUMENT`
270
- - Document succeeded -> `AUTO_HANDOFF=YES`, `AUTO_HANDOFF_TARGET=feature-factory`, `AUTO_HANDOFF_REASON=REVIEW`
271
- - Review requires changes -> `AUTO_HANDOFF=YES`, `AUTO_HANDOFF_TARGET=feature-factory`, `AUTO_HANDOFF_REASON=REWORK`
272
- - Review approved with no further work -> `AUTO_HANDOFF=NO`
264
+ - Planning approved + user confirmed -> `RECOMMENDED_NEXT_STEP` describes running Build
265
+ - Build succeeded -> `RECOMMENDED_NEXT_STEP` describes running Document
266
+ - Document succeeded -> `RECOMMENDED_NEXT_STEP` describes running Review
267
+ - Review requires changes -> `RECOMMENDED_NEXT_STEP` describes Build rework actions
268
+ - Review approved with no further work -> omit `RECOMMENDED_NEXT_STEP`
@@ -2,7 +2,7 @@
2
2
  description: Creates implementation plans and planning gates for pipeline and ad-hoc work. Uses result-based handoff instead of file artifacts.
3
3
  mode: all
4
4
  color: '#3b82f6'
5
- model: openai/gpt-5.4
5
+ model: openai/gpt-5.4-pro
6
6
  permissions:
7
7
  write: deny
8
8
  edit: deny
@@ -118,28 +118,6 @@ When acting as gate reviewer, output status line exactly:
118
118
  - `REVIEW_GATE=APPROVED|REWORK|ESCALATE`
119
119
  - `DOCUMENTATION_GATE=APPROVED|REWORK|ESCALATE`
120
120
 
121
- ## Auto-handoff output contract (for standalone review runs)
122
-
123
- When a standalone review clearly requires rework and the next action is deterministic, append:
124
-
125
- ```text
126
- RECOMMENDED_NEXT_STEP=REWORK "<specific rework summary>"
127
- AUTO_HANDOFF=YES
128
- AUTO_HANDOFF_TARGET=building
129
- AUTO_HANDOFF_REASON=REWORK
130
- AUTO_HANDOFF_PROMPT<<FF_PROMPT
131
- apply the rework recommendations
132
-
133
- - <specific action item>
134
- FF_PROMPT
135
- ```
136
-
137
- Otherwise emit:
138
-
139
- ```text
140
- AUTO_HANDOFF=NO
141
- ```
142
-
143
121
  ## Operating Mode
144
122
 
145
123
  - Use normalized, resolved handoff context (summary/status/action items/issues plus resolved prior output excerpts) rather than file-based artifacts.
@@ -1,22 +1,10 @@
1
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
2
  type Client = PluginInput['client'];
13
3
  type SessionMetadata = {
14
4
  parentID?: string;
15
5
  };
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;
6
+ export declare function parseRecommendedNextStep(text: string): string | null;
7
+ export declare function fingerprint(sessionId: string, messageId: string, recommendedNextStep: string): string;
20
8
  export declare function getSessionMetadata(client: Client, sessionId: string): Promise<SessionMetadata>;
21
- export declare const AutoHandoffHooksPlugin: Plugin;
9
+ export declare const RecommendedNextStepHooksPlugin: Plugin;
22
10
  export {};
@@ -1,9 +1,6 @@
1
1
  const SERVICE_NAME = 'feature-factory';
2
- const MAX_AUTO_HANDOFFS_PER_SESSION = 5;
3
- const AUTO_HANDOFF_FLAG_RE = /^\s*AUTO_HANDOFF\s*=\s*(YES|NO)\s*$/im;
4
- const AUTO_HANDOFF_TARGET_RE = /^\s*AUTO_HANDOFF_TARGET\s*=\s*(feature-factory|building|documenting|reviewing)\s*$/im;
5
- const AUTO_HANDOFF_REASON_RE = /^\s*AUTO_HANDOFF_REASON\s*=\s*(CONTINUE_WORKFLOW|REWORK|DOCUMENT|REVIEW)\s*$/im;
6
- const AUTO_HANDOFF_PROMPT_RE = /^\s*(?:AUTO_HANDOFF_PROMPT|FF_PROMPT)\s*<<\s*FF_PROMPT\s*\r?\n([\s\S]*?)\r?\n\s*FF_PROMPT\s*$/im;
2
+ const RECOMMENDED_NEXT_STEP_COLON_RE = /^\s*RECOMMENDED_NEXT_STEP\s*:\s*"([^"\r\n]+)"\s*$/im;
3
+ const RECOMMENDED_NEXT_STEP_EQUALS_RE = /^\s*RECOMMENDED_NEXT_STEP\s*=\s*"([^"\r\n]+)"\s*$/im;
7
4
  async function log(client, level, message, extra) {
8
5
  try {
9
6
  await client.app.log({
@@ -19,11 +16,11 @@ async function log(client, level, message, extra) {
19
16
  return undefined;
20
17
  }
21
18
  }
22
- function extractMessageText(info) {
23
- if (!info || typeof info !== 'object') {
19
+ function extractMessageText(message) {
20
+ if (!message || typeof message !== 'object') {
24
21
  return '';
25
22
  }
26
- const parts = info.parts;
23
+ const parts = message.parts;
27
24
  if (!Array.isArray(parts)) {
28
25
  return '';
29
26
  }
@@ -37,34 +34,20 @@ function extractMessageText(info) {
37
34
  })
38
35
  .join('');
39
36
  }
40
- export function parseAutoHandoff(text) {
37
+ export function parseRecommendedNextStep(text) {
41
38
  const normalizedText = text.replace(/\r\n/g, '\n');
42
- const enabledMatch = normalizedText.match(AUTO_HANDOFF_FLAG_RE);
43
- if (!enabledMatch) {
44
- return null;
45
- }
46
- const enabled = enabledMatch[1]?.toUpperCase();
47
- if (enabled === 'NO') {
48
- return { enabled: false };
39
+ const colonMatch = normalizedText.match(RECOMMENDED_NEXT_STEP_COLON_RE);
40
+ if (colonMatch?.[1]) {
41
+ return colonMatch[1].trim() || null;
49
42
  }
50
- const targetMatch = normalizedText.match(AUTO_HANDOFF_TARGET_RE);
51
- const reasonMatch = normalizedText.match(AUTO_HANDOFF_REASON_RE);
52
- const promptMatch = normalizedText.match(AUTO_HANDOFF_PROMPT_RE);
53
- const target = targetMatch?.[1]?.toLowerCase();
54
- const reason = reasonMatch?.[1]?.toUpperCase();
55
- const prompt = promptMatch?.[1]?.trim();
56
- if (!target || !reason || !prompt) {
57
- return null;
43
+ const equalsMatch = normalizedText.match(RECOMMENDED_NEXT_STEP_EQUALS_RE);
44
+ if (equalsMatch?.[1]) {
45
+ return equalsMatch[1].trim() || null;
58
46
  }
59
- return {
60
- enabled: true,
61
- target,
62
- reason,
63
- prompt,
64
- };
47
+ return null;
65
48
  }
66
- export function fingerprint(sessionId, messageId, handoff) {
67
- return [sessionId, messageId, handoff.target, handoff.reason, handoff.prompt].join('::');
49
+ export function fingerprint(sessionId, messageId, recommendedNextStep) {
50
+ return [sessionId, messageId, recommendedNextStep].join('::');
68
51
  }
69
52
  export async function getSessionMetadata(client, sessionId) {
70
53
  try {
@@ -78,8 +61,34 @@ export async function getSessionMetadata(client, sessionId) {
78
61
  return {};
79
62
  }
80
63
  }
81
- export const AutoHandoffHooksPlugin = async ({ client }) => {
82
- const buffers = new Map();
64
+ async function getLatestAssistantMessage(client, sessionId) {
65
+ try {
66
+ const response = await client.session.messages({
67
+ path: { id: sessionId },
68
+ });
69
+ const messages = Array.isArray(response.data) ? response.data : [];
70
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
71
+ const current = messages[index];
72
+ if (!current || typeof current !== 'object') {
73
+ continue;
74
+ }
75
+ const info = current.info;
76
+ if (!info || info.role !== 'assistant' || !info.id) {
77
+ continue;
78
+ }
79
+ return {
80
+ messageId: info.id,
81
+ text: extractMessageText(current),
82
+ };
83
+ }
84
+ return null;
85
+ }
86
+ catch {
87
+ await log(client, 'warn', 'auto-handoff.session-messages-fetch-failed', { sessionId });
88
+ return null;
89
+ }
90
+ }
91
+ export const RecommendedNextStepHooksPlugin = async ({ client }) => {
83
92
  const sessionState = new Map();
84
93
  return {
85
94
  event: async ({ event }) => {
@@ -90,89 +99,44 @@ export const AutoHandoffHooksPlugin = async ({ client }) => {
90
99
  return;
91
100
  }
92
101
  if (event.type === 'session.deleted') {
93
- buffers.delete(sessionId);
94
102
  sessionState.delete(sessionId);
95
103
  return;
96
104
  }
97
- if (event.type === 'message.updated') {
98
- const info = event
99
- .properties?.info;
100
- if (!info || info.role !== 'assistant' || !info.id) {
101
- return;
102
- }
103
- buffers.set(sessionId, {
104
- messageId: info.id,
105
- text: extractMessageText(info),
106
- });
107
- return;
108
- }
109
- if (event.type === 'message.part.updated') {
110
- const state = buffers.get(sessionId);
111
- if (!state) {
112
- return;
113
- }
114
- const props = event.properties;
115
- const part = props?.part;
116
- if (part?.type !== 'text') {
117
- return;
118
- }
119
- if (typeof props?.delta === 'string') {
120
- state.text += props.delta;
121
- }
122
- else if (typeof part.text === 'string') {
123
- state.text = part.text;
124
- }
125
- buffers.set(sessionId, state);
126
- return;
127
- }
128
105
  if (event.type !== 'session.idle') {
129
106
  return;
130
107
  }
131
- const buffer = buffers.get(sessionId);
132
- if (!buffer?.text) {
108
+ const latestAssistantMessage = await getLatestAssistantMessage(client, sessionId);
109
+ if (!latestAssistantMessage?.text) {
133
110
  return;
134
111
  }
135
- const parsed = parseAutoHandoff(buffer.text);
136
- if (!parsed || !parsed.enabled) {
112
+ const recommendedNextStep = parseRecommendedNextStep(latestAssistantMessage.text);
113
+ if (!recommendedNextStep) {
137
114
  return;
138
115
  }
139
116
  const meta = await getSessionMetadata(client, sessionId);
140
117
  const dispatchSessionId = meta.parentID ?? sessionId;
141
- const state = sessionState.get(sessionId) ?? {
142
- count: 0,
143
- fingerprints: new Set(),
144
- };
145
- if (state.count >= MAX_AUTO_HANDOFFS_PER_SESSION) {
146
- await log(client, 'warn', 'auto-handoff.skipped-cap-reached', {
147
- sessionId,
148
- max: MAX_AUTO_HANDOFFS_PER_SESSION,
149
- });
150
- sessionState.set(sessionId, state);
151
- return;
152
- }
153
- const key = fingerprint(sessionId, buffer.messageId, parsed);
118
+ const state = sessionState.get(sessionId) ?? { fingerprints: new Set() };
119
+ const key = fingerprint(sessionId, latestAssistantMessage.messageId, recommendedNextStep);
154
120
  if (state.fingerprints.has(key)) {
155
121
  await log(client, 'debug', 'auto-handoff.skipped-duplicate', { sessionId });
156
122
  return;
157
123
  }
158
124
  state.fingerprints.add(key);
159
- state.count += 1;
160
125
  sessionState.set(sessionId, state);
161
126
  await log(client, 'info', 'auto-handoff.dispatching', {
162
127
  sessionId,
163
128
  dispatchSessionId,
164
- target: parsed.target,
165
- reason: parsed.reason,
129
+ target: 'feature-factory',
166
130
  });
167
131
  try {
168
132
  await client.session.prompt({
169
133
  path: { id: dispatchSessionId },
170
134
  body: {
171
- agent: parsed.target,
135
+ agent: 'feature-factory',
172
136
  parts: [
173
137
  {
174
138
  type: 'text',
175
- text: parsed.prompt,
139
+ text: recommendedNextStep,
176
140
  },
177
141
  ],
178
142
  },
@@ -182,8 +146,7 @@ export const AutoHandoffHooksPlugin = async ({ client }) => {
182
146
  await log(client, 'error', 'auto-handoff.dispatch-failed', {
183
147
  sessionId,
184
148
  dispatchSessionId,
185
- target: parsed.target,
186
- reason: parsed.reason,
149
+ target: 'feature-factory',
187
150
  error: String(error),
188
151
  });
189
152
  }
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { StopQualityGateHooksPlugin } from './stop-quality-gate.js';
2
- import { AutoHandoffHooksPlugin } from './auto-handoff.js';
2
+ import { RecommendedNextStepHooksPlugin } from './auto-handoff.js';
3
3
  import { updateMCPConfig } from './mcp-config.js';
4
4
  import { updateAgentConfig } from './agent-config.js';
5
5
  import { updatePluginConfig } from './plugin-config.js';
@@ -54,7 +54,7 @@ export const FeatureFactoryPlugin = async (input) => {
54
54
  }
55
55
  // Load hooks from the quality gate plugin
56
56
  const qualityGateHooks = await StopQualityGateHooksPlugin(input).catch(() => ({}));
57
- const autoHandoffHooks = await AutoHandoffHooksPlugin(input).catch(() => ({}));
57
+ const autoHandoffHooks = await RecommendedNextStepHooksPlugin(input).catch(() => ({}));
58
58
  // Feature Factory orchestration now runs via native sub-agent handoff in
59
59
  // the feature-factory agent prompt (LLM-driven workflow). ff_* workflow MCP
60
60
  // tools are no longer registered from this plugin.
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.11.10",
4
+ "version": "0.11.12",
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",