@doingdev/opencode-claude-manager-plugin 0.1.62 → 0.1.65
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 +106 -120
- package/dist/claude/claude-agent-sdk-adapter.js +1 -1
- package/dist/manager/team-orchestrator.d.ts +5 -0
- package/dist/manager/team-orchestrator.js +10 -0
- package/dist/plugin/agents/common.d.ts +0 -1
- package/dist/plugin/agents/common.js +1 -1
- package/dist/plugin/agents/index.d.ts +2 -3
- package/dist/plugin/agents/index.js +2 -2
- package/dist/plugin/claude-manager.plugin.js +207 -1
- package/dist/plugin/service-factory.d.ts +8 -0
- package/dist/plugin/service-factory.js +32 -0
- package/dist/src/manager/team-orchestrator.d.ts +5 -0
- package/dist/src/manager/team-orchestrator.js +10 -0
- package/dist/src/plugin/agents/common.d.ts +0 -1
- package/dist/src/plugin/agents/common.js +1 -1
- package/dist/src/plugin/agents/index.d.ts +2 -3
- package/dist/src/plugin/agents/index.js +2 -2
- package/dist/src/plugin/claude-manager.plugin.js +207 -1
- package/dist/src/plugin/service-factory.d.ts +8 -0
- package/dist/src/plugin/service-factory.js +32 -0
- package/dist/src/team/roster.d.ts +0 -1
- package/dist/src/team/roster.js +1 -1
- package/dist/team/roster.d.ts +0 -1
- package/dist/team/roster.js +1 -1
- package/dist/test/undo-propagation.test.d.ts +1 -0
- package/dist/test/undo-propagation.test.js +837 -0
- package/package.json +13 -13
package/README.md
CHANGED
|
@@ -1,48 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @doingdev/opencode-claude-manager-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OpenCode plugin that adds a manager-style orchestration layer over Claude Code sessions. A `cto` agent reads the repo, asks focused questions, and delegates to named engineers who each run work in their own persistent Claude Code session.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Useful when you want OpenCode to investigate first, split work across named engineers with session continuity, and keep git and review controls at the manager layer.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## What it adds
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- Uses named engineer subagents for live delegated work while keeping both wrapper memory and Claude session continuity underneath.
|
|
16
|
-
- Keeps session babysitting out of the normal user flow — no public reset/fresh-session controls.
|
|
17
|
-
- Discovers repo-local Claude metadata from `.claude/skills`, `.claude/commands`, `CLAUDE.md`, and settings hooks.
|
|
18
|
-
- Git integration: diff, commit, and reset from the manager layer.
|
|
19
|
-
- Tool approval policy for governing which Claude Code tools are allowed.
|
|
20
|
-
- Persists local team state and transcripts under `.claude-manager/` for continuity and inspection.
|
|
9
|
+
- `cto` orchestrator that reads, delegates, reviews diffs, and owns all manager tools. Never edits files directly.
|
|
10
|
+
- Five persistent general engineers — `tom`, `john`, `maya`, `sara`, `alex` — each backed by its own Claude Code session that survives across assignments in the same CTO session.
|
|
11
|
+
- `team-planner` that runs two engineers in parallel and returns a single synthesized plan.
|
|
12
|
+
- `browser-qa` browser verification specialist using Playwright-oriented prompting. Does not implement code.
|
|
13
|
+
- Manager tools for team state, git, transcript/history inspection, and tool approval policy.
|
|
14
|
+
- Local runtime state under `.claude-manager/` persisted across turns.
|
|
21
15
|
|
|
22
16
|
## Requirements
|
|
23
17
|
|
|
24
|
-
- Node `
|
|
18
|
+
- Node `>=22`
|
|
25
19
|
- OpenCode with plugin loading enabled
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
- Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`) available to the Node process
|
|
21
|
+
- Git - required for the git tools
|
|
22
|
+
- OpenCode Playwright skill/command - required for `browser-qa`
|
|
29
23
|
|
|
30
|
-
Install
|
|
24
|
+
## Install
|
|
31
25
|
|
|
32
26
|
```bash
|
|
33
27
|
pnpm add @doingdev/opencode-claude-manager-plugin
|
|
28
|
+
# npm install @doingdev/opencode-claude-manager-plugin
|
|
29
|
+
# yarn add @doingdev/opencode-claude-manager-plugin
|
|
34
30
|
```
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
pnpm install
|
|
40
|
-
pnpm run build
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## OpenCode Config
|
|
32
|
+
## Setup
|
|
44
33
|
|
|
45
|
-
Add the plugin to your
|
|
34
|
+
Add the plugin to `opencode.json` in your project root:
|
|
46
35
|
|
|
47
36
|
```json
|
|
48
37
|
{
|
|
@@ -50,137 +39,134 @@ Add the plugin to your OpenCode config:
|
|
|
50
39
|
}
|
|
51
40
|
```
|
|
52
41
|
|
|
53
|
-
|
|
42
|
+
Agents register automatically at runtime. No manual agent entries needed.
|
|
54
43
|
|
|
55
|
-
##
|
|
44
|
+
## Key concepts
|
|
56
45
|
|
|
57
|
-
|
|
46
|
+
**Delegation model.** The CTO reads the repo and context, decides what to do, and sends work to a named engineer via the `claude` tool. The engineer runs that assignment inside its own Claude Code session. The CTO never edits files directly.
|
|
58
47
|
|
|
59
|
-
-
|
|
60
|
-
- `team_status` — inspect the current CTO team's engineer bindings, Claude session IDs, busy flags, and context snapshots.
|
|
48
|
+
**Engineer persistence.** Each engineer's Claude Code session persists within a CTO session. A follow-up assignment to the same engineer resumes with prior context, which helps with multi-step tasks or when the engineer already understands a subsystem.
|
|
61
49
|
|
|
62
|
-
|
|
50
|
+
**Modes.** When delegating, the CTO picks a mode:
|
|
51
|
+
- `explore` — read-only investigation. No file edits.
|
|
52
|
+
- `implement` — hands-on implementation work.
|
|
53
|
+
- `verify` — tests, lint, spot-checks after changes.
|
|
63
54
|
|
|
64
|
-
|
|
65
|
-
- `mode` (required) — `explore`, `implement`, or `verify`.
|
|
66
|
-
- `message` (required) — the work to do.
|
|
67
|
-
- `model` (optional) — `claude-opus-4-6` or `claude-sonnet-4-6`.
|
|
55
|
+
**Teams.** Each CTO session has its own team. State is keyed by CTO session ID under `.claude-manager/teams/`.
|
|
68
56
|
|
|
69
|
-
|
|
57
|
+
## Agents
|
|
70
58
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
59
|
+
| Agent | Role |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `cto` | Orchestrator. Reads, delegates, reviews, owns manager tools. Does not edit files. |
|
|
62
|
+
| `tom`, `john`, `maya`, `sara`, `alex` | General engineers. Each exposes only the `claude` tool backed by its own persistent Claude Code session. |
|
|
63
|
+
| `team-planner` | Thin wrapper around `plan_with_team`. Runs two engineers in parallel, returns a synthesized plan. |
|
|
64
|
+
| `browser-qa` | Browser verification specialist. Uses Playwright-oriented prompting. Does not implement code. |
|
|
74
65
|
|
|
75
|
-
|
|
66
|
+
## Tools
|
|
76
67
|
|
|
77
|
-
|
|
78
|
-
- `list_history` — list saved CTO teams for the worktree or inspect one team by ID.
|
|
68
|
+
### Engineer tool
|
|
79
69
|
|
|
80
|
-
|
|
70
|
+
Each general engineer exposes one tool:
|
|
81
71
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
| Tool | Parameters | Notes |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `claude` | `mode` (`explore`/`implement`/`verify`), `assignment` (text), optional `model` | Model choices: `claude-opus-4-6` or `claude-sonnet-4-6`. Defaults to the session default. |
|
|
85
75
|
|
|
86
|
-
|
|
76
|
+
### CTO tools
|
|
87
77
|
|
|
88
|
-
|
|
78
|
+
| Tool | Description |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `team_status` | Show current team: engineers, sessions, context levels. |
|
|
81
|
+
| `reset_engineer` | Clear a stuck engineer's Claude session, wrapper history, or both. |
|
|
82
|
+
| `git_diff` | Diff with optional path filter, staged flag, or ref. |
|
|
83
|
+
| `git_commit` | Commit staged changes with a message and optional file list. |
|
|
84
|
+
| `git_reset` | **Destructive.** Runs `git reset --hard HEAD && git clean -fd`. |
|
|
85
|
+
| `git_status` | Short status and cleanliness check. |
|
|
86
|
+
| `git_log` | Recent commit log. |
|
|
87
|
+
| `list_transcripts` | List saved Claude session transcripts in `.claude-manager/transcripts/`. |
|
|
88
|
+
| `list_history` | List saved team state in `.claude-manager/teams/`. |
|
|
89
|
+
| `approval_policy` | Read the active tool approval policy. |
|
|
90
|
+
| `approval_decisions` | Read the logged approval decisions. |
|
|
91
|
+
| `approval_update` | Update the tool approval policy. |
|
|
89
92
|
|
|
90
|
-
-
|
|
91
|
-
- **`tom`**, **`john`**, **`maya`**, **`sara`**, **`alex`** (subagents) — thin named engineer wrappers. Each uses the `claude` tool and keeps one persistent Claude Code session.
|
|
92
|
-
- **`team-planner`** (subagent) — thin planning wrapper that runs `plan_with_team` so the UI shows live planning activity.
|
|
93
|
-
- **Claude Code sessions** — the underlying execution layer. One session per engineer inside the active team.
|
|
93
|
+
### team-planner tool
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
| Tool | Description |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `plan_with_team` | Runs two general engineers in parallel on the same planning task, then synthesizes one recommended plan. `browser-qa` is not part of the planning pool. |
|
|
96
98
|
|
|
97
|
-
##
|
|
99
|
+
## Approval policy
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
Each engineer's Claude Code session runs under a tool approval manager. The policy is **deny-list based**: unmatched tools stay allowed and `defaultAction` is always `allow`.
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
Rule fields:
|
|
104
|
+
- `pattern` — glob matching the tool name.
|
|
105
|
+
- `inputPattern` (optional) — substring match against tool input.
|
|
106
|
+
- `action` — `allow` or `deny`.
|
|
107
|
+
- `message` (optional) — shown when the rule fires.
|
|
105
108
|
|
|
106
|
-
|
|
109
|
+
Default rules block patterns like `rm -rf /`, `git push --force`, and `git reset --hard`. Read the active policy with `approval_policy`, inspect logged decisions with `approval_decisions`, and change rules with `approval_update`.
|
|
107
110
|
|
|
111
|
+
## Workflows
|
|
112
|
+
|
|
113
|
+
**Investigate then implement:**
|
|
108
114
|
```text
|
|
109
|
-
Ask
|
|
115
|
+
Ask cto to inspect the failing billing tests, send tom to implement the smallest safe fix, then review with git_diff.
|
|
110
116
|
```
|
|
111
117
|
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
**Dual-engineer planning:**
|
|
114
119
|
```text
|
|
115
|
-
Ask
|
|
120
|
+
Ask cto to use team-planner to produce a two-engineer plan for adding SSO to the auth module.
|
|
116
121
|
```
|
|
117
122
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
pnpm install
|
|
124
|
-
pnpm run lint
|
|
125
|
-
pnpm run typecheck
|
|
126
|
-
pnpm run test
|
|
127
|
-
pnpm run build
|
|
123
|
+
**Browser verification:**
|
|
124
|
+
```text
|
|
125
|
+
Ask cto to send browser-qa to verify the signup flow on http://localhost:3000 and report failures.
|
|
128
126
|
```
|
|
129
127
|
|
|
130
|
-
|
|
128
|
+
**Reuse engineer context:**
|
|
129
|
+
```text
|
|
130
|
+
Ask cto to send john to add error handling to the function he just implemented.
|
|
131
|
+
```
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
**Inspect state before committing:**
|
|
134
|
+
```text
|
|
135
|
+
Ask cto to run git_diff, summarize the changes, then run git_commit with a descriptive message.
|
|
136
|
+
```
|
|
133
137
|
|
|
134
|
-
|
|
138
|
+
## State
|
|
135
139
|
|
|
136
|
-
|
|
140
|
+
Runtime state is local to the worktree and gitignored:
|
|
137
141
|
|
|
138
|
-
|
|
142
|
+
```text
|
|
143
|
+
.claude-manager/
|
|
144
|
+
teams/ # One JSON file per CTO session (team ID)
|
|
145
|
+
transcripts/ # Claude session event logs
|
|
146
|
+
approval-policy.json # Active policy, if customized
|
|
147
|
+
debug.log # NDJSON debug log from plugin hooks
|
|
148
|
+
```
|
|
139
149
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
4. Set the GitHub owner/user to your account or org.
|
|
144
|
-
5. Set the repository name.
|
|
145
|
-
6. Set the workflow filename to `publish.yml`.
|
|
146
|
-
7. Leave the environment name empty unless you later add a GitHub Actions environment back to the workflow.
|
|
150
|
+
- State is not shared across machines or worktrees.
|
|
151
|
+
- Continuity is strongest within a single CTO session. Restarting the CTO session starts a new team.
|
|
152
|
+
- Undo at the CTO level propagates to engineer wrapper sessions and inner Claude Code sessions.
|
|
147
153
|
|
|
148
|
-
|
|
154
|
+
## Limits and caveats
|
|
149
155
|
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
-
|
|
156
|
+
- Context usage tracking is heuristic, not exact SDK-reported truth. Actual token counts may differ.
|
|
157
|
+
- `browser-qa` returns `PLAYWRIGHT_UNAVAILABLE: <reason>` if the Playwright skill or command is missing.
|
|
158
|
+
- `plan_with_team` runs two general engineers. `browser-qa` is excluded from the planning pool.
|
|
159
|
+
- `git_reset` is destructive and immediate. Run `git_diff` first to inspect state.
|
|
160
|
+
- Engineer context degrades if sessions are reset or if the CTO session is restarted mid-task.
|
|
153
161
|
|
|
154
|
-
|
|
162
|
+
## Development
|
|
155
163
|
|
|
156
164
|
```bash
|
|
157
|
-
pnpm
|
|
158
|
-
pnpm whoami
|
|
159
|
-
pnpm version patch
|
|
165
|
+
pnpm install
|
|
160
166
|
pnpm run lint
|
|
161
167
|
pnpm run typecheck
|
|
162
168
|
pnpm run test
|
|
163
169
|
pnpm run build
|
|
164
170
|
```
|
|
165
171
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
- creating a GitHub Release, or
|
|
169
|
-
- running the `Publish` workflow manually from the Actions tab
|
|
170
|
-
|
|
171
|
-
After trusted publishing is working, you can tighten npm package security by disabling token-based publishing for the package in npm settings.
|
|
172
|
-
|
|
173
|
-
## Limitations
|
|
174
|
-
|
|
175
|
-
- Claude slash commands and skills come primarily from filesystem discovery; SDK probing is available but optional.
|
|
176
|
-
- Session state is local to the repo under `.claude-manager/` and is ignored by git.
|
|
177
|
-
- The strongest team continuity comes when engineers are spawned from the active `cto` session; the plugin maps named engineers back to that active team automatically.
|
|
178
|
-
- Context tracking is heuristic-based; actual SDK context usage may differ slightly.
|
|
179
|
-
|
|
180
|
-
## Scripts
|
|
181
|
-
|
|
182
|
-
- `pnpm run build`
|
|
183
|
-
- `pnpm run typecheck`
|
|
184
|
-
- `pnpm run lint`
|
|
185
|
-
- `pnpm run format`
|
|
186
|
-
- `pnpm run test`
|
|
172
|
+
`dist/` is generated output. Do not edit it directly.
|
|
@@ -120,7 +120,7 @@ export class ClaudeAgentSdkAdapter {
|
|
|
120
120
|
async getTranscript(sessionId, cwd) {
|
|
121
121
|
const messages = await this.sdkFacade.getSessionMessages(sessionId, cwd ? { dir: cwd } : undefined);
|
|
122
122
|
return messages.map((message) => ({
|
|
123
|
-
role: message.type,
|
|
123
|
+
role: message.type === 'user' ? 'user' : 'assistant',
|
|
124
124
|
sessionId: message.session_id,
|
|
125
125
|
messageId: message.uuid,
|
|
126
126
|
text: extractText(message.message),
|
|
@@ -30,6 +30,11 @@ export declare class TeamOrchestrator {
|
|
|
30
30
|
teamId: string;
|
|
31
31
|
engineer: EngineerName;
|
|
32
32
|
} | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Remove wrapper history entries whose timestamp is strictly after cutoffIso.
|
|
35
|
+
* Used during CTO undo propagation to prune stale wrapper memory.
|
|
36
|
+
*/
|
|
37
|
+
pruneWrapperHistoryAfter(cwd: string, teamId: string, engineer: EngineerName, cutoffIso: string): Promise<void>;
|
|
33
38
|
resetEngineer(cwd: string, teamId: string, engineer: EngineerName, options?: {
|
|
34
39
|
clearSession?: boolean;
|
|
35
40
|
clearHistory?: boolean;
|
|
@@ -80,6 +80,16 @@ export class TeamOrchestrator {
|
|
|
80
80
|
}
|
|
81
81
|
return null;
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Remove wrapper history entries whose timestamp is strictly after cutoffIso.
|
|
85
|
+
* Used during CTO undo propagation to prune stale wrapper memory.
|
|
86
|
+
*/
|
|
87
|
+
async pruneWrapperHistoryAfter(cwd, teamId, engineer, cutoffIso) {
|
|
88
|
+
await this.updateEngineer(cwd, teamId, engineer, (entry) => ({
|
|
89
|
+
...entry,
|
|
90
|
+
wrapperHistory: entry.wrapperHistory.filter((h) => h.timestamp <= cutoffIso),
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
83
93
|
async resetEngineer(cwd, teamId, engineer, options) {
|
|
84
94
|
await this.updateEngineer(cwd, teamId, engineer, (entry) => ({
|
|
85
95
|
...entry,
|
|
@@ -12,7 +12,6 @@ export declare const ENGINEER_AGENT_IDS: {
|
|
|
12
12
|
/** General named engineers only (Tom/John/Maya/Sara/Alex). BrowserQA is a specialist registered separately. */
|
|
13
13
|
export declare const ENGINEER_AGENT_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
|
|
14
14
|
export declare const CTO_ONLY_TOOL_IDS: readonly ["team_status", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update"];
|
|
15
|
-
export declare const ENGINEER_TOOL_IDS: readonly ["claude"];
|
|
16
15
|
export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["team_status", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update", "claude"];
|
|
17
16
|
export type ToolPermission = 'allow' | 'ask' | 'deny';
|
|
18
17
|
export type AgentPermission = {
|
|
@@ -26,7 +26,7 @@ export const CTO_ONLY_TOOL_IDS = [
|
|
|
26
26
|
'approval_decisions',
|
|
27
27
|
'approval_update',
|
|
28
28
|
];
|
|
29
|
-
|
|
29
|
+
const ENGINEER_TOOL_IDS = ['claude'];
|
|
30
30
|
export const ALL_RESTRICTED_TOOL_IDS = [...CTO_ONLY_TOOL_IDS, ...ENGINEER_TOOL_IDS];
|
|
31
31
|
export const CTO_READONLY_TOOLS = {
|
|
32
32
|
read: 'allow',
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export { AGENT_BROWSER_QA, AGENT_CTO, AGENT_TEAM_PLANNER,
|
|
2
|
-
export type { AgentPermission, ToolPermission } from './common.js';
|
|
1
|
+
export { AGENT_BROWSER_QA, AGENT_CTO, AGENT_TEAM_PLANNER, denyRestrictedToolsGlobally, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from './common.js';
|
|
3
2
|
export { buildCtoAgentConfig } from './cto.js';
|
|
4
3
|
export { buildTeamPlannerAgentConfig } from './team-planner.js';
|
|
5
4
|
export { buildEngineerAgentConfig } from './engineers.js';
|
|
6
|
-
export { buildBrowserQaAgentConfig
|
|
5
|
+
export { buildBrowserQaAgentConfig } from './browser-qa.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { AGENT_BROWSER_QA, AGENT_CTO, AGENT_TEAM_PLANNER,
|
|
1
|
+
export { AGENT_BROWSER_QA, AGENT_CTO, AGENT_TEAM_PLANNER, denyRestrictedToolsGlobally, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from './common.js';
|
|
2
2
|
export { buildCtoAgentConfig } from './cto.js';
|
|
3
3
|
export { buildTeamPlannerAgentConfig } from './team-planner.js';
|
|
4
4
|
export { buildEngineerAgentConfig } from './engineers.js';
|
|
5
|
-
export { buildBrowserQaAgentConfig
|
|
5
|
+
export { buildBrowserQaAgentConfig } from './browser-qa.js';
|
|
@@ -4,7 +4,7 @@ import { appendDebugLog } from '../util/fs-helpers.js';
|
|
|
4
4
|
import { isEngineerName } from '../team/roster.js';
|
|
5
5
|
import { TeamOrchestrator, createActionableError, getFailureGuidanceText, } from '../manager/team-orchestrator.js';
|
|
6
6
|
import { AGENT_BROWSER_QA, AGENT_CTO, AGENT_TEAM_PLANNER, buildBrowserQaAgentConfig, buildCtoAgentConfig, buildEngineerAgentConfig, buildTeamPlannerAgentConfig, denyRestrictedToolsGlobally, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from './agents/index.js';
|
|
7
|
-
import { getOrCreatePluginServices, getParentSessionId, getSessionTeam, getWrapperSessionMapping, registerParentSession, registerSessionTeam, setWrapperSessionMapping, } from './service-factory.js';
|
|
7
|
+
import { clearLatestRevertProcessed, clearRevertProcessed, getOrCreatePluginServices, getParentSessionId, getSessionTeam, getWrapperSessionMapping, isRevertAlreadyProcessed, markRevertProcessed, registerParentSession, registerSessionTeam, setWrapperSessionMapping, } from './service-factory.js';
|
|
8
8
|
const MODEL_ENUM = ['claude-opus-4-6', 'claude-sonnet-4-6'];
|
|
9
9
|
const MODE_ENUM = ['explore', 'implement', 'verify'];
|
|
10
10
|
export const ClaudeManagerPlugin = async ({ worktree, client }) => {
|
|
@@ -44,6 +44,169 @@ export const ClaudeManagerPlugin = async ({ worktree, client }) => {
|
|
|
44
44
|
}
|
|
45
45
|
return sessionID;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Propagate a CTO-level undo to all engineer wrapper sessions and their inner
|
|
49
|
+
* Claude Code sessions that ran work after the reverted CTO message.
|
|
50
|
+
*
|
|
51
|
+
* Steps per affected engineer:
|
|
52
|
+
* 1. Determine how many wrapper exchanges happened after the cutoff.
|
|
53
|
+
* 2. Revert the engineer's OpenCode wrapper session to the first message after the cutoff.
|
|
54
|
+
* 3. Send `/undo` to the inner Claude Code session once per affected exchange.
|
|
55
|
+
* 4. Prune the in-disk wrapper history to remove the stale entries.
|
|
56
|
+
*
|
|
57
|
+
* Best-effort: one engineer's failure does not prevent others from being processed.
|
|
58
|
+
*/
|
|
59
|
+
async function handleCtoUndoPropagation(ctoSessionId, teamId, revertMessageId) {
|
|
60
|
+
if (!client) {
|
|
61
|
+
// Throw so the caller's catch clears the dedup marker; transient — should be retried.
|
|
62
|
+
throw new Error('no OpenCode client — cannot resolve revert cutoff');
|
|
63
|
+
}
|
|
64
|
+
// Fetch the CTO message to get a reliable cutoff timestamp.
|
|
65
|
+
// Let errors propagate so the caller's catch clears the dedup marker on failure.
|
|
66
|
+
const msgResult = await client.session.message({
|
|
67
|
+
path: { id: ctoSessionId, messageID: revertMessageId },
|
|
68
|
+
});
|
|
69
|
+
const created = msgResult.data?.info.time.created;
|
|
70
|
+
if (created === undefined) {
|
|
71
|
+
throw new Error('reverted CTO message has no creation timestamp');
|
|
72
|
+
}
|
|
73
|
+
const cutoffMs = created;
|
|
74
|
+
const cutoffIso = new Date(cutoffMs).toISOString();
|
|
75
|
+
const team = await services.orchestrator.getOrCreateTeam(worktree, teamId);
|
|
76
|
+
for (const engineerRecord of team.engineers) {
|
|
77
|
+
try {
|
|
78
|
+
await undoEngineerTurns(teamId, engineerRecord, cutoffMs, cutoffIso);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
// One engineer's failure must not prevent others from being processed.
|
|
82
|
+
try {
|
|
83
|
+
await appendDebugLog(services.debugLogPath, {
|
|
84
|
+
type: 'undo_engineer_error',
|
|
85
|
+
ctoSessionId,
|
|
86
|
+
teamId,
|
|
87
|
+
engineer: engineerRecord.name,
|
|
88
|
+
error: err instanceof Error ? err.message : String(err),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Ignore log write failures.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Undo all work an engineer did after the cutoff in both their OpenCode wrapper
|
|
99
|
+
* session and their inner Claude Code session, then prune the wrapper history.
|
|
100
|
+
*/
|
|
101
|
+
async function undoEngineerTurns(teamId, engineerRecord, cutoffMs, cutoffIso) {
|
|
102
|
+
// Count how many full exchanges (assignment+result pairs) happened after the cutoff.
|
|
103
|
+
const undoCount = engineerRecord.wrapperHistory.filter((h) => h.type === 'assignment' && h.timestamp > cutoffIso).length;
|
|
104
|
+
if (undoCount === 0) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// 1. Revert the engineer's OpenCode wrapper session.
|
|
108
|
+
if (client && engineerRecord.wrapperSessionId) {
|
|
109
|
+
try {
|
|
110
|
+
await revertWrapperSession(engineerRecord.wrapperSessionId, cutoffMs);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
try {
|
|
114
|
+
await appendDebugLog(services.debugLogPath, {
|
|
115
|
+
type: 'undo_wrapper_revert_error',
|
|
116
|
+
teamId,
|
|
117
|
+
engineer: engineerRecord.name,
|
|
118
|
+
error: err instanceof Error ? err.message : String(err),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Ignore log write failures.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 2. Undo the corresponding inner Claude Code session turns.
|
|
127
|
+
let innerUndoFailed = false;
|
|
128
|
+
if (engineerRecord.claudeSessionId) {
|
|
129
|
+
for (let i = 0; i < undoCount; i++) {
|
|
130
|
+
try {
|
|
131
|
+
await services.sessions.runTask({
|
|
132
|
+
cwd: worktree,
|
|
133
|
+
prompt: '/undo',
|
|
134
|
+
resumeSessionId: engineerRecord.claudeSessionId,
|
|
135
|
+
persistSession: true,
|
|
136
|
+
permissionMode: 'acceptEdits',
|
|
137
|
+
maxTurns: 1,
|
|
138
|
+
settingSources: ['user', 'project', 'local'],
|
|
139
|
+
}, undefined);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Best-effort: stop further /undo attempts for this engineer on failure.
|
|
143
|
+
innerUndoFailed = true;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// If inner undo failed, the Claude session may be in an inconsistent state.
|
|
149
|
+
// Reset the session reference and context snapshot so the next assignment starts fresh.
|
|
150
|
+
if (innerUndoFailed) {
|
|
151
|
+
try {
|
|
152
|
+
await services.orchestrator.resetEngineer(worktree, teamId, engineerRecord.name, {
|
|
153
|
+
clearSession: true,
|
|
154
|
+
clearHistory: false,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
try {
|
|
159
|
+
await appendDebugLog(services.debugLogPath, {
|
|
160
|
+
type: 'undo_clear_session_error',
|
|
161
|
+
teamId,
|
|
162
|
+
engineer: engineerRecord.name,
|
|
163
|
+
error: err instanceof Error ? err.message : String(err),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Ignore log write failures.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// 3. Prune the persisted wrapper history.
|
|
172
|
+
try {
|
|
173
|
+
await services.orchestrator.pruneWrapperHistoryAfter(worktree, teamId, engineerRecord.name, cutoffIso);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
try {
|
|
177
|
+
await appendDebugLog(services.debugLogPath, {
|
|
178
|
+
type: 'undo_prune_error',
|
|
179
|
+
teamId,
|
|
180
|
+
engineer: engineerRecord.name,
|
|
181
|
+
error: err instanceof Error ? err.message : String(err),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Ignore log write failures.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Revert an engineer's OpenCode wrapper session to just before the first user
|
|
191
|
+
* message that was created after cutoffMs.
|
|
192
|
+
*/
|
|
193
|
+
async function revertWrapperSession(wrapperSessionId, cutoffMs) {
|
|
194
|
+
if (!client) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const messagesResult = await client.session.messages({
|
|
198
|
+
path: { id: wrapperSessionId },
|
|
199
|
+
});
|
|
200
|
+
const messages = messagesResult.data ?? [];
|
|
201
|
+
const firstAffected = messages.find((m) => m.info.role === 'user' && m.info.time.created > cutoffMs);
|
|
202
|
+
if (!firstAffected) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
await client.session.revert({
|
|
206
|
+
path: { id: wrapperSessionId },
|
|
207
|
+
body: { messageID: firstAffected.info.id },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
47
210
|
return {
|
|
48
211
|
config: async (config) => {
|
|
49
212
|
config.agent ??= {};
|
|
@@ -82,6 +245,49 @@ export const ClaudeManagerPlugin = async ({ worktree, client }) => {
|
|
|
82
245
|
if (session.parentID) {
|
|
83
246
|
registerParentSession(session.id, session.parentID);
|
|
84
247
|
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (sdkEvent.type === 'session.updated') {
|
|
251
|
+
const session = sdkEvent.properties.info;
|
|
252
|
+
if (!session.revert) {
|
|
253
|
+
// Revert marker cleared — remove the stale dedup entry so a future undo
|
|
254
|
+
// of the same message (redo → undo) is processed again.
|
|
255
|
+
clearLatestRevertProcessed(session.id);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Only propagate undo for CTO sessions (teamId === sessionId).
|
|
259
|
+
if (getSessionTeam(session.id) !== session.id) {
|
|
260
|
+
// In-memory miss — check persisted state to survive process restart / cache loss.
|
|
261
|
+
const persistedTeam = await services.teamStore.getTeam(worktree, session.id);
|
|
262
|
+
if (!persistedTeam) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
// Valid CTO team found on disk — register it so future events skip the I/O.
|
|
266
|
+
registerSessionTeam(session.id, session.id);
|
|
267
|
+
}
|
|
268
|
+
const teamId = session.id;
|
|
269
|
+
const revertMessageId = session.revert.messageID;
|
|
270
|
+
// Deduplicate: multiple session.updated events can fire for the same revert marker.
|
|
271
|
+
if (isRevertAlreadyProcessed(session.id, revertMessageId)) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
markRevertProcessed(session.id, revertMessageId);
|
|
275
|
+
// Best-effort: do not throw from the event hook.
|
|
276
|
+
await handleCtoUndoPropagation(session.id, teamId, revertMessageId).catch(async (err) => {
|
|
277
|
+
// On total failure, remove the dedup marker so a retry is possible.
|
|
278
|
+
clearRevertProcessed(session.id, revertMessageId);
|
|
279
|
+
try {
|
|
280
|
+
await appendDebugLog(services.debugLogPath, {
|
|
281
|
+
type: 'undo_propagation_error',
|
|
282
|
+
ctoSessionId: session.id,
|
|
283
|
+
revertMessageId,
|
|
284
|
+
error: err instanceof Error ? err.message : String(err),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// Log write failures must not mask the original error path.
|
|
289
|
+
}
|
|
290
|
+
});
|
|
85
291
|
}
|
|
86
292
|
},
|
|
87
293
|
'experimental.chat.system.transform': async (input, output) => {
|
|
@@ -15,6 +15,14 @@ interface ClaudeManagerPluginServices {
|
|
|
15
15
|
}
|
|
16
16
|
export declare function getOrCreatePluginServices(worktree: string): ClaudeManagerPluginServices;
|
|
17
17
|
export declare function clearPluginServices(): void;
|
|
18
|
+
export declare function isRevertAlreadyProcessed(ctoSessionId: string, revertMessageId: string): boolean;
|
|
19
|
+
export declare function markRevertProcessed(ctoSessionId: string, revertMessageId: string): void;
|
|
20
|
+
export declare function clearRevertProcessed(ctoSessionId: string, revertMessageId: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Clear the latest dedup entry for a CTO session — called when the revert marker disappears
|
|
23
|
+
* so that a subsequent undo of the same message can be processed again.
|
|
24
|
+
*/
|
|
25
|
+
export declare function clearLatestRevertProcessed(ctoSessionId: string): void;
|
|
18
26
|
export declare function registerParentSession(childId: string, parentId: string): void;
|
|
19
27
|
export declare function getParentSessionId(childId: string): string | undefined;
|
|
20
28
|
export declare function registerSessionTeam(sessionId: string, teamId: string): void;
|