@doingdev/opencode-claude-manager-plugin 0.1.44 → 0.1.47
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 +29 -31
- package/dist/index.d.ts +1 -1
- package/dist/manager/persistent-manager.d.ts +3 -23
- package/dist/manager/persistent-manager.js +2 -95
- package/dist/manager/team-orchestrator.d.ts +50 -0
- package/dist/manager/team-orchestrator.js +360 -0
- package/dist/plugin/agent-hierarchy.d.ts +12 -34
- package/dist/plugin/agent-hierarchy.js +36 -129
- package/dist/plugin/claude-manager.plugin.js +190 -445
- package/dist/plugin/service-factory.d.ts +18 -3
- package/dist/plugin/service-factory.js +32 -1
- package/dist/prompts/registry.d.ts +1 -10
- package/dist/prompts/registry.js +42 -270
- package/dist/src/claude/claude-agent-sdk-adapter.js +2 -1
- package/dist/src/claude/session-live-tailer.js +2 -2
- package/dist/src/index.d.ts +1 -1
- package/dist/src/manager/git-operations.d.ts +10 -1
- package/dist/src/manager/git-operations.js +18 -3
- package/dist/src/manager/persistent-manager.d.ts +18 -6
- package/dist/src/manager/persistent-manager.js +19 -13
- package/dist/src/manager/session-controller.d.ts +7 -10
- package/dist/src/manager/session-controller.js +12 -62
- package/dist/src/manager/team-orchestrator.d.ts +50 -0
- package/dist/src/manager/team-orchestrator.js +360 -0
- package/dist/src/plugin/agent-hierarchy.d.ts +12 -26
- package/dist/src/plugin/agent-hierarchy.js +36 -99
- package/dist/src/plugin/claude-manager.plugin.js +214 -393
- package/dist/src/plugin/service-factory.d.ts +18 -3
- package/dist/src/plugin/service-factory.js +33 -9
- package/dist/src/prompts/registry.d.ts +1 -10
- package/dist/src/prompts/registry.js +41 -246
- package/dist/src/state/team-state-store.d.ts +14 -0
- package/dist/src/state/team-state-store.js +85 -0
- package/dist/src/team/roster.d.ts +5 -0
- package/dist/src/team/roster.js +38 -0
- package/dist/src/types/contracts.d.ts +55 -13
- package/dist/src/types/contracts.js +1 -1
- package/dist/state/team-state-store.d.ts +14 -0
- package/dist/state/team-state-store.js +85 -0
- package/dist/team/roster.d.ts +5 -0
- package/dist/team/roster.js +38 -0
- package/dist/test/claude-manager.plugin.test.js +55 -280
- package/dist/test/git-operations.test.js +65 -1
- package/dist/test/persistent-manager.test.js +3 -3
- package/dist/test/prompt-registry.test.js +32 -252
- package/dist/test/session-controller.test.js +27 -27
- package/dist/test/team-orchestrator.test.d.ts +1 -0
- package/dist/test/team-orchestrator.test.js +146 -0
- package/dist/test/team-state-store.test.d.ts +1 -0
- package/dist/test/team-state-store.test.js +54 -0
- package/dist/types/contracts.d.ts +50 -23
- package/dist/types/contracts.js +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
|
-
import { mkdtemp, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { GitOperations } from '../src/manager/git-operations.js';
|
|
@@ -87,4 +87,68 @@ describe('GitOperations', () => {
|
|
|
87
87
|
expect(result.success).toBe(false);
|
|
88
88
|
expect(result.error).toBeDefined();
|
|
89
89
|
});
|
|
90
|
+
it('filters diff by paths', async () => {
|
|
91
|
+
const dir = await createTestRepo();
|
|
92
|
+
await mkdir(join(dir, 'src'), { recursive: true });
|
|
93
|
+
await writeFile(join(dir, 'src/index.ts'), 'export const x = 1;\n');
|
|
94
|
+
await writeFile(join(dir, 'README.md'), '# Changed\n');
|
|
95
|
+
const git = new GitOperations(dir);
|
|
96
|
+
// Stage the new file so git diff can see it
|
|
97
|
+
await execFileAsync('git', ['add', 'src/index.ts'], { cwd: dir });
|
|
98
|
+
const result = await git.diff({ paths: ['src'] });
|
|
99
|
+
expect(result.hasDiff).toBe(true);
|
|
100
|
+
expect(result.diffText).toContain('src/index.ts');
|
|
101
|
+
expect(result.diffText).not.toContain('README.md');
|
|
102
|
+
});
|
|
103
|
+
it('shows staged diff when staged=true', async () => {
|
|
104
|
+
const dir = await createTestRepo();
|
|
105
|
+
await writeFile(join(dir, 'README.md'), '# Changed\n');
|
|
106
|
+
const git = new GitOperations(dir);
|
|
107
|
+
// First commit the current state
|
|
108
|
+
await execFileAsync('git', ['add', 'README.md'], { cwd: dir });
|
|
109
|
+
// Now make another change
|
|
110
|
+
await writeFile(join(dir, 'README.md'), '# Changed more\n');
|
|
111
|
+
await execFileAsync('git', ['add', 'README.md'], { cwd: dir });
|
|
112
|
+
const result = await git.diff({ staged: true });
|
|
113
|
+
expect(result.hasDiff).toBe(true);
|
|
114
|
+
expect(result.diffText).toContain('Changed more');
|
|
115
|
+
});
|
|
116
|
+
it('compares against arbitrary ref', async () => {
|
|
117
|
+
const dir = await createTestRepo();
|
|
118
|
+
await writeFile(join(dir, 'README.md'), '# Changed\n');
|
|
119
|
+
const git = new GitOperations(dir);
|
|
120
|
+
const result = await git.diff({ ref: 'HEAD' });
|
|
121
|
+
expect(result.hasDiff).toBe(true);
|
|
122
|
+
expect(result.diffText).toContain('Changed');
|
|
123
|
+
});
|
|
124
|
+
it('returns status with isClean false when changes exist', async () => {
|
|
125
|
+
const dir = await createTestRepo();
|
|
126
|
+
await writeFile(join(dir, 'README.md'), '# Changed\n');
|
|
127
|
+
const git = new GitOperations(dir);
|
|
128
|
+
const result = await git.status();
|
|
129
|
+
expect(result.isClean).toBe(false);
|
|
130
|
+
expect(result.output).toContain('README.md');
|
|
131
|
+
});
|
|
132
|
+
it('returns status with isClean true on clean repo', async () => {
|
|
133
|
+
const dir = await createTestRepo();
|
|
134
|
+
const git = new GitOperations(dir);
|
|
135
|
+
const result = await git.status();
|
|
136
|
+
expect(result.isClean).toBe(true);
|
|
137
|
+
expect(result.output).toBe('');
|
|
138
|
+
});
|
|
139
|
+
it('returns log with custom count', async () => {
|
|
140
|
+
const dir = await createTestRepo();
|
|
141
|
+
const git = new GitOperations(dir);
|
|
142
|
+
// Add more commits
|
|
143
|
+
await writeFile(join(dir, 'file1.txt'), '1');
|
|
144
|
+
await execFileAsync('git', ['add', '-A'], { cwd: dir });
|
|
145
|
+
await execFileAsync('git', ['commit', '-m', 'commit 1'], { cwd: dir });
|
|
146
|
+
await writeFile(join(dir, 'file2.txt'), '2');
|
|
147
|
+
await execFileAsync('git', ['add', '-A'], { cwd: dir });
|
|
148
|
+
await execFileAsync('git', ['commit', '-m', 'commit 2'], { cwd: dir });
|
|
149
|
+
const result = await git.log(3);
|
|
150
|
+
expect(result).toContain('commit 2');
|
|
151
|
+
expect(result).toContain('commit 1');
|
|
152
|
+
expect(result).toContain('initial');
|
|
153
|
+
});
|
|
90
154
|
});
|
|
@@ -93,7 +93,7 @@ describe('PersistentManager', () => {
|
|
|
93
93
|
const tracker = new ContextTracker();
|
|
94
94
|
const manager = new PersistentManager(sessionCtrl, gitOps, store, tracker, createMockTranscriptStore());
|
|
95
95
|
const result = await manager.sendMessage('/tmp', 'implement feature X');
|
|
96
|
-
expect(sessionCtrl.sendMessage).toHaveBeenCalledWith('
|
|
96
|
+
expect(sessionCtrl.sendMessage).toHaveBeenCalledWith('implement feature X', undefined, undefined);
|
|
97
97
|
expect(result.finalText).toBe('Task completed.');
|
|
98
98
|
expect(result.inputTokens).toBe(20_000);
|
|
99
99
|
expect(result.outputTokens).toBe(1_000);
|
|
@@ -192,9 +192,9 @@ describe('PersistentManager', () => {
|
|
|
192
192
|
const store = createMockStateStore();
|
|
193
193
|
const tracker = new ContextTracker();
|
|
194
194
|
const manager = new PersistentManager(sessionCtrl, gitOps, store, tracker, createMockTranscriptStore());
|
|
195
|
-
const cleared = await manager.clearSession(
|
|
195
|
+
const cleared = await manager.clearSession();
|
|
196
196
|
expect(cleared).toBe('ses_mock');
|
|
197
|
-
expect(sessionCtrl.clearSession).
|
|
197
|
+
expect(sessionCtrl.clearSession).toHaveBeenCalled();
|
|
198
198
|
});
|
|
199
199
|
it('returns status from session controller', () => {
|
|
200
200
|
const sessionCtrl = createMockSessionController();
|
|
@@ -1,256 +1,36 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { managerPromptRegistry } from '../src/prompts/registry.js';
|
|
3
3
|
describe('managerPromptRegistry', () => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('self-contained');
|
|
36
|
-
});
|
|
37
|
-
it('requires review after every delegation', () => {
|
|
38
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Review every change');
|
|
39
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('read the FULL diff');
|
|
40
|
-
});
|
|
41
|
-
it('forbids delegating without verification criteria', () => {
|
|
42
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Do NOT delegate without verification criteria');
|
|
43
|
-
});
|
|
44
|
-
it('establishes delegation-first as core principle', () => {
|
|
45
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('delegation-first');
|
|
46
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('default action is to delegate');
|
|
47
|
-
});
|
|
48
|
-
it('limits direct lookups before delegating', () => {
|
|
49
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('more than 2 direct read/grep/glob lookups');
|
|
50
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('stop and delegate');
|
|
51
|
-
});
|
|
52
|
-
it('prefers engineer_plan for repo exploration', () => {
|
|
53
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Prefer spawning `engineer_plan` for repo exploration');
|
|
54
|
-
});
|
|
55
|
-
it('instructs using the Task tool to invoke engineers', () => {
|
|
56
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Task tool');
|
|
57
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('engineer_plan');
|
|
58
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('engineer_build');
|
|
59
|
-
});
|
|
60
|
-
it('forbids direct engineer_* tool calls', () => {
|
|
61
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Do NOT call any engineer_* tools directly');
|
|
62
|
-
});
|
|
63
|
-
it('includes context efficiency guidance', () => {
|
|
64
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Context efficiency');
|
|
65
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('freshSession');
|
|
66
|
-
});
|
|
67
|
-
it('references CTO-owned tools', () => {
|
|
68
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('git_diff');
|
|
69
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('git_commit');
|
|
70
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('git_reset');
|
|
71
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('todowrite');
|
|
72
|
-
});
|
|
73
|
-
it('does not reference Claude Code session internals', () => {
|
|
74
|
-
expect(managerPromptRegistry.ctoSystemPrompt).not.toContain('compact_context');
|
|
75
|
-
expect(managerPromptRegistry.ctoSystemPrompt).not.toContain('session_health');
|
|
76
|
-
expect(managerPromptRegistry.ctoSystemPrompt).not.toContain('clear_session');
|
|
77
|
-
});
|
|
78
|
-
it('includes autonomy blockers', () => {
|
|
79
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Autonomy blockers');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
describe('engineerPlanPrompt', () => {
|
|
83
|
-
it('establishes staff engineer identity, not a relay', () => {
|
|
84
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('staff engineer');
|
|
85
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('not a forwarding layer');
|
|
86
|
-
});
|
|
87
|
-
it('includes repo-context investigation guidance', () => {
|
|
88
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('Repo-context investigation');
|
|
89
|
-
});
|
|
90
|
-
it('uses read/grep/glob sparingly', () => {
|
|
91
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('read/grep/glob sparingly');
|
|
92
|
-
});
|
|
93
|
-
it('delegates after more than 2 lookups', () => {
|
|
94
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('more than 2 lookups are needed, send the investigation to the engineer');
|
|
95
|
-
});
|
|
96
|
-
it('includes architecture framing guidance', () => {
|
|
97
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('real problem');
|
|
98
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('cleanest architecture');
|
|
99
|
-
});
|
|
100
|
-
it('allows one high-leverage clarification', () => {
|
|
101
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('ONE clarification first if it materially improves architecture');
|
|
102
|
-
});
|
|
103
|
-
it('forbids implementation by the wrapper itself', () => {
|
|
104
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('Do NOT implement changes yourself');
|
|
105
|
-
});
|
|
106
|
-
it('does not prohibit self-investigation', () => {
|
|
107
|
-
expect(managerPromptRegistry.engineerPlanPrompt).not.toContain('Do NOT investigate on your own');
|
|
108
|
-
});
|
|
109
|
-
it('instructs using the plan-mode send tool', () => {
|
|
110
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('explore');
|
|
111
|
-
});
|
|
112
|
-
it('includes context management', () => {
|
|
113
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('session_health');
|
|
114
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('compact_context');
|
|
115
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('clear_session');
|
|
116
|
-
});
|
|
117
|
-
it('includes freshSession guidance', () => {
|
|
118
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('freshSession:true');
|
|
119
|
-
});
|
|
120
|
-
it('requires verbatim response', () => {
|
|
121
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('verbatim');
|
|
122
|
-
});
|
|
123
|
-
it('includes Project Claude Files guidance', () => {
|
|
124
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('Using appended Project Claude Files');
|
|
125
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('Extract only the rules relevant to the current task');
|
|
126
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('specific/nested paths over root-level guidance');
|
|
127
|
-
expect(managerPromptRegistry.engineerPlanPrompt).toContain('Direct user instructions override Claude-file guidance');
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
describe('engineerBuildPrompt', () => {
|
|
131
|
-
it('establishes staff engineer identity, not a relay', () => {
|
|
132
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('staff engineer');
|
|
133
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('not a forwarding layer');
|
|
134
|
-
});
|
|
135
|
-
it('includes repo-context investigation guidance', () => {
|
|
136
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('Repo-context investigation');
|
|
137
|
-
});
|
|
138
|
-
it('uses read/grep/glob sparingly', () => {
|
|
139
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('read/grep/glob sparingly');
|
|
140
|
-
});
|
|
141
|
-
it('delegates after more than 2 lookups', () => {
|
|
142
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('more than 2 lookups are needed, send the investigation to the engineer');
|
|
143
|
-
});
|
|
144
|
-
it('includes architecture framing guidance', () => {
|
|
145
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('real problem');
|
|
146
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('cleanest architecture');
|
|
147
|
-
});
|
|
148
|
-
it('allows one high-leverage clarification', () => {
|
|
149
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('ONE clarification first if it materially improves architecture');
|
|
150
|
-
});
|
|
151
|
-
it('forbids implementation by the wrapper itself', () => {
|
|
152
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('Do NOT implement changes yourself');
|
|
153
|
-
});
|
|
154
|
-
it('does not prohibit self-investigation', () => {
|
|
155
|
-
expect(managerPromptRegistry.engineerBuildPrompt).not.toContain('Do NOT investigate on your own');
|
|
156
|
-
});
|
|
157
|
-
it('instructs using the build-mode send tool', () => {
|
|
158
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('implement');
|
|
159
|
-
});
|
|
160
|
-
it('includes context management', () => {
|
|
161
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('session_health');
|
|
162
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('compact_context');
|
|
163
|
-
});
|
|
164
|
-
it('includes freshSession guidance', () => {
|
|
165
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('freshSession:true');
|
|
166
|
-
});
|
|
167
|
-
it('includes effort max for hard problems', () => {
|
|
168
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('effort "max"');
|
|
169
|
-
});
|
|
170
|
-
it('requires verbatim response', () => {
|
|
171
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('verbatim');
|
|
172
|
-
});
|
|
173
|
-
it('includes Project Claude Files guidance', () => {
|
|
174
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('Using appended Project Claude Files');
|
|
175
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('Extract only the rules relevant to the current task');
|
|
176
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('specific/nested paths over root-level guidance');
|
|
177
|
-
expect(managerPromptRegistry.engineerBuildPrompt).toContain('Direct user instructions override Claude-file guidance');
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
describe('engineerSessionPrompt', () => {
|
|
181
|
-
it('establishes expert engineer identity', () => {
|
|
182
|
-
expect(managerPromptRegistry.engineerSessionPrompt).toContain('expert engineer');
|
|
183
|
-
});
|
|
184
|
-
it('requires self-verification', () => {
|
|
185
|
-
expect(managerPromptRegistry.engineerSessionPrompt).toContain('verify your own work before reporting done');
|
|
186
|
-
});
|
|
187
|
-
it('runs tests even if not explicitly asked', () => {
|
|
188
|
-
expect(managerPromptRegistry.engineerSessionPrompt).toContain('still run relevant tests if they exist');
|
|
189
|
-
});
|
|
190
|
-
it('forbids git operations', () => {
|
|
191
|
-
expect(managerPromptRegistry.engineerSessionPrompt).toContain('git commit');
|
|
192
|
-
expect(managerPromptRegistry.engineerSessionPrompt).toContain('do NOT run');
|
|
193
|
-
});
|
|
194
|
-
it('requires structured reporting', () => {
|
|
195
|
-
expect(managerPromptRegistry.engineerSessionPrompt).toContain('what was done, what was verified, what passed/failed');
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
describe('modePrefixes', () => {
|
|
199
|
-
it('has both plan and free keys', () => {
|
|
200
|
-
expect(managerPromptRegistry.modePrefixes).toHaveProperty('plan');
|
|
201
|
-
expect(managerPromptRegistry.modePrefixes).toHaveProperty('free');
|
|
202
|
-
});
|
|
203
|
-
it('plan prefix enforces read-only', () => {
|
|
204
|
-
expect(managerPromptRegistry.modePrefixes.plan).toContain('PLAN MODE');
|
|
205
|
-
expect(managerPromptRegistry.modePrefixes.plan).toContain('Do NOT create or edit');
|
|
206
|
-
});
|
|
207
|
-
it('free prefix is empty', () => {
|
|
208
|
-
expect(managerPromptRegistry.modePrefixes.free).toBe('');
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
describe('contextWarnings', () => {
|
|
212
|
-
it('ships templates with placeholders', () => {
|
|
213
|
-
expect(managerPromptRegistry.contextWarnings.moderate).toContain('{percent}');
|
|
214
|
-
expect(managerPromptRegistry.contextWarnings.high).toContain('{turns}');
|
|
215
|
-
expect(managerPromptRegistry.contextWarnings.critical).toContain('Clear the session');
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
describe('composeWrapperPrompt', () => {
|
|
220
|
-
const base = 'You manage a Claude Code engineer.';
|
|
221
|
-
it('returns base prompt unchanged when no Claude files provided', () => {
|
|
222
|
-
expect(composeWrapperPrompt(base, [])).toBe(base);
|
|
223
|
-
});
|
|
224
|
-
it('appends a single file with path-labeled section', () => {
|
|
225
|
-
const files = [
|
|
226
|
-
{ relativePath: 'CLAUDE.md', content: 'Use pnpm. No default exports.' },
|
|
227
|
-
];
|
|
228
|
-
const result = composeWrapperPrompt(base, files);
|
|
229
|
-
expect(result).toContain('## Project Claude Files');
|
|
230
|
-
expect(result).toContain('### CLAUDE.md');
|
|
231
|
-
expect(result).toContain('Use pnpm. No default exports.');
|
|
232
|
-
});
|
|
233
|
-
it('appends multiple files with distinct path headers', () => {
|
|
234
|
-
const files = [
|
|
235
|
-
{ relativePath: 'CLAUDE.md', content: 'root rules' },
|
|
236
|
-
{ relativePath: '.claude/settings.md', content: 'settings content' },
|
|
237
|
-
{ relativePath: 'packages/core/CLAUDE.md', content: 'core rules' },
|
|
238
|
-
];
|
|
239
|
-
const result = composeWrapperPrompt(base, files);
|
|
240
|
-
expect(result).toContain('### CLAUDE.md\nroot rules');
|
|
241
|
-
expect(result).toContain('### .claude/settings.md\nsettings content');
|
|
242
|
-
expect(result).toContain('### packages/core/CLAUDE.md\ncore rules');
|
|
243
|
-
});
|
|
244
|
-
it('preserves multi-line file content', () => {
|
|
245
|
-
const files = [
|
|
246
|
-
{ relativePath: 'CLAUDE.md', content: 'Line one\nLine two\nLine three' },
|
|
247
|
-
];
|
|
248
|
-
const result = composeWrapperPrompt(base, files);
|
|
249
|
-
expect(result).toContain('Line one\nLine two\nLine three');
|
|
250
|
-
});
|
|
251
|
-
it('starts with the base prompt', () => {
|
|
252
|
-
const files = [{ relativePath: 'CLAUDE.md', content: 'rules' }];
|
|
253
|
-
const result = composeWrapperPrompt(base, files);
|
|
254
|
-
expect(result.startsWith(base)).toBe(true);
|
|
4
|
+
it('gives the CTO explicit orchestration guidance', () => {
|
|
5
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('You are the CTO');
|
|
6
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Task tool');
|
|
7
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('spawn two engineers in parallel');
|
|
8
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('question');
|
|
9
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Tom, John, Maya, Sara, and Alex');
|
|
10
|
+
expect(managerPromptRegistry.ctoSystemPrompt).not.toContain('clear_session');
|
|
11
|
+
expect(managerPromptRegistry.ctoSystemPrompt).not.toContain('freshSession');
|
|
12
|
+
});
|
|
13
|
+
it('keeps the engineer wrapper prompt short and tool-focused', () => {
|
|
14
|
+
expect(managerPromptRegistry.engineerAgentPrompt).toContain('named engineer');
|
|
15
|
+
expect(managerPromptRegistry.engineerAgentPrompt).toContain('claude');
|
|
16
|
+
expect(managerPromptRegistry.engineerAgentPrompt).toContain('remembers prior turns');
|
|
17
|
+
expect(managerPromptRegistry.engineerAgentPrompt).toContain('wrapper context');
|
|
18
|
+
expect(managerPromptRegistry.engineerAgentPrompt).not.toContain('read/grep/glob');
|
|
19
|
+
});
|
|
20
|
+
it('keeps the engineer session prompt direct and repo-aware', () => {
|
|
21
|
+
expect(managerPromptRegistry.engineerSessionPrompt).toContain('expert software engineer');
|
|
22
|
+
expect(managerPromptRegistry.engineerSessionPrompt).toContain('Execute directly');
|
|
23
|
+
expect(managerPromptRegistry.engineerSessionPrompt).toContain('Verify your own work');
|
|
24
|
+
expect(managerPromptRegistry.engineerSessionPrompt).toContain('Do not run git commit');
|
|
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
|
+
it('keeps context warnings available for engineer sessions', () => {
|
|
32
|
+
expect(managerPromptRegistry.contextWarnings.moderate).toContain('{percent}');
|
|
33
|
+
expect(managerPromptRegistry.contextWarnings.high).toContain('{turns}');
|
|
34
|
+
expect(managerPromptRegistry.contextWarnings.critical).toContain('near capacity');
|
|
255
35
|
});
|
|
256
36
|
});
|
|
@@ -25,9 +25,9 @@ describe('SessionController', () => {
|
|
|
25
25
|
it('creates a new session on first sendMessage', async () => {
|
|
26
26
|
const adapter = createMockAdapter();
|
|
27
27
|
const tracker = new ContextTracker();
|
|
28
|
-
const controller = new SessionController(adapter, tracker, 'test prompt');
|
|
28
|
+
const controller = new SessionController(adapter, tracker, 'test prompt', 'test', '/tmp');
|
|
29
29
|
expect(controller.isActive).toBe(false);
|
|
30
|
-
const result = await controller.sendMessage('
|
|
30
|
+
const result = await controller.sendMessage('hello');
|
|
31
31
|
expect(controller.isActive).toBe(true);
|
|
32
32
|
expect(controller.sessionId).toBe('ses_test');
|
|
33
33
|
expect(result.finalText).toBe('Done.');
|
|
@@ -39,17 +39,17 @@ describe('SessionController', () => {
|
|
|
39
39
|
it('sends settingSources as user-only', async () => {
|
|
40
40
|
const adapter = createMockAdapter();
|
|
41
41
|
const tracker = new ContextTracker();
|
|
42
|
-
const controller = new SessionController(adapter, tracker, 'test');
|
|
43
|
-
await controller.sendMessage('
|
|
42
|
+
const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp');
|
|
43
|
+
await controller.sendMessage('hello');
|
|
44
44
|
const call = adapter.runSession.mock.calls[0][0];
|
|
45
45
|
expect(call.settingSources).toEqual(['user']);
|
|
46
46
|
});
|
|
47
47
|
it('resumes session on subsequent sends', async () => {
|
|
48
48
|
const adapter = createMockAdapter([{}, {}]);
|
|
49
49
|
const tracker = new ContextTracker();
|
|
50
|
-
const controller = new SessionController(adapter, tracker, 'test prompt');
|
|
51
|
-
await controller.sendMessage('
|
|
52
|
-
await controller.sendMessage('
|
|
50
|
+
const controller = new SessionController(adapter, tracker, 'test prompt', 'test', '/tmp');
|
|
51
|
+
await controller.sendMessage('first');
|
|
52
|
+
await controller.sendMessage('second');
|
|
53
53
|
const secondCall = adapter.runSession.mock.calls[1][0];
|
|
54
54
|
expect(secondCall.resumeSessionId).toBe('ses_test');
|
|
55
55
|
expect(secondCall.systemPrompt).toBeUndefined();
|
|
@@ -57,8 +57,8 @@ describe('SessionController', () => {
|
|
|
57
57
|
it('updates context tracker on each message', async () => {
|
|
58
58
|
const adapter = createMockAdapter([{ turns: 3, totalCostUsd: 0.05, inputTokens: 50_000 }]);
|
|
59
59
|
const tracker = new ContextTracker();
|
|
60
|
-
const controller = new SessionController(adapter, tracker, 'test');
|
|
61
|
-
await controller.sendMessage('
|
|
60
|
+
const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp');
|
|
61
|
+
await controller.sendMessage('task');
|
|
62
62
|
const snap = controller.getContextSnapshot();
|
|
63
63
|
expect(snap.totalTurns).toBe(3);
|
|
64
64
|
expect(snap.totalCostUsd).toBe(0.05);
|
|
@@ -68,10 +68,10 @@ describe('SessionController', () => {
|
|
|
68
68
|
it('clears session and resets context', async () => {
|
|
69
69
|
const adapter = createMockAdapter();
|
|
70
70
|
const tracker = new ContextTracker();
|
|
71
|
-
const controller = new SessionController(adapter, tracker, 'test');
|
|
72
|
-
await controller.sendMessage('
|
|
71
|
+
const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp');
|
|
72
|
+
await controller.sendMessage('task');
|
|
73
73
|
expect(controller.isActive).toBe(true);
|
|
74
|
-
const clearedId = await controller.clearSession(
|
|
74
|
+
const clearedId = await controller.clearSession();
|
|
75
75
|
expect(clearedId).toBe('ses_test');
|
|
76
76
|
expect(controller.isActive).toBe(false);
|
|
77
77
|
expect(controller.sessionId).toBeNull();
|
|
@@ -82,9 +82,9 @@ describe('SessionController', () => {
|
|
|
82
82
|
it('sends /compact to current session', async () => {
|
|
83
83
|
const adapter = createMockAdapter([{}, {}]);
|
|
84
84
|
const tracker = new ContextTracker();
|
|
85
|
-
const controller = new SessionController(adapter, tracker, 'test');
|
|
86
|
-
await controller.sendMessage('
|
|
87
|
-
await controller.compactSession(
|
|
85
|
+
const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp');
|
|
86
|
+
await controller.sendMessage('task');
|
|
87
|
+
await controller.compactSession();
|
|
88
88
|
const compactCall = adapter.runSession.mock.calls[1][0];
|
|
89
89
|
expect(compactCall.prompt).toBe('/compact');
|
|
90
90
|
expect(compactCall.resumeSessionId).toBe('ses_test');
|
|
@@ -92,14 +92,14 @@ describe('SessionController', () => {
|
|
|
92
92
|
it('throws when compacting without active session', async () => {
|
|
93
93
|
const adapter = createMockAdapter();
|
|
94
94
|
const tracker = new ContextTracker();
|
|
95
|
-
const controller = new SessionController(adapter, tracker, 'test');
|
|
96
|
-
await expect(controller.compactSession(
|
|
95
|
+
const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp');
|
|
96
|
+
await expect(controller.compactSession()).rejects.toThrow('No active session to compact');
|
|
97
97
|
});
|
|
98
98
|
it('threads effort through to the SDK input', async () => {
|
|
99
99
|
const adapter = createMockAdapter();
|
|
100
100
|
const tracker = new ContextTracker();
|
|
101
|
-
const controller = new SessionController(adapter, tracker, 'test');
|
|
102
|
-
await controller.sendMessage('
|
|
101
|
+
const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp');
|
|
102
|
+
await controller.sendMessage('hard task', { effort: 'max' });
|
|
103
103
|
const call = adapter.runSession.mock.calls[0][0];
|
|
104
104
|
expect(call.effort).toBe('max');
|
|
105
105
|
});
|
|
@@ -111,8 +111,8 @@ describe('SessionController', () => {
|
|
|
111
111
|
it('defaults to free mode with acceptEdits permissionMode', async () => {
|
|
112
112
|
const adapter = createMockAdapter();
|
|
113
113
|
const tracker = new ContextTracker();
|
|
114
|
-
const controller = new SessionController(adapter, tracker, 'test', modePrefixes);
|
|
115
|
-
await controller.sendMessage('
|
|
114
|
+
const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp', modePrefixes);
|
|
115
|
+
await controller.sendMessage('do something');
|
|
116
116
|
const call = adapter.runSession.mock.calls[0][0];
|
|
117
117
|
expect(call.permissionMode).toBe('acceptEdits');
|
|
118
118
|
expect(call.prompt).toBe('do something');
|
|
@@ -120,8 +120,8 @@ describe('SessionController', () => {
|
|
|
120
120
|
it('sets permissionMode to plan and prepends prefix in plan mode', async () => {
|
|
121
121
|
const adapter = createMockAdapter();
|
|
122
122
|
const tracker = new ContextTracker();
|
|
123
|
-
const controller = new SessionController(adapter, tracker, 'test', modePrefixes);
|
|
124
|
-
await controller.sendMessage('
|
|
123
|
+
const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp', modePrefixes);
|
|
124
|
+
await controller.sendMessage('analyze this', { mode: 'plan' });
|
|
125
125
|
const call = adapter.runSession.mock.calls[0][0];
|
|
126
126
|
expect(call.permissionMode).toBe('plan');
|
|
127
127
|
expect(call.prompt).toBe('[PLAN MODE] Read-only planning.\n\nanalyze this');
|
|
@@ -129,8 +129,8 @@ describe('SessionController', () => {
|
|
|
129
129
|
it('explicit free mode uses acceptEdits and no prefix', async () => {
|
|
130
130
|
const adapter = createMockAdapter();
|
|
131
131
|
const tracker = new ContextTracker();
|
|
132
|
-
const controller = new SessionController(adapter, tracker, 'test', modePrefixes);
|
|
133
|
-
await controller.sendMessage('
|
|
132
|
+
const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp', modePrefixes);
|
|
133
|
+
await controller.sendMessage('build it', { mode: 'free' });
|
|
134
134
|
const call = adapter.runSession.mock.calls[0][0];
|
|
135
135
|
expect(call.permissionMode).toBe('acceptEdits');
|
|
136
136
|
expect(call.prompt).toBe('build it');
|
|
@@ -138,8 +138,8 @@ describe('SessionController', () => {
|
|
|
138
138
|
it('works without modePrefixes constructor arg (backward compat)', async () => {
|
|
139
139
|
const adapter = createMockAdapter();
|
|
140
140
|
const tracker = new ContextTracker();
|
|
141
|
-
const controller = new SessionController(adapter, tracker, 'test');
|
|
142
|
-
await controller.sendMessage('
|
|
141
|
+
const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp');
|
|
142
|
+
await controller.sendMessage('hello', { mode: 'plan' });
|
|
143
143
|
const call = adapter.runSession.mock.calls[0][0];
|
|
144
144
|
expect(call.permissionMode).toBe('plan');
|
|
145
145
|
// Empty prefix defaults — prompt should be unchanged
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|