@doingdev/opencode-claude-manager-plugin 0.1.0
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 +119 -0
- package/dist/claude/claude-agent-sdk-adapter.d.ts +24 -0
- package/dist/claude/claude-agent-sdk-adapter.js +256 -0
- package/dist/claude/claude-session.service.d.ts +15 -0
- package/dist/claude/claude-session.service.js +23 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/manager/manager-orchestrator.d.ts +18 -0
- package/dist/manager/manager-orchestrator.js +186 -0
- package/dist/manager/task-planner.d.ts +5 -0
- package/dist/manager/task-planner.js +42 -0
- package/dist/metadata/claude-metadata.service.d.ts +12 -0
- package/dist/metadata/claude-metadata.service.js +38 -0
- package/dist/metadata/repo-claude-config-reader.d.ts +7 -0
- package/dist/metadata/repo-claude-config-reader.js +154 -0
- package/dist/plugin/claude-manager.plugin.d.ts +2 -0
- package/dist/plugin/claude-manager.plugin.js +102 -0
- package/dist/plugin/service-factory.d.ts +7 -0
- package/dist/plugin/service-factory.js +25 -0
- package/dist/prompts/registry.d.ts +2 -0
- package/dist/prompts/registry.js +11 -0
- package/dist/state/file-run-state-store.d.ts +14 -0
- package/dist/state/file-run-state-store.js +97 -0
- package/dist/types/contracts.d.ts +145 -0
- package/dist/types/contracts.js +1 -0
- package/dist/worktree/worktree-coordinator.d.ts +21 -0
- package/dist/worktree/worktree-coordinator.js +64 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# OpenCode Claude Manager Plugin
|
|
2
|
+
|
|
3
|
+
This package provides an OpenCode plugin that lets an OpenCode-side manager agent orchestrate Claude Code sessions through a stable local bridge.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
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 discovering Claude metadata, delegating work to Claude sessions, splitting tasks into subagents, and coordinating optional git worktrees.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Runs Claude Code tasks from OpenCode through `@anthropic-ai/claude-agent-sdk`.
|
|
12
|
+
- Discovers repo-local Claude metadata from `.claude/skills`, `.claude/commands`, `CLAUDE.md`, and settings hooks.
|
|
13
|
+
- Splits multi-step tasks into subagents, optionally assigning dedicated git worktrees.
|
|
14
|
+
- Persists manager runs under `.claude-manager/runs` so sessions can be inspected later.
|
|
15
|
+
- Exposes manager-facing tools instead of relying on undocumented plugin-defined slash commands.
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Node `22+`
|
|
20
|
+
- OpenCode with plugin loading enabled
|
|
21
|
+
- Access to Claude Code / Claude Agent SDK on the machine where OpenCode is running
|
|
22
|
+
- A git repository if you want automatic worktree allocation
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Install from npm:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @doingdev/opencode-claude-manager-plugin
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or for local development in this repo:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install
|
|
36
|
+
npm run build
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## OpenCode Config
|
|
40
|
+
|
|
41
|
+
Add the plugin to your OpenCode config:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"plugin": ["@doingdev/opencode-claude-manager-plugin"]
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If you are testing locally, point OpenCode at the local package or plugin file using your normal local plugin workflow.
|
|
50
|
+
|
|
51
|
+
## OpenCode tools
|
|
52
|
+
|
|
53
|
+
- `claude_manager_run` - run a task through Claude with optional splitting and worktrees
|
|
54
|
+
- `claude_manager_metadata` - inspect available Claude commands, skills, hooks, and settings
|
|
55
|
+
- `claude_manager_sessions` - list Claude sessions or inspect a saved transcript
|
|
56
|
+
- `claude_manager_runs` - inspect persisted manager run records
|
|
57
|
+
- `claude_manager_cleanup_run` - explicitly remove worktrees created for a prior manager run
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
Typical flow inside OpenCode:
|
|
62
|
+
|
|
63
|
+
1. Inspect Claude capabilities with `claude_manager_metadata`.
|
|
64
|
+
2. Delegate work with `claude_manager_run`.
|
|
65
|
+
3. Inspect saved Claude history with `claude_manager_sessions` or prior orchestration records with `claude_manager_runs`.
|
|
66
|
+
|
|
67
|
+
Example task:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
Use claude_manager_run to split this implementation into subagents, use worktrees, and summarize the final result.
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Local Development
|
|
74
|
+
|
|
75
|
+
Clone the repo and run:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install
|
|
79
|
+
npm run lint
|
|
80
|
+
npm run typecheck
|
|
81
|
+
npm run test
|
|
82
|
+
npm run build
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The compiled plugin output is written to `dist/`.
|
|
86
|
+
|
|
87
|
+
## Publishing
|
|
88
|
+
|
|
89
|
+
This package is configured for the npm scope `@doingdev`.
|
|
90
|
+
|
|
91
|
+
Release flow:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm login
|
|
95
|
+
npm whoami
|
|
96
|
+
npm version patch
|
|
97
|
+
npm run lint
|
|
98
|
+
npm run typecheck
|
|
99
|
+
npm run test
|
|
100
|
+
npm run build
|
|
101
|
+
npm publish --access public
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
You can also publish through the GitHub Actions workflow after creating a GitHub release and configuring the `NPM_TOKEN` secret.
|
|
105
|
+
|
|
106
|
+
## Limitations
|
|
107
|
+
|
|
108
|
+
- Claude slash commands and skills come primarily from filesystem discovery; SDK probing is available but optional.
|
|
109
|
+
- Worktree creation only activates when the current directory is a git repo and multiple subtasks are planned.
|
|
110
|
+
- Worktrees are preserved by default so you can inspect changes; clean them up explicitly with `claude_manager_cleanup_run`.
|
|
111
|
+
- Run state is local to the repo under `.claude-manager/` and is ignored by git.
|
|
112
|
+
|
|
113
|
+
## Scripts
|
|
114
|
+
|
|
115
|
+
- `npm run build`
|
|
116
|
+
- `npm run typecheck`
|
|
117
|
+
- `npm run lint`
|
|
118
|
+
- `npm run format`
|
|
119
|
+
- `npm run test`
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Options, type Query, type SDKSessionInfo, type SessionMessage, type SettingSource } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import type { ClaudeCapabilitySnapshot, ClaudeSessionEvent, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, RunClaudeSessionInput } from '../types/contracts.js';
|
|
3
|
+
export type ClaudeSessionEventHandler = (event: ClaudeSessionEvent) => void | Promise<void>;
|
|
4
|
+
export interface ClaudeAgentSdkFacade {
|
|
5
|
+
query(params: {
|
|
6
|
+
prompt: string;
|
|
7
|
+
options?: Options;
|
|
8
|
+
}): Query;
|
|
9
|
+
listSessions(options?: {
|
|
10
|
+
dir?: string;
|
|
11
|
+
}): Promise<SDKSessionInfo[]>;
|
|
12
|
+
getSessionMessages(sessionId: string, options?: {
|
|
13
|
+
dir?: string;
|
|
14
|
+
}): Promise<SessionMessage[]>;
|
|
15
|
+
}
|
|
16
|
+
export declare class ClaudeAgentSdkAdapter {
|
|
17
|
+
private readonly sdkFacade;
|
|
18
|
+
constructor(sdkFacade?: ClaudeAgentSdkFacade);
|
|
19
|
+
runSession(input: RunClaudeSessionInput, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
20
|
+
listSavedSessions(cwd?: string): Promise<ClaudeSessionSummary[]>;
|
|
21
|
+
getTranscript(sessionId: string, cwd?: string): Promise<ClaudeSessionTranscriptMessage[]>;
|
|
22
|
+
probeCapabilities(cwd: string, settingSources?: SettingSource[]): Promise<ClaudeCapabilitySnapshot>;
|
|
23
|
+
private buildOptions;
|
|
24
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { getSessionMessages, listSessions, query, } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
const defaultFacade = {
|
|
3
|
+
query,
|
|
4
|
+
listSessions,
|
|
5
|
+
getSessionMessages,
|
|
6
|
+
};
|
|
7
|
+
export class ClaudeAgentSdkAdapter {
|
|
8
|
+
sdkFacade;
|
|
9
|
+
constructor(sdkFacade = defaultFacade) {
|
|
10
|
+
this.sdkFacade = sdkFacade;
|
|
11
|
+
}
|
|
12
|
+
async runSession(input, onEvent) {
|
|
13
|
+
const sessionQuery = this.sdkFacade.query({
|
|
14
|
+
prompt: input.prompt,
|
|
15
|
+
options: this.buildOptions(input),
|
|
16
|
+
});
|
|
17
|
+
const events = [];
|
|
18
|
+
let finalText = '';
|
|
19
|
+
let sessionId;
|
|
20
|
+
let turns;
|
|
21
|
+
let totalCostUsd;
|
|
22
|
+
try {
|
|
23
|
+
for await (const message of sessionQuery) {
|
|
24
|
+
const event = normalizeSdkMessage(message);
|
|
25
|
+
if (!event) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
sessionId ??= event.sessionId;
|
|
29
|
+
if (event.type === 'result') {
|
|
30
|
+
finalText = event.text;
|
|
31
|
+
turns = event.turns;
|
|
32
|
+
totalCostUsd = event.totalCostUsd;
|
|
33
|
+
}
|
|
34
|
+
events.push(event);
|
|
35
|
+
if (onEvent) {
|
|
36
|
+
await onEvent(event);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
sessionQuery.close();
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
sessionId,
|
|
45
|
+
events,
|
|
46
|
+
finalText,
|
|
47
|
+
turns,
|
|
48
|
+
totalCostUsd,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async listSavedSessions(cwd) {
|
|
52
|
+
const sessions = await this.sdkFacade.listSessions(cwd ? { dir: cwd } : undefined);
|
|
53
|
+
return sessions.map((session) => ({
|
|
54
|
+
sessionId: session.sessionId,
|
|
55
|
+
summary: session.summary,
|
|
56
|
+
cwd: session.cwd,
|
|
57
|
+
gitBranch: session.gitBranch,
|
|
58
|
+
createdAt: session.createdAt,
|
|
59
|
+
lastModified: session.lastModified,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
async getTranscript(sessionId, cwd) {
|
|
63
|
+
const messages = await this.sdkFacade.getSessionMessages(sessionId, cwd ? { dir: cwd } : undefined);
|
|
64
|
+
return messages.map((message) => ({
|
|
65
|
+
role: message.type,
|
|
66
|
+
sessionId: message.session_id,
|
|
67
|
+
messageId: message.uuid,
|
|
68
|
+
text: extractText(message.message),
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
async probeCapabilities(cwd, settingSources = ['project']) {
|
|
72
|
+
const sessionQuery = this.sdkFacade.query({
|
|
73
|
+
prompt: 'Reply with OK.',
|
|
74
|
+
options: {
|
|
75
|
+
cwd,
|
|
76
|
+
maxTurns: 1,
|
|
77
|
+
permissionMode: 'plan',
|
|
78
|
+
persistSession: false,
|
|
79
|
+
tools: [],
|
|
80
|
+
settingSources,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
const [commands, agents, models] = await Promise.all([
|
|
85
|
+
sessionQuery.supportedCommands(),
|
|
86
|
+
sessionQuery.supportedAgents(),
|
|
87
|
+
sessionQuery.supportedModels(),
|
|
88
|
+
]);
|
|
89
|
+
return {
|
|
90
|
+
commands: commands.map(mapSlashCommand),
|
|
91
|
+
agents: agents.map(mapAgent),
|
|
92
|
+
models: models.map((model) => model.value),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
sessionQuery.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
buildOptions(input) {
|
|
100
|
+
const options = {
|
|
101
|
+
cwd: input.cwd,
|
|
102
|
+
tools: { type: 'preset', preset: 'claude_code' },
|
|
103
|
+
allowedTools: input.allowedTools,
|
|
104
|
+
disallowedTools: input.disallowedTools,
|
|
105
|
+
continue: input.continueSession,
|
|
106
|
+
resume: input.resumeSessionId,
|
|
107
|
+
forkSession: input.forkSession,
|
|
108
|
+
persistSession: input.persistSession,
|
|
109
|
+
includePartialMessages: input.includePartialMessages,
|
|
110
|
+
settingSources: input.settingSources,
|
|
111
|
+
maxTurns: input.maxTurns,
|
|
112
|
+
model: input.model,
|
|
113
|
+
permissionMode: input.permissionMode,
|
|
114
|
+
systemPrompt: input.systemPrompt
|
|
115
|
+
? { type: 'preset', preset: 'claude_code', append: input.systemPrompt }
|
|
116
|
+
: { type: 'preset', preset: 'claude_code' },
|
|
117
|
+
env: {
|
|
118
|
+
...process.env,
|
|
119
|
+
CLAUDE_AGENT_SDK_CLIENT_APP: 'opencode-claude-manager-plugin/0.1.0',
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
if (!input.resumeSessionId) {
|
|
123
|
+
delete options.resume;
|
|
124
|
+
}
|
|
125
|
+
return options;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function normalizeSdkMessage(message) {
|
|
129
|
+
const sessionId = 'session_id' in message ? message.session_id : undefined;
|
|
130
|
+
if (message.type === 'assistant') {
|
|
131
|
+
return {
|
|
132
|
+
type: 'assistant',
|
|
133
|
+
sessionId,
|
|
134
|
+
text: extractText(message.message),
|
|
135
|
+
rawType: message.type,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (message.type === 'stream_event') {
|
|
139
|
+
return {
|
|
140
|
+
type: 'partial',
|
|
141
|
+
sessionId,
|
|
142
|
+
text: extractPartialEventText(message.event),
|
|
143
|
+
rawType: message.type,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (message.type === 'result') {
|
|
147
|
+
return {
|
|
148
|
+
type: message.is_error ? 'error' : 'result',
|
|
149
|
+
sessionId,
|
|
150
|
+
text: message.subtype === 'success'
|
|
151
|
+
? message.result
|
|
152
|
+
: message.errors.join('\n') || message.subtype,
|
|
153
|
+
turns: message.num_turns,
|
|
154
|
+
totalCostUsd: message.total_cost_usd,
|
|
155
|
+
rawType: `${message.type}:${message.subtype}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (message.type === 'system') {
|
|
159
|
+
return {
|
|
160
|
+
type: message.subtype === 'init' ? 'init' : 'system',
|
|
161
|
+
sessionId,
|
|
162
|
+
text: message.subtype,
|
|
163
|
+
rawType: `${message.type}:${message.subtype}`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (message.type === 'auth_status') {
|
|
167
|
+
return {
|
|
168
|
+
type: 'system',
|
|
169
|
+
sessionId,
|
|
170
|
+
text: message.output.join('\n') || 'auth_status',
|
|
171
|
+
rawType: message.type,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (message.type === 'prompt_suggestion') {
|
|
175
|
+
return {
|
|
176
|
+
type: 'system',
|
|
177
|
+
sessionId,
|
|
178
|
+
text: message.suggestion,
|
|
179
|
+
rawType: message.type,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (message.type === 'user') {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
type: 'system',
|
|
187
|
+
sessionId,
|
|
188
|
+
text: message.type,
|
|
189
|
+
rawType: message.type,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function extractPartialEventText(event) {
|
|
193
|
+
if (!event || typeof event !== 'object') {
|
|
194
|
+
return 'stream_event';
|
|
195
|
+
}
|
|
196
|
+
const eventRecord = event;
|
|
197
|
+
const eventType = typeof eventRecord.type === 'string' ? eventRecord.type : 'stream_event';
|
|
198
|
+
const delta = eventRecord.delta;
|
|
199
|
+
if (delta && typeof delta === 'object') {
|
|
200
|
+
const deltaRecord = delta;
|
|
201
|
+
if (typeof deltaRecord.text === 'string') {
|
|
202
|
+
return deltaRecord.text;
|
|
203
|
+
}
|
|
204
|
+
if (typeof deltaRecord.partial_json === 'string') {
|
|
205
|
+
return deltaRecord.partial_json;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return eventType;
|
|
209
|
+
}
|
|
210
|
+
function extractText(payload) {
|
|
211
|
+
if (typeof payload === 'string') {
|
|
212
|
+
return payload;
|
|
213
|
+
}
|
|
214
|
+
if (!payload || typeof payload !== 'object') {
|
|
215
|
+
return '';
|
|
216
|
+
}
|
|
217
|
+
const payloadRecord = payload;
|
|
218
|
+
if (Array.isArray(payloadRecord.content)) {
|
|
219
|
+
const parts = payloadRecord.content
|
|
220
|
+
.map((contentPart) => {
|
|
221
|
+
if (!contentPart || typeof contentPart !== 'object') {
|
|
222
|
+
return '';
|
|
223
|
+
}
|
|
224
|
+
const partRecord = contentPart;
|
|
225
|
+
if (typeof partRecord.text === 'string') {
|
|
226
|
+
return partRecord.text;
|
|
227
|
+
}
|
|
228
|
+
if (partRecord.type === 'tool_use' &&
|
|
229
|
+
typeof partRecord.name === 'string') {
|
|
230
|
+
return `[tool:${partRecord.name}]`;
|
|
231
|
+
}
|
|
232
|
+
return '';
|
|
233
|
+
})
|
|
234
|
+
.filter(Boolean);
|
|
235
|
+
if (parts.length > 0) {
|
|
236
|
+
return parts.join('\n');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return JSON.stringify(payload);
|
|
240
|
+
}
|
|
241
|
+
function mapSlashCommand(command) {
|
|
242
|
+
return {
|
|
243
|
+
name: command.name,
|
|
244
|
+
description: command.description,
|
|
245
|
+
argumentHint: command.argumentHint,
|
|
246
|
+
source: 'sdk',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function mapAgent(agent) {
|
|
250
|
+
return {
|
|
251
|
+
name: agent.name,
|
|
252
|
+
description: agent.description,
|
|
253
|
+
model: agent.model,
|
|
254
|
+
source: 'sdk',
|
|
255
|
+
};
|
|
256
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ClaudeCapabilitySnapshot, ClaudeMetadataSnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, RunClaudeSessionInput } from '../types/contracts.js';
|
|
2
|
+
import type { ClaudeMetadataService } from '../metadata/claude-metadata.service.js';
|
|
3
|
+
import type { ClaudeAgentSdkAdapter, ClaudeSessionEventHandler } from './claude-agent-sdk-adapter.js';
|
|
4
|
+
export declare class ClaudeSessionService {
|
|
5
|
+
private readonly sdkAdapter;
|
|
6
|
+
private readonly metadataService;
|
|
7
|
+
constructor(sdkAdapter: ClaudeAgentSdkAdapter, metadataService: ClaudeMetadataService);
|
|
8
|
+
runTask(input: RunClaudeSessionInput, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
9
|
+
listSessions(cwd?: string): Promise<ClaudeSessionSummary[]>;
|
|
10
|
+
getTranscript(sessionId: string, cwd?: string): Promise<ClaudeSessionTranscriptMessage[]>;
|
|
11
|
+
inspectRepository(cwd: string, options?: {
|
|
12
|
+
includeSdkProbe?: boolean;
|
|
13
|
+
}): Promise<ClaudeMetadataSnapshot>;
|
|
14
|
+
probeCapabilities(cwd: string): Promise<ClaudeCapabilitySnapshot>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class ClaudeSessionService {
|
|
2
|
+
sdkAdapter;
|
|
3
|
+
metadataService;
|
|
4
|
+
constructor(sdkAdapter, metadataService) {
|
|
5
|
+
this.sdkAdapter = sdkAdapter;
|
|
6
|
+
this.metadataService = metadataService;
|
|
7
|
+
}
|
|
8
|
+
runTask(input, onEvent) {
|
|
9
|
+
return this.sdkAdapter.runSession(input, onEvent);
|
|
10
|
+
}
|
|
11
|
+
listSessions(cwd) {
|
|
12
|
+
return this.sdkAdapter.listSavedSessions(cwd);
|
|
13
|
+
}
|
|
14
|
+
getTranscript(sessionId, cwd) {
|
|
15
|
+
return this.sdkAdapter.getTranscript(sessionId, cwd);
|
|
16
|
+
}
|
|
17
|
+
inspectRepository(cwd, options) {
|
|
18
|
+
return this.metadataService.collect(cwd, options);
|
|
19
|
+
}
|
|
20
|
+
probeCapabilities(cwd) {
|
|
21
|
+
return this.sdkAdapter.probeCapabilities(cwd);
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Plugin } from '@opencode-ai/plugin';
|
|
2
|
+
export { ClaudeAgentSdkAdapter } from './claude/claude-agent-sdk-adapter.js';
|
|
3
|
+
export { ClaudeSessionService } from './claude/claude-session.service.js';
|
|
4
|
+
export { ManagerOrchestrator } from './manager/manager-orchestrator.js';
|
|
5
|
+
export { TaskPlanner } from './manager/task-planner.js';
|
|
6
|
+
export { ClaudeMetadataService } from './metadata/claude-metadata.service.js';
|
|
7
|
+
export { RepoClaudeConfigReader } from './metadata/repo-claude-config-reader.js';
|
|
8
|
+
export { getOrCreatePluginServices } from './plugin/service-factory.js';
|
|
9
|
+
export { FileRunStateStore } from './state/file-run-state-store.js';
|
|
10
|
+
export { WorktreeCoordinator } from './worktree/worktree-coordinator.js';
|
|
11
|
+
import { ClaudeManagerPlugin } from './plugin/claude-manager.plugin.js';
|
|
12
|
+
export type { ClaudeCapabilitySnapshot, ClaudeMetadataSnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerRunRecord, ManagerRunResult, ManagerTaskRequest, ManagerPromptRegistry, RunClaudeSessionInput, } from './types/contracts.js';
|
|
13
|
+
export { ClaudeManagerPlugin };
|
|
14
|
+
export declare const plugin: Plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { ClaudeAgentSdkAdapter } from './claude/claude-agent-sdk-adapter.js';
|
|
2
|
+
export { ClaudeSessionService } from './claude/claude-session.service.js';
|
|
3
|
+
export { ManagerOrchestrator } from './manager/manager-orchestrator.js';
|
|
4
|
+
export { TaskPlanner } from './manager/task-planner.js';
|
|
5
|
+
export { ClaudeMetadataService } from './metadata/claude-metadata.service.js';
|
|
6
|
+
export { RepoClaudeConfigReader } from './metadata/repo-claude-config-reader.js';
|
|
7
|
+
export { getOrCreatePluginServices } from './plugin/service-factory.js';
|
|
8
|
+
export { FileRunStateStore } from './state/file-run-state-store.js';
|
|
9
|
+
export { WorktreeCoordinator } from './worktree/worktree-coordinator.js';
|
|
10
|
+
import { ClaudeManagerPlugin } from './plugin/claude-manager.plugin.js';
|
|
11
|
+
export { ClaudeManagerPlugin };
|
|
12
|
+
export const plugin = ClaudeManagerPlugin;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ClaudeSessionService } from '../claude/claude-session.service.js';
|
|
2
|
+
import type { FileRunStateStore } from '../state/file-run-state-store.js';
|
|
3
|
+
import type { ManagerRunRecord, ManagerRunResult, ManagerTaskRequest } from '../types/contracts.js';
|
|
4
|
+
import type { WorktreeCoordinator } from '../worktree/worktree-coordinator.js';
|
|
5
|
+
import type { TaskPlanner } from './task-planner.js';
|
|
6
|
+
export declare class ManagerOrchestrator {
|
|
7
|
+
private readonly sessionService;
|
|
8
|
+
private readonly stateStore;
|
|
9
|
+
private readonly worktreeCoordinator;
|
|
10
|
+
private readonly taskPlanner;
|
|
11
|
+
constructor(sessionService: ClaudeSessionService, stateStore: FileRunStateStore, worktreeCoordinator: WorktreeCoordinator, taskPlanner: TaskPlanner);
|
|
12
|
+
run(request: ManagerTaskRequest): Promise<ManagerRunResult>;
|
|
13
|
+
listRuns(cwd: string): Promise<ManagerRunRecord[]>;
|
|
14
|
+
getRun(cwd: string, runId: string): Promise<ManagerRunRecord | null>;
|
|
15
|
+
cleanupRunWorktrees(cwd: string, runId: string): Promise<ManagerRunRecord | null>;
|
|
16
|
+
private executePlan;
|
|
17
|
+
private patchSession;
|
|
18
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { managerPromptRegistry } from '../prompts/registry.js';
|
|
3
|
+
export class ManagerOrchestrator {
|
|
4
|
+
sessionService;
|
|
5
|
+
stateStore;
|
|
6
|
+
worktreeCoordinator;
|
|
7
|
+
taskPlanner;
|
|
8
|
+
constructor(sessionService, stateStore, worktreeCoordinator, taskPlanner) {
|
|
9
|
+
this.sessionService = sessionService;
|
|
10
|
+
this.stateStore = stateStore;
|
|
11
|
+
this.worktreeCoordinator = worktreeCoordinator;
|
|
12
|
+
this.taskPlanner = taskPlanner;
|
|
13
|
+
}
|
|
14
|
+
async run(request) {
|
|
15
|
+
const mode = request.mode ?? 'auto';
|
|
16
|
+
const maxSubagents = Math.max(1, request.maxSubagents ?? 3);
|
|
17
|
+
const useWorktrees = request.useWorktrees ?? maxSubagents > 1;
|
|
18
|
+
const includeProjectSettings = request.includeProjectSettings ?? true;
|
|
19
|
+
const metadata = await this.sessionService.inspectRepository(request.cwd);
|
|
20
|
+
const plans = this.taskPlanner.plan(request.task, mode, maxSubagents);
|
|
21
|
+
const runId = randomUUID();
|
|
22
|
+
const createdAt = new Date().toISOString();
|
|
23
|
+
const assignments = await Promise.all(plans.map((plan) => this.worktreeCoordinator.prepareAssignment({
|
|
24
|
+
cwd: request.cwd,
|
|
25
|
+
runId,
|
|
26
|
+
title: plan.title,
|
|
27
|
+
useWorktree: useWorktrees && plans.length > 1,
|
|
28
|
+
})));
|
|
29
|
+
const plannedAssignments = plans.map((plan, index) => ({
|
|
30
|
+
plan,
|
|
31
|
+
assignment: assignments[index],
|
|
32
|
+
}));
|
|
33
|
+
const runRecord = {
|
|
34
|
+
id: runId,
|
|
35
|
+
cwd: request.cwd,
|
|
36
|
+
task: request.task,
|
|
37
|
+
mode,
|
|
38
|
+
useWorktrees,
|
|
39
|
+
includeProjectSettings,
|
|
40
|
+
status: 'running',
|
|
41
|
+
createdAt,
|
|
42
|
+
updatedAt: createdAt,
|
|
43
|
+
metadata,
|
|
44
|
+
sessions: plannedAssignments.map(({ plan, assignment }) => createManagedSessionRecord(plan, assignment.cwd, assignment)),
|
|
45
|
+
};
|
|
46
|
+
await this.stateStore.saveRun(runRecord);
|
|
47
|
+
const canRunInParallel = plannedAssignments.every(({ assignment }) => assignment.mode === 'git-worktree');
|
|
48
|
+
const settledResults = canRunInParallel
|
|
49
|
+
? await Promise.allSettled(plannedAssignments.map(({ plan, assignment }) => this.executePlan({
|
|
50
|
+
request,
|
|
51
|
+
runId,
|
|
52
|
+
plan,
|
|
53
|
+
assignment,
|
|
54
|
+
includeProjectSettings,
|
|
55
|
+
})))
|
|
56
|
+
: await runSequentially(plannedAssignments.map(({ plan, assignment }) => () => this.executePlan({
|
|
57
|
+
request,
|
|
58
|
+
runId,
|
|
59
|
+
plan,
|
|
60
|
+
assignment,
|
|
61
|
+
includeProjectSettings,
|
|
62
|
+
})));
|
|
63
|
+
const failedResult = settledResults.find((result) => result.status === 'rejected');
|
|
64
|
+
const finalRun = await this.stateStore.updateRun(request.cwd, runId, (currentRun) => ({
|
|
65
|
+
...currentRun,
|
|
66
|
+
status: failedResult ? 'failed' : 'completed',
|
|
67
|
+
updatedAt: new Date().toISOString(),
|
|
68
|
+
finalSummary: summarizeRun(currentRun.sessions),
|
|
69
|
+
}));
|
|
70
|
+
return { run: finalRun };
|
|
71
|
+
}
|
|
72
|
+
listRuns(cwd) {
|
|
73
|
+
return this.stateStore.listRuns(cwd);
|
|
74
|
+
}
|
|
75
|
+
getRun(cwd, runId) {
|
|
76
|
+
return this.stateStore.getRun(cwd, runId);
|
|
77
|
+
}
|
|
78
|
+
async cleanupRunWorktrees(cwd, runId) {
|
|
79
|
+
const run = await this.stateStore.getRun(cwd, runId);
|
|
80
|
+
if (!run) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
for (const session of run.sessions) {
|
|
84
|
+
if (session.worktreeMode !== 'git-worktree') {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
await this.worktreeCoordinator.cleanupAssignment({
|
|
88
|
+
mode: 'git-worktree',
|
|
89
|
+
cwd: session.cwd,
|
|
90
|
+
rootCwd: run.cwd,
|
|
91
|
+
branchName: session.branchName,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return run;
|
|
95
|
+
}
|
|
96
|
+
async executePlan(input) {
|
|
97
|
+
const { request, runId, plan, assignment, includeProjectSettings } = input;
|
|
98
|
+
await this.patchSession(request.cwd, runId, plan.id, (session) => ({
|
|
99
|
+
...session,
|
|
100
|
+
status: 'running',
|
|
101
|
+
}));
|
|
102
|
+
try {
|
|
103
|
+
const sessionResult = await this.sessionService.runTask({
|
|
104
|
+
cwd: assignment.cwd,
|
|
105
|
+
prompt: buildWorkerPrompt(plan.prompt, request.task),
|
|
106
|
+
systemPrompt: managerPromptRegistry.subagentSystemPrompt,
|
|
107
|
+
model: request.model,
|
|
108
|
+
includePartialMessages: true,
|
|
109
|
+
settingSources: includeProjectSettings ? ['project', 'local'] : [],
|
|
110
|
+
}, async (event) => {
|
|
111
|
+
await this.patchSession(request.cwd, runId, plan.id, (session) => ({
|
|
112
|
+
...session,
|
|
113
|
+
claudeSessionId: event.sessionId ?? session.claudeSessionId,
|
|
114
|
+
events: [...session.events, compactEvent(event)],
|
|
115
|
+
}));
|
|
116
|
+
});
|
|
117
|
+
await this.patchSession(request.cwd, runId, plan.id, (session) => ({
|
|
118
|
+
...session,
|
|
119
|
+
status: 'completed',
|
|
120
|
+
claudeSessionId: sessionResult.sessionId,
|
|
121
|
+
finalText: sessionResult.finalText,
|
|
122
|
+
turns: sessionResult.turns,
|
|
123
|
+
totalCostUsd: sessionResult.totalCostUsd,
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
await this.patchSession(request.cwd, runId, plan.id, (session) => ({
|
|
128
|
+
...session,
|
|
129
|
+
status: 'failed',
|
|
130
|
+
error: error instanceof Error ? error.message : String(error),
|
|
131
|
+
}));
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async patchSession(cwd, runId, sessionId, update) {
|
|
136
|
+
await this.stateStore.updateRun(cwd, runId, (run) => ({
|
|
137
|
+
...run,
|
|
138
|
+
updatedAt: new Date().toISOString(),
|
|
139
|
+
sessions: run.sessions.map((session) => session.id === sessionId ? update(session) : session),
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function createManagedSessionRecord(plan, cwd, assignment) {
|
|
144
|
+
return {
|
|
145
|
+
id: plan.id,
|
|
146
|
+
title: plan.title,
|
|
147
|
+
prompt: plan.prompt,
|
|
148
|
+
status: 'pending',
|
|
149
|
+
cwd,
|
|
150
|
+
worktreeMode: assignment.mode,
|
|
151
|
+
branchName: assignment.branchName,
|
|
152
|
+
events: [],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function buildWorkerPrompt(subtaskPrompt, parentTask) {
|
|
156
|
+
return [
|
|
157
|
+
'You are executing a delegated Claude Code subtask.',
|
|
158
|
+
`Parent task: ${parentTask}`,
|
|
159
|
+
`Assigned subtask: ${subtaskPrompt}`,
|
|
160
|
+
'Stay within scope, finish the requested work, and end with a concise verification summary.',
|
|
161
|
+
].join('\n\n');
|
|
162
|
+
}
|
|
163
|
+
function summarizeRun(sessions) {
|
|
164
|
+
return sessions
|
|
165
|
+
.map((session) => `${session.title}: ${session.finalText ?? session.error ?? session.status}`)
|
|
166
|
+
.join('\n');
|
|
167
|
+
}
|
|
168
|
+
function compactEvent(event) {
|
|
169
|
+
return {
|
|
170
|
+
...event,
|
|
171
|
+
text: event.text.slice(0, 4000),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async function runSequentially(tasks) {
|
|
175
|
+
const results = [];
|
|
176
|
+
for (const task of tasks) {
|
|
177
|
+
try {
|
|
178
|
+
await task();
|
|
179
|
+
results.push({ status: 'fulfilled', value: undefined });
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
results.push({ status: 'rejected', reason: error });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return results;
|
|
186
|
+
}
|