@tmustier/pi-agent-teams 0.2.0 → 0.3.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.
@@ -9,36 +9,16 @@ import { TEAM_MAILBOX_NS } from "./protocol.js";
9
9
  import { listTasks, unassignTasksForAgent, type TeamTask } from "./task-store.js";
10
10
  import { TeammateRpc } from "./teammate-rpc.js";
11
11
  import { ensureTeamConfig, loadTeamConfig, setMemberStatus, upsertMember, type TeamConfig } from "./team-config.js";
12
- import { getTeamDir, getTeamsRootDir } from "./paths.js";
12
+ import { getTeamDir } from "./paths.js";
13
13
  import { ensureWorktreeCwd } from "./worktree.js";
14
14
  import { ActivityTracker, TranscriptTracker } from "./activity-tracker.js";
15
15
  import { openInteractiveWidget } from "./teams-panel.js";
16
16
  import { createTeamsWidget } from "./teams-widget.js";
17
17
  import { getTeamsStyleFromEnv, type TeamsStyle, formatMemberDisplayName, getTeamsStrings } from "./teams-style.js";
18
18
  import { pollLeaderInbox as pollLeaderInboxImpl } from "./leader-inbox.js";
19
- import { handleTeamTaskCommand } from "./leader-task-commands.js";
20
- import { handleTeamPlanCommand } from "./leader-plan-commands.js";
21
- import { handleTeamSpawnCommand } from "./leader-spawn-command.js";
19
+ import { handleTeamCommand } from "./leader-team-command.js";
22
20
  import { registerTeamsTool } from "./leader-teams-tool.js";
23
- import {
24
- handleTeamBroadcastCommand,
25
- handleTeamDmCommand,
26
- handleTeamSendCommand,
27
- handleTeamSteerCommand,
28
- } from "./leader-messaging-commands.js";
29
- import { handleTeamEnvCommand, handleTeamIdCommand, handleTeamListCommand } from "./leader-info-commands.js";
30
- import {
31
- handleTeamCleanupCommand,
32
- handleTeamDelegateCommand,
33
- handleTeamKillCommand,
34
- handleTeamShutdownCommand,
35
- handleTeamStopCommand,
36
- handleTeamStyleCommand,
37
- } from "./leader-lifecycle-commands.js";
38
-
39
-
40
- type ContextMode = "fresh" | "branch";
41
- type WorkspaceMode = "shared" | "worktree";
21
+ import type { ContextMode, SpawnTeammateFn, SpawnTeammateResult, WorkspaceMode } from "./spawn-types.js";
42
22
 
43
23
  function getTeamsExtensionEntryPath(): string | null {
44
24
  // In dev, teammates won't automatically have this extension unless it is installed or discoverable.
@@ -59,15 +39,6 @@ function shellQuote(v: string): string {
59
39
  return "'" + v.replace(/'/g, `"'"'"'`) + "'";
60
40
  }
61
41
 
62
- function parseAssigneePrefix(text: string): { assignee?: string; text: string } {
63
- const m = text.match(/^([a-zA-Z0-9_-]+):\s*(.+)$/);
64
- if (!m) return { text };
65
- const assignee = m[1];
66
- const rest = m[2];
67
- if (!assignee || !rest) return { text };
68
- return { assignee, text: rest };
69
- }
70
-
71
42
  function getTeamSessionsDir(teamDir: string): string {
72
43
  return path.join(teamDir, "sessions");
73
44
  }
@@ -121,17 +92,6 @@ async function createSessionForTeammate(
121
92
  }
122
93
  }
123
94
 
124
- function taskAssignmentPayload(task: TeamTask, assignedBy: string) {
125
- return {
126
- type: "task_assignment",
127
- taskId: task.id,
128
- subject: task.subject,
129
- description: task.description,
130
- assignedBy,
131
- timestamp: new Date().toISOString(),
132
- };
133
- }
134
-
135
95
  // Message parsers are shared with the worker implementation.
136
96
  export function runLeader(pi: ExtensionAPI): void {
137
97
  const teammates = new Map<string, TeammateRpc>();
@@ -226,22 +186,7 @@ export function runLeader(pi: ExtensionAPI): void {
226
186
  currentCtx.ui.setWidget("pi-teams", widgetFactory);
227
187
  };
228
188
 
229
- type SpawnTeammateResult =
230
- | {
231
- ok: true;
232
- name: string;
233
- mode: ContextMode;
234
- workspaceMode: WorkspaceMode;
235
- childCwd: string;
236
- note?: string;
237
- warnings: string[];
238
- }
239
- | { ok: false; error: string };
240
-
241
- const spawnTeammate = async (
242
- ctx: ExtensionContext,
243
- opts: { name: string; mode?: ContextMode; workspaceMode?: WorkspaceMode; planRequired?: boolean },
244
- ): Promise<SpawnTeammateResult> => {
189
+ const spawnTeammate: SpawnTeammateFn = async (ctx, opts): Promise<SpawnTeammateResult> => {
245
190
  const warnings: string[] = [];
246
191
  const mode: ContextMode = opts.mode ?? "fresh";
247
192
  let workspaceMode: WorkspaceMode = opts.workspaceMode ?? "shared";
@@ -516,7 +461,6 @@ export function runLeader(pi: ExtensionAPI): void {
516
461
  teammates,
517
462
  spawnTeammate,
518
463
  getTaskListId: () => taskListId,
519
- taskAssignmentPayload,
520
464
  refreshTasks,
521
465
  renderWidget,
522
466
  });
@@ -550,11 +494,11 @@ export function runLeader(pi: ExtensionAPI): void {
550
494
  timestamp: new Date().toISOString(),
551
495
  });
552
496
  },
553
- abortComrade(name: string) {
497
+ abortMember(name: string) {
554
498
  const rpc = teammates.get(name);
555
499
  if (rpc) void rpc.abort();
556
500
  },
557
- killComrade(name: string) {
501
+ killMember(name: string) {
558
502
  const rpc = teammates.get(name);
559
503
  if (!rpc) return;
560
504
 
@@ -613,254 +557,34 @@ export function runLeader(pi: ExtensionAPI): void {
613
557
  currentCtx = ctx;
614
558
  currentTeamId = ctx.sessionManager.getSessionId();
615
559
 
616
- const [sub, ...rest] = args.trim().split(" ");
617
- if (!sub || sub === "help") {
618
- ctx.ui.notify(
619
- [
620
- "Usage:",
621
- " /team id",
622
- " /team env <name>",
623
- " /team spawn <name> [fresh|branch] [shared|worktree] [plan]",
624
- " /team panel",
625
- " /team send <name> <msg...>",
626
- " /team dm <name> <msg...>",
627
- " /team broadcast <msg...>",
628
- " /team steer <name> <msg...>",
629
- " /team stop <name> [reason...]",
630
- " /team kill <name>",
631
- " /team shutdown",
632
- " /team shutdown <name> [reason...]",
633
- " /team delegate [on|off]",
634
- " /team plan approve <name>",
635
- " /team plan reject <name> [feedback...]",
636
- " /team cleanup [--force]",
637
- " /team task add <text...>",
638
- " /team task assign <id> <agent>",
639
- " /team task unassign <id>",
640
- " /team task list",
641
- " /team task clear [completed|all] [--force]",
642
- " /team task show <id>",
643
- " /team task dep add <id> <depId>",
644
- " /team task dep rm <id> <depId>",
645
- " /team task dep ls <id>",
646
- " /team task use <taskListId>",
647
- ].join("\n"),
648
- "info",
649
- );
650
- return;
651
- }
652
-
653
- switch (sub) {
654
- case "list": {
655
- await handleTeamListCommand({
656
- ctx,
657
- teammates,
658
- getTeamConfig: () => teamConfig,
659
- style,
660
- refreshTasks,
661
- renderWidget,
662
- });
663
- return;
664
- }
665
-
666
- case "id": {
667
- await handleTeamIdCommand({
668
- ctx,
669
- taskListId,
670
- leadName: teamConfig?.leadName ?? "team-lead",
671
- style,
672
- });
673
- return;
674
- }
675
-
676
- case "env": {
677
- await handleTeamEnvCommand({
678
- ctx,
679
- rest,
680
- taskListId,
681
- leadName: teamConfig?.leadName ?? "team-lead",
682
- style,
683
- getTeamsExtensionEntryPath,
684
- shellQuote,
685
- });
686
- return;
687
- }
688
-
689
- case "cleanup": {
690
- await handleTeamCleanupCommand({
691
- ctx,
692
- rest,
693
- teammates,
694
- refreshTasks,
695
- getTasks: () => tasks,
696
- renderWidget,
697
- style,
698
- });
699
- return;
700
- }
701
-
702
- case "delegate": {
703
- await handleTeamDelegateCommand({
704
- ctx,
705
- rest,
706
- getDelegateMode: () => delegateMode,
707
- setDelegateMode: (next) => {
708
- delegateMode = next;
709
- },
710
- renderWidget,
711
- });
712
- return;
713
- }
714
-
715
- case "shutdown": {
716
- await handleTeamShutdownCommand({
717
- ctx,
718
- rest,
719
- teammates,
720
- leadName: teamConfig?.leadName ?? "team-lead",
721
- style,
722
- getCurrentCtx: () => currentCtx,
723
- });
724
- return;
725
- }
726
-
727
- case "spawn": {
728
- await handleTeamSpawnCommand({ ctx, rest, teammates, style, spawnTeammate });
729
- return;
730
- }
731
-
732
- case "style": {
733
- const teamId = ctx.sessionManager.getSessionId();
734
- const teamDir = getTeamDir(teamId);
735
- await handleTeamStyleCommand({
736
- ctx,
737
- rest,
738
- teamDir,
739
- getStyle: () => style,
740
- setStyle: (next) => {
741
- style = next;
742
- },
743
- refreshTasks,
744
- renderWidget,
745
- });
746
- return;
747
- }
748
-
749
- case "panel":
750
- case "widget": {
751
- await openWidget(ctx);
752
- return;
753
- }
754
-
755
- case "send": {
756
- await handleTeamSendCommand({
757
- ctx,
758
- rest,
759
- teammates,
760
- style,
761
- renderWidget,
762
- });
763
- return;
764
- }
765
-
766
- case "steer": {
767
- await handleTeamSteerCommand({
768
- ctx,
769
- rest,
770
- teammates,
771
- style,
772
- renderWidget,
773
- });
774
- return;
775
- }
776
-
777
- case "stop": {
778
- await handleTeamStopCommand({
779
- ctx,
780
- rest,
781
- teammates,
782
- leadName: teamConfig?.leadName ?? "team-lead",
783
- style,
784
- refreshTasks,
785
- getTasks: () => tasks,
786
- renderWidget,
787
- });
788
- return;
789
- }
790
-
791
- case "kill": {
792
- await handleTeamKillCommand({
793
- ctx,
794
- rest,
795
- teammates,
796
- leadName: teamConfig?.leadName ?? "team-lead",
797
- style,
798
- taskListId,
799
- refreshTasks,
800
- renderWidget,
801
- });
802
- return;
803
- }
804
-
805
- case "dm": {
806
- await handleTeamDmCommand({
807
- ctx,
808
- rest,
809
- leadName: teamConfig?.leadName ?? "team-lead",
810
- style,
811
- });
812
- return;
813
- }
814
-
815
- case "broadcast": {
816
- await handleTeamBroadcastCommand({
817
- ctx,
818
- rest,
819
- teammates,
820
- leadName: teamConfig?.leadName ?? "team-lead",
821
- style,
822
- refreshTasks,
823
- getTasks: () => tasks,
824
- getTaskListId: () => taskListId,
825
- });
826
- return;
827
- }
828
-
829
- case "task": {
830
- await handleTeamTaskCommand({
831
- ctx,
832
- rest,
833
- leadName: teamConfig?.leadName ?? "team-lead",
834
- style,
835
- getTaskListId: () => taskListId,
836
- setTaskListId: (id) => {
837
- taskListId = id;
838
- },
839
- getTasks: () => tasks,
840
- refreshTasks,
841
- renderWidget,
842
- parseAssigneePrefix,
843
- taskAssignmentPayload,
844
- });
845
- return;
846
- }
847
-
848
- case "plan": {
849
- await handleTeamPlanCommand({
850
- ctx,
851
- rest,
852
- leadName: teamConfig?.leadName ?? "team-lead",
853
- style,
854
- pendingPlanApprovals,
855
- });
856
- return;
857
- }
858
-
859
- default: {
860
- ctx.ui.notify(`Unknown subcommand: ${sub}`, "error");
861
- return;
862
- }
863
- }
560
+ await handleTeamCommand({
561
+ args,
562
+ ctx,
563
+ teammates,
564
+ getTeamConfig: () => teamConfig,
565
+ getTasks: () => tasks,
566
+ refreshTasks,
567
+ renderWidget,
568
+ getTaskListId: () => taskListId,
569
+ setTaskListId: (id) => {
570
+ taskListId = id;
571
+ },
572
+ pendingPlanApprovals,
573
+ getDelegateMode: () => delegateMode,
574
+ setDelegateMode: (next) => {
575
+ delegateMode = next;
576
+ },
577
+ getStyle: () => style,
578
+ setStyle: (next) => {
579
+ style = next;
580
+ },
581
+ spawnTeammate,
582
+ openWidget,
583
+ getTeamsExtensionEntryPath,
584
+ shellQuote,
585
+ getCurrentCtx: () => currentCtx,
586
+ stopAllTeammates,
587
+ });
864
588
  },
865
589
  });
866
590
  }
@@ -1,5 +1,25 @@
1
+ import type { TeamTask } from "./task-store.js";
2
+
1
3
  export const TEAM_MAILBOX_NS = "team";
2
4
 
5
+ export function taskAssignmentPayload(task: TeamTask, assignedBy: string): {
6
+ type: "task_assignment";
7
+ taskId: string;
8
+ subject: string;
9
+ description: string;
10
+ assignedBy: string;
11
+ timestamp: string;
12
+ } {
13
+ return {
14
+ type: "task_assignment",
15
+ taskId: task.id,
16
+ subject: task.subject,
17
+ description: task.description,
18
+ assignedBy,
19
+ timestamp: new Date().toISOString(),
20
+ };
21
+ }
22
+
3
23
  function safeParseJson(text: string): unknown | null {
4
24
  try {
5
25
  const parsed: unknown = JSON.parse(text);
@@ -0,0 +1,21 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+
3
+ export type ContextMode = "fresh" | "branch";
4
+ export type WorkspaceMode = "shared" | "worktree";
5
+
6
+ export type SpawnTeammateResult =
7
+ | {
8
+ ok: true;
9
+ name: string;
10
+ mode: ContextMode;
11
+ workspaceMode: WorkspaceMode;
12
+ childCwd?: string;
13
+ note?: string;
14
+ warnings: string[];
15
+ }
16
+ | { ok: false; error: string };
17
+
18
+ export type SpawnTeammateFn = (
19
+ ctx: ExtensionContext,
20
+ opts: { name: string; mode?: ContextMode; workspaceMode?: WorkspaceMode; planRequired?: boolean },
21
+ ) => Promise<SpawnTeammateResult>;
@@ -328,6 +328,10 @@ export async function unassignTasksForAgent(
328
328
  if (t.owner !== agentName) continue;
329
329
  if (t.status === "completed") continue;
330
330
  const updated = await updateTask(teamDir, taskListId, t.id, (cur) => {
331
+ // Re-check ownership under the per-task lock to avoid races with other claimers.
332
+ if (cur.owner !== agentName) return cur;
333
+ if (cur.status === "completed") return cur;
334
+
331
335
  const metadata = { ...(cur.metadata ?? {}) };
332
336
  if (reason) metadata.unassignedReason = reason;
333
337
  metadata.unassignedAt = new Date().toISOString();
@@ -4,7 +4,7 @@ import type { AgentEvent } from "@mariozechner/pi-agent-core";
4
4
  export type TeammateStatus = "starting" | "idle" | "streaming" | "stopped" | "error";
5
5
 
6
6
  type RpcCommand =
7
- | { id: string; type: "prompt"; message: string; streamingBehavior?: "steer" | "followUp" }
7
+ | { id: string; type: "prompt"; message: string }
8
8
  | { id: string; type: "steer"; message: string }
9
9
  | { id: string; type: "follow_up"; message: string }
10
10
  | { id: string; type: "abort" }
@@ -48,7 +48,31 @@ function isRpcResponse(v: unknown): v is RpcResponse {
48
48
 
49
49
  function isAgentEvent(v: unknown): v is AgentEvent {
50
50
  if (!isRecord(v)) return false;
51
- return typeof v.type === "string";
51
+ if (typeof v.type !== "string") return false;
52
+
53
+ // Validate the minimal shapes we actually dereference below.
54
+ if (v.type === "message_update") {
55
+ const ame = v.assistantMessageEvent;
56
+ if (!isRecord(ame)) return false;
57
+ if (typeof ame.type !== "string") return false;
58
+ if (ame.type === "text_delta" && typeof ame.delta !== "string") return false;
59
+ return true;
60
+ }
61
+
62
+ if (v.type === "tool_execution_start" || v.type === "tool_execution_update" || v.type === "tool_execution_end") {
63
+ if (typeof v.toolCallId !== "string") return false;
64
+ if (typeof v.toolName !== "string") return false;
65
+ return true;
66
+ }
67
+
68
+ return (
69
+ v.type === "agent_start" ||
70
+ v.type === "agent_end" ||
71
+ v.type === "turn_start" ||
72
+ v.type === "turn_end" ||
73
+ v.type === "message_start" ||
74
+ v.type === "message_end"
75
+ );
52
76
  }
53
77
 
54
78
  export class TeammateRpc {