@doingdev/opencode-claude-manager-plugin 0.1.47 → 0.1.50
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 +0 -44
- 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 +8 -1
- package/dist/manager/team-orchestrator.js +70 -11
- package/dist/plugin/agent-hierarchy.d.ts +1 -1
- package/dist/plugin/agent-hierarchy.js +3 -1
- package/dist/plugin/claude-manager.plugin.js +218 -25
- package/dist/plugin/service-factory.d.ts +2 -2
- package/dist/plugin/service-factory.js +18 -12
- package/dist/prompts/registry.js +42 -37
- package/dist/src/claude/claude-agent-sdk-adapter.d.ts +2 -3
- package/dist/src/claude/claude-agent-sdk-adapter.js +0 -44
- 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 +8 -1
- package/dist/src/manager/team-orchestrator.js +70 -11
- package/dist/src/plugin/agent-hierarchy.d.ts +1 -1
- package/dist/src/plugin/agent-hierarchy.js +3 -1
- package/dist/src/plugin/claude-manager.plugin.js +218 -25
- package/dist/src/plugin/service-factory.d.ts +2 -2
- package/dist/src/plugin/service-factory.js +18 -12
- package/dist/src/prompts/registry.js +42 -37
- package/dist/src/state/team-state-store.d.ts +3 -0
- package/dist/src/state/team-state-store.js +26 -1
- package/dist/src/team/roster.js +1 -0
- package/dist/src/types/contracts.d.ts +9 -49
- package/dist/state/team-state-store.d.ts +3 -0
- package/dist/state/team-state-store.js +26 -1
- package/dist/team/roster.js +1 -0
- package/dist/test/claude-agent-sdk-adapter.test.js +0 -11
- package/dist/test/claude-manager.plugin.test.js +6 -1
- package/dist/test/context-tracker.test.js +0 -8
- package/dist/test/cto-active-team.test.d.ts +1 -0
- package/dist/test/cto-active-team.test.js +52 -0
- 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.d.ts +1 -0
- package/dist/test/report-claude-event.test.js +246 -0
- package/dist/test/team-state-store.test.js +18 -0
- package/dist/test/tool-approval-manager.test.js +17 -17
- package/dist/types/contracts.d.ts +9 -49
- package/package.json +1 -1
|
@@ -1,52 +1,57 @@
|
|
|
1
1
|
export const managerPromptRegistry = {
|
|
2
2
|
ctoSystemPrompt: [
|
|
3
|
-
'You are
|
|
3
|
+
'You are a principal engineer orchestrating a team of AI-powered engineers.',
|
|
4
|
+
'You multiply your output by delegating precisely and reviewing critically.',
|
|
5
|
+
'Every prompt you send to an engineer costs time and tokens. Make each one count.',
|
|
4
6
|
'',
|
|
5
|
-
'
|
|
6
|
-
'-
|
|
7
|
-
'-
|
|
8
|
-
'-
|
|
9
|
-
'-
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'-
|
|
7
|
+
'Understand first:',
|
|
8
|
+
'- Ask questions. If the request is ambiguous, underspecified, or has multiple valid interpretations, ask before building. Any question whose answer would change what you build or how you build it is worth asking.',
|
|
9
|
+
'- Use the `question` tool to surface decisions with a concrete recommendation. Prefer one precise question over many vague ones.',
|
|
10
|
+
'- Identify what already exists in the codebase before creating anything new.',
|
|
11
|
+
'- Think about what could go wrong and address it upfront.',
|
|
12
|
+
'',
|
|
13
|
+
'Plan and decompose:',
|
|
14
|
+
'- Break work into independent pieces that can run in parallel. Two engineers exploring in parallel then synthesizing beats one engineer doing everything sequentially.',
|
|
15
|
+
'- For medium or large tasks, dispatch two engineers with complementary perspectives (lead plan + challenger review), then synthesize.',
|
|
16
|
+
'- Define clear success criteria before delegating. A good assignment includes: what to do, why, which files/areas are relevant, and how to verify it worked.',
|
|
13
17
|
'',
|
|
14
|
-
'
|
|
15
|
-
'- Tom, John, Maya, Sara, and Alex are persistent engineers.',
|
|
16
|
-
'-
|
|
17
|
-
'-
|
|
18
|
-
'-
|
|
18
|
+
'Delegate through the Task tool:',
|
|
19
|
+
'- Tom, John, Maya, Sara, and Alex are persistent engineers. Each keeps a Claude Code session that remembers prior turns.',
|
|
20
|
+
'- Reuse the same engineer when follow-up work belongs to their prior context.',
|
|
21
|
+
'- Only one implementing engineer should modify the worktree at a time. Parallelize exploration freely.',
|
|
22
|
+
'- Do not delegate without telling the engineer what done looks like.',
|
|
23
|
+
'',
|
|
24
|
+
'Review and iterate:',
|
|
25
|
+
'- Review diffs with `git_diff`, inspect changed files with `git_status`, and use `git_log` for recent context.',
|
|
26
|
+
'- Give specific, actionable feedback. Not "this could be better" but "this is wrong because X, fix it by doing Y."',
|
|
27
|
+
'- Trust engineer findings but verify critical claims. Do not re-examine every file they already reviewed.',
|
|
28
|
+
'- If something fails, figure out what you missed in the assignment, not just what the engineer got wrong.',
|
|
19
29
|
'',
|
|
20
|
-
'
|
|
21
|
-
'- Do not edit files or run bash directly.',
|
|
22
|
-
'- Do not
|
|
23
|
-
'-
|
|
30
|
+
'Constraints:',
|
|
31
|
+
'- Do not edit files or run bash directly. Engineers do the hands-on work.',
|
|
32
|
+
'- Do not read files or grep when an engineer can answer the question faster.',
|
|
33
|
+
'- Communicate proactively. If the plan changes or you discover something unexpected, tell the user.',
|
|
24
34
|
].join('\n'),
|
|
25
35
|
engineerAgentPrompt: [
|
|
26
|
-
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
36
|
+
"You are a named engineer on the CTO's team.",
|
|
37
|
+
'Your job is to run assignments through the `claude` tool, which connects to a persistent Claude Code session that remembers your prior turns.',
|
|
38
|
+
'',
|
|
39
|
+
'Frame each assignment well:',
|
|
40
|
+
'- Include relevant context, file paths, and constraints the CTO provided.',
|
|
41
|
+
'- Specify the work mode: explore (investigate, no edits), implement (make changes and verify), or verify (run checks and report).',
|
|
42
|
+
"- If the CTO's assignment is unclear, ask for clarification before sending it to Claude Code.",
|
|
43
|
+
'',
|
|
44
|
+
'Your wrapper context from prior turns is reloaded automatically. Use it to avoid repeating work or re-explaining context that Claude Code already knows.',
|
|
45
|
+
"Return the tool result directly. Add your own commentary only when something was unexpected or needs the CTO's attention.",
|
|
32
46
|
].join('\n'),
|
|
33
47
|
engineerSessionPrompt: [
|
|
34
48
|
'You are an expert software engineer working inside Claude Code.',
|
|
35
|
-
'
|
|
36
|
-
'Follow repository conventions and
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'Report blockers clearly and include exact command output on failure.',
|
|
49
|
+
'Start with the smallest investigation that resolves the key uncertainty, then act.',
|
|
50
|
+
'Follow repository conventions, AGENTS.md, and any project-level instructions.',
|
|
51
|
+
'Verify your own work before reporting done. Run the most relevant check (test, lint, typecheck, build) for what you changed.',
|
|
52
|
+
'Report blockers immediately with exact error output. Do not retry silently more than once.',
|
|
40
53
|
'Do not run git commit, git push, git reset, git checkout, or git stash.',
|
|
41
54
|
].join('\n'),
|
|
42
|
-
modePrefixes: {
|
|
43
|
-
plan: [
|
|
44
|
-
'[PLAN MODE] Read-only.',
|
|
45
|
-
'Do not create or edit files.',
|
|
46
|
-
'Analyze the codebase and return the plan inline.',
|
|
47
|
-
].join(' '),
|
|
48
|
-
free: '',
|
|
49
|
-
},
|
|
50
55
|
contextWarnings: {
|
|
51
56
|
moderate: 'Engineer context is getting full ({percent}% estimated). Reuse is still fine, but keep the next prompt focused.',
|
|
52
57
|
high: 'Engineer context is heavy ({percent}% estimated, {turns} turns, ${cost}). Prefer a narrowly scoped follow-up or internal compaction.',
|
|
@@ -7,7 +7,10 @@ export declare class TeamStateStore {
|
|
|
7
7
|
getTeam(cwd: string, teamId: string): Promise<TeamRecord | null>;
|
|
8
8
|
listTeams(cwd: string): Promise<TeamRecord[]>;
|
|
9
9
|
updateTeam(cwd: string, teamId: string, update: (team: TeamRecord) => TeamRecord): Promise<TeamRecord>;
|
|
10
|
+
getActiveTeam(cwd: string): Promise<string | null>;
|
|
11
|
+
setActiveTeam(cwd: string, teamId: string): Promise<void>;
|
|
10
12
|
private getTeamKey;
|
|
13
|
+
private getActiveTeamPath;
|
|
11
14
|
private getTeamsDirectory;
|
|
12
15
|
private getTeamPath;
|
|
13
16
|
private enqueueWrite;
|
|
@@ -31,12 +31,15 @@ export class TeamStateStore {
|
|
|
31
31
|
const directory = this.getTeamsDirectory(cwd);
|
|
32
32
|
try {
|
|
33
33
|
const entries = await fs.readdir(directory);
|
|
34
|
-
const
|
|
34
|
+
const results = await Promise.allSettled(entries
|
|
35
35
|
.filter((entry) => entry.endsWith('.json'))
|
|
36
36
|
.map(async (entry) => {
|
|
37
37
|
const content = await fs.readFile(path.join(directory, entry), 'utf8');
|
|
38
38
|
return JSON.parse(content);
|
|
39
39
|
}));
|
|
40
|
+
const teams = results
|
|
41
|
+
.filter((r) => r.status === 'fulfilled')
|
|
42
|
+
.map((r) => r.value);
|
|
40
43
|
return teams.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
41
44
|
}
|
|
42
45
|
catch (error) {
|
|
@@ -59,9 +62,31 @@ export class TeamStateStore {
|
|
|
59
62
|
return updated;
|
|
60
63
|
});
|
|
61
64
|
}
|
|
65
|
+
async getActiveTeam(cwd) {
|
|
66
|
+
const filePath = this.getActiveTeamPath(cwd);
|
|
67
|
+
try {
|
|
68
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
69
|
+
const parsed = JSON.parse(content);
|
|
70
|
+
return parsed.teamId ?? null;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (isFileNotFoundError(error)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async setActiveTeam(cwd, teamId) {
|
|
80
|
+
const filePath = this.getActiveTeamPath(cwd);
|
|
81
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
82
|
+
await writeJsonAtomically(filePath, { teamId });
|
|
83
|
+
}
|
|
62
84
|
getTeamKey(cwd, teamId) {
|
|
63
85
|
return `${cwd}:${teamId}`;
|
|
64
86
|
}
|
|
87
|
+
getActiveTeamPath(cwd) {
|
|
88
|
+
return path.join(cwd, this.baseDirectoryName, 'active-team.json');
|
|
89
|
+
}
|
|
65
90
|
getTeamsDirectory(cwd) {
|
|
66
91
|
return path.join(cwd, this.baseDirectoryName, 'teams');
|
|
67
92
|
}
|
package/dist/src/team/roster.js
CHANGED
|
@@ -2,10 +2,6 @@ export interface ManagerPromptRegistry {
|
|
|
2
2
|
ctoSystemPrompt: string;
|
|
3
3
|
engineerAgentPrompt: string;
|
|
4
4
|
engineerSessionPrompt: string;
|
|
5
|
-
modePrefixes: {
|
|
6
|
-
plan: string;
|
|
7
|
-
free: string;
|
|
8
|
-
};
|
|
9
5
|
contextWarnings: {
|
|
10
6
|
moderate: string;
|
|
11
7
|
high: string;
|
|
@@ -110,12 +106,21 @@ export interface TeamEngineerRecord {
|
|
|
110
106
|
wrapperSessionId: string | null;
|
|
111
107
|
claudeSessionId: string | null;
|
|
112
108
|
busy: boolean;
|
|
109
|
+
busySince: string | null;
|
|
113
110
|
lastMode: EngineerWorkMode | null;
|
|
114
111
|
lastTaskSummary: string | null;
|
|
115
112
|
lastUsedAt: string | null;
|
|
116
113
|
wrapperHistory: WrapperHistoryEntry[];
|
|
117
114
|
context: SessionContextSnapshot;
|
|
118
115
|
}
|
|
116
|
+
export type EngineerFailureKind = 'sdkError' | 'contextExhausted' | 'toolDenied' | 'aborted' | 'engineerBusy' | 'unknown';
|
|
117
|
+
export interface EngineerFailureResult {
|
|
118
|
+
teamId: string;
|
|
119
|
+
engineer: EngineerName;
|
|
120
|
+
mode: EngineerWorkMode;
|
|
121
|
+
failureKind: EngineerFailureKind;
|
|
122
|
+
message: string;
|
|
123
|
+
}
|
|
119
124
|
export interface TeamRecord {
|
|
120
125
|
id: string;
|
|
121
126
|
cwd: string;
|
|
@@ -163,51 +168,6 @@ export interface GitOperationResult {
|
|
|
163
168
|
output: string;
|
|
164
169
|
error?: string;
|
|
165
170
|
}
|
|
166
|
-
export type PersistentRunStatus = 'running' | 'completed' | 'failed';
|
|
167
|
-
export interface PersistentRunMessageRecord {
|
|
168
|
-
timestamp: string;
|
|
169
|
-
direction: 'sent' | 'received';
|
|
170
|
-
text: string;
|
|
171
|
-
turns?: number;
|
|
172
|
-
totalCostUsd?: number;
|
|
173
|
-
inputTokens?: number;
|
|
174
|
-
outputTokens?: number;
|
|
175
|
-
}
|
|
176
|
-
export interface PersistentRunActionRecord {
|
|
177
|
-
timestamp: string;
|
|
178
|
-
type: 'git_diff' | 'git_commit' | 'git_reset' | 'git_status' | 'git_log' | 'compact' | 'clear';
|
|
179
|
-
result: string;
|
|
180
|
-
}
|
|
181
|
-
export interface PersistentRunRecord {
|
|
182
|
-
id: string;
|
|
183
|
-
cwd: string;
|
|
184
|
-
task: string;
|
|
185
|
-
status: PersistentRunStatus;
|
|
186
|
-
createdAt: string;
|
|
187
|
-
updatedAt: string;
|
|
188
|
-
sessionId: string | null;
|
|
189
|
-
sessionHistory: string[];
|
|
190
|
-
messages: PersistentRunMessageRecord[];
|
|
191
|
-
actions: PersistentRunActionRecord[];
|
|
192
|
-
commits: string[];
|
|
193
|
-
context: SessionContextSnapshot;
|
|
194
|
-
finalSummary?: string;
|
|
195
|
-
}
|
|
196
|
-
export interface PersistentRunResult {
|
|
197
|
-
run: PersistentRunRecord;
|
|
198
|
-
}
|
|
199
|
-
export interface LiveTailEvent {
|
|
200
|
-
type: 'line' | 'error' | 'end';
|
|
201
|
-
sessionId: string;
|
|
202
|
-
data?: unknown;
|
|
203
|
-
rawLine?: string;
|
|
204
|
-
error?: string;
|
|
205
|
-
}
|
|
206
|
-
export interface ToolOutputPreview {
|
|
207
|
-
toolUseId: string;
|
|
208
|
-
content: string;
|
|
209
|
-
isError: boolean;
|
|
210
|
-
}
|
|
211
171
|
export interface DiscoveredClaudeFile {
|
|
212
172
|
/** Relative path from the project root (forward slashes, deterministic order). */
|
|
213
173
|
relativePath: string;
|
|
@@ -7,7 +7,10 @@ export declare class TeamStateStore {
|
|
|
7
7
|
getTeam(cwd: string, teamId: string): Promise<TeamRecord | null>;
|
|
8
8
|
listTeams(cwd: string): Promise<TeamRecord[]>;
|
|
9
9
|
updateTeam(cwd: string, teamId: string, update: (team: TeamRecord) => TeamRecord): Promise<TeamRecord>;
|
|
10
|
+
getActiveTeam(cwd: string): Promise<string | null>;
|
|
11
|
+
setActiveTeam(cwd: string, teamId: string): Promise<void>;
|
|
10
12
|
private getTeamKey;
|
|
13
|
+
private getActiveTeamPath;
|
|
11
14
|
private getTeamsDirectory;
|
|
12
15
|
private getTeamPath;
|
|
13
16
|
private enqueueWrite;
|
|
@@ -31,12 +31,15 @@ export class TeamStateStore {
|
|
|
31
31
|
const directory = this.getTeamsDirectory(cwd);
|
|
32
32
|
try {
|
|
33
33
|
const entries = await fs.readdir(directory);
|
|
34
|
-
const
|
|
34
|
+
const results = await Promise.allSettled(entries
|
|
35
35
|
.filter((entry) => entry.endsWith('.json'))
|
|
36
36
|
.map(async (entry) => {
|
|
37
37
|
const content = await fs.readFile(path.join(directory, entry), 'utf8');
|
|
38
38
|
return JSON.parse(content);
|
|
39
39
|
}));
|
|
40
|
+
const teams = results
|
|
41
|
+
.filter((r) => r.status === 'fulfilled')
|
|
42
|
+
.map((r) => r.value);
|
|
40
43
|
return teams.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
41
44
|
}
|
|
42
45
|
catch (error) {
|
|
@@ -59,9 +62,31 @@ export class TeamStateStore {
|
|
|
59
62
|
return updated;
|
|
60
63
|
});
|
|
61
64
|
}
|
|
65
|
+
async getActiveTeam(cwd) {
|
|
66
|
+
const filePath = this.getActiveTeamPath(cwd);
|
|
67
|
+
try {
|
|
68
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
69
|
+
const parsed = JSON.parse(content);
|
|
70
|
+
return parsed.teamId ?? null;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (isFileNotFoundError(error)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async setActiveTeam(cwd, teamId) {
|
|
80
|
+
const filePath = this.getActiveTeamPath(cwd);
|
|
81
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
82
|
+
await writeJsonAtomically(filePath, { teamId });
|
|
83
|
+
}
|
|
62
84
|
getTeamKey(cwd, teamId) {
|
|
63
85
|
return `${cwd}:${teamId}`;
|
|
64
86
|
}
|
|
87
|
+
getActiveTeamPath(cwd) {
|
|
88
|
+
return path.join(cwd, this.baseDirectoryName, 'active-team.json');
|
|
89
|
+
}
|
|
65
90
|
getTeamsDirectory(cwd) {
|
|
66
91
|
return path.join(cwd, this.baseDirectoryName, 'teams');
|
|
67
92
|
}
|
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({
|
|
@@ -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' });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { ClaudeManagerPlugin } from '../src/plugin/claude-manager.plugin.js';
|
|
6
|
+
import { clearPluginServices, getActiveTeamSession } from '../src/plugin/service-factory.js';
|
|
7
|
+
import { AGENT_CTO } from '../src/plugin/agent-hierarchy.js';
|
|
8
|
+
import { TeamStateStore } from '../src/state/team-state-store.js';
|
|
9
|
+
describe('CTO chat.message — persisted active team', () => {
|
|
10
|
+
let tempRoot;
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'cto-team-'));
|
|
13
|
+
clearPluginServices();
|
|
14
|
+
});
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
clearPluginServices();
|
|
17
|
+
if (tempRoot) {
|
|
18
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
it('persists the active team on the first CTO message', async () => {
|
|
22
|
+
const plugin = await ClaudeManagerPlugin({ worktree: tempRoot });
|
|
23
|
+
const chatMessage = plugin['chat.message'];
|
|
24
|
+
await chatMessage({ agent: AGENT_CTO, sessionID: 'session-cto-1' });
|
|
25
|
+
const store = new TeamStateStore('.claude-manager');
|
|
26
|
+
await expect(store.getActiveTeam(tempRoot)).resolves.toBe('session-cto-1');
|
|
27
|
+
expect(getActiveTeamSession(tempRoot)).toBe('session-cto-1');
|
|
28
|
+
});
|
|
29
|
+
it('a new CTO session adopts the already-persisted active team instead of overwriting it', async () => {
|
|
30
|
+
const store = new TeamStateStore('.claude-manager');
|
|
31
|
+
// Simulate a pre-existing persisted active team (e.g., from a previous process run).
|
|
32
|
+
await store.setActiveTeam(tempRoot, 'old-team-id');
|
|
33
|
+
const plugin = await ClaudeManagerPlugin({ worktree: tempRoot });
|
|
34
|
+
const chatMessage = plugin['chat.message'];
|
|
35
|
+
// New CTO session with a different session ID.
|
|
36
|
+
await chatMessage({ agent: AGENT_CTO, sessionID: 'brand-new-cto-session' });
|
|
37
|
+
// The persisted active team must NOT be overwritten.
|
|
38
|
+
await expect(store.getActiveTeam(tempRoot)).resolves.toBe('old-team-id');
|
|
39
|
+
// The in-memory registry must point to the persisted team, NOT the new session.
|
|
40
|
+
expect(getActiveTeamSession(tempRoot)).toBe('old-team-id');
|
|
41
|
+
});
|
|
42
|
+
it('does not overwrite the persisted team across two CTO messages in the same session', async () => {
|
|
43
|
+
const store = new TeamStateStore('.claude-manager');
|
|
44
|
+
const plugin = await ClaudeManagerPlugin({ worktree: tempRoot });
|
|
45
|
+
const chatMessage = plugin['chat.message'];
|
|
46
|
+
await chatMessage({ agent: AGENT_CTO, sessionID: 'session-cto-1' });
|
|
47
|
+
await chatMessage({ agent: AGENT_CTO, sessionID: 'session-cto-1' });
|
|
48
|
+
// Still the original session — persisted.
|
|
49
|
+
await expect(store.getActiveTeam(tempRoot)).resolves.toBe('session-cto-1');
|
|
50
|
+
expect(getActiveTeamSession(tempRoot)).toBe('session-cto-1');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -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);
|