@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 +3 -3
- package/README.md +14 -2
- package/agents/building.md +9 -3
- package/agents/documenting.md +9 -3
- package/agents/feature-factory.md +34 -0
- package/agents/reviewing.md +23 -1
- package/dist/auto-handoff.d.ts +22 -0
- package/dist/auto-handoff.js +195 -0
- package/dist/index.js +16 -1
- package/package.json +2 -4
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` -> `
|
|
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/
|
|
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 `
|
|
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/
|
|
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:
|
package/agents/building.md
CHANGED
|
@@ -43,13 +43,19 @@ You are the building specialist.
|
|
|
43
43
|
|
|
44
44
|
## Editing Workflow (Required)
|
|
45
45
|
|
|
46
|
-
Prefer
|
|
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
|
|
52
|
-
5.
|
|
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
|
|
package/agents/documenting.md
CHANGED
|
@@ -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
|
|
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
|
|
58
|
-
7.
|
|
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`.
|
package/agents/reviewing.md
CHANGED
|
@@ -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/
|
|
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
|
-
|
|
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.
|
|
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
|
-
"@
|
|
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",
|