@doingdev/opencode-claude-manager-plugin 0.1.22 → 0.1.26
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 +129 -40
- package/dist/claude/claude-agent-sdk-adapter.d.ts +27 -0
- package/dist/claude/claude-agent-sdk-adapter.js +520 -0
- package/dist/claude/claude-session.service.d.ts +15 -0
- package/dist/claude/claude-session.service.js +23 -0
- package/dist/claude/session-live-tailer.d.ts +51 -0
- package/dist/claude/session-live-tailer.js +269 -0
- package/dist/claude/tool-approval-manager.d.ts +27 -0
- package/dist/claude/tool-approval-manager.js +238 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +4 -5
- package/dist/manager/context-tracker.d.ts +33 -0
- package/dist/manager/context-tracker.js +108 -0
- package/dist/manager/git-operations.d.ts +12 -0
- package/dist/manager/git-operations.js +76 -0
- package/dist/manager/persistent-manager.d.ts +74 -0
- package/dist/manager/persistent-manager.js +167 -0
- package/dist/manager/session-controller.d.ts +45 -0
- package/dist/manager/session-controller.js +147 -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/agent-hierarchy.d.ts +47 -0
- package/dist/plugin/agent-hierarchy.js +110 -0
- package/dist/plugin/claude-manager.plugin.d.ts +2 -0
- package/dist/plugin/claude-manager.plugin.js +490 -0
- package/dist/plugin/orchestrator.plugin.js +4 -2
- package/dist/plugin/service-factory.d.ts +12 -0
- package/dist/plugin/service-factory.js +41 -0
- package/dist/prompts/registry.d.ts +2 -8
- package/dist/prompts/registry.js +203 -29
- package/dist/state/file-run-state-store.d.ts +14 -0
- package/dist/state/file-run-state-store.js +87 -0
- package/dist/state/transcript-store.d.ts +15 -0
- package/dist/state/transcript-store.js +44 -0
- package/dist/types/contracts.d.ts +216 -0
- package/dist/types/contracts.js +1 -0
- package/dist/util/fs-helpers.d.ts +2 -0
- package/dist/util/fs-helpers.js +12 -0
- package/dist/util/transcript-append.d.ts +7 -0
- package/dist/util/transcript-append.js +29 -0
- package/package.json +5 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent hierarchy configuration for the CTO → Manager → Engineer architecture.
|
|
3
|
+
*
|
|
4
|
+
* CTO (cto) — direction-setting, orchestration, spawns managers
|
|
5
|
+
* Manager (manager) — operates Claude Code engineer via persistent session
|
|
6
|
+
* Engineer — the Claude Code session itself (prompt only, no OpenCode agent)
|
|
7
|
+
*/
|
|
8
|
+
import type { ManagerPromptRegistry } from '../types/contracts.js';
|
|
9
|
+
export declare const AGENT_CTO = "cto";
|
|
10
|
+
export declare const AGENT_MANAGER = "manager";
|
|
11
|
+
/** All restricted tool IDs (union of engineer + git + approval) */
|
|
12
|
+
export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["engineer_send", "engineer_compact", "engineer_clear", "engineer_status", "engineer_metadata", "engineer_sessions", "engineer_runs", "git_diff", "git_commit", "git_reset", "approval_policy", "approval_decisions", "approval_update"];
|
|
13
|
+
type ToolPermission = 'allow' | 'ask' | 'deny';
|
|
14
|
+
type AgentPermission = {
|
|
15
|
+
'*'?: ToolPermission;
|
|
16
|
+
read?: ToolPermission;
|
|
17
|
+
grep?: ToolPermission;
|
|
18
|
+
glob?: ToolPermission;
|
|
19
|
+
codesearch?: ToolPermission;
|
|
20
|
+
webfetch?: ToolPermission;
|
|
21
|
+
websearch?: ToolPermission;
|
|
22
|
+
/** OpenCode built-in: manage session todo list */
|
|
23
|
+
todowrite?: ToolPermission;
|
|
24
|
+
/** OpenCode built-in: read session todo list */
|
|
25
|
+
todoread?: ToolPermission;
|
|
26
|
+
/** OpenCode built-in: ask the user structured questions with options */
|
|
27
|
+
question?: ToolPermission;
|
|
28
|
+
bash?: ToolPermission | Record<string, ToolPermission>;
|
|
29
|
+
[tool: string]: ToolPermission | Record<string, ToolPermission> | undefined;
|
|
30
|
+
};
|
|
31
|
+
export declare function buildCtoAgentConfig(prompts: ManagerPromptRegistry): {
|
|
32
|
+
description: string;
|
|
33
|
+
mode: "primary";
|
|
34
|
+
color: string;
|
|
35
|
+
permission: AgentPermission;
|
|
36
|
+
prompt: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function buildManagerAgentConfig(prompts: ManagerPromptRegistry): {
|
|
39
|
+
description: string;
|
|
40
|
+
mode: "subagent";
|
|
41
|
+
color: string;
|
|
42
|
+
permission: AgentPermission;
|
|
43
|
+
prompt: string;
|
|
44
|
+
};
|
|
45
|
+
/** Deny all restricted tools at the global level so only designated agents can use them. */
|
|
46
|
+
export declare function denyRestrictedToolsGlobally(permissions: Record<string, ToolPermission>): void;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent hierarchy configuration for the CTO → Manager → Engineer architecture.
|
|
3
|
+
*
|
|
4
|
+
* CTO (cto) — direction-setting, orchestration, spawns managers
|
|
5
|
+
* Manager (manager) — operates Claude Code engineer via persistent session
|
|
6
|
+
* Engineer — the Claude Code session itself (prompt only, no OpenCode agent)
|
|
7
|
+
*/
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Agent names
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
export const AGENT_CTO = 'cto';
|
|
12
|
+
export const AGENT_MANAGER = 'manager';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Tool IDs — grouped by domain
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/** Engineer-facing session tools */
|
|
17
|
+
const ENGINEER_TOOL_IDS = [
|
|
18
|
+
'engineer_send',
|
|
19
|
+
'engineer_compact',
|
|
20
|
+
'engineer_clear',
|
|
21
|
+
'engineer_status',
|
|
22
|
+
'engineer_metadata',
|
|
23
|
+
'engineer_sessions',
|
|
24
|
+
'engineer_runs',
|
|
25
|
+
];
|
|
26
|
+
/** Git tools */
|
|
27
|
+
const GIT_TOOL_IDS = ['git_diff', 'git_commit', 'git_reset'];
|
|
28
|
+
/** Approval tools */
|
|
29
|
+
const APPROVAL_TOOL_IDS = [
|
|
30
|
+
'approval_policy',
|
|
31
|
+
'approval_decisions',
|
|
32
|
+
'approval_update',
|
|
33
|
+
];
|
|
34
|
+
/** All restricted tool IDs (union of engineer + git + approval) */
|
|
35
|
+
export const ALL_RESTRICTED_TOOL_IDS = [
|
|
36
|
+
...ENGINEER_TOOL_IDS,
|
|
37
|
+
...GIT_TOOL_IDS,
|
|
38
|
+
...APPROVAL_TOOL_IDS,
|
|
39
|
+
];
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Shared read-only tool permissions
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
const READONLY_TOOLS = {
|
|
44
|
+
read: 'allow',
|
|
45
|
+
grep: 'allow',
|
|
46
|
+
glob: 'allow',
|
|
47
|
+
codesearch: 'allow',
|
|
48
|
+
webfetch: 'allow',
|
|
49
|
+
websearch: 'allow',
|
|
50
|
+
todowrite: 'allow',
|
|
51
|
+
todoread: 'allow',
|
|
52
|
+
question: 'allow',
|
|
53
|
+
};
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Permission builders
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
/** CTO: orchestration only — explicitly denies every restricted tool, no edit, no bash. */
|
|
58
|
+
function buildCtoPermissions() {
|
|
59
|
+
const denied = {};
|
|
60
|
+
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
61
|
+
denied[toolId] = 'deny';
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
'*': 'deny',
|
|
65
|
+
...READONLY_TOOLS,
|
|
66
|
+
...denied,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Manager: full restricted tool surface + read-only codebase tools. No edit, no bash. */
|
|
70
|
+
function buildManagerPermissions() {
|
|
71
|
+
const allowed = {};
|
|
72
|
+
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
73
|
+
allowed[toolId] = 'allow';
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
'*': 'deny',
|
|
77
|
+
...READONLY_TOOLS,
|
|
78
|
+
...allowed,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Agent config builders
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
export function buildCtoAgentConfig(prompts) {
|
|
85
|
+
return {
|
|
86
|
+
description: 'CTO agent that sets direction and orchestrates work by spawning manager subagents. Does not operate Claude Code directly.',
|
|
87
|
+
mode: 'primary',
|
|
88
|
+
color: '#D97757',
|
|
89
|
+
permission: buildCtoPermissions(),
|
|
90
|
+
prompt: prompts.ctoSystemPrompt,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function buildManagerAgentConfig(prompts) {
|
|
94
|
+
return {
|
|
95
|
+
description: 'Manager agent that operates Claude Code through a persistent session, reviews work via git diff, and commits/resets changes.',
|
|
96
|
+
mode: 'subagent',
|
|
97
|
+
color: '#D97757',
|
|
98
|
+
permission: buildManagerPermissions(),
|
|
99
|
+
prompt: prompts.managerSystemPrompt,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Global permission helper
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
/** Deny all restricted tools at the global level so only designated agents can use them. */
|
|
106
|
+
export function denyRestrictedToolsGlobally(permissions) {
|
|
107
|
+
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
108
|
+
permissions[toolId] ??= 'deny';
|
|
109
|
+
}
|
|
110
|
+
}
|