@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.
- 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 +233 -421
- package/dist/plugin/service-factory.d.ts +20 -3
- package/dist/plugin/service-factory.js +46 -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 +257 -391
- package/dist/src/plugin/service-factory.d.ts +20 -3
- package/dist/src/plugin/service-factory.js +47 -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 +17 -0
- package/dist/src/state/team-state-store.js +107 -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 +17 -0
- package/dist/state/team-state-store.js +107 -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/cto-active-team.test.d.ts +1 -0
- package/dist/test/cto-active-team.test.js +52 -0
- package/dist/test/git-operations.test.js +65 -1
- package/dist/test/persistent-manager.test.js +3 -3
- package/dist/test/prompt-registry.test.js +32 -252
- package/dist/test/report-claude-event.test.d.ts +1 -0
- package/dist/test/report-claude-event.test.js +246 -0
- 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 +72 -0
- package/dist/types/contracts.d.ts +54 -3
- package/dist/types/contracts.js +1 -1
- 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
|
|
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
|
-
-
|
|
13
|
-
-
|
|
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
|
-
-
|
|
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
|
-
###
|
|
57
|
+
### CTO orchestration
|
|
55
58
|
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
- `
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
- `
|
|
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
|
|
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
|
|
88
|
+
The plugin registers a CTO + named engineer team through the OpenCode plugin `config` hook:
|
|
86
89
|
|
|
87
|
-
- **`cto`** (primary agent) —
|
|
88
|
-
- **`
|
|
89
|
-
- **
|
|
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.
|
|
98
|
-
2.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
111
|
+
For a larger feature where you want two independent plans first:
|
|
115
112
|
|
|
116
113
|
```text
|
|
117
|
-
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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 {};
|