@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.
@@ -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 { getActiveTeamSession, getOrCreatePluginServices, getPersistedActiveTeam, getWrapperSessionMapping, setActiveTeamSession, setPersistedActiveTeam, setWrapperSessionMapping, } from './service-factory.js';
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
- // Adopt the persisted active team if one exists, so a new CTO session
27
- // does not orphan previously created engineers and wrapper memory.
28
- const persistedTeamId = await getPersistedActiveTeam(worktree);
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(worktree, input.sessionID));
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.worktree, context.sessionID));
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 ?? getActiveTeamSession(context.worktree) ?? context.sessionID;
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 = getActiveTeamSession(context.worktree) ?? context.sessionID;
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 = getActiveTeamSession(context.worktree) ?? context.sessionID;
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 setActiveTeamSession(worktree: string, teamId: string): void;
18
- export declare function getActiveTeamSession(worktree: string): string | null;
19
- export declare function getPersistedActiveTeam(worktree: string): Promise<string | null>;
20
- export declare function setPersistedActiveTeam(worktree: string, teamId: string): Promise<void>;
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 setActiveTeamSession(worktree, teamId) {
47
- activeTeamRegistry.set(worktree, teamId);
50
+ export function registerParentSession(childId, parentId) {
51
+ parentSessionRegistry.set(childId, parentId);
48
52
  }
49
- export function getActiveTeamSession(worktree) {
50
- return activeTeamRegistry.get(worktree) ?? null;
53
+ export function getParentSessionId(childId) {
54
+ return parentSessionRegistry.get(childId);
51
55
  }
52
- export async function getPersistedActiveTeam(worktree) {
53
- const services = serviceRegistry.get(worktree);
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 async function setPersistedActiveTeam(worktree, teamId) {
60
- const services = serviceRegistry.get(worktree);
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 getting full ({percent}% estimated). Reuse is still fine, but keep the next prompt focused.',
189
- high: 'Engineer context is heavy ({percent}% estimated, {turns} turns, ${cost}). Prefer a narrowly scoped follow-up or internal compaction.',
190
- critical: 'Engineer context is near capacity ({percent}% estimated). Avoid piling unrelated work into this engineer session.',
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
  }