@doingdev/opencode-claude-manager-plugin 0.1.49 → 0.1.51
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/claude/claude-agent-sdk-adapter.d.ts +2 -3
- package/dist/claude/claude-agent-sdk-adapter.js +38 -48
- package/dist/claude/claude-session.service.d.ts +1 -2
- package/dist/claude/claude-session.service.js +0 -3
- package/dist/claude/tool-approval-manager.d.ts +9 -6
- package/dist/claude/tool-approval-manager.js +43 -6
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -1
- package/dist/manager/context-tracker.d.ts +0 -1
- package/dist/manager/context-tracker.js +0 -3
- package/dist/manager/git-operations.d.ts +1 -4
- package/dist/manager/git-operations.js +7 -12
- package/dist/manager/persistent-manager.d.ts +3 -53
- package/dist/manager/persistent-manager.js +3 -135
- package/dist/manager/team-orchestrator.d.ts +9 -4
- package/dist/manager/team-orchestrator.js +84 -31
- package/dist/plugin/agent-hierarchy.d.ts +0 -1
- package/dist/plugin/agent-hierarchy.js +4 -2
- package/dist/plugin/claude-manager.plugin.js +170 -24
- package/dist/plugin/service-factory.d.ts +5 -6
- package/dist/plugin/service-factory.js +9 -17
- package/dist/prompts/registry.js +58 -39
- package/dist/src/claude/claude-agent-sdk-adapter.d.ts +2 -3
- package/dist/src/claude/claude-agent-sdk-adapter.js +38 -48
- package/dist/src/claude/claude-session.service.d.ts +1 -2
- package/dist/src/claude/claude-session.service.js +0 -3
- package/dist/src/claude/tool-approval-manager.d.ts +9 -6
- package/dist/src/claude/tool-approval-manager.js +43 -6
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.js +0 -1
- package/dist/src/manager/context-tracker.d.ts +0 -1
- package/dist/src/manager/context-tracker.js +0 -3
- package/dist/src/manager/git-operations.d.ts +1 -4
- package/dist/src/manager/git-operations.js +7 -12
- package/dist/src/manager/persistent-manager.d.ts +3 -53
- package/dist/src/manager/persistent-manager.js +3 -135
- package/dist/src/manager/team-orchestrator.d.ts +9 -4
- package/dist/src/manager/team-orchestrator.js +84 -31
- package/dist/src/plugin/agent-hierarchy.d.ts +0 -1
- package/dist/src/plugin/agent-hierarchy.js +4 -2
- package/dist/src/plugin/claude-manager.plugin.js +170 -24
- package/dist/src/plugin/service-factory.d.ts +5 -6
- package/dist/src/plugin/service-factory.js +9 -17
- package/dist/src/prompts/registry.js +58 -39
- package/dist/src/state/team-state-store.js +4 -1
- package/dist/src/team/roster.js +1 -0
- package/dist/src/types/contracts.d.ts +18 -57
- package/dist/state/team-state-store.js +4 -1
- package/dist/team/roster.js +1 -0
- package/dist/test/claude-agent-sdk-adapter.test.js +103 -11
- package/dist/test/claude-manager.plugin.test.js +6 -1
- package/dist/test/context-tracker.test.js +0 -8
- package/dist/test/git-operations.test.js +0 -21
- package/dist/test/persistent-manager.test.js +4 -164
- package/dist/test/prompt-registry.test.js +4 -9
- package/dist/test/report-claude-event.test.js +4 -4
- package/dist/test/team-orchestrator.test.js +7 -5
- package/dist/test/tool-approval-manager.test.js +17 -17
- package/dist/types/contracts.d.ts +18 -57
- package/package.json +1 -1
package/dist/team/roster.js
CHANGED
|
@@ -253,17 +253,6 @@ describe('ClaudeAgentSdkAdapter', () => {
|
|
|
253
253
|
});
|
|
254
254
|
expect(result.events.map((event) => event.type)).toEqual(['init', 'result']);
|
|
255
255
|
});
|
|
256
|
-
it('probes supported commands, agents, and models', async () => {
|
|
257
|
-
const adapter = new ClaudeAgentSdkAdapter({
|
|
258
|
-
query: () => createFakeQuery([]),
|
|
259
|
-
listSessions: async () => [],
|
|
260
|
-
getSessionMessages: async () => [],
|
|
261
|
-
});
|
|
262
|
-
const capabilities = await adapter.probeCapabilities('/tmp/project');
|
|
263
|
-
expect(capabilities.commands[0]).toMatchObject({ name: 'review' });
|
|
264
|
-
expect(capabilities.agents[0]).toMatchObject({ name: 'researcher' });
|
|
265
|
-
expect(capabilities.models).toEqual(['claude-sonnet-4-5']);
|
|
266
|
-
});
|
|
267
256
|
it('defaults permissionMode to acceptEdits when omitted', async () => {
|
|
268
257
|
let capturedPermissionMode;
|
|
269
258
|
const adapter = new ClaudeAgentSdkAdapter({
|
|
@@ -429,6 +418,109 @@ describe('ClaudeAgentSdkAdapter', () => {
|
|
|
429
418
|
expect(result.outputTokens).toBe(200);
|
|
430
419
|
expect(result.contextWindowSize).toBe(180_000);
|
|
431
420
|
});
|
|
421
|
+
it('denies write tools when restrictWriteTools is true', async () => {
|
|
422
|
+
let capturedCanUseTool;
|
|
423
|
+
const adapter = new ClaudeAgentSdkAdapter({
|
|
424
|
+
query: (params) => {
|
|
425
|
+
capturedCanUseTool = params.options?.canUseTool;
|
|
426
|
+
return createFakeQuery([
|
|
427
|
+
{
|
|
428
|
+
type: 'result',
|
|
429
|
+
subtype: 'success',
|
|
430
|
+
session_id: 'ses_rw',
|
|
431
|
+
is_error: false,
|
|
432
|
+
result: 'ok',
|
|
433
|
+
num_turns: 1,
|
|
434
|
+
total_cost_usd: 0,
|
|
435
|
+
},
|
|
436
|
+
]);
|
|
437
|
+
},
|
|
438
|
+
listSessions: async () => [],
|
|
439
|
+
getSessionMessages: async () => [],
|
|
440
|
+
});
|
|
441
|
+
await adapter.runSession({
|
|
442
|
+
cwd: '/tmp/project',
|
|
443
|
+
prompt: 'Investigate',
|
|
444
|
+
restrictWriteTools: true,
|
|
445
|
+
});
|
|
446
|
+
expect(capturedCanUseTool).toBeDefined();
|
|
447
|
+
const editResult = await capturedCanUseTool('Edit', { file_path: 'x.ts' }, {});
|
|
448
|
+
expect(editResult.behavior).toBe('deny');
|
|
449
|
+
const writeResult = await capturedCanUseTool('Write', { file_path: 'y.ts' }, {});
|
|
450
|
+
expect(writeResult.behavior).toBe('deny');
|
|
451
|
+
const multiEditResult = await capturedCanUseTool('MultiEdit', { file_path: 'z.ts' }, {});
|
|
452
|
+
expect(multiEditResult.behavior).toBe('deny');
|
|
453
|
+
const readResult = await capturedCanUseTool('Read', { file_path: 'a.ts' }, {});
|
|
454
|
+
expect(readResult.behavior).toBe('allow');
|
|
455
|
+
const grepResult = await capturedCanUseTool('Grep', { pattern: 'foo', path: '.' }, {});
|
|
456
|
+
expect(grepResult.behavior).toBe('allow');
|
|
457
|
+
});
|
|
458
|
+
it('denies destructive bash commands when restrictWriteTools is true', async () => {
|
|
459
|
+
let capturedCanUseTool;
|
|
460
|
+
const adapter = new ClaudeAgentSdkAdapter({
|
|
461
|
+
query: (params) => {
|
|
462
|
+
capturedCanUseTool = params.options?.canUseTool;
|
|
463
|
+
return createFakeQuery([
|
|
464
|
+
{
|
|
465
|
+
type: 'result',
|
|
466
|
+
subtype: 'success',
|
|
467
|
+
session_id: 'ses_bash',
|
|
468
|
+
is_error: false,
|
|
469
|
+
result: 'ok',
|
|
470
|
+
num_turns: 1,
|
|
471
|
+
total_cost_usd: 0,
|
|
472
|
+
},
|
|
473
|
+
]);
|
|
474
|
+
},
|
|
475
|
+
listSessions: async () => [],
|
|
476
|
+
getSessionMessages: async () => [],
|
|
477
|
+
});
|
|
478
|
+
await adapter.runSession({
|
|
479
|
+
cwd: '/tmp/project',
|
|
480
|
+
prompt: 'Check',
|
|
481
|
+
restrictWriteTools: true,
|
|
482
|
+
});
|
|
483
|
+
expect(capturedCanUseTool).toBeDefined();
|
|
484
|
+
const sedResult = await capturedCanUseTool('Bash', { command: "sed -i 's/old/new/' file.ts" }, {});
|
|
485
|
+
expect(sedResult.behavior).toBe('deny');
|
|
486
|
+
const echoResult = await capturedCanUseTool('Bash', { command: 'echo "data" > out.txt' }, {});
|
|
487
|
+
expect(echoResult.behavior).toBe('deny');
|
|
488
|
+
const gitCommitResult = await capturedCanUseTool('Bash', { command: 'git commit -m "fix"' }, {});
|
|
489
|
+
expect(gitCommitResult.behavior).toBe('deny');
|
|
490
|
+
const lsResult = await capturedCanUseTool('Bash', { command: 'ls -la src/' }, {});
|
|
491
|
+
expect(lsResult.behavior).toBe('allow');
|
|
492
|
+
const catResult = await capturedCanUseTool('Bash', { command: 'cat src/index.ts' }, {});
|
|
493
|
+
expect(catResult.behavior).toBe('allow');
|
|
494
|
+
const gitLogResult = await capturedCanUseTool('Bash', { command: 'git log --oneline -10' }, {});
|
|
495
|
+
expect(gitLogResult.behavior).toBe('allow');
|
|
496
|
+
});
|
|
497
|
+
it('allows write tools when restrictWriteTools is false', async () => {
|
|
498
|
+
let capturedCanUseTool;
|
|
499
|
+
const adapter = new ClaudeAgentSdkAdapter({
|
|
500
|
+
query: (params) => {
|
|
501
|
+
capturedCanUseTool = params.options?.canUseTool;
|
|
502
|
+
return createFakeQuery([
|
|
503
|
+
{
|
|
504
|
+
type: 'result',
|
|
505
|
+
subtype: 'success',
|
|
506
|
+
session_id: 'ses_free',
|
|
507
|
+
is_error: false,
|
|
508
|
+
result: 'ok',
|
|
509
|
+
num_turns: 1,
|
|
510
|
+
total_cost_usd: 0,
|
|
511
|
+
},
|
|
512
|
+
]);
|
|
513
|
+
},
|
|
514
|
+
listSessions: async () => [],
|
|
515
|
+
getSessionMessages: async () => [],
|
|
516
|
+
});
|
|
517
|
+
await adapter.runSession({
|
|
518
|
+
cwd: '/tmp/project',
|
|
519
|
+
prompt: 'Implement',
|
|
520
|
+
restrictWriteTools: false,
|
|
521
|
+
});
|
|
522
|
+
expect(capturedCanUseTool).toBeUndefined();
|
|
523
|
+
});
|
|
432
524
|
it('passes through explicit permissionMode', async () => {
|
|
433
525
|
let capturedPermissionMode;
|
|
434
526
|
const adapter = new ClaudeAgentSdkAdapter({
|
|
@@ -26,6 +26,8 @@ describe('ClaudeManagerPlugin', () => {
|
|
|
26
26
|
todoread: 'allow',
|
|
27
27
|
question: 'allow',
|
|
28
28
|
team_status: 'allow',
|
|
29
|
+
plan_with_team: 'allow',
|
|
30
|
+
reset_engineer: 'allow',
|
|
29
31
|
git_diff: 'allow',
|
|
30
32
|
git_commit: 'allow',
|
|
31
33
|
git_reset: 'allow',
|
|
@@ -61,6 +63,8 @@ describe('ClaudeManagerPlugin', () => {
|
|
|
61
63
|
claude: 'allow',
|
|
62
64
|
git_diff: 'deny',
|
|
63
65
|
git_commit: 'deny',
|
|
66
|
+
plan_with_team: 'deny',
|
|
67
|
+
reset_engineer: 'deny',
|
|
64
68
|
});
|
|
65
69
|
expect(agent.permission).not.toHaveProperty('read');
|
|
66
70
|
expect(agent.permission).not.toHaveProperty('grep');
|
|
@@ -73,8 +77,9 @@ describe('ClaudeManagerPlugin', () => {
|
|
|
73
77
|
const tools = plugin.tool;
|
|
74
78
|
expect(tools['claude']).toBeDefined();
|
|
75
79
|
expect(tools['team_status']).toBeDefined();
|
|
80
|
+
expect(tools['plan_with_team']).toBeDefined();
|
|
81
|
+
expect(tools['reset_engineer']).toBeDefined();
|
|
76
82
|
expect(tools['assign_engineer']).toBeUndefined();
|
|
77
|
-
expect(tools['plan_with_team']).toBeUndefined();
|
|
78
83
|
});
|
|
79
84
|
it('claude tool requires mode and message and supports optional model', async () => {
|
|
80
85
|
const plugin = await ClaudeManagerPlugin({
|
|
@@ -122,14 +122,6 @@ describe('ContextTracker', () => {
|
|
|
122
122
|
expect(snap.totalCostUsd).toBe(0.8);
|
|
123
123
|
expect(snap.estimatedContextPercent).toBe(60);
|
|
124
124
|
});
|
|
125
|
-
it('checks token threshold', () => {
|
|
126
|
-
const tracker = new ContextTracker();
|
|
127
|
-
expect(tracker.isAboveTokenThreshold()).toBe(false);
|
|
128
|
-
tracker.recordResult({ inputTokens: 150_000 });
|
|
129
|
-
expect(tracker.isAboveTokenThreshold(200_000)).toBe(false);
|
|
130
|
-
tracker.recordResult({ inputTokens: 210_000 });
|
|
131
|
-
expect(tracker.isAboveTokenThreshold(200_000)).toBe(true);
|
|
132
|
-
});
|
|
133
125
|
it('accumulates session ID from results', () => {
|
|
134
126
|
const tracker = new ContextTracker();
|
|
135
127
|
tracker.recordResult({ sessionId: 'ses_abc' });
|
|
@@ -59,27 +59,6 @@ describe('GitOperations', () => {
|
|
|
59
59
|
const diff = await git.diff();
|
|
60
60
|
expect(diff.hasDiff).toBe(false);
|
|
61
61
|
});
|
|
62
|
-
it('returns current branch', async () => {
|
|
63
|
-
const dir = await createTestRepo();
|
|
64
|
-
const git = new GitOperations(dir);
|
|
65
|
-
const branch = await git.currentBranch();
|
|
66
|
-
// Could be 'main' or 'master' depending on git config
|
|
67
|
-
expect(branch.length).toBeGreaterThan(0);
|
|
68
|
-
});
|
|
69
|
-
it('returns recent commits', async () => {
|
|
70
|
-
const dir = await createTestRepo();
|
|
71
|
-
const git = new GitOperations(dir);
|
|
72
|
-
const log = await git.recentCommits(1);
|
|
73
|
-
expect(log).toContain('initial');
|
|
74
|
-
});
|
|
75
|
-
it('returns diffStat output', async () => {
|
|
76
|
-
const dir = await createTestRepo();
|
|
77
|
-
await writeFile(join(dir, 'README.md'), '# Changed\n');
|
|
78
|
-
const git = new GitOperations(dir);
|
|
79
|
-
const stat = await git.diffStat();
|
|
80
|
-
expect(stat).toContain('README.md');
|
|
81
|
-
expect(stat).toContain('changed');
|
|
82
|
-
});
|
|
83
62
|
it('handles commit failure on clean repo', async () => {
|
|
84
63
|
const dir = await createTestRepo();
|
|
85
64
|
const git = new GitOperations(dir);
|
|
@@ -1,45 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { PersistentManager } from '../src/manager/persistent-manager.js';
|
|
3
|
-
import { ContextTracker } from '../src/manager/context-tracker.js';
|
|
4
|
-
function emptyContext() {
|
|
5
|
-
return {
|
|
6
|
-
sessionId: null,
|
|
7
|
-
totalTurns: 0,
|
|
8
|
-
totalCostUsd: 0,
|
|
9
|
-
latestInputTokens: null,
|
|
10
|
-
latestOutputTokens: null,
|
|
11
|
-
contextWindowSize: null,
|
|
12
|
-
estimatedContextPercent: null,
|
|
13
|
-
warningLevel: 'ok',
|
|
14
|
-
compactionCount: 0,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
function createMockSessionController() {
|
|
18
|
-
return {
|
|
19
|
-
isActive: false,
|
|
20
|
-
sessionId: null,
|
|
21
|
-
sendMessage: vi.fn(async () => ({
|
|
22
|
-
sessionId: 'ses_mock',
|
|
23
|
-
events: [],
|
|
24
|
-
finalText: 'Task completed.',
|
|
25
|
-
turns: 2,
|
|
26
|
-
totalCostUsd: 0.03,
|
|
27
|
-
inputTokens: 20_000,
|
|
28
|
-
outputTokens: 1_000,
|
|
29
|
-
contextWindowSize: 200_000,
|
|
30
|
-
})),
|
|
31
|
-
compactSession: vi.fn(async () => ({
|
|
32
|
-
sessionId: 'ses_mock',
|
|
33
|
-
events: [],
|
|
34
|
-
finalText: 'Compacted.',
|
|
35
|
-
turns: 3,
|
|
36
|
-
totalCostUsd: 0.04,
|
|
37
|
-
})),
|
|
38
|
-
clearSession: vi.fn(async () => 'ses_mock'),
|
|
39
|
-
getContextSnapshot: vi.fn(() => emptyContext()),
|
|
40
|
-
tryRestore: vi.fn(async () => false),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
3
|
function createMockGitOps() {
|
|
44
4
|
return {
|
|
45
5
|
diff: vi.fn(async () => ({
|
|
@@ -55,28 +15,6 @@ function createMockGitOps() {
|
|
|
55
15
|
success: true,
|
|
56
16
|
output: 'HEAD is now at abc1234',
|
|
57
17
|
})),
|
|
58
|
-
diffStat: vi.fn(async () => '1 file changed, 1 insertion(+)'),
|
|
59
|
-
currentBranch: vi.fn(async () => 'main'),
|
|
60
|
-
recentCommits: vi.fn(async () => 'abc1234 initial'),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
function createMockStateStore() {
|
|
64
|
-
const runs = new Map();
|
|
65
|
-
return {
|
|
66
|
-
saveRun: vi.fn(async (run) => {
|
|
67
|
-
runs.set(run.id, structuredClone(run));
|
|
68
|
-
}),
|
|
69
|
-
getRun: vi.fn(async (_cwd, runId) => runs.get(runId) ?? null),
|
|
70
|
-
listRuns: vi.fn(async () => [...runs.values()]),
|
|
71
|
-
updateRun: vi.fn(async (_cwd, runId, update) => {
|
|
72
|
-
const existing = runs.get(runId);
|
|
73
|
-
if (!existing) {
|
|
74
|
-
throw new Error(`Run ${runId} not found`);
|
|
75
|
-
}
|
|
76
|
-
const updated = update(existing);
|
|
77
|
-
runs.set(runId, structuredClone(updated));
|
|
78
|
-
return updated;
|
|
79
|
-
}),
|
|
80
18
|
};
|
|
81
19
|
}
|
|
82
20
|
function createMockTranscriptStore() {
|
|
@@ -86,123 +24,25 @@ function createMockTranscriptStore() {
|
|
|
86
24
|
};
|
|
87
25
|
}
|
|
88
26
|
describe('PersistentManager', () => {
|
|
89
|
-
it('sends a message through the session controller', async () => {
|
|
90
|
-
const sessionCtrl = createMockSessionController();
|
|
91
|
-
const gitOps = createMockGitOps();
|
|
92
|
-
const store = createMockStateStore();
|
|
93
|
-
const tracker = new ContextTracker();
|
|
94
|
-
const manager = new PersistentManager(sessionCtrl, gitOps, store, tracker, createMockTranscriptStore());
|
|
95
|
-
const result = await manager.sendMessage('/tmp', 'implement feature X');
|
|
96
|
-
expect(sessionCtrl.sendMessage).toHaveBeenCalledWith('implement feature X', undefined, undefined);
|
|
97
|
-
expect(result.finalText).toBe('Task completed.');
|
|
98
|
-
expect(result.inputTokens).toBe(20_000);
|
|
99
|
-
expect(result.outputTokens).toBe(1_000);
|
|
100
|
-
expect(result.contextWindowSize).toBe(200_000);
|
|
101
|
-
expect(result.context).toBeDefined();
|
|
102
|
-
});
|
|
103
|
-
it('persists transcript events on sendMessage', async () => {
|
|
104
|
-
const sessionCtrl = createMockSessionController();
|
|
105
|
-
sessionCtrl.sendMessage.mockResolvedValueOnce({
|
|
106
|
-
sessionId: 'ses_tx',
|
|
107
|
-
events: [
|
|
108
|
-
{ type: 'init', text: 'init' },
|
|
109
|
-
{ type: 'assistant', text: 'Done.' },
|
|
110
|
-
{ type: 'result', text: 'Done.', turns: 1, totalCostUsd: 0.01 },
|
|
111
|
-
],
|
|
112
|
-
finalText: 'Done.',
|
|
113
|
-
turns: 1,
|
|
114
|
-
totalCostUsd: 0.01,
|
|
115
|
-
});
|
|
116
|
-
const transcriptStore = createMockTranscriptStore();
|
|
117
|
-
const manager = new PersistentManager(sessionCtrl, createMockGitOps(), createMockStateStore(), new ContextTracker(), transcriptStore);
|
|
118
|
-
await manager.sendMessage('/tmp', 'do something');
|
|
119
|
-
expect(transcriptStore.appendEvents).toHaveBeenCalledWith('/tmp', 'ses_tx', [
|
|
120
|
-
{ type: 'init', text: 'init' },
|
|
121
|
-
{ type: 'assistant', text: 'Done.' },
|
|
122
|
-
{ type: 'result', text: 'Done.', turns: 1, totalCostUsd: 0.01 },
|
|
123
|
-
]);
|
|
124
|
-
});
|
|
125
|
-
it('skips transcript persistence when events are empty', async () => {
|
|
126
|
-
const transcriptStore = createMockTranscriptStore();
|
|
127
|
-
const manager = new PersistentManager(createMockSessionController(), createMockGitOps(), createMockStateStore(), new ContextTracker(), transcriptStore);
|
|
128
|
-
await manager.sendMessage('/tmp', 'hello');
|
|
129
|
-
expect(transcriptStore.appendEvents).not.toHaveBeenCalled();
|
|
130
|
-
});
|
|
131
27
|
it('delegates git diff to GitOperations', async () => {
|
|
132
|
-
const sessionCtrl = createMockSessionController();
|
|
133
28
|
const gitOps = createMockGitOps();
|
|
134
|
-
const
|
|
135
|
-
const tracker = new ContextTracker();
|
|
136
|
-
const manager = new PersistentManager(sessionCtrl, gitOps, store, tracker, createMockTranscriptStore());
|
|
29
|
+
const manager = new PersistentManager(gitOps, createMockTranscriptStore());
|
|
137
30
|
const diff = await manager.gitDiff();
|
|
138
31
|
expect(diff.hasDiff).toBe(true);
|
|
139
32
|
expect(diff.stats.filesChanged).toBe(1);
|
|
140
33
|
});
|
|
141
34
|
it('delegates git commit to GitOperations', async () => {
|
|
142
|
-
const sessionCtrl = createMockSessionController();
|
|
143
35
|
const gitOps = createMockGitOps();
|
|
144
|
-
const
|
|
145
|
-
const tracker = new ContextTracker();
|
|
146
|
-
const manager = new PersistentManager(sessionCtrl, gitOps, store, tracker, createMockTranscriptStore());
|
|
36
|
+
const manager = new PersistentManager(gitOps, createMockTranscriptStore());
|
|
147
37
|
const result = await manager.gitCommit('feat: add X');
|
|
148
|
-
expect(gitOps.commit).toHaveBeenCalledWith('feat: add X');
|
|
38
|
+
expect(gitOps.commit).toHaveBeenCalledWith('feat: add X', undefined);
|
|
149
39
|
expect(result.success).toBe(true);
|
|
150
40
|
});
|
|
151
41
|
it('delegates git reset to GitOperations', async () => {
|
|
152
|
-
const sessionCtrl = createMockSessionController();
|
|
153
42
|
const gitOps = createMockGitOps();
|
|
154
|
-
const
|
|
155
|
-
const tracker = new ContextTracker();
|
|
156
|
-
const manager = new PersistentManager(sessionCtrl, gitOps, store, tracker, createMockTranscriptStore());
|
|
43
|
+
const manager = new PersistentManager(gitOps, createMockTranscriptStore());
|
|
157
44
|
const result = await manager.gitReset();
|
|
158
45
|
expect(gitOps.resetHard).toHaveBeenCalled();
|
|
159
46
|
expect(result.success).toBe(true);
|
|
160
47
|
});
|
|
161
|
-
it('executes a task with run tracking', async () => {
|
|
162
|
-
const sessionCtrl = createMockSessionController();
|
|
163
|
-
const gitOps = createMockGitOps();
|
|
164
|
-
const store = createMockStateStore();
|
|
165
|
-
const tracker = new ContextTracker();
|
|
166
|
-
const manager = new PersistentManager(sessionCtrl, gitOps, store, tracker, createMockTranscriptStore());
|
|
167
|
-
const result = await manager.executeTask('/tmp', 'add tests');
|
|
168
|
-
expect(result.run.status).toBe('completed');
|
|
169
|
-
expect(result.run.task).toBe('add tests');
|
|
170
|
-
expect(result.run.finalSummary).toBe('Task completed.');
|
|
171
|
-
expect(result.run.messages).toHaveLength(2);
|
|
172
|
-
expect(result.run.messages[0].direction).toBe('sent');
|
|
173
|
-
expect(result.run.messages[1].direction).toBe('received');
|
|
174
|
-
// Verify state was persisted
|
|
175
|
-
expect(store.saveRun).toHaveBeenCalled();
|
|
176
|
-
expect(store.updateRun).toHaveBeenCalled();
|
|
177
|
-
});
|
|
178
|
-
it('marks run as failed when session throws', async () => {
|
|
179
|
-
const sessionCtrl = createMockSessionController();
|
|
180
|
-
sessionCtrl.sendMessage.mockRejectedValueOnce(new Error('SDK connection failed'));
|
|
181
|
-
const gitOps = createMockGitOps();
|
|
182
|
-
const store = createMockStateStore();
|
|
183
|
-
const tracker = new ContextTracker();
|
|
184
|
-
const manager = new PersistentManager(sessionCtrl, gitOps, store, tracker, createMockTranscriptStore());
|
|
185
|
-
const result = await manager.executeTask('/tmp', 'failing task');
|
|
186
|
-
expect(result.run.status).toBe('failed');
|
|
187
|
-
expect(result.run.finalSummary).toBe('SDK connection failed');
|
|
188
|
-
});
|
|
189
|
-
it('clears session via controller', async () => {
|
|
190
|
-
const sessionCtrl = createMockSessionController();
|
|
191
|
-
const gitOps = createMockGitOps();
|
|
192
|
-
const store = createMockStateStore();
|
|
193
|
-
const tracker = new ContextTracker();
|
|
194
|
-
const manager = new PersistentManager(sessionCtrl, gitOps, store, tracker, createMockTranscriptStore());
|
|
195
|
-
const cleared = await manager.clearSession();
|
|
196
|
-
expect(cleared).toBe('ses_mock');
|
|
197
|
-
expect(sessionCtrl.clearSession).toHaveBeenCalled();
|
|
198
|
-
});
|
|
199
|
-
it('returns status from session controller', () => {
|
|
200
|
-
const sessionCtrl = createMockSessionController();
|
|
201
|
-
const gitOps = createMockGitOps();
|
|
202
|
-
const store = createMockStateStore();
|
|
203
|
-
const tracker = new ContextTracker();
|
|
204
|
-
const manager = new PersistentManager(sessionCtrl, gitOps, store, tracker, createMockTranscriptStore());
|
|
205
|
-
const status = manager.getStatus();
|
|
206
|
-
expect(status).toEqual(emptyContext());
|
|
207
|
-
});
|
|
208
48
|
});
|
|
@@ -2,9 +2,9 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import { managerPromptRegistry } from '../src/prompts/registry.js';
|
|
3
3
|
describe('managerPromptRegistry', () => {
|
|
4
4
|
it('gives the CTO explicit orchestration guidance', () => {
|
|
5
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('You are
|
|
5
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('You are a principal engineer orchestrating a team of AI-powered engineers');
|
|
6
6
|
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Task tool');
|
|
7
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('
|
|
7
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('dispatch two engineers with complementary perspectives');
|
|
8
8
|
expect(managerPromptRegistry.ctoSystemPrompt).toContain('question');
|
|
9
9
|
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Tom, John, Maya, Sara, and Alex');
|
|
10
10
|
expect(managerPromptRegistry.ctoSystemPrompt).not.toContain('clear_session');
|
|
@@ -13,21 +13,16 @@ describe('managerPromptRegistry', () => {
|
|
|
13
13
|
it('keeps the engineer wrapper prompt short and tool-focused', () => {
|
|
14
14
|
expect(managerPromptRegistry.engineerAgentPrompt).toContain('named engineer');
|
|
15
15
|
expect(managerPromptRegistry.engineerAgentPrompt).toContain('claude');
|
|
16
|
-
expect(managerPromptRegistry.engineerAgentPrompt).toContain('remembers prior turns');
|
|
16
|
+
expect(managerPromptRegistry.engineerAgentPrompt).toContain('remembers your prior turns');
|
|
17
17
|
expect(managerPromptRegistry.engineerAgentPrompt).toContain('wrapper context');
|
|
18
18
|
expect(managerPromptRegistry.engineerAgentPrompt).not.toContain('read/grep/glob');
|
|
19
19
|
});
|
|
20
20
|
it('keeps the engineer session prompt direct and repo-aware', () => {
|
|
21
21
|
expect(managerPromptRegistry.engineerSessionPrompt).toContain('expert software engineer');
|
|
22
|
-
expect(managerPromptRegistry.engineerSessionPrompt).toContain('
|
|
22
|
+
expect(managerPromptRegistry.engineerSessionPrompt).toContain('Start with the smallest investigation that resolves the key uncertainty');
|
|
23
23
|
expect(managerPromptRegistry.engineerSessionPrompt).toContain('Verify your own work');
|
|
24
24
|
expect(managerPromptRegistry.engineerSessionPrompt).toContain('Do not run git commit');
|
|
25
25
|
});
|
|
26
|
-
it('still exposes plan and free mode prefixes', () => {
|
|
27
|
-
expect(managerPromptRegistry.modePrefixes.plan).toContain('PLAN MODE');
|
|
28
|
-
expect(managerPromptRegistry.modePrefixes.plan).toContain('Read-only');
|
|
29
|
-
expect(managerPromptRegistry.modePrefixes.free).toBe('');
|
|
30
|
-
});
|
|
31
26
|
it('keeps context warnings available for engineer sessions', () => {
|
|
32
27
|
expect(managerPromptRegistry.contextWarnings.moderate).toContain('{percent}');
|
|
33
28
|
expect(managerPromptRegistry.contextWarnings.high).toContain('{turns}');
|
|
@@ -95,7 +95,7 @@ describe('reportClaudeEvent — via plugin onEvent chain', () => {
|
|
|
95
95
|
await executeClaude(plugin, ctx);
|
|
96
96
|
const call = metadata.mock.calls.find(([c]) => c?.title?.includes('→'))?.[0];
|
|
97
97
|
expect(call).toBeDefined();
|
|
98
|
-
expect(call.title).toBe('⚡ Tom →
|
|
98
|
+
expect(call.title).toBe('⚡ Tom → Reading: /foo.ts');
|
|
99
99
|
expect(call.metadata.toolName).toBe('read');
|
|
100
100
|
expect(call.metadata.toolId).toBe('call-abc');
|
|
101
101
|
expect(call.metadata.toolArgs).toEqual({ file_path: '/foo.ts' });
|
|
@@ -118,7 +118,7 @@ describe('reportClaudeEvent — via plugin onEvent chain', () => {
|
|
|
118
118
|
const { metadata, ctx } = makeContext(tempRoot, ENGINEER_AGENT_IDS.Tom, 'wrapper-2');
|
|
119
119
|
await executeClaude(plugin, ctx);
|
|
120
120
|
const call = metadata.mock.calls.find(([c]) => c?.title?.includes('→'))?.[0];
|
|
121
|
-
expect(call.title).toBe('⚡ Tom →
|
|
121
|
+
expect(call.title).toBe('⚡ Tom → Running: ls -la');
|
|
122
122
|
expect(call.metadata.toolArgs).toEqual({ command: 'ls -la' });
|
|
123
123
|
});
|
|
124
124
|
it('falls back to generic title and omits tool fields when event.text is not JSON', async () => {
|
|
@@ -180,7 +180,7 @@ describe('second invocation continuity', () => {
|
|
|
180
180
|
// ── Phase 1: first task via orchestrator (no real SDK needed) ──────────
|
|
181
181
|
const store = new TeamStateStore();
|
|
182
182
|
await store.setActiveTeam(tempRoot, 'cto-1');
|
|
183
|
-
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt'
|
|
183
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt');
|
|
184
184
|
await orchestrator.recordWrapperSession(tempRoot, 'cto-1', 'Tom', 'wrapper-tom-1');
|
|
185
185
|
await orchestrator.recordWrapperExchange(tempRoot, 'cto-1', 'Tom', 'wrapper-tom-1', 'explore', 'Investigate the auth flow', 'Found two race conditions in the token refresh path.');
|
|
186
186
|
// ── Phase 2: process restart ───────────────────────────────────────────
|
|
@@ -206,7 +206,7 @@ describe('second invocation continuity', () => {
|
|
|
206
206
|
// ── Phase 1: pre-seed Tom with a claudeSessionId ───────────────────────
|
|
207
207
|
const store = new TeamStateStore();
|
|
208
208
|
await store.setActiveTeam(tempRoot, 'cto-1');
|
|
209
|
-
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt'
|
|
209
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt');
|
|
210
210
|
await orchestrator.getOrCreateTeam(tempRoot, 'cto-1');
|
|
211
211
|
await store.updateTeam(tempRoot, 'cto-1', (team) => ({
|
|
212
212
|
...team,
|
|
@@ -35,7 +35,7 @@ describe('TeamOrchestrator', () => {
|
|
|
35
35
|
outputTokens: 300,
|
|
36
36
|
contextWindowSize: 200_000,
|
|
37
37
|
});
|
|
38
|
-
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt'
|
|
38
|
+
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt');
|
|
39
39
|
const first = await orchestrator.dispatchEngineer({
|
|
40
40
|
teamId: 'team-1',
|
|
41
41
|
cwd: tempRoot,
|
|
@@ -55,12 +55,14 @@ describe('TeamOrchestrator', () => {
|
|
|
55
55
|
expect(runTask.mock.calls[0]?.[0]).toMatchObject({
|
|
56
56
|
systemPrompt: expect.stringContaining('Assigned engineer: Tom.'),
|
|
57
57
|
resumeSessionId: undefined,
|
|
58
|
-
permissionMode: '
|
|
58
|
+
permissionMode: 'acceptEdits',
|
|
59
|
+
restrictWriteTools: true,
|
|
59
60
|
});
|
|
60
61
|
expect(runTask.mock.calls[1]?.[0]).toMatchObject({
|
|
61
62
|
systemPrompt: undefined,
|
|
62
63
|
resumeSessionId: 'ses_tom',
|
|
63
64
|
permissionMode: 'acceptEdits',
|
|
65
|
+
restrictWriteTools: false,
|
|
64
66
|
});
|
|
65
67
|
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
66
68
|
expect(team.engineers.find((engineer) => engineer.name === 'Tom')).toMatchObject({
|
|
@@ -72,7 +74,7 @@ describe('TeamOrchestrator', () => {
|
|
|
72
74
|
it('rejects work when the same engineer is already busy', async () => {
|
|
73
75
|
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
74
76
|
const store = new TeamStateStore('.state');
|
|
75
|
-
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt'
|
|
77
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt');
|
|
76
78
|
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
77
79
|
await store.saveTeam({
|
|
78
80
|
...team,
|
|
@@ -110,7 +112,7 @@ describe('TeamOrchestrator', () => {
|
|
|
110
112
|
events: [],
|
|
111
113
|
finalText: '## Synthesis\nCombined plan\n## Recommended Question\nShould we migrate now?\n## Recommended Answer\nNo, defer it.',
|
|
112
114
|
});
|
|
113
|
-
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt'
|
|
115
|
+
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt');
|
|
114
116
|
const result = await orchestrator.planWithTeam({
|
|
115
117
|
teamId: 'team-1',
|
|
116
118
|
cwd: tempRoot,
|
|
@@ -126,7 +128,7 @@ describe('TeamOrchestrator', () => {
|
|
|
126
128
|
});
|
|
127
129
|
it('persists wrapper session memory for an engineer', async () => {
|
|
128
130
|
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
129
|
-
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt'
|
|
131
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt');
|
|
130
132
|
await orchestrator.recordWrapperSession(tempRoot, 'team-1', 'Tom', 'wrapper-tom');
|
|
131
133
|
await orchestrator.recordWrapperExchange(tempRoot, 'team-1', 'Tom', 'wrapper-tom', 'explore', 'Investigate the auth flow and compare approaches', 'The auth flow uses one shared validator and the cookie refresh path is the main risk.');
|
|
132
134
|
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
@@ -143,41 +143,41 @@ describe('ToolApprovalManager', () => {
|
|
|
143
143
|
});
|
|
144
144
|
});
|
|
145
145
|
describe('policy management', () => {
|
|
146
|
-
it('addRule inserts at specified position', () => {
|
|
146
|
+
it('addRule inserts at specified position', async () => {
|
|
147
147
|
const manager = new ToolApprovalManager({
|
|
148
148
|
rules: [],
|
|
149
149
|
enabled: true,
|
|
150
150
|
defaultAction: 'allow',
|
|
151
151
|
});
|
|
152
|
-
manager.addRule({ id: 'a', toolPattern: 'A', action: 'allow' });
|
|
153
|
-
manager.addRule({ id: 'b', toolPattern: 'B', action: 'allow' });
|
|
154
|
-
manager.addRule({ id: 'c', toolPattern: 'C', action: 'deny' }, 1);
|
|
152
|
+
await manager.addRule({ id: 'a', toolPattern: 'A', action: 'allow' });
|
|
153
|
+
await manager.addRule({ id: 'b', toolPattern: 'B', action: 'allow' });
|
|
154
|
+
await manager.addRule({ id: 'c', toolPattern: 'C', action: 'deny' }, 1);
|
|
155
155
|
const rules = manager.getPolicy().rules;
|
|
156
156
|
expect(rules.map((r) => r.id)).toEqual(['a', 'c', 'b']);
|
|
157
157
|
});
|
|
158
|
-
it('addRule appends to end by default', () => {
|
|
158
|
+
it('addRule appends to end by default', async () => {
|
|
159
159
|
const manager = new ToolApprovalManager({
|
|
160
160
|
rules: [],
|
|
161
161
|
enabled: true,
|
|
162
162
|
defaultAction: 'allow',
|
|
163
163
|
});
|
|
164
|
-
manager.addRule({ id: 'a', toolPattern: 'A', action: 'allow' });
|
|
165
|
-
manager.addRule({ id: 'b', toolPattern: 'B', action: 'allow' });
|
|
164
|
+
await manager.addRule({ id: 'a', toolPattern: 'A', action: 'allow' });
|
|
165
|
+
await manager.addRule({ id: 'b', toolPattern: 'B', action: 'allow' });
|
|
166
166
|
expect(manager.getPolicy().rules.map((r) => r.id)).toEqual(['a', 'b']);
|
|
167
167
|
});
|
|
168
|
-
it('removeRule removes by ID and returns true', () => {
|
|
168
|
+
it('removeRule removes by ID and returns true', async () => {
|
|
169
169
|
const manager = new ToolApprovalManager();
|
|
170
|
-
const removed = manager.removeRule('allow-read');
|
|
170
|
+
const removed = await manager.removeRule('allow-read');
|
|
171
171
|
expect(removed).toBe(true);
|
|
172
172
|
expect(manager.getPolicy().rules.find((r) => r.id === 'allow-read')).toBeUndefined();
|
|
173
173
|
});
|
|
174
|
-
it('removeRule returns false for non-existent ID', () => {
|
|
174
|
+
it('removeRule returns false for non-existent ID', async () => {
|
|
175
175
|
const manager = new ToolApprovalManager();
|
|
176
|
-
expect(manager.removeRule('nonexistent')).toBe(false);
|
|
176
|
+
expect(await manager.removeRule('nonexistent')).toBe(false);
|
|
177
177
|
});
|
|
178
|
-
it('setPolicy replaces entire policy', () => {
|
|
178
|
+
it('setPolicy replaces entire policy', async () => {
|
|
179
179
|
const manager = new ToolApprovalManager();
|
|
180
|
-
manager.setPolicy({
|
|
180
|
+
await manager.setPolicy({
|
|
181
181
|
rules: [{ id: 'only', toolPattern: '*', action: 'deny' }],
|
|
182
182
|
defaultAction: 'deny',
|
|
183
183
|
enabled: true,
|
|
@@ -185,16 +185,16 @@ describe('ToolApprovalManager', () => {
|
|
|
185
185
|
expect(manager.getPolicy().rules).toHaveLength(1);
|
|
186
186
|
expect(manager.getPolicy().defaultAction).toBe('deny');
|
|
187
187
|
});
|
|
188
|
-
it('setDefaultAction changes the fallback', () => {
|
|
188
|
+
it('setDefaultAction changes the fallback', async () => {
|
|
189
189
|
const manager = new ToolApprovalManager({
|
|
190
190
|
rules: [],
|
|
191
191
|
enabled: true,
|
|
192
192
|
defaultAction: 'allow',
|
|
193
193
|
});
|
|
194
|
-
manager.setDefaultAction('deny');
|
|
194
|
+
await manager.setDefaultAction('deny');
|
|
195
195
|
expect(manager.evaluate('Unknown', {}).behavior).toBe('deny');
|
|
196
196
|
});
|
|
197
|
-
it('setEnabled toggles the manager', () => {
|
|
197
|
+
it('setEnabled toggles the manager', async () => {
|
|
198
198
|
const manager = new ToolApprovalManager({
|
|
199
199
|
rules: [
|
|
200
200
|
{
|
|
@@ -208,7 +208,7 @@ describe('ToolApprovalManager', () => {
|
|
|
208
208
|
defaultAction: 'deny',
|
|
209
209
|
});
|
|
210
210
|
expect(manager.evaluate('Read', {}).behavior).toBe('deny');
|
|
211
|
-
manager.setEnabled(false);
|
|
211
|
+
await manager.setEnabled(false);
|
|
212
212
|
expect(manager.evaluate('Read', {}).behavior).toBe('allow');
|
|
213
213
|
});
|
|
214
214
|
});
|