opencode-conductor-cdd-plugin 1.0.0-beta.15 → 1.0.0-beta.17
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 +43 -4
- package/dist/prompts/agent/cdd.md +11 -7
- package/dist/prompts/cdd/implement.json +1 -1
- package/dist/prompts/cdd/newTrack.json +1 -1
- package/dist/prompts/cdd/revert.json +1 -1
- package/dist/prompts/cdd/setup.json +2 -2
- package/dist/prompts/cdd/status.json +1 -1
- 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/archive-tracks.d.ts +25 -0
- package/dist/utils/archive-tracks.js +1 -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
|
@@ -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 {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { getSynergyState, clearSynergyCache } from "./synergyState.js";
|
|
3
|
+
// Mock the configDetection module
|
|
4
|
+
vi.mock("./configDetection.js", () => ({
|
|
5
|
+
detectCDDConfig: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
import { detectCDDConfig } from "./configDetection.js";
|
|
8
|
+
describe("synergyState", () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
clearSynergyCache();
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
describe("getSynergyState", () => {
|
|
14
|
+
it("should detect and cache synergy state on first call", () => {
|
|
15
|
+
vi.mocked(detectCDDConfig).mockReturnValue({
|
|
16
|
+
hasCDDInOpenCode: false,
|
|
17
|
+
hasCDDInOMO: false,
|
|
18
|
+
synergyActive: true,
|
|
19
|
+
synergyFramework: 'oh-my-opencode-slim',
|
|
20
|
+
slimAgents: ['explorer', 'librarian', 'oracle', 'designer'],
|
|
21
|
+
});
|
|
22
|
+
const state = getSynergyState("/test/dir");
|
|
23
|
+
expect(state.framework).toBe('oh-my-opencode-slim');
|
|
24
|
+
expect(state.availableAgents).toEqual(['explorer', 'librarian', 'oracle', 'designer']);
|
|
25
|
+
expect(detectCDDConfig).toHaveBeenCalledTimes(1);
|
|
26
|
+
});
|
|
27
|
+
it("should return cached state on subsequent calls with same directory", () => {
|
|
28
|
+
vi.mocked(detectCDDConfig).mockReturnValue({
|
|
29
|
+
hasCDDInOpenCode: false,
|
|
30
|
+
hasCDDInOMO: false,
|
|
31
|
+
synergyActive: true,
|
|
32
|
+
synergyFramework: 'oh-my-opencode-slim',
|
|
33
|
+
slimAgents: ['explorer', 'librarian'],
|
|
34
|
+
});
|
|
35
|
+
const state1 = getSynergyState("/test/dir");
|
|
36
|
+
const state2 = getSynergyState("/test/dir");
|
|
37
|
+
expect(state1).toBe(state2); // Same object reference
|
|
38
|
+
expect(detectCDDConfig).toHaveBeenCalledTimes(1); // Only called once
|
|
39
|
+
});
|
|
40
|
+
it("should refresh state when working directory changes", () => {
|
|
41
|
+
vi.mocked(detectCDDConfig)
|
|
42
|
+
.mockReturnValueOnce({
|
|
43
|
+
hasCDDInOpenCode: false,
|
|
44
|
+
hasCDDInOMO: false,
|
|
45
|
+
synergyActive: true,
|
|
46
|
+
synergyFramework: 'oh-my-opencode-slim',
|
|
47
|
+
slimAgents: ['explorer'],
|
|
48
|
+
})
|
|
49
|
+
.mockReturnValueOnce({
|
|
50
|
+
hasCDDInOpenCode: false,
|
|
51
|
+
hasCDDInOMO: true,
|
|
52
|
+
synergyActive: true,
|
|
53
|
+
synergyFramework: 'oh-my-opencode',
|
|
54
|
+
cddModel: 'model-1',
|
|
55
|
+
});
|
|
56
|
+
const state1 = getSynergyState("/dir1");
|
|
57
|
+
const state2 = getSynergyState("/dir2");
|
|
58
|
+
expect(state1.framework).toBe('oh-my-opencode-slim');
|
|
59
|
+
expect(state2.framework).toBe('oh-my-opencode');
|
|
60
|
+
expect(detectCDDConfig).toHaveBeenCalledTimes(2);
|
|
61
|
+
});
|
|
62
|
+
it("should force refresh when forceRefresh is true", () => {
|
|
63
|
+
vi.mocked(detectCDDConfig)
|
|
64
|
+
.mockReturnValueOnce({
|
|
65
|
+
hasCDDInOpenCode: false,
|
|
66
|
+
hasCDDInOMO: false,
|
|
67
|
+
synergyActive: true,
|
|
68
|
+
synergyFramework: 'oh-my-opencode-slim',
|
|
69
|
+
slimAgents: ['explorer'],
|
|
70
|
+
})
|
|
71
|
+
.mockReturnValueOnce({
|
|
72
|
+
hasCDDInOpenCode: false,
|
|
73
|
+
hasCDDInOMO: false,
|
|
74
|
+
synergyActive: true,
|
|
75
|
+
synergyFramework: 'oh-my-opencode',
|
|
76
|
+
cddModel: 'model-changed',
|
|
77
|
+
});
|
|
78
|
+
const state1 = getSynergyState("/test/dir");
|
|
79
|
+
const state2 = getSynergyState("/test/dir", true); // Force refresh
|
|
80
|
+
expect(state1.framework).toBe('oh-my-opencode-slim');
|
|
81
|
+
expect(state2.framework).toBe('oh-my-opencode');
|
|
82
|
+
expect(detectCDDConfig).toHaveBeenCalledTimes(2);
|
|
83
|
+
});
|
|
84
|
+
it("should handle no synergy active", () => {
|
|
85
|
+
vi.mocked(detectCDDConfig).mockReturnValue({
|
|
86
|
+
hasCDDInOpenCode: true,
|
|
87
|
+
hasCDDInOMO: false,
|
|
88
|
+
synergyActive: false,
|
|
89
|
+
synergyFramework: 'none',
|
|
90
|
+
cddModel: 'model-1',
|
|
91
|
+
});
|
|
92
|
+
const state = getSynergyState("/test/dir");
|
|
93
|
+
expect(state.framework).toBe('none');
|
|
94
|
+
expect(state.availableAgents).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
it("should refresh cache after TTL expires", () => {
|
|
97
|
+
vi.useFakeTimers();
|
|
98
|
+
vi.mocked(detectCDDConfig)
|
|
99
|
+
.mockReturnValueOnce({
|
|
100
|
+
hasCDDInOpenCode: false,
|
|
101
|
+
hasCDDInOMO: false,
|
|
102
|
+
synergyActive: true,
|
|
103
|
+
synergyFramework: 'oh-my-opencode-slim',
|
|
104
|
+
slimAgents: ['explorer'],
|
|
105
|
+
})
|
|
106
|
+
.mockReturnValueOnce({
|
|
107
|
+
hasCDDInOpenCode: false,
|
|
108
|
+
hasCDDInOMO: false,
|
|
109
|
+
synergyActive: true,
|
|
110
|
+
synergyFramework: 'oh-my-opencode',
|
|
111
|
+
cddModel: 'model-1',
|
|
112
|
+
});
|
|
113
|
+
const state1 = getSynergyState("/test/dir");
|
|
114
|
+
// Advance time by 61 seconds (TTL is 60 seconds)
|
|
115
|
+
vi.advanceTimersByTime(61000);
|
|
116
|
+
const state2 = getSynergyState("/test/dir");
|
|
117
|
+
expect(state1.framework).toBe('oh-my-opencode-slim');
|
|
118
|
+
expect(state2.framework).toBe('oh-my-opencode');
|
|
119
|
+
expect(detectCDDConfig).toHaveBeenCalledTimes(2);
|
|
120
|
+
vi.useRealTimers();
|
|
121
|
+
});
|
|
122
|
+
it("should include detection timestamp", () => {
|
|
123
|
+
const beforeTime = Date.now();
|
|
124
|
+
vi.mocked(detectCDDConfig).mockReturnValue({
|
|
125
|
+
hasCDDInOpenCode: false,
|
|
126
|
+
hasCDDInOMO: false,
|
|
127
|
+
synergyActive: false,
|
|
128
|
+
synergyFramework: 'none',
|
|
129
|
+
});
|
|
130
|
+
const state = getSynergyState("/test/dir");
|
|
131
|
+
const afterTime = Date.now();
|
|
132
|
+
expect(state.detectedAt.getTime()).toBeGreaterThanOrEqual(beforeTime);
|
|
133
|
+
expect(state.detectedAt.getTime()).toBeLessThanOrEqual(afterTime);
|
|
134
|
+
});
|
|
135
|
+
it("should extract agents from oh-my-opencode-slim", () => {
|
|
136
|
+
vi.mocked(detectCDDConfig).mockReturnValue({
|
|
137
|
+
hasCDDInOpenCode: false,
|
|
138
|
+
hasCDDInOMO: false,
|
|
139
|
+
synergyActive: true,
|
|
140
|
+
synergyFramework: 'oh-my-opencode-slim',
|
|
141
|
+
slimAgents: ['explorer', 'oracle'],
|
|
142
|
+
});
|
|
143
|
+
const state = getSynergyState("/test/dir");
|
|
144
|
+
expect(state.availableAgents).toEqual(['explorer', 'oracle']);
|
|
145
|
+
});
|
|
146
|
+
it("should handle oh-my-opencode framework", () => {
|
|
147
|
+
vi.mocked(detectCDDConfig).mockReturnValue({
|
|
148
|
+
hasCDDInOpenCode: false,
|
|
149
|
+
hasCDDInOMO: true,
|
|
150
|
+
synergyActive: true,
|
|
151
|
+
synergyFramework: 'oh-my-opencode',
|
|
152
|
+
cddModel: 'model-1',
|
|
153
|
+
});
|
|
154
|
+
const state = getSynergyState("/test/dir");
|
|
155
|
+
expect(state.framework).toBe('oh-my-opencode');
|
|
156
|
+
// oh-my-opencode doesn't expose agent list through config, so availableAgents should be empty
|
|
157
|
+
expect(state.availableAgents).toEqual([]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe("clearSynergyCache", () => {
|
|
161
|
+
it("should clear the cache and force re-detection", () => {
|
|
162
|
+
vi.mocked(detectCDDConfig)
|
|
163
|
+
.mockReturnValueOnce({
|
|
164
|
+
hasCDDInOpenCode: false,
|
|
165
|
+
hasCDDInOMO: false,
|
|
166
|
+
synergyActive: true,
|
|
167
|
+
synergyFramework: 'oh-my-opencode-slim',
|
|
168
|
+
slimAgents: ['explorer'],
|
|
169
|
+
})
|
|
170
|
+
.mockReturnValueOnce({
|
|
171
|
+
hasCDDInOpenCode: false,
|
|
172
|
+
hasCDDInOMO: false,
|
|
173
|
+
synergyActive: true,
|
|
174
|
+
synergyFramework: 'oh-my-opencode',
|
|
175
|
+
cddModel: 'changed',
|
|
176
|
+
});
|
|
177
|
+
const state1 = getSynergyState("/test/dir");
|
|
178
|
+
clearSynergyCache();
|
|
179
|
+
const state2 = getSynergyState("/test/dir");
|
|
180
|
+
expect(state1.framework).toBe('oh-my-opencode-slim');
|
|
181
|
+
expect(state2.framework).toBe('oh-my-opencode');
|
|
182
|
+
expect(detectCDDConfig).toHaveBeenCalledTimes(2);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SynergyFramework } from './configDetection.js';
|
|
2
|
+
import type { SynergyState } from './synergyState.js';
|
|
3
|
+
/**
|
|
4
|
+
* Synergy status information for display
|
|
5
|
+
*/
|
|
6
|
+
export interface SynergyStatus {
|
|
7
|
+
framework: SynergyFramework;
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
availableAgents: string[];
|
|
10
|
+
workingDir: string;
|
|
11
|
+
detectedAt: Date;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get synergy status for display
|
|
15
|
+
*
|
|
16
|
+
* @param workingDir - Current working directory
|
|
17
|
+
* @param state - Current synergy state
|
|
18
|
+
* @returns Status information
|
|
19
|
+
*/
|
|
20
|
+
export declare function getSynergyStatus(workingDir: string, state: SynergyState): SynergyStatus;
|
|
21
|
+
/**
|
|
22
|
+
* Format synergy status for display
|
|
23
|
+
*
|
|
24
|
+
* @param status - Synergy status to format
|
|
25
|
+
* @returns Formatted status string
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatSynergyStatus(status: SynergyStatus): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get synergy status for display
|
|
3
|
+
*
|
|
4
|
+
* @param workingDir - Current working directory
|
|
5
|
+
* @param state - Current synergy state
|
|
6
|
+
* @returns Status information
|
|
7
|
+
*/
|
|
8
|
+
export function getSynergyStatus(workingDir, state) {
|
|
9
|
+
return {
|
|
10
|
+
framework: state.framework,
|
|
11
|
+
isActive: state.framework !== 'none',
|
|
12
|
+
availableAgents: state.availableAgents,
|
|
13
|
+
workingDir,
|
|
14
|
+
detectedAt: state.detectedAt,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Format synergy status for display
|
|
19
|
+
*
|
|
20
|
+
* @param status - Synergy status to format
|
|
21
|
+
* @returns Formatted status string
|
|
22
|
+
*/
|
|
23
|
+
export function formatSynergyStatus(status) {
|
|
24
|
+
const lines = [];
|
|
25
|
+
lines.push('='.repeat(60));
|
|
26
|
+
lines.push('Synergy Framework Status');
|
|
27
|
+
lines.push('='.repeat(60));
|
|
28
|
+
lines.push('');
|
|
29
|
+
// Framework and status
|
|
30
|
+
lines.push(`Synergy Framework: ${status.framework}`);
|
|
31
|
+
lines.push(`Status: ${status.isActive ? 'Active' : 'Inactive'}`);
|
|
32
|
+
lines.push('');
|
|
33
|
+
// Working directory
|
|
34
|
+
lines.push(`Working Directory: ${status.workingDir}`);
|
|
35
|
+
lines.push('');
|
|
36
|
+
// Detection timestamp
|
|
37
|
+
lines.push(`Detected At: ${status.detectedAt.toISOString()}`);
|
|
38
|
+
lines.push('');
|
|
39
|
+
// Available agents (only if active and has agents)
|
|
40
|
+
if (status.isActive && status.availableAgents.length > 0) {
|
|
41
|
+
lines.push(`Available Agents (${status.availableAgents.length}):`);
|
|
42
|
+
status.availableAgents.forEach(agent => {
|
|
43
|
+
lines.push(` - ${agent}`);
|
|
44
|
+
});
|
|
45
|
+
lines.push('');
|
|
46
|
+
}
|
|
47
|
+
else if (!status.isActive) {
|
|
48
|
+
lines.push('No synergy framework detected.');
|
|
49
|
+
lines.push('Configure oh-my-opencode or oh-my-opencode-slim to enable synergy.');
|
|
50
|
+
lines.push('');
|
|
51
|
+
}
|
|
52
|
+
lines.push('='.repeat(60));
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { getSynergyStatus, formatSynergyStatus } from './synergyStatus.js';
|
|
3
|
+
describe('Synergy Status', () => {
|
|
4
|
+
describe('getSynergyStatus', () => {
|
|
5
|
+
it('should return status with oh-my-opencode framework', () => {
|
|
6
|
+
const status = getSynergyStatus('/fake/dir', {
|
|
7
|
+
framework: 'oh-my-opencode',
|
|
8
|
+
detectedAt: new Date('2026-01-19T14:00:00Z'),
|
|
9
|
+
availableAgents: [],
|
|
10
|
+
lastCheck: new Date('2026-01-19T14:00:00Z'),
|
|
11
|
+
});
|
|
12
|
+
expect(status.framework).toBe('oh-my-opencode');
|
|
13
|
+
expect(status.isActive).toBe(true);
|
|
14
|
+
expect(status.availableAgents).toEqual([]);
|
|
15
|
+
expect(status.detectedAt).toBeInstanceOf(Date);
|
|
16
|
+
});
|
|
17
|
+
it('should return status with oh-my-opencode-slim framework', () => {
|
|
18
|
+
const status = getSynergyStatus('/fake/dir', {
|
|
19
|
+
framework: 'oh-my-opencode-slim',
|
|
20
|
+
detectedAt: new Date('2026-01-19T14:00:00Z'),
|
|
21
|
+
availableAgents: ['explorer', 'designer', 'librarian'],
|
|
22
|
+
lastCheck: new Date('2026-01-19T14:00:00Z'),
|
|
23
|
+
});
|
|
24
|
+
expect(status.framework).toBe('oh-my-opencode-slim');
|
|
25
|
+
expect(status.isActive).toBe(true);
|
|
26
|
+
expect(status.availableAgents).toHaveLength(3);
|
|
27
|
+
expect(status.availableAgents).toContain('explorer');
|
|
28
|
+
});
|
|
29
|
+
it('should return inactive status when no framework', () => {
|
|
30
|
+
const status = getSynergyStatus('/fake/dir', {
|
|
31
|
+
framework: 'none',
|
|
32
|
+
detectedAt: new Date('2026-01-19T14:00:00Z'),
|
|
33
|
+
availableAgents: [],
|
|
34
|
+
lastCheck: new Date('2026-01-19T14:00:00Z'),
|
|
35
|
+
});
|
|
36
|
+
expect(status.framework).toBe('none');
|
|
37
|
+
expect(status.isActive).toBe(false);
|
|
38
|
+
expect(status.availableAgents).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
it('should include working directory in status', () => {
|
|
41
|
+
const workingDir = '/test/project';
|
|
42
|
+
const status = getSynergyStatus(workingDir, {
|
|
43
|
+
framework: 'oh-my-opencode-slim',
|
|
44
|
+
detectedAt: new Date(),
|
|
45
|
+
availableAgents: [],
|
|
46
|
+
lastCheck: new Date(),
|
|
47
|
+
});
|
|
48
|
+
expect(status.workingDir).toBe(workingDir);
|
|
49
|
+
});
|
|
50
|
+
it('should include detection timestamp', () => {
|
|
51
|
+
const detectedAt = new Date('2026-01-19T14:30:00Z');
|
|
52
|
+
const status = getSynergyStatus('/fake/dir', {
|
|
53
|
+
framework: 'oh-my-opencode',
|
|
54
|
+
detectedAt,
|
|
55
|
+
availableAgents: [],
|
|
56
|
+
lastCheck: new Date(),
|
|
57
|
+
});
|
|
58
|
+
expect(status.detectedAt).toBe(detectedAt);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('formatSynergyStatus', () => {
|
|
62
|
+
it('should format status for oh-my-opencode', () => {
|
|
63
|
+
const status = {
|
|
64
|
+
framework: 'oh-my-opencode',
|
|
65
|
+
isActive: true,
|
|
66
|
+
availableAgents: [],
|
|
67
|
+
workingDir: '/test/project',
|
|
68
|
+
detectedAt: new Date('2026-01-19T14:00:00Z'),
|
|
69
|
+
};
|
|
70
|
+
const formatted = formatSynergyStatus(status);
|
|
71
|
+
expect(formatted).toContain('Synergy Framework: oh-my-opencode');
|
|
72
|
+
expect(formatted).toContain('Status: Active');
|
|
73
|
+
expect(formatted).toContain('Working Directory: /test/project');
|
|
74
|
+
expect(formatted).toContain('Detected At:');
|
|
75
|
+
});
|
|
76
|
+
it('should format status for oh-my-opencode-slim with agents', () => {
|
|
77
|
+
const status = {
|
|
78
|
+
framework: 'oh-my-opencode-slim',
|
|
79
|
+
isActive: true,
|
|
80
|
+
availableAgents: ['explorer', 'designer', 'librarian'],
|
|
81
|
+
workingDir: '/test/project',
|
|
82
|
+
detectedAt: new Date('2026-01-19T14:00:00Z'),
|
|
83
|
+
};
|
|
84
|
+
const formatted = formatSynergyStatus(status);
|
|
85
|
+
expect(formatted).toContain('Synergy Framework: oh-my-opencode-slim');
|
|
86
|
+
expect(formatted).toContain('Status: Active');
|
|
87
|
+
expect(formatted).toContain('Available Agents (3):');
|
|
88
|
+
expect(formatted).toContain('- explorer');
|
|
89
|
+
expect(formatted).toContain('- designer');
|
|
90
|
+
expect(formatted).toContain('- librarian');
|
|
91
|
+
});
|
|
92
|
+
it('should format inactive status', () => {
|
|
93
|
+
const status = {
|
|
94
|
+
framework: 'none',
|
|
95
|
+
isActive: false,
|
|
96
|
+
availableAgents: [],
|
|
97
|
+
workingDir: '/test/project',
|
|
98
|
+
detectedAt: new Date('2026-01-19T14:00:00Z'),
|
|
99
|
+
};
|
|
100
|
+
const formatted = formatSynergyStatus(status);
|
|
101
|
+
expect(formatted).toContain('Synergy Framework: none');
|
|
102
|
+
expect(formatted).toContain('Status: Inactive');
|
|
103
|
+
expect(formatted).toContain('No synergy framework detected');
|
|
104
|
+
});
|
|
105
|
+
it('should handle empty agents list for active framework', () => {
|
|
106
|
+
const status = {
|
|
107
|
+
framework: 'oh-my-opencode',
|
|
108
|
+
isActive: true,
|
|
109
|
+
availableAgents: [],
|
|
110
|
+
workingDir: '/test/project',
|
|
111
|
+
detectedAt: new Date('2026-01-19T14:00:00Z'),
|
|
112
|
+
};
|
|
113
|
+
const formatted = formatSynergyStatus(status);
|
|
114
|
+
expect(formatted).toContain('oh-my-opencode');
|
|
115
|
+
expect(formatted).toContain('Active');
|
|
116
|
+
// Should not show agent list for oh-my-opencode (doesn't expose agents)
|
|
117
|
+
expect(formatted).not.toContain('Available Agents');
|
|
118
|
+
});
|
|
119
|
+
it('should format timestamps in readable format', () => {
|
|
120
|
+
const status = {
|
|
121
|
+
framework: 'oh-my-opencode-slim',
|
|
122
|
+
isActive: true,
|
|
123
|
+
availableAgents: [],
|
|
124
|
+
workingDir: '/test/project',
|
|
125
|
+
detectedAt: new Date('2026-01-19T14:30:45.123Z'),
|
|
126
|
+
};
|
|
127
|
+
const formatted = formatSynergyStatus(status);
|
|
128
|
+
expect(formatted).toContain('Detected At:');
|
|
129
|
+
expect(formatted).toMatch(/\d{4}-\d{2}-\d{2}/); // Should contain date
|
|
130
|
+
});
|
|
131
|
+
it('should show agent count correctly', () => {
|
|
132
|
+
const status = {
|
|
133
|
+
framework: 'oh-my-opencode-slim',
|
|
134
|
+
isActive: true,
|
|
135
|
+
availableAgents: ['explorer'],
|
|
136
|
+
workingDir: '/test/project',
|
|
137
|
+
detectedAt: new Date(),
|
|
138
|
+
};
|
|
139
|
+
const formatted = formatSynergyStatus(status);
|
|
140
|
+
expect(formatted).toContain('Available Agents (1):');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
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.17",
|
|
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": {
|