@doingdev/opencode-claude-manager-plugin 0.1.33 → 0.1.35
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 +14 -15
- package/dist/claude/claude-agent-sdk-adapter.js +1 -1
- package/dist/claude/session-live-tailer.js +2 -2
- package/dist/plugin/agent-hierarchy.d.ts +1 -1
- package/dist/plugin/agent-hierarchy.js +14 -19
- package/dist/plugin/claude-manager.plugin.js +115 -67
- package/dist/prompts/registry.js +24 -14
- package/package.json +1 -1
- package/dist/metadata/claude-metadata.service.d.ts +0 -12
- package/dist/metadata/claude-metadata.service.js +0 -38
- package/dist/metadata/repo-claude-config-reader.d.ts +0 -7
- package/dist/metadata/repo-claude-config-reader.js +0 -154
- package/dist/plugin/orchestrator.plugin.d.ts +0 -2
- package/dist/plugin/orchestrator.plugin.js +0 -116
- package/dist/providers/claude-code-wrapper.d.ts +0 -13
- package/dist/providers/claude-code-wrapper.js +0 -13
- package/dist/safety/bash-safety.d.ts +0 -21
- package/dist/safety/bash-safety.js +0 -62
package/README.md
CHANGED
|
@@ -53,15 +53,15 @@ If you are testing locally, point OpenCode at the local package or plugin file u
|
|
|
53
53
|
|
|
54
54
|
### Engineer session
|
|
55
55
|
|
|
56
|
-
- `
|
|
56
|
+
- `explore` — investigate and analyze code without making edits. Read-only exploration of the codebase. Preferred first step before implementation.
|
|
57
57
|
- `message` (required) — the instruction to send.
|
|
58
|
-
- `mode` — `"plan"` (read-only investigation) or `"free"` (default, normal execution with edits).
|
|
59
58
|
- `freshSession` — set to `true` to clear the active session before sending. Use when switching to an unrelated task or when context is contaminated.
|
|
60
59
|
- `model` — `"claude-opus-4-6"` (default, recommended for most coding work), `"claude-sonnet-4-6"`, or `"claude-sonnet-4-5"` (faster/lighter tasks).
|
|
61
60
|
- `effort` — `"high"` (default), `"medium"` (lighter tasks), `"low"`, or `"max"` (especially hard problems).
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
- `
|
|
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.
|
|
65
65
|
|
|
66
66
|
### Git operations
|
|
67
67
|
|
|
@@ -71,9 +71,8 @@ If you are testing locally, point OpenCode at the local package or plugin file u
|
|
|
71
71
|
|
|
72
72
|
### Inspection
|
|
73
73
|
|
|
74
|
-
- `
|
|
75
|
-
- `
|
|
76
|
-
- `engineer_runs` — list or inspect persisted manager run records (may be empty if tasks were sent directly via `engineer_send` rather than the run-tracking path).
|
|
74
|
+
- `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.
|
|
77
76
|
|
|
78
77
|
### Tool approval
|
|
79
78
|
|
|
@@ -86,7 +85,7 @@ If you are testing locally, point OpenCode at the local package or plugin file u
|
|
|
86
85
|
The plugin registers a CTO → Manager → Engineer hierarchy through the OpenCode plugin `config` hook:
|
|
87
86
|
|
|
88
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.
|
|
89
|
-
- **`manager`** (subagent) — operates a Claude Code engineer through a persistent session. Has the full tool surface (`
|
|
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.
|
|
90
89
|
- **Engineer** — the Claude Code persistent session itself (not an OpenCode agent). Receives instructions from the manager, executes code changes, and reports results.
|
|
91
90
|
|
|
92
91
|
These are added to OpenCode config at runtime by the plugin, so they do not require separate manual `opencode.json` entries.
|
|
@@ -95,27 +94,27 @@ These are added to OpenCode config at runtime by the plugin, so they do not requ
|
|
|
95
94
|
|
|
96
95
|
Typical flow inside OpenCode:
|
|
97
96
|
|
|
98
|
-
1.
|
|
99
|
-
2.
|
|
97
|
+
1. Explore the codebase with `explore`.
|
|
98
|
+
2. Implement changes with `implement`.
|
|
100
99
|
3. Review changes with `git_diff`, then commit or reset.
|
|
101
|
-
4. Inspect saved Claude history with `
|
|
100
|
+
4. Inspect saved Claude history with `list_transcripts` or prior orchestration records with `list_history`.
|
|
102
101
|
|
|
103
102
|
Example tasks:
|
|
104
103
|
|
|
105
104
|
```text
|
|
106
|
-
Use
|
|
105
|
+
Use implement to add the new validation logic in src/auth.ts, then review with git_diff.
|
|
107
106
|
```
|
|
108
107
|
|
|
109
108
|
Start a fresh session for an unrelated task:
|
|
110
109
|
|
|
111
110
|
```text
|
|
112
|
-
Use
|
|
111
|
+
Use explore with freshSession:true to investigate the failing CI test in test/api.test.ts.
|
|
113
112
|
```
|
|
114
113
|
|
|
115
114
|
Reclaim context mid-session:
|
|
116
115
|
|
|
117
116
|
```text
|
|
118
|
-
Use
|
|
117
|
+
Use compact_context to free up context, then continue with the next implementation step.
|
|
119
118
|
```
|
|
120
119
|
|
|
121
120
|
## Local Development
|
|
@@ -409,7 +409,7 @@ function truncateJsonish(value, max) {
|
|
|
409
409
|
return truncateString(JSON.stringify(value), max);
|
|
410
410
|
}
|
|
411
411
|
catch {
|
|
412
|
-
return truncateString(
|
|
412
|
+
return truncateString('[non-serializable]', max);
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
415
|
function truncateString(s, max) {
|
|
@@ -99,7 +99,7 @@ export class SessionLiveTailer {
|
|
|
99
99
|
});
|
|
100
100
|
let chunk = '';
|
|
101
101
|
stream.on('data', (data) => {
|
|
102
|
-
chunk += data;
|
|
102
|
+
chunk += typeof data === 'string' ? data : data.toString('utf8');
|
|
103
103
|
});
|
|
104
104
|
stream.on('end', () => {
|
|
105
105
|
reading = false;
|
|
@@ -264,6 +264,6 @@ function stringifyContent(value) {
|
|
|
264
264
|
return JSON.stringify(value);
|
|
265
265
|
}
|
|
266
266
|
catch {
|
|
267
|
-
return
|
|
267
|
+
return '[non-serializable]';
|
|
268
268
|
}
|
|
269
269
|
}
|
|
@@ -11,7 +11,7 @@ export declare const AGENT_CTO = "cto";
|
|
|
11
11
|
export declare const AGENT_ENGINEER_PLAN = "engineer_plan";
|
|
12
12
|
export declare const AGENT_ENGINEER_BUILD = "engineer_build";
|
|
13
13
|
/** All restricted tool IDs (union of all domain groups) */
|
|
14
|
-
export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["
|
|
14
|
+
export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["explore", "implement", "compact_context", "clear_session", "session_health", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "approval_policy", "approval_decisions", "approval_update"];
|
|
15
15
|
type ToolPermission = 'allow' | 'ask' | 'deny';
|
|
16
16
|
type AgentPermission = {
|
|
17
17
|
'*'?: ToolPermission;
|
|
@@ -17,23 +17,18 @@ export const AGENT_ENGINEER_BUILD = 'engineer_build';
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
/** Shared engineer session tools (compact, clear, status, diagnostics) */
|
|
19
19
|
const ENGINEER_SHARED_TOOL_IDS = [
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
];
|
|
26
|
-
/** All engineer tools — generic send + mode-locked sends + shared session tools */
|
|
27
|
-
const ENGINEER_TOOL_IDS = [
|
|
28
|
-
'engineer_send',
|
|
29
|
-
'engineer_send_plan',
|
|
30
|
-
'engineer_send_build',
|
|
31
|
-
...ENGINEER_SHARED_TOOL_IDS,
|
|
20
|
+
'compact_context',
|
|
21
|
+
'clear_session',
|
|
22
|
+
'session_health',
|
|
23
|
+
'list_transcripts',
|
|
24
|
+
'list_history',
|
|
32
25
|
];
|
|
26
|
+
/** All engineer tools — mode-locked sends + shared session tools */
|
|
27
|
+
const ENGINEER_TOOL_IDS = ['explore', 'implement', ...ENGINEER_SHARED_TOOL_IDS];
|
|
33
28
|
/** Tools for the engineer_plan wrapper (plan-mode send + shared) */
|
|
34
|
-
const ENGINEER_PLAN_TOOL_IDS = ['
|
|
29
|
+
const ENGINEER_PLAN_TOOL_IDS = ['explore', ...ENGINEER_SHARED_TOOL_IDS];
|
|
35
30
|
/** Tools for the engineer_build wrapper (build-mode send + shared) */
|
|
36
|
-
const ENGINEER_BUILD_TOOL_IDS = ['
|
|
31
|
+
const ENGINEER_BUILD_TOOL_IDS = ['implement', ...ENGINEER_SHARED_TOOL_IDS];
|
|
37
32
|
/** Git tools — owned by CTO */
|
|
38
33
|
const GIT_TOOL_IDS = ['git_diff', 'git_commit', 'git_reset'];
|
|
39
34
|
/** Approval tools — owned by CTO */
|
|
@@ -87,7 +82,7 @@ function buildCtoPermissions() {
|
|
|
87
82
|
},
|
|
88
83
|
};
|
|
89
84
|
}
|
|
90
|
-
/** Engineer plan wrapper: read-only investigation +
|
|
85
|
+
/** Engineer plan wrapper: read-only investigation + explore + shared session tools. */
|
|
91
86
|
function buildEngineerPlanPermissions() {
|
|
92
87
|
const denied = {};
|
|
93
88
|
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
@@ -104,7 +99,7 @@ function buildEngineerPlanPermissions() {
|
|
|
104
99
|
...allowed,
|
|
105
100
|
};
|
|
106
101
|
}
|
|
107
|
-
/** Engineer build wrapper: read-only investigation +
|
|
102
|
+
/** Engineer build wrapper: read-only investigation + implement + shared session tools. */
|
|
108
103
|
function buildEngineerBuildPermissions() {
|
|
109
104
|
const denied = {};
|
|
110
105
|
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
@@ -126,7 +121,7 @@ function buildEngineerBuildPermissions() {
|
|
|
126
121
|
// ---------------------------------------------------------------------------
|
|
127
122
|
export function buildCtoAgentConfig(prompts) {
|
|
128
123
|
return {
|
|
129
|
-
description: '
|
|
124
|
+
description: 'Delegates by default with minimal spot-checks, spawns engineers for exploration and implementation, reviews diffs, and commits.',
|
|
130
125
|
mode: 'primary',
|
|
131
126
|
color: '#D97757',
|
|
132
127
|
permission: buildCtoPermissions(),
|
|
@@ -135,7 +130,7 @@ export function buildCtoAgentConfig(prompts) {
|
|
|
135
130
|
}
|
|
136
131
|
export function buildEngineerPlanAgentConfig(prompts) {
|
|
137
132
|
return {
|
|
138
|
-
description: '
|
|
133
|
+
description: 'Thin high-judgment wrapper that frames work quickly and dispatches to Claude Code in plan mode for read-only investigation.',
|
|
139
134
|
mode: 'subagent',
|
|
140
135
|
color: '#D97757',
|
|
141
136
|
permission: buildEngineerPlanPermissions(),
|
|
@@ -144,7 +139,7 @@ export function buildEngineerPlanAgentConfig(prompts) {
|
|
|
144
139
|
}
|
|
145
140
|
export function buildEngineerBuildAgentConfig(prompts) {
|
|
146
141
|
return {
|
|
147
|
-
description: '
|
|
142
|
+
description: 'Thin high-judgment wrapper that frames work quickly and dispatches to Claude Code in free mode for implementation.',
|
|
148
143
|
mode: 'subagent',
|
|
149
144
|
color: '#D97757',
|
|
150
145
|
permission: buildEngineerBuildPermissions(),
|
|
@@ -5,7 +5,7 @@ import { AGENT_CTO, AGENT_ENGINEER_BUILD, AGENT_ENGINEER_PLAN, buildCtoAgentConf
|
|
|
5
5
|
import { getOrCreatePluginServices } from './service-factory.js';
|
|
6
6
|
export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
7
7
|
const services = getOrCreatePluginServices(worktree);
|
|
8
|
-
async function
|
|
8
|
+
async function executeDelegate(args, context) {
|
|
9
9
|
const cwd = args.cwd ?? context.worktree;
|
|
10
10
|
if (args.freshSession) {
|
|
11
11
|
await services.manager.clearSession(cwd);
|
|
@@ -13,8 +13,11 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
13
13
|
const hasActiveSession = services.manager.getStatus().sessionId !== null;
|
|
14
14
|
const promptPreview = args.message.length > 100 ? args.message.slice(0, 100) + '...' : args.message;
|
|
15
15
|
context.metadata({
|
|
16
|
-
title: hasActiveSession
|
|
16
|
+
title: hasActiveSession
|
|
17
|
+
? '⚡ Claude Code: Resuming session...'
|
|
18
|
+
: '⚡ Claude Code: Initializing...',
|
|
17
19
|
metadata: {
|
|
20
|
+
status: 'running',
|
|
18
21
|
sessionId: services.manager.getStatus().sessionId,
|
|
19
22
|
prompt: promptPreview,
|
|
20
23
|
},
|
|
@@ -49,8 +52,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
49
52
|
// ignore parse errors
|
|
50
53
|
}
|
|
51
54
|
context.metadata({
|
|
52
|
-
title:
|
|
55
|
+
title: `⚡ Claude Code: Running ${toolName}...${usageSuffix}`,
|
|
53
56
|
metadata: {
|
|
57
|
+
status: 'running',
|
|
54
58
|
sessionId: event.sessionId,
|
|
55
59
|
type: event.type,
|
|
56
60
|
tool: toolName,
|
|
@@ -61,8 +65,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
61
65
|
else if (event.type === 'assistant') {
|
|
62
66
|
const thinkingPreview = event.text.length > 150 ? event.text.slice(0, 150) + '...' : event.text;
|
|
63
67
|
context.metadata({
|
|
64
|
-
title:
|
|
68
|
+
title: `⚡ Claude Code: Thinking...${usageSuffix}`,
|
|
65
69
|
metadata: {
|
|
70
|
+
status: 'running',
|
|
66
71
|
sessionId: event.sessionId,
|
|
67
72
|
type: event.type,
|
|
68
73
|
thinking: thinkingPreview,
|
|
@@ -71,8 +76,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
71
76
|
}
|
|
72
77
|
else if (event.type === 'init') {
|
|
73
78
|
context.metadata({
|
|
74
|
-
title:
|
|
79
|
+
title: `⚡ Claude Code: Session started`,
|
|
75
80
|
metadata: {
|
|
81
|
+
status: 'running',
|
|
76
82
|
sessionId: event.sessionId,
|
|
77
83
|
prompt: promptPreview,
|
|
78
84
|
},
|
|
@@ -80,9 +86,11 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
80
86
|
}
|
|
81
87
|
else if (event.type === 'user') {
|
|
82
88
|
const preview = event.text.length > 200 ? event.text.slice(0, 200) + '...' : event.text;
|
|
89
|
+
const outputPreview = formatToolOutputPreview(event.text);
|
|
83
90
|
context.metadata({
|
|
84
|
-
title:
|
|
91
|
+
title: `⚡ Claude Code: ${outputPreview}${usageSuffix}`,
|
|
85
92
|
metadata: {
|
|
93
|
+
status: 'running',
|
|
86
94
|
sessionId: event.sessionId,
|
|
87
95
|
type: event.type,
|
|
88
96
|
output: preview,
|
|
@@ -92,17 +100,25 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
92
100
|
else if (event.type === 'tool_progress') {
|
|
93
101
|
let toolName = 'tool';
|
|
94
102
|
let elapsed = 0;
|
|
103
|
+
let progressCurrent;
|
|
104
|
+
let progressTotal;
|
|
95
105
|
try {
|
|
96
106
|
const parsed = JSON.parse(event.text);
|
|
97
107
|
toolName = parsed.name ?? 'tool';
|
|
98
108
|
elapsed = parsed.elapsed ?? 0;
|
|
109
|
+
progressCurrent = parsed.current;
|
|
110
|
+
progressTotal = parsed.total;
|
|
99
111
|
}
|
|
100
112
|
catch {
|
|
101
113
|
// ignore
|
|
102
114
|
}
|
|
115
|
+
const progressInfo = progressCurrent !== undefined && progressTotal !== undefined
|
|
116
|
+
? ` [${progressCurrent}/${progressTotal}]`
|
|
117
|
+
: '';
|
|
103
118
|
context.metadata({
|
|
104
|
-
title:
|
|
119
|
+
title: `⚡ Claude Code: ${toolName} running ${elapsed > 0 ? `(${elapsed.toFixed(0)}s)` : ''}${progressInfo}...${usageSuffix}`,
|
|
105
120
|
metadata: {
|
|
121
|
+
status: 'running',
|
|
106
122
|
sessionId: event.sessionId,
|
|
107
123
|
type: event.type,
|
|
108
124
|
tool: toolName,
|
|
@@ -113,8 +129,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
113
129
|
else if (event.type === 'tool_summary') {
|
|
114
130
|
const summary = event.text.length > 200 ? event.text.slice(0, 200) + '...' : event.text;
|
|
115
131
|
context.metadata({
|
|
116
|
-
title:
|
|
132
|
+
title: `✅ Claude Code: Tool done${usageSuffix}`,
|
|
117
133
|
metadata: {
|
|
134
|
+
status: 'success',
|
|
118
135
|
sessionId: event.sessionId,
|
|
119
136
|
type: event.type,
|
|
120
137
|
summary,
|
|
@@ -124,8 +141,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
124
141
|
else if (event.type === 'partial') {
|
|
125
142
|
const delta = event.text.length > 200 ? event.text.slice(0, 200) + '...' : event.text;
|
|
126
143
|
context.metadata({
|
|
127
|
-
title:
|
|
144
|
+
title: `⚡ Claude Code: Writing...${usageSuffix}`,
|
|
128
145
|
metadata: {
|
|
146
|
+
status: 'running',
|
|
129
147
|
sessionId: event.sessionId,
|
|
130
148
|
type: event.type,
|
|
131
149
|
delta,
|
|
@@ -134,12 +152,14 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
134
152
|
}
|
|
135
153
|
else if (event.type === 'error') {
|
|
136
154
|
context.metadata({
|
|
137
|
-
title:
|
|
155
|
+
title: `❌ Claude Code: Error`,
|
|
138
156
|
metadata: {
|
|
157
|
+
status: 'error',
|
|
139
158
|
sessionId: event.sessionId,
|
|
140
159
|
error: event.text.slice(0, 200),
|
|
141
160
|
},
|
|
142
161
|
});
|
|
162
|
+
showToastIfAvailable(context, `Claude Code error: ${event.text.slice(0, 100)}`);
|
|
143
163
|
}
|
|
144
164
|
});
|
|
145
165
|
const costLabel = `$${(result.totalCostUsd ?? 0).toFixed(4)}`;
|
|
@@ -147,15 +167,17 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
147
167
|
const contextWarning = formatContextWarning(result.context);
|
|
148
168
|
if (contextWarning) {
|
|
149
169
|
context.metadata({
|
|
150
|
-
title:
|
|
151
|
-
metadata: { sessionId: result.sessionId, contextWarning },
|
|
170
|
+
title: `⚠️ Claude Code: Context at ${result.context.estimatedContextPercent}% (${turns} turns)`,
|
|
171
|
+
metadata: { status: 'warning', sessionId: result.sessionId, contextWarning },
|
|
152
172
|
});
|
|
173
|
+
showToastIfAvailable(context, `⚠️ Context usage at ${result.context.estimatedContextPercent}% — consider compacting`);
|
|
153
174
|
}
|
|
154
175
|
else {
|
|
155
176
|
context.metadata({
|
|
156
|
-
title:
|
|
157
|
-
metadata: { sessionId: result.sessionId },
|
|
177
|
+
title: `✅ Claude Code: Complete (${turns} turns, ${costLabel})`,
|
|
178
|
+
metadata: { status: 'success', sessionId: result.sessionId },
|
|
158
179
|
});
|
|
180
|
+
showToastIfAvailable(context, `✅ Session complete (${turns} turns, ${costLabel})`);
|
|
159
181
|
}
|
|
160
182
|
let toolOutputs = [];
|
|
161
183
|
if (result.sessionId) {
|
|
@@ -196,28 +218,26 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
196
218
|
config.agent[AGENT_ENGINEER_BUILD] ??= buildEngineerBuildAgentConfig(derivedPrompts);
|
|
197
219
|
},
|
|
198
220
|
tool: {
|
|
199
|
-
|
|
200
|
-
description: '
|
|
201
|
-
'
|
|
221
|
+
explore: tool({
|
|
222
|
+
description: 'Investigate and analyze code without making edits. ' +
|
|
223
|
+
'Read-only exploration of the codebase. ' +
|
|
224
|
+
'Preferred first step before implementation.',
|
|
202
225
|
args: {
|
|
203
226
|
message: tool.schema.string().min(1),
|
|
204
227
|
model: tool.schema
|
|
205
228
|
.enum(['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-sonnet-4-5'])
|
|
206
229
|
.optional(),
|
|
207
230
|
effort: tool.schema.enum(['low', 'medium', 'high', 'max']).default('high'),
|
|
208
|
-
mode: tool.schema.enum(['plan', 'free']).default('free'),
|
|
209
231
|
freshSession: tool.schema.boolean().default(false),
|
|
210
232
|
cwd: tool.schema.string().optional(),
|
|
211
233
|
},
|
|
212
234
|
async execute(args, context) {
|
|
213
|
-
return
|
|
235
|
+
return executeDelegate({ ...args, mode: 'plan' }, context);
|
|
214
236
|
},
|
|
215
237
|
}),
|
|
216
|
-
|
|
217
|
-
description: '
|
|
218
|
-
'
|
|
219
|
-
'Auto-creates a session on first call. Resumes the existing session on subsequent calls. ' +
|
|
220
|
-
'Returns the assistant response and current context health snapshot.',
|
|
238
|
+
implement: tool({
|
|
239
|
+
description: 'Implement code changes - can read, edit, and create files. ' +
|
|
240
|
+
'Use after exploration to make changes.',
|
|
221
241
|
args: {
|
|
222
242
|
message: tool.schema.string().min(1),
|
|
223
243
|
model: tool.schema
|
|
@@ -228,34 +248,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
228
248
|
cwd: tool.schema.string().optional(),
|
|
229
249
|
},
|
|
230
250
|
async execute(args, context) {
|
|
231
|
-
return
|
|
251
|
+
return executeDelegate({ ...args, mode: 'free' }, context);
|
|
232
252
|
},
|
|
233
253
|
}),
|
|
234
|
-
|
|
235
|
-
description: '
|
|
236
|
-
'
|
|
237
|
-
'Auto-creates a session on first call. Resumes the existing session on subsequent calls. ' +
|
|
238
|
-
'Returns the assistant response and current context health snapshot. ' +
|
|
239
|
-
'Prefer claude-opus-4-6 (default) for most coding work; use a Sonnet model for faster/lighter tasks. ' +
|
|
240
|
-
'Prefer effort "high" (default) for most work; use "medium" for lighter tasks and "max" for especially hard problems.',
|
|
241
|
-
args: {
|
|
242
|
-
message: tool.schema.string().min(1),
|
|
243
|
-
model: tool.schema
|
|
244
|
-
.enum(['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-sonnet-4-5'])
|
|
245
|
-
.optional(),
|
|
246
|
-
effort: tool.schema.enum(['low', 'medium', 'high', 'max']).default('high'),
|
|
247
|
-
freshSession: tool.schema.boolean().default(false),
|
|
248
|
-
cwd: tool.schema.string().optional(),
|
|
249
|
-
},
|
|
250
|
-
async execute(args, context) {
|
|
251
|
-
return executeEngineerSend({ ...args, mode: 'free' }, context);
|
|
252
|
-
},
|
|
253
|
-
}),
|
|
254
|
-
engineer_compact: tool({
|
|
255
|
-
description: 'Compact the active Claude Code session to reclaim context space. ' +
|
|
256
|
-
'Sends /compact to the session, which compresses prior conversation while preserving state. ' +
|
|
257
|
-
'Use before clearing when context is high but the session still has useful state. ' +
|
|
258
|
-
'Fails if there is no active session.',
|
|
254
|
+
compact_context: tool({
|
|
255
|
+
description: 'Compress session history to reclaim context window space. ' +
|
|
256
|
+
'Preserves state while reducing token usage.',
|
|
259
257
|
args: {
|
|
260
258
|
cwd: tool.schema.string().optional(),
|
|
261
259
|
},
|
|
@@ -267,9 +265,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
267
265
|
const contextWarning = formatContextWarning(snap);
|
|
268
266
|
context.metadata({
|
|
269
267
|
title: contextWarning
|
|
270
|
-
?
|
|
271
|
-
:
|
|
272
|
-
metadata: {
|
|
268
|
+
? `⚠️ Claude Code: Compacted — context at ${snap.estimatedContextPercent}%`
|
|
269
|
+
: `✅ Claude Code: Compacted (${snap.totalTurns} turns, $${(snap.totalCostUsd ?? 0).toFixed(4)})`,
|
|
270
|
+
metadata: {
|
|
271
|
+
status: contextWarning ? 'warning' : 'success',
|
|
272
|
+
sessionId: result.sessionId,
|
|
273
|
+
},
|
|
273
274
|
});
|
|
274
275
|
return JSON.stringify({
|
|
275
276
|
sessionId: result.sessionId,
|
|
@@ -317,9 +318,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
317
318
|
return JSON.stringify(result, null, 2);
|
|
318
319
|
},
|
|
319
320
|
}),
|
|
320
|
-
|
|
321
|
-
description: 'Clear the active
|
|
322
|
-
'Use when context is full
|
|
321
|
+
clear_session: tool({
|
|
322
|
+
description: 'Clear the active session to start fresh. ' +
|
|
323
|
+
'Use when context is full or starting a new task.',
|
|
323
324
|
args: {
|
|
324
325
|
cwd: tool.schema.string().optional(),
|
|
325
326
|
reason: tool.schema.string().optional(),
|
|
@@ -332,8 +333,8 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
332
333
|
return JSON.stringify({ clearedSessionId: clearedId });
|
|
333
334
|
},
|
|
334
335
|
}),
|
|
335
|
-
|
|
336
|
-
description: '
|
|
336
|
+
session_health: tool({
|
|
337
|
+
description: 'Check session health metrics: context usage %, turn count, cost, and session ID.',
|
|
337
338
|
args: {
|
|
338
339
|
cwd: tool.schema.string().optional(),
|
|
339
340
|
},
|
|
@@ -349,9 +350,8 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
349
350
|
}, null, 2);
|
|
350
351
|
},
|
|
351
352
|
}),
|
|
352
|
-
|
|
353
|
-
description: 'List
|
|
354
|
-
'When sessionId is provided, returns both SDK transcript and local events.',
|
|
353
|
+
list_transcripts: tool({
|
|
354
|
+
description: 'List available session transcripts or inspect a specific transcript by ID.',
|
|
355
355
|
args: {
|
|
356
356
|
cwd: tool.schema.string().optional(),
|
|
357
357
|
sessionId: tool.schema.string().optional(),
|
|
@@ -373,8 +373,8 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
373
373
|
return JSON.stringify(sessions, null, 2);
|
|
374
374
|
},
|
|
375
375
|
}),
|
|
376
|
-
|
|
377
|
-
description: 'List persistent manager run
|
|
376
|
+
list_history: tool({
|
|
377
|
+
description: 'List persistent run records from the manager or inspect a specific run.',
|
|
378
378
|
args: {
|
|
379
379
|
cwd: tool.schema.string().optional(),
|
|
380
380
|
runId: tool.schema.string().optional(),
|
|
@@ -480,8 +480,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
480
480
|
},
|
|
481
481
|
};
|
|
482
482
|
};
|
|
483
|
-
function annotateToolRun(context, title, metadata) {
|
|
484
|
-
|
|
483
|
+
function annotateToolRun(context, title, metadata, status) {
|
|
484
|
+
const emoji = status ? `${formatStatusEmoji(status)} ` : '';
|
|
485
|
+
context.metadata({
|
|
486
|
+
title: `${emoji}${title}`,
|
|
487
|
+
metadata: { ...metadata, ...(status ? { status } : {}) },
|
|
488
|
+
});
|
|
485
489
|
}
|
|
486
490
|
function formatLiveUsage(turns, cost) {
|
|
487
491
|
if (turns === undefined && cost === undefined) {
|
|
@@ -489,10 +493,10 @@ function formatLiveUsage(turns, cost) {
|
|
|
489
493
|
}
|
|
490
494
|
const parts = [];
|
|
491
495
|
if (turns !== undefined) {
|
|
492
|
-
parts.push(
|
|
496
|
+
parts.push(`🔄 ${turns} turns`);
|
|
493
497
|
}
|
|
494
498
|
if (cost !== undefined) {
|
|
495
|
-
parts.push(
|
|
499
|
+
parts.push(`💰 $${cost.toFixed(4)}`);
|
|
496
500
|
}
|
|
497
501
|
return ` (${parts.join(', ')})`;
|
|
498
502
|
}
|
|
@@ -512,3 +516,47 @@ function formatContextWarning(context) {
|
|
|
512
516
|
.replace('{turns}', String(totalTurns))
|
|
513
517
|
.replace('{cost}', totalCostUsd.toFixed(2));
|
|
514
518
|
}
|
|
519
|
+
function formatStatusEmoji(status) {
|
|
520
|
+
switch (status) {
|
|
521
|
+
case 'running':
|
|
522
|
+
return '⚡';
|
|
523
|
+
case 'success':
|
|
524
|
+
return '✅';
|
|
525
|
+
case 'error':
|
|
526
|
+
return '❌';
|
|
527
|
+
case 'warning':
|
|
528
|
+
return '⚠️';
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function formatToolOutputPreview(text) {
|
|
532
|
+
const lower = text.toLowerCase();
|
|
533
|
+
let prefix;
|
|
534
|
+
if (lower.includes('"tool":"read"') ||
|
|
535
|
+
lower.includes('"name":"read"') ||
|
|
536
|
+
lower.includes('file contents')) {
|
|
537
|
+
prefix = '↳ Read: ';
|
|
538
|
+
}
|
|
539
|
+
else if (lower.includes('"tool":"grep"') ||
|
|
540
|
+
lower.includes('"name":"grep"') ||
|
|
541
|
+
lower.includes('matches found')) {
|
|
542
|
+
prefix = '↳ Found: ';
|
|
543
|
+
}
|
|
544
|
+
else if (lower.includes('"tool":"write"') ||
|
|
545
|
+
lower.includes('"name":"write"') ||
|
|
546
|
+
lower.includes('"tool":"edit"') ||
|
|
547
|
+
lower.includes('"name":"edit"') ||
|
|
548
|
+
lower.includes('file written') ||
|
|
549
|
+
lower.includes('file updated')) {
|
|
550
|
+
prefix = '↳ Wrote: ';
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
prefix = '↳ Result: ';
|
|
554
|
+
}
|
|
555
|
+
const snippet = text.replace(/\s+/g, ' ').trim();
|
|
556
|
+
const truncated = snippet.length > 60 ? snippet.slice(0, 60) + '...' : snippet;
|
|
557
|
+
return `${prefix}${truncated}`;
|
|
558
|
+
}
|
|
559
|
+
function showToastIfAvailable(context, message) {
|
|
560
|
+
const ctx = context;
|
|
561
|
+
ctx.client?.tui?.showToast?.(message);
|
|
562
|
+
}
|
package/dist/prompts/registry.js
CHANGED
|
@@ -18,6 +18,15 @@ export const managerPromptRegistry = {
|
|
|
18
18
|
'You are a staff+ technical owner who uses Claude Code better than anyone.',
|
|
19
19
|
'You own the outcome — discover the right problem before solving it.',
|
|
20
20
|
'',
|
|
21
|
+
'## Core principle: delegation-first',
|
|
22
|
+
'Your default action is to delegate. Do not do broad repo exploration yourself',
|
|
23
|
+
'when an engineer can do it. Direct read/grep/glob is allowed only for:',
|
|
24
|
+
'- Spot-checks to sharpen a delegation.',
|
|
25
|
+
'- Verifying a result after an engineer returns.',
|
|
26
|
+
'- Resolving one high-leverage ambiguity before dispatching.',
|
|
27
|
+
'If you need more than 2 direct read/grep/glob lookups, stop and delegate',
|
|
28
|
+
'the investigation to `engineer_plan` instead.',
|
|
29
|
+
'',
|
|
21
30
|
'## Core principle: technical ownership',
|
|
22
31
|
'You are not a ticket-taker. Before acting, look for:',
|
|
23
32
|
'- Hidden assumptions and missing constraints.',
|
|
@@ -42,11 +51,12 @@ export const managerPromptRegistry = {
|
|
|
42
51
|
' 2. What is underspecified or conflicting.',
|
|
43
52
|
' 3. What the cleanest architecture is.',
|
|
44
53
|
' 4. What should be clarified before proceeding.',
|
|
45
|
-
'
|
|
54
|
+
' Prefer spawning `engineer_plan` for repo exploration rather than reading',
|
|
55
|
+
' code yourself. Use at most 1-2 spot-check reads to sharpen the delegation.',
|
|
46
56
|
' Then delegate with file paths, line numbers, patterns, and verification.',
|
|
47
57
|
'',
|
|
48
58
|
'**Complex tasks** (multi-file feature, large refactor):',
|
|
49
|
-
' 1.
|
|
59
|
+
' 1. Spawn `engineer_plan` to explore the repo, map dependencies, and analyze impact.',
|
|
50
60
|
' 2. If requirements are unclear, ask the user ONE high-leverage question —',
|
|
51
61
|
' only when it materially changes architecture, ownership, or destructive behavior.',
|
|
52
62
|
' Prefer the question tool when discrete options exist.',
|
|
@@ -133,18 +143,18 @@ export const managerPromptRegistry = {
|
|
|
133
143
|
'- Ask ONE clarification first if it materially improves architecture.',
|
|
134
144
|
'',
|
|
135
145
|
'## Repo-context investigation',
|
|
136
|
-
'-
|
|
137
|
-
'
|
|
146
|
+
'- Use read/grep/glob sparingly — only for spot-checks to sharpen a delegation.',
|
|
147
|
+
'- If more than 2 lookups are needed, send the investigation to the engineer.',
|
|
138
148
|
'- Do NOT implement changes yourself — investigation only.',
|
|
139
149
|
'',
|
|
140
150
|
'## Behavior',
|
|
141
|
-
'- Send the objective to the engineer using
|
|
151
|
+
'- Send the objective to the engineer using explore.',
|
|
142
152
|
"- Return the engineer's response verbatim. Do not summarize.",
|
|
143
|
-
'- Use freshSession:true on
|
|
153
|
+
'- Use freshSession:true on explore when the task is unrelated to prior work.',
|
|
144
154
|
'',
|
|
145
155
|
'## Context management',
|
|
146
|
-
'- Check
|
|
147
|
-
'- Under 50%: proceed. Over 70%:
|
|
156
|
+
'- Check session_health before sending.',
|
|
157
|
+
'- Under 50%: proceed. Over 70%: compact_context. Over 85%: clear_session.',
|
|
148
158
|
'',
|
|
149
159
|
'## Model selection',
|
|
150
160
|
'- claude-opus-4-6 + high: complex analysis (default).',
|
|
@@ -176,18 +186,18 @@ export const managerPromptRegistry = {
|
|
|
176
186
|
'- Ask ONE clarification first if it materially improves architecture.',
|
|
177
187
|
'',
|
|
178
188
|
'## Repo-context investigation',
|
|
179
|
-
'-
|
|
180
|
-
'
|
|
189
|
+
'- Use read/grep/glob sparingly — only for spot-checks to sharpen a delegation.',
|
|
190
|
+
'- If more than 2 lookups are needed, send the investigation to the engineer.',
|
|
181
191
|
'- Do NOT implement changes yourself — investigation only.',
|
|
182
192
|
'',
|
|
183
193
|
'## Behavior',
|
|
184
|
-
'- Send the objective to the engineer using
|
|
194
|
+
'- Send the objective to the engineer using implement.',
|
|
185
195
|
"- Return the engineer's response verbatim. Do not summarize.",
|
|
186
|
-
'- Use freshSession:true on
|
|
196
|
+
'- Use freshSession:true on implement when the task is unrelated to prior work.',
|
|
187
197
|
'',
|
|
188
198
|
'## Context management',
|
|
189
|
-
'- Check
|
|
190
|
-
'- Under 50%: proceed. Over 70%:
|
|
199
|
+
'- Check session_health before sending.',
|
|
200
|
+
'- Under 50%: proceed. Over 70%: compact_context. Over 85%: clear_session.',
|
|
191
201
|
'',
|
|
192
202
|
'## Model selection',
|
|
193
203
|
'- claude-opus-4-6 + high: most coding tasks (default).',
|
package/package.json
CHANGED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { ClaudeMetadataSnapshot, ClaudeSettingSource } from '../types/contracts.js';
|
|
2
|
-
import type { ClaudeAgentSdkAdapter } from '../claude/claude-agent-sdk-adapter.js';
|
|
3
|
-
import type { RepoClaudeConfigReader } from './repo-claude-config-reader.js';
|
|
4
|
-
export declare class ClaudeMetadataService {
|
|
5
|
-
private readonly configReader;
|
|
6
|
-
private readonly sdkAdapter;
|
|
7
|
-
constructor(configReader: RepoClaudeConfigReader, sdkAdapter: ClaudeAgentSdkAdapter);
|
|
8
|
-
collect(cwd: string, options?: {
|
|
9
|
-
includeSdkProbe?: boolean;
|
|
10
|
-
settingSources?: ClaudeSettingSource[];
|
|
11
|
-
}): Promise<ClaudeMetadataSnapshot>;
|
|
12
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export class ClaudeMetadataService {
|
|
2
|
-
configReader;
|
|
3
|
-
sdkAdapter;
|
|
4
|
-
constructor(configReader, sdkAdapter) {
|
|
5
|
-
this.configReader = configReader;
|
|
6
|
-
this.sdkAdapter = sdkAdapter;
|
|
7
|
-
}
|
|
8
|
-
async collect(cwd, options = {}) {
|
|
9
|
-
const baseSnapshot = await this.configReader.read(cwd);
|
|
10
|
-
if (!options.includeSdkProbe) {
|
|
11
|
-
return dedupeSnapshot(baseSnapshot);
|
|
12
|
-
}
|
|
13
|
-
const capabilities = await this.sdkAdapter.probeCapabilities(cwd, options.settingSources);
|
|
14
|
-
return dedupeSnapshot({
|
|
15
|
-
...baseSnapshot,
|
|
16
|
-
commands: [...baseSnapshot.commands, ...capabilities.commands],
|
|
17
|
-
agents: capabilities.agents,
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
function dedupeSnapshot(snapshot) {
|
|
22
|
-
return {
|
|
23
|
-
...snapshot,
|
|
24
|
-
commands: dedupeByName(snapshot.commands),
|
|
25
|
-
skills: dedupeByName(snapshot.skills),
|
|
26
|
-
hooks: dedupeByName(snapshot.hooks),
|
|
27
|
-
agents: dedupeByName(snapshot.agents),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
function dedupeByName(items) {
|
|
31
|
-
const seen = new Map();
|
|
32
|
-
for (const item of items) {
|
|
33
|
-
if (!seen.has(item.name)) {
|
|
34
|
-
seen.set(item.name, item);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return [...seen.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
38
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import JSON5 from 'json5';
|
|
4
|
-
export class RepoClaudeConfigReader {
|
|
5
|
-
async read(cwd) {
|
|
6
|
-
const claudeDirectory = path.join(cwd, '.claude');
|
|
7
|
-
const skillsDirectory = path.join(claudeDirectory, 'skills');
|
|
8
|
-
const commandsDirectory = path.join(claudeDirectory, 'commands');
|
|
9
|
-
const claudeMdCandidates = [
|
|
10
|
-
path.join(cwd, 'CLAUDE.md'),
|
|
11
|
-
path.join(claudeDirectory, 'CLAUDE.md'),
|
|
12
|
-
];
|
|
13
|
-
const collectedAt = new Date().toISOString();
|
|
14
|
-
const [skills, commands, settingsResult, claudeMdPath] = await Promise.all([
|
|
15
|
-
this.readSkills(skillsDirectory),
|
|
16
|
-
this.readCommands(commandsDirectory),
|
|
17
|
-
this.readSettings(claudeDirectory),
|
|
18
|
-
findFirstExistingPath(claudeMdCandidates),
|
|
19
|
-
]);
|
|
20
|
-
return {
|
|
21
|
-
collectedAt,
|
|
22
|
-
cwd,
|
|
23
|
-
commands: [...skillsToCommands(skills), ...commands],
|
|
24
|
-
skills,
|
|
25
|
-
hooks: settingsResult.hooks,
|
|
26
|
-
agents: [],
|
|
27
|
-
claudeMdPath: claudeMdPath ?? undefined,
|
|
28
|
-
settingsPaths: settingsResult.settingsPaths,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
async readSkills(directory) {
|
|
32
|
-
if (!(await pathExists(directory))) {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
36
|
-
const skills = await Promise.all(entries
|
|
37
|
-
.filter((entry) => entry.isDirectory())
|
|
38
|
-
.map(async (entry) => {
|
|
39
|
-
const skillPath = path.join(directory, entry.name, 'SKILL.md');
|
|
40
|
-
if (!(await pathExists(skillPath))) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
const content = await fs.readFile(skillPath, 'utf8');
|
|
44
|
-
return {
|
|
45
|
-
name: entry.name,
|
|
46
|
-
description: extractMarkdownDescription(content),
|
|
47
|
-
path: skillPath,
|
|
48
|
-
source: 'skill',
|
|
49
|
-
};
|
|
50
|
-
}));
|
|
51
|
-
return skills.filter((skill) => skill !== null);
|
|
52
|
-
}
|
|
53
|
-
async readCommands(directory) {
|
|
54
|
-
if (!(await pathExists(directory))) {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
const commandFiles = await collectMarkdownFiles(directory);
|
|
58
|
-
const commands = await Promise.all(commandFiles.map(async (commandPath) => {
|
|
59
|
-
const content = await fs.readFile(commandPath, 'utf8');
|
|
60
|
-
return {
|
|
61
|
-
name: path.basename(commandPath, path.extname(commandPath)),
|
|
62
|
-
description: extractMarkdownDescription(content),
|
|
63
|
-
source: 'command',
|
|
64
|
-
path: commandPath,
|
|
65
|
-
};
|
|
66
|
-
}));
|
|
67
|
-
return commands.sort((left, right) => left.name.localeCompare(right.name));
|
|
68
|
-
}
|
|
69
|
-
async readSettings(claudeDirectory) {
|
|
70
|
-
const candidatePaths = [
|
|
71
|
-
path.join(claudeDirectory, 'settings.json'),
|
|
72
|
-
path.join(claudeDirectory, 'settings.local.json'),
|
|
73
|
-
];
|
|
74
|
-
const settingsPaths = [];
|
|
75
|
-
const hooks = [];
|
|
76
|
-
for (const candidatePath of candidatePaths) {
|
|
77
|
-
if (!(await pathExists(candidatePath))) {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
settingsPaths.push(candidatePath);
|
|
81
|
-
const content = await fs.readFile(candidatePath, 'utf8');
|
|
82
|
-
const parsed = JSON5.parse(content);
|
|
83
|
-
const hookEntries = Object.entries(parsed.hooks ?? {});
|
|
84
|
-
for (const [hookName, hookValue] of hookEntries) {
|
|
85
|
-
const hookMatchers = Array.isArray(hookValue) ? hookValue : [hookValue];
|
|
86
|
-
for (const hookMatcher of hookMatchers) {
|
|
87
|
-
if (!hookMatcher || typeof hookMatcher !== 'object') {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
const matcher = typeof hookMatcher.matcher === 'string'
|
|
91
|
-
? hookMatcher.matcher
|
|
92
|
-
: undefined;
|
|
93
|
-
const commandCount = Array.isArray(hookMatcher.hooks)
|
|
94
|
-
? (hookMatcher.hooks?.length ?? 0)
|
|
95
|
-
: 0;
|
|
96
|
-
hooks.push({
|
|
97
|
-
name: hookName,
|
|
98
|
-
matcher,
|
|
99
|
-
sourcePath: candidatePath,
|
|
100
|
-
commandCount,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
settingsPaths,
|
|
107
|
-
hooks,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
function extractMarkdownDescription(markdown) {
|
|
112
|
-
const lines = markdown
|
|
113
|
-
.split(/\r?\n/)
|
|
114
|
-
.map((line) => line.trim())
|
|
115
|
-
.filter(Boolean);
|
|
116
|
-
const descriptionLine = lines.find((line) => !line.startsWith('#') && !line.startsWith('---'));
|
|
117
|
-
return descriptionLine ?? 'No description provided.';
|
|
118
|
-
}
|
|
119
|
-
async function collectMarkdownFiles(directory) {
|
|
120
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
121
|
-
const files = await Promise.all(entries.map(async (entry) => {
|
|
122
|
-
const resolvedPath = path.join(directory, entry.name);
|
|
123
|
-
if (entry.isDirectory()) {
|
|
124
|
-
return collectMarkdownFiles(resolvedPath);
|
|
125
|
-
}
|
|
126
|
-
return entry.name.endsWith('.md') ? [resolvedPath] : [];
|
|
127
|
-
}));
|
|
128
|
-
return files.flat();
|
|
129
|
-
}
|
|
130
|
-
async function pathExists(candidatePath) {
|
|
131
|
-
try {
|
|
132
|
-
await fs.access(candidatePath);
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
async function findFirstExistingPath(candidatePaths) {
|
|
140
|
-
for (const candidatePath of candidatePaths) {
|
|
141
|
-
if (await pathExists(candidatePath)) {
|
|
142
|
-
return candidatePath;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
function skillsToCommands(skills) {
|
|
148
|
-
return skills.map((skill) => ({
|
|
149
|
-
name: skill.name,
|
|
150
|
-
description: skill.description,
|
|
151
|
-
source: 'skill',
|
|
152
|
-
path: skill.path,
|
|
153
|
-
}));
|
|
154
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { prompts } from '../prompts/registry.js';
|
|
2
|
-
import { evaluateBashCommand, extractBashCommand, } from '../safety/bash-safety.js';
|
|
3
|
-
/**
|
|
4
|
-
* Thin OpenCode orchestrator plugin with Claude Code specialist subagents.
|
|
5
|
-
*
|
|
6
|
-
* - Registers `claude-code` provider via a local shim over ai-sdk-provider-claude-code.
|
|
7
|
-
* - Creates one orchestrator agent (uses the user's default OpenCode model).
|
|
8
|
-
* - Creates 4 Claude Code subagents: planning + build × opus + sonnet.
|
|
9
|
-
* - Enforces bash safety via the permission.ask hook.
|
|
10
|
-
*
|
|
11
|
-
* NOTE: Claude Code `effort` is not configurable through OpenCode provider/model
|
|
12
|
-
* options at this time. The subagent prompts compensate by setting high-quality
|
|
13
|
-
* expectations directly.
|
|
14
|
-
*/
|
|
15
|
-
// Resolve the shim path at module load time so it is stable for the lifetime
|
|
16
|
-
// of the process. The compiled output for this file sits at dist/plugin/ and
|
|
17
|
-
// the shim at dist/providers/, so we walk up one level.
|
|
18
|
-
const claudeCodeShimUrl = new URL('../providers/claude-code-wrapper.js', import.meta.url).href;
|
|
19
|
-
export const OrchestratorPlugin = async () => {
|
|
20
|
-
return {
|
|
21
|
-
config: async (config) => {
|
|
22
|
-
config.provider ??= {};
|
|
23
|
-
config.agent ??= {};
|
|
24
|
-
// ── Provider ──────────────────────────────────────────────────────
|
|
25
|
-
// Uses a file:// shim so OpenCode's factory-finder heuristic sees only
|
|
26
|
-
// createClaudeCode and not createAPICallError from the upstream package.
|
|
27
|
-
config.provider['claude-code'] ??= {
|
|
28
|
-
npm: claudeCodeShimUrl,
|
|
29
|
-
models: {
|
|
30
|
-
opus: {
|
|
31
|
-
id: 'opus',
|
|
32
|
-
name: 'Claude Code Opus 4.6',
|
|
33
|
-
},
|
|
34
|
-
sonnet: {
|
|
35
|
-
id: 'sonnet',
|
|
36
|
-
name: 'Claude Code Sonnet 4.6',
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
// ── Orchestrator (uses user's default model — no model set) ───────
|
|
41
|
-
config.agent['opencode-orchestrator'] ??= {
|
|
42
|
-
description: 'CTO-level orchestrator that gathers context and delegates coding to Claude Code specialists.',
|
|
43
|
-
mode: 'primary',
|
|
44
|
-
color: '#D97757',
|
|
45
|
-
prompt: prompts.orchestrator,
|
|
46
|
-
permission: {
|
|
47
|
-
'*': 'deny',
|
|
48
|
-
read: 'allow',
|
|
49
|
-
grep: 'allow',
|
|
50
|
-
glob: 'allow',
|
|
51
|
-
list: 'allow',
|
|
52
|
-
webfetch: 'allow',
|
|
53
|
-
question: 'allow',
|
|
54
|
-
todowrite: 'allow',
|
|
55
|
-
todoread: 'allow',
|
|
56
|
-
task: 'allow',
|
|
57
|
-
bash: 'deny',
|
|
58
|
-
edit: 'deny',
|
|
59
|
-
skill: 'deny',
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
// ── Planning subagents ────────────────────────────────────────────
|
|
63
|
-
// Claude Code tools (Bash, Read, Write, Edit, …) are executed internally
|
|
64
|
-
// by the claude CLI subprocess and streamed back with providerExecuted:true.
|
|
65
|
-
// OpenCode's own tools must not be advertised to these agents.
|
|
66
|
-
const claudeCodePermissions = {
|
|
67
|
-
'*': 'deny',
|
|
68
|
-
};
|
|
69
|
-
config.agent['claude-code-planning-opus'] ??= {
|
|
70
|
-
description: 'Claude Code Opus specialist for investigation, architecture, and planning.',
|
|
71
|
-
model: 'claude-code/opus',
|
|
72
|
-
mode: 'subagent',
|
|
73
|
-
color: 'info',
|
|
74
|
-
prompt: prompts.planningAgent,
|
|
75
|
-
permission: { ...claudeCodePermissions },
|
|
76
|
-
};
|
|
77
|
-
config.agent['claude-code-planning-sonnet'] ??= {
|
|
78
|
-
description: 'Claude Code Sonnet specialist for lighter investigation and planning.',
|
|
79
|
-
model: 'claude-code/sonnet',
|
|
80
|
-
mode: 'subagent',
|
|
81
|
-
color: 'info',
|
|
82
|
-
prompt: prompts.planningAgent,
|
|
83
|
-
permission: { ...claudeCodePermissions },
|
|
84
|
-
};
|
|
85
|
-
// ── Build subagents ───────────────────────────────────────────────
|
|
86
|
-
config.agent['claude-code-build-opus'] ??= {
|
|
87
|
-
description: 'Claude Code Opus specialist for implementation and validation.',
|
|
88
|
-
model: 'claude-code/opus',
|
|
89
|
-
mode: 'subagent',
|
|
90
|
-
color: 'success',
|
|
91
|
-
prompt: prompts.buildAgent,
|
|
92
|
-
permission: { ...claudeCodePermissions },
|
|
93
|
-
};
|
|
94
|
-
config.agent['claude-code-build-sonnet'] ??= {
|
|
95
|
-
description: 'Claude Code Sonnet specialist for lighter implementation tasks.',
|
|
96
|
-
model: 'claude-code/sonnet',
|
|
97
|
-
mode: 'subagent',
|
|
98
|
-
color: 'success',
|
|
99
|
-
prompt: prompts.buildAgent,
|
|
100
|
-
permission: { ...claudeCodePermissions },
|
|
101
|
-
};
|
|
102
|
-
},
|
|
103
|
-
// ── Bash safety via permission.ask hook ────────────────────────────
|
|
104
|
-
// Handles both v1 Permission ({ type, pattern }) and v2 PermissionRequest
|
|
105
|
-
// ({ permission, patterns }) via runtime narrowing in extractBashCommand.
|
|
106
|
-
'permission.ask': async (input, output) => {
|
|
107
|
-
const command = extractBashCommand(input);
|
|
108
|
-
if (command === null)
|
|
109
|
-
return;
|
|
110
|
-
const result = evaluateBashCommand(command);
|
|
111
|
-
if (!result.allowed) {
|
|
112
|
-
output.status = 'deny';
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thin re-export shim for ai-sdk-provider-claude-code.
|
|
3
|
-
*
|
|
4
|
-
* OpenCode's provider loader finds the provider factory by scanning
|
|
5
|
-
* `Object.keys(module).find(key => key.startsWith("create"))`. The upstream
|
|
6
|
-
* package exports `createAPICallError` before `createClaudeCode`, so OpenCode
|
|
7
|
-
* picks the wrong function and `.languageModel` ends up undefined.
|
|
8
|
-
*
|
|
9
|
-
* This shim re-exports only `createClaudeCode`, making it the sole "create*"
|
|
10
|
-
* export. The plugin references this file via a `file://` URL so the upstream
|
|
11
|
-
* package is still the actual implementation.
|
|
12
|
-
*/
|
|
13
|
-
export { createClaudeCode } from 'ai-sdk-provider-claude-code';
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thin re-export shim for ai-sdk-provider-claude-code.
|
|
3
|
-
*
|
|
4
|
-
* OpenCode's provider loader finds the provider factory by scanning
|
|
5
|
-
* `Object.keys(module).find(key => key.startsWith("create"))`. The upstream
|
|
6
|
-
* package exports `createAPICallError` before `createClaudeCode`, so OpenCode
|
|
7
|
-
* picks the wrong function and `.languageModel` ends up undefined.
|
|
8
|
-
*
|
|
9
|
-
* This shim re-exports only `createClaudeCode`, making it the sole "create*"
|
|
10
|
-
* export. The plugin references this file via a `file://` URL so the upstream
|
|
11
|
-
* package is still the actual implementation.
|
|
12
|
-
*/
|
|
13
|
-
export { createClaudeCode } from 'ai-sdk-provider-claude-code';
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal bash command safety layer.
|
|
3
|
-
* Denies known-dangerous patterns; allows everything else.
|
|
4
|
-
*/
|
|
5
|
-
export type BashSafetyResult = {
|
|
6
|
-
allowed: true;
|
|
7
|
-
} | {
|
|
8
|
-
allowed: false;
|
|
9
|
-
reason: string;
|
|
10
|
-
};
|
|
11
|
-
export declare function evaluateBashCommand(command: string): BashSafetyResult;
|
|
12
|
-
/**
|
|
13
|
-
* Extract the bash command string from a permission hook input,
|
|
14
|
-
* handling both SDK payload shapes:
|
|
15
|
-
*
|
|
16
|
-
* v1 Permission: { type: string, pattern?: string | string[], metadata }
|
|
17
|
-
* v2 PermissionRequest: { permission: string, patterns: string[], metadata }
|
|
18
|
-
*
|
|
19
|
-
* Returns `null` when the input is not a bash permission request.
|
|
20
|
-
*/
|
|
21
|
-
export declare function extractBashCommand(input: Record<string, unknown>): string | null;
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal bash command safety layer.
|
|
3
|
-
* Denies known-dangerous patterns; allows everything else.
|
|
4
|
-
*/
|
|
5
|
-
const DENY_PATTERNS = [
|
|
6
|
-
{ pattern: 'rm -rf /', reason: 'Destructive: rm -rf / is not allowed.' },
|
|
7
|
-
{
|
|
8
|
-
pattern: 'git push --force',
|
|
9
|
-
reason: 'Force push is not allowed.',
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
pattern: 'git reset --hard',
|
|
13
|
-
reason: 'git reset --hard is not allowed. Use a safer alternative.',
|
|
14
|
-
},
|
|
15
|
-
];
|
|
16
|
-
export function evaluateBashCommand(command) {
|
|
17
|
-
for (const { pattern, reason } of DENY_PATTERNS) {
|
|
18
|
-
if (command.includes(pattern)) {
|
|
19
|
-
return { allowed: false, reason };
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return { allowed: true };
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Extract the bash command string from a permission hook input,
|
|
26
|
-
* handling both SDK payload shapes:
|
|
27
|
-
*
|
|
28
|
-
* v1 Permission: { type: string, pattern?: string | string[], metadata }
|
|
29
|
-
* v2 PermissionRequest: { permission: string, patterns: string[], metadata }
|
|
30
|
-
*
|
|
31
|
-
* Returns `null` when the input is not a bash permission request.
|
|
32
|
-
*/
|
|
33
|
-
export function extractBashCommand(input) {
|
|
34
|
-
// Determine the permission kind from whichever field is present.
|
|
35
|
-
const kind = typeof input['permission'] === 'string'
|
|
36
|
-
? input['permission']
|
|
37
|
-
: typeof input['type'] === 'string'
|
|
38
|
-
? input['type']
|
|
39
|
-
: null;
|
|
40
|
-
if (kind !== 'bash')
|
|
41
|
-
return null;
|
|
42
|
-
// Prefer an explicit command in metadata regardless of shape.
|
|
43
|
-
const meta = input['metadata'];
|
|
44
|
-
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
45
|
-
const cmd = meta['command'];
|
|
46
|
-
if (typeof cmd === 'string' && cmd.length > 0)
|
|
47
|
-
return cmd;
|
|
48
|
-
}
|
|
49
|
-
// v2: patterns is always string[]
|
|
50
|
-
const patterns = input['patterns'];
|
|
51
|
-
if (Array.isArray(patterns) && patterns.length > 0) {
|
|
52
|
-
return patterns.join(' ');
|
|
53
|
-
}
|
|
54
|
-
// v1: pattern may be string or string[]
|
|
55
|
-
const pattern = input['pattern'];
|
|
56
|
-
if (typeof pattern === 'string' && pattern.length > 0)
|
|
57
|
-
return pattern;
|
|
58
|
-
if (Array.isArray(pattern) && pattern.length > 0) {
|
|
59
|
-
return pattern.join(' ');
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|