@doingdev/opencode-claude-manager-plugin 0.1.61 → 0.1.64
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/manager/team-orchestrator.d.ts +5 -0
- package/dist/manager/team-orchestrator.js +11 -0
- package/dist/plugin/agents/browser-qa.js +4 -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 +11 -0
- package/dist/src/plugin/agents/browser-qa.js +4 -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/src/types/contracts.d.ts +7 -0
- package/dist/team/roster.d.ts +0 -1
- package/dist/team/roster.js +1 -1
- package/dist/test/claude-agent-sdk-adapter.test.js +39 -0
- package/dist/test/team-orchestrator.test.js +53 -0
- package/dist/test/undo-propagation.test.d.ts +1 -0
- package/dist/test/undo-propagation.test.js +837 -0
- package/dist/types/contracts.d.ts +7 -0
- package/package.json +1 -1
|
@@ -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,
|
|
@@ -122,6 +132,7 @@ export class TeamOrchestrator {
|
|
|
122
132
|
persistSession: true,
|
|
123
133
|
includePartialMessages: true,
|
|
124
134
|
permissionMode: 'acceptEdits',
|
|
135
|
+
allowedTools: workerCaps?.sessionAllowedTools,
|
|
125
136
|
restrictWriteTools: input.mode === 'explore' || (workerCaps?.restrictWriteTools ?? false),
|
|
126
137
|
model: input.model,
|
|
127
138
|
effort: (workerCaps?.restrictWriteTools ?? false)
|
|
@@ -12,6 +12,10 @@ export function buildWorkerCapabilities(prompts) {
|
|
|
12
12
|
plannerEligible: false,
|
|
13
13
|
isRuntimeUnavailableResponse: (text) => text.trimStart().startsWith('PLAYWRIGHT_UNAVAILABLE:'),
|
|
14
14
|
runtimeUnavailableTitle: '❌ Playwright unavailable',
|
|
15
|
+
// Pre-approve the Playwriter toolchain at the SDK level so headless sessions
|
|
16
|
+
// never stall waiting for interactive confirmation. Write tools remain blocked
|
|
17
|
+
// by restrictWriteTools and the canUseTool write-filter.
|
|
18
|
+
sessionAllowedTools: ['Skill', 'Bash', 'Read', 'Grep', 'Glob', 'LS', 'ListDirectory'],
|
|
15
19
|
},
|
|
16
20
|
};
|
|
17
21
|
}
|
|
@@ -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;
|
|
@@ -15,6 +15,15 @@ const wrapperSessionRegistry = new Map();
|
|
|
15
15
|
const parentSessionRegistry = new Map();
|
|
16
16
|
/** ctoSessionId → teamId — populated when a CTO chat.message fires */
|
|
17
17
|
const sessionTeamRegistry = new Map();
|
|
18
|
+
/**
|
|
19
|
+
* Dedupe set for processed CTO undo events.
|
|
20
|
+
* Key format: "${ctoSessionId}:${revertMessageId}".
|
|
21
|
+
* Cleared when the session no longer shows a revert marker (via a subsequent session.updated
|
|
22
|
+
* with no revert field) or on a full service reset.
|
|
23
|
+
*/
|
|
24
|
+
const processedRevertRegistry = new Set();
|
|
25
|
+
/** ctoSessionId → latest processed revertMessageId — used to clear stale dedup entries when the revert marker disappears */
|
|
26
|
+
const latestRevertRegistry = new Map();
|
|
18
27
|
export function getOrCreatePluginServices(worktree) {
|
|
19
28
|
const existing = serviceRegistry.get(worktree);
|
|
20
29
|
if (existing) {
|
|
@@ -48,6 +57,29 @@ export function clearPluginServices() {
|
|
|
48
57
|
wrapperSessionRegistry.clear();
|
|
49
58
|
parentSessionRegistry.clear();
|
|
50
59
|
sessionTeamRegistry.clear();
|
|
60
|
+
processedRevertRegistry.clear();
|
|
61
|
+
latestRevertRegistry.clear();
|
|
62
|
+
}
|
|
63
|
+
export function isRevertAlreadyProcessed(ctoSessionId, revertMessageId) {
|
|
64
|
+
return processedRevertRegistry.has(`${ctoSessionId}:${revertMessageId}`);
|
|
65
|
+
}
|
|
66
|
+
export function markRevertProcessed(ctoSessionId, revertMessageId) {
|
|
67
|
+
processedRevertRegistry.add(`${ctoSessionId}:${revertMessageId}`);
|
|
68
|
+
latestRevertRegistry.set(ctoSessionId, revertMessageId);
|
|
69
|
+
}
|
|
70
|
+
export function clearRevertProcessed(ctoSessionId, revertMessageId) {
|
|
71
|
+
processedRevertRegistry.delete(`${ctoSessionId}:${revertMessageId}`);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Clear the latest dedup entry for a CTO session — called when the revert marker disappears
|
|
75
|
+
* so that a subsequent undo of the same message can be processed again.
|
|
76
|
+
*/
|
|
77
|
+
export function clearLatestRevertProcessed(ctoSessionId) {
|
|
78
|
+
const revertMessageId = latestRevertRegistry.get(ctoSessionId);
|
|
79
|
+
if (revertMessageId !== undefined) {
|
|
80
|
+
processedRevertRegistry.delete(`${ctoSessionId}:${revertMessageId}`);
|
|
81
|
+
latestRevertRegistry.delete(ctoSessionId);
|
|
82
|
+
}
|
|
51
83
|
}
|
|
52
84
|
export function registerParentSession(childId, parentId) {
|
|
53
85
|
parentSessionRegistry.set(childId, parentId);
|
|
@@ -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,
|
|
@@ -122,6 +132,7 @@ export class TeamOrchestrator {
|
|
|
122
132
|
persistSession: true,
|
|
123
133
|
includePartialMessages: true,
|
|
124
134
|
permissionMode: 'acceptEdits',
|
|
135
|
+
allowedTools: workerCaps?.sessionAllowedTools,
|
|
125
136
|
restrictWriteTools: input.mode === 'explore' || (workerCaps?.restrictWriteTools ?? false),
|
|
126
137
|
model: input.model,
|
|
127
138
|
effort: (workerCaps?.restrictWriteTools ?? false)
|
|
@@ -12,6 +12,10 @@ export function buildWorkerCapabilities(prompts) {
|
|
|
12
12
|
plannerEligible: false,
|
|
13
13
|
isRuntimeUnavailableResponse: (text) => text.trimStart().startsWith('PLAYWRIGHT_UNAVAILABLE:'),
|
|
14
14
|
runtimeUnavailableTitle: '❌ Playwright unavailable',
|
|
15
|
+
// Pre-approve the Playwriter toolchain at the SDK level so headless sessions
|
|
16
|
+
// never stall waiting for interactive confirmation. Write tools remain blocked
|
|
17
|
+
// by restrictWriteTools and the canUseTool write-filter.
|
|
18
|
+
sessionAllowedTools: ['Skill', 'Bash', 'Read', 'Grep', 'Glob', 'LS', 'ListDirectory'],
|
|
15
19
|
},
|
|
16
20
|
};
|
|
17
21
|
}
|
|
@@ -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';
|