@doingdev/opencode-claude-manager-plugin 0.1.35 → 0.1.42
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/dist/claude/claude-agent-sdk-adapter.js +1 -0
- package/dist/manager/git-operations.d.ts +10 -1
- package/dist/manager/git-operations.js +18 -3
- package/dist/manager/persistent-manager.d.ts +16 -1
- package/dist/manager/persistent-manager.js +14 -2
- 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/agent-hierarchy.d.ts +1 -1
- package/dist/plugin/agent-hierarchy.js +1 -1
- package/dist/plugin/claude-manager.plugin.js +43 -10
- package/dist/plugin/orchestrator.plugin.d.ts +2 -0
- package/dist/plugin/orchestrator.plugin.js +116 -0
- package/dist/prompts/registry.js +8 -4
- package/dist/providers/claude-code-wrapper.d.ts +13 -0
- package/dist/providers/claude-code-wrapper.js +13 -0
- package/dist/safety/bash-safety.d.ts +21 -0
- package/dist/safety/bash-safety.js +62 -0
- package/dist/src/claude/claude-agent-sdk-adapter.d.ts +27 -0
- package/dist/src/claude/claude-agent-sdk-adapter.js +517 -0
- package/dist/src/claude/claude-session.service.d.ts +10 -0
- package/dist/src/claude/claude-session.service.js +18 -0
- package/dist/src/claude/session-live-tailer.d.ts +51 -0
- package/dist/src/claude/session-live-tailer.js +269 -0
- package/dist/src/claude/tool-approval-manager.d.ts +27 -0
- package/dist/src/claude/tool-approval-manager.js +232 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +4 -0
- package/dist/src/manager/context-tracker.d.ts +33 -0
- package/dist/src/manager/context-tracker.js +106 -0
- package/dist/src/manager/git-operations.d.ts +12 -0
- package/dist/src/manager/git-operations.js +76 -0
- package/dist/src/manager/persistent-manager.d.ts +77 -0
- package/dist/src/manager/persistent-manager.js +170 -0
- package/dist/src/manager/session-controller.d.ts +44 -0
- package/dist/src/manager/session-controller.js +147 -0
- package/dist/src/plugin/agent-hierarchy.d.ts +60 -0
- package/dist/src/plugin/agent-hierarchy.js +157 -0
- package/dist/src/plugin/claude-manager.plugin.d.ts +2 -0
- package/dist/src/plugin/claude-manager.plugin.js +563 -0
- package/dist/src/plugin/service-factory.d.ts +12 -0
- package/dist/src/plugin/service-factory.js +38 -0
- package/dist/src/prompts/registry.d.ts +11 -0
- package/dist/src/prompts/registry.js +260 -0
- package/dist/src/state/file-run-state-store.d.ts +14 -0
- package/dist/src/state/file-run-state-store.js +85 -0
- package/dist/src/state/transcript-store.d.ts +15 -0
- package/dist/src/state/transcript-store.js +44 -0
- package/dist/src/types/contracts.d.ts +200 -0
- package/dist/src/types/contracts.js +1 -0
- package/dist/src/util/fs-helpers.d.ts +2 -0
- package/dist/src/util/fs-helpers.js +10 -0
- package/dist/src/util/project-context.d.ts +10 -0
- package/dist/src/util/project-context.js +105 -0
- package/dist/src/util/transcript-append.d.ts +7 -0
- package/dist/src/util/transcript-append.js +29 -0
- package/dist/test/claude-agent-sdk-adapter.test.d.ts +1 -0
- package/dist/test/claude-agent-sdk-adapter.test.js +459 -0
- package/dist/test/claude-manager.plugin.test.d.ts +1 -0
- package/dist/test/claude-manager.plugin.test.js +331 -0
- package/dist/test/context-tracker.test.d.ts +1 -0
- package/dist/test/context-tracker.test.js +138 -0
- package/dist/test/file-run-state-store.test.d.ts +1 -0
- package/dist/test/file-run-state-store.test.js +82 -0
- package/dist/test/git-operations.test.d.ts +1 -0
- package/dist/test/git-operations.test.js +90 -0
- package/dist/test/persistent-manager.test.d.ts +1 -0
- package/dist/test/persistent-manager.test.js +208 -0
- package/dist/test/project-context.test.d.ts +1 -0
- package/dist/test/project-context.test.js +92 -0
- package/dist/test/prompt-registry.test.d.ts +1 -0
- package/dist/test/prompt-registry.test.js +256 -0
- package/dist/test/session-controller.test.d.ts +1 -0
- package/dist/test/session-controller.test.js +149 -0
- package/dist/test/session-live-tailer.test.d.ts +1 -0
- package/dist/test/session-live-tailer.test.js +313 -0
- package/dist/test/tool-approval-manager.test.d.ts +1 -0
- package/dist/test/tool-approval-manager.test.js +264 -0
- package/dist/test/transcript-append.test.d.ts +1 -0
- package/dist/test/transcript-append.test.js +37 -0
- package/dist/test/transcript-store.test.d.ts +1 -0
- package/dist/test/transcript-store.test.js +50 -0
- package/dist/types/contracts.d.ts +1 -1
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +11 -0
- package/package.json +2 -2
|
@@ -162,6 +162,7 @@ export class ClaudeAgentSdkAdapter {
|
|
|
162
162
|
forkSession: input.forkSession,
|
|
163
163
|
persistSession: input.persistSession,
|
|
164
164
|
includePartialMessages: input.includePartialMessages,
|
|
165
|
+
enableFileCheckpointing: false,
|
|
165
166
|
settingSources: input.settingSources,
|
|
166
167
|
maxTurns: input.maxTurns,
|
|
167
168
|
model: input.model,
|
|
@@ -2,10 +2,19 @@ import type { GitDiffResult, GitOperationResult } from '../types/contracts.js';
|
|
|
2
2
|
export declare class GitOperations {
|
|
3
3
|
private readonly cwd;
|
|
4
4
|
constructor(cwd: string);
|
|
5
|
-
diff(
|
|
5
|
+
diff(options?: {
|
|
6
|
+
paths?: string[];
|
|
7
|
+
staged?: boolean;
|
|
8
|
+
ref?: string;
|
|
9
|
+
}): Promise<GitDiffResult>;
|
|
6
10
|
diffStat(): Promise<string>;
|
|
7
11
|
commit(message: string): Promise<GitOperationResult>;
|
|
8
12
|
resetHard(): Promise<GitOperationResult>;
|
|
13
|
+
status(): Promise<{
|
|
14
|
+
output: string;
|
|
15
|
+
isClean: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
log(count?: number): Promise<string>;
|
|
9
18
|
currentBranch(): Promise<string>;
|
|
10
19
|
recentCommits(count?: number): Promise<string>;
|
|
11
20
|
private git;
|
|
@@ -6,10 +6,15 @@ export class GitOperations {
|
|
|
6
6
|
constructor(cwd) {
|
|
7
7
|
this.cwd = cwd;
|
|
8
8
|
}
|
|
9
|
-
async diff() {
|
|
9
|
+
async diff(options = {}) {
|
|
10
|
+
const ref = options.staged ? '--cached' : (options.ref ?? 'HEAD');
|
|
11
|
+
const args = ['diff', ref];
|
|
12
|
+
if (options.paths && options.paths.length > 0) {
|
|
13
|
+
args.push('--', ...options.paths);
|
|
14
|
+
}
|
|
10
15
|
const [diffText, statOutput] = await Promise.all([
|
|
11
|
-
this.git(
|
|
12
|
-
this.git([
|
|
16
|
+
this.git(args),
|
|
17
|
+
this.git([...args, '--stat']),
|
|
13
18
|
]);
|
|
14
19
|
const stats = parseStatLine(statOutput);
|
|
15
20
|
return {
|
|
@@ -50,6 +55,16 @@ export class GitOperations {
|
|
|
50
55
|
};
|
|
51
56
|
}
|
|
52
57
|
}
|
|
58
|
+
async status() {
|
|
59
|
+
const output = await this.git(['status', '-s']);
|
|
60
|
+
return {
|
|
61
|
+
output,
|
|
62
|
+
isClean: output.trim().length === 0,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async log(count = 5) {
|
|
66
|
+
return this.git(['log', '--oneline', `-${count}`]);
|
|
67
|
+
}
|
|
53
68
|
async currentBranch() {
|
|
54
69
|
const branch = await this.git(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
55
70
|
return branch.trim();
|
|
@@ -35,11 +35,26 @@ export declare class PersistentManager {
|
|
|
35
35
|
/**
|
|
36
36
|
* Get the current git diff.
|
|
37
37
|
*/
|
|
38
|
-
gitDiff(
|
|
38
|
+
gitDiff(options?: {
|
|
39
|
+
paths?: string[];
|
|
40
|
+
staged?: boolean;
|
|
41
|
+
ref?: string;
|
|
42
|
+
}): Promise<GitDiffResult>;
|
|
39
43
|
/**
|
|
40
44
|
* Commit all current changes.
|
|
41
45
|
*/
|
|
42
46
|
gitCommit(message: string): Promise<GitOperationResult>;
|
|
47
|
+
/**
|
|
48
|
+
* Get git status summary.
|
|
49
|
+
*/
|
|
50
|
+
gitStatus(): Promise<{
|
|
51
|
+
output: string;
|
|
52
|
+
isClean: boolean;
|
|
53
|
+
}>;
|
|
54
|
+
/**
|
|
55
|
+
* Get recent commit log.
|
|
56
|
+
*/
|
|
57
|
+
gitLog(count?: number): Promise<string>;
|
|
43
58
|
/**
|
|
44
59
|
* Hard reset to discard all uncommitted changes.
|
|
45
60
|
*/
|
|
@@ -35,8 +35,8 @@ export class PersistentManager {
|
|
|
35
35
|
/**
|
|
36
36
|
* Get the current git diff.
|
|
37
37
|
*/
|
|
38
|
-
async gitDiff() {
|
|
39
|
-
return this.gitOps.diff();
|
|
38
|
+
async gitDiff(options = {}) {
|
|
39
|
+
return this.gitOps.diff(options);
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
42
|
* Commit all current changes.
|
|
@@ -44,6 +44,18 @@ export class PersistentManager {
|
|
|
44
44
|
async gitCommit(message) {
|
|
45
45
|
return this.gitOps.commit(message);
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Get git status summary.
|
|
49
|
+
*/
|
|
50
|
+
async gitStatus() {
|
|
51
|
+
return this.gitOps.status();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get recent commit log.
|
|
55
|
+
*/
|
|
56
|
+
async gitLog(count = 5) {
|
|
57
|
+
return this.gitOps.log(count);
|
|
58
|
+
}
|
|
47
59
|
/**
|
|
48
60
|
* Hard reset to discard all uncommitted changes.
|
|
49
61
|
*/
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
}
|
|
@@ -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 ["explore", "implement", "compact_context", "clear_session", "session_health", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "approval_policy", "approval_decisions", "approval_update"];
|
|
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", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update"];
|
|
15
15
|
type ToolPermission = 'allow' | 'ask' | 'deny';
|
|
16
16
|
type AgentPermission = {
|
|
17
17
|
'*'?: ToolPermission;
|
|
@@ -30,7 +30,7 @@ const ENGINEER_PLAN_TOOL_IDS = ['explore', ...ENGINEER_SHARED_TOOL_IDS];
|
|
|
30
30
|
/** Tools for the engineer_build wrapper (build-mode send + shared) */
|
|
31
31
|
const ENGINEER_BUILD_TOOL_IDS = ['implement', ...ENGINEER_SHARED_TOOL_IDS];
|
|
32
32
|
/** Git tools — owned by CTO */
|
|
33
|
-
const GIT_TOOL_IDS = ['git_diff', 'git_commit', 'git_reset'];
|
|
33
|
+
const GIT_TOOL_IDS = ['git_diff', 'git_commit', 'git_reset', 'git_status', 'git_log'];
|
|
34
34
|
/** Approval tools — owned by CTO */
|
|
35
35
|
const APPROVAL_TOOL_IDS = ['approval_policy', 'approval_decisions', 'approval_update'];
|
|
36
36
|
/** All restricted tool IDs (union of all domain groups) */
|
|
@@ -283,13 +283,27 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
283
283
|
},
|
|
284
284
|
}),
|
|
285
285
|
git_diff: tool({
|
|
286
|
-
description: '
|
|
286
|
+
description: 'Show diff of uncommitted changes. ' +
|
|
287
|
+
'Use paths to filter to specific files/dirs. ' +
|
|
288
|
+
'Use staged=true to see staged changes. ' +
|
|
289
|
+
'Use ref to compare against a branch/tag/commit (e.g., ref="main").',
|
|
287
290
|
args: {
|
|
288
|
-
|
|
291
|
+
paths: tool.schema.string().array().optional(),
|
|
292
|
+
staged: tool.schema.boolean().optional(),
|
|
293
|
+
ref: tool.schema.string().optional(),
|
|
289
294
|
},
|
|
290
|
-
async execute(
|
|
291
|
-
annotateToolRun(context, 'Running git diff', {
|
|
292
|
-
|
|
295
|
+
async execute(args, context) {
|
|
296
|
+
annotateToolRun(context, 'Running git diff', {
|
|
297
|
+
paths: args.paths,
|
|
298
|
+
staged: args.staged,
|
|
299
|
+
ref: args.ref,
|
|
300
|
+
});
|
|
301
|
+
const paths = args.paths?.filter((p) => p !== undefined);
|
|
302
|
+
const result = await services.manager.gitDiff({
|
|
303
|
+
paths,
|
|
304
|
+
staged: args.staged,
|
|
305
|
+
ref: args.ref,
|
|
306
|
+
});
|
|
293
307
|
return JSON.stringify(result, null, 2);
|
|
294
308
|
},
|
|
295
309
|
}),
|
|
@@ -297,7 +311,6 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
297
311
|
description: 'Stage all changes and commit with the given message.',
|
|
298
312
|
args: {
|
|
299
313
|
message: tool.schema.string().min(1),
|
|
300
|
-
cwd: tool.schema.string().optional(),
|
|
301
314
|
},
|
|
302
315
|
async execute(args, context) {
|
|
303
316
|
annotateToolRun(context, 'Committing changes', {
|
|
@@ -308,16 +321,36 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
308
321
|
},
|
|
309
322
|
}),
|
|
310
323
|
git_reset: tool({
|
|
311
|
-
description: '
|
|
312
|
-
args: {
|
|
313
|
-
cwd: tool.schema.string().optional(),
|
|
314
|
-
},
|
|
324
|
+
description: 'Discard all uncommitted changes: runs git reset --hard HEAD and git clean -fd.',
|
|
325
|
+
args: {},
|
|
315
326
|
async execute(_args, context) {
|
|
316
327
|
annotateToolRun(context, 'Resetting working directory', {});
|
|
317
328
|
const result = await services.manager.gitReset();
|
|
318
329
|
return JSON.stringify(result, null, 2);
|
|
319
330
|
},
|
|
320
331
|
}),
|
|
332
|
+
git_status: tool({
|
|
333
|
+
description: 'Show working tree status — lists changed files in short format. ' +
|
|
334
|
+
'Returns isClean=true if nothing changed.',
|
|
335
|
+
args: {},
|
|
336
|
+
async execute(_args, context) {
|
|
337
|
+
annotateToolRun(context, 'Checking git status', {});
|
|
338
|
+
const result = await services.manager.gitStatus();
|
|
339
|
+
return JSON.stringify(result, null, 2);
|
|
340
|
+
},
|
|
341
|
+
}),
|
|
342
|
+
git_log: tool({
|
|
343
|
+
description: 'Show recent commits in short format. ' +
|
|
344
|
+
'Default shows last 5 commits. Use count to change.',
|
|
345
|
+
args: {
|
|
346
|
+
count: tool.schema.number().optional(),
|
|
347
|
+
},
|
|
348
|
+
async execute(args, context) {
|
|
349
|
+
annotateToolRun(context, 'Fetching git log', { count: args.count });
|
|
350
|
+
const result = await services.manager.gitLog(args.count ?? 5);
|
|
351
|
+
return result;
|
|
352
|
+
},
|
|
353
|
+
}),
|
|
321
354
|
clear_session: tool({
|
|
322
355
|
description: 'Clear the active session to start fresh. ' +
|
|
323
356
|
'Use when context is full or starting a new task.',
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
};
|
package/dist/prompts/registry.js
CHANGED
|
@@ -90,9 +90,11 @@ export const managerPromptRegistry = {
|
|
|
90
90
|
'After each delegation:',
|
|
91
91
|
'1. git_diff — read the FULL diff. Check for unintended changes, missing tests,',
|
|
92
92
|
' style violations.',
|
|
93
|
-
'2.
|
|
94
|
-
'3.
|
|
95
|
-
'4. If
|
|
93
|
+
'2. git_status — check what files changed (quick overview)',
|
|
94
|
+
'3. git_log -n 5 — see recent commits (understand context)',
|
|
95
|
+
'4. If correct: spawn `engineer_build` to run tests/lint/typecheck.',
|
|
96
|
+
'5. If tests pass: git_commit to checkpoint.',
|
|
97
|
+
'6. If wrong: spawn `engineer_build` with a specific correction.',
|
|
96
98
|
' On second failure: git_reset and rewrite the prompt from scratch.',
|
|
97
99
|
' Never send three corrections for the same problem.',
|
|
98
100
|
'',
|
|
@@ -116,7 +118,9 @@ export const managerPromptRegistry = {
|
|
|
116
118
|
'## Tools reference',
|
|
117
119
|
'todowrite / todoread — track multi-step work',
|
|
118
120
|
'question — ask the user structured questions with options',
|
|
119
|
-
'git_diff — review
|
|
121
|
+
'git_diff — review changes (filter with paths, staged, ref args)',
|
|
122
|
+
'git_status — quick overview of changed files',
|
|
123
|
+
'git_log — recent commits (default: last 5)',
|
|
120
124
|
'git_commit — stage all + commit',
|
|
121
125
|
'git_reset — hard reset + clean (destructive)',
|
|
122
126
|
'approval_policy — view tool approval rules',
|
|
@@ -0,0 +1,13 @@
|
|
|
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';
|
|
@@ -0,0 +1,13 @@
|
|
|
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';
|
|
@@ -0,0 +1,21 @@
|
|
|
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;
|