@doingdev/opencode-claude-manager-plugin 0.1.46 → 0.1.49

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.
Files changed (55) hide show
  1. package/README.md +29 -31
  2. package/dist/index.d.ts +1 -1
  3. package/dist/manager/team-orchestrator.d.ts +50 -0
  4. package/dist/manager/team-orchestrator.js +360 -0
  5. package/dist/plugin/agent-hierarchy.d.ts +12 -34
  6. package/dist/plugin/agent-hierarchy.js +36 -129
  7. package/dist/plugin/claude-manager.plugin.js +233 -421
  8. package/dist/plugin/service-factory.d.ts +20 -3
  9. package/dist/plugin/service-factory.js +46 -1
  10. package/dist/prompts/registry.d.ts +1 -10
  11. package/dist/prompts/registry.js +42 -261
  12. package/dist/src/claude/claude-agent-sdk-adapter.js +2 -1
  13. package/dist/src/claude/session-live-tailer.js +2 -2
  14. package/dist/src/index.d.ts +1 -1
  15. package/dist/src/manager/git-operations.d.ts +10 -1
  16. package/dist/src/manager/git-operations.js +18 -3
  17. package/dist/src/manager/persistent-manager.d.ts +18 -6
  18. package/dist/src/manager/persistent-manager.js +19 -13
  19. package/dist/src/manager/session-controller.d.ts +7 -10
  20. package/dist/src/manager/session-controller.js +12 -62
  21. package/dist/src/manager/team-orchestrator.d.ts +50 -0
  22. package/dist/src/manager/team-orchestrator.js +360 -0
  23. package/dist/src/plugin/agent-hierarchy.d.ts +12 -26
  24. package/dist/src/plugin/agent-hierarchy.js +36 -99
  25. package/dist/src/plugin/claude-manager.plugin.js +257 -391
  26. package/dist/src/plugin/service-factory.d.ts +20 -3
  27. package/dist/src/plugin/service-factory.js +47 -9
  28. package/dist/src/prompts/registry.d.ts +1 -10
  29. package/dist/src/prompts/registry.js +41 -246
  30. package/dist/src/state/team-state-store.d.ts +17 -0
  31. package/dist/src/state/team-state-store.js +107 -0
  32. package/dist/src/team/roster.d.ts +5 -0
  33. package/dist/src/team/roster.js +38 -0
  34. package/dist/src/types/contracts.d.ts +55 -13
  35. package/dist/src/types/contracts.js +1 -1
  36. package/dist/state/team-state-store.d.ts +17 -0
  37. package/dist/state/team-state-store.js +107 -0
  38. package/dist/team/roster.d.ts +5 -0
  39. package/dist/team/roster.js +38 -0
  40. package/dist/test/claude-manager.plugin.test.js +55 -280
  41. package/dist/test/cto-active-team.test.d.ts +1 -0
  42. package/dist/test/cto-active-team.test.js +52 -0
  43. package/dist/test/git-operations.test.js +65 -1
  44. package/dist/test/persistent-manager.test.js +3 -3
  45. package/dist/test/prompt-registry.test.js +32 -252
  46. package/dist/test/report-claude-event.test.d.ts +1 -0
  47. package/dist/test/report-claude-event.test.js +246 -0
  48. package/dist/test/session-controller.test.js +27 -27
  49. package/dist/test/team-orchestrator.test.d.ts +1 -0
  50. package/dist/test/team-orchestrator.test.js +146 -0
  51. package/dist/test/team-state-store.test.d.ts +1 -0
  52. package/dist/test/team-state-store.test.js +72 -0
  53. package/dist/types/contracts.d.ts +54 -3
  54. package/dist/types/contracts.js +1 -1
  55. package/package.json +1 -1
@@ -1,7 +1,6 @@
1
1
  export interface ManagerPromptRegistry {
2
2
  ctoSystemPrompt: string;
3
- engineerPlanPrompt: string;
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,17 @@
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
+ getActiveTeam(cwd: string): Promise<string | null>;
11
+ setActiveTeam(cwd: string, teamId: string): Promise<void>;
12
+ private getTeamKey;
13
+ private getActiveTeamPath;
14
+ private getTeamsDirectory;
15
+ private getTeamPath;
16
+ private enqueueWrite;
17
+ }
@@ -0,0 +1,107 @@
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
+ async getActiveTeam(cwd) {
63
+ const filePath = this.getActiveTeamPath(cwd);
64
+ try {
65
+ const content = await fs.readFile(filePath, 'utf8');
66
+ const parsed = JSON.parse(content);
67
+ return parsed.teamId ?? null;
68
+ }
69
+ catch (error) {
70
+ if (isFileNotFoundError(error)) {
71
+ return null;
72
+ }
73
+ throw error;
74
+ }
75
+ }
76
+ async setActiveTeam(cwd, teamId) {
77
+ const filePath = this.getActiveTeamPath(cwd);
78
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
79
+ await writeJsonAtomically(filePath, { teamId });
80
+ }
81
+ getTeamKey(cwd, teamId) {
82
+ return `${cwd}:${teamId}`;
83
+ }
84
+ getActiveTeamPath(cwd) {
85
+ return path.join(cwd, this.baseDirectoryName, 'active-team.json');
86
+ }
87
+ getTeamsDirectory(cwd) {
88
+ return path.join(cwd, this.baseDirectoryName, 'teams');
89
+ }
90
+ getTeamPath(cwd, teamId) {
91
+ return path.join(this.getTeamsDirectory(cwd), `${teamId}.json`);
92
+ }
93
+ async enqueueWrite(key, operation) {
94
+ const previous = this.writeQueues.get(key) ?? Promise.resolve();
95
+ const resultPromise = previous.catch(() => undefined).then(operation);
96
+ const settledPromise = resultPromise.then(() => undefined, () => undefined);
97
+ this.writeQueues.set(key, settledPromise);
98
+ try {
99
+ return await resultPromise;
100
+ }
101
+ finally {
102
+ if (this.writeQueues.get(key) === settledPromise) {
103
+ this.writeQueues.delete(key);
104
+ }
105
+ }
106
+ }
107
+ }
@@ -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 { 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';
1
+ import { describe, expect, it } from 'vitest';
5
2
  import { ClaudeManagerPlugin } from '../src/plugin/claude-manager.plugin.js';
6
- import { AGENT_CTO, AGENT_ENGINEER_BUILD, AGENT_ENGINEER_PLAN, ALL_RESTRICTED_TOOL_IDS, } from '../src/plugin/agent-hierarchy.js';
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 cto with git and approval tools but denies all engineer tools', async () => {
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
- codesearch: 'allow',
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 engineer_build wrapper with only build-mode send tool', async () => {
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
- expect(agents).not.toHaveProperty('claude-manager');
183
- expect(agents).not.toHaveProperty('claude-manager-research');
184
- expect(agents).not.toHaveProperty('claude-cto');
185
- expect(agents).not.toHaveProperty('manager');
186
- });
187
- it('does not register any slash commands', async () => {
188
- const plugin = await ClaudeManagerPlugin({
189
- worktree: '/tmp/project',
190
- });
191
- const config = {};
192
- await plugin.config?.(config);
193
- expect(config).not.toHaveProperty('command');
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('exposes freshSession argument on explore defaulting to false', async () => {
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
- const exploreTool = tools['explore'];
201
- expect(exploreTool).toBeDefined();
202
- const freshSchema = exploreTool.args.freshSession;
203
- expect(freshSchema).toBeDefined();
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('exposes model argument as enum of allowed models on implement', async () => {
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 implementTool = tools['implement'];
214
- const modelSchema = implementTool.args.model;
215
- expect(modelSchema).toBeDefined();
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 effort argument as enum defaulting to high on explore', async () => {
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
- const tools = plugin.tool;
228
- const exploreTool = tools['explore'];
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
  });
@@ -0,0 +1 @@
1
+ export {};