opencode-conductor-cdd-plugin 1.0.0-beta.21 → 1.0.0-beta.22
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/dist/index.js +2 -3
- package/dist/index.test.js +5 -0
- package/dist/test/integration/modelConfigIntegration.test.d.ts +1 -0
- package/dist/test/integration/modelConfigIntegration.test.js +222 -0
- package/dist/test/integration/omo3-delegation.test.js +2 -0
- package/dist/test/integration/rebrand.test.js +5 -1
- package/dist/tools/delegate.js +3 -1
- package/dist/utils/agentInitialization.test.d.ts +1 -0
- package/dist/utils/agentInitialization.test.js +262 -0
- package/dist/utils/commandDefaults.d.ts +38 -0
- package/dist/utils/commandDefaults.js +54 -0
- package/dist/utils/commandDefaults.test.d.ts +1 -0
- package/dist/utils/commandDefaults.test.js +101 -0
- package/dist/utils/configDetection.d.ts +13 -0
- package/dist/utils/configDetection.js +87 -0
- package/dist/utils/modelConfigInjection.test.d.ts +1 -0
- package/dist/utils/modelConfigInjection.test.js +137 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import RevertPromptJson from "./prompts/cdd/revert.json" with { type: "json" };
|
|
|
6
6
|
import SetupPromptJson from "./prompts/cdd/setup.json" with { type: "json" };
|
|
7
7
|
import StatusPromptJson from "./prompts/cdd/status.json" with { type: "json" };
|
|
8
8
|
import { detectCDDConfig } from "./utils/configDetection.js";
|
|
9
|
+
import { createCommandDefaults } from "./utils/commandDefaults.js";
|
|
9
10
|
const asPrompt = (prompt) => (typeof prompt === "string" ? prompt : "");
|
|
10
11
|
const asDescription = (description) => typeof description === "string" ? description : undefined;
|
|
11
12
|
export const MyPlugin = async ({ directory, }) => {
|
|
@@ -44,9 +45,7 @@ export const MyPlugin = async ({ directory, }) => {
|
|
|
44
45
|
const configDetection = detectCDDConfig();
|
|
45
46
|
return {
|
|
46
47
|
config: async (_config) => {
|
|
47
|
-
const commandDefaults = configDetection.cddModel
|
|
48
|
-
? { agent: "cdd", model: configDetection.cddModel }
|
|
49
|
-
: { agent: "cdd" };
|
|
48
|
+
const commandDefaults = createCommandDefaults(configDetection.cddModel);
|
|
50
49
|
_config.command = {
|
|
51
50
|
..._config.command,
|
|
52
51
|
"cdd:implement": {
|
package/dist/index.test.js
CHANGED
|
@@ -120,3 +120,8 @@ describe("getFileTreeSummary behavior", () => {
|
|
|
120
120
|
expect(sorted).not.toContain("conductor-cdd/docs");
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
|
+
describe("Plugin Model Configuration Injection", () => {
|
|
124
|
+
it("should declare model injection test suite", () => {
|
|
125
|
+
expect(true).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { detectCDDConfig } from "../../utils/configDetection.js";
|
|
3
|
+
import { createCommandDefaults } from "../../utils/commandDefaults.js";
|
|
4
|
+
/**
|
|
5
|
+
* Phase 3: Integration Testing & End-to-End Verification
|
|
6
|
+
*
|
|
7
|
+
* Tests verify that all CDD commands respect the resolved model configuration
|
|
8
|
+
* in real-world scenarios with different synergy frameworks.
|
|
9
|
+
*
|
|
10
|
+
* Test Strategy:
|
|
11
|
+
* - Uses detectCDDConfig() to resolve the actual config on the test machine
|
|
12
|
+
* - Uses createCommandDefaults() factory to ensure consistent command configuration
|
|
13
|
+
* - Validates that commands receive proper agent and model settings
|
|
14
|
+
* - Covers all CDD commands: setup, newTrack, implement, status, revert
|
|
15
|
+
*/
|
|
16
|
+
describe("Phase 3: Integration Testing & End-to-End Verification", () => {
|
|
17
|
+
describe("End-to-end tests for all CDD commands with model config", () => {
|
|
18
|
+
it("should apply model config to /cdd:setup command defaults", () => {
|
|
19
|
+
const configDetection = detectCDDConfig();
|
|
20
|
+
const commandDefaults = createCommandDefaults(configDetection.cddModel);
|
|
21
|
+
expect(commandDefaults.agent).toBe("cdd");
|
|
22
|
+
if (configDetection.cddModel) {
|
|
23
|
+
expect(commandDefaults).toHaveProperty("model");
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
it("should apply model config to /cdd:newTrack command defaults", () => {
|
|
27
|
+
const configDetection = detectCDDConfig();
|
|
28
|
+
const commandDefaults = createCommandDefaults(configDetection.cddModel);
|
|
29
|
+
expect(commandDefaults.agent).toBe("cdd");
|
|
30
|
+
if (configDetection.cddModel) {
|
|
31
|
+
expect(commandDefaults).toHaveProperty("model");
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
it("should apply model config to /cdd:implement command defaults", () => {
|
|
35
|
+
const configDetection = detectCDDConfig();
|
|
36
|
+
const commandDefaults = createCommandDefaults(configDetection.cddModel);
|
|
37
|
+
expect(commandDefaults.agent).toBe("cdd");
|
|
38
|
+
if (configDetection.cddModel) {
|
|
39
|
+
expect(commandDefaults).toHaveProperty("model");
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
it("should apply model config to /cdd:status command defaults", () => {
|
|
43
|
+
const configDetection = detectCDDConfig();
|
|
44
|
+
const commandDefaults = createCommandDefaults(configDetection.cddModel);
|
|
45
|
+
expect(commandDefaults.agent).toBe("cdd");
|
|
46
|
+
if (configDetection.cddModel) {
|
|
47
|
+
expect(commandDefaults).toHaveProperty("model");
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
it("should apply model config to /cdd:revert command defaults", () => {
|
|
51
|
+
const configDetection = detectCDDConfig();
|
|
52
|
+
const commandDefaults = createCommandDefaults(configDetection.cddModel);
|
|
53
|
+
expect(commandDefaults.agent).toBe("cdd");
|
|
54
|
+
if (configDetection.cddModel) {
|
|
55
|
+
expect(commandDefaults).toHaveProperty("model");
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe("Synergy framework scenarios", () => {
|
|
60
|
+
it("should resolve model when oh-my-opencode-slim is active", () => {
|
|
61
|
+
const config = detectCDDConfig();
|
|
62
|
+
if (config.synergyFramework === "oh-my-opencode-slim") {
|
|
63
|
+
expect(config.hasCDDInSlim).toBe(true);
|
|
64
|
+
if (config.cddModel) {
|
|
65
|
+
expect(config.cddModel).toMatch(/^[a-z0-9\-]+\/[a-z0-9\-\.]+$/i);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
it("should resolve model when oh-my-opencode is active", () => {
|
|
70
|
+
const config = detectCDDConfig();
|
|
71
|
+
if (config.synergyFramework === "oh-my-opencode") {
|
|
72
|
+
expect(config.hasCDDInOMO).toBe(true);
|
|
73
|
+
if (config.cddModel) {
|
|
74
|
+
expect(config.cddModel).toMatch(/^[a-z0-9\-]+\/[a-z0-9\-\.]+$/i);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
it("should resolve model with only opencode.json config", () => {
|
|
79
|
+
const config = detectCDDConfig();
|
|
80
|
+
if (config.hasCDDInOpenCode &&
|
|
81
|
+
!config.hasCDDInSlim &&
|
|
82
|
+
!config.hasCDDInOMO) {
|
|
83
|
+
if (config.cddModel) {
|
|
84
|
+
expect(config.cddModel).toMatch(/^[a-z0-9\-]+\/[a-z0-9\-\.]+$/i);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
it("should handle fallback behavior when no config files exist", () => {
|
|
89
|
+
const config = detectCDDConfig();
|
|
90
|
+
if (!config.hasCDDInSlim && !config.hasCDDInOMO && !config.hasCDDInOpenCode) {
|
|
91
|
+
expect(config.synergyFramework).toBe("none");
|
|
92
|
+
expect(config.cddModel).toBeUndefined();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe("Backward compatibility tests", () => {
|
|
97
|
+
it("should continue to work with projects without model configuration", () => {
|
|
98
|
+
const config = detectCDDConfig();
|
|
99
|
+
if (!config.cddModel) {
|
|
100
|
+
const commandDefaults = { agent: "cdd" };
|
|
101
|
+
expect(commandDefaults.agent).toBe("cdd");
|
|
102
|
+
expect(commandDefaults).not.toHaveProperty("model");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
it("should handle legacy oh-my-opencode config gracefully", () => {
|
|
106
|
+
const config = detectCDDConfig();
|
|
107
|
+
expect(config).toHaveProperty("hasCDDInOMO");
|
|
108
|
+
expect(typeof config.hasCDDInOMO).toBe("boolean");
|
|
109
|
+
});
|
|
110
|
+
it("should have no regression in existing CDD functionality", () => {
|
|
111
|
+
const config = detectCDDConfig();
|
|
112
|
+
expect(config).toHaveProperty("synergyActive");
|
|
113
|
+
expect(config).toHaveProperty("synergyFramework");
|
|
114
|
+
expect(config).toHaveProperty("hasCDDInSlim");
|
|
115
|
+
expect(config).toHaveProperty("hasCDDInOMO");
|
|
116
|
+
expect(config).toHaveProperty("hasCDDInOpenCode");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe("Model priority resolution chain", () => {
|
|
120
|
+
it("should enforce slim > OMO > opencode priority", () => {
|
|
121
|
+
const config = detectCDDConfig();
|
|
122
|
+
if (config.hasCDDInSlim) {
|
|
123
|
+
expect(config.synergyFramework).toBe("oh-my-opencode-slim");
|
|
124
|
+
}
|
|
125
|
+
else if (config.hasCDDInOMO) {
|
|
126
|
+
expect(config.synergyFramework).toBe("oh-my-opencode");
|
|
127
|
+
}
|
|
128
|
+
else if (config.hasCDDInOpenCode) {
|
|
129
|
+
expect(config.synergyFramework).toBe("none");
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
it("should resolve model from highest priority framework available", () => {
|
|
133
|
+
const config = detectCDDConfig();
|
|
134
|
+
if (config.hasCDDInSlim && config.cddModel) {
|
|
135
|
+
expect(config.synergyFramework).toBe("oh-my-opencode-slim");
|
|
136
|
+
}
|
|
137
|
+
else if (config.hasCDDInOMO && config.cddModel) {
|
|
138
|
+
expect(config.synergyFramework).toBe("oh-my-opencode");
|
|
139
|
+
}
|
|
140
|
+
else if (config.hasCDDInOpenCode && config.cddModel) {
|
|
141
|
+
expect(config.synergyFramework).toBe("none");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
it("should not override model from lower priority frameworks", () => {
|
|
145
|
+
const config = detectCDDConfig();
|
|
146
|
+
if (config.hasCDDInSlim && config.cddModel) {
|
|
147
|
+
expect(config.hasCDDInOMO || config.hasCDDInOpenCode).toBeDefined();
|
|
148
|
+
}
|
|
149
|
+
else if (config.hasCDDInOMO && config.cddModel) {
|
|
150
|
+
expect(config.hasCDDInSlim).toBe(false);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe("Multi-framework coexistence", () => {
|
|
155
|
+
it("should detect when multiple synergy frameworks are present", () => {
|
|
156
|
+
const config = detectCDDConfig();
|
|
157
|
+
const frameworkCount = (config.hasCDDInSlim ? 1 : 0) +
|
|
158
|
+
(config.hasCDDInOMO ? 1 : 0) +
|
|
159
|
+
(config.hasCDDInOpenCode ? 1 : 0);
|
|
160
|
+
expect(frameworkCount).toBeGreaterThanOrEqual(0);
|
|
161
|
+
});
|
|
162
|
+
it("should select slim over OMO when both are available", () => {
|
|
163
|
+
const config = detectCDDConfig();
|
|
164
|
+
if (config.hasCDDInSlim && config.hasCDDInOMO) {
|
|
165
|
+
expect(config.synergyFramework).toBe("oh-my-opencode-slim");
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
it("should select OMO over opencode.json when both are available", () => {
|
|
169
|
+
const config = detectCDDConfig();
|
|
170
|
+
if (config.hasCDDInOMO && config.hasCDDInOpenCode && !config.hasCDDInSlim) {
|
|
171
|
+
expect(config.synergyFramework).toBe("oh-my-opencode");
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe("Configuration consistency across all CDD operations", () => {
|
|
176
|
+
it("should maintain consistent model throughout all command lifecycle", () => {
|
|
177
|
+
const config = detectCDDConfig();
|
|
178
|
+
const model = config.cddModel;
|
|
179
|
+
const commands = ["setup", "newTrack", "implement", "status", "revert"];
|
|
180
|
+
commands.forEach((cmd) => {
|
|
181
|
+
const commandDefaults = model
|
|
182
|
+
? { agent: "cdd", model }
|
|
183
|
+
: { agent: "cdd" };
|
|
184
|
+
expect(commandDefaults.agent).toBe("cdd");
|
|
185
|
+
if (model) {
|
|
186
|
+
expect(commandDefaults.model).toBe(model);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
it("should apply model config consistently across all synergy scenarios", () => {
|
|
191
|
+
const config = detectCDDConfig();
|
|
192
|
+
const scenarios = [
|
|
193
|
+
config.hasCDDInSlim,
|
|
194
|
+
config.hasCDDInOMO,
|
|
195
|
+
config.hasCDDInOpenCode,
|
|
196
|
+
];
|
|
197
|
+
scenarios.forEach(() => {
|
|
198
|
+
const commandDefaults = config.cddModel
|
|
199
|
+
? { agent: "cdd", model: config.cddModel }
|
|
200
|
+
: { agent: "cdd" };
|
|
201
|
+
expect(commandDefaults).toBeDefined();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
describe("Edge cases and error handling", () => {
|
|
206
|
+
it("should handle missing config directory gracefully", () => {
|
|
207
|
+
const config = detectCDDConfig();
|
|
208
|
+
expect(config).toBeDefined();
|
|
209
|
+
expect(config.synergyFramework).toBeDefined();
|
|
210
|
+
});
|
|
211
|
+
it("should handle malformed JSON configs gracefully", () => {
|
|
212
|
+
const config = detectCDDConfig();
|
|
213
|
+
expect(config).toBeDefined();
|
|
214
|
+
expect(() => JSON.stringify(config)).not.toThrow();
|
|
215
|
+
});
|
|
216
|
+
it("should provide sensible defaults when config detection fails", () => {
|
|
217
|
+
const config = detectCDDConfig();
|
|
218
|
+
expect(config.synergyFramework).toBeDefined();
|
|
219
|
+
expect(typeof config.synergyActive).toBe("boolean");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -6,6 +6,7 @@ vi.mock('../../utils/configDetection.js', () => ({
|
|
|
6
6
|
synergyActive: true,
|
|
7
7
|
})),
|
|
8
8
|
getAvailableOMOAgents: vi.fn(() => ['sisyphus', 'explore', 'oracle', 'librarian']),
|
|
9
|
+
getAgentModel: vi.fn((agentName) => `anthropic/claude-3-5-sonnet`),
|
|
9
10
|
}));
|
|
10
11
|
vi.mock('../../utils/synergyDelegation.js', () => ({
|
|
11
12
|
resolveAgentForDelegation: vi.fn((agent) => ({
|
|
@@ -103,6 +104,7 @@ describe('OMO 3.0 Delegation Integration Tests', () => {
|
|
|
103
104
|
path: { id: 'session-123' },
|
|
104
105
|
body: {
|
|
105
106
|
agent: 'sisyphus',
|
|
107
|
+
model: 'anthropic/claude-3-5-sonnet',
|
|
106
108
|
tools: {
|
|
107
109
|
cdd_delegate: false,
|
|
108
110
|
cdd_bg_task: false,
|
|
@@ -74,7 +74,11 @@ describe('Rebrand Integration Tests', () => {
|
|
|
74
74
|
it('should use cdd agent for commands', () => {
|
|
75
75
|
const indexPath = path.join(process.cwd(), 'src/index.ts');
|
|
76
76
|
const indexContent = fs.readFileSync(indexPath, 'utf-8');
|
|
77
|
-
expect(indexContent).toContain('
|
|
77
|
+
expect(indexContent).toContain('createCommandDefaults');
|
|
78
|
+
expect(indexContent).toContain('...commandDefaults');
|
|
79
|
+
const commandDefaultsPath = path.join(process.cwd(), 'src/utils/commandDefaults.ts');
|
|
80
|
+
const commandDefaultsContent = fs.readFileSync(commandDefaultsPath, 'utf-8');
|
|
81
|
+
expect(commandDefaultsContent).toContain('agent: "cdd"');
|
|
78
82
|
});
|
|
79
83
|
});
|
|
80
84
|
describe('Utility Files References', () => {
|
package/dist/tools/delegate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
|
-
import { detectCDDConfig, getAvailableOMOAgents } from "../utils/configDetection.js";
|
|
2
|
+
import { detectCDDConfig, getAvailableOMOAgents, getAgentModel } from "../utils/configDetection.js";
|
|
3
3
|
import { resolveAgentForDelegation } from "../utils/synergyDelegation.js";
|
|
4
4
|
/**
|
|
5
5
|
* Creates a delegation tool that follows the OMO 3.0 agent invocation pattern.
|
|
@@ -41,10 +41,12 @@ export function createDelegationTool(ctx) {
|
|
|
41
41
|
return `Error creating session: ${createResult.error}`;
|
|
42
42
|
}
|
|
43
43
|
const sessionID = createResult.data.id;
|
|
44
|
+
const agentModel = getAgentModel(resolvedAgentName);
|
|
44
45
|
await ctx.client.session.prompt({
|
|
45
46
|
path: { id: sessionID },
|
|
46
47
|
body: {
|
|
47
48
|
agent: resolvedAgentName,
|
|
49
|
+
model: agentModel,
|
|
48
50
|
tools: {
|
|
49
51
|
cdd_delegate: false,
|
|
50
52
|
cdd_bg_task: false,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as configDetection from "./configDetection.js";
|
|
3
|
+
/**
|
|
4
|
+
* Phase 2: Agent Initialization with Resolved Model
|
|
5
|
+
*
|
|
6
|
+
* Tests verify that the resolved model configuration is properly
|
|
7
|
+
* passed to @cdd agent initialization and delegated agents.
|
|
8
|
+
*/
|
|
9
|
+
describe("Phase 2: Agent Initialization with Resolved Model", () => {
|
|
10
|
+
let originalEnv;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
originalEnv = { ...process.env };
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
process.env = originalEnv;
|
|
17
|
+
});
|
|
18
|
+
describe("@cdd agent receives correct model from resolved config", () => {
|
|
19
|
+
it("should pass resolved model to agent initialization when oh-my-opencode-slim has model", () => {
|
|
20
|
+
// Arrange
|
|
21
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
22
|
+
// Simulate what the plugin does with the config
|
|
23
|
+
const commandDefaults = configDetectionResult.cddModel
|
|
24
|
+
? { agent: "cdd", model: configDetectionResult.cddModel }
|
|
25
|
+
: { agent: "cdd" };
|
|
26
|
+
// Assert
|
|
27
|
+
if (configDetectionResult.cddModel) {
|
|
28
|
+
expect(commandDefaults).toHaveProperty("model");
|
|
29
|
+
expect(commandDefaults).toMatchObject({
|
|
30
|
+
agent: "cdd",
|
|
31
|
+
model: configDetectionResult.cddModel,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
it("should use fallback defaults when no model is configured", () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
const configDetectionResult = {
|
|
38
|
+
hasCDDInOpenCode: false,
|
|
39
|
+
hasCDDInOMO: false,
|
|
40
|
+
hasCDDInSlim: false,
|
|
41
|
+
synergyActive: false,
|
|
42
|
+
cddModel: undefined,
|
|
43
|
+
synergyFramework: "none",
|
|
44
|
+
};
|
|
45
|
+
// Act - simulate plugin behavior
|
|
46
|
+
const commandDefaults = configDetectionResult.cddModel
|
|
47
|
+
? { agent: "cdd", model: configDetectionResult.cddModel }
|
|
48
|
+
: { agent: "cdd" };
|
|
49
|
+
// Assert
|
|
50
|
+
expect(commandDefaults).toEqual({
|
|
51
|
+
agent: "cdd",
|
|
52
|
+
});
|
|
53
|
+
expect(commandDefaults).not.toHaveProperty("model");
|
|
54
|
+
});
|
|
55
|
+
it("should maintain model throughout all CDD command defaults", () => {
|
|
56
|
+
// Arrange
|
|
57
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
58
|
+
const commandDefaults = configDetectionResult.cddModel
|
|
59
|
+
? { agent: "cdd", model: configDetectionResult.cddModel }
|
|
60
|
+
: { agent: "cdd" };
|
|
61
|
+
// Simulate all CDD commands
|
|
62
|
+
const commands = {
|
|
63
|
+
"cdd:setup": { ...commandDefaults },
|
|
64
|
+
"cdd:newTrack": { ...commandDefaults },
|
|
65
|
+
"cdd:implement": { ...commandDefaults },
|
|
66
|
+
"cdd:status": { ...commandDefaults },
|
|
67
|
+
"cdd:revert": { ...commandDefaults },
|
|
68
|
+
};
|
|
69
|
+
// Assert: all commands should have consistent model config
|
|
70
|
+
Object.entries(commands).forEach(([name, config]) => {
|
|
71
|
+
expect(config).toMatchObject({
|
|
72
|
+
agent: "cdd",
|
|
73
|
+
});
|
|
74
|
+
if (configDetectionResult.cddModel) {
|
|
75
|
+
expect(config).toHaveProperty("model", configDetectionResult.cddModel);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe("Agent initialization handles invalid model config gracefully", () => {
|
|
81
|
+
it("should handle undefined model gracefully", () => {
|
|
82
|
+
// Arrange
|
|
83
|
+
const configDetectionResult = {
|
|
84
|
+
hasCDDInOpenCode: false,
|
|
85
|
+
hasCDDInOMO: false,
|
|
86
|
+
hasCDDInSlim: false,
|
|
87
|
+
synergyActive: false,
|
|
88
|
+
cddModel: undefined,
|
|
89
|
+
synergyFramework: "none",
|
|
90
|
+
};
|
|
91
|
+
// Act
|
|
92
|
+
const commandDefaults = configDetectionResult.cddModel
|
|
93
|
+
? { agent: "cdd", model: configDetectionResult.cddModel }
|
|
94
|
+
: { agent: "cdd" };
|
|
95
|
+
// Assert
|
|
96
|
+
expect(commandDefaults).toBeDefined();
|
|
97
|
+
expect(commandDefaults.agent).toBe("cdd");
|
|
98
|
+
expect(() => {
|
|
99
|
+
if (!commandDefaults.agent)
|
|
100
|
+
throw new Error("Agent not set");
|
|
101
|
+
}).not.toThrow();
|
|
102
|
+
});
|
|
103
|
+
it("should handle empty model string gracefully", () => {
|
|
104
|
+
// Arrange
|
|
105
|
+
const configDetectionResult = {
|
|
106
|
+
hasCDDInOpenCode: false,
|
|
107
|
+
hasCDDInOMO: false,
|
|
108
|
+
hasCDDInSlim: false,
|
|
109
|
+
synergyActive: false,
|
|
110
|
+
cddModel: "",
|
|
111
|
+
synergyFramework: "none",
|
|
112
|
+
};
|
|
113
|
+
// Act
|
|
114
|
+
const commandDefaults = configDetectionResult.cddModel
|
|
115
|
+
? { agent: "cdd", model: configDetectionResult.cddModel }
|
|
116
|
+
: { agent: "cdd" };
|
|
117
|
+
// Assert - empty string is falsy, should fall back
|
|
118
|
+
expect(commandDefaults).not.toHaveProperty("model");
|
|
119
|
+
});
|
|
120
|
+
it("should validate model string format before passing to agent", () => {
|
|
121
|
+
// Arrange
|
|
122
|
+
const validModel = "anthropic/claude-3-5-sonnet";
|
|
123
|
+
const configDefaults = {
|
|
124
|
+
agent: "cdd",
|
|
125
|
+
model: validModel,
|
|
126
|
+
};
|
|
127
|
+
// Assert - model should be a valid string identifier
|
|
128
|
+
if (configDefaults.model) {
|
|
129
|
+
expect(typeof configDefaults.model).toBe("string");
|
|
130
|
+
expect(configDefaults.model).toMatch(/^[a-z0-9\-]+\/[a-z0-9\-]+$/i);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe("Delegated agent model propagation", () => {
|
|
135
|
+
it("should pass model to @explorer agent when configured", () => {
|
|
136
|
+
// Arrange
|
|
137
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
138
|
+
const parentModel = configDetectionResult.cddModel;
|
|
139
|
+
// Act - simulate delegation
|
|
140
|
+
const delegationConfig = parentModel
|
|
141
|
+
? { agent: "explorer", model: parentModel }
|
|
142
|
+
: { agent: "explorer" };
|
|
143
|
+
// Assert
|
|
144
|
+
expect(delegationConfig).toMatchObject({
|
|
145
|
+
agent: "explorer",
|
|
146
|
+
});
|
|
147
|
+
if (parentModel) {
|
|
148
|
+
expect(delegationConfig).toHaveProperty("model", parentModel);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
it("should pass model to @designer agent when configured", () => {
|
|
152
|
+
// Arrange
|
|
153
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
154
|
+
const parentModel = configDetectionResult.cddModel;
|
|
155
|
+
// Act - simulate delegation
|
|
156
|
+
const delegationConfig = parentModel
|
|
157
|
+
? { agent: "designer", model: parentModel }
|
|
158
|
+
: { agent: "designer" };
|
|
159
|
+
// Assert
|
|
160
|
+
expect(delegationConfig).toMatchObject({
|
|
161
|
+
agent: "designer",
|
|
162
|
+
});
|
|
163
|
+
if (parentModel) {
|
|
164
|
+
expect(delegationConfig).toHaveProperty("model", parentModel);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
it("should pass model to @librarian agent when configured", () => {
|
|
168
|
+
// Arrange
|
|
169
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
170
|
+
const parentModel = configDetectionResult.cddModel;
|
|
171
|
+
// Act - simulate delegation
|
|
172
|
+
const delegationConfig = parentModel
|
|
173
|
+
? { agent: "librarian", model: parentModel }
|
|
174
|
+
: { agent: "librarian" };
|
|
175
|
+
// Assert
|
|
176
|
+
expect(delegationConfig).toMatchObject({
|
|
177
|
+
agent: "librarian",
|
|
178
|
+
});
|
|
179
|
+
if (parentModel) {
|
|
180
|
+
expect(delegationConfig).toHaveProperty("model", parentModel);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
it("should pass model to @oracle agent when configured", () => {
|
|
184
|
+
// Arrange
|
|
185
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
186
|
+
const parentModel = configDetectionResult.cddModel;
|
|
187
|
+
// Act - simulate delegation
|
|
188
|
+
const delegationConfig = parentModel
|
|
189
|
+
? { agent: "oracle", model: parentModel }
|
|
190
|
+
: { agent: "oracle" };
|
|
191
|
+
// Assert
|
|
192
|
+
expect(delegationConfig).toMatchObject({
|
|
193
|
+
agent: "oracle",
|
|
194
|
+
});
|
|
195
|
+
if (parentModel) {
|
|
196
|
+
expect(delegationConfig).toHaveProperty("model", parentModel);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
it("should maintain consistent model across delegation chain", () => {
|
|
200
|
+
// Arrange
|
|
201
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
202
|
+
const parentModel = configDetectionResult.cddModel;
|
|
203
|
+
// Act - simulate full delegation chain
|
|
204
|
+
const cddConfig = parentModel
|
|
205
|
+
? { agent: "cdd", model: parentModel }
|
|
206
|
+
: { agent: "cdd" };
|
|
207
|
+
const delegatedAgents = ["explorer", "designer", "librarian", "oracle"];
|
|
208
|
+
const delegationConfigs = delegatedAgents.map((agent) => parentModel
|
|
209
|
+
? { agent: agent, model: parentModel }
|
|
210
|
+
: { agent: agent });
|
|
211
|
+
// Assert - all delegates should have same model as parent
|
|
212
|
+
delegationConfigs.forEach((delegateConfig) => {
|
|
213
|
+
if (parentModel) {
|
|
214
|
+
expect(delegateConfig).toHaveProperty("model", parentModel);
|
|
215
|
+
}
|
|
216
|
+
if (cddConfig && "model" in cddConfig) {
|
|
217
|
+
expect(delegateConfig).toHaveProperty("model", cddConfig.model);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
describe("Synergy framework agent model propagation", () => {
|
|
223
|
+
it("should detect and use slim framework model when active", () => {
|
|
224
|
+
// Arrange
|
|
225
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
226
|
+
// Act
|
|
227
|
+
const hasSlimModel = configDetectionResult.hasCDDInSlim && configDetectionResult.cddModel;
|
|
228
|
+
// Assert
|
|
229
|
+
if (hasSlimModel) {
|
|
230
|
+
expect(configDetectionResult.synergyFramework).toBe("oh-my-opencode-slim");
|
|
231
|
+
expect(configDetectionResult.cddModel).toBeDefined();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
it("should detect and use OMO framework model when slim not active", () => {
|
|
235
|
+
// Arrange
|
|
236
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
237
|
+
// Act
|
|
238
|
+
const hasOmoModel = configDetectionResult.hasCDDInOMO &&
|
|
239
|
+
!configDetectionResult.hasCDDInSlim &&
|
|
240
|
+
configDetectionResult.cddModel;
|
|
241
|
+
// Assert
|
|
242
|
+
if (hasOmoModel) {
|
|
243
|
+
expect(configDetectionResult.synergyFramework).toBe("oh-my-opencode");
|
|
244
|
+
expect(configDetectionResult.cddModel).toBeDefined();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
it("should respect synergy priority when resolving model", () => {
|
|
248
|
+
// Arrange
|
|
249
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
250
|
+
// Act & Assert - verify priority chain
|
|
251
|
+
if (configDetectionResult.hasCDDInSlim) {
|
|
252
|
+
expect(configDetectionResult.synergyFramework).toBe("oh-my-opencode-slim");
|
|
253
|
+
}
|
|
254
|
+
else if (configDetectionResult.hasCDDInOMO) {
|
|
255
|
+
expect(configDetectionResult.synergyFramework).toBe("oh-my-opencode");
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
expect(configDetectionResult.synergyFramework).toBe("none");
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for creating OpenCode command defaults with CDD agent configuration.
|
|
3
|
+
* Handles model resolution and provides type-safe command default objects.
|
|
4
|
+
*/
|
|
5
|
+
export interface CommandDefaults {
|
|
6
|
+
agent: "cdd";
|
|
7
|
+
model?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Creates command defaults for CDD commands with optional model configuration.
|
|
11
|
+
*
|
|
12
|
+
* @param cddModel - The resolved model string from config detection (undefined = use OpenCode default)
|
|
13
|
+
* @returns Command defaults object with agent and optional model
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // With model configured
|
|
17
|
+
* const defaults = createCommandDefaults("anthropic/claude-3-5-sonnet");
|
|
18
|
+
* // { agent: "cdd", model: "anthropic/claude-3-5-sonnet" }
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Without model (fallback to OpenCode default)
|
|
22
|
+
* const defaults = createCommandDefaults(undefined);
|
|
23
|
+
* // { agent: "cdd" }
|
|
24
|
+
*/
|
|
25
|
+
export declare function createCommandDefaults(cddModel: string | undefined): CommandDefaults;
|
|
26
|
+
/**
|
|
27
|
+
* Validates that a model string is properly formatted.
|
|
28
|
+
*
|
|
29
|
+
* @param model - Model string to validate
|
|
30
|
+
* @returns true if valid, false otherwise
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* validateModelString("anthropic/claude-3-5-sonnet") // true
|
|
34
|
+
* validateModelString("google/gemini-flash-1.5") // true
|
|
35
|
+
* validateModelString("") // false
|
|
36
|
+
* validateModelString(" ") // false
|
|
37
|
+
*/
|
|
38
|
+
export declare function validateModelString(model: string | undefined): boolean;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for creating OpenCode command defaults with CDD agent configuration.
|
|
3
|
+
* Handles model resolution and provides type-safe command default objects.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Creates command defaults for CDD commands with optional model configuration.
|
|
7
|
+
*
|
|
8
|
+
* @param cddModel - The resolved model string from config detection (undefined = use OpenCode default)
|
|
9
|
+
* @returns Command defaults object with agent and optional model
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // With model configured
|
|
13
|
+
* const defaults = createCommandDefaults("anthropic/claude-3-5-sonnet");
|
|
14
|
+
* // { agent: "cdd", model: "anthropic/claude-3-5-sonnet" }
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Without model (fallback to OpenCode default)
|
|
18
|
+
* const defaults = createCommandDefaults(undefined);
|
|
19
|
+
* // { agent: "cdd" }
|
|
20
|
+
*/
|
|
21
|
+
export function createCommandDefaults(cddModel) {
|
|
22
|
+
if (typeof cddModel === "string" && cddModel.trim() === "") {
|
|
23
|
+
console.warn("[CDD] Invalid model configuration detected. Model must be a non-empty string. Falling back to OpenCode default.");
|
|
24
|
+
return { agent: "cdd" };
|
|
25
|
+
}
|
|
26
|
+
if (!cddModel) {
|
|
27
|
+
return { agent: "cdd" };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
agent: "cdd",
|
|
31
|
+
model: cddModel,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validates that a model string is properly formatted.
|
|
36
|
+
*
|
|
37
|
+
* @param model - Model string to validate
|
|
38
|
+
* @returns true if valid, false otherwise
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* validateModelString("anthropic/claude-3-5-sonnet") // true
|
|
42
|
+
* validateModelString("google/gemini-flash-1.5") // true
|
|
43
|
+
* validateModelString("") // false
|
|
44
|
+
* validateModelString(" ") // false
|
|
45
|
+
*/
|
|
46
|
+
export function validateModelString(model) {
|
|
47
|
+
if (!model)
|
|
48
|
+
return false;
|
|
49
|
+
if (typeof model !== "string")
|
|
50
|
+
return false;
|
|
51
|
+
if (model.trim() === "")
|
|
52
|
+
return false;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { createCommandDefaults, validateModelString } from './commandDefaults.js';
|
|
3
|
+
describe('commandDefaults', () => {
|
|
4
|
+
describe('createCommandDefaults', () => {
|
|
5
|
+
it('should return defaults with model when valid model provided', () => {
|
|
6
|
+
const result = createCommandDefaults('anthropic/claude-3-5-sonnet');
|
|
7
|
+
expect(result).toEqual({
|
|
8
|
+
agent: 'cdd',
|
|
9
|
+
model: 'anthropic/claude-3-5-sonnet',
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
it('should return defaults without model when undefined provided', () => {
|
|
13
|
+
const result = createCommandDefaults(undefined);
|
|
14
|
+
expect(result).toEqual({
|
|
15
|
+
agent: 'cdd',
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
it('should return defaults without model when empty string provided', () => {
|
|
19
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
20
|
+
const result = createCommandDefaults('');
|
|
21
|
+
expect(result).toEqual({
|
|
22
|
+
agent: 'cdd',
|
|
23
|
+
});
|
|
24
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('[CDD] Invalid model configuration detected'));
|
|
25
|
+
consoleWarnSpy.mockRestore();
|
|
26
|
+
});
|
|
27
|
+
it('should return defaults without model when whitespace-only string provided', () => {
|
|
28
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
29
|
+
const result = createCommandDefaults(' ');
|
|
30
|
+
expect(result).toEqual({
|
|
31
|
+
agent: 'cdd',
|
|
32
|
+
});
|
|
33
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('[CDD] Invalid model configuration detected'));
|
|
34
|
+
consoleWarnSpy.mockRestore();
|
|
35
|
+
});
|
|
36
|
+
it('should handle various valid model formats', () => {
|
|
37
|
+
const testCases = [
|
|
38
|
+
'anthropic/claude-3-5-sonnet',
|
|
39
|
+
'google/gemini-flash-1.5',
|
|
40
|
+
'openai/gpt-4',
|
|
41
|
+
'anthropic/claude-3-opus-20240229',
|
|
42
|
+
];
|
|
43
|
+
testCases.forEach(model => {
|
|
44
|
+
const result = createCommandDefaults(model);
|
|
45
|
+
expect(result.model).toBe(model);
|
|
46
|
+
expect(result.agent).toBe('cdd');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
it('should return object with correct TypeScript type', () => {
|
|
50
|
+
const result = createCommandDefaults('anthropic/claude-3-5-sonnet');
|
|
51
|
+
expect(result.agent).toBe('cdd');
|
|
52
|
+
expect(typeof result.model).toBe('string');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('validateModelString', () => {
|
|
56
|
+
it('should return true for valid model strings', () => {
|
|
57
|
+
expect(validateModelString('anthropic/claude-3-5-sonnet')).toBe(true);
|
|
58
|
+
expect(validateModelString('google/gemini-flash-1.5')).toBe(true);
|
|
59
|
+
expect(validateModelString('openai/gpt-4')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
it('should return false for undefined', () => {
|
|
62
|
+
expect(validateModelString(undefined)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
it('should return false for empty string', () => {
|
|
65
|
+
expect(validateModelString('')).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
it('should return false for whitespace-only string', () => {
|
|
68
|
+
expect(validateModelString(' ')).toBe(false);
|
|
69
|
+
expect(validateModelString('\t\n')).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
it('should return false for non-string types', () => {
|
|
72
|
+
expect(validateModelString(null)).toBe(false);
|
|
73
|
+
expect(validateModelString(123)).toBe(false);
|
|
74
|
+
expect(validateModelString({})).toBe(false);
|
|
75
|
+
expect(validateModelString([])).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('integration with index.ts', () => {
|
|
79
|
+
it('should create command defaults compatible with OpenCode command config', () => {
|
|
80
|
+
const defaults = createCommandDefaults('anthropic/claude-3-5-sonnet');
|
|
81
|
+
const commandConfig = {
|
|
82
|
+
...defaults,
|
|
83
|
+
template: 'Some prompt template',
|
|
84
|
+
description: 'Command description',
|
|
85
|
+
};
|
|
86
|
+
expect(commandConfig.agent).toBe('cdd');
|
|
87
|
+
expect(commandConfig.model).toBe('anthropic/claude-3-5-sonnet');
|
|
88
|
+
expect(commandConfig.template).toBe('Some prompt template');
|
|
89
|
+
});
|
|
90
|
+
it('should work with spread operator for command defaults', () => {
|
|
91
|
+
const defaults = createCommandDefaults('google/gemini-flash-1.5');
|
|
92
|
+
const command = {
|
|
93
|
+
...defaults,
|
|
94
|
+
someOtherProperty: 'value',
|
|
95
|
+
};
|
|
96
|
+
expect(command.agent).toBe('cdd');
|
|
97
|
+
expect(command.model).toBe('google/gemini-flash-1.5');
|
|
98
|
+
expect(command.someOtherProperty).toBe('value');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -23,3 +23,16 @@ export interface ConfigDetectionResult {
|
|
|
23
23
|
}
|
|
24
24
|
export declare function detectCDDConfig(): ConfigDetectionResult;
|
|
25
25
|
export declare function getAvailableOMOAgents(): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Get the configured model for a specific agent from synergy framework config.
|
|
28
|
+
*
|
|
29
|
+
* Priority Order:
|
|
30
|
+
* 1. oh-my-opencode-slim.json → agents.<agentName>.model
|
|
31
|
+
* 2. oh-my-opencode.json → agents.<agentName>.model
|
|
32
|
+
* 3. opencode.json → agent.<agentName>.model
|
|
33
|
+
* 4. undefined (no model configured)
|
|
34
|
+
*
|
|
35
|
+
* @param agentName - Name of the agent (e.g., 'explorer', 'designer', 'cdd')
|
|
36
|
+
* @returns Model string if configured, undefined otherwise
|
|
37
|
+
*/
|
|
38
|
+
export declare function getAgentModel(agentName: string): string | undefined;
|
|
@@ -2,6 +2,38 @@ import { join } from "path";
|
|
|
2
2
|
import { existsSync, readFileSync } from "fs";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
export function detectCDDConfig() {
|
|
5
|
+
/**
|
|
6
|
+
* Detects and resolves CDD model configuration from synergy framework config files.
|
|
7
|
+
*
|
|
8
|
+
* Priority Order (highest to lowest):
|
|
9
|
+
* 1. oh-my-opencode-slim.json → agents.cdd.model
|
|
10
|
+
* 2. oh-my-opencode.json → agents.cdd.model
|
|
11
|
+
* 3. opencode.json → agent.cdd.model
|
|
12
|
+
* 4. No model (undefined)
|
|
13
|
+
*
|
|
14
|
+
* Synergy Framework Selection:
|
|
15
|
+
* - If oh-my-opencode-slim is present with cdd agent → use slim (highest priority)
|
|
16
|
+
* - Else if oh-my-opencode is present with cdd agent → use OMO
|
|
17
|
+
* - Else → no synergy (manual mode)
|
|
18
|
+
*
|
|
19
|
+
* Important: Synergy only activates if the selected framework explicitly
|
|
20
|
+
* has a "cdd" agent configured. File presence alone is insufficient.
|
|
21
|
+
*
|
|
22
|
+
* Example Config (oh-my-opencode-slim.json):
|
|
23
|
+
* {
|
|
24
|
+
* "agents": {
|
|
25
|
+
* "cdd": {
|
|
26
|
+
* "model": "anthropic/claude-3-5-sonnet"
|
|
27
|
+
* },
|
|
28
|
+
* "explorer": {
|
|
29
|
+
* "model": "google/gemini-flash-1.5"
|
|
30
|
+
* }
|
|
31
|
+
* },
|
|
32
|
+
* "disabled_agents": ["librarian"]
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* @returns ConfigDetectionResult with framework info and model string
|
|
36
|
+
*/
|
|
5
37
|
const opencodeConfigDir = join(homedir(), ".config", "opencode");
|
|
6
38
|
const opencodeJsonPath = join(opencodeConfigDir, "opencode.json");
|
|
7
39
|
const omoJsonPath = join(opencodeConfigDir, "oh-my-opencode.json");
|
|
@@ -99,3 +131,58 @@ export function getAvailableOMOAgents() {
|
|
|
99
131
|
}
|
|
100
132
|
return [];
|
|
101
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Get the configured model for a specific agent from synergy framework config.
|
|
136
|
+
*
|
|
137
|
+
* Priority Order:
|
|
138
|
+
* 1. oh-my-opencode-slim.json → agents.<agentName>.model
|
|
139
|
+
* 2. oh-my-opencode.json → agents.<agentName>.model
|
|
140
|
+
* 3. opencode.json → agent.<agentName>.model
|
|
141
|
+
* 4. undefined (no model configured)
|
|
142
|
+
*
|
|
143
|
+
* @param agentName - Name of the agent (e.g., 'explorer', 'designer', 'cdd')
|
|
144
|
+
* @returns Model string if configured, undefined otherwise
|
|
145
|
+
*/
|
|
146
|
+
export function getAgentModel(agentName) {
|
|
147
|
+
const opencodeConfigDir = join(homedir(), ".config", "opencode");
|
|
148
|
+
const opencodeJsonPath = join(opencodeConfigDir, "opencode.json");
|
|
149
|
+
const omoJsonPath = join(opencodeConfigDir, "oh-my-opencode.json");
|
|
150
|
+
const slimJsonPath = join(opencodeConfigDir, "oh-my-opencode-slim.json");
|
|
151
|
+
// Check oh-my-opencode-slim.json first (highest priority)
|
|
152
|
+
if (existsSync(slimJsonPath)) {
|
|
153
|
+
try {
|
|
154
|
+
const config = JSON.parse(readFileSync(slimJsonPath, "utf-8"));
|
|
155
|
+
if (config?.agents?.[agentName]?.model) {
|
|
156
|
+
return config.agents[agentName].model;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
// Silently fail on parse errors
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Check oh-my-opencode.json (second priority)
|
|
164
|
+
if (existsSync(omoJsonPath)) {
|
|
165
|
+
try {
|
|
166
|
+
const config = JSON.parse(readFileSync(omoJsonPath, "utf-8"));
|
|
167
|
+
if (config?.agents?.[agentName]?.model) {
|
|
168
|
+
return config.agents[agentName].model;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
// Silently fail on parse errors
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Check opencode.json (fallback)
|
|
176
|
+
if (existsSync(opencodeJsonPath)) {
|
|
177
|
+
try {
|
|
178
|
+
const config = JSON.parse(readFileSync(opencodeJsonPath, "utf-8"));
|
|
179
|
+
if (config?.agent?.[agentName]?.model) {
|
|
180
|
+
return config.agent[agentName].model;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
// Silently fail on parse errors
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
vi.mock("fs");
|
|
3
|
+
vi.mock("os");
|
|
4
|
+
describe("Model Configuration Injection in Plugin Initialization", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.clearAllMocks();
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.resetModules();
|
|
10
|
+
});
|
|
11
|
+
it("should inject model into command defaults when oh-my-opencode.json has model", async () => {
|
|
12
|
+
const { existsSync: mockExistsSync, readFileSync: mockReadFileSync, readdirSync: mockReaddirSync, statSync: mockStatSync } = await import("fs");
|
|
13
|
+
const { homedir: mockHomedir } = await import("os");
|
|
14
|
+
vi.mocked(mockHomedir).mockReturnValue("/home/testuser");
|
|
15
|
+
vi.mocked(mockExistsSync).mockImplementation((path) => {
|
|
16
|
+
return String(path).includes("oh-my-opencode.json");
|
|
17
|
+
});
|
|
18
|
+
vi.mocked(mockReaddirSync).mockReturnValue([]);
|
|
19
|
+
vi.mocked(mockStatSync).mockReturnValue({ isDirectory: () => false });
|
|
20
|
+
vi.mocked(mockReadFileSync).mockImplementation((path) => {
|
|
21
|
+
if (String(path).includes("oh-my-opencode.json")) {
|
|
22
|
+
return JSON.stringify({
|
|
23
|
+
agents: {
|
|
24
|
+
cdd: { model: "anthropic/claude-3-5-sonnet" },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return "";
|
|
29
|
+
});
|
|
30
|
+
const { MyPlugin } = await import("../index.js");
|
|
31
|
+
const plugin = await MyPlugin({
|
|
32
|
+
directory: "/test/project",
|
|
33
|
+
client: {},
|
|
34
|
+
project: {},
|
|
35
|
+
worktree: {},
|
|
36
|
+
serverUrl: new URL("http://localhost"),
|
|
37
|
+
$: {},
|
|
38
|
+
});
|
|
39
|
+
const config = { command: {} };
|
|
40
|
+
await plugin.config?.(config);
|
|
41
|
+
const setupCmd = config.command["cdd:setup"];
|
|
42
|
+
expect(setupCmd.model).toBe("anthropic/claude-3-5-sonnet");
|
|
43
|
+
expect(setupCmd.agent).toBe("cdd");
|
|
44
|
+
});
|
|
45
|
+
it("should inject model into ALL CDD commands", async () => {
|
|
46
|
+
const { existsSync: mockExistsSync, readFileSync: mockReadFileSync, readdirSync: mockReaddirSync, statSync: mockStatSync } = await import("fs");
|
|
47
|
+
const { homedir: mockHomedir } = await import("os");
|
|
48
|
+
vi.mocked(mockHomedir).mockReturnValue("/home/testuser");
|
|
49
|
+
vi.mocked(mockExistsSync).mockImplementation((path) => {
|
|
50
|
+
const pathStr = String(path);
|
|
51
|
+
return pathStr.includes("opencode.json") && !pathStr.includes("oh-my");
|
|
52
|
+
});
|
|
53
|
+
vi.mocked(mockReaddirSync).mockReturnValue([]);
|
|
54
|
+
vi.mocked(mockStatSync).mockReturnValue({ isDirectory: () => false });
|
|
55
|
+
vi.mocked(mockReadFileSync).mockImplementation((path) => {
|
|
56
|
+
const pathStr = String(path);
|
|
57
|
+
if (pathStr.includes("opencode.json") && !pathStr.includes("oh-my")) {
|
|
58
|
+
return JSON.stringify({
|
|
59
|
+
agent: {
|
|
60
|
+
cdd: { model: "anthropic/claude-3-5-haiku" },
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return "";
|
|
65
|
+
});
|
|
66
|
+
const { MyPlugin } = await import("../index.js");
|
|
67
|
+
const plugin = await MyPlugin({
|
|
68
|
+
directory: "/test/project",
|
|
69
|
+
client: {},
|
|
70
|
+
project: {},
|
|
71
|
+
worktree: {},
|
|
72
|
+
serverUrl: new URL("http://localhost"),
|
|
73
|
+
$: {},
|
|
74
|
+
});
|
|
75
|
+
const config = { command: {} };
|
|
76
|
+
await plugin.config?.(config);
|
|
77
|
+
const cmdConfig = config.command;
|
|
78
|
+
["cdd:setup", "cdd:newTrack", "cdd:implement", "cdd:status", "cdd:revert"].forEach((cmd) => {
|
|
79
|
+
expect(cmdConfig[cmd].model).toBe("anthropic/claude-3-5-haiku");
|
|
80
|
+
expect(cmdConfig[cmd].agent).toBe("cdd");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
it("should prioritize oh-my-opencode-slim > oh-my-opencode > opencode.json", async () => {
|
|
84
|
+
const { existsSync: mockExistsSync, readFileSync: mockReadFileSync, readdirSync: mockReaddirSync, statSync: mockStatSync } = await import("fs");
|
|
85
|
+
const { homedir: mockHomedir } = await import("os");
|
|
86
|
+
vi.mocked(mockHomedir).mockReturnValue("/home/testuser");
|
|
87
|
+
vi.mocked(mockExistsSync).mockReturnValue(true);
|
|
88
|
+
vi.mocked(mockReaddirSync).mockReturnValue([]);
|
|
89
|
+
vi.mocked(mockStatSync).mockReturnValue({ isDirectory: () => false });
|
|
90
|
+
vi.mocked(mockReadFileSync).mockImplementation((path) => {
|
|
91
|
+
const pathStr = String(path);
|
|
92
|
+
if (pathStr.includes("oh-my-opencode-slim.json")) {
|
|
93
|
+
return JSON.stringify({ agents: { cdd: { model: "google/gemini-2-flash" } } });
|
|
94
|
+
}
|
|
95
|
+
if (pathStr.includes("oh-my-opencode.json")) {
|
|
96
|
+
return JSON.stringify({ agents: { cdd: { model: "anthropic/claude-3-5-sonnet" } } });
|
|
97
|
+
}
|
|
98
|
+
if (pathStr.includes("opencode.json")) {
|
|
99
|
+
return JSON.stringify({ agent: { cdd: { model: "anthropic/claude-3-5-haiku" } } });
|
|
100
|
+
}
|
|
101
|
+
return "";
|
|
102
|
+
});
|
|
103
|
+
const { MyPlugin } = await import("../index.js");
|
|
104
|
+
const plugin = await MyPlugin({
|
|
105
|
+
directory: "/test/project",
|
|
106
|
+
client: {},
|
|
107
|
+
project: {},
|
|
108
|
+
worktree: {},
|
|
109
|
+
serverUrl: new URL("http://localhost"),
|
|
110
|
+
$: {},
|
|
111
|
+
});
|
|
112
|
+
const config = { command: {} };
|
|
113
|
+
await plugin.config?.(config);
|
|
114
|
+
expect(config.command["cdd:setup"].model).toBe("google/gemini-2-flash");
|
|
115
|
+
});
|
|
116
|
+
it("should handle missing model config gracefully", async () => {
|
|
117
|
+
const { existsSync: mockExistsSync, readdirSync: mockReaddirSync, statSync: mockStatSync } = await import("fs");
|
|
118
|
+
const { homedir: mockHomedir } = await import("os");
|
|
119
|
+
vi.mocked(mockHomedir).mockReturnValue("/home/testuser");
|
|
120
|
+
vi.mocked(mockExistsSync).mockReturnValue(false);
|
|
121
|
+
vi.mocked(mockReaddirSync).mockReturnValue([]);
|
|
122
|
+
vi.mocked(mockStatSync).mockReturnValue({ isDirectory: () => false });
|
|
123
|
+
const { MyPlugin } = await import("../index.js");
|
|
124
|
+
const plugin = await MyPlugin({
|
|
125
|
+
directory: "/test/project",
|
|
126
|
+
client: {},
|
|
127
|
+
project: {},
|
|
128
|
+
worktree: {},
|
|
129
|
+
serverUrl: new URL("http://localhost"),
|
|
130
|
+
$: {},
|
|
131
|
+
});
|
|
132
|
+
const config = { command: {} };
|
|
133
|
+
await plugin.config?.(config);
|
|
134
|
+
expect(config.command["cdd:setup"].model).toBeUndefined();
|
|
135
|
+
expect(config.command["cdd:setup"].agent).toBe("cdd");
|
|
136
|
+
});
|
|
137
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-conductor-cdd-plugin",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.22",
|
|
4
4
|
"description": "Context-Driven Development (CDD) plugin for OpenCode - Transform your AI coding workflow with structured specifications, plans, and implementation tracking",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|