@nanmicoder/dsh-agent-teams 0.1.17 → 0.1.18

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/lib/tools.js CHANGED
@@ -12,10 +12,11 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm';
12
12
  import { defineTool } from '@deepseek-ai/dsh-tools';
13
13
  import { join } from 'node:path';
14
14
  import { appendTeamEvent, captainSessionOf } from "./events.js";
15
- import { acknowledgeMailbox, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, removeTeamDir, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
16
- import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime, interruptMember, memberActivity, resolveMemberLlmSelection, spawnMember, steerCaptainReport, validateMemberLlmSelections, } from "./members.js";
15
+ import { acknowledgeMailbox, markMailboxDelivered, discardMailboxMessages, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
16
+ import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime, installMemberDelegationGuard, memberActivity, resolveMemberLlmSelection, spawnMember, steerCaptainReport, validateMemberLlmSelections, } from "./members.js";
17
17
  import { TERMINAL_TASK_STATUSES } from "./types.js";
18
- import { installTeamScheduler } from "./scheduler.js";
18
+ import { collectCompletedDependencyOutputs, formatDependencyOutputs, installTeamScheduler } from "./scheduler.js";
19
+ import { installMailboxAdmission, mailboxPrompt } from "./mailbox.js";
19
20
  import { resolveTeamProfile } from "./profiles.js";
20
21
  export { steerCaptainReport } from "./members.js";
21
22
  /** The caller agent, or a loud failure for non-agent callers. */
@@ -158,32 +159,22 @@ function memberOpenTask(team, memberName, exceptTaskId) {
158
159
  && task.assignee === memberName
159
160
  && (task.status === 'claimed' || task.status === 'in_progress'));
160
161
  }
162
+ function taskDetails(team, task) {
163
+ return [task.subject, task.description ?? '',
164
+ `Kind: ${task.kind ?? 'work'}`,
165
+ `Objective: ${task.objective ?? ''}`,
166
+ `In scope: ${(task.inScope ?? []).join(', ')}; Out of scope: ${(task.outOfScope ?? []).join(', ')}`,
167
+ `Acceptance: ${(task.acceptance ?? []).join('; ')}`,
168
+ `Verify: ${(task.verify ?? []).join('; ')}`,
169
+ `Dependency results:\n${formatDependencyOutputs(collectCompletedDependencyOutputs(team.tasks, task.id))}`,
170
+ ].join('\n');
171
+ }
161
172
  /** Captain work is immediate, not a durable scheduler lane: allow one unfinished takeover at a time. */
162
173
  function captainOpenTask(team, exceptTaskId) {
163
174
  return team.tasks.find(task => task.id !== exceptTaskId
164
175
  && task.assignee === CAPTAIN_KEY
165
176
  && !TERMINAL_TASK_STATUSES.includes(task.status));
166
177
  }
167
- async function waitForMemberIdle(ctx, member, signal) {
168
- if (member.id === '')
169
- return;
170
- const live = ctx.agents.get(member.id);
171
- if (live === undefined)
172
- return;
173
- if (signal.aborted)
174
- throw signal.reason;
175
- let onAbort;
176
- const aborted = new Promise((_resolve, reject) => {
177
- onAbort = () => reject(signal.reason ?? new Error('task reassignment was cancelled'));
178
- signal.addEventListener('abort', onAbort, { once: true });
179
- });
180
- try {
181
- await Promise.race([live.whenIdle(), aborted]);
182
- }
183
- finally {
184
- signal.removeEventListener('abort', onAbort);
185
- }
186
- }
187
178
  /** Stop every currently-resident member activation for one halted team.
188
179
  *
189
180
  * Interrupt requests only cancel the member's current model turn and retain its
@@ -192,34 +183,13 @@ async function waitForMemberIdle(ctx, member, signal) {
192
183
  * keep executing after the captain-chat Stop control has reported success.
193
184
  */
194
185
  async function stopTeamMemberActivations(ctx, captain, members, signal) {
195
- const activeMembers = members.filter((member) => member.id !== '' && member.status !== 'removed');
196
- const memberIds = activeMembers.map((member) => member.id);
186
+ const memberIds = members.filter(member => member.id !== '').map(member => member.id);
197
187
  if (memberIds.length === 0)
198
188
  return;
199
- for (const memberId of memberIds)
200
- interruptMember(ctx, captain, memberId);
201
- // `drainContinuableChildren` is available in the current runtime and releases
202
- // the selected activation handles. Keep the quiescence fallback for pre-rc.8
203
- // hosts, where interrupt is the strongest available lifecycle operation.
204
- const runtime = ctx.subagents;
205
- if (runtime.drainContinuableChildren !== undefined) {
206
- try {
207
- await runtime.drainContinuableChildren(captain, memberIds);
208
- return;
209
- }
210
- catch (error) {
211
- // Do not claim the browser action stopped work when the runtime could not
212
- // release all selected child activations. The HTTP route surfaces this
213
- // failure instead of returning a false successful stop.
214
- ctx.logger.warn(`agent-teams: failed to drain halted members: ${String(error)}`);
215
- throw error;
216
- }
217
- }
218
- const fallbackSignal = signal ?? new AbortController().signal;
219
- const results = await Promise.allSettled(activeMembers.map((member) => waitForMemberIdle(ctx, member, fallbackSignal)));
220
- const failed = results.find((result) => result.status === 'rejected');
221
- if (failed?.status === 'rejected')
222
- throw failed.reason;
189
+ // Every supported exact host has targeted recursive drain. Unlike interrupt,
190
+ // it closes admission and clears queued work before awaiting descendants.
191
+ signal?.throwIfAborted();
192
+ await ctx.subagents.drainContinuableChildren(captain, memberIds);
223
193
  }
224
194
  export async function haltTeamWork(input) {
225
195
  const halted = await withTeamLock(teamLockKey(input.stateRoot, input.teamId), async () => {
@@ -310,16 +280,59 @@ export function stagedPlanFeedbackContext(teamName) {
310
280
  */
311
281
  export function registerAgentTeamsTools(ctx, config) {
312
282
  installRetiredMemberGuard(ctx, config.stateDir);
313
- const scheduler = installTeamScheduler(ctx, { stateDir: config.stateDir, executionPrompt: config.executionPrompt });
283
+ installMemberDelegationGuard(ctx, config.stateDir, config.memberMaxDepth ?? 0);
284
+ installMailboxAdmission(ctx, config.stateDir);
285
+ const scheduler = installTeamScheduler(ctx, { stateDir: config.stateDir, executionPrompt: config.executionPrompt, dispatch: dispatchMember });
314
286
  const memberSelections = installMemberSelectionRuntime(ctx, config.stateDir, (workspace, teamId, memberName) => (scheduler.kickMember(workspace, teamId, memberName)));
315
- const updateStagedPlanBatch = async (captain, teamId, mutations, signal) => {
287
+ async function dispatchMember(captain, teamId, memberName, text, signal, mode, attemptId) {
288
+ const root = stateRootOf(workspaceOf(captain), config);
289
+ let orphan;
290
+ try {
291
+ return await withTeamLock(teamLockKey(root, teamId), async () => {
292
+ const team = await readTeam(root, teamId);
293
+ if (team?.captainSessionId !== captain.id || team.halted === true || team.phase === 'staged')
294
+ return false;
295
+ const member = team.members.find(item => item.name === memberName && item.status !== 'removed');
296
+ if (member === undefined || member.stopping === true || team.tasks.some(task => task.reassigning === true && task.assignee === memberName))
297
+ return false;
298
+ if (attemptId !== undefined && !team.tasks.some(task => task.attemptId === attemptId && task.assignee === memberName && (task.status === 'claimed' || task.status === 'in_progress')))
299
+ return false;
300
+ if (member.id !== '')
301
+ return deliverToMember(ctx, captain, member.id, text, signal, mode);
302
+ const selection = await resolveMemberLlmSelection(ctx, captain, {
303
+ provider: member.provider, model: member.model, reasoningEffort: member.reasoningEffort, fallback: member.fallback,
304
+ }, signal);
305
+ await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, team, member, config.stateDir, signal, text);
306
+ orphan = { ...member };
307
+ await writeTeam(root, team);
308
+ orphan = undefined;
309
+ return true;
310
+ });
311
+ }
312
+ catch (error) {
313
+ if (orphan !== undefined) {
314
+ await recordRetiredMemberIds(root, [orphan.id]);
315
+ await stopTeamMemberActivations(ctx, captain, [orphan]);
316
+ }
317
+ ctx.logger.warn(`agent-teams: member dispatch failed for ${memberName}: ${String(error)}`);
318
+ return false;
319
+ }
320
+ }
321
+ const updatePlanBatch = async (captain, teamId, mutations, signal, allowPendingEdits = false) => {
316
322
  if (mutations.length === 0)
317
323
  throw new Error('at least one staged plan operation is required');
318
324
  const workspace = workspaceOf(captain);
319
325
  const stateRoot = stateRootOf(workspace, config);
320
326
  return withTeamLock(teamLockKey(stateRoot, teamId), async () => {
321
327
  const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
322
- requireStagedTeam(fresh);
328
+ const staged = fresh.phase === 'staged';
329
+ if (!staged && !allowPendingEdits)
330
+ requireStagedTeam(fresh);
331
+ if (fresh.halted === true)
332
+ throw new Error('team is halted; resume before editing tasks');
333
+ if (!staged && mutations.some(mutation => mutation.action !== 'update_task')) {
334
+ throw new Error('a running team only permits update_task edits to pending, never-started tasks; roster and removal edits require a staged plan');
335
+ }
323
336
  for (const mutation of mutations) {
324
337
  if (mutation.action === 'update_member') {
325
338
  const member = requireMember(fresh, mutation.memberName);
@@ -339,9 +352,11 @@ export function registerAgentTeamsTools(ctx, config) {
339
352
  }
340
353
  else if (mutation.action === 'update_task') {
341
354
  const task = requireTask(fresh, mutation.taskId);
342
- if (task.status !== 'pending' || (task.attempt ?? 0) !== 0) {
355
+ if (task.status !== 'pending' || (task.attempt ?? 0) !== 0 || task.reassigning === true) {
343
356
  throw new Error(`task "${task.id}" has already started and cannot be edited`);
344
357
  }
358
+ if (!staged && mutation.assignee === CAPTAIN_KEY)
359
+ throw new Error('use reassign_task for captain takeover');
345
360
  const subject = mutation.subject.trim();
346
361
  if (subject === '')
347
362
  throw new Error('task subject must not be empty');
@@ -390,11 +405,25 @@ export function registerAgentTeamsTools(ctx, config) {
390
405
  }
391
406
  }
392
407
  validateStagedGraph(fresh, false);
393
- fresh.planReviewState = 'awaiting_review';
408
+ if (!staged) {
409
+ for (const mutation of mutations) {
410
+ if (mutation.action !== 'update_task')
411
+ continue;
412
+ const task = requireTask(fresh, mutation.taskId);
413
+ const validation = validateCreateTask({ ...fresh, tasks: fresh.tasks.filter(item => item.id !== task.id) }, task);
414
+ if (!validation.ok)
415
+ throw new Error(validation.error ?? 'edited task violates the quality contract');
416
+ }
417
+ }
418
+ else
419
+ fresh.planReviewState = 'awaiting_review';
420
+ signal?.throwIfAborted();
394
421
  await writeTeam(stateRoot, fresh);
395
422
  return fresh;
396
423
  });
397
424
  };
425
+ // Browser review controls retain their staged-only contract.
426
+ const updateStagedPlanBatch = (captain, teamId, mutations, signal) => (updatePlanBatch(captain, teamId, mutations, signal));
398
427
  const updateStagedPlan = async (captain, teamId, mutation, signal) => (updateStagedPlanBatch(captain, teamId, [mutation], signal));
399
428
  const approveStagedTeam = async (captain, teamId, signal) => {
400
429
  const workspace = workspaceOf(captain);
@@ -407,47 +436,22 @@ export function registerAgentTeamsTools(ctx, config) {
407
436
  // placeholders before transitioning to the stricter running shape.
408
437
  fresh.members = fresh.members.filter((member) => member.status !== 'removed');
409
438
  validateStagedGraph(fresh, true);
410
- const spawned = [];
411
- try {
412
- const selections = new Map();
413
- for (const member of fresh.members) {
414
- if (member.id !== '')
415
- continue;
416
- const selection = await resolveMemberLlmSelection(ctx, captain, {
417
- provider: member.provider,
418
- model: member.model,
419
- reasoningEffort: member.reasoningEffort,
420
- fallback: member.fallback,
421
- }, runSignal);
422
- selections.set(member, selection);
423
- member.provider = selection.provider;
424
- member.model = selection.model;
425
- member.reasoningEffort = selection.reasoningEffort;
426
- }
427
- // This is the approval commit barrier: resolve and validate the whole
428
- // final roster before spawning even the first durable child.
429
- await validateMemberLlmSelections(ctx, [...selections.values()], runSignal);
430
- for (const [member, selection] of selections) {
431
- await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, fresh, member, config.stateDir, runSignal);
432
- spawned.push(member);
433
- }
434
- if (fresh.members.some((member) => member.id === '')) {
435
- throw new Error('one or more staged members could not be spawned');
436
- }
437
- fresh.phase = 'running';
438
- delete fresh.planReviewState;
439
- fresh.approvedAt = Date.now();
440
- await writeTeam(stateRoot, fresh);
441
- return { teamId: fresh.id, members: fresh.members.length, tasks: fresh.tasks.length };
442
- }
443
- catch (error) {
444
- await recordRetiredMemberIds(stateRoot, spawned.map((member) => member.id)).catch(() => undefined);
445
- for (const member of spawned) {
446
- if (member.id !== '')
447
- interruptMember(ctx, captain, member.id);
448
- }
449
- throw error;
439
+ const selections = [];
440
+ for (const member of fresh.members) {
441
+ const selection = await resolveMemberLlmSelection(ctx, captain, {
442
+ provider: member.provider, model: member.model, reasoningEffort: member.reasoningEffort, fallback: member.fallback,
443
+ }, runSignal);
444
+ selections.push(selection);
445
+ member.provider = selection.provider;
446
+ member.model = selection.model;
447
+ member.reasoningEffort = selection.reasoningEffort;
450
448
  }
449
+ await validateMemberLlmSelections(ctx, selections, runSignal);
450
+ fresh.phase = 'running';
451
+ delete fresh.planReviewState;
452
+ fresh.approvedAt = Date.now();
453
+ await writeTeam(stateRoot, fresh);
454
+ return { teamId: fresh.id, members: fresh.members.length, tasks: fresh.tasks.length };
451
455
  });
452
456
  try {
453
457
  await scheduler.kickTeam(workspace, teamId, captain);
@@ -547,6 +551,21 @@ export function registerAgentTeamsTools(ctx, config) {
547
551
  name: { type: 'string', required: true, description: 'Name for the new team (used as its stable id).' },
548
552
  description: { type: 'string', description: 'Team purpose / the goal the team will work on.' },
549
553
  profile: { type: 'string', description: 'Optional configured profile name.' },
554
+ plan: {
555
+ type: 'object', additionalProperties: false,
556
+ description: 'Optional complete ordinary-work roster and DAG in one atomic call, instead of separate add_member/create_task rounds. Mutually exclusive with profile. For quality gates use create_task with the explicit quality contract.',
557
+ properties: {
558
+ members: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
559
+ name: { type: 'string', required: true }, role: { type: 'string' },
560
+ provider: { type: 'string' }, model: { type: 'string' }, reasoning_effort: { type: 'string' },
561
+ } } },
562
+ tasks: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
563
+ id: { type: 'string', required: true, description: 'Local reference used by dependencies in this plan; the result maps it to a durable task id.' },
564
+ subject: { type: 'string', required: true }, description: { type: 'string' }, assignee: { type: 'string' },
565
+ dependencies: { type: 'array', items: { type: 'string' } },
566
+ } } },
567
+ },
568
+ },
550
569
  approval: {
551
570
  type: 'string',
552
571
  enum: ['required', 'automatic'],
@@ -570,9 +589,10 @@ export function registerAgentTeamsTools(ctx, config) {
570
589
  },
571
590
  render: (args, value) => [{
572
591
  type: 'text',
573
- text: value.phase === 'staged'
592
+ text: (value.phase === 'staged'
574
593
  ? `Team "${value.team_name}" plan created under ${value.state_dir}. It is staged: finish the roster and DAG, then wait for the user to edit and approve it. Do not start or approve it yourself.`
575
- : `Team "${value.team_name}" created (id ${value.team_id}) under ${value.state_dir}. You are the captain.`,
594
+ : `Team "${value.team_name}" created (id ${value.team_id}) under ${value.state_dir}. You are the captain.`)
595
+ + (value.tasks === undefined ? '' : '\n' + value.tasks.map(task => `${task.task_id} [${task.seed_id}]: ${task.subject}; assignee=${task.assignee ?? 'unassigned'}; dependencies=${task.dependencies.join(',')}`).join('\n')),
576
596
  }],
577
597
  },
578
598
  async execute(args, exec) {
@@ -590,6 +610,8 @@ export function registerAgentTeamsTools(ctx, config) {
590
610
  const profileName = args.profile !== undefined && args.profile.trim() !== ''
591
611
  ? args.profile.trim()
592
612
  : undefined;
613
+ if (profileName !== undefined && args.plan !== undefined)
614
+ throw new Error('choose either a configured profile or an inline plan');
593
615
  const created = await withTeamLock(captainLockKey(stateRoot, captain.id), async () => {
594
616
  const current = await findTeamByParticipant(stateRoot, captain.id);
595
617
  if (current !== undefined) {
@@ -604,7 +626,7 @@ export function registerAgentTeamsTools(ctx, config) {
604
626
  if (existing !== undefined) {
605
627
  throw new Error(`team id "${teamId}" is taken by another captain — pick a different team name`);
606
628
  }
607
- if (profileName === undefined) {
629
+ if (profileName === undefined && args.plan === undefined) {
608
630
  const state = {
609
631
  name: teamName,
610
632
  id: teamId,
@@ -628,7 +650,8 @@ export function registerAgentTeamsTools(ctx, config) {
628
650
  stateRoot,
629
651
  teamName,
630
652
  teamId,
631
- profileName,
653
+ profileName: profileName ?? 'inline-plan',
654
+ inlinePlan: args.plan,
632
655
  description: args.description,
633
656
  staged,
634
657
  });
@@ -673,7 +696,7 @@ export function registerAgentTeamsTools(ctx, config) {
673
696
  }
674
697
  const persisted = await readTeam(stateRoot, created.state.id).catch(() => undefined);
675
698
  const snapshot = persisted ?? created.state;
676
- if (snapshot.profile === undefined) {
699
+ if (snapshot.profile === undefined && args.plan === undefined) {
677
700
  return {
678
701
  team_id: snapshot.id,
679
702
  team_name: snapshot.name,
@@ -686,8 +709,8 @@ export function registerAgentTeamsTools(ctx, config) {
686
709
  team_name: snapshot.name,
687
710
  state_dir: join(stateRoot, snapshot.id),
688
711
  phase: snapshot.phase ?? 'running',
689
- profile: snapshot.profile.name,
690
- task_planning: snapshot.profile.taskPlanning ?? 'seed',
712
+ ...snapshot.profile === undefined ? {} : { profile: snapshot.profile.name },
713
+ task_planning: snapshot.profile?.taskPlanning ?? 'seed',
691
714
  members: snapshot.members.map((member) => ({
692
715
  member_name: member.name,
693
716
  member_id: member.id,
@@ -710,12 +733,12 @@ export function registerAgentTeamsTools(ctx, config) {
710
733
  }));
711
734
  ctx.tools.register(defineTool({
712
735
  name: 'agent_teams_edit_plan',
713
- description: 'Atomically revise the current staged AgentTeams plan without spawning members or scheduling tasks. Use this when the user continues chatting to change a plan that is waiting for approval. Submit dependent edits in order (update downstream dependencies or assignees, then remove tasks, then remove unused members). Never inspect or edit .agent-teams state files or plugin source code to revise a plan.',
736
+ description: 'Atomically revise an AgentTeams plan. While staged, edit tasks and roster without starting work. While running, only update_task is allowed, and only for pending tasks with no prior attempt: correct dependencies, assignees or descriptions before they start; newly ready work is scheduled after commit. Never edit active/finished attempts. Submit dependent edits in order. Never modify .agent-teams files directly.',
714
737
  parameters: {
715
738
  operations: {
716
739
  type: 'array',
717
740
  required: true,
718
- description: 'One atomic, ordered batch of staged-plan edits. If any operation is invalid, none of the edits are saved.',
741
+ description: 'One atomic, ordered batch. Running teams allow only update_task for never-started pending tasks. If any operation is invalid, none of the edits are saved.',
719
742
  items: {
720
743
  type: 'object',
721
744
  additionalProperties: false,
@@ -756,14 +779,13 @@ export function registerAgentTeamsTools(ctx, config) {
756
779
  },
757
780
  render: (_args, value) => [{
758
781
  type: 'text',
759
- text: `Staged plan updated atomically (${value.members} members, ${value.tasks} tasks, ${value.dependencies} dependencies). No members were spawned and no tasks were scheduled.\n${value.graph.join('\n')}`,
782
+ text: `${value.status === 'staged' ? 'Staged plan' : 'Pending task graph'} updated atomically (${value.members} members, ${value.tasks} tasks, ${value.dependencies} dependencies). ${value.status === 'staged' ? 'No members were spawned and no tasks were scheduled.' : 'The scheduler will dispatch any newly ready work.'}\n${value.graph.join('\n')}`,
760
783
  }],
761
784
  },
762
785
  async execute(args, exec) {
763
786
  const captain = requireCaptain(exec);
764
787
  const workspace = workspaceOf(captain);
765
788
  const team = await requireCaptainTeam(workspace, config, captain);
766
- requireStagedTeam(team);
767
789
  if (args.operations.length === 0)
768
790
  throw new Error('at least one staged plan operation is required');
769
791
  const mutations = args.operations.map((operation, index) => {
@@ -820,9 +842,11 @@ export function registerAgentTeamsTools(ctx, config) {
820
842
  throw new Error(`${label} requires member_name`);
821
843
  return { action: 'remove_member', memberName };
822
844
  });
823
- const updated = await updateStagedPlanBatch(captain, team.id, mutations, exec.signal);
845
+ const updated = await updatePlanBatch(captain, team.id, mutations, exec.signal, true);
846
+ if (updated.phase !== 'staged')
847
+ await scheduler.kickTeam(workspace, team.id, captain);
824
848
  return {
825
- status: 'staged',
849
+ status: updated.phase ?? 'running',
826
850
  team_id: updated.id,
827
851
  members: updated.members.length,
828
852
  tasks: updated.tasks.length,
@@ -866,7 +890,7 @@ export function registerAgentTeamsTools(ctx, config) {
866
890
  }));
867
891
  ctx.tools.register(defineTool({
868
892
  name: 'agent_teams_add_member',
869
- description: 'Add a member to the team roster. In a staged team this only adds an editable plan row and does not spawn a child; approval spawns the final configuration. In a running team it creates the durable continuable member immediately.',
893
+ description: 'Add a member to the team roster. Planning and idle roster rows do not call a model. After approval, the member session starts with its first ready task or explicit message and remains durable for later work.',
870
894
  parameters: {
871
895
  name: { type: 'string', required: true, description: 'Unique member name inside the team.' },
872
896
  role: { type: 'string', description: 'Role of the member (e.g. researcher, engineer, reviewer).' },
@@ -893,7 +917,7 @@ export function registerAgentTeamsTools(ctx, config) {
893
917
  type: 'text',
894
918
  text: value.phase === 'staged'
895
919
  ? `Member "${value.member_name}" added to the staged roster (${value.provider}/${value.model}); no child was spawned.`
896
- : `Member "${value.member_name}" added (subagent id ${value.member_id}, ${value.provider}/${value.model}${value.reasoning_effort === undefined ? '' : `, reasoning ${value.reasoning_effort}`}, status ${value.status}).`,
920
+ : `Member "${value.member_name}" added (session ${value.member_id || 'starts with first ready task'}, ${value.provider}/${value.model}${value.reasoning_effort === undefined ? '' : `, reasoning ${value.reasoning_effort}`}, status ${value.status}).`,
897
921
  }],
898
922
  },
899
923
  async execute(args, exec) {
@@ -935,23 +959,9 @@ export function registerAgentTeamsTools(ctx, config) {
935
959
  joinedAt: Date.now(),
936
960
  status: 'idle',
937
961
  };
938
- if (fresh.phase !== 'staged') {
939
- await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, fresh, member, config.stateDir, exec.signal);
940
- }
962
+ await validateMemberLlmSelections(ctx, [selection], exec.signal);
941
963
  fresh.members.push(member);
942
- try {
943
- await writeTeam(stateRoot, fresh);
944
- }
945
- catch (error) {
946
- // The continuable child is already live, but the durable team record
947
- // never saw it. Retire the orphan so it disappears from subagent
948
- // listings and cannot be resumed, then surface the write failure.
949
- if (member.id !== '') {
950
- await recordRetiredMemberIds(stateRoot, [member.id]).catch(() => undefined);
951
- interruptMember(ctx, captain, member.id);
952
- }
953
- throw error;
954
- }
964
+ await writeTeam(stateRoot, fresh);
955
965
  appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/member-added', {
956
966
  teamId: fresh.id,
957
967
  memberId: member.id,
@@ -971,7 +981,8 @@ export function registerAgentTeamsTools(ctx, config) {
971
981
  };
972
982
  });
973
983
  await scheduler.kickMember(workspace, team.id, created.member_name, captain);
974
- return created;
984
+ const latest = (await readTeam(stateRoot, team.id))?.members.find(member => member.name === created.member_name);
985
+ return latest === undefined ? created : { ...created, member_id: latest.id, status: latest.status };
975
986
  },
976
987
  }));
977
988
  ctx.tools.register(defineTool({
@@ -1002,7 +1013,9 @@ export function registerAgentTeamsTools(ctx, config) {
1002
1013
  const team = await requireCaptainTeam(workspace, config, captain);
1003
1014
  const revoked = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
1004
1015
  const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
1005
- const member = requireMember(fresh, args.name);
1016
+ const member = fresh.members.find(item => item.name === args.name);
1017
+ if (member === undefined)
1018
+ throw new Error(`no member \"${args.name}\" in team \"${fresh.name}\"`);
1006
1019
  const requeued = [];
1007
1020
  for (const task of fresh.tasks) {
1008
1021
  if (task.assignee !== member.name || task.status === 'completed')
@@ -1012,6 +1025,7 @@ export function registerAgentTeamsTools(ctx, config) {
1012
1025
  requeued.push(task.id);
1013
1026
  }
1014
1027
  member.status = 'removed';
1028
+ await discardMailboxMessages(stateRoot, fresh.id, member.name, (await readUnreadMailbox(stateRoot, fresh.id, member.name)).map(message => message.id));
1015
1029
  await writeTeam(stateRoot, fresh);
1016
1030
  appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/member-removed', {
1017
1031
  teamId: fresh.id,
@@ -1021,8 +1035,7 @@ export function registerAgentTeamsTools(ctx, config) {
1021
1035
  });
1022
1036
  if (revoked.member.id !== '') {
1023
1037
  await recordRetiredMemberIds(stateRoot, [revoked.member.id]);
1024
- interruptMember(ctx, captain, revoked.member.id);
1025
- await waitForMemberIdle(ctx, revoked.member, exec.signal);
1038
+ await stopTeamMemberActivations(ctx, captain, [revoked.member], exec.signal);
1026
1039
  }
1027
1040
  await scheduler.kickTeam(workspace, team.id, captain);
1028
1041
  return {
@@ -1034,7 +1047,7 @@ export function registerAgentTeamsTools(ctx, config) {
1034
1047
  }));
1035
1048
  ctx.tools.register(defineTool({
1036
1049
  name: 'agent_teams_create_task',
1037
- description: 'Create a task in your team\'s task list. Every call must include a non-empty subject, including verification and review tasks. Tasks can depend on other tasks (dependencies): a task is only claimable once every dependency is completed. Optionally assign it to a member, who still claims it before working.',
1050
+ description: 'Create a task in your team\'s task list. Use kind=work (default) for research, repository audits and general tasks. kind=review is only a quality gate for an existing implementation/repair task via reviewedTaskId. Every call must include a non-empty subject, including verification and review tasks. Tasks can depend on other tasks (dependencies): a task is only claimable once every dependency is completed. Optionally assign it to a member, who still claims it before working.',
1038
1051
  parameters: {
1039
1052
  subject: { type: 'string', required: true, description: 'Required non-empty title for this task. Never omit it, including for verification or review tasks.' },
1040
1053
  description: { type: 'string', description: 'What needs to be done, in detail.' },
@@ -1043,11 +1056,11 @@ export function registerAgentTeamsTools(ctx, config) {
1043
1056
  items: { type: 'string' },
1044
1057
  description: 'Task ids this task depends on (must be completed before this task can be claimed).',
1045
1058
  },
1046
- assignee: { type: 'string', description: 'Optional member name this task is intended for.' },
1059
+ assignee: { type: 'string', description: 'Member name when an owner is specified. Omission puts this task in the shared pool; roles, subjects and descriptions do not assign an owner.' },
1047
1060
  kind: {
1048
1061
  type: 'string',
1049
1062
  enum: ['work', 'requirements', 'implementation', 'verification', 'review', 'repair', 'integration'],
1050
- description: 'Task kind. Defaults to work (legacy, no quality gates). Quality kinds require a contract.',
1063
+ description: 'Explicit task kind. Omission means work with no quality gates, even if the subject says implementation/review. Quality kinds require a contract. An implementation may be planned before requirements passes when its dependency chain includes that requirements task.',
1051
1064
  },
1052
1065
  round: { type: 'number', description: '1-based review / requirements / repair round.' },
1053
1066
  objective: { type: 'string', description: 'Required non-empty objective for quality kinds.' },
@@ -1057,7 +1070,7 @@ export function registerAgentTeamsTools(ctx, config) {
1057
1070
  verify: { type: 'array', items: { type: 'string' }, description: 'Verification commands. Required for implementation/repair.' },
1058
1071
  deliverables: { type: 'array', items: { type: 'string' }, description: 'Expected deliverable paths or names.' },
1059
1072
  nonGoals: { type: 'array', items: { type: 'string' }, description: 'Explicit non-goals.' },
1060
- reviewedTaskId: { type: 'string', description: 'Task being reviewed. Required for kind=review.' },
1073
+ reviewedTaskId: { type: 'string', description: 'Existing AgentTeams implementation/repair task id. Required for kind=review; an external repository audit is kind=work.' },
1061
1074
  sourceTaskId: { type: 'string', description: 'Source implementation/artifact. Required for kind=repair.' },
1062
1075
  sourceFindingIds: { type: 'array', items: { type: 'string' }, description: 'Finding ids this repair must close.' },
1063
1076
  coverageOf: { type: 'array', items: { type: 'string' }, description: 'User-constraint / goal items this task covers.' },
@@ -1072,12 +1085,13 @@ export function registerAgentTeamsTools(ctx, config) {
1072
1085
  task_id: { type: 'string', required: true },
1073
1086
  subject: { type: 'string', required: true },
1074
1087
  status: { type: 'string', required: true },
1088
+ kind: { type: 'string', required: true },
1075
1089
  assignee: { type: 'string' },
1076
1090
  },
1077
1091
  },
1078
1092
  render: (args, value) => [{
1079
1093
  type: 'text',
1080
- text: `Task "${value.subject}" created as ${value.task_id} (status ${value.status}${value.assignee ? `, assigned to ${value.assignee}` : ''}).`,
1094
+ text: `Task "${value.subject}" created as ${value.task_id} (status ${value.status}, kind ${value.kind}, ${value.assignee ? `assigned to ${value.assignee}` : 'unassigned shared pool'}).`,
1081
1095
  }],
1082
1096
  },
1083
1097
  async execute(args, exec) {
@@ -1182,6 +1196,7 @@ export function registerAgentTeamsTools(ctx, config) {
1182
1196
  task_id: task.id,
1183
1197
  subject: task.subject,
1184
1198
  status: task.status,
1199
+ kind: taskKindOf(task),
1185
1200
  ...task.assignee !== undefined ? { assignee: task.assignee } : {},
1186
1201
  };
1187
1202
  });
@@ -1228,8 +1243,12 @@ export function registerAgentTeamsTools(ctx, config) {
1228
1243
  const task = requireTask(fresh, args.task_id);
1229
1244
  if (task.status === 'completed')
1230
1245
  throw new Error(`completed task ${task.id} is immutable and cannot be reassigned`);
1231
- if (task.reassigning === true)
1232
- throw new Error(`task ${task.id} is already being reassigned`);
1246
+ if (task.reassigning === true) {
1247
+ const previousMember = fresh.members.find(member => member.id === task.handoffFromMemberId && member.stopping === true);
1248
+ if (task.assignee !== target || previousMember === undefined)
1249
+ throw new Error(`task ${task.id} is already being reassigned`);
1250
+ return { previousAssignee: previousMember.name, previousMember: { ...previousMember }, handoffId: task.handoffId };
1251
+ }
1233
1252
  const targetMember = target === CAPTAIN_KEY ? undefined : requireMember(fresh, target);
1234
1253
  if (target === CAPTAIN_KEY) {
1235
1254
  const busy = captainOpenTask(fresh, task.id);
@@ -1253,6 +1272,11 @@ export function registerAgentTeamsTools(ctx, config) {
1253
1272
  ? undefined
1254
1273
  : fresh.members.find(member => member.name === task.assignee && member.status !== 'removed');
1255
1274
  invalidateTaskAttempt(task, target, true);
1275
+ if (previousMember !== undefined) {
1276
+ previousMember.stopping = true;
1277
+ task.handoffFromMemberId = previousMember.id;
1278
+ await discardMailboxMessages(stateRoot, fresh.id, previousMember.name, (await readUnreadMailbox(stateRoot, fresh.id, previousMember.name)).map(message => message.id));
1279
+ }
1256
1280
  await writeTeam(stateRoot, fresh);
1257
1281
  return {
1258
1282
  previousAssignee,
@@ -1262,9 +1286,8 @@ export function registerAgentTeamsTools(ctx, config) {
1262
1286
  });
1263
1287
  let quiescenceError;
1264
1288
  if (revoked.previousMember !== undefined) {
1265
- interruptMember(ctx, captain, revoked.previousMember.id);
1266
1289
  try {
1267
- await waitForMemberIdle(ctx, revoked.previousMember, exec.signal);
1290
+ await stopTeamMemberActivations(ctx, captain, [revoked.previousMember], exec.signal);
1268
1291
  }
1269
1292
  catch (error) {
1270
1293
  quiescenceError = error;
@@ -1276,7 +1299,13 @@ export function registerAgentTeamsTools(ctx, config) {
1276
1299
  if (task.handoffId !== revoked.handoffId || task.assignee !== target || task.reassigning !== true) {
1277
1300
  throw new Error(`task ${task.id} changed during reassignment; refusing to overwrite the newer state`);
1278
1301
  }
1279
- task.reassigning = false;
1302
+ task.reassigning = quiescenceError !== undefined;
1303
+ if (quiescenceError === undefined) {
1304
+ const previous = fresh.members.find(member => member.id === revoked.previousMember?.id);
1305
+ if (previous !== undefined)
1306
+ delete previous.stopping;
1307
+ delete task.handoffFromMemberId;
1308
+ }
1280
1309
  if (quiescenceError === undefined && target === CAPTAIN_KEY) {
1281
1310
  beginTaskAttempt(task, CAPTAIN_KEY);
1282
1311
  // The captain is already in the turn that requested takeover; there
@@ -1328,11 +1357,12 @@ export function registerAgentTeamsTools(ctx, config) {
1328
1357
  assignee: { type: 'string', required: true },
1329
1358
  attempt: { type: 'number', required: true },
1330
1359
  attempt_id: { type: 'string' },
1360
+ task_details: { type: 'string', required: true },
1331
1361
  },
1332
1362
  },
1333
1363
  render: (args, value) => [{
1334
1364
  type: 'text',
1335
- text: `Task ${value.task_id} claimed by ${value.assignee} (attempt ${value.attempt}${value.attempt_id ? `, attempt_id ${value.attempt_id}` : ''}, status ${value.status}).`,
1365
+ text: `Task ${value.task_id} claimed by ${value.assignee} (attempt ${value.attempt}${value.attempt_id ? `, attempt_id ${value.attempt_id}` : ''}, status ${value.status}).\n${value.task_details}`,
1336
1366
  }],
1337
1367
  },
1338
1368
  async execute(args, exec) {
@@ -1371,6 +1401,7 @@ export function registerAgentTeamsTools(ctx, config) {
1371
1401
  throw new Error(`task ${task.id} is already claimed by "${task.assignee ?? 'nobody'}"`);
1372
1402
  }
1373
1403
  return {
1404
+ task_details: taskDetails(fresh, task),
1374
1405
  task_id: task.id,
1375
1406
  status: task.status,
1376
1407
  assignee,
@@ -1401,6 +1432,7 @@ export function registerAgentTeamsTools(ctx, config) {
1401
1432
  assignee: task.assignee,
1402
1433
  });
1403
1434
  return {
1435
+ task_details: taskDetails(fresh, task),
1404
1436
  task_id: task.id,
1405
1437
  status: task.status,
1406
1438
  assignee: task.assignee ?? '',
@@ -1415,13 +1447,13 @@ export function registerAgentTeamsTools(ctx, config) {
1415
1447
  description: 'Update a task status/output. Members must supply the current attempt_id returned by claim_task; stale attempts are rejected after takeover/reassignment. Terminal results are immutable. A captain must use reassign_task(assignee="captain") before updating member-owned work.',
1416
1448
  parameters: {
1417
1449
  task_id: { type: 'string', required: true, description: 'The task id to update.' },
1450
+ attempt_id: { type: 'string', description: 'Members must explicitly include the current attempt_id from their assignment/claim in EVERY update, including failed reviews with findings. If omitted, retry with the same current id; omission does not revoke the attempt.' },
1418
1451
  status: {
1419
1452
  type: 'string',
1420
1453
  enum: ['in_progress', 'completed', 'failed', 'cancelled'],
1421
1454
  description: 'New status (in_progress, completed, failed, cancelled).',
1422
1455
  },
1423
1456
  output: { type: 'string', description: 'Result summary; set when completing or failing.' },
1424
- attempt_id: { type: 'string', description: 'Current execution capability returned by claim_task (required for members when present on the task).' },
1425
1457
  verdict: {
1426
1458
  type: 'string',
1427
1459
  enum: ['pass', 'needs_revision', 'reject'],
@@ -1504,13 +1536,17 @@ export function registerAgentTeamsTools(ctx, config) {
1504
1536
  const task = requireTask(fresh, args.task_id);
1505
1537
  if (identity.kind === 'captain'
1506
1538
  && task.assignee !== undefined
1507
- && task.assignee !== CAPTAIN_KEY) {
1539
+ && task.assignee !== CAPTAIN_KEY
1540
+ && !(args.status === 'cancelled' && task.status === 'pending' && (task.attempt ?? 0) === 0 && task.reassigning !== true)) {
1508
1541
  throw new Error(`task ${task.id} is owned by member "${task.assignee}"; call agent_teams_reassign_task with assignee="captain" before takeover`);
1509
1542
  }
1510
1543
  if (identity.kind === 'member') {
1511
1544
  if (task.assignee !== identity.name) {
1512
1545
  throw new Error(`task ${task.id} is assigned to "${task.assignee ?? 'nobody'}", not you`);
1513
1546
  }
1547
+ if (task.attemptId !== undefined && (args.attempt_id === undefined || args.attempt_id.trim() === '')) {
1548
+ throw new Error(`missing attempt_id for task ${task.id}. Retry this update with attempt_id="${task.attemptId}" from your current assignment. This is a missing parameter, not a revoked attempt; do not restart the work or request reassignment.`);
1549
+ }
1514
1550
  if (task.attemptId !== undefined && args.attempt_id !== task.attemptId) {
1515
1551
  throw new Error(`stale attempt for task ${task.id}: expected the current attempt_id; stop work and request fresh assignment`);
1516
1552
  }
@@ -1607,7 +1643,7 @@ export function registerAgentTeamsTools(ctx, config) {
1607
1643
  }));
1608
1644
  ctx.tools.register(defineTool({
1609
1645
  name: 'agent_teams_send_message',
1610
- description: 'Send a message to the captain or to a teammate. Messages go straight into the recipient\'s mailbox; when the captain agent is online the plugin also schedules live delivery (member recipients get the message as their next turn; a running captain sees it at the nearest model step). No relay is involved: teammates talk to each other directly, exactly like the Claude Code AgentTeams mailbox model.',
1646
+ description: 'Send coordination or current-task guidance directly to the captain or a teammate. A running recipient receives it at the next model step; an idle recipient wakes. Messages are durably retained until read. Use task creation/reassignment for a new unit of work, not repeated status nudges.',
1611
1647
  parameters: {
1612
1648
  to: { type: 'string', required: true, description: 'Recipient: "captain" or a member name.' },
1613
1649
  content: { type: 'string', required: true, description: 'The message text.' },
@@ -1660,7 +1696,10 @@ export function registerAgentTeamsTools(ctx, config) {
1660
1696
  throw new Error(`team "${fresh.name}" is halted; call agent_teams_resume before waking a member`);
1661
1697
  }
1662
1698
  const recipient = requireMember(fresh, to);
1663
- const message = { ...createMessage(from, recipient.name, args.content), deliveryClaimedAt: Date.now() };
1699
+ const owned = memberOpenTask(fresh, recipient.name);
1700
+ const message = { ...createMessage(from, recipient.name, args.content), deliveryClaimedAt: Date.now(),
1701
+ ...owned?.attemptId === undefined ? {} : { taskId: owned.id, attemptId: owned.attemptId },
1702
+ };
1664
1703
  await appendMailbox(stateRoot, fresh.id, recipient.name, message);
1665
1704
  appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, caller.session), 'agent-teams/message-sent', {
1666
1705
  teamId: fresh.id,
@@ -1678,10 +1717,10 @@ export function registerAgentTeamsTools(ctx, config) {
1678
1717
  if (prepared.kind === 'captain') {
1679
1718
  let delivered = 'mailbox';
1680
1719
  if (captain !== undefined && prepared.identity.kind === 'member') {
1681
- delivered = steerCaptainReport(captain, prepared.from, args.content) ? 'live' : 'mailbox';
1720
+ delivered = steerCaptainReport(captain, prepared.from, args.content, mailboxPrompt(prepared.fresh.id, CAPTAIN_KEY, [prepared.message])) ? 'live' : 'mailbox';
1682
1721
  }
1683
1722
  if (delivered === 'live') {
1684
- await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (acknowledgeMailbox(stateRoot, prepared.fresh.id, CAPTAIN_KEY, [prepared.message.id])));
1723
+ await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (markMailboxDelivered(stateRoot, prepared.fresh.id, CAPTAIN_KEY, [prepared.message.id])));
1685
1724
  }
1686
1725
  else {
1687
1726
  await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (releaseMailboxDelivery(stateRoot, prepared.fresh.id, CAPTAIN_KEY, [prepared.message.id])));
@@ -1689,15 +1728,12 @@ export function registerAgentTeamsTools(ctx, config) {
1689
1728
  return { message_id: prepared.message.id, from: prepared.from, to: CAPTAIN_KEY, delivered };
1690
1729
  }
1691
1730
  let delivered = 'mailbox';
1692
- if (captain !== undefined && prepared.recipient.id !== '') {
1693
- const senderText = prepared.from === CAPTAIN_KEY
1694
- ? args.content
1695
- : `Message from team member ${prepared.from}:\n\n${args.content}`;
1696
- const text = `AgentTeams state policy: inspect ${config.stateDir}/${prepared.fresh.id}/ read-only; never edit team.json or inbox files directly. Use agent_teams_* tools for team state.\n\n${senderText}`;
1697
- const accepted = await deliverToMember(ctx, captain, prepared.recipient.id, text, exec.signal);
1731
+ if (captain !== undefined) {
1732
+ const text = mailboxPrompt(prepared.fresh.id, prepared.recipient.name, [prepared.message]);
1733
+ const accepted = await dispatchMember(captain, prepared.fresh.id, prepared.recipient.name, text, exec.signal, 'steer', prepared.message.attemptId);
1698
1734
  delivered = accepted ? 'wake' : 'mailbox';
1699
1735
  if (accepted) {
1700
- await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (acknowledgeMailbox(stateRoot, prepared.fresh.id, prepared.recipient.name, [prepared.message.id])));
1736
+ await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (markMailboxDelivered(stateRoot, prepared.fresh.id, prepared.recipient.name, [prepared.message.id])));
1701
1737
  }
1702
1738
  }
1703
1739
  if (delivered === 'mailbox') {
@@ -1767,6 +1803,7 @@ export function registerAgentTeamsTools(ctx, config) {
1767
1803
  const captainInbox = identity.kind === 'captain'
1768
1804
  ? await readUnreadMailbox(stateRoot, team.id, CAPTAIN_KEY, reportMalformed(CAPTAIN_KEY))
1769
1805
  : [];
1806
+ const ownInbox = identity.kind === 'member' ? (await readUnreadMailbox(stateRoot, team.id, identity.name)).slice(0, 10) : [];
1770
1807
  const memberInboxes = {};
1771
1808
  const visibleMembers = identity.kind === 'captain'
1772
1809
  ? members
@@ -1813,18 +1850,19 @@ export function registerAgentTeamsTools(ctx, config) {
1813
1850
  viewer: identity.name,
1814
1851
  members,
1815
1852
  tasks,
1816
- captain_inbox: captainInbox.slice(-10).map((message) => ({
1853
+ captain_inbox: captainInbox.slice(0, 10).map((message) => ({
1817
1854
  from: message.from,
1818
1855
  content: message.content,
1819
1856
  ts: message.ts,
1820
1857
  })),
1858
+ member_inbox: ownInbox.map(message => ({ from: message.from, content: message.content, ts: message.ts })),
1821
1859
  member_inboxes: memberInboxes,
1822
1860
  mailbox_warnings: mailboxWarnings,
1823
1861
  mailbox_warning_count: mailboxWarningCount,
1824
1862
  };
1825
1863
  const acknowledged = identity.kind === 'captain'
1826
- ? captainInbox.map(message => message.id)
1827
- : await readUnreadMailbox(stateRoot, team.id, identity.name).then(messages => messages.map(message => message.id));
1864
+ ? captainInbox.slice(0, 10).map(message => message.id)
1865
+ : ownInbox.map(message => message.id);
1828
1866
  if (acknowledged.length > 0) {
1829
1867
  await withTeamLock(teamLockKey(stateRoot, team.id), () => (acknowledgeMailbox(stateRoot, team.id, identity.kind === 'captain' ? CAPTAIN_KEY : identity.name, acknowledged)));
1830
1868
  }
@@ -1913,6 +1951,7 @@ export function registerAgentTeamsTools(ctx, config) {
1913
1951
  // retires durable catalog entries left behind by remove_member.
1914
1952
  const roster = fresh.members.map(member => ({ ...member }));
1915
1953
  for (const member of fresh.members) {
1954
+ await discardMailboxMessages(stateRoot, fresh.id, member.name, (await readUnreadMailbox(stateRoot, fresh.id, member.name)).map(message => message.id));
1916
1955
  if (member.status === 'removed')
1917
1956
  continue;
1918
1957
  member.status = 'removed';
@@ -1925,17 +1964,7 @@ export function registerAgentTeamsTools(ctx, config) {
1925
1964
  return roster;
1926
1965
  });
1927
1966
  await recordRetiredMemberIds(stateRoot, members.map(member => member.id));
1928
- for (const member of members) {
1929
- if (member.id === '')
1930
- continue;
1931
- interruptMember(ctx, captain, member.id);
1932
- }
1933
- const quiescence = await Promise.allSettled(members.map(member => waitForMemberIdle(ctx, member, exec.signal)));
1934
- for (const result of quiescence) {
1935
- if (result.status === 'rejected') {
1936
- ctx.logger.warn(`agent-teams: member did not quiesce cleanly before team archive: ${String(result.reason)}`);
1937
- }
1938
- }
1967
+ await stopTeamMemberActivations(ctx, captain, members, exec.signal);
1939
1968
  await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
1940
1969
  const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
1941
1970
  appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/team-deleted', {
@@ -1951,7 +1980,7 @@ export function registerAgentTeamsTools(ctx, config) {
1951
1980
  return runtime;
1952
1981
  }
1953
1982
  async function initializeProfileTeam(input) {
1954
- const profile = resolveTeamProfile(input.config.profiles, input.profileName, input.config.maxMembers);
1983
+ const profile = resolveTeamProfile(input.inlinePlan === undefined ? input.config.profiles : { [input.profileName]: input.inlinePlan }, input.profileName, input.config.maxMembers);
1955
1984
  const selections = [];
1956
1985
  for (const template of profile.members) {
1957
1986
  selections.push(await resolveMemberLlmSelection(input.ctx, input.captain, {
@@ -2011,50 +2040,12 @@ async function initializeProfileTeam(input) {
2011
2040
  })),
2012
2041
  taskSeq: profile.tasks.length,
2013
2042
  };
2014
- if (input.staged) {
2015
- await createTeamDir(input.stateRoot, draft);
2016
- return { committed: true, state: draft };
2017
- }
2018
- const spawned = [];
2019
- try {
2020
- for (const member of draft.members) {
2021
- const selection = selections[spawned.length];
2022
- await spawnMember(input.ctx, memberRuntime(input.config), input.memberSelections, selection, input.captain, draft, member, input.config.stateDir, input.exec.signal);
2023
- spawned.push(member);
2024
- }
2025
- if (draft.members.some((member) => member.id === '')) {
2026
- throw new Error(`failed to initialize profile "${profile.name}": a spawned member is missing its child id`);
2027
- }
2028
- await createTeamDir(input.stateRoot, draft);
2029
- return { committed: true, state: draft };
2030
- }
2031
- catch (error) {
2032
- const cleanupErrors = [];
2033
- try {
2034
- await removeTeamDir(input.stateRoot, draft.id);
2035
- }
2036
- catch (cleanupError) {
2037
- cleanupErrors.push(cleanupError);
2038
- }
2039
- try {
2040
- await recordRetiredMemberIds(input.stateRoot, spawned.map((member) => member.id));
2041
- }
2042
- catch (cleanupError) {
2043
- cleanupErrors.push(cleanupError);
2044
- }
2045
- for (const member of spawned) {
2046
- try {
2047
- interruptMember(input.ctx, input.captain, member.id);
2048
- }
2049
- catch (cleanupError) {
2050
- cleanupErrors.push(cleanupError);
2051
- }
2052
- }
2053
- if (cleanupErrors.length > 0) {
2054
- throw new AggregateError([error, ...cleanupErrors], `failed to initialize profile "${profile.name}"`);
2055
- }
2056
- throw error;
2057
- }
2043
+ // Roster creation is durable planning only. The scheduler starts each
2044
+ // member with its first actual task once its dependencies are satisfied.
2045
+ if (input.inlinePlan !== undefined)
2046
+ delete draft.profile;
2047
+ await createTeamDir(input.stateRoot, draft);
2048
+ return { committed: true, state: draft };
2058
2049
  }
2059
2050
  function parseFindings(value) {
2060
2051
  if (value === undefined)
@@ -2244,7 +2235,8 @@ function renderStatus(value) {
2244
2235
  `Delivery: ${team.delivery.ok ? 'ok' : `blocked (${team.delivery.blockers.join('; ')})`}`,
2245
2236
  ],
2246
2237
  `Captain inbox (${team.captain_inbox.length}):`,
2247
- ...team.captain_inbox.map((message) => ` - [${message.from}] ${message.content.slice(0, 200)}`),
2238
+ ...team.captain_inbox.map((message) => ` - [${message.from}] ${message.content}`),
2239
+ ...(team.member_inbox ?? []).map(message => ` - [${message.from}] ${message.content}`),
2248
2240
  ];
2249
2241
  for (const [name, inbox] of Object.entries(team.member_inboxes)) {
2250
2242
  lines.push(`Member inbox ${name} (${inbox.count}): latest — ${inbox.latest.slice(0, 120)}`);