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.
@@ -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
- // 1. Create a sub-session linked to the current one
12
- const createResult = await ctx.client.session.create({
13
- body: {
14
- parentID: toolContext.sessionID,
15
- title: `${args.task_description} (Delegated to ${args.subagent_type})`,
16
- },
17
- });
18
- if (createResult.error)
19
- return `Error: ${createResult.error}`;
20
- const sessionID = createResult.data.id;
21
- // 2. Send the prompt to the subagent
22
- await ctx.client.session.prompt({
23
- path: { id: sessionID },
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
- parts: [{ type: "text", text: args.prompt }],
30
- },
31
- });
32
- // 3. Fetch and return the assistant's response
33
- const messagesResult = await ctx.client.session.messages({
34
- path: { id: sessionID },
35
- });
36
- const lastMessage = messagesResult.data
37
- ?.filter((m) => m.info.role === "assistant")
38
- .pop();
39
- const responseText = lastMessage?.parts
40
- .filter((p) => p.type === "text")
41
- .map((p) => p.text).join("\n") || "No response.";
42
- return `${responseText}\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`;
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
- synergyFramework = 'oh-my-opencode-slim';
21
- // Extract available agents (filter out disabled ones)
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 is not active)
32
- if (synergyFramework === 'none' && existsSync(omoJsonPath)) {
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
- synergyFramework = 'oh-my-opencode';
38
- // Extract model from oh-my-opencode.json
39
- if (config.agents.cdd.model) {
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 oh-my-opencode-slim when only slim config is present", () => {
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(true);
134
- expect(result.synergyFramework).toBe('oh-my-opencode-slim');
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-slim over oh-my-opencode when both present", () => {
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-slim');
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('oh-my-opencode-slim');
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