@tmustier/pi-agent-teams 0.1.2 → 0.2.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.
Files changed (35) hide show
  1. package/.github/workflows/ci.yml +32 -0
  2. package/README.md +36 -6
  3. package/docs/claude-parity.md +16 -14
  4. package/docs/field-notes-teams-setup.md +6 -4
  5. package/docs/smoke-test-plan.md +130 -0
  6. package/extensions/teams/activity-tracker.ts +234 -0
  7. package/extensions/teams/fs-lock.ts +21 -5
  8. package/extensions/teams/leader-inbox.ts +175 -0
  9. package/extensions/teams/leader-info-commands.ts +139 -0
  10. package/extensions/teams/leader-lifecycle-commands.ts +330 -0
  11. package/extensions/teams/leader-messaging-commands.ts +149 -0
  12. package/extensions/teams/leader-plan-commands.ts +96 -0
  13. package/extensions/teams/leader-spawn-command.ts +73 -0
  14. package/extensions/teams/leader-task-commands.ts +417 -0
  15. package/extensions/teams/leader-teams-tool.ts +238 -0
  16. package/extensions/teams/leader.ts +396 -1422
  17. package/extensions/teams/mailbox.ts +54 -29
  18. package/extensions/teams/names.ts +87 -0
  19. package/extensions/teams/protocol.ts +221 -0
  20. package/extensions/teams/task-store.ts +32 -21
  21. package/extensions/teams/team-config.ts +71 -25
  22. package/extensions/teams/teammate-rpc.ts +56 -22
  23. package/extensions/teams/teams-panel.ts +698 -0
  24. package/extensions/teams/teams-style.ts +62 -0
  25. package/extensions/teams/teams-widget.ts +235 -0
  26. package/extensions/teams/worker.ts +100 -138
  27. package/extensions/teams/worktree.ts +4 -7
  28. package/package.json +25 -3
  29. package/scripts/integration-claim-test.mts +227 -0
  30. package/scripts/integration-todo-test.mts +583 -0
  31. package/scripts/smoke-test.mjs +1 -1
  32. package/scripts/smoke-test.mts +424 -0
  33. package/skills/agent-teams/SKILL.md +136 -0
  34. package/tsconfig.strict.json +22 -0
  35. package/extensions/teams/tasks.ts +0 -95
@@ -0,0 +1,149 @@
1
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import { writeToMailbox } from "./mailbox.js";
3
+ import { sanitizeName } from "./names.js";
4
+ import { getTeamDir } from "./paths.js";
5
+ import { TEAM_MAILBOX_NS } from "./protocol.js";
6
+ import { ensureTeamConfig } from "./team-config.js";
7
+ import type { TeamTask } from "./task-store.js";
8
+ import type { TeammateRpc } from "./teammate-rpc.js";
9
+ import type { TeamsStyle } from "./teams-style.js";
10
+ import { formatMemberDisplayName, getTeamsStrings } from "./teams-style.js";
11
+
12
+ export async function handleTeamSendCommand(opts: {
13
+ ctx: ExtensionCommandContext;
14
+ rest: string[];
15
+ teammates: Map<string, TeammateRpc>;
16
+ style: TeamsStyle;
17
+ renderWidget: () => void;
18
+ }): Promise<void> {
19
+ const { ctx, rest, teammates, style, renderWidget } = opts;
20
+ const strings = getTeamsStrings(style);
21
+
22
+ const nameRaw = rest[0];
23
+ const msg = rest.slice(1).join(" ").trim();
24
+ if (!nameRaw || !msg) {
25
+ ctx.ui.notify("Usage: /team send <name> <msg...>", "error");
26
+ return;
27
+ }
28
+ const name = sanitizeName(nameRaw);
29
+ const t = teammates.get(name);
30
+ if (!t) {
31
+ ctx.ui.notify(`Unknown ${strings.memberTitle.toLowerCase()}: ${name}`, "error");
32
+ return;
33
+ }
34
+ if (t.status === "streaming") await t.followUp(msg);
35
+ else await t.prompt(msg);
36
+ ctx.ui.notify(`Sent to ${name}`, "info");
37
+ renderWidget();
38
+ }
39
+
40
+ export async function handleTeamSteerCommand(opts: {
41
+ ctx: ExtensionCommandContext;
42
+ rest: string[];
43
+ teammates: Map<string, TeammateRpc>;
44
+ style: TeamsStyle;
45
+ renderWidget: () => void;
46
+ }): Promise<void> {
47
+ const { ctx, rest, teammates, style, renderWidget } = opts;
48
+ const strings = getTeamsStrings(style);
49
+
50
+ const nameRaw = rest[0];
51
+ const msg = rest.slice(1).join(" ").trim();
52
+ if (!nameRaw || !msg) {
53
+ ctx.ui.notify("Usage: /team steer <name> <msg...>", "error");
54
+ return;
55
+ }
56
+ const name = sanitizeName(nameRaw);
57
+ const t = teammates.get(name);
58
+ if (!t) {
59
+ ctx.ui.notify(`Unknown ${strings.memberTitle.toLowerCase()}: ${name}`, "error");
60
+ return;
61
+ }
62
+ await t.steer(msg);
63
+ ctx.ui.notify(`Steering sent to ${name}`, "info");
64
+ renderWidget();
65
+ }
66
+
67
+ export async function handleTeamDmCommand(opts: {
68
+ ctx: ExtensionCommandContext;
69
+ rest: string[];
70
+ leadName: string;
71
+ style: TeamsStyle;
72
+ }): Promise<void> {
73
+ const { ctx, rest, leadName, style } = opts;
74
+ const strings = getTeamsStrings(style);
75
+
76
+ const nameRaw = rest[0];
77
+ const msg = rest.slice(1).join(" ").trim();
78
+ if (!nameRaw || !msg) {
79
+ ctx.ui.notify("Usage: /team dm <name> <msg...>", "error");
80
+ return;
81
+ }
82
+ const name = sanitizeName(nameRaw);
83
+ const teamId = ctx.sessionManager.getSessionId();
84
+ await writeToMailbox(getTeamDir(teamId), TEAM_MAILBOX_NS, name, {
85
+ from: leadName,
86
+ text: msg,
87
+ timestamp: new Date().toISOString(),
88
+ });
89
+ ctx.ui.notify(`DM queued for ${formatMemberDisplayName(style, name)}`, "info");
90
+ }
91
+
92
+ export async function handleTeamBroadcastCommand(opts: {
93
+ ctx: ExtensionCommandContext;
94
+ rest: string[];
95
+ teammates: Map<string, TeammateRpc>;
96
+ leadName: string;
97
+ style: TeamsStyle;
98
+ refreshTasks: () => Promise<void>;
99
+ getTasks: () => TeamTask[];
100
+ getTaskListId: () => string | null;
101
+ }): Promise<void> {
102
+ const { ctx, rest, teammates, leadName, style, refreshTasks, getTasks, getTaskListId } = opts;
103
+ const strings = getTeamsStrings(style);
104
+
105
+ const msg = rest.join(" ").trim();
106
+ if (!msg) {
107
+ ctx.ui.notify("Usage: /team broadcast <msg...>", "error");
108
+ return;
109
+ }
110
+
111
+ const teamId = ctx.sessionManager.getSessionId();
112
+ const teamDir = getTeamDir(teamId);
113
+ const taskListId = getTaskListId();
114
+ const cfg = await ensureTeamConfig(teamDir, { teamId, taskListId: taskListId ?? teamId, leadName, style });
115
+
116
+ const recipients = new Set<string>();
117
+ for (const m of cfg.members) {
118
+ if (m.role === "worker") recipients.add(m.name);
119
+ }
120
+ for (const name of teammates.keys()) recipients.add(name);
121
+
122
+ // Include task owners (helps reach manual tmux workers not tracked as RPC teammates).
123
+ await refreshTasks();
124
+ for (const t of getTasks()) {
125
+ if (t.owner && t.owner !== leadName) recipients.add(t.owner);
126
+ }
127
+
128
+ const names = Array.from(recipients).sort();
129
+ if (names.length === 0) {
130
+ ctx.ui.notify(`No ${strings.memberTitle.toLowerCase()}s to broadcast to`, "warning");
131
+ return;
132
+ }
133
+
134
+ const ts = new Date().toISOString();
135
+ await Promise.all(
136
+ names.map((name) =>
137
+ writeToMailbox(teamDir, TEAM_MAILBOX_NS, name, {
138
+ from: leadName,
139
+ text: msg,
140
+ timestamp: ts,
141
+ }),
142
+ ),
143
+ );
144
+
145
+ ctx.ui.notify(
146
+ `Broadcast queued for ${names.length} ${strings.memberTitle.toLowerCase()}(s): ${names.map((n) => formatMemberDisplayName(style, n)).join(", ")}`,
147
+ "info",
148
+ );
149
+ }
@@ -0,0 +1,96 @@
1
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import { writeToMailbox } from "./mailbox.js";
3
+ import { sanitizeName } from "./names.js";
4
+ import { getTeamDir } from "./paths.js";
5
+ import { TEAM_MAILBOX_NS } from "./protocol.js";
6
+ import type { TeamsStyle } from "./teams-style.js";
7
+ import { formatMemberDisplayName, getTeamsStrings } from "./teams-style.js";
8
+
9
+ export async function handleTeamPlanCommand(opts: {
10
+ ctx: ExtensionCommandContext;
11
+ rest: string[];
12
+ leadName: string;
13
+ style: TeamsStyle;
14
+ pendingPlanApprovals: Map<string, { requestId: string; name: string; taskId?: string }>;
15
+ }): Promise<void> {
16
+ const { ctx, rest, leadName, style, pendingPlanApprovals } = opts;
17
+
18
+ const [planSub, ...planRest] = rest;
19
+ if (!planSub || planSub === "help") {
20
+ ctx.ui.notify(
21
+ [
22
+ "Usage:",
23
+ " /team plan approve <name>",
24
+ " /team plan reject <name> [feedback...]",
25
+ ].join("\n"),
26
+ "info",
27
+ );
28
+ return;
29
+ }
30
+
31
+ if (planSub === "approve") {
32
+ const nameRaw = planRest[0];
33
+ if (!nameRaw) {
34
+ ctx.ui.notify("Usage: /team plan approve <name>", "error");
35
+ return;
36
+ }
37
+ const name = sanitizeName(nameRaw);
38
+ const pending = pendingPlanApprovals.get(name);
39
+ if (!pending) {
40
+ ctx.ui.notify(`No pending plan approval for ${name}`, "error");
41
+ return;
42
+ }
43
+
44
+ const teamId = ctx.sessionManager.getSessionId();
45
+ const teamDir = getTeamDir(teamId);
46
+ const ts = new Date().toISOString();
47
+ await writeToMailbox(teamDir, TEAM_MAILBOX_NS, name, {
48
+ from: leadName,
49
+ text: JSON.stringify({
50
+ type: "plan_approved",
51
+ requestId: pending.requestId,
52
+ from: leadName,
53
+ timestamp: ts,
54
+ }),
55
+ timestamp: ts,
56
+ });
57
+ pendingPlanApprovals.delete(name);
58
+ ctx.ui.notify(`Approved plan for ${formatMemberDisplayName(style, name)}`, "info");
59
+ return;
60
+ }
61
+
62
+ if (planSub === "reject") {
63
+ const nameRaw = planRest[0];
64
+ if (!nameRaw) {
65
+ ctx.ui.notify("Usage: /team plan reject <name> [feedback...]", "error");
66
+ return;
67
+ }
68
+ const name = sanitizeName(nameRaw);
69
+ const pending = pendingPlanApprovals.get(name);
70
+ if (!pending) {
71
+ ctx.ui.notify(`No pending plan approval for ${name}`, "error");
72
+ return;
73
+ }
74
+
75
+ const feedback = planRest.slice(1).join(" ").trim() || "Plan rejected";
76
+ const teamId = ctx.sessionManager.getSessionId();
77
+ const teamDir = getTeamDir(teamId);
78
+ const ts = new Date().toISOString();
79
+ await writeToMailbox(teamDir, TEAM_MAILBOX_NS, name, {
80
+ from: leadName,
81
+ text: JSON.stringify({
82
+ type: "plan_rejected",
83
+ requestId: pending.requestId,
84
+ from: leadName,
85
+ feedback,
86
+ timestamp: ts,
87
+ }),
88
+ timestamp: ts,
89
+ });
90
+ pendingPlanApprovals.delete(name);
91
+ ctx.ui.notify(`Rejected plan for ${formatMemberDisplayName(style, name)}: ${feedback}`, "info");
92
+ return;
93
+ }
94
+
95
+ ctx.ui.notify(`Unknown plan subcommand: ${planSub}`, "error");
96
+ }
@@ -0,0 +1,73 @@
1
+ import type { ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import { pickComradeNames } from "./names.js";
3
+ import type { TeammateRpc } from "./teammate-rpc.js";
4
+ import type { TeamsStyle } from "./teams-style.js";
5
+ import { formatMemberDisplayName, getTeamsStrings, isSovietStyle } from "./teams-style.js";
6
+
7
+ export type ContextMode = "fresh" | "branch";
8
+ export type WorkspaceMode = "shared" | "worktree";
9
+
10
+ export type SpawnTeammateResult =
11
+ | {
12
+ ok: true;
13
+ name: string;
14
+ mode: ContextMode;
15
+ workspaceMode: WorkspaceMode;
16
+ note?: string;
17
+ warnings: string[];
18
+ }
19
+ | { ok: false; error: string };
20
+
21
+ export async function handleTeamSpawnCommand(opts: {
22
+ ctx: ExtensionCommandContext;
23
+ rest: string[];
24
+ teammates: Map<string, TeammateRpc>;
25
+ style: TeamsStyle;
26
+ spawnTeammate: (
27
+ ctx: ExtensionContext,
28
+ opts: { name: string; mode?: ContextMode; workspaceMode?: WorkspaceMode; planRequired?: boolean },
29
+ ) => Promise<SpawnTeammateResult>;
30
+ }): Promise<void> {
31
+ const { ctx, rest, teammates, style, spawnTeammate } = opts;
32
+ const strings = getTeamsStrings(style);
33
+
34
+ // Parse flags from any position
35
+ let nameRaw: string | undefined;
36
+ let mode: ContextMode = "fresh";
37
+ let workspaceMode: WorkspaceMode = "shared";
38
+ let planRequired = false;
39
+ for (const a of rest) {
40
+ if (a === "fresh" || a === "branch") mode = a;
41
+ else if (a === "shared" || a === "worktree") workspaceMode = a;
42
+ else if (a === "plan") planRequired = true;
43
+ else if (!nameRaw) nameRaw = a;
44
+ }
45
+
46
+ // Auto-pick a name only in soviet style.
47
+ if (!nameRaw) {
48
+ if (!isSovietStyle(style)) {
49
+ ctx.ui.notify("Usage: /team spawn <name> [fresh|branch] [shared|worktree] [plan]", "error");
50
+ return;
51
+ }
52
+ const taken = new Set(teammates.keys());
53
+ const picked = pickComradeNames(1, taken)[0];
54
+ if (!picked) {
55
+ ctx.ui.notify("Failed to pick a comrade name", "error");
56
+ return;
57
+ }
58
+ nameRaw = picked;
59
+ }
60
+
61
+ const res = await spawnTeammate(ctx, { name: nameRaw, mode, workspaceMode, planRequired });
62
+ if (!res.ok) {
63
+ ctx.ui.notify(res.error, "error");
64
+ return;
65
+ }
66
+
67
+ for (const w of res.warnings) ctx.ui.notify(w, "warning");
68
+ const displayName = formatMemberDisplayName(style, res.name);
69
+ ctx.ui.notify(
70
+ `${displayName} ${strings.joinedVerb} (${res.mode}${res.note ? ", " + res.note : ""} \u00b7 ${res.workspaceMode})`,
71
+ "info",
72
+ );
73
+ }