@tmustier/pi-agent-teams 0.3.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
|
@@ -124,7 +124,8 @@ All management commands live under `/team`.
|
|
|
124
124
|
| `/team broadcast <msg>` | Message all teammates |
|
|
125
125
|
| `/team stop <name> [reason]` | Abort current work (resets task to pending) |
|
|
126
126
|
| `/team shutdown <name> [reason]` | Graceful shutdown (handshake) |
|
|
127
|
-
| `/team shutdown` | Stop all teammates (leader session remains active) |
|
|
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) |
|
|
128
129
|
| `/team kill <name>` | Force-terminate |
|
|
129
130
|
| `/team cleanup [--force]` | Delete team artifacts |
|
|
130
131
|
| `/team id` | Print team/task-list IDs and paths |
|
package/docs/smoke-test-plan.md
CHANGED
|
@@ -112,6 +112,13 @@ Optional: stop all teammates without ending the leader session:
|
|
|
112
112
|
|
|
113
113
|
**Expected:** all teammates stop; leader remains active until you exit it (e.g. ctrl+d).
|
|
114
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
|
+
|
|
115
122
|
## 4. Worker-side Smoke (verifying child process)
|
|
116
123
|
|
|
117
124
|
To test the worker role directly:
|
|
@@ -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,14 +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;
|
|
147
148
|
stopAllTeammates: (ctx: ExtensionContext, reason: string) => Promise<void>;
|
|
148
149
|
refreshTasks: () => Promise<void>;
|
|
150
|
+
getTasks: () => TeamTask[];
|
|
149
151
|
renderWidget: () => void;
|
|
150
152
|
}): Promise<void> {
|
|
151
|
-
const { ctx, rest, teammates, leadName, style, getCurrentCtx, stopAllTeammates, refreshTasks, renderWidget } = opts;
|
|
153
|
+
const { ctx, rest, teammates, getTeamConfig, leadName, style, getCurrentCtx, stopAllTeammates, refreshTasks, getTasks, renderWidget } = opts;
|
|
152
154
|
const strings = getTeamsStrings(style);
|
|
153
155
|
const nameRaw = rest[0];
|
|
154
156
|
|
|
@@ -220,7 +222,15 @@ export async function handleTeamShutdownCommand(opts: {
|
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
// /team shutdown (no args) = stop all teammates but keep the leader session alive
|
|
223
|
-
|
|
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) {
|
|
224
234
|
ctx.ui.notify(`No ${strings.memberTitle.toLowerCase()}s to shut down`, "info");
|
|
225
235
|
return;
|
|
226
236
|
}
|
|
@@ -229,7 +239,7 @@ export async function handleTeamShutdownCommand(opts: {
|
|
|
229
239
|
const msg =
|
|
230
240
|
style === "soviet"
|
|
231
241
|
? `Dismiss all ${strings.memberTitle.toLowerCase()}s from the ${strings.teamNoun}?`
|
|
232
|
-
: `Stop all ${String(
|
|
242
|
+
: `Stop all ${String(activeNames.size)} teammate${activeNames.size === 1 ? "" : "s"}?`;
|
|
233
243
|
const ok = await ctx.ui.confirm("Shutdown team", msg);
|
|
234
244
|
if (!ok) return;
|
|
235
245
|
}
|
|
@@ -238,8 +248,51 @@ export async function handleTeamShutdownCommand(opts: {
|
|
|
238
248
|
style === "soviet"
|
|
239
249
|
? `The ${strings.teamNoun} is dissolved by the chairman`
|
|
240
250
|
: "Stopped by /team shutdown";
|
|
251
|
+
// Stop RPC teammates we own
|
|
241
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.
|
|
242
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
|
+
|
|
243
296
|
renderWidget();
|
|
244
297
|
ctx.ui.notify(
|
|
245
298
|
`Team ended: all ${strings.memberTitle.toLowerCase()}s stopped (leader session remains active)`,
|
|
@@ -247,6 +300,77 @@ export async function handleTeamShutdownCommand(opts: {
|
|
|
247
300
|
);
|
|
248
301
|
}
|
|
249
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
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
250
374
|
export async function handleTeamStopCommand(opts: {
|
|
251
375
|
ctx: ExtensionCommandContext;
|
|
252
376
|
rest: string[];
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
handleTeamCleanupCommand,
|
|
10
10
|
handleTeamDelegateCommand,
|
|
11
11
|
handleTeamKillCommand,
|
|
12
|
+
handleTeamPruneCommand,
|
|
12
13
|
handleTeamShutdownCommand,
|
|
13
14
|
handleTeamStopCommand,
|
|
14
15
|
handleTeamStyleCommand,
|
|
@@ -46,6 +47,7 @@ const TEAM_HELP_TEXT = [
|
|
|
46
47
|
" /team plan approve <name>",
|
|
47
48
|
" /team plan reject <name> [feedback...]",
|
|
48
49
|
" /team cleanup [--force]",
|
|
50
|
+
" /team prune [--all] # hide stale manual teammates (mark offline)",
|
|
49
51
|
" /team task add <text...>",
|
|
50
52
|
" /team task assign <id> <agent>",
|
|
51
53
|
" /team task unassign <id>",
|
|
@@ -163,6 +165,19 @@ export async function handleTeamCommand(opts: {
|
|
|
163
165
|
});
|
|
164
166
|
},
|
|
165
167
|
|
|
168
|
+
prune: async () => {
|
|
169
|
+
await handleTeamPruneCommand({
|
|
170
|
+
ctx,
|
|
171
|
+
rest,
|
|
172
|
+
teammates,
|
|
173
|
+
getTeamConfig,
|
|
174
|
+
refreshTasks,
|
|
175
|
+
getTasks,
|
|
176
|
+
style,
|
|
177
|
+
renderWidget,
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
|
|
166
181
|
delegate: async () => {
|
|
167
182
|
await handleTeamDelegateCommand({
|
|
168
183
|
ctx,
|
|
@@ -178,11 +193,13 @@ export async function handleTeamCommand(opts: {
|
|
|
178
193
|
ctx,
|
|
179
194
|
rest,
|
|
180
195
|
teammates,
|
|
196
|
+
getTeamConfig,
|
|
181
197
|
leadName,
|
|
182
198
|
style,
|
|
183
199
|
getCurrentCtx,
|
|
184
200
|
stopAllTeammates,
|
|
185
201
|
refreshTasks,
|
|
202
|
+
getTasks,
|
|
186
203
|
renderWidget,
|
|
187
204
|
});
|
|
188
205
|
},
|
package/package.json
CHANGED
|
@@ -95,8 +95,9 @@ Spawning with `plan` restricts the teammate to read-only tools. After producing
|
|
|
95
95
|
```
|
|
96
96
|
/team panel # interactive overlay with teammate details
|
|
97
97
|
/team list # show teammates and their state
|
|
98
|
-
/team shutdown # stop all teammates (leader session remains active)
|
|
98
|
+
/team shutdown # stop all teammates (RPC + best-effort manual) (leader session remains active)
|
|
99
99
|
/team shutdown <name> # graceful shutdown (teammate can reject if busy)
|
|
100
|
+
/team prune [--all] # hide stale manual teammates (mark offline in config)
|
|
100
101
|
/team kill <name> # force-terminate one RPC teammate
|
|
101
102
|
/team cleanup [--force] # delete team directory after all teammates stopped
|
|
102
103
|
```
|