@tmustier/pi-agent-teams 0.4.0-beta.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +72 -9
- package/WORKFLOW.md +110 -0
- package/docs/claude-parity.md +18 -13
- package/docs/hook-contract.md +183 -0
- package/docs/smoke-test-plan.md +26 -7
- package/extensions/teams/activity-tracker.ts +296 -8
- package/extensions/teams/cleanup.ts +216 -3
- package/extensions/teams/hooks.ts +57 -5
- package/extensions/teams/leader-attach-commands.ts +8 -4
- package/extensions/teams/leader-inbox.ts +162 -4
- package/extensions/teams/leader-info-commands.ts +105 -3
- package/extensions/teams/leader-lifecycle-commands.ts +205 -3
- package/extensions/teams/leader-messaging-commands.ts +19 -7
- package/extensions/teams/leader-spawn-command.ts +5 -1
- package/extensions/teams/leader-team-command.ts +51 -2
- package/extensions/teams/leader-teams-tool.ts +387 -11
- package/extensions/teams/leader.ts +126 -52
- package/extensions/teams/mailbox.ts +6 -1
- package/extensions/teams/model-policy.ts +117 -0
- package/extensions/teams/spawn-types.ts +4 -0
- package/extensions/teams/teammate-rpc.ts +14 -0
- package/extensions/teams/teams-panel.ts +117 -19
- package/extensions/teams/teams-ui-shared.ts +205 -2
- package/extensions/teams/teams-widget.ts +67 -14
- package/extensions/teams/worker.ts +18 -6
- package/extensions/teams/worktree.ts +143 -0
- package/package.json +4 -2
- package/scripts/integration-cleanup-test.mts +419 -0
- package/scripts/integration-hooks-remediation-test.mts +382 -0
- package/scripts/integration-spawn-overrides-test.mts +10 -0
- package/scripts/smoke-test.mts +701 -3
- package/skills/agent-teams/SKILL.md +28 -7
|
@@ -9,6 +9,12 @@ import { getTeamDir } from "./paths.js";
|
|
|
9
9
|
import { TEAM_MAILBOX_NS, taskAssignmentPayload } from "./protocol.js";
|
|
10
10
|
import { ensureTeamConfig, setMemberStatus, updateTeamHooksPolicy } from "./team-config.js";
|
|
11
11
|
import { getTeamsNamingRules, getTeamsStyleFromEnv, type TeamsStyle, formatMemberDisplayName, getTeamsStrings } from "./teams-style.js";
|
|
12
|
+
import {
|
|
13
|
+
formatProviderModel,
|
|
14
|
+
isDeprecatedTeammateModelId,
|
|
15
|
+
resolveTeammateModelSelection,
|
|
16
|
+
type TeammateModelSource,
|
|
17
|
+
} from "./model-policy.js";
|
|
12
18
|
import {
|
|
13
19
|
getTeamsHookFailureAction,
|
|
14
20
|
getTeamsHookFollowupOwnerPolicy,
|
|
@@ -27,10 +33,25 @@ import {
|
|
|
27
33
|
updateTask,
|
|
28
34
|
} from "./task-store.js";
|
|
29
35
|
import type { TeammateRpc } from "./teammate-rpc.js";
|
|
36
|
+
import type { ActivityTracker } from "./activity-tracker.js";
|
|
37
|
+
import {
|
|
38
|
+
resolveDisplayStatus,
|
|
39
|
+
formatElapsed,
|
|
40
|
+
lastMessageSummary,
|
|
41
|
+
formatTokens,
|
|
42
|
+
toolActivity,
|
|
43
|
+
} from "./teams-ui-shared.js";
|
|
30
44
|
import type { ContextMode, WorkspaceMode, SpawnTeammateFn } from "./spawn-types.js";
|
|
45
|
+
import type { DelegationTracker } from "./leader-inbox.js";
|
|
31
46
|
|
|
32
47
|
type TeamsToolDelegateTask = { text: string; assignee?: string };
|
|
33
48
|
|
|
49
|
+
function describeModelSource(source: TeammateModelSource): string {
|
|
50
|
+
if (source === "override") return "override";
|
|
51
|
+
if (source === "inherit_leader") return "leader";
|
|
52
|
+
return "teammate-default";
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
const TeamsActionSchema = StringEnum(
|
|
35
56
|
[
|
|
36
57
|
"delegate",
|
|
@@ -44,13 +65,17 @@ const TeamsActionSchema = StringEnum(
|
|
|
44
65
|
"message_broadcast",
|
|
45
66
|
"message_steer",
|
|
46
67
|
"member_spawn",
|
|
68
|
+
"member_status",
|
|
47
69
|
"member_shutdown",
|
|
48
70
|
"member_kill",
|
|
49
71
|
"member_prune",
|
|
72
|
+
"team_done",
|
|
50
73
|
"plan_approve",
|
|
51
74
|
"plan_reject",
|
|
52
75
|
"hooks_policy_get",
|
|
53
76
|
"hooks_policy_set",
|
|
77
|
+
"model_policy_get",
|
|
78
|
+
"model_policy_check",
|
|
54
79
|
] as const,
|
|
55
80
|
{
|
|
56
81
|
description: "Teams tool action.",
|
|
@@ -101,7 +126,7 @@ const TeamsToolParamsSchema = Type.Object({
|
|
|
101
126
|
message: Type.Optional(Type.String({ description: "Message body for messaging actions." })),
|
|
102
127
|
reason: Type.Optional(Type.String({ description: "Optional reason for lifecycle actions." })),
|
|
103
128
|
feedback: Type.Optional(Type.String({ description: "Feedback for action=plan_reject." })),
|
|
104
|
-
all: Type.Optional(Type.Boolean({ description: "For member_shutdown/member_prune
|
|
129
|
+
all: Type.Optional(Type.Boolean({ description: "For member_shutdown/member_prune: apply to all workers. For team_done: force even with in-progress tasks." })),
|
|
105
130
|
planRequired: Type.Optional(Type.Boolean({ description: "For member_spawn, start worker in plan-required mode." })),
|
|
106
131
|
teammates: Type.Optional(
|
|
107
132
|
Type.Array(Type.String(), {
|
|
@@ -131,6 +156,7 @@ const TeamsToolParamsSchema = Type.Object({
|
|
|
131
156
|
),
|
|
132
157
|
hookFollowupOwner: Type.Optional(TeamsHookFollowupOwnerSchema),
|
|
133
158
|
hooksPolicyReset: Type.Optional(Type.Boolean({ description: "For hooks_policy_set, clear team-level overrides before applying fields." })),
|
|
159
|
+
urgent: Type.Optional(Type.Boolean({ description: "For message_dm/message_broadcast: interrupt the recipient's active turn via steering instead of waiting for idle. Use sparingly." })),
|
|
134
160
|
});
|
|
135
161
|
|
|
136
162
|
type TeamsToolParamsType = Static<typeof TeamsToolParamsSchema>;
|
|
@@ -141,18 +167,25 @@ export function registerTeamsTool(opts: {
|
|
|
141
167
|
spawnTeammate: SpawnTeammateFn;
|
|
142
168
|
getTeamId: (ctx: Parameters<SpawnTeammateFn>[0]) => string;
|
|
143
169
|
getTaskListId: () => string | null;
|
|
170
|
+
getTracker: () => ActivityTracker;
|
|
171
|
+
getTeamConfig: () => import("./team-config.js").TeamConfig | null;
|
|
144
172
|
refreshTasks: () => Promise<void>;
|
|
145
173
|
renderWidget: () => void;
|
|
174
|
+
hideWidget: () => void;
|
|
175
|
+
stopAllTeammates: (reason: string) => Promise<void>;
|
|
146
176
|
pendingPlanApprovals: Map<string, { requestId: string; name: string; taskId?: string }>;
|
|
177
|
+
delegationTracker?: DelegationTracker;
|
|
147
178
|
}): void {
|
|
148
|
-
const { pi, teammates, spawnTeammate, getTeamId, getTaskListId, refreshTasks, renderWidget, pendingPlanApprovals } = opts;
|
|
179
|
+
const { pi, teammates, spawnTeammate, getTeamId, getTaskListId, getTracker, getTeamConfig: getTeamCfg, refreshTasks, renderWidget, hideWidget, stopAllTeammates, pendingPlanApprovals, delegationTracker } = opts;
|
|
149
180
|
|
|
150
181
|
pi.registerTool({
|
|
151
182
|
name: "teams",
|
|
152
183
|
label: "Teams",
|
|
153
184
|
description: [
|
|
154
185
|
"Spawn comrade agents and delegate tasks. Each comrade is a child Pi process that executes work autonomously and reports back.",
|
|
155
|
-
"You can also mutate existing tasks (assign, unassign, set status, dependencies), send team messages, run teammate lifecycle actions, and manage hooks
|
|
186
|
+
"You can also mutate existing tasks (assign, unassign, set status, dependencies), send team messages, run teammate lifecycle actions, and manage hooks/model policy without user slash commands.",
|
|
187
|
+
"Use member_status (with optional name) to get real-time worker state: activity, time in state, stall detection, tool use, tokens, and last message summary.",
|
|
188
|
+
"Use team_done to end a team run when all tasks are complete (stops teammates, hides widget).",
|
|
156
189
|
"Provide a list of tasks with optional assignees; comrades are spawned automatically and assigned round-robin if unspecified.",
|
|
157
190
|
"Options: contextMode=branch (clone session context), workspaceMode=worktree (git worktree isolation).",
|
|
158
191
|
"Optional overrides: model='<provider>/<modelId>' and thinking (off|minimal|low|medium|high|xhigh).",
|
|
@@ -369,14 +402,17 @@ export function registerTeamsTool(opts: {
|
|
|
369
402
|
};
|
|
370
403
|
}
|
|
371
404
|
const name = sanitizeName(nameRaw);
|
|
405
|
+
const isUrgent = params.urgent === true;
|
|
372
406
|
await writeToMailbox(teamDir, TEAM_MAILBOX_NS, name, {
|
|
373
407
|
from: cfg.leadName,
|
|
374
408
|
text: message,
|
|
375
409
|
timestamp: new Date().toISOString(),
|
|
410
|
+
...(isUrgent ? { urgent: true } : {}),
|
|
376
411
|
});
|
|
412
|
+
const verb = isUrgent ? "Urgent DM" : "DM";
|
|
377
413
|
return {
|
|
378
|
-
content: [{ type: "text", text:
|
|
379
|
-
details: { action, teamId, name, mailboxNamespace: TEAM_MAILBOX_NS },
|
|
414
|
+
content: [{ type: "text", text: `${verb} queued for ${formatMemberDisplayName(style, name)}` }],
|
|
415
|
+
details: { action, teamId, name, urgent: isUrgent, mailboxNamespace: TEAM_MAILBOX_NS },
|
|
380
416
|
};
|
|
381
417
|
}
|
|
382
418
|
|
|
@@ -404,6 +440,7 @@ export function registerTeamsTool(opts: {
|
|
|
404
440
|
details: { action, recipients: [] },
|
|
405
441
|
};
|
|
406
442
|
}
|
|
443
|
+
const isUrgent = params.urgent === true;
|
|
407
444
|
const ts = new Date().toISOString();
|
|
408
445
|
await Promise.all(
|
|
409
446
|
names.map((name) =>
|
|
@@ -411,12 +448,14 @@ export function registerTeamsTool(opts: {
|
|
|
411
448
|
from: cfg.leadName,
|
|
412
449
|
text: message,
|
|
413
450
|
timestamp: ts,
|
|
451
|
+
...(isUrgent ? { urgent: true } : {}),
|
|
414
452
|
}),
|
|
415
453
|
),
|
|
416
454
|
);
|
|
455
|
+
const verb = isUrgent ? "Urgent broadcast" : "Broadcast";
|
|
417
456
|
return {
|
|
418
|
-
content: [{ type: "text", text:
|
|
419
|
-
details: { action, teamId, recipients: names, mailboxNamespace: TEAM_MAILBOX_NS },
|
|
457
|
+
content: [{ type: "text", text: `${verb} queued for ${names.length} ${strings.memberTitle.toLowerCase()}(s): ${names.map((n) => formatMemberDisplayName(style, n)).join(", ")}` }],
|
|
458
|
+
details: { action, teamId, recipients: names, urgent: isUrgent, mailboxNamespace: TEAM_MAILBOX_NS },
|
|
420
459
|
};
|
|
421
460
|
}
|
|
422
461
|
|
|
@@ -485,11 +524,152 @@ export function registerTeamsTool(opts: {
|
|
|
485
524
|
const lines: string[] = [
|
|
486
525
|
`Spawned ${formatMemberDisplayName(style, res.name)} (${res.mode}/${res.workspaceMode})`,
|
|
487
526
|
];
|
|
527
|
+
if (res.model) lines.push(`model: ${res.model}`);
|
|
528
|
+
if (res.thinking) lines.push(`thinking: ${res.thinking}`);
|
|
488
529
|
if (res.note) lines.push(`note: ${res.note}`);
|
|
489
530
|
for (const w of res.warnings) lines.push(`warning: ${w}`);
|
|
490
531
|
return {
|
|
491
532
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
492
|
-
details: {
|
|
533
|
+
details: {
|
|
534
|
+
action,
|
|
535
|
+
teamId,
|
|
536
|
+
name: res.name,
|
|
537
|
+
mode: res.mode,
|
|
538
|
+
workspaceMode: res.workspaceMode,
|
|
539
|
+
model: res.model,
|
|
540
|
+
thinking: res.thinking,
|
|
541
|
+
warnings: res.warnings,
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (action === "member_status") {
|
|
547
|
+
const nameRaw = params.name?.trim();
|
|
548
|
+
const name = sanitizeName(nameRaw ?? "");
|
|
549
|
+
const teamCfg = getTeamCfg();
|
|
550
|
+
|
|
551
|
+
// If no name given, return summary for all workers.
|
|
552
|
+
if (!name) {
|
|
553
|
+
const allTasks = await listTasks(teamDir, effectiveTlId);
|
|
554
|
+
const tracker = getTracker();
|
|
555
|
+
const cfgMembers = teamCfg?.members ?? [];
|
|
556
|
+
const cfgByName = new Map<string, (typeof cfgMembers)[number]>();
|
|
557
|
+
for (const m of cfgMembers) cfgByName.set(m.name, m);
|
|
558
|
+
|
|
559
|
+
const workerNames = new Set<string>();
|
|
560
|
+
for (const n of teammates.keys()) workerNames.add(n);
|
|
561
|
+
for (const m of cfgMembers) {
|
|
562
|
+
if (m.role === "worker" && m.status === "online") workerNames.add(m.name);
|
|
563
|
+
}
|
|
564
|
+
for (const t of allTasks) {
|
|
565
|
+
if (t.owner && t.owner !== (teamCfg?.leadName ?? "") && t.status === "in_progress") workerNames.add(t.owner);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (workerNames.size === 0) {
|
|
569
|
+
return {
|
|
570
|
+
content: [{ type: "text", text: `No ${strings.memberTitle.toLowerCase()}s to report on` }],
|
|
571
|
+
details: { action, teamId, workers: [] },
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const workers: Array<Record<string, unknown>> = [];
|
|
576
|
+
const lines: string[] = [];
|
|
577
|
+
for (const n of Array.from(workerNames).sort()) {
|
|
578
|
+
const rpc = teammates.get(n);
|
|
579
|
+
const memberCfg = cfgByName.get(n);
|
|
580
|
+
const displayStatus = resolveDisplayStatus(rpc, memberCfg);
|
|
581
|
+
const activity = tracker.get(n);
|
|
582
|
+
const elapsed = rpc ? formatElapsed(Date.now() - rpc.lastStatusChangeAt) : "";
|
|
583
|
+
const noEventFor = rpc ? formatElapsed(Date.now() - rpc.lastEventAt) : "";
|
|
584
|
+
const currentTool = toolActivity(activity.currentToolName);
|
|
585
|
+
const msgPreview = lastMessageSummary(rpc, 80);
|
|
586
|
+
const model = memberCfg?.meta?.["model"];
|
|
587
|
+
|
|
588
|
+
lines.push(`${formatMemberDisplayName(style, n)}: ${displayStatus} ${elapsed}${currentTool ? ` (${currentTool})` : ""} · ${formatTokens(activity.totalTokens)} tokens`);
|
|
589
|
+
if (msgPreview) lines.push(` last: ${msgPreview}`);
|
|
590
|
+
|
|
591
|
+
workers.push({
|
|
592
|
+
name: n,
|
|
593
|
+
status: displayStatus,
|
|
594
|
+
transportStatus: rpc?.status ?? "unknown",
|
|
595
|
+
timeInState: elapsed,
|
|
596
|
+
lastEventAgo: noEventFor,
|
|
597
|
+
currentTool: activity.currentToolName,
|
|
598
|
+
toolUseCount: activity.toolUseCount,
|
|
599
|
+
turnCount: activity.turnCount,
|
|
600
|
+
totalTokens: activity.totalTokens,
|
|
601
|
+
model: typeof model === "string" ? model : undefined,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
607
|
+
details: { action, teamId, workers },
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Single worker status
|
|
612
|
+
const rpc = teammates.get(name);
|
|
613
|
+
const memberCfg = (teamCfg?.members ?? []).find((m) => m.name === name);
|
|
614
|
+
if (!rpc && !memberCfg) {
|
|
615
|
+
return {
|
|
616
|
+
content: [{ type: "text", text: `Unknown ${strings.memberTitle.toLowerCase()}: ${name}` }],
|
|
617
|
+
details: { action, name },
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const displayStatus = resolveDisplayStatus(rpc, memberCfg);
|
|
622
|
+
const tracker = getTracker();
|
|
623
|
+
const activity = tracker.get(name);
|
|
624
|
+
const elapsed = rpc ? formatElapsed(Date.now() - rpc.lastStatusChangeAt) : "";
|
|
625
|
+
const noEventFor = rpc ? formatElapsed(Date.now() - rpc.lastEventAt) : "";
|
|
626
|
+
const currentTool = toolActivity(activity.currentToolName);
|
|
627
|
+
const msgPreview = lastMessageSummary(rpc, 120);
|
|
628
|
+
const allTasks = await listTasks(teamDir, effectiveTlId);
|
|
629
|
+
const owned = allTasks.filter((t) => t.owner === name);
|
|
630
|
+
const activeTask = owned.find((t) => t.status === "in_progress");
|
|
631
|
+
const model = memberCfg?.meta?.["model"];
|
|
632
|
+
const cwd = memberCfg?.cwd;
|
|
633
|
+
|
|
634
|
+
const lines: string[] = [
|
|
635
|
+
`${formatMemberDisplayName(style, name)}: ${displayStatus}`,
|
|
636
|
+
`time in state: ${elapsed || "(unknown)"}`,
|
|
637
|
+
`last event: ${noEventFor || "(unknown)"} ago`,
|
|
638
|
+
`current activity: ${currentTool || "(none)"}`,
|
|
639
|
+
`tool calls: ${activity.toolUseCount} · turns: ${activity.turnCount} · tokens: ${formatTokens(activity.totalTokens)}`,
|
|
640
|
+
];
|
|
641
|
+
if (typeof model === "string" && model) lines.push(`model: ${model}`);
|
|
642
|
+
if (cwd) lines.push(`cwd: ${cwd}`);
|
|
643
|
+
if (activeTask) lines.push(`active task: #${activeTask.id} ${activeTask.subject}`);
|
|
644
|
+
lines.push(`tasks: ${owned.filter((t) => t.status === "pending").length} pending · ${owned.filter((t) => t.status === "in_progress").length} in-progress · ${owned.filter((t) => t.status === "completed").length} completed`);
|
|
645
|
+
if (msgPreview) lines.push(`last message: ${msgPreview}`);
|
|
646
|
+
if (displayStatus === "stalled") {
|
|
647
|
+
lines.push(`\u26a0 WARNING: no agent events for ${noEventFor} — worker may be stalled`);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
652
|
+
details: {
|
|
653
|
+
action,
|
|
654
|
+
teamId,
|
|
655
|
+
name,
|
|
656
|
+
status: displayStatus,
|
|
657
|
+
transportStatus: rpc?.status ?? "unknown",
|
|
658
|
+
timeInState: elapsed,
|
|
659
|
+
lastEventAgo: noEventFor,
|
|
660
|
+
currentTool: activity.currentToolName,
|
|
661
|
+
toolUseCount: activity.toolUseCount,
|
|
662
|
+
turnCount: activity.turnCount,
|
|
663
|
+
totalTokens: activity.totalTokens,
|
|
664
|
+
model: typeof model === "string" ? model : undefined,
|
|
665
|
+
activeTaskId: activeTask?.id,
|
|
666
|
+
tasks: {
|
|
667
|
+
pending: owned.filter((t) => t.status === "pending").length,
|
|
668
|
+
inProgress: owned.filter((t) => t.status === "in_progress").length,
|
|
669
|
+
completed: owned.filter((t) => t.status === "completed").length,
|
|
670
|
+
},
|
|
671
|
+
stalled: displayStatus === "stalled",
|
|
672
|
+
},
|
|
493
673
|
};
|
|
494
674
|
}
|
|
495
675
|
|
|
@@ -626,6 +806,79 @@ export function registerTeamsTool(opts: {
|
|
|
626
806
|
};
|
|
627
807
|
}
|
|
628
808
|
|
|
809
|
+
if (action === "team_done") {
|
|
810
|
+
const tasks = await listTasks(teamDir, effectiveTlId);
|
|
811
|
+
const inProgress = tasks.filter((t) => t.status === "in_progress");
|
|
812
|
+
const force = params.all === true;
|
|
813
|
+
|
|
814
|
+
if (inProgress.length > 0 && !force) {
|
|
815
|
+
return {
|
|
816
|
+
content: [{
|
|
817
|
+
type: "text",
|
|
818
|
+
text: `${inProgress.length} task(s) still in progress. Set all=true to force.`,
|
|
819
|
+
}],
|
|
820
|
+
details: {
|
|
821
|
+
action,
|
|
822
|
+
teamId,
|
|
823
|
+
status: "blocked",
|
|
824
|
+
reason: "tasks_in_progress",
|
|
825
|
+
inProgress: inProgress.length,
|
|
826
|
+
hint: "Set all=true to force, or wait for tasks to complete.",
|
|
827
|
+
},
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Stop all RPC teammates (reuses leader's stopAllTeammates for proper
|
|
832
|
+
// event unsub + tracker/transcript cleanup — avoids stale state on reuse).
|
|
833
|
+
await stopAllTeammates("team done");
|
|
834
|
+
|
|
835
|
+
// Mark config workers offline + send shutdown mailbox messages
|
|
836
|
+
const cfgWorkers = cfg.members.filter((m) => m.role === "worker" && m.status === "online");
|
|
837
|
+
for (const m of cfgWorkers) {
|
|
838
|
+
if (teammates.has(m.name)) continue; // already stopped via RPC above
|
|
839
|
+
const ts = new Date().toISOString();
|
|
840
|
+
try {
|
|
841
|
+
await writeToMailbox(teamDir, TEAM_MAILBOX_NS, m.name, {
|
|
842
|
+
from: cfg.leadName,
|
|
843
|
+
text: JSON.stringify({
|
|
844
|
+
type: "shutdown_request",
|
|
845
|
+
requestId: randomUUID(),
|
|
846
|
+
from: cfg.leadName,
|
|
847
|
+
timestamp: ts,
|
|
848
|
+
reason: "Team done",
|
|
849
|
+
}),
|
|
850
|
+
timestamp: ts,
|
|
851
|
+
});
|
|
852
|
+
} catch {
|
|
853
|
+
// ignore mailbox errors
|
|
854
|
+
}
|
|
855
|
+
await setMemberStatus(teamDir, m.name, "offline", {
|
|
856
|
+
meta: { stoppedReason: "team-done", stoppedAt: ts },
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
await refreshTasks();
|
|
861
|
+
hideWidget();
|
|
862
|
+
|
|
863
|
+
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
864
|
+
const pending = tasks.filter((t) => t.status === "pending").length;
|
|
865
|
+
return {
|
|
866
|
+
content: [{
|
|
867
|
+
type: "text",
|
|
868
|
+
text: `Team done. ${tasks.length} task(s): ${completed} completed, ${pending} pending${inProgress.length > 0 ? `, ${inProgress.length} were in-progress (unassigned)` : ""}. Widget hidden.`,
|
|
869
|
+
}],
|
|
870
|
+
details: {
|
|
871
|
+
action,
|
|
872
|
+
teamId,
|
|
873
|
+
status: "succeeded",
|
|
874
|
+
total: tasks.length,
|
|
875
|
+
completed,
|
|
876
|
+
pending,
|
|
877
|
+
unassigned: inProgress.length,
|
|
878
|
+
},
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
629
882
|
if (action === "plan_approve") {
|
|
630
883
|
const nameRaw = params.name?.trim();
|
|
631
884
|
const name = sanitizeName(nameRaw ?? "");
|
|
@@ -696,6 +949,114 @@ export function registerTeamsTool(opts: {
|
|
|
696
949
|
};
|
|
697
950
|
}
|
|
698
951
|
|
|
952
|
+
if (action === "model_policy_get") {
|
|
953
|
+
const leaderProvider = ctx.model?.provider;
|
|
954
|
+
const leaderModelId = ctx.model?.id;
|
|
955
|
+
const leaderModel = formatProviderModel(leaderProvider, leaderModelId);
|
|
956
|
+
const leaderModelDeprecated = leaderModelId ? isDeprecatedTeammateModelId(leaderModelId) : false;
|
|
957
|
+
const resolved = resolveTeammateModelSelection({
|
|
958
|
+
leaderProvider,
|
|
959
|
+
leaderModelId,
|
|
960
|
+
});
|
|
961
|
+
if (!resolved.ok) {
|
|
962
|
+
return {
|
|
963
|
+
content: [{ type: "text", text: `Model policy resolution failed: ${resolved.error}` }],
|
|
964
|
+
details: {
|
|
965
|
+
action,
|
|
966
|
+
teamId,
|
|
967
|
+
error: resolved.error,
|
|
968
|
+
reason: resolved.reason,
|
|
969
|
+
},
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const effectiveModel = formatProviderModel(resolved.value.provider, resolved.value.modelId);
|
|
974
|
+
const lines: string[] = [
|
|
975
|
+
"Model policy",
|
|
976
|
+
"deprecated model family: claude-sonnet-4* (except claude-sonnet-4-5 / claude-sonnet-4.5)",
|
|
977
|
+
`leader model: ${leaderModel ?? "(unknown)"}`,
|
|
978
|
+
`leader model deprecated: ${leaderModelDeprecated ? "yes" : "no"}`,
|
|
979
|
+
`default teammate selection: source=${describeModelSource(resolved.value.source)}, model=${effectiveModel ?? "(teammate default)"}`,
|
|
980
|
+
"override forms: '<provider>/<modelId>' or '<modelId>' (inherits leader provider when available)",
|
|
981
|
+
];
|
|
982
|
+
|
|
983
|
+
return {
|
|
984
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
985
|
+
details: {
|
|
986
|
+
action,
|
|
987
|
+
teamId,
|
|
988
|
+
deprecatedPolicy: {
|
|
989
|
+
family: "claude-sonnet-4",
|
|
990
|
+
allowedExceptions: ["claude-sonnet-4-5", "claude-sonnet-4.5"],
|
|
991
|
+
},
|
|
992
|
+
leader: {
|
|
993
|
+
provider: leaderProvider,
|
|
994
|
+
modelId: leaderModelId,
|
|
995
|
+
model: leaderModel,
|
|
996
|
+
deprecated: leaderModelDeprecated,
|
|
997
|
+
},
|
|
998
|
+
defaultSelection: {
|
|
999
|
+
source: resolved.value.source,
|
|
1000
|
+
provider: resolved.value.provider,
|
|
1001
|
+
modelId: resolved.value.modelId,
|
|
1002
|
+
model: effectiveModel,
|
|
1003
|
+
warnings: resolved.value.warnings,
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (action === "model_policy_check") {
|
|
1010
|
+
const modelInput = params.model?.trim();
|
|
1011
|
+
const resolved = resolveTeammateModelSelection({
|
|
1012
|
+
modelOverride: modelInput,
|
|
1013
|
+
leaderProvider: ctx.model?.provider,
|
|
1014
|
+
leaderModelId: ctx.model?.id,
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
if (!resolved.ok) {
|
|
1018
|
+
const lines = [
|
|
1019
|
+
"Model policy check: rejected",
|
|
1020
|
+
`input: ${modelInput ?? "(none)"}`,
|
|
1021
|
+
`reason: ${resolved.error}`,
|
|
1022
|
+
];
|
|
1023
|
+
return {
|
|
1024
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1025
|
+
details: {
|
|
1026
|
+
action,
|
|
1027
|
+
teamId,
|
|
1028
|
+
accepted: false,
|
|
1029
|
+
input: modelInput,
|
|
1030
|
+
error: resolved.error,
|
|
1031
|
+
reason: resolved.reason,
|
|
1032
|
+
},
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const resolvedModel = formatProviderModel(resolved.value.provider, resolved.value.modelId);
|
|
1037
|
+
const lines = [
|
|
1038
|
+
"Model policy check: accepted",
|
|
1039
|
+
`input: ${modelInput ?? "(none)"}`,
|
|
1040
|
+
`source: ${describeModelSource(resolved.value.source)}`,
|
|
1041
|
+
`resolved model: ${resolvedModel ?? "(teammate default)"}`,
|
|
1042
|
+
];
|
|
1043
|
+
for (const warning of resolved.value.warnings) lines.push(`warning: ${warning}`);
|
|
1044
|
+
return {
|
|
1045
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1046
|
+
details: {
|
|
1047
|
+
action,
|
|
1048
|
+
teamId,
|
|
1049
|
+
accepted: true,
|
|
1050
|
+
input: modelInput,
|
|
1051
|
+
source: resolved.value.source,
|
|
1052
|
+
provider: resolved.value.provider,
|
|
1053
|
+
modelId: resolved.value.modelId,
|
|
1054
|
+
model: resolvedModel,
|
|
1055
|
+
warnings: resolved.value.warnings,
|
|
1056
|
+
},
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
699
1060
|
if (action === "hooks_policy_get") {
|
|
700
1061
|
const configuredFailureAction: TeamsHookFailureAction | undefined = cfg.hooks?.failureAction;
|
|
701
1062
|
const configuredFollowupOwner: TeamsHookFollowupOwnerPolicy | undefined = cfg.hooks?.followupOwner;
|
|
@@ -869,6 +1230,9 @@ export function registerTeamsTool(opts: {
|
|
|
869
1230
|
warnings.push(...res.warnings);
|
|
870
1231
|
}
|
|
871
1232
|
|
|
1233
|
+
// Two-pass delegation: create all tasks first, then notify workers.
|
|
1234
|
+
// This ensures DelegationTracker has the batch registered before any
|
|
1235
|
+
// worker can complete a task and emit an idle_notification.
|
|
872
1236
|
const assignments: Array<{ taskId: string; assignee: string; subject: string }> = [];
|
|
873
1237
|
let rr = 0;
|
|
874
1238
|
for (const t of inputTasks) {
|
|
@@ -909,13 +1273,23 @@ export function registerTeamsTool(opts: {
|
|
|
909
1273
|
const subject = firstLine.slice(0, 120);
|
|
910
1274
|
const task = await createTask(teamDir, effectiveTlId, { subject, description, owner: assignee });
|
|
911
1275
|
|
|
912
|
-
|
|
1276
|
+
assignments.push({ taskId: task.id, assignee, subject });
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Register batch BEFORE notifying workers so completions are never missed.
|
|
1280
|
+
if (delegationTracker && assignments.length > 0) {
|
|
1281
|
+
delegationTracker.addBatch(assignments.map((a) => a.taskId));
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Now notify workers via mailbox.
|
|
1285
|
+
for (const a of assignments) {
|
|
1286
|
+
const task = await getTask(teamDir, effectiveTlId, a.taskId);
|
|
1287
|
+
if (!task) continue;
|
|
1288
|
+
await writeToMailbox(teamDir, effectiveTlId, a.assignee, {
|
|
913
1289
|
from: cfg.leadName,
|
|
914
1290
|
text: JSON.stringify(taskAssignmentPayload(task, cfg.leadName)),
|
|
915
1291
|
timestamp: new Date().toISOString(),
|
|
916
1292
|
});
|
|
917
|
-
|
|
918
|
-
assignments.push({ taskId: task.id, assignee, subject });
|
|
919
1293
|
}
|
|
920
1294
|
|
|
921
1295
|
void refreshTasks().finally(renderWidget);
|
|
@@ -923,6 +1297,8 @@ export function registerTeamsTool(opts: {
|
|
|
923
1297
|
const lines: string[] = [];
|
|
924
1298
|
if (spawned.length) {
|
|
925
1299
|
lines.push(`Spawned: ${spawned.map((n) => formatMemberDisplayName(style, n)).join(", ")}`);
|
|
1300
|
+
if (spawnModel) lines.push(`model: ${spawnModel}`);
|
|
1301
|
+
if (spawnThinking) lines.push(`thinking: ${spawnThinking}`);
|
|
926
1302
|
}
|
|
927
1303
|
lines.push(`Delegated ${assignments.length} task(s):`);
|
|
928
1304
|
for (const a of assignments) {
|