opencode-conductor-cdd-plugin 1.0.0-beta.13 → 1.0.0-beta.16
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 +45 -6
- package/dist/prompts/agent/cdd.md +11 -7
- package/dist/test/integration/rebrand.test.d.ts +1 -0
- package/dist/test/integration/rebrand.test.js +126 -0
- package/dist/test/integration/slim-synergy.test.d.ts +1 -0
- package/dist/test/integration/slim-synergy.test.js +246 -0
- package/dist/utils/agentMapping.d.ts +24 -0
- package/dist/utils/agentMapping.js +65 -0
- package/dist/utils/agentMapping.test.d.ts +1 -0
- package/dist/utils/agentMapping.test.js +80 -0
- package/dist/utils/configDetection.d.ts +3 -0
- package/dist/utils/configDetection.js +26 -3
- package/dist/utils/configDetection.test.js +87 -0
- package/dist/utils/synergyDelegation.d.ts +33 -0
- package/dist/utils/synergyDelegation.js +65 -0
- package/dist/utils/synergyDelegation.test.d.ts +1 -0
- package/dist/utils/synergyDelegation.test.js +88 -0
- package/dist/utils/synergyState.d.ts +18 -0
- package/dist/utils/synergyState.js +52 -0
- package/dist/utils/synergyState.test.d.ts +1 -0
- package/dist/utils/synergyState.test.js +185 -0
- package/dist/utils/synergyStatus.d.ts +27 -0
- package/dist/utils/synergyStatus.js +54 -0
- package/dist/utils/synergyStatus.test.d.ts +1 -0
- package/dist/utils/synergyStatus.test.js +143 -0
- package/package.json +1 -1
- package/scripts/postinstall.cjs +5 -5
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { resolveAgentName, getAgentMapping, isAgentAvailable } from './agentMapping.js';
|
|
3
|
+
describe('Agent Mapping', () => {
|
|
4
|
+
describe('resolveAgentName', () => {
|
|
5
|
+
it('should map generic agent names to oh-my-opencode-slim agents', () => {
|
|
6
|
+
expect(resolveAgentName('explore', 'oh-my-opencode-slim')).toBe('explorer');
|
|
7
|
+
expect(resolveAgentName('frontend-ui-ux-engineer', 'oh-my-opencode-slim')).toBe('designer');
|
|
8
|
+
expect(resolveAgentName('document-writer', 'oh-my-opencode-slim')).toBe('librarian');
|
|
9
|
+
expect(resolveAgentName('oracle', 'oh-my-opencode-slim')).toBe('oracle');
|
|
10
|
+
});
|
|
11
|
+
it('should pass through oh-my-opencode-slim native agent names', () => {
|
|
12
|
+
expect(resolveAgentName('explorer', 'oh-my-opencode-slim')).toBe('explorer');
|
|
13
|
+
expect(resolveAgentName('designer', 'oh-my-opencode-slim')).toBe('designer');
|
|
14
|
+
expect(resolveAgentName('librarian', 'oh-my-opencode-slim')).toBe('librarian');
|
|
15
|
+
});
|
|
16
|
+
it('should return null for agents without slim equivalent', () => {
|
|
17
|
+
expect(resolveAgentName('sisyphus', 'oh-my-opencode-slim')).toBe(null);
|
|
18
|
+
});
|
|
19
|
+
it('should return null for unknown agent names in slim framework', () => {
|
|
20
|
+
expect(resolveAgentName('unknown-agent', 'oh-my-opencode-slim')).toBe(null);
|
|
21
|
+
});
|
|
22
|
+
it('should pass through agent names for oh-my-opencode framework', () => {
|
|
23
|
+
expect(resolveAgentName('sisyphus', 'oh-my-opencode')).toBe('sisyphus');
|
|
24
|
+
expect(resolveAgentName('oracle', 'oh-my-opencode')).toBe('oracle');
|
|
25
|
+
expect(resolveAgentName('explore', 'oh-my-opencode')).toBe('explore');
|
|
26
|
+
});
|
|
27
|
+
it('should return null when framework is none', () => {
|
|
28
|
+
expect(resolveAgentName('explore', 'none')).toBe(null);
|
|
29
|
+
expect(resolveAgentName('oracle', 'none')).toBe(null);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('getAgentMapping', () => {
|
|
33
|
+
it('should return the complete mapping for oh-my-opencode-slim', () => {
|
|
34
|
+
const mapping = getAgentMapping('oh-my-opencode-slim');
|
|
35
|
+
expect(mapping).toHaveProperty('explore', 'explorer');
|
|
36
|
+
expect(mapping).toHaveProperty('explorer', 'explorer');
|
|
37
|
+
expect(mapping).toHaveProperty('frontend-ui-ux-engineer', 'designer');
|
|
38
|
+
expect(mapping).toHaveProperty('designer', 'designer');
|
|
39
|
+
expect(mapping).toHaveProperty('document-writer', 'librarian');
|
|
40
|
+
expect(mapping).toHaveProperty('librarian', 'librarian');
|
|
41
|
+
expect(mapping).toHaveProperty('oracle', 'oracle');
|
|
42
|
+
expect(mapping).toHaveProperty('sisyphus', null);
|
|
43
|
+
});
|
|
44
|
+
it('should return identity mapping for oh-my-opencode', () => {
|
|
45
|
+
const mapping = getAgentMapping('oh-my-opencode');
|
|
46
|
+
// For oh-my-opencode, agent names pass through unchanged
|
|
47
|
+
expect(mapping).toEqual({});
|
|
48
|
+
});
|
|
49
|
+
it('should return empty mapping when framework is none', () => {
|
|
50
|
+
const mapping = getAgentMapping('none');
|
|
51
|
+
expect(mapping).toEqual({});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('isAgentAvailable', () => {
|
|
55
|
+
it('should return true if agent is in available list', () => {
|
|
56
|
+
const availableAgents = ['explorer', 'designer', 'librarian'];
|
|
57
|
+
expect(isAgentAvailable('explorer', availableAgents)).toBe(true);
|
|
58
|
+
expect(isAgentAvailable('designer', availableAgents)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
it('should return false if agent is not in available list', () => {
|
|
61
|
+
const availableAgents = ['explorer', 'designer'];
|
|
62
|
+
expect(isAgentAvailable('librarian', availableAgents)).toBe(false);
|
|
63
|
+
expect(isAgentAvailable('oracle', availableAgents)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
it('should return false for null agent name', () => {
|
|
66
|
+
const availableAgents = ['explorer', 'designer'];
|
|
67
|
+
expect(isAgentAvailable(null, availableAgents)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
it('should return false when available agents list is empty', () => {
|
|
70
|
+
expect(isAgentAvailable('explorer', [])).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
it('should handle case-sensitive matching', () => {
|
|
73
|
+
const availableAgents = ['explorer', 'Designer'];
|
|
74
|
+
expect(isAgentAvailable('explorer', availableAgents)).toBe(true);
|
|
75
|
+
expect(isAgentAvailable('Explorer', availableAgents)).toBe(false);
|
|
76
|
+
expect(isAgentAvailable('Designer', availableAgents)).toBe(true);
|
|
77
|
+
expect(isAgentAvailable('designer', availableAgents)).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
export type SynergyFramework = 'none' | 'oh-my-opencode' | 'oh-my-opencode-slim';
|
|
1
2
|
export interface ConfigDetectionResult {
|
|
2
3
|
hasCDDInOpenCode: boolean;
|
|
3
4
|
hasCDDInOMO: boolean;
|
|
4
5
|
synergyActive: boolean;
|
|
5
6
|
cddModel?: string;
|
|
7
|
+
synergyFramework: SynergyFramework;
|
|
8
|
+
slimAgents?: string[];
|
|
6
9
|
}
|
|
7
10
|
export declare function detectCDDConfig(): ConfigDetectionResult;
|
|
@@ -5,15 +5,36 @@ export function detectCDDConfig() {
|
|
|
5
5
|
const opencodeConfigDir = join(homedir(), ".config", "opencode");
|
|
6
6
|
const opencodeJsonPath = join(opencodeConfigDir, "opencode.json");
|
|
7
7
|
const omoJsonPath = join(opencodeConfigDir, "oh-my-opencode.json");
|
|
8
|
+
const slimJsonPath = join(opencodeConfigDir, "oh-my-opencode-slim.json");
|
|
8
9
|
let hasCDDInOpenCode = false;
|
|
9
10
|
let hasCDDInOMO = false;
|
|
10
11
|
let cddModel;
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
let synergyFramework = 'none';
|
|
13
|
+
let slimAgents;
|
|
14
|
+
// Check oh-my-opencode-slim.json first (highest priority for synergy)
|
|
15
|
+
if (existsSync(slimJsonPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const config = JSON.parse(readFileSync(slimJsonPath, "utf-8"));
|
|
18
|
+
// Check if config is not empty and has actual content
|
|
19
|
+
if (config && Object.keys(config).length > 0) {
|
|
20
|
+
synergyFramework = 'oh-my-opencode-slim';
|
|
21
|
+
// Extract available agents (filter out disabled ones)
|
|
22
|
+
const coreAgents = ['explorer', 'librarian', 'oracle', 'designer'];
|
|
23
|
+
const disabledAgents = new Set(config.disabled_agents ?? []);
|
|
24
|
+
slimAgents = coreAgents.filter(agent => !disabledAgents.has(agent));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
// Silently fail on parse errors
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Check oh-my-opencode.json (only if slim is not active)
|
|
32
|
+
if (synergyFramework === 'none' && existsSync(omoJsonPath)) {
|
|
13
33
|
try {
|
|
14
34
|
const config = JSON.parse(readFileSync(omoJsonPath, "utf-8"));
|
|
15
35
|
if (config.agents && config.agents.cdd) {
|
|
16
36
|
hasCDDInOMO = true;
|
|
37
|
+
synergyFramework = 'oh-my-opencode';
|
|
17
38
|
// Extract model from oh-my-opencode.json
|
|
18
39
|
if (config.agents.cdd.model) {
|
|
19
40
|
cddModel = config.agents.cdd.model;
|
|
@@ -43,7 +64,9 @@ export function detectCDDConfig() {
|
|
|
43
64
|
return {
|
|
44
65
|
hasCDDInOpenCode,
|
|
45
66
|
hasCDDInOMO,
|
|
46
|
-
synergyActive:
|
|
67
|
+
synergyActive: synergyFramework !== 'none',
|
|
47
68
|
cddModel,
|
|
69
|
+
synergyFramework,
|
|
70
|
+
slimAgents,
|
|
48
71
|
};
|
|
49
72
|
}
|
|
@@ -11,6 +11,7 @@ vi.mock("os", () => ({
|
|
|
11
11
|
describe("configDetection", () => {
|
|
12
12
|
const opencodeJsonPath = "/home/user/.config/opencode/opencode.json";
|
|
13
13
|
const omoJsonPath = "/home/user/.config/opencode/oh-my-opencode.json";
|
|
14
|
+
const slimJsonPath = "/home/user/.config/opencode/oh-my-opencode-slim.json";
|
|
14
15
|
beforeEach(() => {
|
|
15
16
|
vi.clearAllMocks();
|
|
16
17
|
});
|
|
@@ -116,4 +117,90 @@ describe("configDetection", () => {
|
|
|
116
117
|
expect(result.hasCDDInOMO).toBe(true);
|
|
117
118
|
expect(result.cddModel).toBeUndefined();
|
|
118
119
|
});
|
|
120
|
+
// New tests for oh-my-opencode-slim detection
|
|
121
|
+
describe("oh-my-opencode-slim detection", () => {
|
|
122
|
+
it("should detect oh-my-opencode-slim when only slim config is present", () => {
|
|
123
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
124
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
125
|
+
if (path === slimJsonPath) {
|
|
126
|
+
return JSON.stringify({
|
|
127
|
+
agents: { designer: { model: "google/gemini-3-flash" } }
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return "";
|
|
131
|
+
});
|
|
132
|
+
const result = detectCDDConfig();
|
|
133
|
+
expect(result.synergyActive).toBe(true);
|
|
134
|
+
expect(result.synergyFramework).toBe('oh-my-opencode-slim');
|
|
135
|
+
expect(result.slimAgents).toEqual(['explorer', 'librarian', 'oracle', 'designer']);
|
|
136
|
+
});
|
|
137
|
+
it("should prioritize oh-my-opencode-slim over oh-my-opencode when both present", () => {
|
|
138
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
139
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
140
|
+
if (path === slimJsonPath) {
|
|
141
|
+
return JSON.stringify({ agents: {} });
|
|
142
|
+
}
|
|
143
|
+
if (path === omoJsonPath) {
|
|
144
|
+
return JSON.stringify({ agents: { cdd: { model: "model-from-omo" } } });
|
|
145
|
+
}
|
|
146
|
+
return "";
|
|
147
|
+
});
|
|
148
|
+
const result = detectCDDConfig();
|
|
149
|
+
expect(result.synergyFramework).toBe('oh-my-opencode-slim');
|
|
150
|
+
expect(result.synergyActive).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
it("should filter out disabled agents from slim config", () => {
|
|
153
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
154
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
155
|
+
if (path === slimJsonPath) {
|
|
156
|
+
return JSON.stringify({
|
|
157
|
+
disabled_agents: ['oracle', 'designer']
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return "";
|
|
161
|
+
});
|
|
162
|
+
const result = detectCDDConfig();
|
|
163
|
+
expect(result.synergyFramework).toBe('oh-my-opencode-slim');
|
|
164
|
+
expect(result.slimAgents).toEqual(['explorer', 'librarian']);
|
|
165
|
+
});
|
|
166
|
+
it("should handle empty oh-my-opencode-slim config", () => {
|
|
167
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
168
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
169
|
+
if (path === slimJsonPath) {
|
|
170
|
+
return JSON.stringify({});
|
|
171
|
+
}
|
|
172
|
+
return "";
|
|
173
|
+
});
|
|
174
|
+
const result = detectCDDConfig();
|
|
175
|
+
expect(result.synergyFramework).toBe('none');
|
|
176
|
+
expect(result.synergyActive).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
it("should fallback to oh-my-opencode when slim config is invalid", () => {
|
|
179
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
180
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
181
|
+
if (path === slimJsonPath) {
|
|
182
|
+
return "invalid json";
|
|
183
|
+
}
|
|
184
|
+
if (path === omoJsonPath) {
|
|
185
|
+
return JSON.stringify({ agents: { cdd: { model: "model-from-omo" } } });
|
|
186
|
+
}
|
|
187
|
+
return "";
|
|
188
|
+
});
|
|
189
|
+
const result = detectCDDConfig();
|
|
190
|
+
expect(result.synergyFramework).toBe('oh-my-opencode');
|
|
191
|
+
expect(result.hasCDDInOMO).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
it("should handle malformed slim config gracefully", () => {
|
|
194
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
195
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
196
|
+
if (path === slimJsonPath) {
|
|
197
|
+
throw new Error("File read error");
|
|
198
|
+
}
|
|
199
|
+
return "";
|
|
200
|
+
});
|
|
201
|
+
const result = detectCDDConfig();
|
|
202
|
+
expect(result.synergyFramework).toBe('none');
|
|
203
|
+
expect(result.synergyActive).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
119
206
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SynergyFramework } from './configDetection.js';
|
|
2
|
+
/**
|
|
3
|
+
* Result of agent resolution for delegation
|
|
4
|
+
*/
|
|
5
|
+
export interface DelegationResult {
|
|
6
|
+
success: boolean;
|
|
7
|
+
resolvedAgent: string | null;
|
|
8
|
+
shouldFallback: boolean;
|
|
9
|
+
reason?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Determine if delegation to synergy framework should occur
|
|
13
|
+
*
|
|
14
|
+
* @param framework - The active synergy framework
|
|
15
|
+
* @returns True if delegation should occur
|
|
16
|
+
*/
|
|
17
|
+
export declare function shouldDelegateToSynergy(framework: SynergyFramework): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Get the delegation strategy file name based on active framework
|
|
20
|
+
*
|
|
21
|
+
* @param framework - The active synergy framework
|
|
22
|
+
* @returns Strategy file name ('delegate' or 'manual')
|
|
23
|
+
*/
|
|
24
|
+
export declare function getDelegationStrategy(framework: SynergyFramework): 'delegate' | 'manual';
|
|
25
|
+
/**
|
|
26
|
+
* Resolve an agent name for delegation, handling mapping and availability
|
|
27
|
+
*
|
|
28
|
+
* @param requestedAgent - The agent name requested
|
|
29
|
+
* @param framework - The active synergy framework
|
|
30
|
+
* @param availableAgents - List of available agents in the framework
|
|
31
|
+
* @returns Delegation result with resolved agent or fallback info
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveAgentForDelegation(requestedAgent: string, framework: SynergyFramework, availableAgents: string[]): DelegationResult;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { resolveAgentName, isAgentAvailable } from './agentMapping.js';
|
|
2
|
+
/**
|
|
3
|
+
* Determine if delegation to synergy framework should occur
|
|
4
|
+
*
|
|
5
|
+
* @param framework - The active synergy framework
|
|
6
|
+
* @returns True if delegation should occur
|
|
7
|
+
*/
|
|
8
|
+
export function shouldDelegateToSynergy(framework) {
|
|
9
|
+
return framework !== 'none';
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Get the delegation strategy file name based on active framework
|
|
13
|
+
*
|
|
14
|
+
* @param framework - The active synergy framework
|
|
15
|
+
* @returns Strategy file name ('delegate' or 'manual')
|
|
16
|
+
*/
|
|
17
|
+
export function getDelegationStrategy(framework) {
|
|
18
|
+
return shouldDelegateToSynergy(framework) ? 'delegate' : 'manual';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve an agent name for delegation, handling mapping and availability
|
|
22
|
+
*
|
|
23
|
+
* @param requestedAgent - The agent name requested
|
|
24
|
+
* @param framework - The active synergy framework
|
|
25
|
+
* @param availableAgents - List of available agents in the framework
|
|
26
|
+
* @returns Delegation result with resolved agent or fallback info
|
|
27
|
+
*/
|
|
28
|
+
export function resolveAgentForDelegation(requestedAgent, framework, availableAgents) {
|
|
29
|
+
// If no synergy framework active, must fallback
|
|
30
|
+
if (framework === 'none') {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
resolvedAgent: null,
|
|
34
|
+
shouldFallback: true,
|
|
35
|
+
reason: 'No synergy framework active',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Resolve the agent name through mapping
|
|
39
|
+
const resolvedAgent = resolveAgentName(requestedAgent, framework);
|
|
40
|
+
// If agent maps to null (no equivalent), must fallback
|
|
41
|
+
if (resolvedAgent === null) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
resolvedAgent: null,
|
|
45
|
+
shouldFallback: true,
|
|
46
|
+
reason: `Agent '${requestedAgent}' is not available in ${framework}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Check if the resolved agent is actually available
|
|
50
|
+
const available = isAgentAvailable(resolvedAgent, availableAgents);
|
|
51
|
+
if (!available) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
resolvedAgent,
|
|
55
|
+
shouldFallback: true,
|
|
56
|
+
reason: `Agent '${resolvedAgent}' is disabled or not available in ${framework}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Success! Agent can be delegated to
|
|
60
|
+
return {
|
|
61
|
+
success: true,
|
|
62
|
+
resolvedAgent,
|
|
63
|
+
shouldFallback: false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { shouldDelegateToSynergy, getDelegationStrategy, resolveAgentForDelegation } from './synergyDelegation.js';
|
|
3
|
+
describe('Synergy Delegation', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
// Clear any mocks
|
|
6
|
+
vi.clearAllMocks();
|
|
7
|
+
});
|
|
8
|
+
describe('shouldDelegateToSynergy', () => {
|
|
9
|
+
it('should return true when oh-my-opencode is active', () => {
|
|
10
|
+
const result = shouldDelegateToSynergy('oh-my-opencode');
|
|
11
|
+
expect(result).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
it('should return true when oh-my-opencode-slim is active', () => {
|
|
14
|
+
const result = shouldDelegateToSynergy('oh-my-opencode-slim');
|
|
15
|
+
expect(result).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
it('should return false when no synergy framework is active', () => {
|
|
18
|
+
const result = shouldDelegateToSynergy('none');
|
|
19
|
+
expect(result).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe('getDelegationStrategy', () => {
|
|
23
|
+
it('should return delegate strategy for oh-my-opencode', () => {
|
|
24
|
+
const strategy = getDelegationStrategy('oh-my-opencode');
|
|
25
|
+
expect(strategy).toBe('delegate');
|
|
26
|
+
});
|
|
27
|
+
it('should return delegate strategy for oh-my-opencode-slim', () => {
|
|
28
|
+
const strategy = getDelegationStrategy('oh-my-opencode-slim');
|
|
29
|
+
expect(strategy).toBe('delegate');
|
|
30
|
+
});
|
|
31
|
+
it('should return manual strategy when no framework active', () => {
|
|
32
|
+
const strategy = getDelegationStrategy('none');
|
|
33
|
+
expect(strategy).toBe('manual');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('resolveAgentForDelegation', () => {
|
|
37
|
+
it('should resolve sisyphus to sisyphus for oh-my-opencode', () => {
|
|
38
|
+
const result = resolveAgentForDelegation('sisyphus', 'oh-my-opencode', ['sisyphus', 'oracle']);
|
|
39
|
+
expect(result.success).toBe(true);
|
|
40
|
+
expect(result.resolvedAgent).toBe('sisyphus');
|
|
41
|
+
expect(result.shouldFallback).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
it('should resolve explore to explorer for oh-my-opencode-slim', () => {
|
|
44
|
+
const result = resolveAgentForDelegation('explore', 'oh-my-opencode-slim', ['explorer', 'designer']);
|
|
45
|
+
expect(result.success).toBe(true);
|
|
46
|
+
expect(result.resolvedAgent).toBe('explorer');
|
|
47
|
+
expect(result.shouldFallback).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it('should handle sisyphus mapping to null for slim (not available)', () => {
|
|
50
|
+
const result = resolveAgentForDelegation('sisyphus', 'oh-my-opencode-slim', ['explorer', 'designer']);
|
|
51
|
+
expect(result.success).toBe(false);
|
|
52
|
+
expect(result.resolvedAgent).toBe(null);
|
|
53
|
+
expect(result.shouldFallback).toBe(true);
|
|
54
|
+
expect(result.reason).toContain('not available');
|
|
55
|
+
});
|
|
56
|
+
it('should fallback when agent not in available list', () => {
|
|
57
|
+
const result = resolveAgentForDelegation('oracle', 'oh-my-opencode-slim', ['explorer', 'designer'] // oracle not in list
|
|
58
|
+
);
|
|
59
|
+
expect(result.success).toBe(false);
|
|
60
|
+
expect(result.shouldFallback).toBe(true);
|
|
61
|
+
expect(result.reason).toContain('disabled or not available');
|
|
62
|
+
});
|
|
63
|
+
it('should handle unknown agent names', () => {
|
|
64
|
+
const result = resolveAgentForDelegation('unknown-agent', 'oh-my-opencode-slim', ['explorer']);
|
|
65
|
+
expect(result.success).toBe(false);
|
|
66
|
+
expect(result.shouldFallback).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it('should return fallback when framework is none', () => {
|
|
69
|
+
const result = resolveAgentForDelegation('explore', 'none', []);
|
|
70
|
+
expect(result.success).toBe(false);
|
|
71
|
+
expect(result.shouldFallback).toBe(true);
|
|
72
|
+
expect(result.reason).toContain('No synergy framework');
|
|
73
|
+
});
|
|
74
|
+
it('should handle frontend-ui-ux-engineer mapping to designer', () => {
|
|
75
|
+
const result = resolveAgentForDelegation('frontend-ui-ux-engineer', 'oh-my-opencode-slim', ['designer', 'explorer']);
|
|
76
|
+
expect(result.success).toBe(true);
|
|
77
|
+
expect(result.resolvedAgent).toBe('designer');
|
|
78
|
+
expect(result.shouldFallback).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
it('should pass through agent names for oh-my-opencode', () => {
|
|
81
|
+
const result = resolveAgentForDelegation('explore', // not a standard omo agent, but should pass through
|
|
82
|
+
'oh-my-opencode', ['explore', 'sisyphus']);
|
|
83
|
+
expect(result.success).toBe(true);
|
|
84
|
+
expect(result.resolvedAgent).toBe('explore');
|
|
85
|
+
expect(result.shouldFallback).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type SynergyFramework } from "./configDetection.js";
|
|
2
|
+
export interface SynergyState {
|
|
3
|
+
framework: SynergyFramework;
|
|
4
|
+
detectedAt: Date;
|
|
5
|
+
availableAgents: string[];
|
|
6
|
+
lastCheck: Date;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Get the current synergy state, with caching
|
|
10
|
+
* @param workingDir - Current working directory
|
|
11
|
+
* @param forceRefresh - Force re-detection even if cache is valid
|
|
12
|
+
* @returns Current synergy state
|
|
13
|
+
*/
|
|
14
|
+
export declare function getSynergyState(workingDir: string, forceRefresh?: boolean): SynergyState;
|
|
15
|
+
/**
|
|
16
|
+
* Clear the synergy state cache, forcing re-detection on next call
|
|
17
|
+
*/
|
|
18
|
+
export declare function clearSynergyCache(): void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { detectCDDConfig } from "./configDetection.js";
|
|
2
|
+
// In-memory cache
|
|
3
|
+
let synergyStateCache = null;
|
|
4
|
+
let lastWorkingDir = null;
|
|
5
|
+
// Cache TTL: 60 seconds
|
|
6
|
+
const CACHE_TTL_MS = 60000;
|
|
7
|
+
/**
|
|
8
|
+
* Get the current synergy state, with caching
|
|
9
|
+
* @param workingDir - Current working directory
|
|
10
|
+
* @param forceRefresh - Force re-detection even if cache is valid
|
|
11
|
+
* @returns Current synergy state
|
|
12
|
+
*/
|
|
13
|
+
export function getSynergyState(workingDir, forceRefresh = false) {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
// Return cached state if:
|
|
16
|
+
// 1. Not forcing refresh
|
|
17
|
+
// 2. Cache exists
|
|
18
|
+
// 3. Same working directory
|
|
19
|
+
// 4. Cache is still fresh (within TTL)
|
|
20
|
+
if (!forceRefresh &&
|
|
21
|
+
synergyStateCache &&
|
|
22
|
+
lastWorkingDir === workingDir &&
|
|
23
|
+
now - synergyStateCache.lastCheck.getTime() < CACHE_TTL_MS) {
|
|
24
|
+
return synergyStateCache;
|
|
25
|
+
}
|
|
26
|
+
// Perform detection
|
|
27
|
+
const detection = detectCDDConfig();
|
|
28
|
+
// Extract available agents based on framework
|
|
29
|
+
let availableAgents = [];
|
|
30
|
+
if (detection.synergyFramework === 'oh-my-opencode-slim' && detection.slimAgents) {
|
|
31
|
+
availableAgents = detection.slimAgents;
|
|
32
|
+
}
|
|
33
|
+
// Note: oh-my-opencode doesn't expose agent list through config detection,
|
|
34
|
+
// so we leave availableAgents empty for that framework
|
|
35
|
+
const state = {
|
|
36
|
+
framework: detection.synergyFramework,
|
|
37
|
+
detectedAt: new Date(),
|
|
38
|
+
availableAgents,
|
|
39
|
+
lastCheck: new Date(),
|
|
40
|
+
};
|
|
41
|
+
// Update cache
|
|
42
|
+
synergyStateCache = state;
|
|
43
|
+
lastWorkingDir = workingDir;
|
|
44
|
+
return state;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Clear the synergy state cache, forcing re-detection on next call
|
|
48
|
+
*/
|
|
49
|
+
export function clearSynergyCache() {
|
|
50
|
+
synergyStateCache = null;
|
|
51
|
+
lastWorkingDir = null;
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|