opencode-conductor-cdd-plugin 1.0.0-beta.19 → 1.0.0-beta.21
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 +19 -3
- package/dist/prompts/strategies/delegate.md +124 -10
- package/dist/prompts/strategies/manual.md +138 -6
- package/dist/test/integration/omo3-delegation.test.d.ts +1 -0
- package/dist/test/integration/omo3-delegation.test.js +581 -0
- package/dist/tools/delegate.d.ts +12 -0
- package/dist/tools/delegate.js +82 -33
- package/dist/utils/configDetection.d.ts +15 -0
- package/dist/utils/configDetection.js +38 -9
- package/dist/utils/configDetection.test.js +320 -8
- package/dist/utils/documentGeneration.d.ts +3 -0
- package/dist/utils/documentGeneration.js +29 -9
- package/dist/utils/interactiveMenu.test.js +5 -0
- package/dist/utils/languageSupport.d.ts +5 -0
- package/dist/utils/languageSupport.js +163 -0
- package/dist/utils/languageSupport.test.d.ts +1 -0
- package/dist/utils/languageSupport.test.js +158 -0
- package/dist/utils/synergyState.test.js +17 -3
- package/package.json +3 -2
package/dist/tools/delegate.js
CHANGED
|
@@ -1,45 +1,94 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import { detectCDDConfig, getAvailableOMOAgents } from "../utils/configDetection.js";
|
|
3
|
+
import { resolveAgentForDelegation } from "../utils/synergyDelegation.js";
|
|
4
|
+
/**
|
|
5
|
+
* Creates a delegation tool that follows the OMO 3.0 agent invocation pattern.
|
|
6
|
+
*
|
|
7
|
+
* This implementation uses the synchronous prompt() API pattern:
|
|
8
|
+
* 1. Create a child session with parentID
|
|
9
|
+
* 2. Send prompt with agent specification and tool restrictions
|
|
10
|
+
* 3. Poll for completion by checking when session becomes idle
|
|
11
|
+
* 4. Extract and return the final response
|
|
12
|
+
*
|
|
13
|
+
* Based on OMO 3.0 call_omo_agent implementation:
|
|
14
|
+
* https://github.com/code-yeongyu/oh-my-opencode/blob/main/src/tools/call-omo-agent/tools.ts
|
|
15
|
+
*/
|
|
2
16
|
export function createDelegationTool(ctx) {
|
|
3
17
|
return tool({
|
|
4
|
-
description: "Delegate a specific task to a specialized subagent",
|
|
18
|
+
description: "Delegate a specific task to a specialized subagent using OMO 3.0 invocation pattern",
|
|
5
19
|
args: {
|
|
6
20
|
task_description: tool.schema.string().describe("Summary of the work"),
|
|
7
|
-
subagent_type: tool.schema.string().describe("The name of the agent to call"),
|
|
21
|
+
subagent_type: tool.schema.string().describe("The name of the agent to call (e.g., explore, oracle, librarian)"),
|
|
8
22
|
prompt: tool.schema.string().describe("Detailed instructions for the subagent"),
|
|
9
23
|
},
|
|
10
24
|
async execute(args, toolContext) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
body: {
|
|
25
|
-
agent: args.subagent_type,
|
|
26
|
-
tools: {
|
|
27
|
-
"cdd_delegate": false,
|
|
25
|
+
try {
|
|
26
|
+
const config = detectCDDConfig();
|
|
27
|
+
const availableAgents = getAvailableOMOAgents();
|
|
28
|
+
const delegationResult = resolveAgentForDelegation(args.subagent_type, config.synergyFramework, availableAgents);
|
|
29
|
+
if (!delegationResult.success) {
|
|
30
|
+
return `Cannot delegate to '${args.subagent_type}': ${delegationResult.reason}\n\nFalling back to @cdd for manual implementation.`;
|
|
31
|
+
}
|
|
32
|
+
const resolvedAgentName = delegationResult.resolvedAgent;
|
|
33
|
+
// 1. Create a sub-session linked to the current one
|
|
34
|
+
const createResult = await ctx.client.session.create({
|
|
35
|
+
body: {
|
|
36
|
+
parentID: toolContext.sessionID,
|
|
37
|
+
title: `${args.task_description} (@${resolvedAgentName})`,
|
|
28
38
|
},
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
});
|
|
40
|
+
if (createResult.error) {
|
|
41
|
+
return `Error creating session: ${createResult.error}`;
|
|
42
|
+
}
|
|
43
|
+
const sessionID = createResult.data.id;
|
|
44
|
+
await ctx.client.session.prompt({
|
|
45
|
+
path: { id: sessionID },
|
|
46
|
+
body: {
|
|
47
|
+
agent: resolvedAgentName,
|
|
48
|
+
tools: {
|
|
49
|
+
cdd_delegate: false,
|
|
50
|
+
cdd_bg_task: false,
|
|
51
|
+
},
|
|
52
|
+
parts: [{ type: "text", text: args.prompt }],
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const MAX_POLL_TIME_MS = 5 * 60 * 1000;
|
|
56
|
+
const POLL_INTERVAL_MS = 2000;
|
|
57
|
+
const startTime = Date.now();
|
|
58
|
+
while (Date.now() - startTime < MAX_POLL_TIME_MS) {
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
60
|
+
try {
|
|
61
|
+
const statusResult = await ctx.client.session.status();
|
|
62
|
+
const sessionStatus = statusResult.data?.[sessionID];
|
|
63
|
+
if (sessionStatus?.type === "idle") {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (statusError) {
|
|
68
|
+
console.warn("[CDD Delegate] Status check failed:", statusError);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const messagesResult = await ctx.client.session.messages({
|
|
72
|
+
path: { id: sessionID },
|
|
73
|
+
});
|
|
74
|
+
if (messagesResult.error) {
|
|
75
|
+
return `Error fetching messages: ${messagesResult.error}`;
|
|
76
|
+
}
|
|
77
|
+
const assistantMessages = (messagesResult.data || [])
|
|
78
|
+
.filter((m) => m.info.role === "assistant");
|
|
79
|
+
if (assistantMessages.length === 0) {
|
|
80
|
+
return `No response from agent ${resolvedAgentName}`;
|
|
81
|
+
}
|
|
82
|
+
const lastMessage = assistantMessages[assistantMessages.length - 1];
|
|
83
|
+
const responseText = lastMessage.parts
|
|
84
|
+
.filter((p) => p.type === "text")
|
|
85
|
+
.map((p) => p.text)
|
|
86
|
+
.join("\n") || "No response.";
|
|
87
|
+
return `${responseText}\n\n<task_metadata>\nsession_id: ${sessionID}\nagent: ${resolvedAgentName}\nrequested: ${args.subagent_type}\n</task_metadata>`;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return `Error during delegation: ${error.message || String(error)}`;
|
|
91
|
+
}
|
|
43
92
|
},
|
|
44
93
|
});
|
|
45
94
|
}
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
export type SynergyFramework = 'none' | 'oh-my-opencode' | 'oh-my-opencode-slim';
|
|
2
|
+
/**
|
|
3
|
+
* Result of CDD configuration detection across multiple config files.
|
|
4
|
+
* Checks ~/.config/opencode/{opencode.json, oh-my-opencode.json, oh-my-opencode-slim.json}
|
|
5
|
+
*/
|
|
2
6
|
export interface ConfigDetectionResult {
|
|
7
|
+
/** Whether CDD agent is configured in opencode.json */
|
|
3
8
|
hasCDDInOpenCode: boolean;
|
|
9
|
+
/** Whether CDD agent is configured in oh-my-opencode.json */
|
|
4
10
|
hasCDDInOMO: boolean;
|
|
11
|
+
/** Whether CDD agent is configured in oh-my-opencode-slim.json */
|
|
12
|
+
hasCDDInSlim: boolean;
|
|
13
|
+
/** Whether synergy mode should be activated (true if any synergy framework detected) */
|
|
5
14
|
synergyActive: boolean;
|
|
15
|
+
/** The CDD model string extracted from configs (priority: slim > OMO > opencode) */
|
|
6
16
|
cddModel?: string;
|
|
17
|
+
/** Which synergy framework to use (priority: slim > OMO > none) */
|
|
7
18
|
synergyFramework: SynergyFramework;
|
|
19
|
+
/** Available agents from slim config (filtered by disabled_agents) */
|
|
8
20
|
slimAgents?: string[];
|
|
21
|
+
/** Available agents from OMO config (filtered by disabled_agents) */
|
|
22
|
+
omoAgents?: string[];
|
|
9
23
|
}
|
|
10
24
|
export declare function detectCDDConfig(): ConfigDetectionResult;
|
|
25
|
+
export declare function getAvailableOMOAgents(): string[];
|
|
@@ -8,17 +8,27 @@ export function detectCDDConfig() {
|
|
|
8
8
|
const slimJsonPath = join(opencodeConfigDir, "oh-my-opencode-slim.json");
|
|
9
9
|
let hasCDDInOpenCode = false;
|
|
10
10
|
let hasCDDInOMO = false;
|
|
11
|
+
let hasCDDInSlim = false;
|
|
11
12
|
let cddModel;
|
|
12
13
|
let synergyFramework = 'none';
|
|
13
14
|
let slimAgents;
|
|
15
|
+
let omoAgents;
|
|
14
16
|
// Check oh-my-opencode-slim.json first (highest priority for synergy)
|
|
15
17
|
if (existsSync(slimJsonPath)) {
|
|
16
18
|
try {
|
|
17
19
|
const config = JSON.parse(readFileSync(slimJsonPath, "utf-8"));
|
|
18
20
|
// Check if config is not empty and has actual content
|
|
19
21
|
if (config && Object.keys(config).length > 0) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
// Check for CDD agent in slim config (strict detection)
|
|
23
|
+
if (config.agents && config.agents.cdd) {
|
|
24
|
+
hasCDDInSlim = true;
|
|
25
|
+
synergyFramework = 'oh-my-opencode-slim';
|
|
26
|
+
// Extract model from slim config (priority over OMO and opencode.json)
|
|
27
|
+
if (config.agents.cdd.model) {
|
|
28
|
+
cddModel = config.agents.cdd.model;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Extract available agents (filter out disabled ones) regardless of CDD presence
|
|
22
32
|
const coreAgents = ['explorer', 'librarian', 'oracle', 'designer'];
|
|
23
33
|
const disabledAgents = new Set(config.disabled_agents ?? []);
|
|
24
34
|
slimAgents = coreAgents.filter(agent => !disabledAgents.has(agent));
|
|
@@ -28,30 +38,37 @@ export function detectCDDConfig() {
|
|
|
28
38
|
// Silently fail on parse errors
|
|
29
39
|
}
|
|
30
40
|
}
|
|
31
|
-
// Check oh-my-opencode.json (only if slim
|
|
32
|
-
if (
|
|
41
|
+
// Check oh-my-opencode.json (only if slim doesn't have CDD)
|
|
42
|
+
if (existsSync(omoJsonPath)) {
|
|
33
43
|
try {
|
|
34
44
|
const config = JSON.parse(readFileSync(omoJsonPath, "utf-8"));
|
|
35
45
|
if (config.agents && config.agents.cdd) {
|
|
36
46
|
hasCDDInOMO = true;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
// Only activate OMO synergy if slim doesn't have CDD (slim takes priority)
|
|
48
|
+
if (synergyFramework === 'none') {
|
|
49
|
+
synergyFramework = 'oh-my-opencode';
|
|
50
|
+
}
|
|
51
|
+
// Extract model from oh-my-opencode.json (only if not already set by slim)
|
|
52
|
+
if (!cddModel && config.agents.cdd.model) {
|
|
40
53
|
cddModel = config.agents.cdd.model;
|
|
41
54
|
}
|
|
55
|
+
// Extract available OMO agents (filter out disabled ones)
|
|
56
|
+
const allConfiguredAgents = Object.keys(config.agents || {});
|
|
57
|
+
const disabledAgents = new Set(config.disabled_agents ?? []);
|
|
58
|
+
omoAgents = allConfiguredAgents.filter(agent => !disabledAgents.has(agent));
|
|
42
59
|
}
|
|
43
60
|
}
|
|
44
61
|
catch (e) {
|
|
45
62
|
// Silently fail on parse errors
|
|
46
63
|
}
|
|
47
64
|
}
|
|
48
|
-
// Check opencode.json (fallback if model not found in OMO)
|
|
65
|
+
// Check opencode.json (fallback if model not found in slim or OMO)
|
|
49
66
|
if (existsSync(opencodeJsonPath)) {
|
|
50
67
|
try {
|
|
51
68
|
const config = JSON.parse(readFileSync(opencodeJsonPath, "utf-8"));
|
|
52
69
|
if (config.agent && config.agent.cdd) {
|
|
53
70
|
hasCDDInOpenCode = true;
|
|
54
|
-
// Only use this model if we didn't find one in oh-my-opencode.json
|
|
71
|
+
// Only use this model if we didn't find one in slim or oh-my-opencode.json
|
|
55
72
|
if (!cddModel && config.agent.cdd.model) {
|
|
56
73
|
cddModel = config.agent.cdd.model;
|
|
57
74
|
}
|
|
@@ -64,9 +81,21 @@ export function detectCDDConfig() {
|
|
|
64
81
|
return {
|
|
65
82
|
hasCDDInOpenCode,
|
|
66
83
|
hasCDDInOMO,
|
|
84
|
+
hasCDDInSlim,
|
|
67
85
|
synergyActive: synergyFramework !== 'none',
|
|
68
86
|
cddModel,
|
|
69
87
|
synergyFramework,
|
|
70
88
|
slimAgents,
|
|
89
|
+
omoAgents,
|
|
71
90
|
};
|
|
72
91
|
}
|
|
92
|
+
export function getAvailableOMOAgents() {
|
|
93
|
+
const config = detectCDDConfig();
|
|
94
|
+
if (config.synergyFramework === 'oh-my-opencode-slim' && config.slimAgents) {
|
|
95
|
+
return config.slimAgents;
|
|
96
|
+
}
|
|
97
|
+
if (config.synergyFramework === 'oh-my-opencode' && config.omoAgents) {
|
|
98
|
+
return config.omoAgents;
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import { existsSync, readFileSync } from "fs";
|
|
3
|
-
import { detectCDDConfig } from "./configDetection.js";
|
|
3
|
+
import { detectCDDConfig, getAvailableOMOAgents } from "./configDetection.js";
|
|
4
4
|
vi.mock("fs", () => ({
|
|
5
5
|
existsSync: vi.fn(),
|
|
6
6
|
readFileSync: vi.fn(),
|
|
@@ -119,7 +119,7 @@ describe("configDetection", () => {
|
|
|
119
119
|
});
|
|
120
120
|
// New tests for oh-my-opencode-slim detection
|
|
121
121
|
describe("oh-my-opencode-slim detection", () => {
|
|
122
|
-
it("should detect
|
|
122
|
+
it("should NOT detect synergy when slim config has no cdd agent", () => {
|
|
123
123
|
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
124
124
|
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
125
125
|
if (path === slimJsonPath) {
|
|
@@ -130,11 +130,11 @@ describe("configDetection", () => {
|
|
|
130
130
|
return "";
|
|
131
131
|
});
|
|
132
132
|
const result = detectCDDConfig();
|
|
133
|
-
expect(result.synergyActive).toBe(
|
|
134
|
-
expect(result.synergyFramework).toBe('
|
|
133
|
+
expect(result.synergyActive).toBe(false);
|
|
134
|
+
expect(result.synergyFramework).toBe('none');
|
|
135
135
|
expect(result.slimAgents).toEqual(['explorer', 'librarian', 'oracle', 'designer']);
|
|
136
136
|
});
|
|
137
|
-
it("should prioritize oh-my-opencode
|
|
137
|
+
it("should prioritize oh-my-opencode over slim when slim has no cdd agent", () => {
|
|
138
138
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
139
139
|
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
140
140
|
if (path === slimJsonPath) {
|
|
@@ -146,10 +146,11 @@ describe("configDetection", () => {
|
|
|
146
146
|
return "";
|
|
147
147
|
});
|
|
148
148
|
const result = detectCDDConfig();
|
|
149
|
-
expect(result.synergyFramework).toBe('oh-my-opencode
|
|
149
|
+
expect(result.synergyFramework).toBe('oh-my-opencode');
|
|
150
150
|
expect(result.synergyActive).toBe(true);
|
|
151
|
+
expect(result.cddModel).toBe('model-from-omo');
|
|
151
152
|
});
|
|
152
|
-
it("should filter out disabled agents from slim config", () => {
|
|
153
|
+
it("should filter out disabled agents from slim config (but require cdd for synergy)", () => {
|
|
153
154
|
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
154
155
|
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
155
156
|
if (path === slimJsonPath) {
|
|
@@ -160,7 +161,8 @@ describe("configDetection", () => {
|
|
|
160
161
|
return "";
|
|
161
162
|
});
|
|
162
163
|
const result = detectCDDConfig();
|
|
163
|
-
expect(result.synergyFramework).toBe('
|
|
164
|
+
expect(result.synergyFramework).toBe('none');
|
|
165
|
+
expect(result.synergyActive).toBe(false);
|
|
164
166
|
expect(result.slimAgents).toEqual(['explorer', 'librarian']);
|
|
165
167
|
});
|
|
166
168
|
it("should handle empty oh-my-opencode-slim config", () => {
|
|
@@ -203,4 +205,314 @@ describe("configDetection", () => {
|
|
|
203
205
|
expect(result.synergyActive).toBe(false);
|
|
204
206
|
});
|
|
205
207
|
});
|
|
208
|
+
// New tests for CDD agent detection in oh-my-opencode-slim
|
|
209
|
+
describe("CDD agent detection in oh-my-opencode-slim", () => {
|
|
210
|
+
it("should detect cdd agent in slim config and activate synergy", () => {
|
|
211
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
212
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
213
|
+
if (path === slimJsonPath) {
|
|
214
|
+
return JSON.stringify({
|
|
215
|
+
agents: {
|
|
216
|
+
cdd: { model: "anthropic/claude-3-5-sonnet" },
|
|
217
|
+
designer: { model: "google/gemini-3-flash" }
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return "";
|
|
222
|
+
});
|
|
223
|
+
const result = detectCDDConfig();
|
|
224
|
+
expect(result.hasCDDInSlim).toBe(true);
|
|
225
|
+
expect(result.synergyFramework).toBe('oh-my-opencode-slim');
|
|
226
|
+
expect(result.synergyActive).toBe(true);
|
|
227
|
+
expect(result.cddModel).toBe("anthropic/claude-3-5-sonnet");
|
|
228
|
+
});
|
|
229
|
+
it("should not activate synergy when slim config exists but no cdd agent", () => {
|
|
230
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
231
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
232
|
+
if (path === slimJsonPath) {
|
|
233
|
+
return JSON.stringify({
|
|
234
|
+
agents: {
|
|
235
|
+
designer: { model: "google/gemini-3-flash" }
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return "";
|
|
240
|
+
});
|
|
241
|
+
const result = detectCDDConfig();
|
|
242
|
+
expect(result.hasCDDInSlim).toBe(false);
|
|
243
|
+
expect(result.synergyFramework).toBe('none');
|
|
244
|
+
expect(result.synergyActive).toBe(false);
|
|
245
|
+
});
|
|
246
|
+
it("should not activate synergy when slim config has empty agents object", () => {
|
|
247
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
248
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
249
|
+
if (path === slimJsonPath) {
|
|
250
|
+
return JSON.stringify({ agents: {} });
|
|
251
|
+
}
|
|
252
|
+
return "";
|
|
253
|
+
});
|
|
254
|
+
const result = detectCDDConfig();
|
|
255
|
+
expect(result.hasCDDInSlim).toBe(false);
|
|
256
|
+
expect(result.synergyFramework).toBe('none');
|
|
257
|
+
expect(result.synergyActive).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
it("should handle malformed slim config gracefully with hasCDDInSlim false", () => {
|
|
260
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
261
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
262
|
+
if (path === slimJsonPath) {
|
|
263
|
+
return "invalid json";
|
|
264
|
+
}
|
|
265
|
+
return "";
|
|
266
|
+
});
|
|
267
|
+
const result = detectCDDConfig();
|
|
268
|
+
expect(result.hasCDDInSlim).toBe(false);
|
|
269
|
+
expect(result.synergyFramework).toBe('none');
|
|
270
|
+
expect(result.synergyActive).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
it("should extract cdd model from slim config when present", () => {
|
|
273
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
274
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
275
|
+
if (path === slimJsonPath) {
|
|
276
|
+
return JSON.stringify({
|
|
277
|
+
agents: {
|
|
278
|
+
cdd: { model: "anthropic/claude-3-5-haiku" }
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return "";
|
|
283
|
+
});
|
|
284
|
+
const result = detectCDDConfig();
|
|
285
|
+
expect(result.cddModel).toBe("anthropic/claude-3-5-haiku");
|
|
286
|
+
expect(result.hasCDDInSlim).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
it("should handle cdd agent without model field in slim", () => {
|
|
289
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
290
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
291
|
+
if (path === slimJsonPath) {
|
|
292
|
+
return JSON.stringify({
|
|
293
|
+
agents: {
|
|
294
|
+
cdd: {}
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return "";
|
|
299
|
+
});
|
|
300
|
+
const result = detectCDDConfig();
|
|
301
|
+
expect(result.hasCDDInSlim).toBe(true);
|
|
302
|
+
expect(result.cddModel).toBeUndefined();
|
|
303
|
+
});
|
|
304
|
+
it("should prioritize slim model over oh-my-opencode model when both have cdd", () => {
|
|
305
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
306
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
307
|
+
if (path === slimJsonPath) {
|
|
308
|
+
return JSON.stringify({
|
|
309
|
+
agents: {
|
|
310
|
+
cdd: { model: "model-from-slim" }
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (path === omoJsonPath) {
|
|
315
|
+
return JSON.stringify({
|
|
316
|
+
agents: {
|
|
317
|
+
cdd: { model: "model-from-omo" }
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return "";
|
|
322
|
+
});
|
|
323
|
+
const result = detectCDDConfig();
|
|
324
|
+
expect(result.cddModel).toBe("model-from-slim");
|
|
325
|
+
expect(result.synergyFramework).toBe('oh-my-opencode-slim');
|
|
326
|
+
expect(result.hasCDDInSlim).toBe(true);
|
|
327
|
+
expect(result.hasCDDInOMO).toBe(true);
|
|
328
|
+
});
|
|
329
|
+
it("should select oh-my-opencode when only OMO has cdd", () => {
|
|
330
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
331
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
332
|
+
if (path === slimJsonPath) {
|
|
333
|
+
return JSON.stringify({
|
|
334
|
+
agents: {
|
|
335
|
+
designer: { model: "google/gemini-3-flash" }
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (path === omoJsonPath) {
|
|
340
|
+
return JSON.stringify({
|
|
341
|
+
agents: {
|
|
342
|
+
cdd: { model: "model-from-omo" }
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return "";
|
|
347
|
+
});
|
|
348
|
+
const result = detectCDDConfig();
|
|
349
|
+
expect(result.hasCDDInSlim).toBe(false);
|
|
350
|
+
expect(result.hasCDDInOMO).toBe(true);
|
|
351
|
+
expect(result.synergyFramework).toBe('oh-my-opencode');
|
|
352
|
+
});
|
|
353
|
+
it("should select slim when only slim has cdd", () => {
|
|
354
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
355
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
356
|
+
if (path === slimJsonPath) {
|
|
357
|
+
return JSON.stringify({
|
|
358
|
+
agents: {
|
|
359
|
+
cdd: { model: "model-from-slim" }
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
if (path === omoJsonPath) {
|
|
364
|
+
return JSON.stringify({
|
|
365
|
+
agents: {
|
|
366
|
+
sisyphus: { model: "google/gemini-3-flash" }
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return "";
|
|
371
|
+
});
|
|
372
|
+
const result = detectCDDConfig();
|
|
373
|
+
expect(result.hasCDDInSlim).toBe(true);
|
|
374
|
+
expect(result.hasCDDInOMO).toBe(false);
|
|
375
|
+
expect(result.synergyFramework).toBe('oh-my-opencode-slim');
|
|
376
|
+
});
|
|
377
|
+
it("should not activate synergy when neither framework has cdd", () => {
|
|
378
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
379
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
380
|
+
if (path === slimJsonPath) {
|
|
381
|
+
return JSON.stringify({
|
|
382
|
+
agents: {
|
|
383
|
+
designer: { model: "google/gemini-3-flash" }
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (path === omoJsonPath) {
|
|
388
|
+
return JSON.stringify({
|
|
389
|
+
agents: {
|
|
390
|
+
sisyphus: { model: "google/gemini-3-flash" }
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return "";
|
|
395
|
+
});
|
|
396
|
+
const result = detectCDDConfig();
|
|
397
|
+
expect(result.hasCDDInSlim).toBe(false);
|
|
398
|
+
expect(result.hasCDDInOMO).toBe(false);
|
|
399
|
+
expect(result.synergyFramework).toBe('none');
|
|
400
|
+
expect(result.synergyActive).toBe(false);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
describe("OMO agent availability detection", () => {
|
|
404
|
+
it("should extract available agents from oh-my-opencode config", () => {
|
|
405
|
+
vi.mocked(existsSync).mockImplementation((path) => path === omoJsonPath);
|
|
406
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
407
|
+
if (path === omoJsonPath) {
|
|
408
|
+
return JSON.stringify({
|
|
409
|
+
agents: {
|
|
410
|
+
cdd: { model: "anthropic/claude-3-5-sonnet" },
|
|
411
|
+
sisyphus: { model: "anthropic/claude-3-5-sonnet" },
|
|
412
|
+
explore: { model: "google/gemini-3-flash" },
|
|
413
|
+
oracle: { model: "anthropic/claude-3-5-sonnet" }
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return "";
|
|
418
|
+
});
|
|
419
|
+
const result = detectCDDConfig();
|
|
420
|
+
expect(result.omoAgents).toEqual(['cdd', 'sisyphus', 'explore', 'oracle']);
|
|
421
|
+
});
|
|
422
|
+
it("should filter out disabled agents from oh-my-opencode config", () => {
|
|
423
|
+
vi.mocked(existsSync).mockImplementation((path) => path === omoJsonPath);
|
|
424
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
425
|
+
if (path === omoJsonPath) {
|
|
426
|
+
return JSON.stringify({
|
|
427
|
+
agents: {
|
|
428
|
+
cdd: { model: "anthropic/claude-3-5-sonnet" },
|
|
429
|
+
sisyphus: { model: "anthropic/claude-3-5-sonnet" },
|
|
430
|
+
explore: { model: "google/gemini-3-flash" },
|
|
431
|
+
oracle: { model: "anthropic/claude-3-5-sonnet" }
|
|
432
|
+
},
|
|
433
|
+
disabled_agents: ['explore', 'oracle']
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return "";
|
|
437
|
+
});
|
|
438
|
+
const result = detectCDDConfig();
|
|
439
|
+
expect(result.omoAgents).toEqual(['cdd', 'sisyphus']);
|
|
440
|
+
});
|
|
441
|
+
it("should handle empty agents object in oh-my-opencode config", () => {
|
|
442
|
+
vi.mocked(existsSync).mockImplementation((path) => path === omoJsonPath);
|
|
443
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
444
|
+
if (path === omoJsonPath) {
|
|
445
|
+
return JSON.stringify({
|
|
446
|
+
agents: {}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return "";
|
|
450
|
+
});
|
|
451
|
+
const result = detectCDDConfig();
|
|
452
|
+
expect(result.omoAgents).toBeUndefined();
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
describe("getAvailableOMOAgents", () => {
|
|
456
|
+
it("should return slim agents when slim framework is active", () => {
|
|
457
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
458
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
459
|
+
if (path === slimJsonPath) {
|
|
460
|
+
return JSON.stringify({
|
|
461
|
+
agents: {
|
|
462
|
+
cdd: { model: "anthropic/claude-3-5-sonnet" }
|
|
463
|
+
},
|
|
464
|
+
disabled_agents: ['oracle']
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
return "";
|
|
468
|
+
});
|
|
469
|
+
const agents = getAvailableOMOAgents();
|
|
470
|
+
expect(agents).toEqual(['explorer', 'librarian', 'designer']);
|
|
471
|
+
});
|
|
472
|
+
it("should return omo agents when oh-my-opencode framework is active", () => {
|
|
473
|
+
vi.mocked(existsSync).mockImplementation((path) => path === omoJsonPath);
|
|
474
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
475
|
+
if (path === omoJsonPath) {
|
|
476
|
+
return JSON.stringify({
|
|
477
|
+
agents: {
|
|
478
|
+
cdd: { model: "anthropic/claude-3-5-sonnet" },
|
|
479
|
+
sisyphus: { model: "anthropic/claude-3-5-sonnet" },
|
|
480
|
+
explore: { model: "google/gemini-3-flash" }
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return "";
|
|
485
|
+
});
|
|
486
|
+
const agents = getAvailableOMOAgents();
|
|
487
|
+
expect(agents).toEqual(['cdd', 'sisyphus', 'explore']);
|
|
488
|
+
});
|
|
489
|
+
it("should return empty array when no synergy framework is active", () => {
|
|
490
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
491
|
+
const agents = getAvailableOMOAgents();
|
|
492
|
+
expect(agents).toEqual([]);
|
|
493
|
+
});
|
|
494
|
+
it("should prioritize slim agents over omo agents when both configs exist", () => {
|
|
495
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
496
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
497
|
+
if (path === slimJsonPath) {
|
|
498
|
+
return JSON.stringify({
|
|
499
|
+
agents: {
|
|
500
|
+
cdd: { model: "model-from-slim" }
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (path === omoJsonPath) {
|
|
505
|
+
return JSON.stringify({
|
|
506
|
+
agents: {
|
|
507
|
+
cdd: { model: "model-from-omo" },
|
|
508
|
+
sisyphus: { model: "model" }
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return "";
|
|
513
|
+
});
|
|
514
|
+
const agents = getAvailableOMOAgents();
|
|
515
|
+
expect(agents).toEqual(['explorer', 'librarian', 'oracle', 'designer']);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
206
518
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Question, Section } from './questionGenerator.js';
|
|
2
2
|
import { CodebaseAnalysis } from './codebaseAnalysis.js';
|
|
3
|
+
import { Language } from './languageSupport.js';
|
|
3
4
|
/**
|
|
4
5
|
* Document Generation Module
|
|
5
6
|
*
|
|
@@ -65,6 +66,7 @@ export interface DocumentGenerationOptions {
|
|
|
65
66
|
outputPath: string;
|
|
66
67
|
maxRevisions?: number;
|
|
67
68
|
customInputPrompt?: (question: Question) => Promise<string>;
|
|
69
|
+
language?: Language;
|
|
68
70
|
}
|
|
69
71
|
export interface DocumentGenerationResult {
|
|
70
72
|
success: boolean;
|
|
@@ -76,6 +78,7 @@ export interface DocumentGenerationResult {
|
|
|
76
78
|
export interface PresentQuestionsOptions {
|
|
77
79
|
maxQuestions?: number;
|
|
78
80
|
customInputPrompt?: (question: Question) => Promise<string>;
|
|
81
|
+
language?: Language;
|
|
79
82
|
}
|
|
80
83
|
/**
|
|
81
84
|
* Present questions sequentially to the user
|