@doingdev/opencode-claude-manager-plugin 0.1.58 → 0.1.60
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 +10 -1
- package/dist/manager/team-orchestrator.js +77 -1
- package/dist/plugin/agents/common.d.ts +2 -2
- package/dist/plugin/agents/common.js +2 -0
- package/dist/plugin/claude-manager.plugin.js +95 -24
- package/dist/plugin/service-factory.d.ts +4 -4
- package/dist/plugin/service-factory.js +14 -18
- package/dist/prompts/registry.js +22 -4
- package/dist/src/manager/team-orchestrator.d.ts +10 -1
- package/dist/src/manager/team-orchestrator.js +77 -1
- package/dist/src/plugin/agents/common.d.ts +2 -2
- package/dist/src/plugin/agents/common.js +2 -0
- package/dist/src/plugin/claude-manager.plugin.js +95 -24
- package/dist/src/plugin/service-factory.d.ts +4 -4
- package/dist/src/plugin/service-factory.js +14 -18
- package/dist/src/prompts/registry.js +22 -4
- package/dist/src/state/team-state-store.d.ts +0 -3
- package/dist/src/state/team-state-store.js +0 -22
- package/dist/src/types/contracts.d.ts +19 -0
- package/dist/state/team-state-store.d.ts +0 -3
- package/dist/state/team-state-store.js +0 -22
- package/dist/test/claude-manager.plugin.test.js +172 -1
- package/dist/test/cto-active-team.test.js +176 -29
- package/dist/test/prompt-registry.test.js +52 -0
- package/dist/test/report-claude-event.test.js +16 -12
- package/dist/test/team-orchestrator.test.js +158 -2
- package/dist/test/team-state-store.test.js +0 -18
- package/dist/types/contracts.d.ts +19 -0
- package/package.json +1 -1
|
@@ -3,12 +3,46 @@ import { managerPromptRegistry } from '../prompts/registry.js';
|
|
|
3
3
|
import { isEngineerName } from '../team/roster.js';
|
|
4
4
|
import { TeamOrchestrator, createActionableError, getFailureGuidanceText, } from '../manager/team-orchestrator.js';
|
|
5
5
|
import { AGENT_BROWSER_QA, AGENT_CTO, AGENT_TEAM_PLANNER, buildBrowserQaAgentConfig, buildCtoAgentConfig, buildEngineerAgentConfig, buildTeamPlannerAgentConfig, denyRestrictedToolsGlobally, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from './agents/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getOrCreatePluginServices, getParentSessionId, getSessionTeam, getWrapperSessionMapping, registerParentSession, registerSessionTeam, setWrapperSessionMapping, } from './service-factory.js';
|
|
7
7
|
const MODEL_ENUM = ['claude-opus-4-6', 'claude-sonnet-4-6'];
|
|
8
8
|
const MODE_ENUM = ['explore', 'implement', 'verify'];
|
|
9
|
-
export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
9
|
+
export const ClaudeManagerPlugin = async ({ worktree, client }) => {
|
|
10
10
|
const services = getOrCreatePluginServices(worktree);
|
|
11
11
|
await services.approvalManager.loadPersistedPolicy();
|
|
12
|
+
/**
|
|
13
|
+
* Resolves the team ID for a brand-new engineer wrapper session.
|
|
14
|
+
*
|
|
15
|
+
* 1. Walk the cached parentID chain (populated by session.created events).
|
|
16
|
+
* 2. On a cache miss, attempt a live client.session.get() lookup and cache
|
|
17
|
+
* whatever parentID the SDK returns.
|
|
18
|
+
* 3. Fall back to the orphan sentinel (sessionID itself) only when both
|
|
19
|
+
* cache and live lookup come up empty.
|
|
20
|
+
*/
|
|
21
|
+
async function resolveTeamId(sessionID) {
|
|
22
|
+
let current = sessionID;
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
while (current && !seen.has(current)) {
|
|
25
|
+
seen.add(current);
|
|
26
|
+
const team = getSessionTeam(current);
|
|
27
|
+
if (team !== undefined)
|
|
28
|
+
return team;
|
|
29
|
+
// Cache miss on this node's parent: try the live SDK.
|
|
30
|
+
if (client && !getParentSessionId(current)) {
|
|
31
|
+
try {
|
|
32
|
+
const result = await client.session.get({ path: { id: current } });
|
|
33
|
+
const parentID = result.data?.parentID;
|
|
34
|
+
if (parentID) {
|
|
35
|
+
registerParentSession(current, parentID);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Network / auth failure — let the walk continue to orphan.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
current = getParentSessionId(current);
|
|
43
|
+
}
|
|
44
|
+
return sessionID;
|
|
45
|
+
}
|
|
12
46
|
return {
|
|
13
47
|
config: async (config) => {
|
|
14
48
|
config.agent ??= {};
|
|
@@ -23,15 +57,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
23
57
|
},
|
|
24
58
|
'chat.message': async (input) => {
|
|
25
59
|
if (input.agent === AGENT_CTO) {
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
const activeTeamId = persistedTeamId ?? input.sessionID;
|
|
30
|
-
setActiveTeamSession(worktree, activeTeamId);
|
|
31
|
-
if (!persistedTeamId) {
|
|
32
|
-
// First CTO session for this worktree — persist this session as active team.
|
|
33
|
-
await setPersistedActiveTeam(worktree, activeTeamId);
|
|
34
|
-
}
|
|
60
|
+
// Each CTO session ID is its own team. The session ID is the durable
|
|
61
|
+
// identity: no worktree-global active-team state.
|
|
62
|
+
registerSessionTeam(input.sessionID, input.sessionID);
|
|
35
63
|
return;
|
|
36
64
|
}
|
|
37
65
|
if (input.agent && isEngineerAgent(input.agent)) {
|
|
@@ -39,7 +67,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
39
67
|
const existing = getWrapperSessionMapping(worktree, input.sessionID);
|
|
40
68
|
const persisted = existing ??
|
|
41
69
|
(await services.orchestrator.findTeamByWrapperSession(worktree, input.sessionID));
|
|
42
|
-
const teamId = persisted?.teamId ?? (await resolveTeamId(
|
|
70
|
+
const teamId = persisted?.teamId ?? (await resolveTeamId(input.sessionID));
|
|
43
71
|
setWrapperSessionMapping(worktree, input.sessionID, {
|
|
44
72
|
teamId,
|
|
45
73
|
workerName: engineer,
|
|
@@ -47,6 +75,14 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
47
75
|
await services.orchestrator.recordWrapperSession(worktree, teamId, engineer, input.sessionID);
|
|
48
76
|
}
|
|
49
77
|
},
|
|
78
|
+
event: async ({ event: sdkEvent }) => {
|
|
79
|
+
if (sdkEvent.type === 'session.created') {
|
|
80
|
+
const session = sdkEvent.properties.info;
|
|
81
|
+
if (session.parentID) {
|
|
82
|
+
registerParentSession(session.id, session.parentID);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
50
86
|
'experimental.chat.system.transform': async (input, output) => {
|
|
51
87
|
if (!input.sessionID) {
|
|
52
88
|
return;
|
|
@@ -86,7 +122,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
86
122
|
const existing = getWrapperSessionMapping(context.worktree, context.sessionID);
|
|
87
123
|
const persisted = existing ??
|
|
88
124
|
(await services.orchestrator.findTeamByWrapperSession(context.worktree, context.sessionID));
|
|
89
|
-
const teamId = persisted?.teamId ?? (await resolveTeamId(context.
|
|
125
|
+
const teamId = persisted?.teamId ?? (await resolveTeamId(context.sessionID));
|
|
90
126
|
setWrapperSessionMapping(context.worktree, context.sessionID, {
|
|
91
127
|
teamId,
|
|
92
128
|
workerName: engineer,
|
|
@@ -117,7 +153,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
117
153
|
teamId: tool.schema.string().optional(),
|
|
118
154
|
},
|
|
119
155
|
async execute(args, context) {
|
|
120
|
-
const teamId = args.teamId ??
|
|
156
|
+
const teamId = args.teamId ?? context.sessionID;
|
|
121
157
|
annotateToolRun(context, 'Reading team status', {
|
|
122
158
|
teamId,
|
|
123
159
|
});
|
|
@@ -134,7 +170,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
134
170
|
model: tool.schema.enum(MODEL_ENUM).optional(),
|
|
135
171
|
},
|
|
136
172
|
async execute(args, context) {
|
|
137
|
-
const teamId =
|
|
173
|
+
const teamId = context.sessionID;
|
|
138
174
|
// Pre-determine engineers for event labeling (using orchestrator selection logic)
|
|
139
175
|
const { lead, challenger } = await services.orchestrator.selectPlanEngineers(context.worktree, teamId, args.leadEngineer, args.challengerEngineer);
|
|
140
176
|
annotateToolRun(context, 'Running dual-engineer plan synthesis', {
|
|
@@ -170,6 +206,49 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
170
206
|
}, null, 2);
|
|
171
207
|
},
|
|
172
208
|
}),
|
|
209
|
+
confirm_plan: tool({
|
|
210
|
+
description: 'Persist plan confirmation and optional slice metadata after the user confirms a plan. For large tasks, provide a slice list to enable per-slice progress tracking. Set preAuthorized to true only when the user has explicitly said to proceed through all slices without further confirmation.',
|
|
211
|
+
args: {
|
|
212
|
+
summary: tool.schema.string().min(1),
|
|
213
|
+
taskSize: tool.schema.enum(['trivial', 'simple', 'large']),
|
|
214
|
+
slices: tool.schema.string().array().optional(),
|
|
215
|
+
preAuthorized: tool.schema.boolean().optional(),
|
|
216
|
+
},
|
|
217
|
+
async execute(args, context) {
|
|
218
|
+
const teamId = context.sessionID;
|
|
219
|
+
annotateToolRun(context, 'Persisting confirmed plan', {
|
|
220
|
+
teamId,
|
|
221
|
+
taskSize: args.taskSize,
|
|
222
|
+
sliceCount: args.slices?.length ?? 0,
|
|
223
|
+
});
|
|
224
|
+
const activePlan = await services.orchestrator.setActivePlan(context.worktree, teamId, {
|
|
225
|
+
summary: args.summary,
|
|
226
|
+
taskSize: args.taskSize,
|
|
227
|
+
slices: args.slices ?? [],
|
|
228
|
+
preAuthorized: args.preAuthorized ?? false,
|
|
229
|
+
});
|
|
230
|
+
return JSON.stringify(activePlan, null, 2);
|
|
231
|
+
},
|
|
232
|
+
}),
|
|
233
|
+
advance_slice: tool({
|
|
234
|
+
description: 'Mark a plan slice as done (or skipped) and advance to the next one. Use this after each slice completes to track large-task progress.',
|
|
235
|
+
args: {
|
|
236
|
+
sliceIndex: tool.schema.number(),
|
|
237
|
+
status: tool.schema.enum(['done', 'skipped']).optional(),
|
|
238
|
+
},
|
|
239
|
+
async execute(args, context) {
|
|
240
|
+
const teamId = context.sessionID;
|
|
241
|
+
const status = args.status ?? 'done';
|
|
242
|
+
annotateToolRun(context, `Advancing slice ${args.sliceIndex} → ${status}`, {
|
|
243
|
+
teamId,
|
|
244
|
+
sliceIndex: args.sliceIndex,
|
|
245
|
+
status,
|
|
246
|
+
});
|
|
247
|
+
await services.orchestrator.updateActivePlanSlice(context.worktree, teamId, args.sliceIndex, status);
|
|
248
|
+
const team = await services.orchestrator.getOrCreateTeam(context.worktree, teamId);
|
|
249
|
+
return JSON.stringify({ activePlan: team.activePlan ?? null }, null, 2);
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
173
252
|
reset_engineer: tool({
|
|
174
253
|
description: 'Reset a stuck or corrupted engineer. Clears the busy flag. Optionally clears the Claude session (starts fresh) and/or wrapper history.',
|
|
175
254
|
args: {
|
|
@@ -178,7 +257,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
178
257
|
clearHistory: tool.schema.boolean().optional(),
|
|
179
258
|
},
|
|
180
259
|
async execute(args, context) {
|
|
181
|
-
const teamId =
|
|
260
|
+
const teamId = context.sessionID;
|
|
182
261
|
annotateToolRun(context, `Resetting ${args.engineer}`, {
|
|
183
262
|
teamId,
|
|
184
263
|
clearSession: args.clearSession,
|
|
@@ -456,14 +535,6 @@ export function isEngineerAgent(agentId) {
|
|
|
456
535
|
const normalized = normalizeAgentId(agentId);
|
|
457
536
|
return Object.values(ENGINEER_AGENT_IDS).some((id) => id === normalized);
|
|
458
537
|
}
|
|
459
|
-
/**
|
|
460
|
-
* Resolves the team ID for an engineer session.
|
|
461
|
-
* Reads the persisted active team first (survives process restarts), then
|
|
462
|
-
* falls back to the in-memory registry, then to the raw session ID as a last resort.
|
|
463
|
-
*/
|
|
464
|
-
async function resolveTeamId(worktree, sessionID) {
|
|
465
|
-
return (await getPersistedActiveTeam(worktree)) ?? getActiveTeamSession(worktree) ?? sessionID;
|
|
466
|
-
}
|
|
467
538
|
function formatToolDescription(toolName, toolArgs) {
|
|
468
539
|
if (!toolArgs || typeof toolArgs !== 'object')
|
|
469
540
|
return undefined;
|
|
@@ -14,10 +14,10 @@ interface ClaudeManagerPluginServices {
|
|
|
14
14
|
}
|
|
15
15
|
export declare function getOrCreatePluginServices(worktree: string): ClaudeManagerPluginServices;
|
|
16
16
|
export declare function clearPluginServices(): void;
|
|
17
|
-
export declare function
|
|
18
|
-
export declare function
|
|
19
|
-
export declare function
|
|
20
|
-
export declare function
|
|
17
|
+
export declare function registerParentSession(childId: string, parentId: string): void;
|
|
18
|
+
export declare function getParentSessionId(childId: string): string | undefined;
|
|
19
|
+
export declare function registerSessionTeam(sessionId: string, teamId: string): void;
|
|
20
|
+
export declare function getSessionTeam(sessionId: string): string | undefined;
|
|
21
21
|
export declare function setWrapperSessionMapping(worktree: string, wrapperSessionId: string, mapping: {
|
|
22
22
|
teamId: string;
|
|
23
23
|
workerName: EngineerName;
|
|
@@ -10,8 +10,11 @@ import { TeamStateStore } from '../state/team-state-store.js';
|
|
|
10
10
|
import { TranscriptStore } from '../state/transcript-store.js';
|
|
11
11
|
import { buildWorkerCapabilities } from './agents/browser-qa.js';
|
|
12
12
|
const serviceRegistry = new Map();
|
|
13
|
-
const activeTeamRegistry = new Map();
|
|
14
13
|
const wrapperSessionRegistry = new Map();
|
|
14
|
+
/** childSessionId → parentSessionId — populated from session.created events */
|
|
15
|
+
const parentSessionRegistry = new Map();
|
|
16
|
+
/** ctoSessionId → teamId — populated when a CTO chat.message fires */
|
|
17
|
+
const sessionTeamRegistry = new Map();
|
|
15
18
|
export function getOrCreatePluginServices(worktree) {
|
|
16
19
|
const existing = serviceRegistry.get(worktree);
|
|
17
20
|
if (existing) {
|
|
@@ -40,28 +43,21 @@ export function getOrCreatePluginServices(worktree) {
|
|
|
40
43
|
}
|
|
41
44
|
export function clearPluginServices() {
|
|
42
45
|
serviceRegistry.clear();
|
|
43
|
-
activeTeamRegistry.clear();
|
|
44
46
|
wrapperSessionRegistry.clear();
|
|
47
|
+
parentSessionRegistry.clear();
|
|
48
|
+
sessionTeamRegistry.clear();
|
|
45
49
|
}
|
|
46
|
-
export function
|
|
47
|
-
|
|
50
|
+
export function registerParentSession(childId, parentId) {
|
|
51
|
+
parentSessionRegistry.set(childId, parentId);
|
|
48
52
|
}
|
|
49
|
-
export function
|
|
50
|
-
return
|
|
53
|
+
export function getParentSessionId(childId) {
|
|
54
|
+
return parentSessionRegistry.get(childId);
|
|
51
55
|
}
|
|
52
|
-
export
|
|
53
|
-
|
|
54
|
-
if (!services) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
return services.teamStore.getActiveTeam(worktree);
|
|
56
|
+
export function registerSessionTeam(sessionId, teamId) {
|
|
57
|
+
sessionTeamRegistry.set(sessionId, teamId);
|
|
58
58
|
}
|
|
59
|
-
export
|
|
60
|
-
|
|
61
|
-
if (!services) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
await services.teamStore.setActiveTeam(worktree, teamId);
|
|
59
|
+
export function getSessionTeam(sessionId) {
|
|
60
|
+
return sessionTeamRegistry.get(sessionId);
|
|
65
61
|
}
|
|
66
62
|
export function setWrapperSessionMapping(worktree, wrapperSessionId, mapping) {
|
|
67
63
|
wrapperSessionRegistry.set(`${worktree}:${wrapperSessionId}`, mapping);
|
|
@@ -4,7 +4,7 @@ export const managerPromptRegistry = {
|
|
|
4
4
|
'Your role is to decompose work, delegate precisely, review diffs for production risks, and verify outcomes.',
|
|
5
5
|
'You do not write code. All edits go through engineers. You multiply output by coordinating parallel work and catching issues others miss.',
|
|
6
6
|
'',
|
|
7
|
-
'# Operating Loop: Orient → Classify → Plan → Delegate → Review → Verify → Close',
|
|
7
|
+
'# Operating Loop: Orient → Classify → Plan → Confirm → Delegate → Review → Verify → Close',
|
|
8
8
|
'',
|
|
9
9
|
'## Orient: Understand the request',
|
|
10
10
|
'- Extract what you can from the user message, codebase (read/grep/glob/codesearch), prior engineer results, and `websearch`/`webfetch` when relevant.',
|
|
@@ -15,6 +15,7 @@ export const managerPromptRegistry = {
|
|
|
15
15
|
'',
|
|
16
16
|
'## Classify: Frame the work',
|
|
17
17
|
'- Is this a bug fix, feature, refactor, or something else?',
|
|
18
|
+
'- Task size: classify as trivial (single-line fix, unambiguous, no side effects), simple (one focused task, clear scope, 1–2 files), or large (multiple steps, cross-cutting changes, requires vertical slicing).',
|
|
18
19
|
'- What could go wrong? Is it reversible or irreversible? Can it fail in prod?',
|
|
19
20
|
'- Does it require careful rollout, data migration, observability, or backwards compatibility handling?',
|
|
20
21
|
'- Are there decisions the user has not explicitly made (architecture, scope, deployment strategy)?',
|
|
@@ -24,16 +25,29 @@ export const managerPromptRegistry = {
|
|
|
24
25
|
"- For medium or large tasks: use `task(subagent_type: 'team-planner', ...)` for dual-engineer exploration and plan synthesis.",
|
|
25
26
|
' - Team-planner automatically selects two non-overlapping engineers by availability and context; you may optionally specify lead and challenger.',
|
|
26
27
|
' - Challenger engineer identifies missing decisions, risks, and scope gaps before implementation.',
|
|
28
|
+
'- For large tasks: break into vertical slices before delegating. Each slice must deliver end-to-end, user-testable value independently (e.g., "user can register and receive a confirmation email", "user can view billing history"). Horizontal layers (e.g., "just types", "just tests") are not vertical slices. Document slices when calling `confirm_plan`.',
|
|
27
29
|
'- Break work into independent pieces that can run in parallel. Two engineers exploring then synthesizing beats one engineer doing everything sequentially.',
|
|
28
30
|
'- Before delegating, state your success criteria, not just the task. What done looks like. How you will verify it.',
|
|
29
31
|
'',
|
|
32
|
+
'## Confirm: Get user buy-in before implementing',
|
|
33
|
+
'- After planning but before dispatching any engineer in implement mode, present the plan to the user with the `question` tool.',
|
|
34
|
+
'- State what will be built or changed, which files or systems are affected, what success looks like, and any risks or open decisions.',
|
|
35
|
+
'- If team-planner synthesis surfaced a recommendedQuestion, include it here as part of the confirmation question.',
|
|
36
|
+
'- Do not proceed to implementation until the user confirms the plan.',
|
|
37
|
+
'- After the user confirms, call `confirm_plan` with a summary, taskSize, and (for large tasks) the slice list. Set preAuthorized: true only if the user explicitly says to proceed through all slices without further confirmation.',
|
|
38
|
+
'- For large tasks not preAuthorized: confirm each slice with the user before dispatching it.',
|
|
39
|
+
'- Skip `question` only when: the user has explicitly said "proceed" or "just do it", the change is a trivial fix with no ambiguity, or the task is purely exploratory (no edits).',
|
|
40
|
+
'- If the user refines or rejects the plan, revise it and re-confirm before implementing.',
|
|
41
|
+
'',
|
|
30
42
|
'## Delegate: Send precise assignments',
|
|
31
43
|
"- For single-engineer work: use `task(subagent_type: 'tom'|'john'|'maya'|'sara'|'alex', ...)` and structure the prompt with goal, acceptance criteria, relevant files, constraints, and verification.",
|
|
32
44
|
"- For dual-engineer planning: use `task(subagent_type: 'team-planner', ...)` which will lead + challenger synthesis.",
|
|
33
45
|
"- For browser/UI verification: use `task(subagent_type: 'browser-qa', ...)` with a clear verification goal. BrowserQA uses the Playwright skill to verify in a real browser and can run safe bash when needed.",
|
|
34
46
|
'- Each assignment includes: goal, acceptance criteria, relevant context, constraints, and verification method.',
|
|
47
|
+
'- For large tasks: after each slice completes, call `advance_slice` to record progress, then confirm the next slice with the user before dispatching (unless preAuthorized).',
|
|
35
48
|
'- Reuse the same engineer when follow-up work builds on their prior context.',
|
|
36
49
|
'- Only one implementing engineer modifies the worktree at a time. Parallelize exploration, research, and browser verification freely.',
|
|
50
|
+
'- Context warnings (moderate/high/critical) are informational only. Do NOT reset an engineer session in response to a context warning. Sessions auto-reset only on an actual contextExhausted error.',
|
|
37
51
|
'',
|
|
38
52
|
'## Review: Inspect diffs for production safety',
|
|
39
53
|
'- After an engineer reports implementation done, review the diff with `git_diff` before declaring it complete.',
|
|
@@ -66,6 +80,7 @@ export const managerPromptRegistry = {
|
|
|
66
80
|
'- Questions: Use the `question` tool when a decision will materially affect scope, architecture, or how you verify the outcome. Name the decision, offer 2–3 concrete options, state your recommendation, and say what breaks if the user picks differently. One high-leverage question at a time.',
|
|
67
81
|
'- Reframing: Before planning, ask what the user is actually trying to achieve, not just what they asked for. If the request sounds like a feature, ask what job-to-be-done it serves.',
|
|
68
82
|
'- Engineer selection: When assigning to a single engineer, prefer lower context pressure and less-recently-used engineers. Reuse if follow-up work builds on prior context.',
|
|
83
|
+
'- Context warnings: At moderate/high/critical context levels the system surfaces a warning. These are advisory — do not force session reset. Reserve reset for actual contextExhausted errors only.',
|
|
69
84
|
'- Failure handling:',
|
|
70
85
|
" - contextExhausted: The engineer's session ran out of tokens. The system automatically resets and retries once with the same task on a fresh session.",
|
|
71
86
|
' - sdkError or toolDenied: The underlying SDK failed or a tool call was denied. Investigate the error, adjust constraints, and retry.',
|
|
@@ -92,6 +107,7 @@ export const managerPromptRegistry = {
|
|
|
92
107
|
'',
|
|
93
108
|
'Your wrapper context from prior turns is reloaded automatically. Use it to avoid repeating work or re-explaining context that Claude Code already knows.',
|
|
94
109
|
"Return the tool result directly. Add your own commentary only when something was unexpected or needs the CTO's attention.",
|
|
110
|
+
'If you discover during implementation that the agreed approach is not viable (unexpected constraints, wrong files, missing context), stop immediately and surface the deviation to the CTO before proceeding with a different approach. Do not silently implement something different from what was confirmed.',
|
|
95
111
|
].join('\n'),
|
|
96
112
|
engineerSessionPrompt: [
|
|
97
113
|
'You are an expert software engineer working inside Claude Code.',
|
|
@@ -149,6 +165,7 @@ export const managerPromptRegistry = {
|
|
|
149
165
|
'- If lead and challenger engineer names are both specified, use them.',
|
|
150
166
|
'- If either name is missing, `plan_with_team` will auto-select two non-overlapping engineers based on availability and context.',
|
|
151
167
|
'Do not attempt any planning or analysis yourself. Delegate entirely to `plan_with_team`.',
|
|
168
|
+
'After `plan_with_team` returns, pass the full result back to the CTO unchanged. Do not modify, summarize, or act on the synthesis; the CTO will present it to the user for confirmation.',
|
|
152
169
|
].join('\n'),
|
|
153
170
|
browserQaAgentPrompt: [
|
|
154
171
|
"You are the browser QA specialist on the CTO's team.",
|
|
@@ -165,6 +182,7 @@ export const managerPromptRegistry = {
|
|
|
165
182
|
'- Never simulate or fabricate test results.',
|
|
166
183
|
'- If the Playwright tool is not available, the result will start with PLAYWRIGHT_UNAVAILABLE:.',
|
|
167
184
|
'- Your persistent Claude Code session remembers prior verification runs.',
|
|
185
|
+
'- If the verification scope changes unexpectedly (feature absent, URL wrong, task cannot be completed as specified), stop and report the scope mismatch rather than silently verifying something else.',
|
|
168
186
|
].join('\n'),
|
|
169
187
|
browserQaSessionPrompt: [
|
|
170
188
|
'You are a browser QA specialist. Your job is to verify web features and user flows using the Playwright skill/command.',
|
|
@@ -185,8 +203,8 @@ export const managerPromptRegistry = {
|
|
|
185
203
|
'Allowed tools: Playwright skill/command, safe bash, read-only tools (Read, Grep, Glob). No file editing or code modifications.',
|
|
186
204
|
].join('\n'),
|
|
187
205
|
contextWarnings: {
|
|
188
|
-
moderate: 'Engineer context is
|
|
189
|
-
high: 'Engineer context is
|
|
190
|
-
critical: 'Engineer context is near capacity ({percent}% estimated).
|
|
206
|
+
moderate: 'Engineer context is at {percent}% estimated. Session is healthy; keep the next task focused.',
|
|
207
|
+
high: 'Engineer context is at {percent}% estimated ({turns} turns, ${cost}). Session continues — prefer a narrowly scoped follow-up.',
|
|
208
|
+
critical: 'Engineer context is near capacity ({percent}% estimated). Warn only — do not force a reset; avoid large new tasks in this session.',
|
|
191
209
|
},
|
|
192
210
|
};
|
|
@@ -7,10 +7,7 @@ export declare class TeamStateStore {
|
|
|
7
7
|
getTeam(cwd: string, teamId: string): Promise<TeamRecord | null>;
|
|
8
8
|
listTeams(cwd: string): Promise<TeamRecord[]>;
|
|
9
9
|
updateTeam(cwd: string, teamId: string, update: (team: TeamRecord) => TeamRecord): Promise<TeamRecord>;
|
|
10
|
-
getActiveTeam(cwd: string): Promise<string | null>;
|
|
11
|
-
setActiveTeam(cwd: string, teamId: string): Promise<void>;
|
|
12
10
|
private getTeamKey;
|
|
13
|
-
private getActiveTeamPath;
|
|
14
11
|
private getTeamsDirectory;
|
|
15
12
|
private getTeamPath;
|
|
16
13
|
private enqueueWrite;
|
|
@@ -62,31 +62,9 @@ export class TeamStateStore {
|
|
|
62
62
|
return updated;
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
async getActiveTeam(cwd) {
|
|
66
|
-
const filePath = this.getActiveTeamPath(cwd);
|
|
67
|
-
try {
|
|
68
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
69
|
-
const parsed = JSON.parse(content);
|
|
70
|
-
return parsed.teamId ?? null;
|
|
71
|
-
}
|
|
72
|
-
catch (error) {
|
|
73
|
-
if (isFileNotFoundError(error)) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
async setActiveTeam(cwd, teamId) {
|
|
80
|
-
const filePath = this.getActiveTeamPath(cwd);
|
|
81
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
82
|
-
await writeJsonAtomically(filePath, { teamId });
|
|
83
|
-
}
|
|
84
65
|
getTeamKey(cwd, teamId) {
|
|
85
66
|
return `${cwd}:${teamId}`;
|
|
86
67
|
}
|
|
87
|
-
getActiveTeamPath(cwd) {
|
|
88
|
-
return path.join(cwd, this.baseDirectoryName, 'active-team.json');
|
|
89
|
-
}
|
|
90
68
|
getTeamsDirectory(cwd) {
|
|
91
69
|
return path.join(cwd, this.baseDirectoryName, 'teams');
|
|
92
70
|
}
|
|
@@ -116,6 +116,24 @@ export interface SessionContextSnapshot {
|
|
|
116
116
|
warningLevel: ContextWarningLevel;
|
|
117
117
|
compactionCount: number;
|
|
118
118
|
}
|
|
119
|
+
export type TaskSize = 'trivial' | 'simple' | 'large';
|
|
120
|
+
export interface PlanSlice {
|
|
121
|
+
index: number;
|
|
122
|
+
description: string;
|
|
123
|
+
status: 'pending' | 'in_progress' | 'done' | 'skipped';
|
|
124
|
+
completedAt?: string;
|
|
125
|
+
}
|
|
126
|
+
export interface ActivePlan {
|
|
127
|
+
id: string;
|
|
128
|
+
summary: string;
|
|
129
|
+
taskSize: TaskSize;
|
|
130
|
+
createdAt: string;
|
|
131
|
+
confirmedAt: string | null;
|
|
132
|
+
preAuthorized: boolean;
|
|
133
|
+
slices: PlanSlice[];
|
|
134
|
+
/** Null when the plan has no slices (trivial/simple tasks). */
|
|
135
|
+
currentSliceIndex: number | null;
|
|
136
|
+
}
|
|
119
137
|
export interface TeamEngineerRecord {
|
|
120
138
|
name: EngineerName;
|
|
121
139
|
wrapperSessionId: string | null;
|
|
@@ -142,6 +160,7 @@ export interface TeamRecord {
|
|
|
142
160
|
createdAt: string;
|
|
143
161
|
updatedAt: string;
|
|
144
162
|
engineers: TeamEngineerRecord[];
|
|
163
|
+
activePlan?: ActivePlan;
|
|
145
164
|
}
|
|
146
165
|
export interface EngineerTaskResult {
|
|
147
166
|
teamId: string;
|
|
@@ -7,10 +7,7 @@ export declare class TeamStateStore {
|
|
|
7
7
|
getTeam(cwd: string, teamId: string): Promise<TeamRecord | null>;
|
|
8
8
|
listTeams(cwd: string): Promise<TeamRecord[]>;
|
|
9
9
|
updateTeam(cwd: string, teamId: string, update: (team: TeamRecord) => TeamRecord): Promise<TeamRecord>;
|
|
10
|
-
getActiveTeam(cwd: string): Promise<string | null>;
|
|
11
|
-
setActiveTeam(cwd: string, teamId: string): Promise<void>;
|
|
12
10
|
private getTeamKey;
|
|
13
|
-
private getActiveTeamPath;
|
|
14
11
|
private getTeamsDirectory;
|
|
15
12
|
private getTeamPath;
|
|
16
13
|
private enqueueWrite;
|
|
@@ -62,31 +62,9 @@ export class TeamStateStore {
|
|
|
62
62
|
return updated;
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
async getActiveTeam(cwd) {
|
|
66
|
-
const filePath = this.getActiveTeamPath(cwd);
|
|
67
|
-
try {
|
|
68
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
69
|
-
const parsed = JSON.parse(content);
|
|
70
|
-
return parsed.teamId ?? null;
|
|
71
|
-
}
|
|
72
|
-
catch (error) {
|
|
73
|
-
if (isFileNotFoundError(error)) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
async setActiveTeam(cwd, teamId) {
|
|
80
|
-
const filePath = this.getActiveTeamPath(cwd);
|
|
81
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
82
|
-
await writeJsonAtomically(filePath, { teamId });
|
|
83
|
-
}
|
|
84
65
|
getTeamKey(cwd, teamId) {
|
|
85
66
|
return `${cwd}:${teamId}`;
|
|
86
67
|
}
|
|
87
|
-
getActiveTeamPath(cwd) {
|
|
88
|
-
return path.join(cwd, this.baseDirectoryName, 'active-team.json');
|
|
89
|
-
}
|
|
90
68
|
getTeamsDirectory(cwd) {
|
|
91
69
|
return path.join(cwd, this.baseDirectoryName, 'teams');
|
|
92
70
|
}
|