@doingdev/opencode-claude-manager-plugin 0.1.46 → 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/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 -423
- 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 -261
- 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 +54 -3
- package/dist/types/contracts.js +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export interface ManagerPromptRegistry {
|
|
2
2
|
ctoSystemPrompt: string;
|
|
3
|
-
|
|
4
|
-
engineerBuildPrompt: string;
|
|
3
|
+
engineerAgentPrompt: string;
|
|
5
4
|
engineerSessionPrompt: string;
|
|
6
5
|
modePrefixes: {
|
|
7
6
|
plan: string;
|
|
@@ -14,6 +13,15 @@ export interface ManagerPromptRegistry {
|
|
|
14
13
|
};
|
|
15
14
|
}
|
|
16
15
|
export type SessionMode = 'plan' | 'free';
|
|
16
|
+
export declare const DEFAULT_ENGINEER_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
|
|
17
|
+
export type EngineerName = (typeof DEFAULT_ENGINEER_NAMES)[number];
|
|
18
|
+
export type EngineerWorkMode = 'explore' | 'implement' | 'verify';
|
|
19
|
+
export interface WrapperHistoryEntry {
|
|
20
|
+
timestamp: string;
|
|
21
|
+
type: 'assignment' | 'result';
|
|
22
|
+
mode?: EngineerWorkMode;
|
|
23
|
+
text: string;
|
|
24
|
+
}
|
|
17
25
|
export interface ClaudeCommandMetadata {
|
|
18
26
|
name: string;
|
|
19
27
|
description: string;
|
|
@@ -97,6 +105,50 @@ export interface SessionContextSnapshot {
|
|
|
97
105
|
warningLevel: ContextWarningLevel;
|
|
98
106
|
compactionCount: number;
|
|
99
107
|
}
|
|
108
|
+
export interface TeamEngineerRecord {
|
|
109
|
+
name: EngineerName;
|
|
110
|
+
wrapperSessionId: string | null;
|
|
111
|
+
claudeSessionId: string | null;
|
|
112
|
+
busy: boolean;
|
|
113
|
+
lastMode: EngineerWorkMode | null;
|
|
114
|
+
lastTaskSummary: string | null;
|
|
115
|
+
lastUsedAt: string | null;
|
|
116
|
+
wrapperHistory: WrapperHistoryEntry[];
|
|
117
|
+
context: SessionContextSnapshot;
|
|
118
|
+
}
|
|
119
|
+
export interface TeamRecord {
|
|
120
|
+
id: string;
|
|
121
|
+
cwd: string;
|
|
122
|
+
createdAt: string;
|
|
123
|
+
updatedAt: string;
|
|
124
|
+
engineers: TeamEngineerRecord[];
|
|
125
|
+
}
|
|
126
|
+
export interface EngineerTaskResult {
|
|
127
|
+
teamId: string;
|
|
128
|
+
engineer: EngineerName;
|
|
129
|
+
mode: EngineerWorkMode;
|
|
130
|
+
sessionId?: string;
|
|
131
|
+
finalText: string;
|
|
132
|
+
turns?: number;
|
|
133
|
+
totalCostUsd?: number;
|
|
134
|
+
inputTokens?: number;
|
|
135
|
+
outputTokens?: number;
|
|
136
|
+
contextWindowSize?: number;
|
|
137
|
+
context: SessionContextSnapshot;
|
|
138
|
+
}
|
|
139
|
+
export interface PlanDraft extends EngineerTaskResult {
|
|
140
|
+
request: string;
|
|
141
|
+
}
|
|
142
|
+
export interface SynthesizedPlanResult {
|
|
143
|
+
teamId: string;
|
|
144
|
+
request: string;
|
|
145
|
+
leadEngineer: EngineerName;
|
|
146
|
+
challengerEngineer: EngineerName;
|
|
147
|
+
drafts: [PlanDraft, PlanDraft];
|
|
148
|
+
synthesis: string;
|
|
149
|
+
recommendedQuestion: string | null;
|
|
150
|
+
recommendedAnswer: string | null;
|
|
151
|
+
}
|
|
100
152
|
export interface GitDiffResult {
|
|
101
153
|
hasDiff: boolean;
|
|
102
154
|
diffText: string;
|
|
@@ -111,16 +163,6 @@ export interface GitOperationResult {
|
|
|
111
163
|
output: string;
|
|
112
164
|
error?: string;
|
|
113
165
|
}
|
|
114
|
-
export interface ActiveSessionState {
|
|
115
|
-
sessionId: string;
|
|
116
|
-
cwd: string;
|
|
117
|
-
startedAt: string;
|
|
118
|
-
totalTurns: number;
|
|
119
|
-
totalCostUsd: number;
|
|
120
|
-
estimatedContextPercent: number | null;
|
|
121
|
-
contextWindowSize: number | null;
|
|
122
|
-
latestInputTokens: number | null;
|
|
123
|
-
}
|
|
124
166
|
export type PersistentRunStatus = 'running' | 'completed' | 'failed';
|
|
125
167
|
export interface PersistentRunMessageRecord {
|
|
126
168
|
timestamp: string;
|
|
@@ -133,7 +175,7 @@ export interface PersistentRunMessageRecord {
|
|
|
133
175
|
}
|
|
134
176
|
export interface PersistentRunActionRecord {
|
|
135
177
|
timestamp: string;
|
|
136
|
-
type: 'git_diff' | 'git_commit' | 'git_reset' | 'compact' | 'clear';
|
|
178
|
+
type: 'git_diff' | 'git_commit' | 'git_reset' | 'git_status' | 'git_log' | 'compact' | 'clear';
|
|
137
179
|
result: string;
|
|
138
180
|
}
|
|
139
181
|
export interface PersistentRunRecord {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const DEFAULT_ENGINEER_NAMES = ['Tom', 'John', 'Maya', 'Sara', 'Alex'];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TeamRecord } from '../types/contracts.js';
|
|
2
|
+
export declare class TeamStateStore {
|
|
3
|
+
private readonly baseDirectoryName;
|
|
4
|
+
private readonly writeQueues;
|
|
5
|
+
constructor(baseDirectoryName?: string);
|
|
6
|
+
saveTeam(team: TeamRecord): Promise<void>;
|
|
7
|
+
getTeam(cwd: string, teamId: string): Promise<TeamRecord | null>;
|
|
8
|
+
listTeams(cwd: string): Promise<TeamRecord[]>;
|
|
9
|
+
updateTeam(cwd: string, teamId: string, update: (team: TeamRecord) => TeamRecord): Promise<TeamRecord>;
|
|
10
|
+
private getTeamKey;
|
|
11
|
+
private getTeamsDirectory;
|
|
12
|
+
private getTeamPath;
|
|
13
|
+
private enqueueWrite;
|
|
14
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isFileNotFoundError, writeJsonAtomically } from '../util/fs-helpers.js';
|
|
4
|
+
export class TeamStateStore {
|
|
5
|
+
baseDirectoryName;
|
|
6
|
+
writeQueues = new Map();
|
|
7
|
+
constructor(baseDirectoryName = '.claude-manager') {
|
|
8
|
+
this.baseDirectoryName = baseDirectoryName;
|
|
9
|
+
}
|
|
10
|
+
async saveTeam(team) {
|
|
11
|
+
await this.enqueueWrite(this.getTeamKey(team.cwd, team.id), async () => {
|
|
12
|
+
const filePath = this.getTeamPath(team.cwd, team.id);
|
|
13
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
14
|
+
await writeJsonAtomically(filePath, team);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
async getTeam(cwd, teamId) {
|
|
18
|
+
const filePath = this.getTeamPath(cwd, teamId);
|
|
19
|
+
try {
|
|
20
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
21
|
+
return JSON.parse(content);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (isFileNotFoundError(error)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async listTeams(cwd) {
|
|
31
|
+
const directory = this.getTeamsDirectory(cwd);
|
|
32
|
+
try {
|
|
33
|
+
const entries = await fs.readdir(directory);
|
|
34
|
+
const teams = await Promise.all(entries
|
|
35
|
+
.filter((entry) => entry.endsWith('.json'))
|
|
36
|
+
.map(async (entry) => {
|
|
37
|
+
const content = await fs.readFile(path.join(directory, entry), 'utf8');
|
|
38
|
+
return JSON.parse(content);
|
|
39
|
+
}));
|
|
40
|
+
return teams.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (isFileNotFoundError(error)) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async updateTeam(cwd, teamId, update) {
|
|
50
|
+
return this.enqueueWrite(this.getTeamKey(cwd, teamId), async () => {
|
|
51
|
+
const existing = await this.getTeam(cwd, teamId);
|
|
52
|
+
if (!existing) {
|
|
53
|
+
throw new Error(`Team ${teamId} does not exist.`);
|
|
54
|
+
}
|
|
55
|
+
const updated = update(existing);
|
|
56
|
+
const filePath = this.getTeamPath(cwd, teamId);
|
|
57
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
58
|
+
await writeJsonAtomically(filePath, updated);
|
|
59
|
+
return updated;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
getTeamKey(cwd, teamId) {
|
|
63
|
+
return `${cwd}:${teamId}`;
|
|
64
|
+
}
|
|
65
|
+
getTeamsDirectory(cwd) {
|
|
66
|
+
return path.join(cwd, this.baseDirectoryName, 'teams');
|
|
67
|
+
}
|
|
68
|
+
getTeamPath(cwd, teamId) {
|
|
69
|
+
return path.join(this.getTeamsDirectory(cwd), `${teamId}.json`);
|
|
70
|
+
}
|
|
71
|
+
async enqueueWrite(key, operation) {
|
|
72
|
+
const previous = this.writeQueues.get(key) ?? Promise.resolve();
|
|
73
|
+
const resultPromise = previous.catch(() => undefined).then(operation);
|
|
74
|
+
const settledPromise = resultPromise.then(() => undefined, () => undefined);
|
|
75
|
+
this.writeQueues.set(key, settledPromise);
|
|
76
|
+
try {
|
|
77
|
+
return await resultPromise;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
if (this.writeQueues.get(key) === settledPromise) {
|
|
81
|
+
this.writeQueues.delete(key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type EngineerName, type TeamEngineerRecord, type TeamRecord } from '../types/contracts.js';
|
|
2
|
+
export declare const TEAM_ENGINEERS: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
|
|
3
|
+
export declare function isEngineerName(value: string): value is EngineerName;
|
|
4
|
+
export declare function createEmptyTeamRecord(teamId: string, cwd: string): TeamRecord;
|
|
5
|
+
export declare function createEmptyEngineerRecord(name: EngineerName): TeamEngineerRecord;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DEFAULT_ENGINEER_NAMES, } from '../types/contracts.js';
|
|
2
|
+
export const TEAM_ENGINEERS = DEFAULT_ENGINEER_NAMES;
|
|
3
|
+
export function isEngineerName(value) {
|
|
4
|
+
return TEAM_ENGINEERS.includes(value);
|
|
5
|
+
}
|
|
6
|
+
export function createEmptyTeamRecord(teamId, cwd) {
|
|
7
|
+
const timestamp = new Date().toISOString();
|
|
8
|
+
return {
|
|
9
|
+
id: teamId,
|
|
10
|
+
cwd,
|
|
11
|
+
createdAt: timestamp,
|
|
12
|
+
updatedAt: timestamp,
|
|
13
|
+
engineers: TEAM_ENGINEERS.map((name) => createEmptyEngineerRecord(name)),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function createEmptyEngineerRecord(name) {
|
|
17
|
+
return {
|
|
18
|
+
name,
|
|
19
|
+
wrapperSessionId: null,
|
|
20
|
+
claudeSessionId: null,
|
|
21
|
+
busy: false,
|
|
22
|
+
lastMode: null,
|
|
23
|
+
lastTaskSummary: null,
|
|
24
|
+
lastUsedAt: null,
|
|
25
|
+
wrapperHistory: [],
|
|
26
|
+
context: {
|
|
27
|
+
sessionId: null,
|
|
28
|
+
totalTurns: 0,
|
|
29
|
+
totalCostUsd: 0,
|
|
30
|
+
latestInputTokens: null,
|
|
31
|
+
latestOutputTokens: null,
|
|
32
|
+
contextWindowSize: null,
|
|
33
|
+
estimatedContextPercent: null,
|
|
34
|
+
warningLevel: 'ok',
|
|
35
|
+
compactionCount: 0,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -1,22 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
5
2
|
import { ClaudeManagerPlugin } from '../src/plugin/claude-manager.plugin.js';
|
|
6
|
-
import { AGENT_CTO,
|
|
7
|
-
import { managerPromptRegistry } from '../src/prompts/registry.js';
|
|
8
|
-
/** All engineer tools — none belong to CTO. */
|
|
9
|
-
const ENGINEER_TOOL_IDS = [
|
|
10
|
-
'explore',
|
|
11
|
-
'implement',
|
|
12
|
-
'compact_context',
|
|
13
|
-
'clear_session',
|
|
14
|
-
'session_health',
|
|
15
|
-
'list_transcripts',
|
|
16
|
-
'list_history',
|
|
17
|
-
];
|
|
3
|
+
import { AGENT_CTO, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from '../src/plugin/agent-hierarchy.js';
|
|
18
4
|
describe('ClaudeManagerPlugin', () => {
|
|
19
|
-
it('configures
|
|
5
|
+
it('configures CTO with orchestration tools and question access', async () => {
|
|
20
6
|
const plugin = await ClaudeManagerPlugin({
|
|
21
7
|
worktree: '/tmp/project',
|
|
22
8
|
});
|
|
@@ -26,66 +12,12 @@ describe('ClaudeManagerPlugin', () => {
|
|
|
26
12
|
const cto = agents[AGENT_CTO];
|
|
27
13
|
expect(cto).toBeDefined();
|
|
28
14
|
expect(cto.mode).toBe('primary');
|
|
29
|
-
// CTO should have read-only codebase and utility tools
|
|
30
15
|
expect(cto.permission).toMatchObject({
|
|
31
16
|
'*': 'deny',
|
|
32
17
|
read: 'allow',
|
|
33
18
|
grep: 'allow',
|
|
34
19
|
glob: 'allow',
|
|
35
|
-
|
|
36
|
-
webfetch: 'allow',
|
|
37
|
-
websearch: 'allow',
|
|
38
|
-
todowrite: 'allow',
|
|
39
|
-
todoread: 'allow',
|
|
40
|
-
question: 'allow',
|
|
41
|
-
});
|
|
42
|
-
// CTO should have git tools allowed
|
|
43
|
-
expect(cto.permission).toHaveProperty('git_diff', 'allow');
|
|
44
|
-
expect(cto.permission).toHaveProperty('git_commit', 'allow');
|
|
45
|
-
expect(cto.permission).toHaveProperty('git_reset', 'allow');
|
|
46
|
-
// CTO should have approval tools allowed
|
|
47
|
-
expect(cto.permission).toHaveProperty('approval_policy', 'allow');
|
|
48
|
-
expect(cto.permission).toHaveProperty('approval_decisions', 'allow');
|
|
49
|
-
expect(cto.permission).toHaveProperty('approval_update', 'allow');
|
|
50
|
-
// CTO should have ALL engineer tools DENIED
|
|
51
|
-
for (const toolId of ENGINEER_TOOL_IDS) {
|
|
52
|
-
expect(cto.permission).toHaveProperty(toolId, 'deny');
|
|
53
|
-
}
|
|
54
|
-
// CTO should have task permission to spawn engineer subagents
|
|
55
|
-
expect(cto.permission).toHaveProperty('task');
|
|
56
|
-
expect(cto.permission['task']).toMatchObject({
|
|
57
|
-
'*': 'deny',
|
|
58
|
-
engineer_plan: 'allow',
|
|
59
|
-
engineer_build: 'allow',
|
|
60
|
-
});
|
|
61
|
-
// CTO should NOT have bash or edit
|
|
62
|
-
expect(cto.permission).not.toHaveProperty('bash');
|
|
63
|
-
expect(cto.permission).not.toHaveProperty('edit');
|
|
64
|
-
});
|
|
65
|
-
it('configures engineer_plan wrapper with only plan-mode send tool', async () => {
|
|
66
|
-
const plugin = await ClaudeManagerPlugin({
|
|
67
|
-
worktree: '/tmp/project',
|
|
68
|
-
});
|
|
69
|
-
const config = {};
|
|
70
|
-
await plugin.config?.(config);
|
|
71
|
-
const agents = (config.agent ?? {});
|
|
72
|
-
const engineerPlan = agents[AGENT_ENGINEER_PLAN];
|
|
73
|
-
expect(engineerPlan).toBeDefined();
|
|
74
|
-
expect(engineerPlan.mode).toBe('subagent');
|
|
75
|
-
// Should allow explore and shared session tools
|
|
76
|
-
expect(engineerPlan.permission).toHaveProperty('explore', 'allow');
|
|
77
|
-
expect(engineerPlan.permission).toHaveProperty('compact_context', 'allow');
|
|
78
|
-
expect(engineerPlan.permission).toHaveProperty('session_health', 'allow');
|
|
79
|
-
// Should DENY the build-mode send
|
|
80
|
-
expect(engineerPlan.permission).toHaveProperty('implement', 'deny');
|
|
81
|
-
// Should deny git and approval tools
|
|
82
|
-
expect(engineerPlan.permission).toHaveProperty('git_diff', 'deny');
|
|
83
|
-
expect(engineerPlan.permission).toHaveProperty('approval_policy', 'deny');
|
|
84
|
-
// Should have read-only investigation tools
|
|
85
|
-
expect(engineerPlan.permission).toMatchObject({
|
|
86
|
-
read: 'allow',
|
|
87
|
-
grep: 'allow',
|
|
88
|
-
glob: 'allow',
|
|
20
|
+
list: 'allow',
|
|
89
21
|
codesearch: 'allow',
|
|
90
22
|
webfetch: 'allow',
|
|
91
23
|
websearch: 'allow',
|
|
@@ -93,239 +25,82 @@ describe('ClaudeManagerPlugin', () => {
|
|
|
93
25
|
todowrite: 'allow',
|
|
94
26
|
todoread: 'allow',
|
|
95
27
|
question: 'allow',
|
|
28
|
+
team_status: 'allow',
|
|
29
|
+
git_diff: 'allow',
|
|
30
|
+
git_commit: 'allow',
|
|
31
|
+
git_reset: 'allow',
|
|
32
|
+
git_status: 'allow',
|
|
33
|
+
git_log: 'allow',
|
|
34
|
+
claude: 'deny',
|
|
35
|
+
});
|
|
36
|
+
expect(cto.permission.task).toEqual({
|
|
37
|
+
'*': 'deny',
|
|
38
|
+
tom: 'allow',
|
|
39
|
+
john: 'allow',
|
|
40
|
+
maya: 'allow',
|
|
41
|
+
sara: 'allow',
|
|
42
|
+
alex: 'allow',
|
|
96
43
|
});
|
|
97
44
|
});
|
|
98
|
-
it('configures
|
|
99
|
-
const plugin = await ClaudeManagerPlugin({
|
|
100
|
-
worktree: '/tmp/project',
|
|
101
|
-
});
|
|
102
|
-
const config = {};
|
|
103
|
-
await plugin.config?.(config);
|
|
104
|
-
const agents = (config.agent ?? {});
|
|
105
|
-
const engineerBuild = agents[AGENT_ENGINEER_BUILD];
|
|
106
|
-
expect(engineerBuild).toBeDefined();
|
|
107
|
-
expect(engineerBuild.mode).toBe('subagent');
|
|
108
|
-
// Should allow implement and shared session tools
|
|
109
|
-
expect(engineerBuild.permission).toHaveProperty('implement', 'allow');
|
|
110
|
-
expect(engineerBuild.permission).toHaveProperty('compact_context', 'allow');
|
|
111
|
-
expect(engineerBuild.permission).toHaveProperty('session_health', 'allow');
|
|
112
|
-
// Should DENY the plan-mode send
|
|
113
|
-
expect(engineerBuild.permission).toHaveProperty('explore', 'deny');
|
|
114
|
-
// Should deny git and approval tools
|
|
115
|
-
expect(engineerBuild.permission).toHaveProperty('git_diff', 'deny');
|
|
116
|
-
expect(engineerBuild.permission).toHaveProperty('approval_policy', 'deny');
|
|
117
|
-
// Should have read-only investigation tools
|
|
118
|
-
expect(engineerBuild.permission).toMatchObject({
|
|
119
|
-
read: 'allow',
|
|
120
|
-
grep: 'allow',
|
|
121
|
-
glob: 'allow',
|
|
122
|
-
codesearch: 'allow',
|
|
123
|
-
webfetch: 'allow',
|
|
124
|
-
websearch: 'allow',
|
|
125
|
-
lsp: 'allow',
|
|
126
|
-
todowrite: 'allow',
|
|
127
|
-
todoread: 'allow',
|
|
128
|
-
question: 'allow',
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
it('CTO description reflects delegation-first behavior', async () => {
|
|
132
|
-
const plugin = await ClaudeManagerPlugin({
|
|
133
|
-
worktree: '/tmp/project',
|
|
134
|
-
});
|
|
135
|
-
const config = {};
|
|
136
|
-
await plugin.config?.(config);
|
|
137
|
-
const agents = (config.agent ?? {});
|
|
138
|
-
expect(agents[AGENT_CTO].description).toContain('Delegates by default');
|
|
139
|
-
expect(agents[AGENT_CTO].description).toContain('minimal spot-checks');
|
|
140
|
-
});
|
|
141
|
-
it('engineer_plan description reflects thin wrapper dispatching to Claude Code', async () => {
|
|
142
|
-
const plugin = await ClaudeManagerPlugin({
|
|
143
|
-
worktree: '/tmp/project',
|
|
144
|
-
});
|
|
145
|
-
const config = {};
|
|
146
|
-
await plugin.config?.(config);
|
|
147
|
-
const agents = (config.agent ?? {});
|
|
148
|
-
expect(agents[AGENT_ENGINEER_PLAN].description).toContain('Thin high-judgment wrapper');
|
|
149
|
-
expect(agents[AGENT_ENGINEER_PLAN].description).toContain('dispatches to Claude Code');
|
|
150
|
-
});
|
|
151
|
-
it('engineer_build description reflects thin wrapper dispatching to Claude Code', async () => {
|
|
152
|
-
const plugin = await ClaudeManagerPlugin({
|
|
153
|
-
worktree: '/tmp/project',
|
|
154
|
-
});
|
|
155
|
-
const config = {};
|
|
156
|
-
await plugin.config?.(config);
|
|
157
|
-
const agents = (config.agent ?? {});
|
|
158
|
-
expect(agents[AGENT_ENGINEER_BUILD].description).toContain('Thin high-judgment wrapper');
|
|
159
|
-
expect(agents[AGENT_ENGINEER_BUILD].description).toContain('dispatches to Claude Code');
|
|
160
|
-
});
|
|
161
|
-
it('explore tool description marks it as preferred first step', async () => {
|
|
162
|
-
const plugin = await ClaudeManagerPlugin({
|
|
163
|
-
worktree: '/tmp/project',
|
|
164
|
-
});
|
|
165
|
-
const tools = plugin.tool;
|
|
166
|
-
expect(tools['explore'].description).toContain('Preferred first step before implementation');
|
|
167
|
-
});
|
|
168
|
-
it('implement tool description marks it for code changes', async () => {
|
|
169
|
-
const plugin = await ClaudeManagerPlugin({
|
|
170
|
-
worktree: '/tmp/project',
|
|
171
|
-
});
|
|
172
|
-
const tools = plugin.tool;
|
|
173
|
-
expect(tools['implement'].description).toContain('Implement code changes');
|
|
174
|
-
});
|
|
175
|
-
it('does not register old agent names', async () => {
|
|
45
|
+
it('configures every named engineer with only the claude bridge tool', async () => {
|
|
176
46
|
const plugin = await ClaudeManagerPlugin({
|
|
177
47
|
worktree: '/tmp/project',
|
|
178
48
|
});
|
|
179
49
|
const config = {};
|
|
180
50
|
await plugin.config?.(config);
|
|
181
51
|
const agents = (config.agent ?? {});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
52
|
+
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
53
|
+
const agentId = ENGINEER_AGENT_IDS[engineer];
|
|
54
|
+
const agent = agents[agentId];
|
|
55
|
+
expect(agent).toBeDefined();
|
|
56
|
+
expect(agent.mode).toBe('subagent');
|
|
57
|
+
expect(agent.description).toContain(engineer);
|
|
58
|
+
expect(agent.description).toContain('persistent engineer');
|
|
59
|
+
expect(agent.permission).toMatchObject({
|
|
60
|
+
'*': 'deny',
|
|
61
|
+
claude: 'allow',
|
|
62
|
+
git_diff: 'deny',
|
|
63
|
+
git_commit: 'deny',
|
|
64
|
+
});
|
|
65
|
+
expect(agent.permission).not.toHaveProperty('read');
|
|
66
|
+
expect(agent.permission).not.toHaveProperty('grep');
|
|
67
|
+
}
|
|
194
68
|
});
|
|
195
|
-
it('
|
|
69
|
+
it('registers the named engineer bridge and team status tools', async () => {
|
|
196
70
|
const plugin = await ClaudeManagerPlugin({
|
|
197
71
|
worktree: '/tmp/project',
|
|
198
72
|
});
|
|
199
73
|
const tools = plugin.tool;
|
|
200
|
-
|
|
201
|
-
expect(
|
|
202
|
-
|
|
203
|
-
expect(
|
|
204
|
-
expect(freshSchema.parse(undefined)).toBe(false);
|
|
205
|
-
expect(freshSchema.safeParse(true).success).toBe(true);
|
|
206
|
-
expect(freshSchema.safeParse(false).success).toBe(true);
|
|
74
|
+
expect(tools['claude']).toBeDefined();
|
|
75
|
+
expect(tools['team_status']).toBeDefined();
|
|
76
|
+
expect(tools['assign_engineer']).toBeUndefined();
|
|
77
|
+
expect(tools['plan_with_team']).toBeUndefined();
|
|
207
78
|
});
|
|
208
|
-
it('
|
|
79
|
+
it('claude tool requires mode and message and supports optional model', async () => {
|
|
209
80
|
const plugin = await ClaudeManagerPlugin({
|
|
210
81
|
worktree: '/tmp/project',
|
|
211
82
|
});
|
|
212
83
|
const tools = plugin.tool;
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
84
|
+
const claudeTool = tools['claude'];
|
|
85
|
+
const modeSchema = claudeTool.args.mode;
|
|
86
|
+
const messageSchema = claudeTool.args.message;
|
|
87
|
+
const modelSchema = claudeTool.args.model;
|
|
88
|
+
expect(modeSchema.safeParse('explore').success).toBe(true);
|
|
89
|
+
expect(modeSchema.safeParse('implement').success).toBe(true);
|
|
90
|
+
expect(modeSchema.safeParse('verify').success).toBe(true);
|
|
91
|
+
expect(modeSchema.safeParse('plan').success).toBe(false);
|
|
92
|
+
expect(messageSchema.safeParse('do the work').success).toBe(true);
|
|
93
|
+
expect(messageSchema.safeParse('').success).toBe(false);
|
|
216
94
|
expect(modelSchema.safeParse('claude-opus-4-6').success).toBe(true);
|
|
217
95
|
expect(modelSchema.safeParse('claude-sonnet-4-6').success).toBe(true);
|
|
218
|
-
expect(modelSchema.safeParse('claude-sonnet-4-5').success).toBe(true);
|
|
219
96
|
expect(modelSchema.safeParse(undefined).success).toBe(true);
|
|
220
97
|
expect(modelSchema.safeParse('claude-haiku-4-5').success).toBe(false);
|
|
221
|
-
expect(modelSchema.safeParse('anything-else').success).toBe(false);
|
|
222
98
|
});
|
|
223
|
-
it('exposes
|
|
99
|
+
it('exposes hooks for CTO team tracking and wrapper memory injection', async () => {
|
|
224
100
|
const plugin = await ClaudeManagerPlugin({
|
|
225
101
|
worktree: '/tmp/project',
|
|
226
102
|
});
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const effortSchema = exploreTool.args.effort;
|
|
230
|
-
expect(effortSchema).toBeDefined();
|
|
231
|
-
expect(effortSchema.safeParse('low').success).toBe(true);
|
|
232
|
-
expect(effortSchema.safeParse('medium').success).toBe(true);
|
|
233
|
-
expect(effortSchema.safeParse('high').success).toBe(true);
|
|
234
|
-
expect(effortSchema.safeParse('max').success).toBe(true);
|
|
235
|
-
expect(effortSchema.parse(undefined)).toBe('high');
|
|
236
|
-
expect(effortSchema.safeParse('turbo').success).toBe(false);
|
|
237
|
-
});
|
|
238
|
-
it('exposes compact_context tool', async () => {
|
|
239
|
-
const plugin = await ClaudeManagerPlugin({
|
|
240
|
-
worktree: '/tmp/project',
|
|
241
|
-
});
|
|
242
|
-
const tools = plugin.tool;
|
|
243
|
-
expect(tools['compact_context']).toBeDefined();
|
|
244
|
-
});
|
|
245
|
-
it('exposes all tool IDs matching the hierarchy constants', async () => {
|
|
246
|
-
const plugin = await ClaudeManagerPlugin({
|
|
247
|
-
worktree: '/tmp/project',
|
|
248
|
-
});
|
|
249
|
-
const tools = plugin.tool;
|
|
250
|
-
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
251
|
-
expect(tools).toHaveProperty(toolId);
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
it('does not expose old claude_manager_* tool IDs', async () => {
|
|
255
|
-
const plugin = await ClaudeManagerPlugin({
|
|
256
|
-
worktree: '/tmp/project',
|
|
257
|
-
});
|
|
258
|
-
const tools = plugin.tool;
|
|
259
|
-
const oldToolIds = [
|
|
260
|
-
'claude_manager_send',
|
|
261
|
-
'claude_manager_compact',
|
|
262
|
-
'claude_manager_git_diff',
|
|
263
|
-
'claude_manager_git_commit',
|
|
264
|
-
'claude_manager_git_reset',
|
|
265
|
-
'claude_manager_clear',
|
|
266
|
-
'claude_manager_status',
|
|
267
|
-
'claude_manager_metadata',
|
|
268
|
-
'claude_manager_sessions',
|
|
269
|
-
'claude_manager_runs',
|
|
270
|
-
'claude_manager_approval_policy',
|
|
271
|
-
'claude_manager_approval_decisions',
|
|
272
|
-
'claude_manager_approval_update',
|
|
273
|
-
];
|
|
274
|
-
for (const oldId of oldToolIds) {
|
|
275
|
-
expect(tools).not.toHaveProperty(oldId);
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
describe('wrapper prompts include discovered Claude files', () => {
|
|
279
|
-
const tmpDirs = [];
|
|
280
|
-
async function makeTmp() {
|
|
281
|
-
const d = await mkdtemp(join(tmpdir(), 'plugin-test-'));
|
|
282
|
-
tmpDirs.push(d);
|
|
283
|
-
return d;
|
|
284
|
-
}
|
|
285
|
-
afterEach(async () => {
|
|
286
|
-
for (const d of tmpDirs) {
|
|
287
|
-
await rm(d, { recursive: true, force: true });
|
|
288
|
-
}
|
|
289
|
-
tmpDirs.length = 0;
|
|
290
|
-
});
|
|
291
|
-
it('injects project Claude files into engineer plan/build wrapper prompts', async () => {
|
|
292
|
-
const worktree = await makeTmp();
|
|
293
|
-
await writeFile(join(worktree, 'CLAUDE.md'), 'Use pnpm. No default exports.');
|
|
294
|
-
await mkdir(join(worktree, '.claude'), { recursive: true });
|
|
295
|
-
await writeFile(join(worktree, '.claude/settings.md'), 'Extra settings');
|
|
296
|
-
const plugin = await ClaudeManagerPlugin({ worktree });
|
|
297
|
-
const config = {};
|
|
298
|
-
await plugin.config?.(config);
|
|
299
|
-
const agents = config.agent;
|
|
300
|
-
const planPrompt = agents[AGENT_ENGINEER_PLAN].prompt;
|
|
301
|
-
const buildPrompt = agents[AGENT_ENGINEER_BUILD].prompt;
|
|
302
|
-
// Both wrapper prompts should contain discovered file content
|
|
303
|
-
expect(planPrompt).toContain('## Project Claude Files');
|
|
304
|
-
expect(planPrompt).toContain('### CLAUDE.md');
|
|
305
|
-
expect(planPrompt).toContain('Use pnpm. No default exports.');
|
|
306
|
-
expect(planPrompt).toContain('### .claude/settings.md');
|
|
307
|
-
expect(planPrompt).toContain('Extra settings');
|
|
308
|
-
expect(buildPrompt).toContain('## Project Claude Files');
|
|
309
|
-
expect(buildPrompt).toContain('### CLAUDE.md');
|
|
310
|
-
expect(buildPrompt).toContain('Use pnpm. No default exports.');
|
|
311
|
-
});
|
|
312
|
-
it('CTO prompt stays unchanged regardless of Claude files', async () => {
|
|
313
|
-
const worktree = await makeTmp();
|
|
314
|
-
await writeFile(join(worktree, 'CLAUDE.md'), 'project rules');
|
|
315
|
-
const plugin = await ClaudeManagerPlugin({ worktree });
|
|
316
|
-
const config = {};
|
|
317
|
-
await plugin.config?.(config);
|
|
318
|
-
const agents = config.agent;
|
|
319
|
-
expect(agents[AGENT_CTO].prompt).toBe(managerPromptRegistry.ctoSystemPrompt);
|
|
320
|
-
});
|
|
321
|
-
it('wrapper prompts equal base when no Claude files exist', async () => {
|
|
322
|
-
const worktree = await makeTmp();
|
|
323
|
-
const plugin = await ClaudeManagerPlugin({ worktree });
|
|
324
|
-
const config = {};
|
|
325
|
-
await plugin.config?.(config);
|
|
326
|
-
const agents = config.agent;
|
|
327
|
-
expect(agents[AGENT_ENGINEER_PLAN].prompt).toBe(managerPromptRegistry.engineerPlanPrompt);
|
|
328
|
-
expect(agents[AGENT_ENGINEER_BUILD].prompt).toBe(managerPromptRegistry.engineerBuildPrompt);
|
|
329
|
-
});
|
|
103
|
+
expect(plugin['chat.message']).toBeTypeOf('function');
|
|
104
|
+
expect(plugin['experimental.chat.system.transform']).toBeTypeOf('function');
|
|
330
105
|
});
|
|
331
106
|
});
|