pi-squad 0.6.5 → 0.14.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/README.md +200 -318
- package/package.json +21 -3
- package/src/advisor.ts +145 -0
- package/src/agent-pool.ts +39 -15
- package/src/agents/_defaults/architect.json +8 -1
- package/src/agents/_defaults/backend.json +9 -1
- package/src/agents/_defaults/debugger.json +8 -1
- package/src/agents/_defaults/devops.json +9 -1
- package/src/agents/_defaults/docs.json +8 -1
- package/src/agents/_defaults/frontend.json +10 -1
- package/src/agents/_defaults/fullstack.json +8 -1
- package/src/agents/_defaults/planner.json +7 -1
- package/src/agents/_defaults/qa.json +8 -1
- package/src/agents/_defaults/researcher.json +8 -1
- package/src/agents/_defaults/reviewer.json +10 -0
- package/src/agents/_defaults/security.json +8 -1
- package/src/index.ts +375 -212
- package/src/monitor.ts +20 -7
- package/src/panel/message-view.ts +2 -2
- package/src/panel/squad-panel.ts +3 -3
- package/src/panel/squad-widget.ts +3 -3
- package/src/panel/task-list.ts +2 -2
- package/src/plan-rules.ts +134 -0
- package/src/planner.ts +18 -14
- package/src/protocol.ts +11 -20
- package/src/scheduler.ts +172 -95
- package/src/skills/squad-architecture/SKILL.md +53 -0
- package/src/skills/squad-backend-dev/SKILL.md +45 -0
- package/src/skills/squad-code-review/SKILL.md +89 -0
- package/src/skills/{collaboration → squad-collaboration}/SKILL.md +6 -2
- package/src/skills/squad-debugging/SKILL.md +61 -0
- package/src/skills/squad-frontend-dev/SKILL.md +51 -0
- package/src/skills/squad-protocol/SKILL.md +19 -0
- package/src/skills/squad-qa-testing/SKILL.md +72 -0
- package/src/skills/squad-security-audit/SKILL.md +43 -0
- package/src/skills/squad-supervisor/SKILL.md +85 -11
- package/src/skills/{verification → squad-verification}/SKILL.md +1 -1
- package/src/store.ts +24 -26
- package/src/types.ts +48 -1
package/src/index.ts
CHANGED
|
@@ -12,12 +12,15 @@
|
|
|
12
12
|
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
|
-
import { Type } from "
|
|
16
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
15
|
+
import { Type } from "typebox";
|
|
16
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import type { Squad, Task, SquadConfig, PlannerOutput } from "./types.js";
|
|
18
|
-
import { DEFAULT_SQUAD_CONFIG } from "./types.js";
|
|
19
|
-
import { Scheduler, type SchedulerEvent } from "./scheduler.js";
|
|
18
|
+
import { DEFAULT_SQUAD_CONFIG, THINKING_LEVELS } from "./types.js";
|
|
19
|
+
import { Scheduler, type SchedulerEvent, type SchedulerSpawnContext } from "./scheduler.js";
|
|
20
20
|
import { runPlanner } from "./planner.js";
|
|
21
|
+
import { validatePlan, PLAN_STRUCTURE_RULES } from "./plan-rules.js";
|
|
22
|
+
import { ADVISOR_SYSTEM_PROMPT, buildAdvisorConsultText, type AdvisorConsultInput } from "./advisor.js";
|
|
23
|
+
import { completeSimple, type Message, type TextContent } from "@earendil-works/pi-ai";
|
|
21
24
|
import { SquadPanel, type SquadPanelResult } from "./panel/squad-panel.js";
|
|
22
25
|
import { setupSquadWidget, type SquadWidgetState } from "./panel/squad-widget.js";
|
|
23
26
|
import * as store from "./store.js";
|
|
@@ -36,11 +39,143 @@ let activeSquadId: string | null = null;
|
|
|
36
39
|
/** Whether an overlay panel is currently open (prevents double-open) */
|
|
37
40
|
let overlayOpen = false;
|
|
38
41
|
/** Stored ExtensionContext for widget updates from background scheduler events */
|
|
39
|
-
let uiCtx: import("@
|
|
42
|
+
let uiCtx: import("@earendil-works/pi-coding-agent").ExtensionContext | null = null;
|
|
40
43
|
/** Component-based widget state + controls */
|
|
41
44
|
const widgetState: SquadWidgetState = { squadId: null, enabled: true };
|
|
42
45
|
let widgetControls: { requestUpdate: () => void; dispose: () => void } | null = null;
|
|
43
46
|
|
|
47
|
+
/** Reviewer instructions appended to squad-completed notifications —
|
|
48
|
+
* makes the main session behave like the QA/verification agents do. */
|
|
49
|
+
const REVIEW_INSTRUCTIONS =
|
|
50
|
+
"Before reporting success to the user, REVIEW the work like a QA agent would: " +
|
|
51
|
+
"(1) check task outputs for QA verdicts (## Verdict: PASS/FAIL) and verification evidence (commands + results); " +
|
|
52
|
+
"(2) if a task claimed done without evidence, run its Verify command yourself; " +
|
|
53
|
+
"(3) report to the user what was verified (with evidence) and flag anything unverified — don't just relay the summary.";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a model string (or null = session default) to its context window.
|
|
57
|
+
* Reads uiCtx lazily so it always uses the live session's registry.
|
|
58
|
+
*/
|
|
59
|
+
function resolveContextWindow(model: string | null): number | undefined {
|
|
60
|
+
const ctx = uiCtx;
|
|
61
|
+
if (!ctx) return undefined;
|
|
62
|
+
try {
|
|
63
|
+
if (!model) return ctx.model?.contextWindow;
|
|
64
|
+
// Strip a :<thinking> suffix if present
|
|
65
|
+
let clean = model;
|
|
66
|
+
const lastColon = model.lastIndexOf(":");
|
|
67
|
+
if (lastColon > 0 && (THINKING_LEVELS as readonly string[]).includes(model.slice(lastColon + 1))) {
|
|
68
|
+
clean = model.slice(0, lastColon);
|
|
69
|
+
}
|
|
70
|
+
const all = ctx.modelRegistry.getAll();
|
|
71
|
+
const slash = clean.indexOf("/");
|
|
72
|
+
if (slash > 0) {
|
|
73
|
+
const provider = clean.slice(0, slash);
|
|
74
|
+
const id = clean.slice(slash + 1);
|
|
75
|
+
const m = all.find((x) => x.provider === provider && x.id === id);
|
|
76
|
+
if (m) return m.contextWindow;
|
|
77
|
+
}
|
|
78
|
+
return all.find((x) => x.id === clean)?.contextWindow;
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Main session's current model as "provider/id", if known */
|
|
85
|
+
function getMainSessionModel(): string | undefined {
|
|
86
|
+
try {
|
|
87
|
+
const m = uiCtx?.model;
|
|
88
|
+
return m ? `${m.provider}/${m.id}` : undefined;
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Main session's current thinking level (set inside the extension entry, needs `pi`) */
|
|
95
|
+
let getMainSessionThinking: () => string | undefined = () => undefined;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the effective squad defaults from ~/.pi/squad/settings.json.
|
|
99
|
+
* "main" follows the live main session; "pi-default" leaves values unset
|
|
100
|
+
* (child pi resolves its own default); anything else is an explicit value.
|
|
101
|
+
*/
|
|
102
|
+
function resolveSquadDefaults(): { model?: string; thinking?: string } {
|
|
103
|
+
const settings = store.loadSquadSettings();
|
|
104
|
+
let model: string | undefined;
|
|
105
|
+
if (settings.defaultModel === "main") model = getMainSessionModel();
|
|
106
|
+
else if (settings.defaultModel !== "pi-default") model = settings.defaultModel;
|
|
107
|
+
let thinking: string | undefined;
|
|
108
|
+
if (settings.defaultThinking === "main") thinking = getMainSessionThinking();
|
|
109
|
+
else if (settings.defaultThinking !== "pi-default") thinking = settings.defaultThinking;
|
|
110
|
+
return { model, thinking };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Consult the advisor model in-process via pi-ai (no subprocess).
|
|
115
|
+
* Returns advice text or null when disabled/unresolvable.
|
|
116
|
+
*/
|
|
117
|
+
async function consultAdvisor(input: AdvisorConsultInput): Promise<string | null> {
|
|
118
|
+
const ctx = uiCtx;
|
|
119
|
+
if (!ctx) return null;
|
|
120
|
+
const settings = store.loadSquadSettings();
|
|
121
|
+
if (!settings.advisor.enabled) return null;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
// Resolve advisor model: "main" = the main session's live model object
|
|
125
|
+
let model = settings.advisor.model === "main" ? ctx.model : undefined;
|
|
126
|
+
if (!model && settings.advisor.model !== "main") {
|
|
127
|
+
const ref = settings.advisor.model;
|
|
128
|
+
const slash = ref.indexOf("/");
|
|
129
|
+
if (slash > 0) model = ctx.modelRegistry.find(ref.slice(0, slash), ref.slice(slash + 1));
|
|
130
|
+
}
|
|
131
|
+
if (!model) {
|
|
132
|
+
logError("squad-advisor", `advisor model "${settings.advisor.model}" not resolvable`);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
137
|
+
if (!auth.ok || !auth.apiKey) {
|
|
138
|
+
logError("squad-advisor", `no auth for advisor model ${model.provider}/${model.id}`);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const userMessage: Message = {
|
|
143
|
+
role: "user",
|
|
144
|
+
content: [{ type: "text", text: buildAdvisorConsultText(input) }],
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
} as Message;
|
|
147
|
+
|
|
148
|
+
const response = await completeSimple(
|
|
149
|
+
model,
|
|
150
|
+
{ systemPrompt: ADVISOR_SYSTEM_PROMPT, messages: [userMessage] },
|
|
151
|
+
{
|
|
152
|
+
apiKey: auth.apiKey,
|
|
153
|
+
headers: auth.headers,
|
|
154
|
+
maxTokens: settings.advisor.maxTokens,
|
|
155
|
+
reasoning: settings.advisor.reasoning as never,
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const text = response.content
|
|
160
|
+
.filter((b): b is TextContent => b.type === "text")
|
|
161
|
+
.map((b) => b.text)
|
|
162
|
+
.join("\n")
|
|
163
|
+
.trim();
|
|
164
|
+
debug("squad-advisor", `consulted ${model.provider}/${model.id} for ${input.taskId}: in=${response.usage?.input ?? 0} out=${response.usage?.output ?? 0}`);
|
|
165
|
+
return text || null;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
logError("squad-advisor", `consult failed: ${(error as Error).message}`);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Spawn context shared by all Scheduler instances */
|
|
173
|
+
const schedulerSpawnContext: SchedulerSpawnContext = {
|
|
174
|
+
resolveContextWindow,
|
|
175
|
+
getDefaultModelThinking: resolveSquadDefaults,
|
|
176
|
+
consultAdvisor,
|
|
177
|
+
};
|
|
178
|
+
|
|
44
179
|
/** Get the active scheduler (for the focused squad) */
|
|
45
180
|
function getActiveScheduler(): Scheduler | null {
|
|
46
181
|
if (!activeSquadId) return null;
|
|
@@ -56,6 +191,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
56
191
|
// Don't load in child agent processes (prevent recursive squad-in-squad)
|
|
57
192
|
if (process.env.PI_SQUAD_CHILD === "1") return;
|
|
58
193
|
|
|
194
|
+
// Wire main-session thinking lookup (needs `pi`, guarded against stale API)
|
|
195
|
+
getMainSessionThinking = () => {
|
|
196
|
+
try {
|
|
197
|
+
return pi.getThinkingLevel();
|
|
198
|
+
} catch {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
59
203
|
// Bootstrap default agents on first load
|
|
60
204
|
const defaultsDir = path.join(path.dirname(new URL(import.meta.url).pathname), "agents", "_defaults");
|
|
61
205
|
store.bootstrapAgents(defaultsDir);
|
|
@@ -97,6 +241,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
97
241
|
taskLines,
|
|
98
242
|
`</squad_status>`,
|
|
99
243
|
`You have an active squad. Use squad_message to talk to agents, squad_status for details, squad_modify to change tasks.`,
|
|
244
|
+
`Do NOT poll squad_status in a loop or sleep-wait — the squad wakes you automatically on completion, failure, or escalation. Keep helping the user with other work, or end your turn and stay idle.`,
|
|
100
245
|
].join("\n");
|
|
101
246
|
|
|
102
247
|
return {
|
|
@@ -115,6 +260,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
115
260
|
`The squad tool decomposes work into tasks, assigns specialist agents, and runs them in parallel.`,
|
|
116
261
|
`When in doubt about whether a task is complex enough, prefer using squad — it handles the coordination for you.`,
|
|
117
262
|
allAgents.length > 0 ? `Available agents: ${agentList}. When providing tasks, the "agent" field must be one of these names.` : ``,
|
|
263
|
+
`When you provide tasks yourself, you take the planner's role — follow its rules: contract/design task first for shared interfaces, final QA task for user-facing changes, 3-7 tasks, first task(s) with empty depends.`,
|
|
264
|
+
`Structure descriptions as: Goal (outcome first), Context (files to read), Output (deliverable), Boundaries (what must not change), Verify (proving command).`,
|
|
118
265
|
`</squad_hint>`,
|
|
119
266
|
].filter(Boolean).join("\n");
|
|
120
267
|
|
|
@@ -139,16 +286,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
139
286
|
"Do NOT use for simple single-file changes, quick bug fixes, or tasks a single agent can handle in a few minutes.",
|
|
140
287
|
"When in doubt about complexity, use squad — it's better to parallelize than to do everything sequentially.",
|
|
141
288
|
"Non-blocking: returns immediately with the plan while agents work in background.",
|
|
289
|
+
"If you provide tasks yourself (skipping the planner agent), follow the same rules the planner follows:",
|
|
290
|
+
PLAN_STRUCTURE_RULES.replace(/\n- /g, " ").replace(/^- /, ""),
|
|
291
|
+
"Plans are validated on submission — structural errors are rejected, rule violations come back as warnings.",
|
|
142
292
|
].join(" "),
|
|
293
|
+
promptSnippet: "squad({ goal, tasks?, agents? }): decompose complex work into parallel specialist agents → non-blocking, monitor via squad_status",
|
|
294
|
+
promptGuidelines: [
|
|
295
|
+
"Use squad when work spans 2+ concerns (backend+frontend+tests+docs) or has natural parallelism",
|
|
296
|
+
"Skip squad for single-file changes, quick fixes, or anything one agent finishes in minutes",
|
|
297
|
+
"Providing tasks yourself makes you the planner — follow the planner rules (contract task first, final QA task, 3-7 tasks)",
|
|
298
|
+
"Act on ⚠️ plan warnings in the response — fix with squad_modify or address at review",
|
|
299
|
+
"After starting a squad: report the plan and END YOUR TURN — never poll squad_status or sleep-wait; squad events wake you automatically",
|
|
300
|
+
"When the squad completes, review evidence like a QA agent before reporting success",
|
|
301
|
+
],
|
|
143
302
|
parameters: Type.Object({
|
|
144
303
|
goal: Type.String({ description: "What the squad should accomplish" }),
|
|
145
304
|
agents: Type.Optional(
|
|
146
305
|
Type.Record(
|
|
147
306
|
Type.String(),
|
|
148
307
|
Type.Object({
|
|
149
|
-
model: Type.Optional(Type.String()),
|
|
308
|
+
model: Type.Optional(Type.String({ description: "Model override (e.g. 'github-copilot/claude-sonnet-5')" })),
|
|
309
|
+
thinking: Type.Optional(Type.String({ description: "Thinking level: off, minimal, low, medium, high, xhigh, max" })),
|
|
150
310
|
}),
|
|
151
|
-
{ description: "Agent roster with optional model overrides. Keys must match agent names in .pi/squad/agents/" },
|
|
311
|
+
{ description: "Agent roster with optional model/thinking overrides. Keys must match agent names in .pi/squad/agents/" },
|
|
152
312
|
),
|
|
153
313
|
),
|
|
154
314
|
tasks: Type.Optional(
|
|
@@ -156,11 +316,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
156
316
|
Type.Object({
|
|
157
317
|
id: Type.String(),
|
|
158
318
|
title: Type.String(),
|
|
159
|
-
description: Type.Optional(Type.String()),
|
|
319
|
+
description: Type.Optional(Type.String({ description: "Structure as: Goal (outcome first, not steps), Context (files/contracts to read), Output (deliverable), Boundaries (what must NOT change), Verify (command that proves it works). Include only the parts that help." })),
|
|
160
320
|
agent: Type.String(),
|
|
161
321
|
depends: Type.Optional(Type.Array(Type.String())),
|
|
322
|
+
inheritContext: Type.Optional(Type.Boolean({ description: "Fork the current pi session so the agent inherits this conversation's full context. Use ONLY when the task depends on decisions/details discussed here that can't be restated briefly. Costly (agent pays the whole history as input each turn) and auto-skipped when the session exceeds 50% of the agent model's context window — prefer restating key context in the description." })),
|
|
162
323
|
}),
|
|
163
|
-
{ description: "Pre-defined task breakdown. If provided, skips the planner agent." },
|
|
324
|
+
{ description: "Pre-defined task breakdown. If provided, skips the planner agent. Scope tasks to required work only — no optional polish." },
|
|
164
325
|
),
|
|
165
326
|
),
|
|
166
327
|
config: Type.Optional(
|
|
@@ -171,21 +332,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
171
332
|
}),
|
|
172
333
|
|
|
173
334
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
174
|
-
if (!squadEnabled) return { content: [{ type: "text" as const, text: "Squad is disabled. Use /squad enable to re-enable." }] };
|
|
335
|
+
if (!squadEnabled) return { content: [{ type: "text" as const, text: "Squad is disabled. Use /squad enable to re-enable." }], details: undefined };
|
|
175
336
|
if (!uiCtx) uiCtx = ctx;
|
|
176
337
|
|
|
177
338
|
// Check if the user cancelled before we start
|
|
178
|
-
if (signal?.aborted) return { content: [{ type: "text" as const, text: "Cancelled." }] };
|
|
339
|
+
if (signal?.aborted) return { content: [{ type: "text" as const, text: "Cancelled." }], details: undefined };
|
|
179
340
|
|
|
180
341
|
// Multiple squads can run concurrently — no guard needed
|
|
181
342
|
|
|
343
|
+
// Resolve to absolute: the fork happens later from a child process whose
|
|
344
|
+
// cwd may differ (e.g. when a relative --session-dir was used).
|
|
345
|
+
const rawSessionFile = ctx.sessionManager.getSessionFile();
|
|
346
|
+
const sessionFile = rawSessionFile ? path.resolve(rawSessionFile) : null;
|
|
182
347
|
const squadId = store.makeTaskId(params.goal);
|
|
183
348
|
if (store.squadExists(squadId)) {
|
|
184
349
|
const uniqueId = `${squadId}-${Date.now().toString(36)}`;
|
|
185
|
-
return await startSquad(uniqueId, params, ctx.cwd, squadSkillPaths, pi);
|
|
350
|
+
return await startSquad(uniqueId, params, ctx.cwd, squadSkillPaths, pi, sessionFile);
|
|
186
351
|
}
|
|
187
352
|
|
|
188
|
-
return await startSquad(squadId, params, ctx.cwd, squadSkillPaths, pi);
|
|
353
|
+
return await startSquad(squadId, params, ctx.cwd, squadSkillPaths, pi, sessionFile);
|
|
189
354
|
},
|
|
190
355
|
});
|
|
191
356
|
|
|
@@ -196,7 +361,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
196
361
|
pi.registerTool({
|
|
197
362
|
name: "squad_status",
|
|
198
363
|
label: "Squad Status",
|
|
199
|
-
description: "Check current squad status, task progress, and recent activity.",
|
|
364
|
+
description: "Check current squad status, task progress, and recent activity. Do NOT call this in a loop or after sleep-waits — squad completion/failure/escalations wake you automatically. Use only when the user asks for a status update or after being woken by a squad event.",
|
|
200
365
|
parameters: Type.Object({
|
|
201
366
|
squadId: Type.Optional(Type.String({ description: "Specific squad ID (default: most recent)" })),
|
|
202
367
|
}),
|
|
@@ -211,7 +376,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
211
376
|
}
|
|
212
377
|
|
|
213
378
|
if (!id) {
|
|
214
|
-
return { content: [{ type: "text" as const, text: "No squads found. Use the squad tool to start one." }] };
|
|
379
|
+
return { content: [{ type: "text" as const, text: "No squads found. Use the squad tool to start one." }], details: undefined };
|
|
215
380
|
}
|
|
216
381
|
|
|
217
382
|
// If scheduler is running, force a context refresh
|
|
@@ -220,7 +385,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
220
385
|
|
|
221
386
|
const context = store.loadContext(id);
|
|
222
387
|
if (!context) {
|
|
223
|
-
return { content: [{ type: "text" as const, text: `Squad '${id}' not found or has no context yet.` }] };
|
|
388
|
+
return { content: [{ type: "text" as const, text: `Squad '${id}' not found or has no context yet.` }], details: undefined };
|
|
224
389
|
}
|
|
225
390
|
|
|
226
391
|
const taskLines = Object.entries(context.tasks)
|
|
@@ -247,7 +412,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
247
412
|
taskLines,
|
|
248
413
|
].join("\n");
|
|
249
414
|
|
|
250
|
-
return { content: [{ type: "text" as const, text: summary }] };
|
|
415
|
+
return { content: [{ type: "text" as const, text: summary }], details: undefined };
|
|
251
416
|
},
|
|
252
417
|
});
|
|
253
418
|
|
|
@@ -268,7 +433,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
268
433
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
269
434
|
const activeScheduler = getActiveScheduler();
|
|
270
435
|
if (!activeScheduler || !activeSquadId) {
|
|
271
|
-
return { content: [{ type: "text" as const, text: "No active squad." }] };
|
|
436
|
+
return { content: [{ type: "text" as const, text: "No active squad." }], details: undefined };
|
|
272
437
|
}
|
|
273
438
|
|
|
274
439
|
let taskId = params.taskId;
|
|
@@ -279,13 +444,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
279
444
|
}
|
|
280
445
|
|
|
281
446
|
if (!taskId) {
|
|
282
|
-
return { content: [{ type: "text" as const, text: "Could not determine target task. Provide taskId or an agent name that is currently running." }] };
|
|
447
|
+
return { content: [{ type: "text" as const, text: "Could not determine target task. Provide taskId or an agent name that is currently running." }], details: undefined };
|
|
283
448
|
}
|
|
284
449
|
|
|
285
450
|
const sent = await activeScheduler!.sendHumanMessage(taskId, params.message);
|
|
286
451
|
const status = sent ? "delivered" : "queued for when the agent starts";
|
|
287
452
|
|
|
288
|
-
return { content: [{ type: "text" as const, text: `Message ${status}: "${params.message}"` }] };
|
|
453
|
+
return { content: [{ type: "text" as const, text: `Message ${status}: "${params.message}"` }], details: undefined };
|
|
289
454
|
},
|
|
290
455
|
});
|
|
291
456
|
|
|
@@ -318,8 +483,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
318
483
|
description: Type.Optional(Type.String()),
|
|
319
484
|
agent: Type.String(),
|
|
320
485
|
depends: Type.Optional(Type.Array(Type.String())),
|
|
321
|
-
|
|
322
|
-
{ description: "Task definition for add_task" },
|
|
486
|
+
inheritContext: Type.Optional(Type.Boolean({ description: "Fork the current pi session so the agent inherits this conversation's context (see squad tool docs for caveats)" })),
|
|
487
|
+
}, { description: "Task definition for add_task" }),
|
|
323
488
|
),
|
|
324
489
|
}),
|
|
325
490
|
|
|
@@ -332,12 +497,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
332
497
|
.sort((a, b) => b.created.localeCompare(a.created))[0]?.id;
|
|
333
498
|
|
|
334
499
|
if (!squadId) {
|
|
335
|
-
return { content: [{ type: "text" as const, text: "No paused squad found to resume." }] };
|
|
500
|
+
return { content: [{ type: "text" as const, text: "No paused squad found to resume." }], details: undefined };
|
|
336
501
|
}
|
|
337
502
|
|
|
338
503
|
// Create a fresh scheduler if needed
|
|
339
504
|
if (!schedulers.has(squadId)) {
|
|
340
|
-
const scheduler = new Scheduler(squadId, squadSkillPaths);
|
|
505
|
+
const scheduler = new Scheduler(squadId, squadSkillPaths, schedulerSpawnContext);
|
|
341
506
|
schedulers.set(squadId, scheduler);
|
|
342
507
|
activeSquadId = squadId;
|
|
343
508
|
|
|
@@ -352,16 +517,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
352
517
|
switch (event.type) {
|
|
353
518
|
case "squad_completed": {
|
|
354
519
|
const tasks = store.loadAllTasks(squadId);
|
|
520
|
+
const summary = tasks
|
|
521
|
+
.filter((t) => t.status === "done")
|
|
522
|
+
.map((t) => `- ${t.id} (${t.agent}): ${t.output?.slice(0, 150) || "done"}`)
|
|
523
|
+
.join("\n");
|
|
355
524
|
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
356
525
|
const s = schedulers.get(squadId); if (s) s.updateContext();
|
|
357
|
-
|
|
526
|
+
// followUp + triggerTurn: wake an idle main agent to review;
|
|
527
|
+
// if it's mid-conversation with the human, deliver after it finishes
|
|
358
528
|
pi.sendMessage({
|
|
359
529
|
customType: "squad-completed",
|
|
360
|
-
content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\n
|
|
361
|
-
(overview ? `## Squad Overview\n\n${overview}\n\n` : "") +
|
|
362
|
-
`Total cost: $${totalCost.toFixed(4)}`,
|
|
530
|
+
content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\nSummary:\n${summary}\n\nTotal cost: $${totalCost.toFixed(4)}\n\n${REVIEW_INSTRUCTIONS}`,
|
|
363
531
|
display: true,
|
|
364
|
-
});
|
|
532
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
365
533
|
schedulers.delete(squadId);
|
|
366
534
|
forceWidgetUpdate();
|
|
367
535
|
break;
|
|
@@ -370,14 +538,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
370
538
|
const tasks = store.loadAllTasks(squadId);
|
|
371
539
|
const failed = tasks.filter((t) => t.status === "failed");
|
|
372
540
|
const done = tasks.filter((t) => t.status === "done");
|
|
373
|
-
const overview = store.loadOverview(squadId);
|
|
374
541
|
pi.sendMessage({
|
|
375
542
|
customType: "squad-failed",
|
|
376
|
-
content: `[squad] Squad "${squadId}" has stalled. ${done.length}/${tasks.length} done, ${failed.length} failed.\
|
|
377
|
-
`Failed: ${failed.map((t) => `${t.id}: ${t.error?.slice(0, 100)}`).join("; ")}` +
|
|
378
|
-
(overview ? `\n\n## Squad Overview\n\n${overview}` : ""),
|
|
543
|
+
content: `[squad] Squad "${squadId}" has stalled. ${done.length}/${tasks.length} done, ${failed.length} failed.\nFailed: ${failed.map((t) => `${t.id}: ${t.error?.slice(0, 100)}`).join("; ")}`,
|
|
379
544
|
display: true,
|
|
380
|
-
}, { triggerTurn: true });
|
|
545
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
381
546
|
forceWidgetUpdate();
|
|
382
547
|
break;
|
|
383
548
|
}
|
|
@@ -400,17 +565,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
400
565
|
|
|
401
566
|
const tasks = store.loadAllTasks(squadId);
|
|
402
567
|
const done = tasks.filter(t => t.status === "done").length;
|
|
403
|
-
return { content: [{ type: "text" as const, text: `Squad "${squadId}" resumed (${done}/${tasks.length} done). Agents restarting in background.` }] };
|
|
568
|
+
return { content: [{ type: "text" as const, text: `Squad "${squadId}" resumed (${done}/${tasks.length} done). Agents restarting in background.` }], details: undefined };
|
|
404
569
|
}
|
|
405
570
|
|
|
571
|
+
const activeScheduler = getActiveScheduler();
|
|
406
572
|
if (!activeScheduler || !activeSquadId) {
|
|
407
|
-
return { content: [{ type: "text" as const, text: "No active squad. Use squad_modify with action 'resume' to resume a paused squad, or start a new one with the squad tool." }] };
|
|
573
|
+
return { content: [{ type: "text" as const, text: "No active squad. Use squad_modify with action 'resume' to resume a paused squad, or start a new one with the squad tool." }], details: undefined };
|
|
408
574
|
}
|
|
409
575
|
|
|
410
576
|
switch (params.action) {
|
|
411
577
|
case "add_task": {
|
|
412
578
|
if (!params.task) {
|
|
413
|
-
return { content: [{ type: "text" as const, text: "Provide a task definition for add_task." }] };
|
|
579
|
+
return { content: [{ type: "text" as const, text: "Provide a task definition for add_task." }], details: undefined };
|
|
580
|
+
}
|
|
581
|
+
// Validate against the live squad: deps must exist, agent must exist
|
|
582
|
+
const existing = store.loadAllTasks(activeSquadId);
|
|
583
|
+
const existingIds = new Set(existing.map((t) => t.id));
|
|
584
|
+
if (existingIds.has(params.task.id)) {
|
|
585
|
+
return { content: [{ type: "text" as const, text: `Task id '${params.task.id}' already exists in this squad.` }], details: undefined };
|
|
586
|
+
}
|
|
587
|
+
const badDeps = (params.task.depends || []).filter((d) => !existingIds.has(d));
|
|
588
|
+
if (badDeps.length > 0) {
|
|
589
|
+
return { content: [{ type: "text" as const, text: `Unknown dependency task(s): ${badDeps.join(", ")}. Existing tasks: ${[...existingIds].join(", ")}` }], details: undefined };
|
|
590
|
+
}
|
|
591
|
+
if (!store.loadAgentDef(params.task.agent, ctx.cwd)) {
|
|
592
|
+
const available = store.loadAllAgentDefs(ctx.cwd).filter((a) => !a.disabled).map((a) => a.name).join(", ");
|
|
593
|
+
return { content: [{ type: "text" as const, text: `Unknown agent '${params.task.agent}'. Available: ${available}` }], details: undefined };
|
|
414
594
|
}
|
|
415
595
|
const task: Task = {
|
|
416
596
|
id: params.task.id,
|
|
@@ -419,6 +599,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
419
599
|
agent: params.task.agent,
|
|
420
600
|
status: "pending",
|
|
421
601
|
depends: params.task.depends || [],
|
|
602
|
+
...(params.task.inheritContext ? { inheritContext: true } : {}),
|
|
422
603
|
created: store.now(),
|
|
423
604
|
started: null,
|
|
424
605
|
completed: null,
|
|
@@ -428,27 +609,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
428
609
|
};
|
|
429
610
|
store.createTask(activeSquadId, task);
|
|
430
611
|
activeScheduler.updateContext();
|
|
431
|
-
return { content: [{ type: "text" as const, text: `Task '${task.id}' added.` }] };
|
|
612
|
+
return { content: [{ type: "text" as const, text: `Task '${task.id}' added.` }], details: undefined };
|
|
432
613
|
}
|
|
433
614
|
|
|
434
615
|
case "cancel_task": {
|
|
435
|
-
if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }] };
|
|
616
|
+
if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }], details: undefined };
|
|
436
617
|
await activeScheduler.cancelTask(params.taskId);
|
|
437
|
-
return { content: [{ type: "text" as const, text: `Task '${params.taskId}' cancelled.` }] };
|
|
618
|
+
return { content: [{ type: "text" as const, text: `Task '${params.taskId}' cancelled.` }], details: undefined };
|
|
438
619
|
}
|
|
439
620
|
|
|
440
621
|
case "pause_task": {
|
|
441
|
-
if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }] };
|
|
622
|
+
if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }], details: undefined };
|
|
442
623
|
await activeScheduler.pauseTask(params.taskId);
|
|
443
|
-
return { content: [{ type: "text" as const, text: `Task '${params.taskId}' paused.` }] };
|
|
624
|
+
return { content: [{ type: "text" as const, text: `Task '${params.taskId}' paused.` }], details: undefined };
|
|
444
625
|
}
|
|
445
626
|
|
|
446
627
|
case "resume_task": {
|
|
447
|
-
if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }] };
|
|
628
|
+
if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }], details: undefined };
|
|
448
629
|
activeScheduler.resumeTask(params.taskId).catch((err) => {
|
|
449
630
|
logError("squad", `Resume task error: ${(err as Error).message}`);
|
|
450
631
|
});
|
|
451
|
-
return { content: [{ type: "text" as const, text: `Task '${params.taskId}' resumed.` }] };
|
|
632
|
+
return { content: [{ type: "text" as const, text: `Task '${params.taskId}' resumed.` }], details: undefined };
|
|
452
633
|
}
|
|
453
634
|
|
|
454
635
|
case "pause": {
|
|
@@ -458,13 +639,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
458
639
|
store.saveSquad(squad);
|
|
459
640
|
}
|
|
460
641
|
await activeScheduler.stop();
|
|
461
|
-
return { content: [{ type: "text" as const, text: "Squad paused. Use squad_modify with action 'resume' to continue." }] };
|
|
642
|
+
return { content: [{ type: "text" as const, text: "Squad paused. Use squad_modify with action 'resume' to continue." }], details: undefined };
|
|
462
643
|
}
|
|
463
644
|
|
|
464
|
-
|
|
465
|
-
// Handled above (before the activeScheduler guard)
|
|
466
|
-
return { content: [{ type: "text" as const, text: "Squad resumed." }] };
|
|
467
|
-
}
|
|
645
|
+
// Note: "resume" is handled above, before the activeScheduler guard.
|
|
468
646
|
|
|
469
647
|
case "cancel": {
|
|
470
648
|
await activeScheduler.stop();
|
|
@@ -475,11 +653,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
475
653
|
}
|
|
476
654
|
schedulers.delete(activeSquadId);
|
|
477
655
|
activeSquadId = null;
|
|
478
|
-
return { content: [{ type: "text" as const, text: "Squad cancelled." }] };
|
|
656
|
+
return { content: [{ type: "text" as const, text: "Squad cancelled." }], details: undefined };
|
|
479
657
|
}
|
|
480
658
|
|
|
481
659
|
default:
|
|
482
|
-
return { content: [{ type: "text" as const, text: `Unknown action: ${params.action}` }] };
|
|
660
|
+
return { content: [{ type: "text" as const, text: `Unknown action: ${params.action}` }], details: undefined };
|
|
483
661
|
}
|
|
484
662
|
},
|
|
485
663
|
});
|
|
@@ -555,7 +733,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
555
733
|
}
|
|
556
734
|
|
|
557
735
|
if (activeSquadId) {
|
|
558
|
-
openPanel(ctx, schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths), activeSquadId);
|
|
736
|
+
openPanel(ctx, schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths, schedulerSpawnContext), activeSquadId);
|
|
559
737
|
}
|
|
560
738
|
return { consume: true };
|
|
561
739
|
}
|
|
@@ -587,6 +765,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
587
765
|
{ value: "all", label: "all", description: "List all squads, select to activate" },
|
|
588
766
|
{ value: "select", label: "select", description: "Pick a squad to view (interactive)" },
|
|
589
767
|
{ value: "agents", label: "agents", description: "List, view, or edit agent definitions" },
|
|
768
|
+
{ value: "defaults", label: "defaults", description: "Default model/thinking for agents (follow main session, pi default, or fixed)" },
|
|
769
|
+
{ value: "advisor", label: "advisor", description: "Advisor-first rescue for stuck agents (on/off, model, limits)" },
|
|
590
770
|
{ value: "msg", label: "msg", description: "Send message to agent: /squad msg [agent] text" },
|
|
591
771
|
{ value: "widget", label: "widget", description: "Toggle live widget" },
|
|
592
772
|
{ value: "panel", label: "panel", description: "Toggle overlay panel" },
|
|
@@ -679,7 +859,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
679
859
|
}
|
|
680
860
|
}
|
|
681
861
|
if (activeSquadId) {
|
|
682
|
-
const sched = schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths);
|
|
862
|
+
const sched = schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths, schedulerSpawnContext);
|
|
683
863
|
openPanel(ctx, sched, activeSquadId);
|
|
684
864
|
}
|
|
685
865
|
return;
|
|
@@ -886,6 +1066,92 @@ export default function (pi: ExtensionAPI) {
|
|
|
886
1066
|
return;
|
|
887
1067
|
}
|
|
888
1068
|
|
|
1069
|
+
case "defaults": {
|
|
1070
|
+
const settings = store.loadSquadSettings();
|
|
1071
|
+
const mainModel = getMainSessionModel() || "(unknown)";
|
|
1072
|
+
const mainThinking = getMainSessionThinking() || "(unknown)";
|
|
1073
|
+
const fmtPolicy = (v: string, live: string) =>
|
|
1074
|
+
v === "main" ? `follow main session (now: ${live})` : v === "pi-default" ? "pi default" : v;
|
|
1075
|
+
|
|
1076
|
+
const which = await ctx.ui.select(
|
|
1077
|
+
`Squad defaults — model: ${fmtPolicy(settings.defaultModel, mainModel)} | thinking: ${fmtPolicy(settings.defaultThinking, mainThinking)}`,
|
|
1078
|
+
["Change default model", "Change default thinking", "Cancel"],
|
|
1079
|
+
);
|
|
1080
|
+
if (!which || which === "Cancel") return;
|
|
1081
|
+
|
|
1082
|
+
if (which === "Change default model") {
|
|
1083
|
+
const choice = await ctx.ui.select("Default model for squad agents", [
|
|
1084
|
+
`Follow main session (now: ${mainModel})`,
|
|
1085
|
+
"pi default (child pi resolves its own)",
|
|
1086
|
+
"Custom model…",
|
|
1087
|
+
]);
|
|
1088
|
+
if (!choice) return;
|
|
1089
|
+
if (choice.startsWith("Follow")) settings.defaultModel = "main";
|
|
1090
|
+
else if (choice.startsWith("pi default")) settings.defaultModel = "pi-default";
|
|
1091
|
+
else {
|
|
1092
|
+
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);
|
|
1093
|
+
if (!custom || !custom.trim()) return;
|
|
1094
|
+
settings.defaultModel = custom.trim();
|
|
1095
|
+
}
|
|
1096
|
+
store.saveSquadSettings(settings);
|
|
1097
|
+
ctx.ui.notify(`Squad default model → ${fmtPolicy(settings.defaultModel, mainModel)}`, "info");
|
|
1098
|
+
} else {
|
|
1099
|
+
const choice = await ctx.ui.select("Default thinking for squad agents", [
|
|
1100
|
+
`Follow main session (now: ${mainThinking})`,
|
|
1101
|
+
"pi default (child pi resolves its own)",
|
|
1102
|
+
...THINKING_LEVELS,
|
|
1103
|
+
]);
|
|
1104
|
+
if (!choice) return;
|
|
1105
|
+
if (choice.startsWith("Follow")) settings.defaultThinking = "main";
|
|
1106
|
+
else if (choice.startsWith("pi default")) settings.defaultThinking = "pi-default";
|
|
1107
|
+
else settings.defaultThinking = choice;
|
|
1108
|
+
store.saveSquadSettings(settings);
|
|
1109
|
+
ctx.ui.notify(`Squad default thinking → ${fmtPolicy(settings.defaultThinking, mainThinking)}`, "info");
|
|
1110
|
+
}
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
case "advisor": {
|
|
1115
|
+
const settings = store.loadSquadSettings();
|
|
1116
|
+
const adv = settings.advisor;
|
|
1117
|
+
const mainModelLabel = getMainSessionModel() || "(unknown)";
|
|
1118
|
+
const modelLabel = adv.model === "main" ? `main session (now: ${mainModelLabel})` : adv.model;
|
|
1119
|
+
|
|
1120
|
+
const choice = await ctx.ui.select(
|
|
1121
|
+
`Squad advisor — ${adv.enabled ? "ON" : "OFF"} | model: ${modelLabel} | ${adv.maxCallsPerTask} calls/task, ${adv.reasoning} reasoning`,
|
|
1122
|
+
[adv.enabled ? "Disable advisor" : "Enable advisor", "Change advisor model", "Change max calls per task", "Change reasoning effort", "Cancel"],
|
|
1123
|
+
);
|
|
1124
|
+
if (!choice || choice === "Cancel") return;
|
|
1125
|
+
|
|
1126
|
+
if (choice.startsWith("Disable") || choice.startsWith("Enable")) {
|
|
1127
|
+
adv.enabled = !adv.enabled;
|
|
1128
|
+
ctx.ui.notify(`Squad advisor ${adv.enabled ? "enabled — stuck agents get a strong-model rescue before escalating" : "disabled — stuck agents escalate directly"}`, "info");
|
|
1129
|
+
} else if (choice === "Change advisor model") {
|
|
1130
|
+
const sel = await ctx.ui.select("Advisor model", [`Follow main session (now: ${mainModelLabel})`, "Custom model…"]);
|
|
1131
|
+
if (!sel) return;
|
|
1132
|
+
if (sel.startsWith("Follow")) adv.model = "main";
|
|
1133
|
+
else {
|
|
1134
|
+
const custom = await ctx.ui.input("Advisor model (provider/id)", adv.model === "main" ? "" : adv.model);
|
|
1135
|
+
if (!custom || !custom.trim()) return;
|
|
1136
|
+
adv.model = custom.trim();
|
|
1137
|
+
}
|
|
1138
|
+
ctx.ui.notify(`Advisor model → ${adv.model}`, "info");
|
|
1139
|
+
} else if (choice === "Change max calls per task") {
|
|
1140
|
+
const n = await ctx.ui.input("Max advisor calls per task", String(adv.maxCallsPerTask));
|
|
1141
|
+
const parsed = n ? Number.parseInt(n, 10) : NaN;
|
|
1142
|
+
if (!Number.isFinite(parsed) || parsed < 0) return;
|
|
1143
|
+
adv.maxCallsPerTask = parsed;
|
|
1144
|
+
ctx.ui.notify(`Advisor max calls/task → ${parsed}`, "info");
|
|
1145
|
+
} else {
|
|
1146
|
+
const lvl = await ctx.ui.select("Advisor reasoning effort", ["minimal", "low", "medium", "high", "xhigh"]);
|
|
1147
|
+
if (!lvl) return;
|
|
1148
|
+
adv.reasoning = lvl;
|
|
1149
|
+
ctx.ui.notify(`Advisor reasoning → ${lvl}`, "info");
|
|
1150
|
+
}
|
|
1151
|
+
store.saveSquadSettings(settings);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
889
1155
|
case "agents": {
|
|
890
1156
|
const agentArg = parts[1];
|
|
891
1157
|
const allAgents = store.loadAllAgentDefs(ctx.cwd);
|
|
@@ -897,7 +1163,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
897
1163
|
return;
|
|
898
1164
|
}
|
|
899
1165
|
const options = allAgents.map((a) => {
|
|
900
|
-
const model = a.model ? ` [${a.model}]` : " [default]";
|
|
1166
|
+
const model = a.model ? ` [${a.model}${a.thinking ? `:${a.thinking}` : ""}]` : a.thinking ? ` [default:${a.thinking}]` : " [default]";
|
|
901
1167
|
const status = a.disabled ? " ✗ disabled" : "";
|
|
902
1168
|
return `${a.name} — ${a.role}${model}${status}`;
|
|
903
1169
|
});
|
|
@@ -913,6 +1179,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
913
1179
|
"View details",
|
|
914
1180
|
"Edit in editor",
|
|
915
1181
|
"Change model",
|
|
1182
|
+
"Change thinking",
|
|
916
1183
|
"Toggle tools (restrict/unrestrict)",
|
|
917
1184
|
disableLabel,
|
|
918
1185
|
"Cancel",
|
|
@@ -926,6 +1193,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
926
1193
|
`Role: ${agent.role}`,
|
|
927
1194
|
`Description: ${agent.description}`,
|
|
928
1195
|
`Model: ${agent.model || "(default)"}`,
|
|
1196
|
+
`Thinking: ${agent.thinking || "(default)"}`,
|
|
929
1197
|
`Tools: ${agent.tools ? agent.tools.join(", ") : "(all)"}`,
|
|
930
1198
|
`Tags: ${agent.tags.join(", ")}`,
|
|
931
1199
|
``,
|
|
@@ -955,6 +1223,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
955
1223
|
store.saveAgentDef(agent);
|
|
956
1224
|
ctx.ui.notify(`${agent.name} model → ${agent.model || "(default)"}`, "info");
|
|
957
1225
|
}
|
|
1226
|
+
} else if (action === "Change thinking") {
|
|
1227
|
+
const levels = ["(default)", ...THINKING_LEVELS];
|
|
1228
|
+
const level = await ctx.ui.select(`Thinking level for ${agent.name}`, levels);
|
|
1229
|
+
if (level !== undefined) {
|
|
1230
|
+
agent.thinking = level === "(default)" ? null : level;
|
|
1231
|
+
store.saveAgentDef(agent);
|
|
1232
|
+
ctx.ui.notify(`${agent.name} thinking → ${agent.thinking || "(default)"}`, "info");
|
|
1233
|
+
}
|
|
958
1234
|
} else if (action === disableLabel) {
|
|
959
1235
|
agent.disabled = !agent.disabled;
|
|
960
1236
|
store.saveAgentDef(agent);
|
|
@@ -988,6 +1264,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
988
1264
|
`${agent.name} — ${agent.role}${status}`,
|
|
989
1265
|
`${agent.description}`,
|
|
990
1266
|
`Model: ${agent.model || "(default)"}`,
|
|
1267
|
+
`Thinking: ${agent.thinking || "(default)"}`,
|
|
991
1268
|
`Tools: ${agent.tools ? agent.tools.join(", ") : "(all)"}`,
|
|
992
1269
|
`Tags: ${agent.tags.join(", ")}`,
|
|
993
1270
|
].join("\n");
|
|
@@ -1005,7 +1282,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1005
1282
|
activateSquadView(direct.id, ctx);
|
|
1006
1283
|
return;
|
|
1007
1284
|
}
|
|
1008
|
-
ctx.ui.notify(`Unknown: /squad ${sub}. Try: list, all, select, widget, panel, cancel, clear`, "warning");
|
|
1285
|
+
ctx.ui.notify(`Unknown: /squad ${sub}. Try: list, all, select, agents, defaults, msg, widget, panel, cancel, clear, cleanup`, "warning");
|
|
1009
1286
|
}
|
|
1010
1287
|
},
|
|
1011
1288
|
});
|
|
@@ -1021,7 +1298,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1021
1298
|
* Returns the selected squad or undefined if cancelled.
|
|
1022
1299
|
*/
|
|
1023
1300
|
async function pickSquad(
|
|
1024
|
-
ctx: import("@
|
|
1301
|
+
ctx: import("@earendil-works/pi-coding-agent").ExtensionContext | import("@earendil-works/pi-coding-agent").ExtensionCommandContext,
|
|
1025
1302
|
squads: Squad[],
|
|
1026
1303
|
showProject = false,
|
|
1027
1304
|
): Promise<Squad | undefined> {
|
|
@@ -1048,7 +1325,7 @@ async function pickSquad(
|
|
|
1048
1325
|
* Sets activeSquadId, starts widget, shows notification.
|
|
1049
1326
|
* Does NOT start a scheduler (view-only unless squad needs resuming).
|
|
1050
1327
|
*/
|
|
1051
|
-
function activateSquadView(squadId: string, ctx: import("@
|
|
1328
|
+
function activateSquadView(squadId: string, ctx: import("@earendil-works/pi-coding-agent").ExtensionContext | import("@earendil-works/pi-coding-agent").ExtensionCommandContext): void {
|
|
1052
1329
|
const squad = store.loadSquad(squadId);
|
|
1053
1330
|
if (!squad) {
|
|
1054
1331
|
ctx.ui.notify(`Squad '${squadId}' not found`, "error");
|
|
@@ -1090,7 +1367,7 @@ function forceWidgetUpdate(): void {
|
|
|
1090
1367
|
* that resolves when done() is called. The panel calls done() on close.
|
|
1091
1368
|
*/
|
|
1092
1369
|
function openPanel(
|
|
1093
|
-
ctx: import("@
|
|
1370
|
+
ctx: import("@earendil-works/pi-coding-agent").ExtensionContext,
|
|
1094
1371
|
scheduler: Scheduler,
|
|
1095
1372
|
squadId: string,
|
|
1096
1373
|
): void {
|
|
@@ -1153,19 +1430,21 @@ async function startSquad(
|
|
|
1153
1430
|
squadId: string,
|
|
1154
1431
|
params: {
|
|
1155
1432
|
goal: string;
|
|
1156
|
-
agents?: Record<string, { model?: string }>;
|
|
1433
|
+
agents?: Record<string, { model?: string; thinking?: string }>;
|
|
1157
1434
|
tasks?: Array<{
|
|
1158
1435
|
id: string;
|
|
1159
1436
|
title: string;
|
|
1160
1437
|
description?: string;
|
|
1161
1438
|
agent: string;
|
|
1162
1439
|
depends?: string[];
|
|
1440
|
+
inheritContext?: boolean;
|
|
1163
1441
|
}>;
|
|
1164
1442
|
config?: { maxConcurrency?: number };
|
|
1165
1443
|
},
|
|
1166
1444
|
cwd: string,
|
|
1167
1445
|
skillPaths: string[],
|
|
1168
1446
|
pi: ExtensionAPI,
|
|
1447
|
+
sessionFile: string | null = null,
|
|
1169
1448
|
) {
|
|
1170
1449
|
let plan: PlannerOutput;
|
|
1171
1450
|
|
|
@@ -1190,27 +1469,33 @@ async function startSquad(
|
|
|
1190
1469
|
}
|
|
1191
1470
|
}
|
|
1192
1471
|
} else {
|
|
1193
|
-
// Run planner to generate task breakdown
|
|
1472
|
+
// Run planner to generate task breakdown (squad default policy as fallback)
|
|
1194
1473
|
try {
|
|
1195
|
-
|
|
1474
|
+
const defaults = resolveSquadDefaults();
|
|
1475
|
+
plan = await runPlanner({ goal: params.goal, cwd, fallbackModel: defaults.model, fallbackThinking: defaults.thinking });
|
|
1196
1476
|
} catch (error) {
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
{ type: "text" as const, text: `Failed to plan: ${(error as Error).message}` },
|
|
1200
|
-
],
|
|
1201
|
-
isError: true,
|
|
1202
|
-
};
|
|
1477
|
+
// Throwing marks the tool result as an error for the LLM (returning isError is ignored in current pi)
|
|
1478
|
+
throw new Error(`Failed to plan: ${(error as Error).message}`);
|
|
1203
1479
|
}
|
|
1204
1480
|
}
|
|
1205
1481
|
|
|
1206
1482
|
// Merge agent roster
|
|
1207
|
-
const agents: Record<string, { model?: string }> = { ...plan.agents };
|
|
1483
|
+
const agents: Record<string, { model?: string; thinking?: string }> = { ...plan.agents };
|
|
1208
1484
|
if (params.agents) {
|
|
1209
1485
|
for (const [name, entry] of Object.entries(params.agents)) {
|
|
1210
1486
|
agents[name] = { ...agents[name], ...entry };
|
|
1211
1487
|
}
|
|
1212
1488
|
}
|
|
1213
1489
|
|
|
1490
|
+
// Validate the plan — same enforcement for main-session and planner plans.
|
|
1491
|
+
// Errors block squad creation; warnings are reported back to the plan author.
|
|
1492
|
+
const validation = validatePlan(plan.tasks);
|
|
1493
|
+
if (validation.errors.length > 0) {
|
|
1494
|
+
throw new Error(
|
|
1495
|
+
`Plan rejected:\n- ${validation.errors.join("\n- ")}\n\nFix the task list and call squad again.`,
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1214
1499
|
// Create squad
|
|
1215
1500
|
const config: SquadConfig = {
|
|
1216
1501
|
...DEFAULT_SQUAD_CONFIG,
|
|
@@ -1223,37 +1508,13 @@ async function startSquad(
|
|
|
1223
1508
|
status: "running",
|
|
1224
1509
|
created: store.now(),
|
|
1225
1510
|
cwd,
|
|
1511
|
+
sessionFile,
|
|
1226
1512
|
agents,
|
|
1227
1513
|
config,
|
|
1228
1514
|
};
|
|
1229
1515
|
|
|
1230
1516
|
store.saveSquad(squad);
|
|
1231
1517
|
|
|
1232
|
-
// Create initial OVERVIEW.md with squad goal, task plan, and design contract
|
|
1233
|
-
const planLines = [
|
|
1234
|
-
`# Squad: ${params.goal}`,
|
|
1235
|
-
``,
|
|
1236
|
-
`**Created:** ${squad.created}`,
|
|
1237
|
-
`**Tasks:** ${plan.tasks.length}`,
|
|
1238
|
-
``,
|
|
1239
|
-
`## Task Plan`,
|
|
1240
|
-
``,
|
|
1241
|
-
...plan.tasks.map((t) => {
|
|
1242
|
-
const deps = t.depends.length > 0 ? ` (after: ${t.depends.join(", ")})` : "";
|
|
1243
|
-
return `- **${t.id}** → ${t.agent}: ${t.title}${deps}`;
|
|
1244
|
-
}),
|
|
1245
|
-
``,
|
|
1246
|
-
];
|
|
1247
|
-
|
|
1248
|
-
// Extract design contract from task descriptions — API paths, schemas,
|
|
1249
|
-
// ports, file conventions — so parallel agents share a single source of truth.
|
|
1250
|
-
const contractLines = buildDesignContract(plan.tasks, params.goal);
|
|
1251
|
-
if (contractLines.length > 0) {
|
|
1252
|
-
planLines.push(...contractLines);
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
store.appendOverview(squadId, planLines.join("\n"));
|
|
1256
|
-
|
|
1257
1518
|
// Create task files
|
|
1258
1519
|
for (const taskDef of plan.tasks) {
|
|
1259
1520
|
const task: Task = {
|
|
@@ -1263,6 +1524,7 @@ async function startSquad(
|
|
|
1263
1524
|
agent: taskDef.agent,
|
|
1264
1525
|
status: taskDef.depends.length === 0 ? "pending" : "blocked",
|
|
1265
1526
|
depends: taskDef.depends,
|
|
1527
|
+
...(taskDef.inheritContext ? { inheritContext: true } : {}),
|
|
1266
1528
|
created: store.now(),
|
|
1267
1529
|
started: null,
|
|
1268
1530
|
completed: null,
|
|
@@ -1270,22 +1532,14 @@ async function startSquad(
|
|
|
1270
1532
|
error: null,
|
|
1271
1533
|
usage: { inputTokens: 0, outputTokens: 0, cost: 0, turns: 0 },
|
|
1272
1534
|
};
|
|
1273
|
-
|
|
1274
|
-
//
|
|
1275
|
-
if (task.depends.length > 0) {
|
|
1276
|
-
const allDepsMet = task.depends.every((depId) =>
|
|
1277
|
-
plan.tasks.some((t) => t.id === depId),
|
|
1278
|
-
);
|
|
1279
|
-
if (!allDepsMet) {
|
|
1280
|
-
task.status = "pending"; // deps reference external tasks, treat as ready
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1535
|
+
// Note: unknown dependency references are hard validation errors above,
|
|
1536
|
+
// so blocked tasks here always have resolvable deps.
|
|
1283
1537
|
|
|
1284
1538
|
store.createTask(squadId, task);
|
|
1285
1539
|
}
|
|
1286
1540
|
|
|
1287
1541
|
// Start scheduler
|
|
1288
|
-
const scheduler = new Scheduler(squadId, skillPaths);
|
|
1542
|
+
const scheduler = new Scheduler(squadId, skillPaths, schedulerSpawnContext);
|
|
1289
1543
|
schedulers.set(squadId, scheduler);
|
|
1290
1544
|
activeSquadId = squadId;
|
|
1291
1545
|
|
|
@@ -1301,6 +1555,10 @@ async function startSquad(
|
|
|
1301
1555
|
switch (event.type) {
|
|
1302
1556
|
case "squad_completed": {
|
|
1303
1557
|
const tasks = store.loadAllTasks(squadId);
|
|
1558
|
+
const summary = tasks
|
|
1559
|
+
.filter((t) => t.status === "done")
|
|
1560
|
+
.map((t) => `- ${t.id} (${t.agent}): ${t.output?.slice(0, 150) || "done"}`)
|
|
1561
|
+
.join("\n");
|
|
1304
1562
|
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
1305
1563
|
|
|
1306
1564
|
// Final context update before clearing scheduler
|
|
@@ -1309,20 +1567,16 @@ async function startSquad(
|
|
|
1309
1567
|
completedSched.updateContext();
|
|
1310
1568
|
}
|
|
1311
1569
|
|
|
1312
|
-
//
|
|
1313
|
-
//
|
|
1314
|
-
const overview = store.loadOverview(squadId);
|
|
1315
|
-
|
|
1316
|
-
// Send into LLM context (not display-only) so the main agent
|
|
1317
|
-
// knows exactly what happened. No triggerTurn — user decides
|
|
1318
|
-
// what to do next.
|
|
1570
|
+
// followUp + triggerTurn: wake an idle main agent to review; if it's
|
|
1571
|
+
// mid-conversation with the human, deliver after it finishes
|
|
1319
1572
|
pi.sendMessage({
|
|
1320
1573
|
customType: "squad-completed",
|
|
1321
1574
|
content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\n` +
|
|
1322
|
-
|
|
1323
|
-
`Total cost: $${totalCost.toFixed(4)}
|
|
1575
|
+
`Summary:\n${summary}\n\n` +
|
|
1576
|
+
`Total cost: $${totalCost.toFixed(4)}\n\n` +
|
|
1577
|
+
REVIEW_INSTRUCTIONS,
|
|
1324
1578
|
display: true,
|
|
1325
|
-
});
|
|
1579
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
1326
1580
|
|
|
1327
1581
|
// Clear scheduler but keep activeSquadId so squad_status still works
|
|
1328
1582
|
schedulers.delete(squadId);
|
|
@@ -1334,17 +1588,15 @@ async function startSquad(
|
|
|
1334
1588
|
const tasks = store.loadAllTasks(squadId);
|
|
1335
1589
|
const failed = tasks.filter((t) => t.status === "failed");
|
|
1336
1590
|
const done = tasks.filter((t) => t.status === "done");
|
|
1337
|
-
const overview = store.loadOverview(squadId);
|
|
1338
1591
|
|
|
1339
1592
|
pi.sendMessage({
|
|
1340
1593
|
customType: "squad-failed",
|
|
1341
1594
|
content: `[squad] Squad "${squadId}" has stalled. ` +
|
|
1342
1595
|
`${done.length}/${tasks.length} tasks done, ${failed.length} failed.\n` +
|
|
1343
1596
|
`Failed: ${failed.map((t) => `${t.id}: ${t.error?.slice(0, 100)}`).join("; ")}\n` +
|
|
1344
|
-
(overview ? `\n## Squad Overview\n\n${overview}\n\n` : "\n") +
|
|
1345
1597
|
`Use squad_status for details or squad_modify to adjust.`,
|
|
1346
1598
|
display: true,
|
|
1347
|
-
}, { triggerTurn: true });
|
|
1599
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
1348
1600
|
forceWidgetUpdate();
|
|
1349
1601
|
break;
|
|
1350
1602
|
}
|
|
@@ -1384,9 +1636,14 @@ async function startSquad(
|
|
|
1384
1636
|
content: [
|
|
1385
1637
|
{
|
|
1386
1638
|
type: "text" as const,
|
|
1387
|
-
text: `Squad "${squadId}" started with ${plan.tasks.length} tasks.\n\n${taskSummary}
|
|
1639
|
+
text: `Squad "${squadId}" started with ${plan.tasks.length} tasks.\n\n${taskSummary}${
|
|
1640
|
+
validation.warnings.length > 0
|
|
1641
|
+
? `\n\n⚠️ Plan warnings (fix with squad_modify, or address at review):\n- ${validation.warnings.join("\n- ")}`
|
|
1642
|
+
: ""
|
|
1643
|
+
}\n\nAgents work in the background — you will be woken automatically when the squad completes, fails, or needs help. Report this plan to the user and END YOUR TURN now. Do NOT poll squad_status, do NOT sleep-wait, do NOT loop.`,
|
|
1388
1644
|
},
|
|
1389
1645
|
],
|
|
1646
|
+
details: undefined,
|
|
1390
1647
|
};
|
|
1391
1648
|
}
|
|
1392
1649
|
|
|
@@ -1403,98 +1660,4 @@ function getSquadSkillPaths(skillsDir: string): string[] {
|
|
|
1403
1660
|
.filter((dir) => fs.existsSync(path.join(dir, "SKILL.md")));
|
|
1404
1661
|
}
|
|
1405
1662
|
|
|
1406
|
-
/**
|
|
1407
|
-
* Extract a design contract from task descriptions.
|
|
1408
|
-
*
|
|
1409
|
-
* Scans all tasks for API paths, ports, file names, data schemas, and
|
|
1410
|
-
* shared conventions. Produces a "## Design Contract" section in the
|
|
1411
|
-
* OVERVIEW.md so that ALL agents (including parallel ones) share a
|
|
1412
|
-
* single source of truth before they start working.
|
|
1413
|
-
*/
|
|
1414
|
-
function buildDesignContract(
|
|
1415
|
-
tasks: Array<{ id: string; title: string; description: string; agent: string; depends: string[] }>,
|
|
1416
|
-
goal: string,
|
|
1417
|
-
): string[] {
|
|
1418
|
-
const lines: string[] = [];
|
|
1419
|
-
|
|
1420
|
-
// Collect API routes mentioned in any task description
|
|
1421
|
-
const apiRoutes: string[] = [];
|
|
1422
|
-
const ports: string[] = [];
|
|
1423
|
-
const files: string[] = [];
|
|
1424
|
-
const schemas: string[] = [];
|
|
1425
|
-
|
|
1426
|
-
const allText = tasks.map((t) => `${t.title}\n${t.description}`).join("\n") + "\n" + goal;
|
|
1427
|
-
|
|
1428
|
-
// Extract API paths like GET /foo, POST /bar/:id
|
|
1429
|
-
const routePattern = /\b(GET|POST|PUT|PATCH|DELETE|HEAD)\s+(\/[\w\/:]+)/gi;
|
|
1430
|
-
for (const match of allText.matchAll(routePattern)) {
|
|
1431
|
-
const route = `${match[1].toUpperCase()} ${match[2]}`;
|
|
1432
|
-
if (!apiRoutes.includes(route)) apiRoutes.push(route);
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
// Extract port numbers
|
|
1436
|
-
const portPattern = /\b(?:port|PORT|Port)\s*[=:]?\s*(\d{4,5})\b/gi;
|
|
1437
|
-
for (const match of allText.matchAll(portPattern)) {
|
|
1438
|
-
if (!ports.includes(match[1])) ports.push(match[1]);
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// Extract key file names mentioned (e.g., server.js, index.html)
|
|
1442
|
-
const filePattern = /\b([\w-]+\.(js|ts|json|html|css|sh|mjs|cjs))\b/gi;
|
|
1443
|
-
for (const match of allText.matchAll(filePattern)) {
|
|
1444
|
-
const f = match[1];
|
|
1445
|
-
if (!files.includes(f) && !['package.json'].includes(f)) files.push(f);
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
// Extract data schemas like {field, field, field}
|
|
1449
|
-
const schemaPattern = /\{([a-zA-Z_][\w,\s\[\]?]+)\}/g;
|
|
1450
|
-
for (const match of allText.matchAll(schemaPattern)) {
|
|
1451
|
-
const fields = match[1].trim();
|
|
1452
|
-
if (fields.includes(',') && fields.length > 5 && fields.length < 200) {
|
|
1453
|
-
if (!schemas.includes(fields)) schemas.push(fields);
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
// Only emit a contract section if we found shared design elements
|
|
1458
|
-
if (apiRoutes.length === 0 && ports.length === 0 && schemas.length === 0) {
|
|
1459
|
-
return [];
|
|
1460
|
-
}
|
|
1461
1663
|
|
|
1462
|
-
lines.push(`## Design Contract`);
|
|
1463
|
-
lines.push(``);
|
|
1464
|
-
lines.push(`> **All agents MUST follow these specifications.** Do not invent`);
|
|
1465
|
-
lines.push(`> alternative paths, ports, or schemas. If you need to deviate,`);
|
|
1466
|
-
lines.push(`> document the reason in your output.`);
|
|
1467
|
-
lines.push(``);
|
|
1468
|
-
|
|
1469
|
-
if (ports.length > 0) {
|
|
1470
|
-
lines.push(`### Server`);
|
|
1471
|
-
lines.push(`- Port: **${ports.join(", ")}**`);
|
|
1472
|
-
lines.push(``);
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
if (apiRoutes.length > 0) {
|
|
1476
|
-
lines.push(`### API Endpoints`);
|
|
1477
|
-
for (const r of apiRoutes) {
|
|
1478
|
-
lines.push(`- \`${r}\``);
|
|
1479
|
-
}
|
|
1480
|
-
lines.push(``);
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
if (schemas.length > 0) {
|
|
1484
|
-
lines.push(`### Data Schemas`);
|
|
1485
|
-
for (const s of schemas) {
|
|
1486
|
-
lines.push(`- \`{ ${s} }\``);
|
|
1487
|
-
}
|
|
1488
|
-
lines.push(``);
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
if (files.length > 0) {
|
|
1492
|
-
lines.push(`### Key Files`);
|
|
1493
|
-
for (const f of files) {
|
|
1494
|
-
lines.push(`- \`${f}\``);
|
|
1495
|
-
}
|
|
1496
|
-
lines.push(``);
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
return lines;
|
|
1500
|
-
}
|