@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.
Files changed (51) 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 +190 -423
  8. package/dist/plugin/service-factory.d.ts +18 -3
  9. package/dist/plugin/service-factory.js +32 -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 +214 -393
  26. package/dist/src/plugin/service-factory.d.ts +18 -3
  27. package/dist/src/plugin/service-factory.js +33 -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 +14 -0
  31. package/dist/src/state/team-state-store.js +85 -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 +14 -0
  37. package/dist/state/team-state-store.js +85 -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/git-operations.test.js +65 -1
  42. package/dist/test/persistent-manager.test.js +3 -3
  43. package/dist/test/prompt-registry.test.js +32 -252
  44. package/dist/test/session-controller.test.js +27 -27
  45. package/dist/test/team-orchestrator.test.d.ts +1 -0
  46. package/dist/test/team-orchestrator.test.js +146 -0
  47. package/dist/test/team-state-store.test.d.ts +1 -0
  48. package/dist/test/team-state-store.test.js +54 -0
  49. package/dist/types/contracts.d.ts +54 -3
  50. package/dist/types/contracts.js +1 -1
  51. package/package.json +1 -1
package/README.md CHANGED
@@ -4,17 +4,20 @@ This package provides an OpenCode plugin that lets an OpenCode-side agent hierar
4
4
 
5
5
  ## Overview
6
6
 
7
- Use this when you want OpenCode to act as a manager over Claude Code instead of talking to Claude directly. The plugin gives OpenCode a stable tool surface for delegating work to Claude Code sessions, managing session lifecycle (compact, clear, fresh start), reviewing changes via git, and inspecting session history.
7
+ Use this when you want OpenCode to act like a real technical lead over Claude Code instead of being a thin relay. The plugin gives you a CTO agent that can ask better questions, explicitly assign named engineers, reuse each engineer's Claude session for continuity, preserve wrapper-level engineer memory, compare multiple plans, and keep git/review work at the manager layer.
8
8
 
9
9
  ## Features
10
10
 
11
11
  - Runs Claude Code tasks from OpenCode through `@anthropic-ai/claude-agent-sdk`.
12
- - Persistent sessions with `freshSession`, model, and effort controls for safe task isolation.
13
- - Context lifecycle: compact (preserve state) or clear (start fresh).
12
+ - Creates a persistent named team: `Tom`, `John`, `Maya`, `Sara`, and `Alex`.
13
+ - Reuses one Claude Code session per engineer within the active CTO team.
14
+ - Reloads prior engineer wrapper context so each named subagent can prompt Claude better over time.
15
+ - Uses named engineer subagents for live delegated work while keeping both wrapper memory and Claude session continuity underneath.
16
+ - Keeps session babysitting out of the normal user flow — no public reset/fresh-session controls.
14
17
  - Discovers repo-local Claude metadata from `.claude/skills`, `.claude/commands`, `CLAUDE.md`, and settings hooks.
15
18
  - Git integration: diff, commit, and reset from the manager layer.
16
19
  - Tool approval policy for governing which Claude Code tools are allowed.
17
- - Optionally persists manager run records under `.claude-manager/runs` for post-hoc inspection (populated when tasks are executed through the run-tracking path).
20
+ - Persists local team state and transcripts under `.claude-manager/` for continuity and inspection.
18
21
 
19
22
  ## Requirements
20
23
 
@@ -51,17 +54,17 @@ If you are testing locally, point OpenCode at the local package or plugin file u
51
54
 
52
55
  ## OpenCode tools
53
56
 
54
- ### Engineer session
57
+ ### CTO orchestration
55
58
 
56
- - `explore` investigate and analyze code without making edits. Read-only exploration of the codebase. Preferred first step before implementation.
57
- - `message` (required) — the instruction to send.
58
- - `freshSession` — set to `true` to clear the active session before sending. Use when switching to an unrelated task or when context is contaminated.
59
- - `model` — `"claude-opus-4-6"` (default, recommended for most coding work), `"claude-sonnet-4-6"`, or `"claude-sonnet-4-5"` (faster/lighter tasks).
60
- - `effort` — `"high"` (default), `"medium"` (lighter tasks), `"low"`, or `"max"` (especially hard problems).
61
- - `implement` — implement code changes; can read, edit, and create files. Use after exploration to make changes. Same args as `explore`.
62
- - `compact_context` — compress session history to reclaim context window space. Preserves state while reducing token usage.
63
- - `clear_session` — clear the active session to start fresh. Use when context is full or starting a new task.
64
- - `session_health` — check session health metrics: context usage %, turn count, cost, and session ID.
59
+ - Use the built-in OpenCode `task` tool to delegate to named engineers: `tom`, `john`, `maya`, `sara`, `alex`.
60
+ - `team_status` — inspect the current CTO team's engineer bindings, Claude session IDs, busy flags, and context snapshots.
61
+
62
+ ### Engineer bridge
63
+
64
+ - `claude` — available only inside named engineer subagents. Sends work through that engineer's persistent Claude Code session.
65
+ - `mode` (required) `explore`, `implement`, or `verify`.
66
+ - `message` (required) — the work to do.
67
+ - `model` (optional) `claude-opus-4-6` or `claude-sonnet-4-6`.
65
68
 
66
69
  ### Git operations
67
70
 
@@ -72,7 +75,7 @@ If you are testing locally, point OpenCode at the local package or plugin file u
72
75
  ### Inspection
73
76
 
74
77
  - `list_transcripts` — list available session transcripts or inspect a specific transcript by ID.
75
- - `list_history` — list persistent run records from the manager or inspect a specific run.
78
+ - `list_history` — list saved CTO teams for the worktree or inspect one team by ID.
76
79
 
77
80
  ### Tool approval
78
81
 
@@ -82,11 +85,11 @@ If you are testing locally, point OpenCode at the local package or plugin file u
82
85
 
83
86
  ## Agent hierarchy
84
87
 
85
- The plugin registers a CTO Manager Engineer hierarchy through the OpenCode plugin `config` hook:
88
+ The plugin registers a CTO + named engineer team through the OpenCode plugin `config` hook:
86
89
 
87
- - **`cto`** (primary agent) — sets direction and orchestrates work by spawning `manager` subagents. Has read/search/web tools but does NOT operate Claude Code directly.
88
- - **`manager`** (subagent) — operates a Claude Code engineer through a persistent session. Has the full tool surface (`explore`, `implement`, `compact_context`, `clear_session`, `session_health`, `list_transcripts`, `list_history`, `git_*`, `approval_*`) plus read/search/web tools for investigation.
89
- - **Engineer** — the Claude Code persistent session itself (not an OpenCode agent). Receives instructions from the manager, executes code changes, and reports results.
90
+ - **`cto`** (primary agent) — owns the outcome, finds missing requirements, spawns named engineers with the Task tool, compares plans, reviews diffs, and manages git.
91
+ - **`tom`**, **`john`**, **`maya`**, **`sara`**, **`alex`** (subagents) — thin named engineer wrappers. Each uses the `claude` tool and keeps one persistent Claude Code session.
92
+ - **Claude Code sessions** the underlying execution layer. One session per engineer inside the active team.
90
93
 
91
94
  These are added to OpenCode config at runtime by the plugin, so they do not require separate manual `opencode.json` entries.
92
95
 
@@ -94,27 +97,21 @@ These are added to OpenCode config at runtime by the plugin, so they do not requ
94
97
 
95
98
  Typical flow inside OpenCode:
96
99
 
97
- 1. Explore the codebase with `explore`.
98
- 2. Implement changes with `implement`.
100
+ 1. Ask the `cto` agent for the work.
101
+ 2. Let `cto` spawn one or more named engineers with the Task tool.
99
102
  3. Review changes with `git_diff`, then commit or reset.
100
- 4. Inspect saved Claude history with `list_transcripts` or prior orchestration records with `list_history`.
103
+ 4. Inspect saved Claude history with `list_transcripts` or saved team state with `list_history` / `team_status`.
101
104
 
102
105
  Example tasks:
103
106
 
104
107
  ```text
105
- Use implement to add the new validation logic in src/auth.ts, then review with git_diff.
106
- ```
107
-
108
- Start a fresh session for an unrelated task:
109
-
110
- ```text
111
- Use explore with freshSession:true to investigate the failing CI test in test/api.test.ts.
108
+ Ask CTO to send Tom to implement the new validation logic in src/auth.ts, then review with git_diff.
112
109
  ```
113
110
 
114
- Reclaim context mid-session:
111
+ For a larger feature where you want two independent plans first:
115
112
 
116
113
  ```text
117
- Use compact_context to free up context, then continue with the next implementation step.
114
+ Ask CTO to spawn Maya and Alex in parallel to create two plans for the new billing feature, then synthesize the best combined plan.
118
115
  ```
119
116
 
120
117
  ## Local Development
@@ -176,6 +173,7 @@ After trusted publishing is working, you can tighten npm package security by dis
176
173
 
177
174
  - Claude slash commands and skills come primarily from filesystem discovery; SDK probing is available but optional.
178
175
  - Session state is local to the repo under `.claude-manager/` and is ignored by git.
176
+ - The strongest team continuity comes when engineers are spawned from the active `cto` session; the plugin maps named engineers back to that active team automatically.
179
177
  - Context tracking is heuristic-based; actual SDK context usage may differ slightly.
180
178
 
181
179
  ## Scripts
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Plugin } from '@opencode-ai/plugin';
2
2
  import { ClaudeManagerPlugin } from './plugin/claude-manager.plugin.js';
3
- export type { ClaudeCapabilitySnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerPromptRegistry, RunClaudeSessionInput, SessionContextSnapshot, GitDiffResult, GitOperationResult, PersistentRunRecord, PersistentRunResult, ContextWarningLevel, SessionMode, LiveTailEvent, ToolOutputPreview, ToolApprovalRule, ToolApprovalPolicy, ToolApprovalDecision, } from './types/contracts.js';
3
+ export type { ClaudeCapabilitySnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerPromptRegistry, RunClaudeSessionInput, SessionContextSnapshot, GitDiffResult, GitOperationResult, PersistentRunRecord, PersistentRunResult, ContextWarningLevel, SessionMode, EngineerName, EngineerWorkMode, WrapperHistoryEntry, TeamEngineerRecord, TeamRecord, EngineerTaskResult, PlanDraft, SynthesizedPlanResult, LiveTailEvent, ToolOutputPreview, ToolApprovalRule, ToolApprovalPolicy, ToolApprovalDecision, } from './types/contracts.js';
4
4
  export { SessionLiveTailer } from './claude/session-live-tailer.js';
5
5
  export { ClaudeManagerPlugin };
6
6
  export declare const plugin: Plugin;
@@ -0,0 +1,50 @@
1
+ import type { ClaudeSessionEventHandler } from '../claude/claude-agent-sdk-adapter.js';
2
+ import type { ClaudeSessionService } from '../claude/claude-session.service.js';
3
+ import type { TeamStateStore } from '../state/team-state-store.js';
4
+ import type { TranscriptStore } from '../state/transcript-store.js';
5
+ import type { DiscoveredClaudeFile, EngineerName, EngineerTaskResult, EngineerWorkMode, SynthesizedPlanResult, TeamRecord } from '../types/contracts.js';
6
+ interface DispatchEngineerInput {
7
+ teamId: string;
8
+ cwd: string;
9
+ engineer: EngineerName;
10
+ mode: EngineerWorkMode;
11
+ message: string;
12
+ model?: string;
13
+ abortSignal?: AbortSignal;
14
+ onEvent?: ClaudeSessionEventHandler;
15
+ }
16
+ export declare class TeamOrchestrator {
17
+ private readonly sessions;
18
+ private readonly teamStore;
19
+ private readonly transcriptStore;
20
+ private readonly engineerSessionPrompt;
21
+ private readonly projectClaudeFiles;
22
+ constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, projectClaudeFiles: DiscoveredClaudeFile[]);
23
+ getOrCreateTeam(cwd: string, teamId: string): Promise<TeamRecord>;
24
+ listTeams(cwd: string): Promise<TeamRecord[]>;
25
+ recordWrapperSession(cwd: string, teamId: string, engineer: EngineerName, wrapperSessionId: string): Promise<void>;
26
+ recordWrapperExchange(cwd: string, teamId: string, engineer: EngineerName, wrapperSessionId: string, mode: EngineerWorkMode, assignment: string, result: string): Promise<void>;
27
+ getWrapperSystemContext(cwd: string, teamId: string, engineer: EngineerName): Promise<string | null>;
28
+ findTeamByWrapperSession(cwd: string, wrapperSessionId: string): Promise<{
29
+ teamId: string;
30
+ engineer: EngineerName;
31
+ } | null>;
32
+ dispatchEngineer(input: DispatchEngineerInput): Promise<EngineerTaskResult>;
33
+ planWithTeam(input: {
34
+ teamId: string;
35
+ cwd: string;
36
+ request: string;
37
+ leadEngineer: EngineerName;
38
+ challengerEngineer: EngineerName;
39
+ model?: string;
40
+ abortSignal?: AbortSignal;
41
+ }): Promise<SynthesizedPlanResult>;
42
+ private updateEngineer;
43
+ private reserveEngineer;
44
+ private getEngineerState;
45
+ private normalizeTeamRecord;
46
+ private buildSessionSystemPrompt;
47
+ private buildEngineerPrompt;
48
+ private mapWorkModeToSessionMode;
49
+ }
50
+ export {};
@@ -0,0 +1,360 @@
1
+ import { createEmptyEngineerRecord, createEmptyTeamRecord } from '../team/roster.js';
2
+ import { ContextTracker } from './context-tracker.js';
3
+ export class TeamOrchestrator {
4
+ sessions;
5
+ teamStore;
6
+ transcriptStore;
7
+ engineerSessionPrompt;
8
+ projectClaudeFiles;
9
+ constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, projectClaudeFiles) {
10
+ this.sessions = sessions;
11
+ this.teamStore = teamStore;
12
+ this.transcriptStore = transcriptStore;
13
+ this.engineerSessionPrompt = engineerSessionPrompt;
14
+ this.projectClaudeFiles = projectClaudeFiles;
15
+ }
16
+ async getOrCreateTeam(cwd, teamId) {
17
+ const existing = await this.teamStore.getTeam(cwd, teamId);
18
+ if (existing) {
19
+ return this.normalizeTeamRecord(existing);
20
+ }
21
+ const created = createEmptyTeamRecord(teamId, cwd);
22
+ await this.teamStore.saveTeam(created);
23
+ return created;
24
+ }
25
+ async listTeams(cwd) {
26
+ const teams = await this.teamStore.listTeams(cwd);
27
+ return teams.map((team) => this.normalizeTeamRecord(team));
28
+ }
29
+ async recordWrapperSession(cwd, teamId, engineer, wrapperSessionId) {
30
+ await this.updateEngineer(cwd, teamId, engineer, (entry) => ({
31
+ ...entry,
32
+ wrapperSessionId,
33
+ }));
34
+ }
35
+ async recordWrapperExchange(cwd, teamId, engineer, wrapperSessionId, mode, assignment, result) {
36
+ const timestamp = new Date().toISOString();
37
+ await this.updateEngineer(cwd, teamId, engineer, (entry) => ({
38
+ ...entry,
39
+ wrapperSessionId,
40
+ wrapperHistory: appendWrapperHistoryEntries(entry.wrapperHistory, [
41
+ { timestamp, type: 'assignment', mode, text: summarizeText(assignment, 320) },
42
+ { timestamp, type: 'result', mode, text: summarizeText(result, 320) },
43
+ ]),
44
+ lastMode: mode,
45
+ lastTaskSummary: summarizeMessage(assignment),
46
+ lastUsedAt: timestamp,
47
+ }));
48
+ }
49
+ async getWrapperSystemContext(cwd, teamId, engineer) {
50
+ const team = await this.getOrCreateTeam(cwd, teamId);
51
+ const state = this.getEngineerState(team, engineer);
52
+ if (state.wrapperHistory.length === 0) {
53
+ return null;
54
+ }
55
+ const historyLines = state.wrapperHistory.map((entry) => {
56
+ const modeLabel = entry.mode ? ` [${entry.mode}]` : '';
57
+ return `- ${entry.type}${modeLabel}: ${entry.text}`;
58
+ });
59
+ return [
60
+ `Persistent wrapper memory for ${engineer} in CTO team ${teamId}:`,
61
+ 'Use this only to improve delegation quality and continuity.',
62
+ 'Prefer the current assignment when it conflicts with older context.',
63
+ ...historyLines,
64
+ ].join('\n');
65
+ }
66
+ async findTeamByWrapperSession(cwd, wrapperSessionId) {
67
+ const teams = await this.listTeams(cwd);
68
+ for (const team of teams) {
69
+ for (const engineer of team.engineers) {
70
+ if (engineer.wrapperSessionId === wrapperSessionId) {
71
+ return {
72
+ teamId: team.id,
73
+ engineer: engineer.name,
74
+ };
75
+ }
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+ async dispatchEngineer(input) {
81
+ const team = await this.getOrCreateTeam(input.cwd, input.teamId);
82
+ const engineerState = this.getEngineerState(team, input.engineer);
83
+ await this.reserveEngineer(input.cwd, input.teamId, input.engineer);
84
+ try {
85
+ const tracker = new ContextTracker();
86
+ if (engineerState.context.sessionId) {
87
+ tracker.restore({
88
+ sessionId: engineerState.context.sessionId,
89
+ totalTurns: engineerState.context.totalTurns,
90
+ totalCostUsd: engineerState.context.totalCostUsd,
91
+ estimatedContextPercent: engineerState.context.estimatedContextPercent,
92
+ contextWindowSize: engineerState.context.contextWindowSize,
93
+ latestInputTokens: engineerState.context.latestInputTokens,
94
+ });
95
+ }
96
+ const result = await this.sessions.runTask({
97
+ cwd: input.cwd,
98
+ prompt: this.buildEngineerPrompt(input.mode, input.message),
99
+ systemPrompt: engineerState.claudeSessionId
100
+ ? undefined
101
+ : this.buildSessionSystemPrompt(input.engineer, input.mode),
102
+ resumeSessionId: engineerState.claudeSessionId ?? undefined,
103
+ persistSession: true,
104
+ includePartialMessages: true,
105
+ permissionMode: this.mapWorkModeToSessionMode(input.mode) === 'plan' ? 'plan' : 'acceptEdits',
106
+ model: input.model,
107
+ effort: input.mode === 'implement' ? 'high' : 'medium',
108
+ settingSources: ['user'],
109
+ abortSignal: input.abortSignal,
110
+ }, input.onEvent);
111
+ tracker.recordResult({
112
+ sessionId: result.sessionId,
113
+ turns: result.turns,
114
+ totalCostUsd: result.totalCostUsd,
115
+ inputTokens: result.inputTokens,
116
+ outputTokens: result.outputTokens,
117
+ contextWindowSize: result.contextWindowSize,
118
+ });
119
+ if (result.sessionId && result.events.length > 0) {
120
+ await this.transcriptStore.appendEvents(input.cwd, result.sessionId, result.events);
121
+ }
122
+ const context = tracker.snapshot();
123
+ await this.updateEngineer(input.cwd, input.teamId, input.engineer, (entry) => ({
124
+ ...entry,
125
+ claudeSessionId: result.sessionId ?? engineerState.claudeSessionId,
126
+ busy: false,
127
+ lastMode: input.mode,
128
+ lastTaskSummary: summarizeMessage(input.message),
129
+ lastUsedAt: new Date().toISOString(),
130
+ context,
131
+ }));
132
+ return {
133
+ teamId: input.teamId,
134
+ engineer: input.engineer,
135
+ mode: input.mode,
136
+ sessionId: result.sessionId,
137
+ finalText: result.finalText,
138
+ turns: result.turns,
139
+ totalCostUsd: result.totalCostUsd,
140
+ inputTokens: result.inputTokens,
141
+ outputTokens: result.outputTokens,
142
+ contextWindowSize: result.contextWindowSize,
143
+ context,
144
+ };
145
+ }
146
+ catch (error) {
147
+ await this.updateEngineer(input.cwd, input.teamId, input.engineer, (engineer) => ({
148
+ ...engineer,
149
+ busy: false,
150
+ }));
151
+ throw error;
152
+ }
153
+ }
154
+ async planWithTeam(input) {
155
+ if (input.leadEngineer === input.challengerEngineer) {
156
+ throw new Error('Choose two different engineers for plan synthesis.');
157
+ }
158
+ const [leadDraft, challengerDraft] = await Promise.all([
159
+ this.dispatchEngineer({
160
+ teamId: input.teamId,
161
+ cwd: input.cwd,
162
+ engineer: input.leadEngineer,
163
+ mode: 'explore',
164
+ message: buildPlanDraftRequest('lead', input.request),
165
+ model: input.model,
166
+ abortSignal: input.abortSignal,
167
+ }),
168
+ this.dispatchEngineer({
169
+ teamId: input.teamId,
170
+ cwd: input.cwd,
171
+ engineer: input.challengerEngineer,
172
+ mode: 'explore',
173
+ message: buildPlanDraftRequest('challenger', input.request),
174
+ model: input.model,
175
+ abortSignal: input.abortSignal,
176
+ }),
177
+ ]);
178
+ const drafts = [
179
+ { ...leadDraft, request: input.request },
180
+ { ...challengerDraft, request: input.request },
181
+ ];
182
+ const synthesisResult = await this.sessions.runTask({
183
+ cwd: input.cwd,
184
+ prompt: buildSynthesisPrompt(input.request, drafts),
185
+ systemPrompt: buildSynthesisSystemPrompt(),
186
+ persistSession: false,
187
+ includePartialMessages: false,
188
+ permissionMode: 'plan',
189
+ model: input.model,
190
+ effort: 'high',
191
+ settingSources: ['user'],
192
+ abortSignal: input.abortSignal,
193
+ });
194
+ const parsedSynthesis = parseSynthesisResult(synthesisResult.finalText);
195
+ return {
196
+ teamId: input.teamId,
197
+ request: input.request,
198
+ leadEngineer: input.leadEngineer,
199
+ challengerEngineer: input.challengerEngineer,
200
+ drafts,
201
+ synthesis: parsedSynthesis.synthesis,
202
+ recommendedQuestion: parsedSynthesis.recommendedQuestion,
203
+ recommendedAnswer: parsedSynthesis.recommendedAnswer,
204
+ };
205
+ }
206
+ async updateEngineer(cwd, teamId, engineerName, update) {
207
+ await this.getOrCreateTeam(cwd, teamId);
208
+ await this.teamStore.updateTeam(cwd, teamId, (team) => {
209
+ const normalized = this.normalizeTeamRecord(team);
210
+ const existing = this.getEngineerState(normalized, engineerName);
211
+ return {
212
+ ...normalized,
213
+ updatedAt: new Date().toISOString(),
214
+ engineers: normalized.engineers.map((engineer) => engineer.name === engineerName ? update(existing) : engineer),
215
+ };
216
+ });
217
+ }
218
+ async reserveEngineer(cwd, teamId, engineerName) {
219
+ await this.getOrCreateTeam(cwd, teamId);
220
+ await this.teamStore.updateTeam(cwd, teamId, (team) => {
221
+ const normalized = this.normalizeTeamRecord(team);
222
+ const engineer = this.getEngineerState(normalized, engineerName);
223
+ if (engineer.busy) {
224
+ throw new Error(`${engineerName} is already working on another assignment.`);
225
+ }
226
+ return {
227
+ ...normalized,
228
+ updatedAt: new Date().toISOString(),
229
+ engineers: normalized.engineers.map((entry) => entry.name === engineerName
230
+ ? {
231
+ ...entry,
232
+ busy: true,
233
+ }
234
+ : entry),
235
+ };
236
+ });
237
+ }
238
+ getEngineerState(team, engineerName) {
239
+ return (team.engineers.find((engineer) => engineer.name === engineerName) ??
240
+ createEmptyEngineerRecord(engineerName));
241
+ }
242
+ normalizeTeamRecord(team) {
243
+ const engineerMap = new Map(team.engineers.map((engineer) => [engineer.name, engineer]));
244
+ return {
245
+ ...team,
246
+ engineers: createEmptyTeamRecord(team.id, team.cwd).engineers.map((engineer) => engineerMap.get(engineer.name) ?? engineer),
247
+ };
248
+ }
249
+ buildSessionSystemPrompt(engineer, mode) {
250
+ const claudeFileSection = this.projectClaudeFiles.length
251
+ ? `\n\nProject Claude Files:\n${this.projectClaudeFiles
252
+ .map((file) => `## ${file.relativePath}\n${file.content}`)
253
+ .join('\n\n')}`
254
+ : '';
255
+ return [
256
+ this.engineerSessionPrompt,
257
+ '',
258
+ `Assigned engineer: ${engineer}.`,
259
+ `Current work mode: ${mode}.`,
260
+ claudeFileSection,
261
+ ]
262
+ .join('\n')
263
+ .trim();
264
+ }
265
+ buildEngineerPrompt(mode, message) {
266
+ return `${buildModeInstruction(mode)}\n\n${message}`;
267
+ }
268
+ mapWorkModeToSessionMode(mode) {
269
+ return mode === 'explore' ? 'plan' : 'free';
270
+ }
271
+ }
272
+ function buildModeInstruction(mode) {
273
+ switch (mode) {
274
+ case 'explore':
275
+ return 'Work in planning mode. Investigate, reason, and write the plan inline. Do not make file edits.';
276
+ case 'implement':
277
+ return 'Work in implementation mode. Make the changes, verify them, and report clearly.';
278
+ case 'verify':
279
+ return 'Work in verification mode. Run the narrowest useful checks first, then broaden only if needed.';
280
+ }
281
+ }
282
+ function summarizeMessage(message) {
283
+ const compact = message.replace(/\s+/g, ' ').trim();
284
+ return compact.length > 120 ? `${compact.slice(0, 117)}...` : compact;
285
+ }
286
+ function summarizeText(text, limit) {
287
+ const compact = text.replace(/\s+/g, ' ').trim();
288
+ return compact.length > limit ? `${compact.slice(0, limit - 3)}...` : compact;
289
+ }
290
+ function appendWrapperHistoryEntries(existing, nextEntries) {
291
+ return [...existing, ...nextEntries].slice(-12);
292
+ }
293
+ function buildPlanDraftRequest(perspective, request) {
294
+ const posture = perspective === 'lead'
295
+ ? 'Propose the most direct workable plan.'
296
+ : 'Challenge weak assumptions, find missing decisions, and propose a stronger alternative if needed.';
297
+ return [
298
+ posture,
299
+ '',
300
+ 'Return exactly these sections:',
301
+ '1. Objective',
302
+ '2. Proposed approach',
303
+ '3. Files or systems likely involved',
304
+ '4. Risks and open questions',
305
+ '5. Verification',
306
+ '6. Step-by-step plan',
307
+ '',
308
+ `User request: ${request}`,
309
+ ].join('\n');
310
+ }
311
+ function buildSynthesisSystemPrompt() {
312
+ return [
313
+ 'You are the CTO synthesis engine.',
314
+ 'Combine two independent engineering plans into one better plan.',
315
+ 'Prefer the clearest, simplest, highest-leverage path.',
316
+ 'If one user decision is still required, surface exactly one recommended question and one recommended answer.',
317
+ 'Use this output format exactly:',
318
+ '## Synthesis',
319
+ '<combined plan>',
320
+ '## Recommended Question',
321
+ '<question or NONE>',
322
+ '## Recommended Answer',
323
+ '<answer or NONE>',
324
+ ].join('\n');
325
+ }
326
+ function buildSynthesisPrompt(request, drafts) {
327
+ return [
328
+ `User request: ${request}`,
329
+ '',
330
+ `Lead engineer (${drafts[0].engineer}) draft:`,
331
+ drafts[0].finalText,
332
+ '',
333
+ `Challenger engineer (${drafts[1].engineer}) draft:`,
334
+ drafts[1].finalText,
335
+ ].join('\n');
336
+ }
337
+ function parseSynthesisResult(text) {
338
+ const synthesis = extractSection(text, 'Synthesis') ?? text.trim();
339
+ const recommendedQuestion = normalizeOptionalSection(extractSection(text, 'Recommended Question'));
340
+ const recommendedAnswer = normalizeOptionalSection(extractSection(text, 'Recommended Answer'));
341
+ return {
342
+ synthesis,
343
+ recommendedQuestion,
344
+ recommendedAnswer,
345
+ };
346
+ }
347
+ function extractSection(text, heading) {
348
+ const regex = new RegExp(`## ${escapeRegExp(heading)}\\n([\\s\\S]*?)(?=\\n## |$)`);
349
+ const match = text.match(regex);
350
+ return match?.[1]?.trim() ?? null;
351
+ }
352
+ function normalizeOptionalSection(value) {
353
+ if (!value) {
354
+ return null;
355
+ }
356
+ return value.toUpperCase() === 'NONE' ? null : value;
357
+ }
358
+ function escapeRegExp(value) {
359
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
360
+ }
@@ -1,18 +1,14 @@
1
- /**
2
- * Agent hierarchy configuration for the CTO + Engineer Wrapper architecture.
3
- *
4
- * CTO (cto) — pure orchestrator, spawns engineers, reviews diffs, commits
5
- * Engineer Explore (engineer_explore) — manages a Claude Code session for read-only investigation
6
- * Engineer Implement (engineer_implement) — manages a Claude Code session for implementation
7
- * Claude Code session — the underlying AI session (prompt only, no OpenCode agent)
8
- */
9
- import type { ManagerPromptRegistry } from '../types/contracts.js';
1
+ import type { EngineerName, ManagerPromptRegistry } from '../types/contracts.js';
10
2
  export declare const AGENT_CTO = "cto";
11
- export declare const AGENT_ENGINEER_EXPLORE = "engineer_explore";
12
- export declare const AGENT_ENGINEER_IMPLEMENT = "engineer_implement";
13
- export declare const AGENT_ENGINEER_VERIFY = "engineer_verify";
14
- /** All restricted tool IDs (union of all domain groups) */
15
- export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["explore", "implement", "verify", "compact_context", "clear_session", "session_health", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update"];
3
+ export declare const ENGINEER_AGENT_IDS: {
4
+ readonly Tom: "tom";
5
+ readonly John: "john";
6
+ readonly Maya: "maya";
7
+ readonly Sara: "sara";
8
+ readonly Alex: "alex";
9
+ };
10
+ export declare const ENGINEER_AGENT_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
11
+ export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["team_status", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update", "claude"];
16
12
  type ToolPermission = 'allow' | 'ask' | 'deny';
17
13
  type AgentPermission = {
18
14
  '*'?: ToolPermission;
@@ -24,13 +20,9 @@ type AgentPermission = {
24
20
  webfetch?: ToolPermission;
25
21
  websearch?: ToolPermission;
26
22
  lsp?: ToolPermission;
27
- /** OpenCode built-in: manage session todo list */
28
23
  todowrite?: ToolPermission;
29
- /** OpenCode built-in: read session todo list */
30
24
  todoread?: ToolPermission;
31
- /** OpenCode built-in: ask the user structured questions with options */
32
25
  question?: ToolPermission;
33
- /** OpenCode built-in: launch subagents (matches subagent type, last-match-wins) */
34
26
  task?: ToolPermission | Record<string, ToolPermission>;
35
27
  bash?: ToolPermission | Record<string, ToolPermission>;
36
28
  [tool: string]: ToolPermission | Record<string, ToolPermission> | undefined;
@@ -42,27 +34,13 @@ export declare function buildCtoAgentConfig(prompts: ManagerPromptRegistry): {
42
34
  permission: AgentPermission;
43
35
  prompt: string;
44
36
  };
45
- export declare function buildEngineerExploreAgentConfig(prompts: ManagerPromptRegistry): {
46
- description: string;
47
- mode: "subagent";
48
- color: string;
49
- permission: AgentPermission;
50
- prompt: string;
51
- };
52
- export declare function buildEngineerImplementAgentConfig(prompts: ManagerPromptRegistry): {
53
- description: string;
54
- mode: "subagent";
55
- color: string;
56
- permission: AgentPermission;
57
- prompt: string;
58
- };
59
- export declare function buildEngineerVerifyAgentConfig(prompts: ManagerPromptRegistry): {
37
+ export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry, engineer: EngineerName): {
60
38
  description: string;
61
39
  mode: "subagent";
40
+ hidden: boolean;
62
41
  color: string;
63
42
  permission: AgentPermission;
64
43
  prompt: string;
65
44
  };
66
- /** Deny all restricted tools at the global level so only designated agents can use them. */
67
45
  export declare function denyRestrictedToolsGlobally(permissions: Record<string, ToolPermission>): void;
68
46
  export {};