pi-squad 0.17.1 → 0.18.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 +74 -0
- package/README.md +12 -2
- package/docs/persistent-master-switch-contract.md +148 -0
- package/package.json +2 -1
- package/src/commands.ts +585 -0
- package/src/index.ts +16 -1921
- package/src/lifecycle.ts +164 -0
- package/src/panel/squad-panel.ts +16 -0
- package/src/panel-runtime.ts +157 -0
- package/src/protocol.ts +3 -47
- package/src/report.ts +40 -2
- package/src/runtime.ts +196 -0
- package/src/scheduler-runtime.ts +117 -0
- package/src/scheduler.ts +149 -135
- package/src/start-squad.ts +176 -0
- package/src/store.ts +3 -44
- package/src/tools-registration.ts +609 -0
- package/src/types.ts +2 -26
- package/src/supervisor.ts +0 -142
package/src/commands.ts
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { setupSquadWidget } from "./panel/squad-widget.js";
|
|
4
|
+
import { activateSquadView, openPanel, pickSquad } from "./panel-runtime.js";
|
|
5
|
+
import { cancelExactSquad, ensureScheduler, reviveScheduler } from "./scheduler-runtime.js";
|
|
6
|
+
import * as store from "./store.js";
|
|
7
|
+
import type { Squad } from "./types.js";
|
|
8
|
+
import { THINKING_LEVELS } from "./types.js";
|
|
9
|
+
import { DISABLED_GUIDANCE, focusSquad, forceWidgetUpdate, formatTaskProgress, getActiveScheduler, getMainSessionModel, resolveResumeSquad, runtime } from "./runtime.js";
|
|
10
|
+
|
|
11
|
+
export function registerCommands(pi: ExtensionAPI, squadSkillPaths: string[]): void {
|
|
12
|
+
// =========================================================================
|
|
13
|
+
// Slash Commands
|
|
14
|
+
// =========================================================================
|
|
15
|
+
|
|
16
|
+
pi.registerCommand("squad", {
|
|
17
|
+
description: "Browse, select, and manage squads. Usage: /squad [list|all|select|resume|agents|msg|widget|panel|cancel|clear]",
|
|
18
|
+
getArgumentCompletions: (prefix) => {
|
|
19
|
+
const subs = [
|
|
20
|
+
{ value: "list", label: "list", description: "List squads for current project" },
|
|
21
|
+
{ value: "all", label: "all", description: "List all squads, select to activate" },
|
|
22
|
+
{ value: "select", label: "select", description: "Pick a squad to view (interactive)" },
|
|
23
|
+
{ value: "resume", label: "resume", description: "Resume an exact paused/failed/failed-review squad" },
|
|
24
|
+
{ value: "agents", label: "agents", description: "List, view, or edit agent definitions" },
|
|
25
|
+
{ value: "defaults", label: "defaults", description: "Default model/thinking for agents (follow main session, pi default, or fixed)" },
|
|
26
|
+
{ value: "advisor", label: "advisor", description: "Advisor-first rescue for stuck agents (on/off, model, limits)" },
|
|
27
|
+
{ value: "msg", label: "msg", description: "Message a task: /squad msg [task-id|running-agent] text" },
|
|
28
|
+
{ value: "widget", label: "widget", description: "Toggle live widget" },
|
|
29
|
+
{ value: "panel", label: "panel", description: "Toggle overlay panel" },
|
|
30
|
+
{ value: "cancel", label: "cancel", description: "Cancel running squad" },
|
|
31
|
+
{ value: "clear", label: "clear", description: "Dismiss widget and deactivate squad" },
|
|
32
|
+
{ value: "cleanup", label: "cleanup", description: "Delete squad data (select or all)" },
|
|
33
|
+
{ value: "enable", label: "enable", description: "Enable pi-squad (tools, widget, system prompt)" },
|
|
34
|
+
{ value: "disable", label: "disable", description: "Disable pi-squad completely" },
|
|
35
|
+
];
|
|
36
|
+
return subs.filter((s) => s.value.startsWith(prefix));
|
|
37
|
+
},
|
|
38
|
+
handler: async (args, ctx) => {
|
|
39
|
+
const requested = args.trim();
|
|
40
|
+
if (!runtime.squadEnabled && requested !== "enable" && requested !== "disable") {
|
|
41
|
+
ctx.ui.notify(DISABLED_GUIDANCE, "warning");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const parts = requested.split(/\s+/);
|
|
45
|
+
const sub = parts[0] || "select";
|
|
46
|
+
|
|
47
|
+
switch (sub) {
|
|
48
|
+
case "list": {
|
|
49
|
+
const squads = store.listSquadsForProject(ctx.cwd);
|
|
50
|
+
if (squads.length === 0) {
|
|
51
|
+
ctx.ui.notify(`No squads for this project`, "info");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const selected = await pickSquad(ctx, squads);
|
|
55
|
+
if (selected) activateSquadView(selected.id, ctx);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case "all": {
|
|
60
|
+
const all = store.listSquads()
|
|
61
|
+
.map((id) => store.loadSquad(id))
|
|
62
|
+
.filter((s): s is Squad => s !== null)
|
|
63
|
+
.sort((a, b) => b.created.localeCompare(a.created));
|
|
64
|
+
if (all.length === 0) {
|
|
65
|
+
ctx.ui.notify("No squads found", "info");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const selected = await pickSquad(ctx, all, true);
|
|
69
|
+
if (selected) activateSquadView(selected.id, ctx);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
case "select": {
|
|
74
|
+
// Interactive selector — show project squads first, fall back to all
|
|
75
|
+
let squads = store.listSquadsForProject(ctx.cwd);
|
|
76
|
+
let showProject = false;
|
|
77
|
+
if (squads.length === 0) {
|
|
78
|
+
squads = store.listSquads()
|
|
79
|
+
.map((id) => store.loadSquad(id))
|
|
80
|
+
.filter((s): s is Squad => s !== null)
|
|
81
|
+
.sort((a, b) => b.created.localeCompare(a.created));
|
|
82
|
+
showProject = true;
|
|
83
|
+
}
|
|
84
|
+
if (squads.length === 0) {
|
|
85
|
+
ctx.ui.notify("No squads found", "info");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// If only one, activate it directly
|
|
89
|
+
if (squads.length === 1) {
|
|
90
|
+
activateSquadView(squads[0].id, ctx);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const selected = await pickSquad(ctx, squads, showProject);
|
|
94
|
+
if (selected) activateSquadView(selected.id, ctx);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case "resume": {
|
|
99
|
+
const squad = resolveResumeSquad(ctx.cwd, parts[1]);
|
|
100
|
+
if (!squad) {
|
|
101
|
+
ctx.ui.notify(parts[1] ? `Squad '${parts[1]}' not found` : "No paused, failed, or failed-review squad found", "warning");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const scheduler = ensureScheduler(pi, squad.id, squadSkillPaths);
|
|
105
|
+
try {
|
|
106
|
+
await scheduler.resume();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
ctx.ui.notify(`Resume failed: ${(error as Error).message}`, "error");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const tasks = store.loadAllTasks(squad.id);
|
|
112
|
+
ctx.ui.notify(`Resumed: ${squad.id} (${formatTaskProgress(tasks)})`, "info");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case "widget": {
|
|
117
|
+
runtime.widgetState.enabled = !runtime.widgetState.enabled;
|
|
118
|
+
if (runtime.widgetState.enabled) {
|
|
119
|
+
if (!runtime.activeSquadId) {
|
|
120
|
+
const latest = store.findLatestSquad(ctx.cwd);
|
|
121
|
+
if (latest) activateSquadView(latest.id, ctx);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// requestUpdate handles both enable (renders) and disable (clears)
|
|
125
|
+
runtime.widgetControls?.requestUpdate();
|
|
126
|
+
ctx.ui.notify(`Squad widget ${runtime.widgetState.enabled ? "enabled" : "disabled"}`, "info");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case "panel": {
|
|
131
|
+
// Activate latest squad if none active
|
|
132
|
+
if (!runtime.activeSquadId) {
|
|
133
|
+
const latest = store.findLatestSquad(ctx.cwd);
|
|
134
|
+
if (latest) {
|
|
135
|
+
activateSquadView(latest.id, ctx);
|
|
136
|
+
} else {
|
|
137
|
+
ctx.ui.notify("No squads found", "info");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (runtime.activeSquadId) {
|
|
142
|
+
const sched = reviveScheduler(pi, runtime.activeSquadId, squadSkillPaths);
|
|
143
|
+
openPanel(pi, ctx, sched, runtime.activeSquadId, squadSkillPaths);
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case "msg": {
|
|
149
|
+
if (!runtime.activeSquadId) {
|
|
150
|
+
ctx.ui.notify("No active squad. Use /squad select first.", "info");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const msgSquad = store.loadSquad(runtime.activeSquadId);
|
|
154
|
+
if (!msgSquad) return;
|
|
155
|
+
// Parse: /squad msg [task-id|running-agent] message text
|
|
156
|
+
const msgParts = parts.slice(1);
|
|
157
|
+
let targetAgent: string | undefined;
|
|
158
|
+
let msgText: string;
|
|
159
|
+
|
|
160
|
+
if (msgParts.length === 0) {
|
|
161
|
+
// Interactive: ask for message
|
|
162
|
+
const input = await ctx.ui.input("Message to squad agent", "Type your message...");
|
|
163
|
+
if (!input) return;
|
|
164
|
+
msgText = input;
|
|
165
|
+
} else {
|
|
166
|
+
// Check if first word is an agent name
|
|
167
|
+
const maybeAgent = store.loadAgentDef(msgParts[0], msgSquad.cwd);
|
|
168
|
+
if (maybeAgent && msgParts.length > 1) {
|
|
169
|
+
targetAgent = msgParts[0];
|
|
170
|
+
msgText = msgParts.slice(1).join(" ");
|
|
171
|
+
} else {
|
|
172
|
+
msgText = msgParts.join(" ");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Find one exact target task. A task ID can address completed or
|
|
177
|
+
// stopped work; an agent name remains shorthand for a live task only.
|
|
178
|
+
const msgTasks = store.loadAllTasks(runtime.activeSquadId);
|
|
179
|
+
let targetTaskId: string | undefined;
|
|
180
|
+
const explicitTask = msgParts.length > 1
|
|
181
|
+
? msgTasks.find((task) => task.id === msgParts[0])
|
|
182
|
+
: undefined;
|
|
183
|
+
if (explicitTask) {
|
|
184
|
+
targetTaskId = explicitTask.id;
|
|
185
|
+
targetAgent = explicitTask.agent;
|
|
186
|
+
msgText = msgParts.slice(1).join(" ");
|
|
187
|
+
} else if (targetAgent) {
|
|
188
|
+
const liveMatches = msgTasks.filter(
|
|
189
|
+
(task) => task.agent === targetAgent && getActiveScheduler()?.getPool().isRunning(task.id),
|
|
190
|
+
);
|
|
191
|
+
if (liveMatches.length === 1) targetTaskId = liveMatches[0].id;
|
|
192
|
+
if (!targetTaskId) {
|
|
193
|
+
ctx.ui.notify(`Agent '${targetAgent}' is not working on exactly one live task; provide an exact task ID`, "warning");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
const liveTasks = msgTasks.filter((task) =>
|
|
198
|
+
getActiveScheduler()?.getPool().isRunning(task.id),
|
|
199
|
+
);
|
|
200
|
+
if (liveTasks.length === 1) {
|
|
201
|
+
targetTaskId = liveTasks[0].id;
|
|
202
|
+
targetAgent = liveTasks[0].agent;
|
|
203
|
+
} else {
|
|
204
|
+
ctx.ui.notify("No unique live task to message; provide an exact task ID", "warning");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let msgSched = getActiveScheduler();
|
|
210
|
+
if (!msgSched) {
|
|
211
|
+
msgSched = reviveScheduler(pi, runtime.activeSquadId, squadSkillPaths);
|
|
212
|
+
await msgSched.start();
|
|
213
|
+
}
|
|
214
|
+
const delivered = await msgSched.sendHumanMessage(targetTaskId, msgText);
|
|
215
|
+
ctx.ui.notify(
|
|
216
|
+
delivered ? `Sent to ${targetAgent}: "${msgText}"` : `Queued durably for ${targetTaskId}`,
|
|
217
|
+
"info",
|
|
218
|
+
);
|
|
219
|
+
forceWidgetUpdate();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case "cancel": {
|
|
224
|
+
const cancelledId = runtime.activeSquadId;
|
|
225
|
+
if (!cancelledId) {
|
|
226
|
+
ctx.ui.notify("No focused squad to cancel", "info");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
await cancelExactSquad(cancelledId, squadSkillPaths);
|
|
230
|
+
ctx.ui.notify(`Squad '${cancelledId}' cancelled`, "info");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case "clear": {
|
|
235
|
+
if (runtime.activeSquadId) runtime.schedulers.delete(runtime.activeSquadId);
|
|
236
|
+
focusSquad(null);
|
|
237
|
+
runtime.widgetControls?.dispose();
|
|
238
|
+
ctx.ui.notify("Squad view cleared", "info");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
case "cleanup": {
|
|
243
|
+
const cleanupArg = parts[1];
|
|
244
|
+
const allSquadIds = store.listSquads();
|
|
245
|
+
|
|
246
|
+
if (allSquadIds.length === 0) {
|
|
247
|
+
ctx.ui.notify("No squads to clean up", "info");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (cleanupArg === "all") {
|
|
252
|
+
// Stop any running runtime.schedulers first
|
|
253
|
+
for (const [id, sched] of runtime.schedulers) {
|
|
254
|
+
await sched.stop();
|
|
255
|
+
}
|
|
256
|
+
runtime.schedulers.clear();
|
|
257
|
+
focusSquad(null);
|
|
258
|
+
|
|
259
|
+
let count = 0;
|
|
260
|
+
for (const id of allSquadIds) {
|
|
261
|
+
fs.rmSync(store.getSquadDir(id), { recursive: true, force: true });
|
|
262
|
+
count++;
|
|
263
|
+
}
|
|
264
|
+
ctx.ui.notify(`Deleted ${count} squad(s)`, "info");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Interactive: pick squads to delete
|
|
269
|
+
const squads = allSquadIds
|
|
270
|
+
.map((id) => store.loadSquad(id))
|
|
271
|
+
.filter((s): s is Squad => s !== null)
|
|
272
|
+
.sort((a, b) => b.created.localeCompare(a.created));
|
|
273
|
+
|
|
274
|
+
const options = [
|
|
275
|
+
"🗑 Delete ALL squads",
|
|
276
|
+
...squads.map((s) => {
|
|
277
|
+
const tasks = store.loadAllTasks(s.id);
|
|
278
|
+
const cost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
279
|
+
const icon = s.status === "done" ? "✓" : s.status === "running" ? "⏳" : s.status === "review" ? "◆" : s.status === "failed" ? "✗" : "·";
|
|
280
|
+
return `${icon} ${s.id} [${s.status}] ${formatTaskProgress(tasks)} $${cost.toFixed(2)}`;
|
|
281
|
+
}),
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
const choice = await ctx.ui.select("Delete squad data", options);
|
|
285
|
+
if (!choice) return;
|
|
286
|
+
|
|
287
|
+
if (choice.startsWith("🗑")) {
|
|
288
|
+
// Delete all
|
|
289
|
+
for (const [id, sched] of runtime.schedulers) {
|
|
290
|
+
await sched.stop();
|
|
291
|
+
}
|
|
292
|
+
runtime.schedulers.clear();
|
|
293
|
+
focusSquad(null);
|
|
294
|
+
let count = 0;
|
|
295
|
+
for (const id of allSquadIds) {
|
|
296
|
+
fs.rmSync(store.getSquadDir(id), { recursive: true, force: true });
|
|
297
|
+
count++;
|
|
298
|
+
}
|
|
299
|
+
ctx.ui.notify(`Deleted ${count} squad(s)`, "info");
|
|
300
|
+
} else {
|
|
301
|
+
// Delete selected
|
|
302
|
+
const idx = options.indexOf(choice) - 1; // -1 for the "Delete ALL" option
|
|
303
|
+
if (idx >= 0 && idx < squads.length) {
|
|
304
|
+
const squad = squads[idx];
|
|
305
|
+
// Stop scheduler if running
|
|
306
|
+
const sched = runtime.schedulers.get(squad.id);
|
|
307
|
+
if (sched) {
|
|
308
|
+
await sched.stop();
|
|
309
|
+
runtime.schedulers.delete(squad.id);
|
|
310
|
+
}
|
|
311
|
+
if (runtime.activeSquadId === squad.id) focusSquad(null);
|
|
312
|
+
fs.rmSync(store.getSquadDir(squad.id), { recursive: true, force: true });
|
|
313
|
+
ctx.ui.notify(`Deleted: ${squad.id}`, "info");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
case "enable": {
|
|
320
|
+
const wasEnabled = runtime.squadEnabled;
|
|
321
|
+
try {
|
|
322
|
+
const settings = store.loadSquadSettings();
|
|
323
|
+
settings.enabled = true;
|
|
324
|
+
store.saveSquadSettings(settings);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
ctx.ui.notify(`Could not enable pi-squad: ${(error as Error).message}`, "error");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
runtime.squadEnabled = true;
|
|
330
|
+
runtime.widgetState.enabled = true;
|
|
331
|
+
if (!wasEnabled) runtime.widgetState.squadId = null;
|
|
332
|
+
if (!runtime.widgetControls && runtime.uiCtx?.hasUI) runtime.widgetControls = setupSquadWidget(runtime.uiCtx, runtime.widgetState);
|
|
333
|
+
runtime.widgetControls?.requestUpdate();
|
|
334
|
+
ctx.ui.notify("pi-squad enabled. No suspended work was resumed; use /squad select and an explicit /squad resume <id> or exact resume_task if needed.", "info");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
case "disable": {
|
|
339
|
+
if (!runtime.squadEnabled) {
|
|
340
|
+
ctx.ui.notify("pi-squad is already disabled. Run /squad enable to use squad operations.", "info");
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const settings = store.loadSquadSettings();
|
|
345
|
+
settings.enabled = false;
|
|
346
|
+
store.saveSquadSettings(settings);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
ctx.ui.notify(`Could not disable pi-squad: ${(error as Error).message}`, "error");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
// The persisted state is authoritative before shutdown begins.
|
|
352
|
+
runtime.squadEnabled = false;
|
|
353
|
+
runtime.closeOverlay?.();
|
|
354
|
+
for (const sched of runtime.schedulers.values()) await sched.stop();
|
|
355
|
+
runtime.schedulers.clear();
|
|
356
|
+
runtime.widgetState.enabled = false;
|
|
357
|
+
runtime.widgetState.squadId = null;
|
|
358
|
+
focusSquad(null);
|
|
359
|
+
runtime.widgetControls?.dispose();
|
|
360
|
+
runtime.widgetControls = null;
|
|
361
|
+
ctx.ui.notify("pi-squad disabled. Running work was durably suspended; run /squad enable before any squad operation.", "info");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
case "defaults": {
|
|
366
|
+
const settings = store.loadSquadSettings();
|
|
367
|
+
const mainModel = getMainSessionModel() || "(unknown)";
|
|
368
|
+
const mainThinking = runtime.getMainSessionThinking() || "(unknown)";
|
|
369
|
+
const fmtPolicy = (v: string, live: string) =>
|
|
370
|
+
v === "main" ? `follow main session (now: ${live})` : v === "pi-default" ? "pi default" : v;
|
|
371
|
+
|
|
372
|
+
const which = await ctx.ui.select(
|
|
373
|
+
`Squad defaults — model: ${fmtPolicy(settings.defaultModel, mainModel)} | thinking: ${fmtPolicy(settings.defaultThinking, mainThinking)}`,
|
|
374
|
+
["Change default model", "Change default thinking", "Cancel"],
|
|
375
|
+
);
|
|
376
|
+
if (!which || which === "Cancel") return;
|
|
377
|
+
|
|
378
|
+
if (which === "Change default model") {
|
|
379
|
+
const choice = await ctx.ui.select("Default model for squad agents", [
|
|
380
|
+
`Follow main session (now: ${mainModel})`,
|
|
381
|
+
"pi default (child pi resolves its own)",
|
|
382
|
+
"Custom model…",
|
|
383
|
+
]);
|
|
384
|
+
if (!choice) return;
|
|
385
|
+
if (choice.startsWith("Follow")) settings.defaultModel = "main";
|
|
386
|
+
else if (choice.startsWith("pi default")) settings.defaultModel = "pi-default";
|
|
387
|
+
else {
|
|
388
|
+
const custom = await ctx.ui.input("Model id (e.g. openai-codex/gpt-5.6-terra)", settings.defaultModel === "main" || settings.defaultModel === "pi-default" ? "" : settings.defaultModel);
|
|
389
|
+
if (!custom || !custom.trim()) return;
|
|
390
|
+
settings.defaultModel = custom.trim();
|
|
391
|
+
}
|
|
392
|
+
store.saveSquadSettings(settings);
|
|
393
|
+
ctx.ui.notify(`Squad default model → ${fmtPolicy(settings.defaultModel, mainModel)}`, "info");
|
|
394
|
+
} else {
|
|
395
|
+
const choice = await ctx.ui.select("Default thinking for squad agents", [
|
|
396
|
+
`Follow main session (now: ${mainThinking})`,
|
|
397
|
+
"pi default (child pi resolves its own)",
|
|
398
|
+
...THINKING_LEVELS,
|
|
399
|
+
]);
|
|
400
|
+
if (!choice) return;
|
|
401
|
+
if (choice.startsWith("Follow")) settings.defaultThinking = "main";
|
|
402
|
+
else if (choice.startsWith("pi default")) settings.defaultThinking = "pi-default";
|
|
403
|
+
else settings.defaultThinking = choice;
|
|
404
|
+
store.saveSquadSettings(settings);
|
|
405
|
+
ctx.ui.notify(`Squad default thinking → ${fmtPolicy(settings.defaultThinking, mainThinking)}`, "info");
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
case "advisor": {
|
|
411
|
+
const settings = store.loadSquadSettings();
|
|
412
|
+
const adv = settings.advisor;
|
|
413
|
+
const mainModelLabel = getMainSessionModel() || "(unknown)";
|
|
414
|
+
const modelLabel = adv.model === "main" ? `main session (now: ${mainModelLabel})` : adv.model;
|
|
415
|
+
|
|
416
|
+
const choice = await ctx.ui.select(
|
|
417
|
+
`Squad advisor — ${adv.enabled ? "ON" : "OFF"} | model: ${modelLabel} | ${adv.maxCallsPerTask} calls/task, ${adv.reasoning} reasoning`,
|
|
418
|
+
[adv.enabled ? "Disable advisor" : "Enable advisor", "Change advisor model", "Change max calls per task", "Change reasoning effort", "Cancel"],
|
|
419
|
+
);
|
|
420
|
+
if (!choice || choice === "Cancel") return;
|
|
421
|
+
|
|
422
|
+
if (choice.startsWith("Disable") || choice.startsWith("Enable")) {
|
|
423
|
+
adv.enabled = !adv.enabled;
|
|
424
|
+
ctx.ui.notify(`Squad advisor ${adv.enabled ? "enabled — stuck agents get a strong-model rescue before escalating" : "disabled — stuck agents escalate directly"}`, "info");
|
|
425
|
+
} else if (choice === "Change advisor model") {
|
|
426
|
+
const sel = await ctx.ui.select("Advisor model", [`Follow main session (now: ${mainModelLabel})`, "Custom model…"]);
|
|
427
|
+
if (!sel) return;
|
|
428
|
+
if (sel.startsWith("Follow")) adv.model = "main";
|
|
429
|
+
else {
|
|
430
|
+
const custom = await ctx.ui.input("Advisor model (provider/id)", adv.model === "main" ? "" : adv.model);
|
|
431
|
+
if (!custom || !custom.trim()) return;
|
|
432
|
+
adv.model = custom.trim();
|
|
433
|
+
}
|
|
434
|
+
ctx.ui.notify(`Advisor model → ${adv.model}`, "info");
|
|
435
|
+
} else if (choice === "Change max calls per task") {
|
|
436
|
+
const n = await ctx.ui.input("Max advisor calls per task", String(adv.maxCallsPerTask));
|
|
437
|
+
const parsed = n ? Number.parseInt(n, 10) : NaN;
|
|
438
|
+
if (!Number.isFinite(parsed) || parsed < 0) return;
|
|
439
|
+
adv.maxCallsPerTask = parsed;
|
|
440
|
+
ctx.ui.notify(`Advisor max calls/task → ${parsed}`, "info");
|
|
441
|
+
} else {
|
|
442
|
+
const lvl = await ctx.ui.select("Advisor reasoning effort", ["minimal", "low", "medium", "high", "xhigh"]);
|
|
443
|
+
if (!lvl) return;
|
|
444
|
+
adv.reasoning = lvl;
|
|
445
|
+
ctx.ui.notify(`Advisor reasoning → ${lvl}`, "info");
|
|
446
|
+
}
|
|
447
|
+
store.saveSquadSettings(settings);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
case "agents": {
|
|
452
|
+
const agentArg = parts[1];
|
|
453
|
+
const allAgents = store.loadAllAgentDefs(ctx.cwd);
|
|
454
|
+
|
|
455
|
+
if (!agentArg) {
|
|
456
|
+
// List all agents — interactive selector
|
|
457
|
+
if (allAgents.length === 0) {
|
|
458
|
+
ctx.ui.notify("No agents found", "info");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const options = allAgents.map((a) => {
|
|
462
|
+
const model = a.model ? ` [${a.model}${a.thinking ? `:${a.thinking}` : ""}]` : a.thinking ? ` [default:${a.thinking}]` : " [default]";
|
|
463
|
+
const status = a.disabled ? " ✗ disabled" : "";
|
|
464
|
+
return `${a.name} — ${a.role}${model}${status}`;
|
|
465
|
+
});
|
|
466
|
+
const choice = await ctx.ui.select("Squad Agents (select to view/edit)", options);
|
|
467
|
+
if (!choice) return;
|
|
468
|
+
const selectedName = choice.split(" — ")[0];
|
|
469
|
+
const agent = allAgents.find((a) => a.name === selectedName);
|
|
470
|
+
if (!agent) return;
|
|
471
|
+
|
|
472
|
+
// Show agent details and offer actions
|
|
473
|
+
const disableLabel = agent.disabled ? "Enable agent" : "Disable agent";
|
|
474
|
+
const actions = [
|
|
475
|
+
"View details",
|
|
476
|
+
"Edit in editor",
|
|
477
|
+
"Change model",
|
|
478
|
+
"Change thinking",
|
|
479
|
+
"Toggle tools (restrict/unrestrict)",
|
|
480
|
+
disableLabel,
|
|
481
|
+
"Cancel",
|
|
482
|
+
];
|
|
483
|
+
const action = await ctx.ui.select(`${agent.name} (${agent.role})`, actions);
|
|
484
|
+
if (!action || action === "Cancel") return;
|
|
485
|
+
|
|
486
|
+
if (action === "View details") {
|
|
487
|
+
const details = [
|
|
488
|
+
`Name: ${agent.name}`,
|
|
489
|
+
`Role: ${agent.role}`,
|
|
490
|
+
`Description: ${agent.description}`,
|
|
491
|
+
`Model: ${agent.model || "(default)"}`,
|
|
492
|
+
`Thinking: ${agent.thinking || "(default)"}`,
|
|
493
|
+
`Tools: ${agent.tools ? agent.tools.join(", ") : "(all)"}`,
|
|
494
|
+
`Tags: ${agent.tags.join(", ")}`,
|
|
495
|
+
``,
|
|
496
|
+
`Prompt:`,
|
|
497
|
+
agent.prompt,
|
|
498
|
+
``,
|
|
499
|
+
`File: ${store.getGlobalAgentsDir()}/${agent.name}.json`,
|
|
500
|
+
].join("\n");
|
|
501
|
+
ctx.ui.notify(details, "info");
|
|
502
|
+
} else if (action === "Edit in editor") {
|
|
503
|
+
// Check for local override first, fall back to global
|
|
504
|
+
const localPath = `${store.getLocalAgentsDir(ctx.cwd)}/${agent.name}.json`;
|
|
505
|
+
const globalPath = `${store.getGlobalAgentsDir()}/${agent.name}.json`;
|
|
506
|
+
const filePath = fs.existsSync(localPath) ? localPath : globalPath;
|
|
507
|
+
pi.sendMessage({
|
|
508
|
+
customType: "squad-edit-agent",
|
|
509
|
+
content: `Edit agent file: ${filePath}`,
|
|
510
|
+
display: true,
|
|
511
|
+
}, { triggerTurn: true });
|
|
512
|
+
} else if (action === "Change model") {
|
|
513
|
+
const newModel = await ctx.ui.input(
|
|
514
|
+
`Model for ${agent.name} (empty = default)`,
|
|
515
|
+
agent.model || "",
|
|
516
|
+
);
|
|
517
|
+
if (newModel !== undefined) {
|
|
518
|
+
agent.model = newModel.trim() || null;
|
|
519
|
+
store.saveAgentDef(agent);
|
|
520
|
+
ctx.ui.notify(`${agent.name} model → ${agent.model || "(default)"}`, "info");
|
|
521
|
+
}
|
|
522
|
+
} else if (action === "Change thinking") {
|
|
523
|
+
const levels = ["(default)", ...THINKING_LEVELS];
|
|
524
|
+
const level = await ctx.ui.select(`Thinking level for ${agent.name}`, levels);
|
|
525
|
+
if (level !== undefined) {
|
|
526
|
+
agent.thinking = level === "(default)" ? null : level;
|
|
527
|
+
store.saveAgentDef(agent);
|
|
528
|
+
ctx.ui.notify(`${agent.name} thinking → ${agent.thinking || "(default)"}`, "info");
|
|
529
|
+
}
|
|
530
|
+
} else if (action === disableLabel) {
|
|
531
|
+
agent.disabled = !agent.disabled;
|
|
532
|
+
store.saveAgentDef(agent);
|
|
533
|
+
const newState = agent.disabled ? "disabled — planner will not assign tasks to this agent" : "enabled";
|
|
534
|
+
ctx.ui.notify(`${agent.name}: ${newState}`, "info");
|
|
535
|
+
} else if (action === "Toggle tools") {
|
|
536
|
+
if (agent.tools) {
|
|
537
|
+
agent.tools = null;
|
|
538
|
+
store.saveAgentDef(agent);
|
|
539
|
+
ctx.ui.notify(`${agent.name}: all tools enabled`, "info");
|
|
540
|
+
} else {
|
|
541
|
+
const toolList = await ctx.ui.input(
|
|
542
|
+
`Tools for ${agent.name} (comma-separated)`,
|
|
543
|
+
"bash,read,write,edit",
|
|
544
|
+
);
|
|
545
|
+
if (toolList) {
|
|
546
|
+
agent.tools = toolList.split(",").map((t) => t.trim()).filter(Boolean);
|
|
547
|
+
store.saveAgentDef(agent);
|
|
548
|
+
ctx.ui.notify(`${agent.name}: tools = [${agent.tools.join(", ")}]`, "info");
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// /squad agents <name> — show specific agent
|
|
556
|
+
const agent = store.loadAgentDef(agentArg, ctx.cwd);
|
|
557
|
+
if (agent) {
|
|
558
|
+
const status = agent.disabled ? " ✗ DISABLED" : "";
|
|
559
|
+
const details = [
|
|
560
|
+
`${agent.name} — ${agent.role}${status}`,
|
|
561
|
+
`${agent.description}`,
|
|
562
|
+
`Model: ${agent.model || "(default)"}`,
|
|
563
|
+
`Thinking: ${agent.thinking || "(default)"}`,
|
|
564
|
+
`Tools: ${agent.tools ? agent.tools.join(", ") : "(all)"}`,
|
|
565
|
+
`Tags: ${agent.tags.join(", ")}`,
|
|
566
|
+
].join("\n");
|
|
567
|
+
ctx.ui.notify(details, "info");
|
|
568
|
+
} else {
|
|
569
|
+
ctx.ui.notify(`Agent '${agentArg}' not found`, "warning");
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
default:
|
|
575
|
+
// Treat as a squad ID — try to activate it directly
|
|
576
|
+
const direct = store.loadSquad(sub);
|
|
577
|
+
if (direct) {
|
|
578
|
+
activateSquadView(direct.id, ctx);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
ctx.ui.notify(`Unknown: /squad ${sub}. Try: list, all, select, resume, agents, defaults, msg, widget, panel, cancel, clear, cleanup`, "warning");
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
}
|