@nanmicoder/dsh-agent-teams 0.1.22-rc.1 → 0.1.23

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.
Files changed (46) hide show
  1. package/README.md +35 -12
  2. package/README_ZH.md +35 -12
  3. package/compatibility.json +6 -10
  4. package/lib/artifacts.js +342 -0
  5. package/lib/capabilities.js +6 -4
  6. package/lib/client/ActivityPanel.js +5 -2
  7. package/lib/client/agent-teams-card-definition.js +100 -6
  8. package/lib/client.js +90 -7
  9. package/lib/client.js.map +1 -1
  10. package/lib/command.js +3 -2
  11. package/lib/git-artifact-stamp.json +2 -2
  12. package/lib/harness-compat.js +70 -21
  13. package/lib/index.js +26 -3
  14. package/lib/lineage.js +198 -0
  15. package/lib/mailbox.js +59 -34
  16. package/lib/members.js +301 -53
  17. package/lib/profiles.js +3 -2
  18. package/lib/quality-gates.js +850 -65
  19. package/lib/scheduler.js +22 -3
  20. package/lib/snapshot.js +22 -1
  21. package/lib/state.js +279 -13
  22. package/lib/tool-policies.js +187 -0
  23. package/lib/tools.js +370 -89
  24. package/lib/types/artifacts.d.ts +66 -0
  25. package/lib/types/client/activity-monitor.d.ts +6 -0
  26. package/lib/types/client/agent-teams-card-definition.d.ts +56 -0
  27. package/lib/types/harness-compat.d.ts +56 -14
  28. package/lib/types/index.d.ts +26 -1
  29. package/lib/types/lineage.d.ts +92 -0
  30. package/lib/types/members.d.ts +112 -1
  31. package/lib/types/profiles.d.ts +6 -0
  32. package/lib/types/quality-gates.d.ts +216 -10
  33. package/lib/types/scheduler.d.ts +2 -1
  34. package/lib/types/snapshot.d.ts +20 -0
  35. package/lib/types/state.d.ts +22 -5
  36. package/lib/types/tool-policies.d.ts +119 -0
  37. package/lib/types/tools.d.ts +12 -1
  38. package/lib/types/types.d.ts +95 -0
  39. package/lib/types/workspace-audit.d.ts +27 -0
  40. package/lib/workspace-audit.js +52 -0
  41. package/package.json +59 -52
  42. package/pnpm-workspace.yaml +273 -273
  43. package/release-notes/v0.1.22.md +18 -0
  44. package/release-notes/v0.1.23.md +34 -0
  45. package/scripts/doctor.mjs +4 -4
  46. package/scripts/legacy-session-source-repair.mjs +344 -0
package/lib/tools.js CHANGED
@@ -12,13 +12,19 @@ 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 { amendTaskContract, acknowledgeMailbox, markMailboxDelivered, discardMailboxMessages, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, readMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
15
+ import { amendTaskContract, acknowledgeMailbox, markMailboxDelivered, discardMailboxMessages, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, readMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, isValidFindingLine, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
16
16
  import { appendTaskEvidence } from "./quality-gates.js";
17
+ import { artifactDrift, bindReviewToRevision, isQualityKind, verdictScopePatterns, } from "./quality-gates.js";
18
+ import { auditTeamWorkspace } from "./workspace-audit.js";
19
+ import { readArtifactScope, snapshotWorkspacePaths, workspacePathExists } from "./artifacts.js";
20
+ import { restrictableToolNames } from "./harness-compat.js";
21
+ import { memberSpawnRelaxableDeny, optionalToolPolicy, requireToolPolicy, resolveMemberToolFilter } from "./tool-policies.js";
17
22
  import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime, installMemberDelegationGuard, memberActivity, resolveMemberLlmSelection, spawnMember, steerCaptainReport, validateMemberLlmSelections, } from "./members.js";
18
23
  import { TERMINAL_TASK_STATUSES } from "./types.js";
19
24
  import { collectCompletedDependencyOutputs, formatDependencyOutputs, installTeamScheduler } from "./scheduler.js";
20
25
  import { installMailboxAdmission, isCurrentMail, mailboxContent, mailboxPrompt, readCurrentMailbox } from "./mailbox.js";
21
26
  import { resolveTeamProfile } from "./profiles.js";
27
+ import { forkLineageNote, leadIdentityOfAsync, nearestLiveLead, sessionLeadsTeamAsync } from "./lineage.js";
22
28
  export { steerCaptainReport } from "./members.js";
23
29
  /** The caller agent, or a loud failure for non-agent callers. */
24
30
  function requireCaptain(exec) {
@@ -39,29 +45,64 @@ function stateRootOf(workspace, config) {
39
45
  function teamLockKey(stateRoot, teamId) {
40
46
  return `team:${stateRoot}:${teamId}`;
41
47
  }
48
+ /**
49
+ * Bind a closing verdict to the revision it judged, using the snapshot taken
50
+ * when its attempt started (if any) to notice that the workspace changed while
51
+ * the gate was being formed. A verdict that ran over a moving workspace binds
52
+ * the drift explicitly instead of claiming the current bytes.
53
+ */
54
+ function bindClosingVerdict(workspace, team, task) {
55
+ if (task.status !== 'completed')
56
+ return;
57
+ const kind = taskKindOf(task);
58
+ if (kind !== 'review' && kind !== 'requirements' && kind !== 'verification')
59
+ return;
60
+ if ((kind === 'review' || kind === 'requirements') && task.verdict !== 'pass')
61
+ return;
62
+ const scope = verdictScopePatterns(team, task);
63
+ if (scope.length === 0)
64
+ return;
65
+ const read = readArtifactScope(workspace, scope);
66
+ const duringDrift = task.verdictStartSnapshot === undefined
67
+ ? []
68
+ : artifactDrift(task.verdictStartSnapshot.artifacts, read.artifacts);
69
+ const bound = bindReviewToRevision(team, task, {
70
+ artifacts: read.artifacts,
71
+ gaps: read.gaps,
72
+ scope,
73
+ duringDrift,
74
+ });
75
+ task.reviewedTaskId = bound.reviewedTaskId;
76
+ task.reviewedAttempt = bound.reviewedAttempt;
77
+ task.reviewedArtifacts = bound.reviewedArtifacts;
78
+ task.reviewedGaps = bound.reviewedGaps;
79
+ task.reviewedScope = bound.reviewedScope;
80
+ task.reviewedDuringDrift = bound.reviewedDuringDrift;
81
+ task.verdictStartSnapshot = undefined;
82
+ }
42
83
  /** Process-local lock key enforcing one active team per captain session. */
43
84
  function captainLockKey(stateRoot, captainId) {
44
85
  return `captain:${stateRoot}:${captainId}`;
45
86
  }
46
87
  /** The team this captain currently leads, or a loud failure. */
47
- async function requireCaptainTeam(workspace, config, captain) {
48
- const team = await findTeamByCaptain(stateRootOf(workspace, config), captain.id);
88
+ async function requireCaptainTeam(ctx, workspace, config, captain) {
89
+ const team = await findTeamByCaptain(stateRootOf(workspace, config), captain.id, await leadIdentityOfAsync(ctx, captain.session));
49
90
  if (team === undefined) {
50
- throw new Error('you are not leading any team yet — call agent_teams_create first');
91
+ throw new Error(`you are not leading any team yet — call agent_teams_create first${forkLineageNote(captain.session)}`);
51
92
  }
52
93
  return team;
53
94
  }
54
95
  /** The team this captain or active member currently participates in. */
55
- async function requireParticipantTeam(workspace, config, caller) {
56
- const team = await findTeamByParticipant(stateRootOf(workspace, config), caller.id);
96
+ async function requireParticipantTeam(ctx, workspace, config, caller) {
97
+ const team = await findTeamByParticipant(stateRootOf(workspace, config), caller.id, await leadIdentityOfAsync(ctx, caller.session));
57
98
  if (team === undefined) {
58
- throw new Error('you do not lead or belong to any active team yet');
99
+ throw new Error(`you do not lead or belong to any active team yet${forkLineageNote(caller.session)}`);
59
100
  }
60
101
  return team;
61
102
  }
62
103
  /** Re-derive a caller's role from fresh state while holding the team lock. */
63
- function participantIdentityOf(team, agentId) {
64
- if (team.captainSessionId === agentId)
104
+ async function participantIdentityOf(ctx, team, agentId) {
105
+ if (await sessionLeadsTeamAsync(ctx, team, agentId))
65
106
  return { kind: 'captain', name: CAPTAIN_KEY };
66
107
  const member = team.members.find((candidate) => candidate.id === agentId && candidate.status !== 'removed');
67
108
  return member === undefined ? undefined : { kind: 'member', name: member.name };
@@ -74,17 +115,17 @@ async function requireFreshTeam(stateRoot, teamId) {
74
115
  return fresh;
75
116
  }
76
117
  /** Fresh state with captain authorization rechecked inside the lock. */
77
- async function requireFreshCaptainTeam(stateRoot, teamId, captainId) {
118
+ async function requireFreshCaptainTeam(ctx, stateRoot, teamId, captainId) {
78
119
  const fresh = await requireFreshTeam(stateRoot, teamId);
79
- if (fresh.captainSessionId !== captainId) {
80
- throw new Error(`only the captain of team "${fresh.name}" may perform this operation`);
120
+ if (!(await sessionLeadsTeamAsync(ctx, fresh, captainId))) {
121
+ throw new Error(`only the captain of team "${fresh.name}" (session ${fresh.captainSessionId}) or one of its fork descendants may perform this operation`);
81
122
  }
82
123
  return fresh;
83
124
  }
84
125
  /** Fresh state and caller identity rechecked inside the lock. */
85
- async function requireFreshParticipant(stateRoot, teamId, callerId) {
126
+ async function requireFreshParticipant(ctx, stateRoot, teamId, callerId) {
86
127
  const fresh = await requireFreshTeam(stateRoot, teamId);
87
- const identity = participantIdentityOf(fresh, callerId);
128
+ const identity = await participantIdentityOf(ctx, fresh, callerId);
88
129
  if (identity === undefined)
89
130
  throw new Error(`you are no longer an active participant in team "${fresh.name}"`);
90
131
  return { team: fresh, identity };
@@ -116,6 +157,48 @@ function trimmedOptional(value) {
116
157
  const trimmed = value?.trim();
117
158
  return trimmed === undefined || trimmed === '' ? undefined : trimmed;
118
159
  }
160
+ /**
161
+ * Resolve one member's selected tool policy into the persisted policy id and
162
+ * the immutable effective filter (#59).
163
+ *
164
+ * An unknown id fails here — before the member is written and before any child
165
+ * is created — so a typo can never persist a half-configured member or leave a
166
+ * resumable orphan behind. A member without a policy resolves to `{}`: the
167
+ * previous behavior and record shape stay untouched.
168
+ * @param config - resolved plugin config (declared policies + depth cap).
169
+ * @param captain - the live captain, whose registry view names the host tools.
170
+ * @param policyId - the selected policy id, or undefined for no policy.
171
+ * @returns the member fields to persist (`toolPolicy` + `toolFilter`), or `{}`.
172
+ */
173
+ function memberToolSelection(config, captain, policyId) {
174
+ if (policyId === undefined)
175
+ return {};
176
+ const policy = requireToolPolicy(config.toolPolicies, policyId);
177
+ return {
178
+ toolPolicy: policyId,
179
+ toolFilter: resolveMemberToolFilter(policy, {
180
+ maxDepth: config.memberMaxDepth,
181
+ knownTools: restrictableToolNames(captain),
182
+ }),
183
+ };
184
+ }
185
+ /**
186
+ * Deny names the lenient spawn retry may drop for one member.
187
+ *
188
+ * A member without a policy keeps the built-in #163/#164 recovery path. A
189
+ * policy member may only relax built-in entries the owner did not declare
190
+ * itself (`memberSpawnRelaxableDeny`), and a selected policy id this
191
+ * deployment no longer declares relaxes nothing: the member then fails loud
192
+ * instead of spawning under a different filter.
193
+ * @param config - resolved plugin config.
194
+ * @param member - the member being spawned.
195
+ * @returns the relaxable deny names, or undefined for the legacy default.
196
+ */
197
+ function memberRelaxableDeny(config, member) {
198
+ if (member.toolPolicy === undefined)
199
+ return undefined;
200
+ return memberSpawnRelaxableDeny(optionalToolPolicy(config.toolPolicies, member.toolPolicy), true);
201
+ }
119
202
  /** Validate references and cycles before a staged graph can be saved or run. */
120
203
  function validateStagedGraph(team, requireRunnable) {
121
204
  const members = team.members.filter((member) => member.status !== 'removed');
@@ -183,6 +266,106 @@ function captainOpenTask(team, exceptTaskId) {
183
266
  * boundary: it waits for the activation handles to release, so a child cannot
184
267
  * keep executing after the captain-chat Stop control has reported success.
185
268
  */
269
+ /**
270
+ * The live Agent the host admits member operations from.
271
+ *
272
+ * Harness continuable delivery is authorized against the member's durable
273
+ * direct parent (`subagents.authorizeLineage`), which is the session that
274
+ * spawned it — the team's recorded captain. A captain acting from its own fork
275
+ * therefore commands members *through* that session:
276
+ *
277
+ * - while the recorded captain is resident, its Agent is used directly;
278
+ * - while it is cold, this resolves it through the public session controller
279
+ * (`workspaceRegistry.create` + `sessionController.create` with the existing
280
+ * session id — attach/resume only: no prompt, no model request, and the
281
+ * durable `captainSessionId` is never rewritten);
282
+ * - when the composition exposes neither service, or the resume fails, the
283
+ * operation fails with an explicit bounded error instead of migrating team
284
+ * ownership to a session the host would reject at delivery time.
285
+ *
286
+ * @param ctx - the plugin context (injects `agents`).
287
+ * @param team - current team state.
288
+ * @param caller - the captain-side session performing the operation.
289
+ * @returns the caller itself, or the live recorded captain session.
290
+ */
291
+ async function memberOperationParent(ctx, team, caller) {
292
+ if (caller.id === team.captainSessionId)
293
+ return caller;
294
+ const resident = ctx.agents.get(team.captainSessionId);
295
+ if (resident !== undefined)
296
+ return resident;
297
+ const resumed = await resumeLeadAnchor(ctx, team, caller);
298
+ if (resumed !== undefined)
299
+ return resumed;
300
+ throw new Error(`member operations require the live captain session ${team.captainSessionId} that owns team "${team.name}"; this session is its fork and ${resumeUnavailableReason(ctx, team)}`);
301
+ }
302
+ /**
303
+ * Best-effort live anchor for scheduler calls: the recorded captain when
304
+ * resident, the resumed one when this composition can attach it, and
305
+ * `undefined` otherwise (the scheduler then keeps its existing
306
+ * registry-only behavior). Never throws, so mutating tools stay usable on
307
+ * compositions without a session controller.
308
+ * @param ctx - the plugin context (injects `agents`).
309
+ * @param team - the team whose recorded captain is the delivery parent.
310
+ * @param caller - the captain-side session performing the operation.
311
+ * @returns the anchor agent when one is live, else undefined.
312
+ */
313
+ async function liveLeadAnchorAgent(ctx, team, caller) {
314
+ if (caller.id === team.captainSessionId)
315
+ return caller;
316
+ const resident = ctx.agents.get(team.captainSessionId);
317
+ if (resident !== undefined)
318
+ return resident;
319
+ return resumeLeadAnchor(ctx, team, caller);
320
+ }
321
+ /** Why the recorded captain could not be resumed, for one bounded error message. */
322
+ function resumeUnavailableReason(ctx, team) {
323
+ const services = optionalTeamServices(ctx);
324
+ if (services.controller === undefined || services.registry === undefined) {
325
+ return 'this composition exposes no session controller to resume it, so add_member, member delivery and member drain are unavailable from the fork';
326
+ }
327
+ return `resuming it through the session controller failed (its workspace is "${team.name}"'s workspace)`;
328
+ }
329
+ /** Public optional services used only to attach the recorded captain session. */
330
+ function optionalTeamServices(ctx) {
331
+ const get = ctx.get;
332
+ if (typeof get !== 'function')
333
+ return {};
334
+ const read = (name) => {
335
+ try {
336
+ return get.call(ctx, name);
337
+ }
338
+ catch {
339
+ return undefined;
340
+ }
341
+ };
342
+ return {
343
+ controller: read('sessionController'),
344
+ registry: read('workspaceRegistry'),
345
+ };
346
+ }
347
+ /**
348
+ * Attach the team's recorded captain session in this process without sending a
349
+ * prompt: the Harness controller's create-with-existing-id path resumes the
350
+ * session and leaves every durable field (including `captainSessionId`)
351
+ * untouched.
352
+ * @returns the resumed Agent, or undefined when this composition cannot resume it.
353
+ */
354
+ async function resumeLeadAnchor(ctx, team, caller) {
355
+ const { controller, registry } = optionalTeamServices(ctx);
356
+ if (controller?.create === undefined || registry?.create === undefined)
357
+ return undefined;
358
+ const cwd = caller.session.header.cwd ?? process.cwd();
359
+ try {
360
+ const workspace = await registry.create(cwd, cwd);
361
+ await controller.create({ sessionId: team.captainSessionId, workspaceId: workspace.id, agentPreset: 'standard' });
362
+ return ctx.agents.get(team.captainSessionId);
363
+ }
364
+ catch (error) {
365
+ ctx.logger.warn(`agent-teams: could not resume the recorded captain session ${team.captainSessionId}: ${String(error)}`);
366
+ return undefined;
367
+ }
368
+ }
186
369
  async function stopTeamMemberActivations(ctx, captain, members, signal) {
187
370
  const memberIds = members.filter(member => member.id !== '').map(member => member.id);
188
371
  if (memberIds.length === 0)
@@ -194,10 +377,11 @@ async function stopTeamMemberActivations(ctx, captain, members, signal) {
194
377
  }
195
378
  export async function haltTeamWork(input) {
196
379
  const halted = await withTeamLock(teamLockKey(input.stateRoot, input.teamId), async () => {
197
- const fresh = await requireFreshCaptainTeam(input.stateRoot, input.teamId, input.captain.id);
380
+ const fresh = await requireFreshCaptainTeam(input.ctx, input.stateRoot, input.teamId, input.captain.id);
198
381
  if (fresh.halted === true) {
199
382
  return {
200
383
  teamName: fresh.name,
384
+ captainSessionId: fresh.captainSessionId,
201
385
  cancelledTasks: fresh.tasks.filter((task) => task.status === 'cancelled').length,
202
386
  alreadyHalted: true,
203
387
  members: fresh.members.filter((member) => member.id !== '' && member.status !== 'removed').map((member) => ({ ...member })),
@@ -225,6 +409,7 @@ export async function haltTeamWork(input) {
225
409
  });
226
410
  return {
227
411
  teamName: fresh.name,
412
+ captainSessionId: fresh.captainSessionId,
228
413
  cancelledTasks,
229
414
  alreadyHalted: false,
230
415
  members: fresh.members.filter((member) => member.id !== '' && member.status !== 'removed').map((member) => ({ ...member })),
@@ -234,7 +419,17 @@ export async function haltTeamWork(input) {
234
419
  // children. Otherwise its current model turn can observe `halted`, call
235
420
  // resume, and race the still-running HTTP stop request.
236
421
  input.captain.cancel({ kind: 'user' }, { keepInbox: true });
237
- await stopTeamMemberActivations(input.ctx, input.captain, halted.members, input.signal);
422
+ // Members are children of the recorded captain session, so the parent-bound
423
+ // drain must run as that session when a fork requested the stop.
424
+ const haltAnchor = input.captain.id === halted.captainSessionId
425
+ ? input.captain
426
+ : input.ctx.agents.get(halted.captainSessionId);
427
+ if (haltAnchor === undefined) {
428
+ input.ctx.logger.warn(`agent-teams: captain session ${halted.captainSessionId} is not live; member activations of "${halted.teamName}" were not drained from this session`);
429
+ }
430
+ else {
431
+ await stopTeamMemberActivations(input.ctx, haltAnchor, halted.members, input.signal);
432
+ }
238
433
  // Interrupting a child emits a trailing subagent-settled notification. That
239
434
  // notification can start a fresh Captain turn after the first cancellation,
240
435
  // so close the stop boundary again once every child activation has drained.
@@ -243,6 +438,7 @@ export async function haltTeamWork(input) {
243
438
  input.captain.cancel({ kind: 'user' }, { keepInbox: true });
244
439
  return {
245
440
  teamName: halted.teamName,
441
+ captainSessionId: halted.captainSessionId,
246
442
  cancelledTasks: halted.cancelledTasks,
247
443
  alreadyHalted: halted.alreadyHalted,
248
444
  };
@@ -293,7 +489,7 @@ export function registerAgentTeamsTools(ctx, config) {
293
489
  const recordSpawnError = async (reason) => {
294
490
  try {
295
491
  await withTeamLock(teamLockKey(root, teamId), async () => {
296
- const fresh = await requireFreshCaptainTeam(root, teamId, captain.id);
492
+ const fresh = await requireFreshCaptainTeam(ctx, root, teamId, captain.id);
297
493
  const failed = fresh.members.find(item => item.name === memberName && item.status !== 'removed');
298
494
  if (failed === undefined || failed.id !== '')
299
495
  return;
@@ -309,7 +505,7 @@ export function registerAgentTeamsTools(ctx, config) {
309
505
  try {
310
506
  return await withTeamLock(teamLockKey(root, teamId), async () => {
311
507
  const team = await readTeam(root, teamId);
312
- if (team?.captainSessionId !== captain.id || team.halted === true || team.phase === 'staged')
508
+ if (team === undefined || !(await sessionLeadsTeamAsync(ctx, team, captain.id)) || team.halted === true || team.phase === 'staged')
313
509
  return false;
314
510
  const member = team.members.find(item => item.name === memberName && item.status !== 'removed');
315
511
  if (member === undefined || member.stopping === true || team.tasks.some(task => task.reassigning === true && task.assignee === memberName))
@@ -321,7 +517,7 @@ export function registerAgentTeamsTools(ctx, config) {
321
517
  const selection = await resolveMemberLlmSelection(ctx, captain, {
322
518
  provider: member.provider, model: member.model, reasoningEffort: member.reasoningEffort, fallback: member.fallback,
323
519
  }, signal);
324
- await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, team, member, config.stateDir, signal, text);
520
+ await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, team, member, config.stateDir, signal, text, member.toolFilter, memberRelaxableDeny(config, member));
325
521
  orphan = { ...member };
326
522
  delete member.spawnError;
327
523
  await writeTeam(root, team);
@@ -349,7 +545,7 @@ export function registerAgentTeamsTools(ctx, config) {
349
545
  const workspace = workspaceOf(captain);
350
546
  const stateRoot = stateRootOf(workspace, config);
351
547
  return withTeamLock(teamLockKey(stateRoot, teamId), async () => {
352
- const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
548
+ const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
353
549
  const staged = fresh.phase === 'staged';
354
550
  if (!staged && !allowPendingEdits)
355
551
  requireStagedTeam(fresh);
@@ -374,6 +570,14 @@ export function registerAgentTeamsTools(ctx, config) {
374
570
  member.model = selection.model;
375
571
  member.reasoningEffort = selection.reasoningEffort;
376
572
  member.executionPrompt = trimmedOptional(mutation.executionPrompt);
573
+ // The roster editor replaces route/prompt fields; the tool policy is
574
+ // only touched when the mutation names one, so a plan revision cannot
575
+ // silently widen a member's tool set.
576
+ if (mutation.toolPolicy !== undefined) {
577
+ const selected = memberToolSelection(config, captain, trimmedOptional(mutation.toolPolicy));
578
+ member.toolPolicy = selected.toolPolicy;
579
+ member.toolFilter = selected.toolFilter;
580
+ }
377
581
  }
378
582
  else if (mutation.action === 'update_task') {
379
583
  const task = requireTask(fresh, mutation.taskId);
@@ -455,7 +659,7 @@ export function registerAgentTeamsTools(ctx, config) {
455
659
  const stateRoot = stateRootOf(workspace, config);
456
660
  const runSignal = signal ?? new AbortController().signal;
457
661
  const approved = await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
458
- const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
662
+ const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
459
663
  requireStagedTeam(fresh);
460
664
  // A staged removal has no child session to retain in history. Drop those
461
665
  // placeholders before transitioning to the stricter running shape.
@@ -479,7 +683,7 @@ export function registerAgentTeamsTools(ctx, config) {
479
683
  return { teamId: fresh.id, members: fresh.members.length, tasks: fresh.tasks.length };
480
684
  });
481
685
  try {
482
- await scheduler.kickTeam(workspace, teamId, captain);
686
+ await scheduler.kickTeam(workspace, teamId, await liveLeadAnchorAgent(ctx, { captainSessionId: captain.id, name: teamId }, captain));
483
687
  }
484
688
  catch (error) {
485
689
  // Approval is already durably committed. A transient wake-up failure is
@@ -493,7 +697,7 @@ export function registerAgentTeamsTools(ctx, config) {
493
697
  const workspace = workspaceOf(captain);
494
698
  const stateRoot = stateRootOf(workspace, config);
495
699
  const prepared = await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
496
- const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
700
+ const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
497
701
  requireStagedTeam(fresh);
498
702
  if (fresh.planReviewState === 'awaiting_feedback') {
499
703
  return { teamName: fresh.name, alreadyWaiting: true };
@@ -518,7 +722,7 @@ export function registerAgentTeamsTools(ctx, config) {
518
722
  // Do not leave the durable UI in a false waiting state when the live
519
723
  // Captain disappeared between lookup and delivery.
520
724
  await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
521
- const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
725
+ const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
522
726
  requireStagedTeam(fresh);
523
727
  if (fresh.planReviewState === 'awaiting_feedback') {
524
728
  fresh.planReviewState = 'awaiting_review';
@@ -533,7 +737,7 @@ export function registerAgentTeamsTools(ctx, config) {
533
737
  const workspace = workspaceOf(captain);
534
738
  const stateRoot = stateRootOf(workspace, config);
535
739
  const discarded = await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
536
- const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
740
+ const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
537
741
  requireStagedTeam(fresh);
538
742
  appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/plan-discarded', {
539
743
  teamId: fresh.id,
@@ -583,6 +787,7 @@ export function registerAgentTeamsTools(ctx, config) {
583
787
  members: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
584
788
  name: { type: 'string', required: true }, role: { type: 'string' },
585
789
  provider: { type: 'string' }, model: { type: 'string' }, reasoning_effort: { type: 'string' },
790
+ tool_policy: { type: 'string', description: 'Optional owner-declared tool-policy id for this member.' },
586
791
  } } },
587
792
  tasks: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
588
793
  id: { type: 'string', required: true, description: 'Local reference used by dependencies in this plan; the result maps it to a durable task id.' },
@@ -604,11 +809,12 @@ export function registerAgentTeamsTools(ctx, config) {
604
809
  properties: {
605
810
  team_id: { type: 'string', required: true },
606
811
  team_name: { type: 'string', required: true },
812
+ captain_session_id: { type: 'string', required: true },
607
813
  state_dir: { type: 'string', required: true },
608
814
  phase: { type: 'string', required: true },
609
815
  profile: { type: 'string' },
610
816
  task_planning: { type: 'string' },
611
- members: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { member_name: { type: 'string', required: true }, member_id: { type: 'string', required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true }, reasoning_effort: { type: 'string' }, status: { type: 'string', required: true } } } },
817
+ members: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { member_name: { type: 'string', required: true }, member_id: { type: 'string', required: true }, member_role: { type: 'string' }, provider: { type: 'string', required: true }, model: { type: 'string', required: true }, reasoning_effort: { type: 'string' }, tool_policy: { type: 'string' }, status: { type: 'string', required: true } } } },
612
818
  tasks: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { task_id: { type: 'string', required: true }, seed_id: { type: 'string', required: true }, subject: { type: 'string', required: true }, status: { type: 'string', required: true }, kind: { type: 'string' }, assignee: { type: 'string' }, dependencies: { type: 'array', items: { type: 'string' }, required: true } } } },
613
819
  },
614
820
  },
@@ -619,6 +825,23 @@ export function registerAgentTeamsTools(ctx, config) {
619
825
  : `Team "${value.team_name}" created (id ${value.team_id}) under ${value.state_dir}. You are the captain.`)
620
826
  + (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')),
621
827
  }],
828
+ // The conversation card folds this durable result, and the card cannot
829
+ // reproduce the host's key cleaning for every name (issue #203). Publish
830
+ // the canonical identity instead of letting a client re-derive it.
831
+ presentationMeta: (_args, value) => ({
832
+ team_id: value.team_id,
833
+ team_name: value.team_name,
834
+ captain_session_id: value.captain_session_id,
835
+ phase: value.phase,
836
+ ...value.members === undefined ? {} : {
837
+ members: value.members.map(member => ({
838
+ id: member.member_id,
839
+ name: member.member_name,
840
+ role: member.member_role ?? '',
841
+ status: member.status,
842
+ })),
843
+ },
844
+ }),
622
845
  },
623
846
  async execute(args, exec) {
624
847
  const captain = requireCaptain(exec);
@@ -638,10 +861,11 @@ export function registerAgentTeamsTools(ctx, config) {
638
861
  if (profileName !== undefined && args.plan !== undefined)
639
862
  throw new Error('choose either a configured profile or an inline plan');
640
863
  const created = await withTeamLock(captainLockKey(stateRoot, captain.id), async () => {
641
- const current = await findTeamByParticipant(stateRoot, captain.id);
864
+ const current = await findTeamByParticipant(stateRoot, captain.id, await leadIdentityOfAsync(ctx, captain.session));
642
865
  if (current !== undefined) {
643
- const relationship = current.captainSessionId === captain.id ? 'lead' : 'belong to';
644
- const guidance = current.captainSessionId === captain.id
866
+ const leads = await sessionLeadsTeamAsync(ctx, current, captain.id);
867
+ const relationship = leads ? 'lead' : 'belong to';
868
+ const guidance = leads
645
869
  ? 'Use agent_teams_status and continue the existing team. Do not delete and recreate it merely to continue work. End it only when the user explicitly wants a separate new team.'
646
870
  : 'Continue your assigned member work and report to your captain; do not create a separate team.';
647
871
  throw new Error(`you already ${relationship} team "${current.name}" (id ${current.id}). ${guidance}`);
@@ -684,7 +908,7 @@ export function registerAgentTeamsTools(ctx, config) {
684
908
  });
685
909
  if (created.committed) {
686
910
  try {
687
- await scheduler.kickTeam(workspace, created.state.id, captain);
911
+ await scheduler.kickTeam(workspace, created.state.id, await liveLeadAnchorAgent(ctx, created.state, captain));
688
912
  }
689
913
  catch (error) {
690
914
  ctx.logger.warn(`agent-teams: post-create kick failed for "${created.state.id}": ${String(error)}`);
@@ -725,6 +949,7 @@ export function registerAgentTeamsTools(ctx, config) {
725
949
  return {
726
950
  team_id: snapshot.id,
727
951
  team_name: snapshot.name,
952
+ captain_session_id: snapshot.captainSessionId,
728
953
  state_dir: join(stateRoot, snapshot.id),
729
954
  phase: snapshot.phase ?? 'running',
730
955
  };
@@ -732,6 +957,7 @@ export function registerAgentTeamsTools(ctx, config) {
732
957
  return {
733
958
  team_id: snapshot.id,
734
959
  team_name: snapshot.name,
960
+ captain_session_id: snapshot.captainSessionId,
735
961
  state_dir: join(stateRoot, snapshot.id),
736
962
  phase: snapshot.phase ?? 'running',
737
963
  ...snapshot.profile === undefined ? {} : { profile: snapshot.profile.name },
@@ -739,9 +965,11 @@ export function registerAgentTeamsTools(ctx, config) {
739
965
  members: snapshot.members.map((member) => ({
740
966
  member_name: member.name,
741
967
  member_id: member.id,
968
+ ...member.role === undefined ? {} : { member_role: member.role },
742
969
  provider: member.provider ?? '',
743
970
  model: member.model ?? '',
744
971
  ...member.reasoningEffort === undefined ? {} : { reasoning_effort: member.reasoningEffort },
972
+ ...member.toolPolicy === undefined ? {} : { tool_policy: member.toolPolicy },
745
973
  status: member.status,
746
974
  })),
747
975
  tasks: snapshot.tasks.map((task) => ({
@@ -784,6 +1012,7 @@ export function registerAgentTeamsTools(ctx, config) {
784
1012
  model: { type: 'string', description: 'Optional member model; defaults to the current staged route.' },
785
1013
  reasoning_effort: { type: 'string', description: 'Optional member reasoning effort.' },
786
1014
  execution_prompt: { type: 'string', description: 'Optional member-specific execution prompt.' },
1015
+ tool_policy: { type: 'string', description: 'Optional owner-declared tool-policy id for this member; an empty string clears it back to the unchanged default tool set.' },
787
1016
  },
788
1017
  },
789
1018
  },
@@ -810,7 +1039,7 @@ export function registerAgentTeamsTools(ctx, config) {
810
1039
  async execute(args, exec) {
811
1040
  const captain = requireCaptain(exec);
812
1041
  const workspace = workspaceOf(captain);
813
- const team = await requireCaptainTeam(workspace, config, captain);
1042
+ const team = await requireCaptainTeam(ctx, workspace, config, captain);
814
1043
  if (args.operations.length === 0)
815
1044
  throw new Error('at least one staged plan operation is required');
816
1045
  const mutations = args.operations.map((operation, index) => {
@@ -828,6 +1057,9 @@ export function registerAgentTeamsTools(ctx, config) {
828
1057
  model: operation.model?.trim() || member.model || '',
829
1058
  reasoningEffort: operation.reasoning_effort ?? member.reasoningEffort,
830
1059
  executionPrompt: operation.execution_prompt ?? member.executionPrompt,
1060
+ // Omitted means "keep the current policy": a route/prompt revision
1061
+ // must not silently change what the member may do.
1062
+ ...operation.tool_policy === undefined ? {} : { toolPolicy: operation.tool_policy },
831
1063
  };
832
1064
  }
833
1065
  if (operation.action === 'update_task') {
@@ -869,7 +1101,7 @@ export function registerAgentTeamsTools(ctx, config) {
869
1101
  });
870
1102
  const updated = await updatePlanBatch(captain, team.id, mutations, exec.signal, true);
871
1103
  if (updated.phase !== 'staged')
872
- await scheduler.kickTeam(workspace, team.id, captain);
1104
+ await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, captain));
873
1105
  return {
874
1106
  status: updated.phase ?? 'running',
875
1107
  team_id: updated.id,
@@ -908,7 +1140,7 @@ export function registerAgentTeamsTools(ctx, config) {
908
1140
  throw new Error('explicit user approval text is required');
909
1141
  const captain = requireCaptain(exec);
910
1142
  const workspace = workspaceOf(captain);
911
- const team = await requireCaptainTeam(workspace, config, captain);
1143
+ const team = await requireCaptainTeam(ctx, workspace, config, captain);
912
1144
  const approved = await approveStagedTeam(captain, team.id, exec.signal);
913
1145
  return { status: 'running', team_id: approved.teamId, members: approved.members, tasks: approved.tasks };
914
1146
  },
@@ -919,10 +1151,11 @@ export function registerAgentTeamsTools(ctx, config) {
919
1151
  parameters: {
920
1152
  name: { type: 'string', required: true, description: 'Unique member name inside the team.' },
921
1153
  role: { type: 'string', description: 'Role of the member (e.g. researcher, engineer, reviewer).' },
922
- provider: { type: 'string', description: 'Optional LLM provider route. Use only when the user explicitly requests a different provider; requires model.' },
923
- model: { type: 'string', description: 'Optional model override. Omit for the captain\'s current model (or the configured memberModel default).' },
1154
+ provider: { type: 'string', description: 'Optional LLM provider route. Use only when the user explicitly requests a different provider; requires model. Omit to use the configured member route (memberLlmProvider/memberModel) or the captain\'s current route.' },
1155
+ model: { type: 'string', description: 'Optional model override (used verbatim, including any "/" in the id). Omit for the configured memberModel default (paired with memberLlmProvider when set) or the captain\'s current model.' },
924
1156
  reasoning_effort: { type: 'string', description: 'Optional reasoning effort override: one of the target model\'s supported effort ids, or "default" to force its default. When omitted, the captain\'s effort is inherited only for the same provider/model; a changed route uses the target default.' },
925
1157
  executionPrompt: { type: 'string', description: 'Optional member-specific execution prompt. It remains editable while staged.' },
1158
+ tool_policy: { type: 'string', description: 'Optional id of a tool policy declared by the deployment owner (config toolPolicies). It restricts which global tools this member sees and may invoke: a policy allow-list also keeps the four coordination tools, and captain-only tools can never be reopened. Omit for the unchanged default tool set.' },
926
1159
  },
927
1160
  output: {
928
1161
  schema: {
@@ -934,24 +1167,26 @@ export function registerAgentTeamsTools(ctx, config) {
934
1167
  provider: { type: 'string', required: true },
935
1168
  model: { type: 'string', required: true },
936
1169
  reasoning_effort: { type: 'string' },
1170
+ tool_policy: { type: 'string' },
937
1171
  status: { type: 'string', required: true },
938
1172
  phase: { type: 'string', required: true },
939
1173
  },
940
1174
  },
941
1175
  render: (args, value) => [{
942
1176
  type: 'text',
943
- text: value.phase === 'staged'
1177
+ text: (value.phase === 'staged'
944
1178
  ? `Member "${value.member_name}" added to the staged roster (${value.provider}/${value.model}); no child was spawned.`
945
- : `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}).`,
1179
+ : `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}).`)
1180
+ + (value.tool_policy === undefined ? '' : ` Tool policy "${value.tool_policy}" applies to this member.`),
946
1181
  }],
947
1182
  },
948
1183
  async execute(args, exec) {
949
1184
  const captain = requireCaptain(exec);
950
1185
  const workspace = workspaceOf(captain);
951
1186
  const stateRoot = stateRootOf(workspace, config);
952
- const team = await requireCaptainTeam(workspace, config, captain);
1187
+ const team = await requireCaptainTeam(ctx, workspace, config, captain);
953
1188
  const created = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
954
- const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
1189
+ const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
955
1190
  const memberName = args.name.trim();
956
1191
  if (memberName === '')
957
1192
  throw new Error('member name must not be empty');
@@ -969,9 +1204,11 @@ export function registerAgentTeamsTools(ctx, config) {
969
1204
  provider: args.provider,
970
1205
  model: args.model,
971
1206
  defaultModel: config.memberModel,
1207
+ defaultProvider: config.memberLlmProvider,
972
1208
  reasoningEffort: args.reasoning_effort,
973
1209
  fallback: config.fallback,
974
1210
  }, exec.signal);
1211
+ const toolSelection = memberToolSelection(config, captain, trimmedOptional(args.tool_policy));
975
1212
  const member = {
976
1213
  id: '',
977
1214
  name: memberName,
@@ -981,6 +1218,7 @@ export function registerAgentTeamsTools(ctx, config) {
981
1218
  reasoningEffort: selection.reasoningEffort,
982
1219
  ...selection.fallback === undefined ? {} : { fallback: selection.fallback },
983
1220
  executionPrompt: trimmedOptional(args.executionPrompt),
1221
+ ...toolSelection,
984
1222
  joinedAt: Date.now(),
985
1223
  status: 'idle',
986
1224
  };
@@ -1001,11 +1239,12 @@ export function registerAgentTeamsTools(ctx, config) {
1001
1239
  ...selection.reasoningEffort === undefined
1002
1240
  ? {}
1003
1241
  : { reasoning_effort: selection.reasoningEffort },
1242
+ ...member.toolPolicy === undefined ? {} : { tool_policy: member.toolPolicy },
1004
1243
  status: member.status,
1005
1244
  phase: fresh.phase ?? 'running',
1006
1245
  };
1007
1246
  });
1008
- await scheduler.kickMember(workspace, team.id, created.member_name, captain);
1247
+ await scheduler.kickMember(workspace, team.id, created.member_name, await liveLeadAnchorAgent(ctx, team, captain));
1009
1248
  const latest = (await readTeam(stateRoot, team.id))?.members.find(member => member.name === created.member_name);
1010
1249
  return latest === undefined ? created : { ...created, member_id: latest.id, status: latest.status };
1011
1250
  },
@@ -1035,9 +1274,9 @@ export function registerAgentTeamsTools(ctx, config) {
1035
1274
  const captain = requireCaptain(exec);
1036
1275
  const workspace = workspaceOf(captain);
1037
1276
  const stateRoot = stateRootOf(workspace, config);
1038
- const team = await requireCaptainTeam(workspace, config, captain);
1277
+ const team = await requireCaptainTeam(ctx, workspace, config, captain);
1039
1278
  const revoked = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
1040
- const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
1279
+ const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
1041
1280
  const member = fresh.members.find(item => item.name === args.name);
1042
1281
  if (member === undefined)
1043
1282
  throw new Error(`no member \"${args.name}\" in team \"${fresh.name}\"`);
@@ -1060,9 +1299,9 @@ export function registerAgentTeamsTools(ctx, config) {
1060
1299
  });
1061
1300
  if (revoked.member.id !== '') {
1062
1301
  await recordRetiredMemberIds(stateRoot, [revoked.member.id]);
1063
- await stopTeamMemberActivations(ctx, captain, [revoked.member], exec.signal);
1302
+ await stopTeamMemberActivations(ctx, await memberOperationParent(ctx, team, captain), [revoked.member], exec.signal);
1064
1303
  }
1065
- await scheduler.kickTeam(workspace, team.id, captain);
1304
+ await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, captain));
1066
1305
  return {
1067
1306
  member_name: revoked.member.name,
1068
1307
  status: revoked.member.status,
@@ -1123,14 +1362,14 @@ export function registerAgentTeamsTools(ctx, config) {
1123
1362
  const captain = requireCaptain(exec);
1124
1363
  const workspace = workspaceOf(captain);
1125
1364
  const stateRoot = stateRootOf(workspace, config);
1126
- const team = await requireCaptainTeam(workspace, config, captain);
1365
+ const team = await requireCaptainTeam(ctx, workspace, config, captain);
1127
1366
  // Some models materialize optional parameters as "" instead of omitting
1128
1367
  // them (issue #105). Normalize blank optional fields to omitted before
1129
1368
  // validation so a blank value can neither be rejected spuriously nor be
1130
1369
  // persisted into team.json, where it would brick the team on reload.
1131
1370
  const input = normalizeBlankOptionalTaskFields(args);
1132
1371
  const created = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
1133
- const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
1372
+ const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
1134
1373
  const gate = validateCreateTask(fresh, {
1135
1374
  subject: input.subject,
1136
1375
  description: input.description,
@@ -1225,7 +1464,7 @@ export function registerAgentTeamsTools(ctx, config) {
1225
1464
  ...task.assignee !== undefined ? { assignee: task.assignee } : {},
1226
1465
  };
1227
1466
  });
1228
- await scheduler.kickTeam(workspace, team.id, captain);
1467
+ await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, captain));
1229
1468
  return created;
1230
1469
  },
1231
1470
  }));
@@ -1259,12 +1498,12 @@ export function registerAgentTeamsTools(ctx, config) {
1259
1498
  const captain = requireCaptain(exec);
1260
1499
  const workspace = workspaceOf(captain);
1261
1500
  const stateRoot = stateRootOf(workspace, config);
1262
- const team = await requireCaptainTeam(workspace, config, captain);
1501
+ const team = await requireCaptainTeam(ctx, workspace, config, captain);
1263
1502
  const target = args.assignee.trim();
1264
1503
  if (target === '')
1265
1504
  throw new Error('reassignment assignee must not be empty');
1266
1505
  const revoked = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
1267
- const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
1506
+ const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
1268
1507
  const task = requireTask(fresh, args.task_id);
1269
1508
  if (task.status === 'completed')
1270
1509
  throw new Error(`completed task ${task.id} is immutable and cannot be reassigned`);
@@ -1312,14 +1551,14 @@ export function registerAgentTeamsTools(ctx, config) {
1312
1551
  let quiescenceError;
1313
1552
  if (revoked.previousMember !== undefined) {
1314
1553
  try {
1315
- await stopTeamMemberActivations(ctx, captain, [revoked.previousMember], exec.signal);
1554
+ await stopTeamMemberActivations(ctx, await memberOperationParent(ctx, team, captain), [revoked.previousMember], exec.signal);
1316
1555
  }
1317
1556
  catch (error) {
1318
1557
  quiescenceError = error;
1319
1558
  }
1320
1559
  }
1321
1560
  await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
1322
- const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
1561
+ const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
1323
1562
  const task = requireTask(fresh, args.task_id);
1324
1563
  if (task.handoffId !== revoked.handoffId || task.assignee !== target || task.reassigning !== true) {
1325
1564
  throw new Error(`task ${task.id} changed during reassignment; refusing to overwrite the newer state`);
@@ -1350,7 +1589,7 @@ export function registerAgentTeamsTools(ctx, config) {
1350
1589
  if (quiescenceError !== undefined)
1351
1590
  throw quiescenceError;
1352
1591
  if (target !== CAPTAIN_KEY)
1353
- await scheduler.kickMember(workspace, team.id, target, captain);
1592
+ await scheduler.kickMember(workspace, team.id, target, await liveLeadAnchorAgent(ctx, team, captain));
1354
1593
  const current = await readTeam(stateRoot, team.id);
1355
1594
  const task = current === undefined ? undefined : requireTask(current, args.task_id);
1356
1595
  if (task === undefined)
@@ -1394,9 +1633,9 @@ export function registerAgentTeamsTools(ctx, config) {
1394
1633
  const caller = requireCaptain(exec);
1395
1634
  const workspace = workspaceOf(caller);
1396
1635
  const stateRoot = stateRootOf(workspace, config);
1397
- const team = await requireParticipantTeam(workspace, config, caller);
1636
+ const team = await requireParticipantTeam(ctx, workspace, config, caller);
1398
1637
  return withTeamLock(teamLockKey(stateRoot, team.id), async () => {
1399
- const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
1638
+ const { team: fresh, identity } = await requireFreshParticipant(ctx, stateRoot, team.id, caller.id);
1400
1639
  const task = requireTask(fresh, args.task_id);
1401
1640
  if (task.reassigning === true) {
1402
1641
  throw new Error(`task ${task.id} is being reassigned; wait for the handoff to finish`);
@@ -1558,10 +1797,10 @@ export function registerAgentTeamsTools(ctx, config) {
1558
1797
  const caller = requireCaptain(exec);
1559
1798
  const workspace = workspaceOf(caller);
1560
1799
  const stateRoot = stateRootOf(workspace, config);
1561
- const team = await requireParticipantTeam(workspace, config, caller);
1800
+ const team = await requireParticipantTeam(ctx, workspace, config, caller);
1562
1801
  let followUpMessage;
1563
1802
  const updated = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
1564
- const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
1803
+ const { team: fresh, identity } = await requireFreshParticipant(ctx, stateRoot, team.id, caller.id);
1565
1804
  const task = requireTask(fresh, args.task_id);
1566
1805
  if (identity.kind === 'captain'
1567
1806
  && task.assignee !== undefined
@@ -1635,19 +1874,22 @@ export function registerAgentTeamsTools(ctx, config) {
1635
1874
  task.acceptanceResults = acceptanceResults;
1636
1875
  if (commandsRun !== undefined)
1637
1876
  task.commandsRun = commandsRun;
1877
+ // A passing gate is evidence about a revision: record which one, so a
1878
+ // later edit cannot leave the team advertising an unreviewed revision.
1879
+ bindClosingVerdict(workspace, fresh, task);
1638
1880
  task.updatedAt = Date.now();
1639
1881
  const priorDependencies = new Map(fresh.tasks.map(item => [item.id, [...item.dependencies]]));
1640
- const followUp = (task.status === 'failed' && (task.verdict === 'needs_revision' || task.verdict === 'reject'))
1641
- ? applyQualityFollowUp(fresh, task)
1882
+ const followUp = (task.status === 'failed' && isQualityKind(taskKindOf(task)))
1883
+ ? applyQualityFollowUp(fresh, task, (paths) => snapshotWorkspacePaths(workspace, paths), (path) => workspacePathExists(workspace, path))
1642
1884
  : undefined;
1643
1885
  let followUpSummary;
1644
1886
  if ((followUp?.created.length ?? 0) > 0) {
1645
1887
  const rewired = fresh.tasks.filter(item => priorDependencies.has(item.id) && JSON.stringify(priorDependencies.get(item.id)) !== JSON.stringify(item.dependencies));
1646
- followUpSummary = `Automatic quality follow-up for ${task.id}: ${followUp.created.map(item => `${item.id} (${item.kind}, owner=${item.assignee ?? 'unassigned'}, deps=${item.dependencies.join(',') || 'none'})`).join('; ')}.${rewired.length ? ` Updated dependencies: ${rewired.map(item => `${item.id} -> ${item.dependencies.join(',')}`).join('; ')}.` : ''} Use these tasks; do not create duplicate repair/review work.`;
1888
+ followUpSummary = `Automatic quality follow-up for ${task.id}: ${followUp.created.map(item => `${item.id} (${item.kind}, owner=${item.assignee ?? 'unassigned'}, deps=${item.dependencies.join(',') || 'none'})`).join('; ')}.${rewired.length ? ` Updated dependencies: ${rewired.map(item => `${item.id} -> ${item.dependencies.join(',')}`).join('; ')}.` : ''} Use these tasks; do not create duplicate repair/review work.${followUp.excludedFindings === undefined ? '' : ` Findings on default-excluded paths were not turned into a contract and stay with the user: ${followUp.excludedFindings.join(', ')}.`}`;
1647
1889
  followUpMessage = createMessage(CAPTAIN_KEY, CAPTAIN_KEY, followUpSummary);
1648
1890
  }
1649
1891
  if (followUp?.escalated === true) {
1650
- await appendMailbox(stateRoot, fresh.id, CAPTAIN_KEY, createMessage(CAPTAIN_KEY, CAPTAIN_KEY, `Quality-gate loop escalated after ${task.id} (${task.kind ?? 'review'} verdict=${task.verdict}). Automatic repair/review stopped.`));
1892
+ await appendMailbox(stateRoot, fresh.id, CAPTAIN_KEY, createMessage(CAPTAIN_KEY, CAPTAIN_KEY, `Quality-gate loop escalated after ${task.id} (${task.kind ?? 'review'} verdict=${task.verdict}). Automatic repair/review stopped.${followUp.reason === undefined ? '' : ` Reason: ${followUp.reason}.`}${followUp.excludedFindings === undefined ? '' : ` Findings left for the user (default-excluded paths, not a member's to edit): ${followUp.excludedFindings.join(', ')}.`}`));
1651
1893
  }
1652
1894
  await writeTeam(stateRoot, fresh);
1653
1895
  if (followUpMessage !== undefined)
@@ -1682,12 +1924,12 @@ export function registerAgentTeamsTools(ctx, config) {
1682
1924
  };
1683
1925
  });
1684
1926
  if (followUpMessage !== undefined) {
1685
- const captain = ctx.agents.get(team.captainSessionId);
1927
+ const captain = nearestLiveLead(ctx, team);
1686
1928
  if (captain !== undefined && steerCaptainReport(captain, CAPTAIN_KEY, followUpMessage.content, mailboxPrompt(team.id, CAPTAIN_KEY, [followUpMessage]))) {
1687
1929
  await withTeamLock(teamLockKey(stateRoot, team.id), () => markMailboxDelivered(stateRoot, team.id, CAPTAIN_KEY, [followUpMessage.id]));
1688
1930
  }
1689
1931
  }
1690
- await scheduler.kickTeam(workspace, team.id, team.captainSessionId === caller.id ? caller : undefined);
1932
+ await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, caller));
1691
1933
  return updated;
1692
1934
  },
1693
1935
  }));
@@ -1724,9 +1966,9 @@ export function registerAgentTeamsTools(ctx, config) {
1724
1966
  const captain = requireCaptain(exec);
1725
1967
  const workspace = workspaceOf(captain);
1726
1968
  const stateRoot = stateRootOf(workspace, config);
1727
- const team = await requireCaptainTeam(workspace, config, captain);
1969
+ const team = await requireCaptainTeam(ctx, workspace, config, captain);
1728
1970
  const amended = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
1729
- const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
1971
+ const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
1730
1972
  const task = requireTask(fresh, args.task_id);
1731
1973
  const input = {
1732
1974
  ...args.objective === undefined ? {} : { objective: args.objective },
@@ -1801,10 +2043,10 @@ export function registerAgentTeamsTools(ctx, config) {
1801
2043
  const caller = requireCaptain(exec);
1802
2044
  const workspace = workspaceOf(caller);
1803
2045
  const stateRoot = stateRootOf(workspace, config);
1804
- const team = await requireParticipantTeam(workspace, config, caller);
2046
+ const team = await requireParticipantTeam(ctx, workspace, config, caller);
1805
2047
  const to = args.to.trim();
1806
2048
  const prepared = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
1807
- const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
2049
+ const { team: fresh, identity } = await requireFreshParticipant(ctx, stateRoot, team.id, caller.id);
1808
2050
  const from = identity.name;
1809
2051
  // `from` may only be the caller's own identity: impersonating another
1810
2052
  // member (or the captain) would poison the mailbox and event records.
@@ -1862,9 +2104,11 @@ export function registerAgentTeamsTools(ctx, config) {
1862
2104
  });
1863
2105
  if (prepared.kind === 'duplicate')
1864
2106
  return { message_id: prepared.message.id, from: prepared.from, to: prepared.message.to, delivered: 'duplicate' };
1865
- // Resolve the exact live captain only after releasing the state lock.
1866
- // The plugin mailbox is already durable if live delivery cannot proceed.
1867
- const captain = ctx.agents.get(prepared.fresh.captainSessionId);
2107
+ // Resolve a live lead only after releasing the state lock: the recorded
2108
+ // captain while resident, otherwise the nearest live fork descendant, so
2109
+ // a human working in a fork still receives member reports. The plugin
2110
+ // mailbox is already durable if no lead session is live.
2111
+ const captain = nearestLiveLead(ctx, prepared.fresh);
1868
2112
  if (prepared.kind === 'captain') {
1869
2113
  let delivered = 'mailbox';
1870
2114
  if (captain !== undefined && prepared.identity.kind === 'member') {
@@ -1910,12 +2154,13 @@ export function registerAgentTeamsTools(ctx, config) {
1910
2154
  const caller = requireCaptain(exec);
1911
2155
  const workspace = workspaceOf(caller);
1912
2156
  const stateRoot = stateRootOf(workspace, config);
1913
- const located = await requireParticipantTeam(workspace, config, caller);
1914
- if (located.captainSessionId === caller.id) {
1915
- await scheduler.kickTeam(workspace, located.id, caller);
2157
+ const located = await requireParticipantTeam(ctx, workspace, config, caller);
2158
+ if (await sessionLeadsTeamAsync(ctx, located, caller.id)) {
2159
+ await scheduler.kickTeam(workspace, located.id, await memberOperationParent(ctx, located, caller));
1916
2160
  }
1917
- const { team, identity } = await withTeamLock(teamLockKey(stateRoot, located.id), () => requireFreshParticipant(stateRoot, located.id, caller.id));
2161
+ const { team, identity } = await withTeamLock(teamLockKey(stateRoot, located.id), () => requireFreshParticipant(ctx, stateRoot, located.id, caller.id));
1918
2162
  const activity = memberActivity(ctx, team.members.map((member) => member.id));
2163
+ const audit = auditTeamWorkspace(workspace, team);
1919
2164
  const members = team.members
1920
2165
  .filter((member) => member.status !== 'removed')
1921
2166
  .map((member) => ({
@@ -1924,6 +2169,7 @@ export function registerAgentTeamsTools(ctx, config) {
1924
2169
  provider: member.provider ?? '',
1925
2170
  model: member.model ?? '',
1926
2171
  reasoning_effort: member.reasoningEffort ?? '',
2172
+ tool_policy: member.toolPolicy ?? '',
1927
2173
  status: member.status,
1928
2174
  activity: member.id !== '' ? (activity.get(member.id) ?? 'unknown') : 'unspawned',
1929
2175
  ...member.spawnError === undefined ? {} : { spawn_error: member.spawnError },
@@ -1941,6 +2187,7 @@ export function registerAgentTeamsTools(ctx, config) {
1941
2187
  ...task.round === undefined ? {} : { round: task.round },
1942
2188
  ...task.verdict === undefined ? {} : { verdict: task.verdict },
1943
2189
  ...task.supplementalEvidence === undefined ? {} : { supplemental_evidence: JSON.stringify(task.supplementalEvidence) },
2190
+ ...task.findingSourceTaskIds === undefined ? {} : { fixes: task.findingSourceTaskIds.join(',') },
1944
2191
  findings_open: (task.findings ?? []).filter((finding) => finding.resolved !== true).length,
1945
2192
  ...task.profileSeedId === undefined ? {} : { seed_id: task.profileSeedId },
1946
2193
  ...task.output !== undefined ? { output: task.output } : {},
@@ -1976,9 +2223,13 @@ export function registerAgentTeamsTools(ctx, config) {
1976
2223
  status: row.status,
1977
2224
  ...row.evidence === undefined ? {} : { evidence: row.evidence },
1978
2225
  }));
1979
- const deliveryCheck = canDeclareDelivery(team);
1980
- const delivery = { ok: deliveryCheck.ok, blockers: [...deliveryCheck.blockers] };
1981
- const loop = describeQualityLoop(team);
2226
+ const deliveryCheck = canDeclareDelivery(team, audit);
2227
+ const delivery = {
2228
+ ok: deliveryCheck.ok,
2229
+ blockers: [...deliveryCheck.blockers],
2230
+ warnings: [...deliveryCheck.warnings],
2231
+ };
2232
+ const loop = describeQualityLoop(team, audit);
1982
2233
  const result = {
1983
2234
  team_id: team.id,
1984
2235
  team_name: team.name,
@@ -2049,9 +2300,9 @@ export function registerAgentTeamsTools(ctx, config) {
2049
2300
  const captain = requireCaptain(exec);
2050
2301
  const workspace = workspaceOf(captain);
2051
2302
  const stateRoot = stateRootOf(workspace, config);
2052
- const team = await requireCaptainTeam(workspace, config, captain);
2303
+ const team = await requireCaptainTeam(ctx, workspace, config, captain);
2053
2304
  const result = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
2054
- const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
2305
+ const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
2055
2306
  const resumed = resumeTeamState(fresh, args.reason);
2056
2307
  if (resumed.status === 'rejected')
2057
2308
  throw new Error(resumed.error ?? 'resume rejected');
@@ -2071,7 +2322,7 @@ export function registerAgentTeamsTools(ctx, config) {
2071
2322
  };
2072
2323
  });
2073
2324
  if (result.status === 'resumed')
2074
- await scheduler.kickTeam(workspace, team.id, captain);
2325
+ await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, captain));
2075
2326
  return result;
2076
2327
  },
2077
2328
  }));
@@ -2097,9 +2348,9 @@ export function registerAgentTeamsTools(ctx, config) {
2097
2348
  const captain = requireCaptain(exec);
2098
2349
  const workspace = workspaceOf(captain);
2099
2350
  const stateRoot = stateRootOf(workspace, config);
2100
- const team = await requireCaptainTeam(workspace, config, captain);
2351
+ const team = await requireCaptainTeam(ctx, workspace, config, captain);
2101
2352
  const members = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
2102
- const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
2353
+ const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
2103
2354
  // Include previously removed members so deleting a pre-fix team also
2104
2355
  // retires durable catalog entries left behind by remove_member.
2105
2356
  const roster = fresh.members.map(member => ({ ...member }));
@@ -2117,9 +2368,9 @@ export function registerAgentTeamsTools(ctx, config) {
2117
2368
  return roster;
2118
2369
  });
2119
2370
  await recordRetiredMemberIds(stateRoot, members.map(member => member.id));
2120
- await stopTeamMemberActivations(ctx, captain, members, exec.signal);
2371
+ await stopTeamMemberActivations(ctx, await memberOperationParent(ctx, team, captain), members, exec.signal);
2121
2372
  await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
2122
- const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
2373
+ const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
2123
2374
  appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/team-deleted', {
2124
2375
  teamId: fresh.id,
2125
2376
  });
@@ -2140,6 +2391,7 @@ async function initializeProfileTeam(input) {
2140
2391
  provider: template.provider,
2141
2392
  model: template.model,
2142
2393
  defaultModel: input.config.memberModel,
2394
+ defaultProvider: input.config.memberLlmProvider,
2143
2395
  reasoningEffort: template.reasoningEffort,
2144
2396
  fallback: template.fallback ?? profile.fallback ?? input.config.fallback,
2145
2397
  }, input.exec.signal));
@@ -2166,6 +2418,9 @@ async function initializeProfileTeam(input) {
2166
2418
  ...input.staged ? { phase: 'staged', planReviewState: 'awaiting_review' } : {},
2167
2419
  members: profile.members.map((template, index) => {
2168
2420
  const selection = selections[index];
2421
+ // A profile member selects a policy by id; an unknown id fails here,
2422
+ // before the team directory and any child session exist (#59).
2423
+ const toolSelection = memberToolSelection(input.config, input.captain, template.toolPolicy);
2169
2424
  return {
2170
2425
  id: '',
2171
2426
  name: template.name,
@@ -2175,6 +2430,7 @@ async function initializeProfileTeam(input) {
2175
2430
  reasoningEffort: selection.reasoningEffort,
2176
2431
  executionPrompt: template.executionPrompt ?? profile.executionPrompt ?? input.config.executionPrompt,
2177
2432
  ...selection.fallback === undefined ? {} : { fallback: selection.fallback },
2433
+ ...toolSelection,
2178
2434
  joinedAt: now,
2179
2435
  status: 'idle',
2180
2436
  };
@@ -2227,7 +2483,12 @@ function parseFindings(value) {
2227
2483
  // A blank optional file must be omitted, not persisted: durable-state
2228
2484
  // validation requires non-empty optional strings (issue #105 class).
2229
2485
  ...typeof raw['file'] === 'string' && raw['file'].trim() !== '' ? { file: raw['file'] } : {},
2230
- ...typeof raw['line'] === 'number' ? { line: raw['line'] } : {},
2486
+ // Only a contract-valid line is persisted. A model that materializes
2487
+ // "no line number" as -1 (or sends a non-integer) must not write a record
2488
+ // the durable validator rejects on the next read, which used to brick the
2489
+ // entire team (issue #188). The predicate is shared with
2490
+ // `isReviewFinding`, so the two sides cannot drift apart again.
2491
+ ...isValidFindingLine(raw['line']) ? { line: raw['line'] } : {},
2231
2492
  ...typeof raw['resolved'] === 'boolean' ? { resolved: raw['resolved'] } : {},
2232
2493
  };
2233
2494
  });
@@ -2279,8 +2540,8 @@ function parseCommandResults(value) {
2279
2540
  };
2280
2541
  });
2281
2542
  }
2282
- export function applyQualityFollowUp(team, closed) {
2283
- const planned = planQualityFollowUp(team, closed);
2543
+ export function applyQualityFollowUp(team, closed, snapshotPaths, pathExists) {
2544
+ const planned = planQualityFollowUp(team, closed, pathExists === undefined ? {} : { pathExists });
2284
2545
  if (planned.escalated === true)
2285
2546
  team.escalated = true;
2286
2547
  const created = [];
@@ -2299,6 +2560,12 @@ export function applyQualityFollowUp(team, closed) {
2299
2560
  return dependency;
2300
2561
  return idBySubject.get(dependency) ?? dependency;
2301
2562
  });
2563
+ // Record the workspace the generated work order was derived from, so a
2564
+ // later round that changes a cited file can mark it stale instead of
2565
+ // letting a member read a frozen snapshot as authoritative (issue #174).
2566
+ const contractSnapshot = draft.contractPaths === undefined || draft.contractPaths.length === 0 || snapshotPaths === undefined
2567
+ ? undefined
2568
+ : snapshotPaths(draft.contractPaths);
2302
2569
  const next = {
2303
2570
  id,
2304
2571
  subject: draft.subject ?? `${draft.kind}-round-${draft.round ?? 1}`,
@@ -2309,6 +2576,7 @@ export function applyQualityFollowUp(team, closed) {
2309
2576
  createdAt: now,
2310
2577
  updatedAt: now,
2311
2578
  kind: draft.kind,
2579
+ ...draft.description === undefined ? {} : { description: draft.description },
2312
2580
  ...draft.round === undefined ? {} : { round: draft.round },
2313
2581
  ...draft.objective === undefined ? {} : { objective: draft.objective },
2314
2582
  ...draft.inScope === undefined ? {} : { inScope: draft.inScope },
@@ -2317,7 +2585,9 @@ export function applyQualityFollowUp(team, closed) {
2317
2585
  ...draft.verify === undefined ? {} : { verify: draft.verify },
2318
2586
  ...draft.sourceTaskId === undefined ? {} : { sourceTaskId: draft.sourceTaskId },
2319
2587
  ...draft.sourceFindingIds === undefined ? {} : { sourceFindingIds: draft.sourceFindingIds },
2588
+ ...draft.findingSourceTaskIds === undefined ? {} : { findingSourceTaskIds: draft.findingSourceTaskIds },
2320
2589
  ...draft.reviewedTaskId === undefined ? {} : { reviewedTaskId: idBySubject.get(draft.reviewedTaskId) ?? draft.reviewedTaskId },
2590
+ ...contractSnapshot === undefined ? {} : { contractSnapshot },
2321
2591
  };
2322
2592
  team.tasks.push(next);
2323
2593
  created.push(next);
@@ -2336,7 +2606,12 @@ export function applyQualityFollowUp(team, closed) {
2336
2606
  task.updatedAt = now;
2337
2607
  }
2338
2608
  }
2339
- return { created, escalated: planned.escalated === true };
2609
+ return {
2610
+ created,
2611
+ escalated: planned.escalated === true,
2612
+ ...planned.reason === undefined ? {} : { reason: planned.reason },
2613
+ ...planned.excludedFindings === undefined ? {} : { excludedFindings: planned.excludedFindings },
2614
+ };
2340
2615
  }
2341
2616
  /** Build the `memberRuntime` config handed to member helpers. */
2342
2617
  function memberRuntime(config) {
@@ -2368,7 +2643,8 @@ function renderStatus(value) {
2368
2643
  const route = member.provider && member.model ? ` · ${member.provider}/${member.model}` : '';
2369
2644
  const effort = member.reasoning_effort ? ` · reasoning ${member.reasoning_effort}` : '';
2370
2645
  const failure = member.spawn_error === undefined ? '' : `\n start failed: ${member.spawn_error.slice(0, 400)}`;
2371
- return ` - ${member.name} [${member.role}] ${member.status}/${member.activity}${route}${effort}${failure}`;
2646
+ const policy = member.tool_policy ? ` · policy ${member.tool_policy}` : '';
2647
+ return ` - ${member.name} [${member.role}] ${member.status}/${member.activity}${route}${effort}${policy}${failure}`;
2372
2648
  }),
2373
2649
  `Tasks (${team.tasks.length}):`,
2374
2650
  ...team.tasks.map((task) => {
@@ -2380,7 +2656,8 @@ function renderStatus(value) {
2380
2656
  const kind = task.kind ? ` ${task.kind}` : '';
2381
2657
  const round = task.round === undefined ? '' : ` r${task.round}`;
2382
2658
  const verdict = task.verdict === undefined ? '' : ` verdict ${task.verdict}`;
2383
- return ` - ${task.id} [${task.status}]${kind}${round}${verdict} attempt ${task.attempt}${handoff}${seed} ${task.subject} → ${task.assignee || 'unassigned'}${deps}${output}${evidence}`;
2659
+ const fixes = task.fixes === undefined ? '' : ` fixes ${task.fixes}`;
2660
+ return ` - ${task.id} [${task.status}]${kind}${round}${verdict}${fixes} attempt ${task.attempt}${handoff}${seed} ${task.subject} → ${task.assignee || 'unassigned'}${deps}${output}${evidence}`;
2384
2661
  }),
2385
2662
  ...team.coverage === undefined || team.coverage.length === 0 ? [] : [
2386
2663
  'Coverage:',
@@ -2389,6 +2666,10 @@ function renderStatus(value) {
2389
2666
  ...team.delivery === undefined ? [] : [
2390
2667
  `Delivery: ${team.delivery.ok ? 'ok' : `blocked (${team.delivery.blockers.join('; ')})`}`,
2391
2668
  ],
2669
+ ...(team.delivery?.warnings ?? []).length === 0 ? [] : [
2670
+ 'Gate warnings (delivery is not blocked, but these recorded verdicts or generated work orders no longer describe the current bytes):',
2671
+ ...(team.delivery?.warnings ?? []).map((warning) => ` - ${warning}`),
2672
+ ],
2392
2673
  `Captain inbox (${team.captain_inbox.length}):`,
2393
2674
  ...team.captain_inbox.map((message) => ` - [${message.from}] ${message.content}`),
2394
2675
  ...(team.member_inbox ?? []).map(message => ` - [${message.from}] ${message.content}`),