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

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 (54) hide show
  1. package/README.md +36 -15
  2. package/README_ZH.md +36 -15
  3. package/assets/readme/workspace.png +0 -0
  4. package/compatibility.json +22 -2
  5. package/lib/capabilities.js +3 -1
  6. package/lib/client/ActivityPanel.js +80 -51
  7. package/lib/client/AgentTeamsCard.js +14 -5
  8. package/lib/client/TeamChatEntry.js +22 -0
  9. package/lib/client/WorkspaceActivity.js +100 -0
  10. package/lib/client/activity-model.js +73 -8
  11. package/lib/client/activity-monitor.js +11 -4
  12. package/lib/client/agent-teams-card-definition.js +11 -1
  13. package/lib/client/icons.js +18 -0
  14. package/lib/client/index.js +35 -4
  15. package/lib/client/locales.js +44 -0
  16. package/lib/client/session-navigation.js +13 -1
  17. package/lib/client/workspace-state.js +43 -0
  18. package/lib/client.js +832 -210
  19. package/lib/client.js.map +1 -1
  20. package/lib/git-artifact-stamp.json +5 -0
  21. package/lib/harness-compat.js +20 -3
  22. package/lib/index.js +1 -1
  23. package/lib/mailbox.js +23 -3
  24. package/lib/members.js +3 -3
  25. package/lib/quality-gates.js +54 -0
  26. package/lib/scheduler.js +8 -2
  27. package/lib/snapshot.js +1 -0
  28. package/lib/state.js +15 -3
  29. package/lib/tools.js +71 -22
  30. package/lib/types/capabilities.d.ts +0 -1
  31. package/lib/types/client/ActivityPanel.d.ts +18 -1
  32. package/lib/types/client/AgentTeamsCard.d.ts +14 -2
  33. package/lib/types/client/TeamChatEntry.d.ts +4 -0
  34. package/lib/types/client/WorkspaceActivity.d.ts +22 -0
  35. package/lib/types/client/activity-model.d.ts +37 -1
  36. package/lib/types/client/activity-monitor.d.ts +4 -0
  37. package/lib/types/client/agent-teams-card-definition.d.ts +8 -0
  38. package/lib/types/client/icons.d.ts +5 -0
  39. package/lib/types/client/locales.d.ts +44 -0
  40. package/lib/types/client/session-navigation.d.ts +13 -2
  41. package/lib/types/client/workspace-state.d.ts +19 -0
  42. package/lib/types/harness-compat.d.ts +14 -1
  43. package/lib/types/mailbox.d.ts +4 -0
  44. package/lib/types/quality-gates.d.ts +7 -1
  45. package/lib/types/scheduler.d.ts +1 -0
  46. package/lib/types/snapshot.d.ts +2 -0
  47. package/lib/types/types.d.ts +16 -0
  48. package/package.json +61 -54
  49. package/pnpm-workspace.yaml +288 -0
  50. package/release-notes/v0.1.21-rc.1.md +14 -0
  51. package/release-notes/v0.1.21.md +23 -0
  52. package/release-notes/v0.1.22-rc.1.md +24 -0
  53. package/scripts/compatibility.mjs +26 -4
  54. package/scripts/doctor.mjs +7 -2
@@ -0,0 +1,5 @@
1
+ {
2
+ "schema": 1,
3
+ "source": "5f2a18e5208435f0838152058ab8929f024699a61fb85338a5dffcaf0c829447",
4
+ "output": "1373527d718f862f5fae89f40dc6bd3347cef086159d596ff2519c5f5182bd4b"
5
+ }
@@ -18,6 +18,21 @@ function boundary(runtime) {
18
18
  function unsupported(detail) {
19
19
  throw new Error(`agent-teams: unsupported Harness subagent contract (${detail}); use an explicitly tested Harness version and a coherent dependency installation`);
20
20
  }
21
+ /**
22
+ * 0.1.6 moved startup admission to serial agent/created. Older created events
23
+ * have no source and precede session-start; ignore those until the old hook.
24
+ * Keep the removed event's type confined to this audited compatibility seam.
25
+ */
26
+ export function onAgentReady(ctx, listener) {
27
+ const stopCreated = ctx.on('agent/created', payload => {
28
+ if ('source' in payload)
29
+ listener(payload.agent, true);
30
+ return undefined;
31
+ });
32
+ const legacy = ctx;
33
+ const stopLegacy = legacy.on('agent/session-start', ({ agent }) => listener(agent, false));
34
+ return () => { stopCreated(); stopLegacy(); };
35
+ }
21
36
  /** Read child-owned history, excluding any descriptor inherited from a parent. */
22
37
  export function sessionOwnEvents(session) {
23
38
  const current = session;
@@ -48,7 +63,7 @@ export function installContinuableMemberSetup(ctx, setup) {
48
63
  const installed = new WeakSet();
49
64
  const active = new Set();
50
65
  ctx.effect(() => {
51
- const stop = ctx.on('agent/session-start', ({ agent }) => {
66
+ const stop = onAgentReady(ctx, (agent, vetoable) => {
52
67
  if (installed.has(agent))
53
68
  return;
54
69
  // Deliberately synchronous: awaiting here loses the first-request race.
@@ -57,6 +72,8 @@ export function installContinuableMemberSetup(ctx, setup) {
57
72
  teardown = setup(agent.ctx, agent);
58
73
  }
59
74
  catch (error) {
75
+ if (vetoable)
76
+ throw error;
60
77
  // session-start is a notification: Harness logs a thrown listener and
61
78
  // still admits the first prompt. Reject request assembly explicitly so
62
79
  // a malformed saved route cannot silently execute on a default model.
@@ -95,7 +112,7 @@ export function installContinuableMemberSetup(ctx, setup) {
95
112
  /** Queue a distinct host-authored turn; never substitute model-message steer. */
96
113
  export async function queueMemberPrompt(runtime, parent, childId, content, signal) {
97
114
  const host = boundary(runtime);
98
- const source = { kind: 'plugin', plugin: 'dsh-agent-teams' };
115
+ const source = { kind: 'agent-teams' };
99
116
  if (typeof host.followup === 'function') {
100
117
  return host.followup.call(runtime, parent, childId, content, { source, signal });
101
118
  }
@@ -110,7 +127,7 @@ export async function queueMemberPrompt(runtime, parent, childId, content, signa
110
127
  /** Coordination joins the nearest step, including waking an idle/cold child. */
111
128
  export async function steerMemberPrompt(runtime, parent, childId, content, signal, live) {
112
129
  const host = boundary(runtime);
113
- const source = { kind: 'plugin', plugin: 'dsh-agent-teams' };
130
+ const source = { kind: 'agent-teams' };
114
131
  const deliver = host[hostPromptDeliver];
115
132
  if (typeof deliver === 'function')
116
133
  return deliver.call(runtime, parent, childId, content, source, signal, 'steer');
package/lib/index.js CHANGED
@@ -285,7 +285,7 @@ export function apply(ctx, config) {
285
285
  try {
286
286
  captain.steer(createUserMessage({
287
287
  content: [{ type: 'text', text: stagedPlanApprovedContext(team.name) }],
288
- source: { kind: 'plugin', plugin: 'dsh-agent-teams' },
288
+ source: { kind: 'agent-teams' },
289
289
  }));
290
290
  }
291
291
  catch (error) {
package/lib/mailbox.js CHANGED
@@ -1,19 +1,39 @@
1
1
  import { join } from 'node:path';
2
- import { acknowledgeMailbox, CAPTAIN_KEY, discardMailboxMessages, findTeamByCaptain, readMailbox, readTeam, sanitizeKey, withTeamLock } from "./state.js";
2
+ import { acknowledgeMailbox, CAPTAIN_KEY, discardMailboxMessages, findTeamByCaptain, readMailbox, readUnreadMailbox, readTeam, sanitizeKey, withTeamLock } from "./state.js";
3
3
  import { sessionOwnEvents } from "./harness-compat.js";
4
4
  const PREFIX = 'AgentTeams inbox receipt: ';
5
5
  export function isCurrentMail(team, message) {
6
6
  if (message.discardedAt !== undefined)
7
7
  return false;
8
+ if (message.sourceAttemptId !== undefined && !team.tasks.some(task => task.id === message.sourceTaskId
9
+ && task.attemptId === message.sourceAttemptId && task.assignee === message.from
10
+ && (message.sourceTaskStatus === undefined || !['completed', 'failed', 'cancelled'].includes(task.status) || message.sourceTaskStatus === task.status)))
11
+ return false;
8
12
  if (message.attemptId === undefined)
9
13
  return true;
10
14
  return team.tasks.some(task => task.id === message.taskId && task.attemptId === message.attemptId
11
15
  && task.assignee === message.to && (task.status === 'claimed' || task.status === 'in_progress'));
12
16
  }
17
+ /** Include structured provenance even in the status-tool fallback presentation. */
18
+ export function mailboxContent(message) {
19
+ return (message.sourceTaskId === undefined ? ''
20
+ : `[Source task ${message.sourceTaskId}, attempt_id ${message.sourceAttemptId}${message.sourceTaskStatus === undefined ? '' : `, status=${message.sourceTaskStatus}`}]\n`) + message.content;
21
+ }
13
22
  function mailboxBody(recipient, messages) {
14
23
  return messages.map(message => recipient === CAPTAIN_KEY
15
- ? `AgentTeams message from member ${message.from}:\n\n${message.content}`
16
- : `AgentTeams message from ${message.from}${message.attemptId === undefined ? '' : ` for task ${message.taskId}, attempt_id ${message.attemptId}`}:\n\n${message.content}`).join('\n\n');
24
+ ? `AgentTeams message from member ${message.from}:\n\n${mailboxContent(message)}`
25
+ : `AgentTeams message from ${message.from}${message.attemptId === undefined ? '' : ` for task ${message.taskId}, attempt_id ${message.attemptId}`}:\n\n${mailboxContent(message)}`).join('\n\n');
26
+ }
27
+ /** Fallback reads must enforce the same current-generation rule as live admission. */
28
+ export async function readCurrentMailbox(root, teamId, recipient, onMalformed) {
29
+ return withTeamLock(`team:${root}:${teamId}`, async () => {
30
+ const team = await readTeam(root, teamId);
31
+ if (team === undefined)
32
+ return [];
33
+ const messages = await readUnreadMailbox(root, teamId, recipient, onMalformed);
34
+ await discardMailboxMessages(root, teamId, recipient, messages.filter(message => !isCurrentMail(team, message)).map(message => message.id));
35
+ return messages.filter(message => isCurrentMail(team, message));
36
+ });
17
37
  }
18
38
  export function mailboxPrompt(teamId, recipient, messages) {
19
39
  return PREFIX + JSON.stringify({ teamId, recipient, ids: messages.map(message => message.id) }) + '\n\n' + mailboxBody(recipient, messages);
package/lib/members.js CHANGED
@@ -72,7 +72,7 @@ export function steerCaptainReport(captain, from, content, receipt) {
72
72
  try {
73
73
  captain.steer(createUserMessage({
74
74
  content: [{ type: 'text', text: receipt ?? `AgentTeams message from member ${from}:\n\n${content}` }],
75
- source: { kind: 'plugin', plugin: 'dsh-agent-teams' },
75
+ source: { kind: 'agent-teams' },
76
76
  }));
77
77
  return true;
78
78
  }
@@ -459,13 +459,13 @@ Working rules:
459
459
  - a stale-attempt rejection means the captain reassigned or took over the task; stop touching that task and wait for new work.
460
460
  claimed cannot jump to completed. Mark in_progress first, then completed or failed.
461
461
  Include attempt_id on every update. Then report once as described below and become idle.
462
- 4. Send one short report with agent_teams_send_message (to=captain) when you complete a task or hit a blocker. The captain is also your parent: this single message satisfies both reporting duties. Do not repeat it through the native send_message tool or send acknowledgments that add no new information.
462
+ 4. Send one short report with agent_teams_send_message (to=captain) when you complete a task or hit a blocker. Include source_task_id and source_attempt_id in task reports; a stale source rejection means stop, never relabel an old result with a new attempt. The captain is also your parent: this single message satisfies both reporting duties. Do not repeat it through the native send_message tool or send acknowledgments that add no new information.
463
463
  5. To ask a teammate something, use agent_teams_send_message with to=<teammate name>; the message lands in their mailbox and wakes them directly — teammates talk to each other without the captain in the loop. The same applies to the captain (to=captain).
464
464
  6. After your turn becomes idle, the shared task scheduler may assign your next ready task automatically. Never claim a second task while you still own unfinished work.
465
465
  7. If you already own an open attempt (claimed or in_progress) and receive mail, treat it as guidance for that same attempt_id unless the mail explicitly tells you to stop or fail. Do not claim a new task in that turn.
466
466
  8. Do not start a teammate's assigned task. Do not privately tell the next-stage member to start; the scheduler assigns unlocked work after you become idle.
467
467
  9. You are a worker: do not create or delete teams, reassign tasks, or add/remove members — that is the captain's job.
468
- 10. Quality-gate kinds carry a contract (kind, objective, inScope, acceptance, verify). Stay inside inScope. Do not mark your own implementation as review pass. Review/requirements complete only with verdict=pass; needs_revision/reject must fail with findings. Mail is not a formal next review.`;
468
+ 10. Quality-gate kinds carry a contract (kind, objective, inScope, acceptance, verify). Stay inside inScope. Do not mark your own implementation as review pass. Review/requirements complete only with verdict=pass; needs_revision/reject must fail with findings. Mail is not a formal next review. Completed work must not be repeated to attach late evidence: call update_task on the original task with its attempt_id and acceptanceResults/commandsRun/evidence_note; supplements are append-only and cannot change its verdict.`;
469
469
  }
470
470
  /**
471
471
  * The initial user message delivered when the member is created.
@@ -236,6 +236,11 @@ export function validateCreateTask(team, input) {
236
236
  if (!team.tasks.some((item) => item.id === input.sourceTaskId)) {
237
237
  return { ok: false, error: `source task "${input.sourceTaskId}" does not exist` };
238
238
  }
239
+ const duplicate = team.tasks.find(item => taskKindOf(item) === 'repair'
240
+ && OPEN_STATUSES.includes(item.status) && item.sourceTaskId === input.sourceTaskId
241
+ && findingKey(item.sourceFindingIds ?? []) === findingKey(input.sourceFindingIds ?? []));
242
+ if (duplicate !== undefined)
243
+ return { ok: false, error: `repair task ${duplicate.id} already covers these findings; use that task instead of creating duplicate work` };
239
244
  }
240
245
  const dependencies = input.dependencies ?? [];
241
246
  for (const dependency of dependencies) {
@@ -850,6 +855,8 @@ export function hasValidQualityTaskFields(value) {
850
855
  if (!Array.isArray(value['commandsRun']) || !value['commandsRun'].every(isCommandResult))
851
856
  return false;
852
857
  }
858
+ if (value['supplementalEvidence'] !== undefined && (!Array.isArray(value['supplementalEvidence']) || !value['supplementalEvidence'].every(isTaskEvidence)))
859
+ return false;
853
860
  if (value['revisions'] !== undefined) {
854
861
  if (!Array.isArray(value['revisions']) || !value['revisions'].every(isTaskRevision))
855
862
  return false;
@@ -991,3 +998,50 @@ export function describeQualityLoop(team) {
991
998
  };
992
999
  }
993
1000
  export { QUALITY_KINDS, WRITE_KINDS };
1001
+ /** Persisted evidence must remain loadable after process restart. */
1002
+ export function isTaskEvidence(value) {
1003
+ if (!isRecord(value))
1004
+ return false;
1005
+ return typeof value['at'] === 'number' && Number.isFinite(value['at'])
1006
+ && nonemptyString(value['by']) && Number.isSafeInteger(value['attempt']) && value['attempt'] >= 0
1007
+ && (value['attemptId'] === undefined || nonemptyString(value['attemptId']))
1008
+ && (value['note'] === undefined || nonemptyString(value['note']))
1009
+ && (value['acceptanceResults'] === undefined || (Array.isArray(value['acceptanceResults']) && value['acceptanceResults'].every(isAcceptanceResult)))
1010
+ && (value['commandsRun'] === undefined || (Array.isArray(value['commandsRun']) && value['commandsRun'].every(isCommandResult)))
1011
+ && (value['note'] !== undefined || (Array.isArray(value['acceptanceResults']) && value['acceptanceResults'].length > 0) || (Array.isArray(value['commandsRun']) && value['commandsRun'].length > 0));
1012
+ }
1013
+ /** Append observations without mutating the original result or its completion time. */
1014
+ export function appendTaskEvidence(task, input, by) {
1015
+ if (!TERMINAL_TASK_STATUSES.includes(task.status))
1016
+ throw new Error('supplemental evidence requires a terminal task');
1017
+ // Key order must not turn a retried JSON object into a different observation.
1018
+ const stable = (value) => JSON.stringify(value, (_key, item) => typeof item === 'object' && item !== null && !Array.isArray(item)
1019
+ ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item);
1020
+ for (const key of ['status', 'output', 'verdict', 'findings', 'changedPaths']) {
1021
+ if (input[key] !== undefined && stable(input[key]) !== stable(task[key])) {
1022
+ throw new Error(`terminal task ${task.id} is immutable: cannot change ${key}; append evidence with evidence_note, acceptanceResults or commandsRun instead. Do not reclaim or redo completed work.`);
1023
+ }
1024
+ }
1025
+ const prior = (task.supplementalEvidence ?? []).filter(row => row.by === by && row.attempt === (task.attempt ?? 0) && row.attemptId === task.attemptId);
1026
+ const freshItems = (items, existing) => {
1027
+ const seen = new Set(existing.map(stable));
1028
+ return (items ?? []).filter(item => { const key = stable(item); if (seen.has(key))
1029
+ return false; seen.add(key); return true; });
1030
+ };
1031
+ const acceptanceResults = freshItems(input.acceptanceResults, [...task.acceptanceResults ?? [], ...prior.flatMap(row => row.acceptanceResults ?? [])]);
1032
+ const commandsRun = freshItems(input.commandsRun, [...task.commandsRun ?? [], ...prior.flatMap(row => row.commandsRun ?? [])]);
1033
+ const trimmed = input.evidence_note?.trim();
1034
+ const note = trimmed && !prior.some(row => row.note === trimmed) ? trimmed : undefined;
1035
+ if (!note && acceptanceResults.length === 0 && commandsRun.length === 0)
1036
+ return false;
1037
+ const entry = { at: Date.now(), by, attempt: task.attempt ?? 0,
1038
+ ...task.attemptId === undefined ? {} : { attemptId: task.attemptId },
1039
+ ...note === undefined ? {} : { note },
1040
+ ...acceptanceResults.length === 0 ? {} : { acceptanceResults },
1041
+ ...commandsRun.length === 0 ? {} : { commandsRun },
1042
+ };
1043
+ if (!isTaskEvidence(entry))
1044
+ throw new Error('invalid supplemental evidence');
1045
+ task.supplementalEvidence = [...task.supplementalEvidence ?? [], entry];
1046
+ return true;
1047
+ }
package/lib/scheduler.js CHANGED
@@ -166,8 +166,8 @@ ${structuredCompletion}
166
166
  Attempt: ${ticket.attempt}
167
167
  Attempt id: ${ticket.attemptId}
168
168
 
169
- Call agent_teams_claim_task for ${ticket.taskId}; it will return this same attempt_id. Include attempt_id=${ticket.attemptId} in every agent_teams_update_task call. If it is rejected as stale, stop work because the task was reassigned. claimed cannot jump to completed. Mark in_progress first, then completed or failed. Include attempt_id on every update. Then send_message to captain and become idle.
170
- When finishing: use status=completed only when the task's success criteria are satisfied; use status=failed when blocking findings or validation failures mean downstream work must not proceed; include a concise output in either case. Quality kinds must submit structured fields: review/requirements need verdict=pass to complete (needs_revision/reject must fail with findings); implementation/repair/verification/integration need acceptanceResults and commandsRun, while implementation/repair also need in-scope changedPaths. Use status values "passed" or "failed" inside those arrays. After the work and verification finish, call agent_teams_update_task immediately; do not wait for captain confirmation and do not continue exploring. Do not approve your own implementation. Mail is not a formal next review. Treat the dependency results above as source material. Do not ignore them. Work only this task and only its in-scope paths in this turn.
169
+ Call agent_teams_claim_task for ${ticket.taskId}; it will return this same attempt_id. Include attempt_id=${ticket.attemptId} in every agent_teams_update_task call. If it is rejected as stale, stop work because the task was reassigned. claimed cannot jump to completed. Mark in_progress first, then completed or failed. Include attempt_id on every update. Then send_message to captain with source_task_id and source_attempt_id and become idle.
170
+ When finishing: use status=completed only when the task's success criteria are satisfied; use status=failed when blocking findings or validation failures mean downstream work must not proceed; include a concise output in either case. Quality kinds must submit structured fields: review/requirements need verdict=pass to complete (needs_revision/reject must fail with findings); implementation/repair/verification/integration need acceptanceResults and commandsRun, while implementation/repair also need in-scope changedPaths. Use status values "passed" or "failed" inside those arrays. After the work and verification finish, call agent_teams_update_task immediately; do not wait for captain confirmation and do not continue exploring. Do not approve your own implementation. Mail is not a formal next review. Completed work must not be repeated to attach late evidence: call update_task on the original task with its attempt_id and acceptanceResults/commandsRun/evidence_note; supplements are append-only and cannot change its verdict. Treat the dependency results above as source material. Do not ignore them. Work only this task and only its in-scope paths in this turn.
171
171
 
172
172
  State policy: ${stateDir}/${teamId}/ is read-only diagnostics; mutate team state only through agent_teams_* tools.`;
173
173
  }
@@ -281,6 +281,10 @@ export function installTeamScheduler(ctx, config) {
281
281
  const previousStatus = recoverOwned ? task.status : undefined;
282
282
  const previousAttempt = recoverOwned ? task.attempt : undefined;
283
283
  const previousAttemptId = recoverOwned ? task.attemptId : undefined;
284
+ const previousResult = recoverOwned ? {
285
+ output: task.output, verdict: task.verdict, findings: task.findings,
286
+ changedPaths: task.changedPaths, acceptanceResults: task.acceptanceResults, commandsRun: task.commandsRun,
287
+ } : undefined;
284
288
  const attemptId = beginTaskAttempt(task, currentMember.name);
285
289
  // A recovered generation is parked before delivery. This makes each
286
290
  // (member, attempt) recovery idempotent even if every status poll
@@ -302,6 +306,7 @@ export function installTeamScheduler(ctx, config) {
302
306
  attemptId,
303
307
  previousAssignee,
304
308
  recoveredOwned: recoverOwned,
309
+ ...previousResult === undefined ? {} : { previousResult },
305
310
  ...previousStatus === undefined ? {} : { previousStatus },
306
311
  ...previousAttempt === undefined ? {} : { previousAttempt },
307
312
  ...previousAttemptId === undefined ? {} : { previousAttemptId },
@@ -350,6 +355,7 @@ export function installTeamScheduler(ctx, config) {
350
355
  task.assignee = ticket.previousAssignee;
351
356
  task.attempt = ticket.previousAttempt;
352
357
  task.attemptId = ticket.previousAttemptId;
358
+ Object.assign(task, ticket.previousResult);
353
359
  parkedAttempts.set(ticket.memberId, ticket.previousAttemptId);
354
360
  }
355
361
  else {
package/lib/snapshot.js CHANGED
@@ -110,6 +110,7 @@ export async function assembleTeamSnapshot(ctx, stateRoot, workspace, state, opt
110
110
  ...task.kind === undefined ? {} : { kind: task.kind },
111
111
  ...task.round === undefined ? {} : { round: task.round },
112
112
  ...task.verdict === undefined ? {} : { verdict: task.verdict },
113
+ updatedAt: task.updatedAt,
113
114
  })),
114
115
  messageCount: captainInbox.length
115
116
  + members.reduce((count, member) => count + member.unread, 0),
package/lib/state.js CHANGED
@@ -133,6 +133,15 @@ export function transitionError(current, next) {
133
133
  }
134
134
  return undefined;
135
135
  }
136
+ /** Execution results belong to one attempt; a retry must produce its own evidence. */
137
+ function clearAttemptResult(task) {
138
+ task.output = undefined;
139
+ task.verdict = undefined;
140
+ task.findings = undefined;
141
+ task.changedPaths = undefined;
142
+ task.acceptanceResults = undefined;
143
+ task.commandsRun = undefined;
144
+ }
136
145
  /** Activate the task's current generation for one owner and return its capability id. */
137
146
  export function activateTaskAttempt(task, assignee) {
138
147
  const attemptId = randomUUID();
@@ -141,7 +150,7 @@ export function activateTaskAttempt(task, assignee) {
141
150
  task.attemptId = attemptId;
142
151
  task.handoffId = undefined;
143
152
  task.reassigning = false;
144
- task.output = undefined;
153
+ clearAttemptResult(task);
145
154
  task.updatedAt = Date.now();
146
155
  return attemptId;
147
156
  }
@@ -172,7 +181,7 @@ export function invalidateTaskAttempt(task, nextAssignee, reassigning = false) {
172
181
  task.status = 'pending';
173
182
  task.assignee = nextAssignee;
174
183
  task.reassigning = reassigning;
175
- task.output = undefined;
184
+ clearAttemptResult(task);
176
185
  task.updatedAt = Date.now();
177
186
  }
178
187
  /**
@@ -780,7 +789,10 @@ function isTeamMessage(value) {
780
789
  && (value['readAt'] === undefined || isFiniteNumber(value['readAt']))
781
790
  && (value['discardedAt'] === undefined || isFiniteNumber(value['discardedAt']))
782
791
  && (value['taskId'] === undefined || typeof value['taskId'] === 'string')
783
- && (value['attemptId'] === undefined || typeof value['attemptId'] === 'string');
792
+ && (value['attemptId'] === undefined || typeof value['attemptId'] === 'string')
793
+ && (value['sourceTaskId'] === undefined || typeof value['sourceTaskId'] === 'string')
794
+ && (value['sourceAttemptId'] === undefined || typeof value['sourceAttemptId'] === 'string')
795
+ && (value['sourceTaskStatus'] === undefined || ['pending', 'claimed', 'in_progress', 'completed', 'failed', 'cancelled'].includes(value['sourceTaskStatus']));
784
796
  }
785
797
  /**
786
798
  * Remove a team's whole directory (members should be interrupted first).
package/lib/tools.js CHANGED
@@ -12,11 +12,12 @@ 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, 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, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
16
+ import { appendTaskEvidence } from "./quality-gates.js";
16
17
  import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime, installMemberDelegationGuard, memberActivity, resolveMemberLlmSelection, spawnMember, steerCaptainReport, validateMemberLlmSelections, } from "./members.js";
17
18
  import { TERMINAL_TASK_STATUSES } from "./types.js";
18
19
  import { collectCompletedDependencyOutputs, formatDependencyOutputs, installTeamScheduler } from "./scheduler.js";
19
- import { installMailboxAdmission, mailboxPrompt } from "./mailbox.js";
20
+ import { installMailboxAdmission, isCurrentMail, mailboxContent, mailboxPrompt, readCurrentMailbox } from "./mailbox.js";
20
21
  import { resolveTeamProfile } from "./profiles.js";
21
22
  export { steerCaptainReport } from "./members.js";
22
23
  /** The caller agent, or a loud failure for non-agent callers. */
@@ -510,7 +511,7 @@ export function registerAgentTeamsTools(ctx, config) {
510
511
  try {
511
512
  captain.followup(createUserMessage({
512
513
  content: [{ type: 'text', text: stagedPlanFeedbackContext(prepared.teamName) }],
513
- source: { kind: 'plugin', plugin: 'dsh-agent-teams' },
514
+ source: { kind: 'agent-teams' },
514
515
  }));
515
516
  }
516
517
  catch (error) {
@@ -548,7 +549,7 @@ export function registerAgentTeamsTools(ctx, config) {
548
549
  try {
549
550
  captain.inject(createUserMessage({
550
551
  content: [{ type: 'text', text: stagedPlanDiscardContext(discarded.teamName) }],
551
- source: { kind: 'plugin', plugin: 'dsh-agent-teams' },
552
+ source: { kind: 'agent-teams' },
552
553
  }));
553
554
  }
554
555
  catch (error) {
@@ -1468,7 +1469,7 @@ export function registerAgentTeamsTools(ctx, config) {
1468
1469
  }));
1469
1470
  ctx.tools.register(defineTool({
1470
1471
  name: 'agent_teams_update_task',
1471
- 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.',
1472
+ 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, but owners and the captain can append acceptanceResults/commandsRun/evidence_note as attributed supplemental evidence, without reclaiming or changing the verdict. A captain must use reassign_task(assignee="captain") before updating active member-owned work.',
1472
1473
  parameters: {
1473
1474
  task_id: { type: 'string', required: true, description: 'The task id to update.' },
1474
1475
  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.' },
@@ -1477,7 +1478,8 @@ export function registerAgentTeamsTools(ctx, config) {
1477
1478
  enum: ['in_progress', 'completed', 'failed', 'cancelled'],
1478
1479
  description: 'New status (in_progress, completed, failed, cancelled).',
1479
1480
  },
1480
- output: { type: 'string', description: 'Result summary; set when completing or failing.' },
1481
+ output: { type: 'string', description: 'Original result summary; immutable after completion/failure.' },
1482
+ evidence_note: { type: 'string', description: 'Append-only supplementary observation on a terminal task. Does not reopen work or change the original result.' },
1481
1483
  verdict: {
1482
1484
  type: 'string',
1483
1485
  enum: ['pass', 'needs_revision', 'reject'],
@@ -1543,11 +1545,13 @@ export function registerAgentTeamsTools(ctx, config) {
1543
1545
  output: { type: 'string' },
1544
1546
  attempt: { type: 'number', required: true },
1545
1547
  attempt_id: { type: 'string' },
1548
+ evidence_count: { type: 'number' },
1549
+ follow_up: { type: 'string' },
1546
1550
  },
1547
1551
  },
1548
1552
  render: (args, value) => [{
1549
1553
  type: 'text',
1550
- text: `Task ${value.task_id} attempt ${value.attempt} → ${value.status}${value.output !== undefined ? `\nOutput: ${value.output}` : ''}`,
1554
+ text: `Task ${value.task_id} attempt ${value.attempt} → ${value.status}${value.output !== undefined ? `\nOutput: ${value.output}` : ''}${value.evidence_count === undefined ? '' : `\nSupplemental evidence records: ${value.evidence_count}. Original result unchanged.`}${value.follow_up ? `\n${value.follow_up}` : ''}`,
1551
1555
  }],
1552
1556
  },
1553
1557
  async execute(args, exec) {
@@ -1555,12 +1559,14 @@ export function registerAgentTeamsTools(ctx, config) {
1555
1559
  const workspace = workspaceOf(caller);
1556
1560
  const stateRoot = stateRootOf(workspace, config);
1557
1561
  const team = await requireParticipantTeam(workspace, config, caller);
1562
+ let followUpMessage;
1558
1563
  const updated = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
1559
1564
  const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
1560
1565
  const task = requireTask(fresh, args.task_id);
1561
1566
  if (identity.kind === 'captain'
1562
1567
  && task.assignee !== undefined
1563
1568
  && task.assignee !== CAPTAIN_KEY
1569
+ && !TERMINAL_TASK_STATUSES.includes(task.status)
1564
1570
  && !(args.status === 'cancelled' && task.status === 'pending' && (task.attempt ?? 0) === 0 && task.reassigning !== true)) {
1565
1571
  throw new Error(`task ${task.id} is owned by member "${task.assignee}"; call agent_teams_reassign_task with assignee="captain" before takeover`);
1566
1572
  }
@@ -1576,12 +1582,14 @@ export function registerAgentTeamsTools(ctx, config) {
1576
1582
  }
1577
1583
  }
1578
1584
  if (TERMINAL_TASK_STATUSES.includes(task.status)) {
1579
- const sameStatus = args.status === undefined || args.status === task.status;
1580
- const sameOutput = args.output === undefined || args.output === task.output;
1581
- if (!sameStatus || !sameOutput) {
1582
- throw new Error(`terminal task ${task.id} is immutable; use agent_teams_reassign_task to retry failed/cancelled work`);
1583
- }
1585
+ const appended = appendTaskEvidence(task, {
1586
+ ...args, findings: parseFindings(args.findings), changedPaths: normalizeBlankOptionalTaskFields(args).changedPaths,
1587
+ acceptanceResults: parseAcceptanceResults(args.acceptanceResults), commandsRun: parseCommandResults(args.commandsRun),
1588
+ }, identity.name);
1589
+ if (appended)
1590
+ await writeTeam(stateRoot, fresh);
1584
1591
  return {
1592
+ evidence_count: task.supplementalEvidence?.length ?? 0,
1585
1593
  task_id: task.id,
1586
1594
  status: task.status,
1587
1595
  attempt: task.attempt ?? 0,
@@ -1589,6 +1597,8 @@ export function registerAgentTeamsTools(ctx, config) {
1589
1597
  ...task.output !== undefined ? { output: task.output } : {},
1590
1598
  };
1591
1599
  }
1600
+ if (args.evidence_note?.trim())
1601
+ throw new Error('evidence_note is for terminal tasks; record active work with output and structured evidence');
1592
1602
  // Blank optional list entries (e.g. changedPaths:[""]) must not be
1593
1603
  // persisted: hasValidQualityTaskFields rejects them on reload and
1594
1604
  // would brick the whole team state (issue #105 class).
@@ -1626,13 +1636,22 @@ export function registerAgentTeamsTools(ctx, config) {
1626
1636
  if (commandsRun !== undefined)
1627
1637
  task.commandsRun = commandsRun;
1628
1638
  task.updatedAt = Date.now();
1639
+ const priorDependencies = new Map(fresh.tasks.map(item => [item.id, [...item.dependencies]]));
1629
1640
  const followUp = (task.status === 'failed' && (task.verdict === 'needs_revision' || task.verdict === 'reject'))
1630
1641
  ? applyQualityFollowUp(fresh, task)
1631
1642
  : undefined;
1643
+ let followUpSummary;
1644
+ if ((followUp?.created.length ?? 0) > 0) {
1645
+ 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.`;
1647
+ followUpMessage = createMessage(CAPTAIN_KEY, CAPTAIN_KEY, followUpSummary);
1648
+ }
1632
1649
  if (followUp?.escalated === true) {
1633
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.`));
1634
1651
  }
1635
1652
  await writeTeam(stateRoot, fresh);
1653
+ if (followUpMessage !== undefined)
1654
+ await appendMailbox(stateRoot, fresh.id, CAPTAIN_KEY, followUpMessage);
1636
1655
  appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, caller.session), 'agent-teams/task-updated', {
1637
1656
  teamId: fresh.id,
1638
1657
  taskId: task.id,
@@ -1654,6 +1673,7 @@ export function registerAgentTeamsTools(ctx, config) {
1654
1673
  });
1655
1674
  }
1656
1675
  return {
1676
+ ...followUpSummary === undefined ? {} : { follow_up: followUpSummary },
1657
1677
  task_id: task.id,
1658
1678
  status: task.status,
1659
1679
  attempt: task.attempt ?? 0,
@@ -1661,6 +1681,12 @@ export function registerAgentTeamsTools(ctx, config) {
1661
1681
  ...task.output !== undefined ? { output: task.output } : {},
1662
1682
  };
1663
1683
  });
1684
+ if (followUpMessage !== undefined) {
1685
+ const captain = ctx.agents.get(team.captainSessionId);
1686
+ if (captain !== undefined && steerCaptainReport(captain, CAPTAIN_KEY, followUpMessage.content, mailboxPrompt(team.id, CAPTAIN_KEY, [followUpMessage]))) {
1687
+ await withTeamLock(teamLockKey(stateRoot, team.id), () => markMailboxDelivered(stateRoot, team.id, CAPTAIN_KEY, [followUpMessage.id]));
1688
+ }
1689
+ }
1664
1690
  await scheduler.kickTeam(workspace, team.id, team.captainSessionId === caller.id ? caller : undefined);
1665
1691
  return updated;
1666
1692
  },
@@ -1749,6 +1775,8 @@ export function registerAgentTeamsTools(ctx, config) {
1749
1775
  name: 'agent_teams_send_message',
1750
1776
  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.',
1751
1777
  parameters: {
1778
+ source_task_id: { type: 'string', description: 'Sender task, NOT the recipient task. Members include it with source_attempt_id. Captains sending guidance normally omit both source fields.' },
1779
+ source_attempt_id: { type: 'string', description: 'Sender execution capability paired with source_task_id. Stale reports are rejected. Omit for ordinary captain guidance.' },
1752
1780
  to: { type: 'string', required: true, description: 'Recipient: "captain" or a member name.' },
1753
1781
  content: { type: 'string', required: true, description: 'The message text.' },
1754
1782
  from: { type: 'string', description: 'Sender (defaults to the caller: the captain, or the calling member).' },
@@ -1761,7 +1789,7 @@ export function registerAgentTeamsTools(ctx, config) {
1761
1789
  message_id: { type: 'string', required: true },
1762
1790
  from: { type: 'string', required: true },
1763
1791
  to: { type: 'string', required: true },
1764
- delivered: { type: 'string', required: true, description: 'live (accepted by the live captain), wake (member recipient woken), or mailbox (durable inbox only).' },
1792
+ delivered: { type: 'string', required: true, description: 'live (accepted by the live captain), wake (member recipient woken), mailbox (durable inbox only), or duplicate (same message already retained).' },
1765
1793
  },
1766
1794
  },
1767
1795
  render: (args, value) => [{
@@ -1783,8 +1811,26 @@ export function registerAgentTeamsTools(ctx, config) {
1783
1811
  if (args.from !== undefined && args.from !== from) {
1784
1812
  throw new Error(`agent_teams_send_message: "from" must be your own identity ("${from}"), not "${args.from}"`);
1785
1813
  }
1814
+ const sourceTaskId = args.source_task_id?.trim() || undefined;
1815
+ const sourceAttemptId = args.source_attempt_id?.trim() || undefined;
1816
+ if ((sourceTaskId === undefined) !== (sourceAttemptId === undefined))
1817
+ throw new Error('send_message requires source_task_id and source_attempt_id together');
1818
+ const source = sourceTaskId === undefined
1819
+ ? identity.kind === 'member' ? memberOpenTask(fresh, identity.name) ?? fresh.tasks.filter(item => item.assignee === identity.name && item.attemptId !== undefined).sort((a, b) => b.updatedAt - a.updatedAt)[0] : undefined
1820
+ : requireTask(fresh, sourceTaskId);
1821
+ if (sourceTaskId !== undefined && (source?.assignee !== identity.name || source.attemptId !== sourceAttemptId)) {
1822
+ throw new Error('stale or foreign source attempt; stop sending results from the revoked task');
1823
+ }
1824
+ const sourceFields = source?.attemptId === undefined ? {} : { sourceTaskId: source.id, sourceAttemptId: source.attemptId, sourceTaskStatus: source.status };
1825
+ const owned = to === CAPTAIN_KEY ? undefined : memberOpenTask(fresh, requireMember(fresh, to).name);
1826
+ const duplicate = (await readMailbox(stateRoot, fresh.id, to)).find(message => message.from === from
1827
+ && message.content === args.content && message.taskId === owned?.id && message.attemptId === owned?.attemptId
1828
+ && message.sourceTaskId === sourceFields.sourceTaskId && message.sourceAttemptId === sourceFields.sourceAttemptId && message.sourceTaskStatus === sourceFields.sourceTaskStatus
1829
+ && isCurrentMail(fresh, message) && (message.attemptId !== undefined || message.sourceAttemptId !== undefined || message.readAt === undefined));
1830
+ if (duplicate !== undefined)
1831
+ return { kind: 'duplicate', message: duplicate, from };
1786
1832
  if (to === CAPTAIN_KEY) {
1787
- const message = { ...createMessage(from, CAPTAIN_KEY, args.content), deliveryClaimedAt: Date.now() };
1833
+ const message = { ...createMessage(from, CAPTAIN_KEY, args.content), ...sourceFields, deliveryClaimedAt: Date.now() };
1788
1834
  await appendMailbox(stateRoot, fresh.id, CAPTAIN_KEY, message);
1789
1835
  appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, caller.session), 'agent-teams/message-sent', {
1790
1836
  teamId: fresh.id,
@@ -1800,8 +1846,7 @@ export function registerAgentTeamsTools(ctx, config) {
1800
1846
  throw new Error(`team "${fresh.name}" is halted; call agent_teams_resume before waking a member`);
1801
1847
  }
1802
1848
  const recipient = requireMember(fresh, to);
1803
- const owned = memberOpenTask(fresh, recipient.name);
1804
- const message = { ...createMessage(from, recipient.name, args.content), deliveryClaimedAt: Date.now(),
1849
+ const message = { ...createMessage(from, recipient.name, args.content), ...sourceFields, deliveryClaimedAt: Date.now(),
1805
1850
  ...owned?.attemptId === undefined ? {} : { taskId: owned.id, attemptId: owned.attemptId },
1806
1851
  };
1807
1852
  await appendMailbox(stateRoot, fresh.id, recipient.name, message);
@@ -1815,6 +1860,8 @@ export function registerAgentTeamsTools(ctx, config) {
1815
1860
  });
1816
1861
  return { kind: 'member', fresh, identity, message, from, recipient };
1817
1862
  });
1863
+ if (prepared.kind === 'duplicate')
1864
+ return { message_id: prepared.message.id, from: prepared.from, to: prepared.message.to, delivered: 'duplicate' };
1818
1865
  // Resolve the exact live captain only after releasing the state lock.
1819
1866
  // The plugin mailbox is already durable if live delivery cannot proceed.
1820
1867
  const captain = ctx.agents.get(prepared.fresh.captainSessionId);
@@ -1893,6 +1940,7 @@ export function registerAgentTeamsTools(ctx, config) {
1893
1940
  kind: taskKindOf(task),
1894
1941
  ...task.round === undefined ? {} : { round: task.round },
1895
1942
  ...task.verdict === undefined ? {} : { verdict: task.verdict },
1943
+ ...task.supplementalEvidence === undefined ? {} : { supplemental_evidence: JSON.stringify(task.supplementalEvidence) },
1896
1944
  findings_open: (task.findings ?? []).filter((finding) => finding.resolved !== true).length,
1897
1945
  ...task.profileSeedId === undefined ? {} : { seed_id: task.profileSeedId },
1898
1946
  ...task.output !== undefined ? { output: task.output } : {},
@@ -1906,15 +1954,15 @@ export function registerAgentTeamsTools(ctx, config) {
1906
1954
  }
1907
1955
  };
1908
1956
  const captainInbox = identity.kind === 'captain'
1909
- ? await readUnreadMailbox(stateRoot, team.id, CAPTAIN_KEY, reportMalformed(CAPTAIN_KEY))
1957
+ ? await readCurrentMailbox(stateRoot, team.id, CAPTAIN_KEY, reportMalformed(CAPTAIN_KEY))
1910
1958
  : [];
1911
- const ownInbox = identity.kind === 'member' ? (await readUnreadMailbox(stateRoot, team.id, identity.name)).slice(0, 10) : [];
1959
+ const ownInbox = identity.kind === 'member' ? (await readCurrentMailbox(stateRoot, team.id, identity.name)).slice(0, 10) : [];
1912
1960
  const memberInboxes = {};
1913
1961
  const visibleMembers = identity.kind === 'captain'
1914
1962
  ? members
1915
1963
  : members.filter((member) => member.name === identity.name);
1916
1964
  for (const member of visibleMembers) {
1917
- const messages = await readUnreadMailbox(stateRoot, team.id, member.name, reportMalformed(member.name));
1965
+ const messages = await readCurrentMailbox(stateRoot, team.id, member.name, reportMalformed(member.name));
1918
1966
  if (messages.length > 0) {
1919
1967
  memberInboxes[member.name] = {
1920
1968
  count: messages.length,
@@ -1957,10 +2005,10 @@ export function registerAgentTeamsTools(ctx, config) {
1957
2005
  tasks,
1958
2006
  captain_inbox: captainInbox.slice(0, 10).map((message) => ({
1959
2007
  from: message.from,
1960
- content: message.content,
2008
+ content: mailboxContent(message),
1961
2009
  ts: message.ts,
1962
2010
  })),
1963
- member_inbox: ownInbox.map(message => ({ from: message.from, content: message.content, ts: message.ts })),
2011
+ member_inbox: ownInbox.map(message => ({ from: message.from, content: mailboxContent(message), ts: message.ts })),
1964
2012
  member_inboxes: memberInboxes,
1965
2013
  mailbox_warnings: mailboxWarnings,
1966
2014
  mailbox_warning_count: mailboxWarningCount,
@@ -2326,12 +2374,13 @@ function renderStatus(value) {
2326
2374
  ...team.tasks.map((task) => {
2327
2375
  const deps = task.dependencies.length > 0 ? ` (deps: ${task.dependencies.join(',')})` : '';
2328
2376
  const output = task.output !== undefined ? `\n output: ${task.output.slice(0, 300)}` : '';
2377
+ const evidence = task.supplemental_evidence ? `\n Supplemental observations (original verdict unchanged): ${task.supplemental_evidence}` : '';
2329
2378
  const handoff = task.reassigning ? ' (reassigning)' : '';
2330
2379
  const seed = task.seed_id === undefined || task.seed_id === '' ? '' : ` seed ${task.seed_id}`;
2331
2380
  const kind = task.kind ? ` ${task.kind}` : '';
2332
2381
  const round = task.round === undefined ? '' : ` r${task.round}`;
2333
2382
  const verdict = task.verdict === undefined ? '' : ` verdict ${task.verdict}`;
2334
- return ` - ${task.id} [${task.status}]${kind}${round}${verdict} attempt ${task.attempt}${handoff}${seed} ${task.subject} → ${task.assignee || 'unassigned'}${deps}${output}`;
2383
+ return ` - ${task.id} [${task.status}]${kind}${round}${verdict} attempt ${task.attempt}${handoff}${seed} ${task.subject} → ${task.assignee || 'unassigned'}${deps}${output}${evidence}`;
2335
2384
  }),
2336
2385
  ...team.coverage === undefined || team.coverage.length === 0 ? [] : [
2337
2386
  'Coverage:',
@@ -1,4 +1,3 @@
1
- /** Stable, agent-scoped presentation. Business authority stays in the tools. */
2
1
  import type { Context } from '@deepseek-ai/cordis';
3
2
  import type { Agent } from '@deepseek-ai/dsh-agent';
4
3
  export declare const TEAM_ACTIVATION_PROMPT = "AgentTeams (Agent Teams) provides multi-agent team collaboration. Apply these rules when the user requests it (including /agent-teams) or when continuing an existing team. Mentioning, quoting, discussing, or declining AgentTeams alone is not a request to start work.";