@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.
package/README.md CHANGED
@@ -79,6 +79,7 @@ Or drive it manually:
79
79
  /tw # open the interactive widget panel
80
80
 
81
81
  /team shutdown alice # graceful shutdown (handshake)
82
+ /team shutdown # stop all teammates (leader session remains active)
82
83
  /team cleanup # remove team artifacts when done
83
84
  ```
84
85
 
@@ -105,6 +106,7 @@ Or let the model drive it with the delegate tool:
105
106
  | --- | --- |
106
107
  | `/swarm [task]` | Tell the agent to spawn a team and work on a task |
107
108
  | `/tw` | Open the interactive widget panel |
109
+ | `/team-widget` | Open the interactive widget panel (alias for `/tw`) |
108
110
 
109
111
  ### Team management
110
112
 
@@ -122,7 +124,8 @@ All management commands live under `/team`.
122
124
  | `/team broadcast <msg>` | Message all teammates |
123
125
  | `/team stop <name> [reason]` | Abort current work (resets task to pending) |
124
126
  | `/team shutdown <name> [reason]` | Graceful shutdown (handshake) |
125
- | `/team shutdown` | Shutdown leader + all teammates |
127
+ | `/team shutdown` | Stop all teammates (RPC + best-effort manual) (leader session remains active) |
128
+ | `/team prune [--all]` | Mark stale manual teammates offline (hides them in widget) |
126
129
  | `/team kill <name>` | Force-terminate |
127
130
  | `/team cleanup [--force]` | Delete team artifacts |
128
131
  | `/team id` | Print team/task-list IDs and paths |
@@ -166,13 +169,22 @@ All management commands live under `/team`.
166
169
 
167
170
  ## Development
168
171
 
172
+ ### Quality gate
173
+
174
+ ```bash
175
+ npm run check
176
+ ```
177
+
178
+ Runs strict TypeScript typechecking (`npm run typecheck`) and ESLint (`npm run lint`).
179
+
169
180
  ### Smoke test (no API keys)
170
181
 
171
182
  ```bash
172
- node scripts/smoke-test.mjs
183
+ npm run smoke-test
184
+ # or: npx tsx scripts/smoke-test.mts
173
185
  ```
174
186
 
175
- Filesystem-level test of the task store, mailbox, and team config.
187
+ Filesystem-level smoke test of the task store, mailbox, team config, and protocol parsers.
176
188
 
177
189
  ### E2E RPC test (spawns pi + one teammate)
178
190
 
@@ -40,11 +40,11 @@ Legend: ✅ implemented • 🟡 partial • ❌ missing
40
40
  | Display modes | In-process selection (Shift+Up/Down); split panes (tmux/iTerm) | ❌ | Pi has a widget + commands, but no terminal-level comrade navigation/panes. | P2 |
41
41
  | Delegate mode | Lead restricted to coordination-only tools | ✅ | `/team delegate [on|off]` toggles; `pi.on("tool_call")` blocks `bash/edit/write`; `PI_TEAMS_DELEGATE_MODE=1` env. Widget shows `[delegate]`. | P1 |
42
42
  | Plan approval | Comrade can be "plan required" and needs lead approval to implement | ✅ | `/team spawn <name> plan` sets `PI_TEAMS_PLAN_REQUIRED=1`; worker restricted to read-only tools; submits plan via `plan_approval_request`; `/team plan approve|reject <name>`. | P1 |
43
- | Shutdown handshake | Lead requests shutdown; comrade can approve/reject | ✅ | Full protocol: `shutdown_request` → `shutdown_approved` or `shutdown_rejected` (when worker is busy). `/team shutdown <name>` (graceful) + `/team kill` (force). | P1 |
43
+ | Shutdown handshake | Lead requests shutdown; comrade can approve/reject | ✅ | Full protocol: `shutdown_request` → `shutdown_approved` or `shutdown_rejected` (when worker is busy). `/team shutdown <name>` (graceful) + `/team kill <name>` (force). | P1 |
44
44
  | Cleanup team | “Clean up the team” removes shared resources after comrades stopped | ✅ | `/team cleanup [--force]` deletes only `<teamsRoot>/<teamId>` after safety checks. | P1 |
45
45
  | Hooks / quality gates | `ComradeIdle`, `TaskCompleted` hooks | ❌ | Add optional hook runner in leader on idle/task-complete events (script execution + exit-code gating). | P2 |
46
46
  | Task list UX | Ctrl+T toggle; show all/clear tasks by asking | 🟡 | Widget + `/team task list` show blocked/deps; `/team task show <id>`; `/team task clear [completed|all]`. No Ctrl+T toggle yet. | P0 |
47
- | Shared task list across sessions | `CLAUDE_CODE_TASK_LIST_ID=...` | ✅ | `PI_TEAMS_TASK_LIST_ID` env on leader + worker; `/team task use <taskListId>` switches the leader (and newly spawned workers). Existing workers need a restart to pick up changes. Persisted in config.json. | P1 |
47
+ | Shared task list across sessions | `CLAUDE_CODE_TASK_LIST_ID=...` | ✅ | `PI_TEAMS_TASK_LIST_ID` env is **worker-side** (for manually started workers). The leader switches task lists via `/team task use <taskListId>` (persisted in `config.json`). Newly spawned workers inherit the new task list ID; existing workers need a restart to pick up changes. | P1 |
48
48
 
49
49
  ## Prioritized roadmap
50
50
 
@@ -66,7 +66,7 @@ Legend: ✅ implemented • 🟡 partial • ❌ missing
66
66
  4) **Shutdown handshake** ✅
67
67
  - Full protocol: `shutdown_request` → `shutdown_approved` / `shutdown_rejected`
68
68
  - Worker rejects when busy (streaming + active task), auto-approves when idle
69
- - Leader command: `/team shutdown <name> [reason...]` (graceful), `/team kill` as force
69
+ - Leader command: `/team shutdown <name> [reason...]` (graceful), `/team kill <name>` as force
70
70
 
71
71
  5) **Plan approval** ✅
72
72
  - `/team spawn <name> [fresh|branch] [shared|worktree] plan` sets `PI_TEAMS_PLAN_REQUIRED=1`
@@ -109,7 +109,9 @@ Legend: ✅ implemented • 🟡 partial • ❌ missing
109
109
 
110
110
  ## Where changes would land (code map)
111
111
 
112
- - Leader orchestration + commands + tool: `extensions/teams/leader.ts`
112
+ - Leader orchestration: `extensions/teams/leader.ts`
113
+ - Leader `/team` command dispatch: `extensions/teams/leader-team-command.ts`
114
+ - Leader LLM tool (`teams`): `extensions/teams/leader-teams-tool.ts`
113
115
  - Worker mailbox polling + self-claim + protocols: `extensions/teams/worker.ts`
114
116
  - Task store + locking: `extensions/teams/task-store.ts`, `extensions/teams/fs-lock.ts`
115
117
  - Mailbox store + locking: `extensions/teams/mailbox.ts`
@@ -120,5 +122,5 @@ Legend: ✅ implemented • 🟡 partial • ❌ missing
120
122
 
121
123
  - Keep tests hermetic by setting `PI_TEAMS_ROOT_DIR` to a temp directory.
122
124
  - Extend:
123
- - `scripts/smoke-test.mjs` for filesystem-only behaviors (deps, claiming, locking)
125
+ - `scripts/smoke-test.mts` (run via `npm run smoke-test`) for filesystem-only behaviors (deps, claiming, locking)
124
126
  - `scripts/e2e-rpc-test.mjs` for protocol flows (shutdown handshake, plan approval)
@@ -12,9 +12,10 @@ Exercises all core primitives directly via `tsx`:
12
12
 
13
13
  ```bash
14
14
  npx tsx scripts/smoke-test.mts
15
+ # or: npm run smoke-test
15
16
  ```
16
17
 
17
- **What it tests** (60 assertions):
18
+ **What it tests** (overview):
18
19
 
19
20
  | Module | Coverage |
20
21
  |------------------|-----------------------------------------------------------------|
@@ -24,10 +25,10 @@ npx tsx scripts/smoke-test.mts
24
25
  | `task-store.ts` | CRUD, `startAssignedTask`, `completeTask`, `claimNextAvailable`,|
25
26
  | | `unassignTasksForAgent`, dependencies, `clearTasks` |
26
27
  | `team-config.ts` | `ensureTeamConfig` (idempotent), `upsertMember`, `setMemberStatus`, `loadTeamConfig` |
27
- | `protocol.ts` | All 11 message parsers (valid + invalid JSON + wrong type) |
28
- | Pi CLI | `pi --version` executes |
28
+ | `protocol.ts` | Structured message parsers (valid + invalid JSON + wrong type) |
29
+ | Pi CLI | `pi --version` executes (skipped in CI if `pi` not on PATH) |
29
30
 
30
- **Expected result:** `PASSED: 60 FAILED: 0`
31
+ **Expected result:** `PASSED: <n> FAILED: 0`
31
32
 
32
33
  ## 2. Extension Loading Test
33
34
 
@@ -103,6 +104,21 @@ Ask the model:
103
104
 
104
105
  **Expected:** agent1 goes offline, widget updates.
105
106
 
107
+ Optional: stop all teammates without ending the leader session:
108
+
109
+ ```
110
+ /team shutdown
111
+ ```
112
+
113
+ **Expected:** all teammates stop; leader remains active until you exit it (e.g. ctrl+d).
114
+
115
+ If old/manual teammates still show as idle (stale config entries), prune them:
116
+
117
+ ```
118
+ /team prune
119
+ # or: /team prune --all
120
+ ```
121
+
106
122
  ## 4. Worker-side Smoke (verifying child process)
107
123
 
108
124
  To test the worker role directly:
@@ -0,0 +1,74 @@
1
+ // @ts-check
2
+ import eslint from "@eslint/js";
3
+ import tseslint from "typescript-eslint";
4
+
5
+ export default tseslint.config(
6
+ {
7
+ ignores: [
8
+ "node_modules/**",
9
+ "dist/**",
10
+ "build/**",
11
+ "coverage/**",
12
+ ".artifacts/**",
13
+ ".research/**",
14
+ ".resume-sessions/**",
15
+ ],
16
+ },
17
+
18
+ eslint.configs.recommended,
19
+ ...tseslint.configs.recommended,
20
+
21
+ {
22
+ files: ["extensions/**/*.ts", "scripts/**/*.ts", "scripts/**/*.mts"],
23
+ rules: {
24
+ // ━━ Project invariants (AGENTS.md) ━━
25
+ "@typescript-eslint/no-explicit-any": "error",
26
+ "@typescript-eslint/no-non-null-assertion": "error",
27
+ "@typescript-eslint/ban-ts-comment": [
28
+ "error",
29
+ {
30
+ "ts-ignore": true,
31
+ "ts-expect-error": true,
32
+ "ts-nocheck": true,
33
+ "ts-check": false,
34
+ },
35
+ ],
36
+
37
+ // Imports/types
38
+ "@typescript-eslint/consistent-type-imports": [
39
+ "error",
40
+ { prefer: "type-imports", disallowTypeAnnotations: false },
41
+ ],
42
+
43
+ // General correctness
44
+ eqeqeq: ["error", "always"],
45
+
46
+ // Prefer TS-aware unused-vars
47
+ "no-unused-vars": "off",
48
+ "@typescript-eslint/no-unused-vars": [
49
+ "warn",
50
+ {
51
+ argsIgnorePattern: "^_",
52
+ varsIgnorePattern: "^_",
53
+ caughtErrorsIgnorePattern: "^_",
54
+ },
55
+ ],
56
+ },
57
+ },
58
+
59
+ // Scripts/tests: console is expected
60
+ {
61
+ files: ["scripts/**/*.ts", "scripts/**/*.mts"],
62
+ rules: {
63
+ "no-console": "off",
64
+ },
65
+ },
66
+
67
+ // Extension source: discourage stray console.log
68
+ {
69
+ files: ["extensions/**/*.ts"],
70
+ rules: {
71
+ "no-console": "warn",
72
+ },
73
+ },
74
+ );
@@ -68,7 +68,7 @@ export class TranscriptTracker {
68
68
  if (ev.type === "tool_execution_end") {
69
69
  const starts = this.toolStarts.get(name);
70
70
  const startTs = starts?.get(ev.toolCallId);
71
- const durationMs = startTs != null ? now - startTs : 0;
71
+ const durationMs = startTs === undefined ? 0 : now - startTs;
72
72
  starts?.delete(ev.toolCallId);
73
73
  log.push({ kind: "tool_end", toolName: ev.toolName, durationMs, timestamp: now });
74
74
  return;
@@ -135,7 +135,7 @@ export class TranscriptTracker {
135
135
  // Flush all complete lines, keep the last (potentially incomplete) part
136
136
  for (let i = 0; i < parts.length - 1; i++) {
137
137
  const part = parts[i];
138
- if (part == null) continue;
138
+ if (part === undefined) continue;
139
139
  const trimmed = part.trimEnd();
140
140
  if (trimmed) log.push({ kind: "text", text: trimmed, timestamp });
141
141
  }
@@ -6,7 +6,7 @@ import { sanitizeName } from "./names.js";
6
6
  import { getTeamDir, getTeamsRootDir } from "./paths.js";
7
7
  import { TEAM_MAILBOX_NS } from "./protocol.js";
8
8
  import { unassignTasksForAgent, type TeamTask } from "./task-store.js";
9
- import { setMemberStatus, setTeamStyle } from "./team-config.js";
9
+ import { setMemberStatus, setTeamStyle, type TeamConfig } from "./team-config.js";
10
10
  import { TEAMS_STYLES, type TeamsStyle, getTeamsStrings, formatMemberDisplayName } from "./teams-style.js";
11
11
  import type { TeammateRpc } from "./teammate-rpc.js";
12
12
 
@@ -141,11 +141,16 @@ export async function handleTeamShutdownCommand(opts: {
141
141
  ctx: ExtensionCommandContext;
142
142
  rest: string[];
143
143
  teammates: Map<string, TeammateRpc>;
144
+ getTeamConfig: () => TeamConfig | null;
144
145
  leadName: string;
145
146
  style: TeamsStyle;
146
147
  getCurrentCtx: () => ExtensionContext | null;
148
+ stopAllTeammates: (ctx: ExtensionContext, reason: string) => Promise<void>;
149
+ refreshTasks: () => Promise<void>;
150
+ getTasks: () => TeamTask[];
151
+ renderWidget: () => void;
147
152
  }): Promise<void> {
148
- const { ctx, rest, teammates, leadName, style, getCurrentCtx } = opts;
153
+ const { ctx, rest, teammates, getTeamConfig, leadName, style, getCurrentCtx, stopAllTeammates, refreshTasks, getTasks, renderWidget } = opts;
149
154
  const strings = getTeamsStrings(style);
150
155
  const nameRaw = rest[0];
151
156
 
@@ -216,21 +221,154 @@ export async function handleTeamShutdownCommand(opts: {
216
221
  return;
217
222
  }
218
223
 
219
- // /team shutdown (no args) = shutdown leader + all teammates
220
- // Only prompt in interactive TTY mode. In RPC mode, confirm() would require
221
- // the host to send extension_ui_response messages.
224
+ // /team shutdown (no args) = stop all teammates but keep the leader session alive
225
+ await refreshTasks();
226
+ const cfgBefore = getTeamConfig();
227
+ const cfgWorkersOnline = (cfgBefore?.members ?? []).filter((m) => m.role === "worker" && m.status === "online");
228
+
229
+ const activeNames = new Set<string>();
230
+ for (const name of teammates.keys()) activeNames.add(name);
231
+ for (const m of cfgWorkersOnline) activeNames.add(m.name);
232
+
233
+ if (activeNames.size === 0) {
234
+ ctx.ui.notify(`No ${strings.memberTitle.toLowerCase()}s to shut down`, "info");
235
+ return;
236
+ }
237
+
222
238
  if (process.stdout.isTTY && process.stdin.isTTY) {
223
239
  const msg =
224
240
  style === "soviet"
225
- ? `Dissolve the ${strings.teamNoun} and dismiss all ${strings.memberTitle.toLowerCase()}s?`
226
- : "Shutdown this team and stop all teammates?";
227
- const ok = await ctx.ui.confirm("Shutdown", msg);
241
+ ? `Dismiss all ${strings.memberTitle.toLowerCase()}s from the ${strings.teamNoun}?`
242
+ : `Stop all ${String(activeNames.size)} teammate${activeNames.size === 1 ? "" : "s"}?`;
243
+ const ok = await ctx.ui.confirm("Shutdown team", msg);
228
244
  if (!ok) return;
229
245
  }
230
- // In RPC mode, shutdown is deferred until the next input line is handled.
231
- // Teammates are stopped in the session_shutdown handler.
232
- ctx.ui.notify("Shutdown requested", "info");
233
- ctx.shutdown();
246
+
247
+ const reason =
248
+ style === "soviet"
249
+ ? `The ${strings.teamNoun} is dissolved by the chairman`
250
+ : "Stopped by /team shutdown";
251
+ // Stop RPC teammates we own
252
+ await stopAllTeammates(ctx, reason);
253
+
254
+ // Best-effort: ask *manual* workers (persisted in config.json) to shut down too.
255
+ // Also mark them offline so they stop cluttering the UI if they were left behind from old runs.
256
+ await refreshTasks();
257
+ const cfg = getTeamConfig();
258
+ const teamId = ctx.sessionManager.getSessionId();
259
+ const teamDir = getTeamDir(teamId);
260
+
261
+ const inProgressOwners = new Set<string>();
262
+ for (const t of getTasks()) {
263
+ if (t.owner && t.status === "in_progress") inProgressOwners.add(t.owner);
264
+ }
265
+
266
+ const manualWorkers = (cfg?.members ?? []).filter((m) => m.role === "worker" && m.status === "online");
267
+ for (const m of manualWorkers) {
268
+ // If it's an RPC teammate we already stopped above, skip mailbox request.
269
+ if (teammates.has(m.name)) continue;
270
+ // If a manual worker still owns an in-progress task, don't force it offline in the UI.
271
+ if (inProgressOwners.has(m.name)) continue;
272
+
273
+ const requestId = randomUUID();
274
+ const ts = new Date().toISOString();
275
+ try {
276
+ await writeToMailbox(teamDir, TEAM_MAILBOX_NS, m.name, {
277
+ from: leadName,
278
+ text: JSON.stringify({
279
+ type: "shutdown_request",
280
+ requestId,
281
+ from: leadName,
282
+ timestamp: ts,
283
+ reason,
284
+ }),
285
+ timestamp: ts,
286
+ });
287
+ } catch {
288
+ // ignore mailbox errors
289
+ }
290
+
291
+ void setMemberStatus(teamDir, m.name, "offline", {
292
+ meta: { shutdownRequestedAt: ts, shutdownRequestId: requestId, stoppedReason: reason },
293
+ });
294
+ }
295
+
296
+ renderWidget();
297
+ ctx.ui.notify(
298
+ `Team ended: all ${strings.memberTitle.toLowerCase()}s stopped (leader session remains active)`,
299
+ "info",
300
+ );
301
+ }
302
+
303
+ export async function handleTeamPruneCommand(opts: {
304
+ ctx: ExtensionCommandContext;
305
+ rest: string[];
306
+ teammates: Map<string, TeammateRpc>;
307
+ getTeamConfig: () => TeamConfig | null;
308
+ refreshTasks: () => Promise<void>;
309
+ getTasks: () => TeamTask[];
310
+ style: TeamsStyle;
311
+ renderWidget: () => void;
312
+ }): Promise<void> {
313
+ const { ctx, rest, teammates, getTeamConfig, refreshTasks, getTasks, style, renderWidget } = opts;
314
+ const strings = getTeamsStrings(style);
315
+
316
+ const flags = rest.filter((a) => a.startsWith("--"));
317
+ const argsOnly = rest.filter((a) => !a.startsWith("--"));
318
+ const all = flags.includes("--all");
319
+ const unknownFlags = flags.filter((f) => f !== "--all");
320
+ if (unknownFlags.length) {
321
+ ctx.ui.notify(`Unknown flag(s): ${unknownFlags.join(", ")}`, "error");
322
+ return;
323
+ }
324
+ if (argsOnly.length) {
325
+ ctx.ui.notify("Usage: /team prune [--all]", "error");
326
+ return;
327
+ }
328
+
329
+ await refreshTasks();
330
+ const cfg = getTeamConfig();
331
+ const members = (cfg?.members ?? []).filter((m) => m.role === "worker");
332
+ if (!members.length) {
333
+ ctx.ui.notify(`No ${strings.memberTitle.toLowerCase()}s to prune`, "info");
334
+ renderWidget();
335
+ return;
336
+ }
337
+
338
+ const inProgressOwners = new Set<string>();
339
+ for (const t of getTasks()) {
340
+ if (t.owner && t.status === "in_progress") inProgressOwners.add(t.owner);
341
+ }
342
+
343
+ const cutoffMs = 60 * 60 * 1000; // 1h
344
+ const now = Date.now();
345
+
346
+ const pruned: string[] = [];
347
+ for (const m of members) {
348
+ if (teammates.has(m.name)) continue; // still tracked as RPC
349
+ if (inProgressOwners.has(m.name)) continue; // still actively working
350
+ if (!all) {
351
+ const lastSeen = m.lastSeenAt ? Date.parse(m.lastSeenAt) : NaN;
352
+ if (!Number.isFinite(lastSeen)) continue;
353
+ if (now - lastSeen < cutoffMs) continue;
354
+ }
355
+
356
+ const teamId = ctx.sessionManager.getSessionId();
357
+ const teamDir = getTeamDir(teamId);
358
+ await setMemberStatus(teamDir, m.name, "offline", {
359
+ meta: { prunedAt: new Date().toISOString(), prunedBy: "leader" },
360
+ });
361
+ pruned.push(m.name);
362
+ }
363
+
364
+ await refreshTasks();
365
+ renderWidget();
366
+ ctx.ui.notify(
367
+ pruned.length
368
+ ? `Pruned ${pruned.length} stale ${strings.memberTitle.toLowerCase()}(s): ${pruned.join(", ")}`
369
+ : `No stale ${strings.memberTitle.toLowerCase()}s to prune (use --all to force)`,
370
+ "info",
371
+ );
234
372
  }
235
373
 
236
374
  export async function handleTeamStopCommand(opts: {
@@ -244,7 +382,6 @@ export async function handleTeamStopCommand(opts: {
244
382
  renderWidget: () => void;
245
383
  }): Promise<void> {
246
384
  const { ctx, rest, teammates, leadName, style, refreshTasks, getTasks, renderWidget } = opts;
247
- const strings = getTeamsStrings(style);
248
385
 
249
386
  const nameRaw = rest[0];
250
387
  const reason = rest.slice(1).join(" ").trim();
@@ -300,7 +437,7 @@ export async function handleTeamKillCommand(opts: {
300
437
  refreshTasks: () => Promise<void>;
301
438
  renderWidget: () => void;
302
439
  }): Promise<void> {
303
- const { ctx, rest, teammates, taskListId, leadName, style, refreshTasks, renderWidget } = opts;
440
+ const { ctx, rest, teammates, taskListId, leadName: _leadName, style, refreshTasks, renderWidget } = opts;
304
441
  const strings = getTeamsStrings(style);
305
442
 
306
443
  const nameRaw = rest[0];
@@ -71,7 +71,6 @@ export async function handleTeamDmCommand(opts: {
71
71
  style: TeamsStyle;
72
72
  }): Promise<void> {
73
73
  const { ctx, rest, leadName, style } = opts;
74
- const strings = getTeamsStrings(style);
75
74
 
76
75
  const nameRaw = rest[0];
77
76
  const msg = rest.slice(1).join(" ").trim();
@@ -4,7 +4,7 @@ import { sanitizeName } from "./names.js";
4
4
  import { getTeamDir } from "./paths.js";
5
5
  import { TEAM_MAILBOX_NS } from "./protocol.js";
6
6
  import type { TeamsStyle } from "./teams-style.js";
7
- import { formatMemberDisplayName, getTeamsStrings } from "./teams-style.js";
7
+ import { formatMemberDisplayName } from "./teams-style.js";
8
8
 
9
9
  export async function handleTeamPlanCommand(opts: {
10
10
  ctx: ExtensionCommandContext;
@@ -1,32 +1,16 @@
1
- import type { ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
2
  import { pickComradeNames } from "./names.js";
3
3
  import type { TeammateRpc } from "./teammate-rpc.js";
4
4
  import type { TeamsStyle } from "./teams-style.js";
5
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 };
6
+ import type { ContextMode, WorkspaceMode, SpawnTeammateFn } from "./spawn-types.js";
20
7
 
21
8
  export async function handleTeamSpawnCommand(opts: {
22
9
  ctx: ExtensionCommandContext;
23
10
  rest: string[];
24
11
  teammates: Map<string, TeammateRpc>;
25
12
  style: TeamsStyle;
26
- spawnTeammate: (
27
- ctx: ExtensionContext,
28
- opts: { name: string; mode?: ContextMode; workspaceMode?: WorkspaceMode; planRequired?: boolean },
29
- ) => Promise<SpawnTeammateResult>;
13
+ spawnTeammate: SpawnTeammateFn;
30
14
  }): Promise<void> {
31
15
  const { ctx, rest, teammates, style, spawnTeammate } = opts;
32
16
  const strings = getTeamsStrings(style);
@@ -3,7 +3,7 @@ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
3
3
  import { writeToMailbox } from "./mailbox.js";
4
4
  import { sanitizeName } from "./names.js";
5
5
  import { getTeamDir } from "./paths.js";
6
- import { TEAM_MAILBOX_NS } from "./protocol.js";
6
+ import { taskAssignmentPayload } from "./protocol.js";
7
7
  import {
8
8
  addTaskDependency,
9
9
  clearTasks,
@@ -17,7 +17,16 @@ import {
17
17
  } from "./task-store.js";
18
18
  import { ensureTeamConfig } from "./team-config.js";
19
19
  import type { TeamsStyle } from "./teams-style.js";
20
- import { formatMemberDisplayName, getTeamsStrings } from "./teams-style.js";
20
+ import { formatMemberDisplayName } from "./teams-style.js";
21
+
22
+ function parseAssigneePrefix(text: string): { assignee?: string; text: string } {
23
+ const m = text.match(/^([a-zA-Z0-9_-]+):\s*(.+)$/);
24
+ if (!m) return { text };
25
+ const assignee = m[1];
26
+ const rest = m[2];
27
+ if (!assignee || !rest) return { text };
28
+ return { assignee, text: rest };
29
+ }
21
30
 
22
31
  export async function handleTeamTaskCommand(opts: {
23
32
  ctx: ExtensionCommandContext;
@@ -29,8 +38,6 @@ export async function handleTeamTaskCommand(opts: {
29
38
  getTasks: () => TeamTask[];
30
39
  refreshTasks: () => Promise<void>;
31
40
  renderWidget: () => void;
32
- parseAssigneePrefix: (text: string) => { assignee?: string; text: string };
33
- taskAssignmentPayload: (task: TeamTask, assignedBy: string) => unknown;
34
41
  }): Promise<void> {
35
42
  const {
36
43
  ctx,
@@ -42,10 +49,7 @@ export async function handleTeamTaskCommand(opts: {
42
49
  getTasks,
43
50
  refreshTasks,
44
51
  renderWidget,
45
- parseAssigneePrefix,
46
- taskAssignmentPayload,
47
52
  } = opts;
48
- const strings = getTeamsStrings(style);
49
53
 
50
54
  const [taskSub, ...taskRest] = rest;
51
55
  const teamId = ctx.sessionManager.getSessionId();