@doingdev/opencode-claude-manager-plugin 0.1.35 → 0.1.43
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.js +1 -0
- package/dist/manager/git-operations.d.ts +10 -1
- package/dist/manager/git-operations.js +18 -3
- package/dist/manager/persistent-manager.d.ts +19 -3
- package/dist/manager/persistent-manager.js +21 -9
- package/dist/manager/session-controller.d.ts +8 -5
- package/dist/manager/session-controller.js +25 -20
- package/dist/metadata/claude-metadata.service.d.ts +12 -0
- package/dist/metadata/claude-metadata.service.js +38 -0
- package/dist/metadata/repo-claude-config-reader.d.ts +7 -0
- package/dist/metadata/repo-claude-config-reader.js +154 -0
- package/dist/plugin/agent-hierarchy.d.ts +9 -9
- package/dist/plugin/agent-hierarchy.js +25 -25
- package/dist/plugin/claude-manager.plugin.js +83 -46
- package/dist/plugin/orchestrator.plugin.d.ts +2 -0
- package/dist/plugin/orchestrator.plugin.js +116 -0
- package/dist/plugin/service-factory.js +3 -8
- package/dist/prompts/registry.js +100 -103
- package/dist/providers/claude-code-wrapper.d.ts +13 -0
- package/dist/providers/claude-code-wrapper.js +13 -0
- package/dist/safety/bash-safety.d.ts +21 -0
- package/dist/safety/bash-safety.js +62 -0
- package/dist/src/claude/claude-agent-sdk-adapter.d.ts +27 -0
- package/dist/src/claude/claude-agent-sdk-adapter.js +517 -0
- package/dist/src/claude/claude-session.service.d.ts +10 -0
- package/dist/src/claude/claude-session.service.js +18 -0
- package/dist/src/claude/session-live-tailer.d.ts +51 -0
- package/dist/src/claude/session-live-tailer.js +269 -0
- package/dist/src/claude/tool-approval-manager.d.ts +27 -0
- package/dist/src/claude/tool-approval-manager.js +232 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +4 -0
- package/dist/src/manager/context-tracker.d.ts +33 -0
- package/dist/src/manager/context-tracker.js +106 -0
- package/dist/src/manager/git-operations.d.ts +12 -0
- package/dist/src/manager/git-operations.js +76 -0
- package/dist/src/manager/persistent-manager.d.ts +77 -0
- package/dist/src/manager/persistent-manager.js +170 -0
- package/dist/src/manager/session-controller.d.ts +44 -0
- package/dist/src/manager/session-controller.js +147 -0
- package/dist/src/plugin/agent-hierarchy.d.ts +60 -0
- package/dist/src/plugin/agent-hierarchy.js +157 -0
- package/dist/src/plugin/claude-manager.plugin.d.ts +2 -0
- package/dist/src/plugin/claude-manager.plugin.js +563 -0
- package/dist/src/plugin/service-factory.d.ts +12 -0
- package/dist/src/plugin/service-factory.js +38 -0
- package/dist/src/prompts/registry.d.ts +11 -0
- package/dist/src/prompts/registry.js +260 -0
- package/dist/src/state/file-run-state-store.d.ts +14 -0
- package/dist/src/state/file-run-state-store.js +85 -0
- package/dist/src/state/transcript-store.d.ts +15 -0
- package/dist/src/state/transcript-store.js +44 -0
- package/dist/src/types/contracts.d.ts +200 -0
- package/dist/src/types/contracts.js +1 -0
- package/dist/src/util/fs-helpers.d.ts +2 -0
- package/dist/src/util/fs-helpers.js +10 -0
- package/dist/src/util/project-context.d.ts +10 -0
- package/dist/src/util/project-context.js +105 -0
- package/dist/src/util/transcript-append.d.ts +7 -0
- package/dist/src/util/transcript-append.js +29 -0
- package/dist/test/claude-agent-sdk-adapter.test.d.ts +1 -0
- package/dist/test/claude-agent-sdk-adapter.test.js +459 -0
- package/dist/test/claude-manager.plugin.test.d.ts +1 -0
- package/dist/test/claude-manager.plugin.test.js +331 -0
- package/dist/test/context-tracker.test.d.ts +1 -0
- package/dist/test/context-tracker.test.js +138 -0
- package/dist/test/file-run-state-store.test.d.ts +1 -0
- package/dist/test/file-run-state-store.test.js +82 -0
- package/dist/test/git-operations.test.d.ts +1 -0
- package/dist/test/git-operations.test.js +90 -0
- package/dist/test/persistent-manager.test.d.ts +1 -0
- package/dist/test/persistent-manager.test.js +208 -0
- package/dist/test/project-context.test.d.ts +1 -0
- package/dist/test/project-context.test.js +92 -0
- package/dist/test/prompt-registry.test.d.ts +1 -0
- package/dist/test/prompt-registry.test.js +256 -0
- package/dist/test/session-controller.test.d.ts +1 -0
- package/dist/test/session-controller.test.js +149 -0
- package/dist/test/session-live-tailer.test.d.ts +1 -0
- package/dist/test/session-live-tailer.test.js +313 -0
- package/dist/test/tool-approval-manager.test.d.ts +1 -0
- package/dist/test/tool-approval-manager.test.js +264 -0
- package/dist/test/transcript-append.test.d.ts +1 -0
- package/dist/test/transcript-append.test.js +37 -0
- package/dist/test/transcript-store.test.d.ts +1 -0
- package/dist/test/transcript-store.test.js +50 -0
- package/dist/types/contracts.d.ts +3 -4
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +11 -0
- package/package.json +2 -2
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
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
|
+
function createMockGitOps() {
|
|
44
|
+
return {
|
|
45
|
+
diff: vi.fn(async () => ({
|
|
46
|
+
hasDiff: true,
|
|
47
|
+
diffText: 'diff --git a/file.ts\n+new line',
|
|
48
|
+
stats: { filesChanged: 1, insertions: 1, deletions: 0 },
|
|
49
|
+
})),
|
|
50
|
+
commit: vi.fn(async () => ({
|
|
51
|
+
success: true,
|
|
52
|
+
output: '[main abc1234] test commit',
|
|
53
|
+
})),
|
|
54
|
+
resetHard: vi.fn(async () => ({
|
|
55
|
+
success: true,
|
|
56
|
+
output: 'HEAD is now at abc1234',
|
|
57
|
+
})),
|
|
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
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function createMockTranscriptStore() {
|
|
83
|
+
return {
|
|
84
|
+
appendEvents: vi.fn(async () => { }),
|
|
85
|
+
readEvents: vi.fn(async () => []),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
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('/tmp', '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
|
+
it('delegates git diff to GitOperations', async () => {
|
|
132
|
+
const sessionCtrl = createMockSessionController();
|
|
133
|
+
const gitOps = createMockGitOps();
|
|
134
|
+
const store = createMockStateStore();
|
|
135
|
+
const tracker = new ContextTracker();
|
|
136
|
+
const manager = new PersistentManager(sessionCtrl, gitOps, store, tracker, createMockTranscriptStore());
|
|
137
|
+
const diff = await manager.gitDiff();
|
|
138
|
+
expect(diff.hasDiff).toBe(true);
|
|
139
|
+
expect(diff.stats.filesChanged).toBe(1);
|
|
140
|
+
});
|
|
141
|
+
it('delegates git commit to GitOperations', async () => {
|
|
142
|
+
const sessionCtrl = createMockSessionController();
|
|
143
|
+
const gitOps = createMockGitOps();
|
|
144
|
+
const store = createMockStateStore();
|
|
145
|
+
const tracker = new ContextTracker();
|
|
146
|
+
const manager = new PersistentManager(sessionCtrl, gitOps, store, tracker, createMockTranscriptStore());
|
|
147
|
+
const result = await manager.gitCommit('feat: add X');
|
|
148
|
+
expect(gitOps.commit).toHaveBeenCalledWith('feat: add X');
|
|
149
|
+
expect(result.success).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
it('delegates git reset to GitOperations', async () => {
|
|
152
|
+
const sessionCtrl = createMockSessionController();
|
|
153
|
+
const gitOps = createMockGitOps();
|
|
154
|
+
const store = createMockStateStore();
|
|
155
|
+
const tracker = new ContextTracker();
|
|
156
|
+
const manager = new PersistentManager(sessionCtrl, gitOps, store, tracker, createMockTranscriptStore());
|
|
157
|
+
const result = await manager.gitReset();
|
|
158
|
+
expect(gitOps.resetHard).toHaveBeenCalled();
|
|
159
|
+
expect(result.success).toBe(true);
|
|
160
|
+
});
|
|
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('/tmp');
|
|
196
|
+
expect(cleared).toBe('ses_mock');
|
|
197
|
+
expect(sessionCtrl.clearSession).toHaveBeenCalledWith('/tmp');
|
|
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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { discoverProjectClaudeFiles } from '../src/util/project-context.js';
|
|
6
|
+
describe('discoverProjectClaudeFiles', () => {
|
|
7
|
+
const dirs = [];
|
|
8
|
+
async function makeTmp() {
|
|
9
|
+
const d = await mkdtemp(join(tmpdir(), 'ctx-test-'));
|
|
10
|
+
dirs.push(d);
|
|
11
|
+
return d;
|
|
12
|
+
}
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
for (const d of dirs) {
|
|
15
|
+
await rm(d, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
dirs.length = 0;
|
|
18
|
+
});
|
|
19
|
+
it('returns empty array when no Claude files exist', async () => {
|
|
20
|
+
const cwd = await makeTmp();
|
|
21
|
+
expect(await discoverProjectClaudeFiles(cwd)).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
it('discovers root CLAUDE.md', async () => {
|
|
24
|
+
const cwd = await makeTmp();
|
|
25
|
+
await writeFile(join(cwd, 'CLAUDE.md'), 'project rules\n');
|
|
26
|
+
const files = await discoverProjectClaudeFiles(cwd);
|
|
27
|
+
expect(files).toEqual([{ relativePath: 'CLAUDE.md', content: 'project rules' }]);
|
|
28
|
+
});
|
|
29
|
+
it('discovers nested CLAUDE.md files recursively', async () => {
|
|
30
|
+
const cwd = await makeTmp();
|
|
31
|
+
await writeFile(join(cwd, 'CLAUDE.md'), 'root rules');
|
|
32
|
+
await mkdir(join(cwd, 'packages/core'), { recursive: true });
|
|
33
|
+
await writeFile(join(cwd, 'packages/core/CLAUDE.md'), 'core rules');
|
|
34
|
+
const files = await discoverProjectClaudeFiles(cwd);
|
|
35
|
+
expect(files).toHaveLength(2);
|
|
36
|
+
expect(files[0].relativePath).toBe('CLAUDE.md');
|
|
37
|
+
expect(files[1].relativePath).toBe('packages/core/CLAUDE.md');
|
|
38
|
+
});
|
|
39
|
+
it('discovers files under .claude/ recursively', async () => {
|
|
40
|
+
const cwd = await makeTmp();
|
|
41
|
+
await mkdir(join(cwd, '.claude/settings'), { recursive: true });
|
|
42
|
+
await writeFile(join(cwd, '.claude/CLAUDE.md'), 'claude dir md');
|
|
43
|
+
await writeFile(join(cwd, '.claude/settings/config.txt'), 'some config');
|
|
44
|
+
const files = await discoverProjectClaudeFiles(cwd);
|
|
45
|
+
expect(files).toHaveLength(2);
|
|
46
|
+
expect(files.map((f) => f.relativePath)).toEqual([
|
|
47
|
+
'.claude/CLAUDE.md',
|
|
48
|
+
'.claude/settings/config.txt',
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
it('deduplicates when a file matches both CLAUDE.md walk and .claude/ walk', async () => {
|
|
52
|
+
const cwd = await makeTmp();
|
|
53
|
+
await mkdir(join(cwd, '.claude'), { recursive: true });
|
|
54
|
+
await writeFile(join(cwd, '.claude/CLAUDE.md'), 'shared content');
|
|
55
|
+
const files = await discoverProjectClaudeFiles(cwd);
|
|
56
|
+
expect(files).toHaveLength(1);
|
|
57
|
+
expect(files[0].relativePath).toBe('.claude/CLAUDE.md');
|
|
58
|
+
expect(files[0].content).toBe('shared content');
|
|
59
|
+
});
|
|
60
|
+
it('skips blank/whitespace-only files', async () => {
|
|
61
|
+
const cwd = await makeTmp();
|
|
62
|
+
await writeFile(join(cwd, 'CLAUDE.md'), ' \n \n');
|
|
63
|
+
expect(await discoverProjectClaudeFiles(cwd)).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
it('trims content whitespace', async () => {
|
|
66
|
+
const cwd = await makeTmp();
|
|
67
|
+
await writeFile(join(cwd, 'CLAUDE.md'), '\n hello world \n\n');
|
|
68
|
+
const files = await discoverProjectClaudeFiles(cwd);
|
|
69
|
+
expect(files[0].content).toBe('hello world');
|
|
70
|
+
});
|
|
71
|
+
it('returns deterministic sorted order', async () => {
|
|
72
|
+
const cwd = await makeTmp();
|
|
73
|
+
await mkdir(join(cwd, 'b'), { recursive: true });
|
|
74
|
+
await mkdir(join(cwd, 'a'), { recursive: true });
|
|
75
|
+
await writeFile(join(cwd, 'b/CLAUDE.md'), 'b rules');
|
|
76
|
+
await writeFile(join(cwd, 'a/CLAUDE.md'), 'a rules');
|
|
77
|
+
await writeFile(join(cwd, 'CLAUDE.md'), 'root rules');
|
|
78
|
+
const files = await discoverProjectClaudeFiles(cwd);
|
|
79
|
+
expect(files.map((f) => f.relativePath)).toEqual(['CLAUDE.md', 'a/CLAUDE.md', 'b/CLAUDE.md']);
|
|
80
|
+
});
|
|
81
|
+
it('skips node_modules and .git directories', async () => {
|
|
82
|
+
const cwd = await makeTmp();
|
|
83
|
+
await mkdir(join(cwd, 'node_modules/pkg'), { recursive: true });
|
|
84
|
+
await writeFile(join(cwd, 'node_modules/pkg/CLAUDE.md'), 'should not appear');
|
|
85
|
+
await mkdir(join(cwd, '.git/hooks'), { recursive: true });
|
|
86
|
+
await writeFile(join(cwd, '.git/hooks/CLAUDE.md'), 'should not appear');
|
|
87
|
+
await writeFile(join(cwd, 'CLAUDE.md'), 'root');
|
|
88
|
+
const files = await discoverProjectClaudeFiles(cwd);
|
|
89
|
+
expect(files).toHaveLength(1);
|
|
90
|
+
expect(files[0].relativePath).toBe('CLAUDE.md');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { composeWrapperPrompt, managerPromptRegistry } from '../src/prompts/registry.js';
|
|
3
|
+
describe('managerPromptRegistry', () => {
|
|
4
|
+
describe('ctoSystemPrompt', () => {
|
|
5
|
+
it('establishes technical owner identity', () => {
|
|
6
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('technical owner');
|
|
7
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('not a ticket-taker');
|
|
8
|
+
});
|
|
9
|
+
it('instructs to discover the right problem before solving', () => {
|
|
10
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('discover the right problem before solving it');
|
|
11
|
+
});
|
|
12
|
+
it('checks for hidden assumptions and root causes', () => {
|
|
13
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Hidden assumptions');
|
|
14
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('root causes');
|
|
15
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('abstraction leaks');
|
|
16
|
+
});
|
|
17
|
+
it('includes missed-opportunity check', () => {
|
|
18
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Missed-opportunity check');
|
|
19
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('cleaner alternative');
|
|
20
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('requester likely missed');
|
|
21
|
+
});
|
|
22
|
+
it('puts verification-first as core principle', () => {
|
|
23
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('verification-first');
|
|
24
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Every delegation MUST include how to verify success');
|
|
25
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('single highest-leverage');
|
|
26
|
+
});
|
|
27
|
+
it('right-sizes approach by complexity', () => {
|
|
28
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Simple tasks');
|
|
29
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Medium+ tasks');
|
|
30
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Complex tasks');
|
|
31
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Right-size your approach');
|
|
32
|
+
});
|
|
33
|
+
it('requires self-contained delegation with context', () => {
|
|
34
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('does not have your context');
|
|
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);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|