pi-squad 0.7.2 → 0.15.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/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 "@sinclair/typebox";
16
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
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("@mariozechner/pi-coding-agent").ExtensionContext | null = null;
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
 
@@ -296,7 +461,7 @@ export default function (pi: ExtensionAPI) {
296
461
  pi.registerTool({
297
462
  name: "squad_modify",
298
463
  label: "Squad Modify",
299
- description: "Modify the running squad: add_task, cancel_task, pause, resume, cancel (entire squad).",
464
+ description: "Modify the running squad: add_task, cancel_task, complete_task (mark done + schedule dependents), pause, resume (also recovers failed squads), cancel (entire squad).",
300
465
  parameters: Type.Object({
301
466
  action: Type.Union(
302
467
  [
@@ -304,6 +469,7 @@ export default function (pi: ExtensionAPI) {
304
469
  Type.Literal("cancel_task"),
305
470
  Type.Literal("pause_task"),
306
471
  Type.Literal("resume_task"),
472
+ Type.Literal("complete_task"),
307
473
  Type.Literal("pause"),
308
474
  Type.Literal("resume"),
309
475
  Type.Literal("cancel"),
@@ -311,6 +477,7 @@ export default function (pi: ExtensionAPI) {
311
477
  { description: "Action to perform" },
312
478
  ),
313
479
  taskId: Type.Optional(Type.String({ description: "Task ID for task-specific actions" })),
480
+ output: Type.Optional(Type.String({ description: "Result summary for complete_task (what was accomplished)" })),
314
481
  task: Type.Optional(
315
482
  Type.Object({
316
483
  id: Type.String(),
@@ -318,8 +485,8 @@ export default function (pi: ExtensionAPI) {
318
485
  description: Type.Optional(Type.String()),
319
486
  agent: Type.String(),
320
487
  depends: Type.Optional(Type.Array(Type.String())),
321
- }),
322
- { description: "Task definition for add_task" },
488
+ 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)" })),
489
+ }, { description: "Task definition for add_task" }),
323
490
  ),
324
491
  }),
325
492
 
@@ -328,16 +495,16 @@ export default function (pi: ExtensionAPI) {
328
495
  if (params.action === "resume") {
329
496
  // Find a squad to resume: use activeSquadId or find the latest paused one
330
497
  const squadId = activeSquadId || store.findActiveSquads()
331
- .filter((s) => s.cwd === ctx.cwd && s.status === "paused")
498
+ .filter((s) => s.cwd === ctx.cwd && (s.status === "paused" || s.status === "failed"))
332
499
  .sort((a, b) => b.created.localeCompare(a.created))[0]?.id;
333
500
 
334
501
  if (!squadId) {
335
- return { content: [{ type: "text" as const, text: "No paused squad found to resume." }] };
502
+ return { content: [{ type: "text" as const, text: "No paused or failed squad found to resume." }], details: undefined };
336
503
  }
337
504
 
338
505
  // Create a fresh scheduler if needed
339
506
  if (!schedulers.has(squadId)) {
340
- const scheduler = new Scheduler(squadId, squadSkillPaths);
507
+ const scheduler = new Scheduler(squadId, squadSkillPaths, schedulerSpawnContext);
341
508
  schedulers.set(squadId, scheduler);
342
509
  activeSquadId = squadId;
343
510
 
@@ -358,11 +525,13 @@ export default function (pi: ExtensionAPI) {
358
525
  .join("\n");
359
526
  const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
360
527
  const s = schedulers.get(squadId); if (s) s.updateContext();
528
+ // followUp + triggerTurn: wake an idle main agent to review;
529
+ // if it's mid-conversation with the human, deliver after it finishes
361
530
  pi.sendMessage({
362
531
  customType: "squad-completed",
363
- content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\nSummary:\n${summary}\n\nTotal cost: $${totalCost.toFixed(4)}`,
532
+ content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\nSummary:\n${summary}\n\nTotal cost: $${totalCost.toFixed(4)}\n\n${REVIEW_INSTRUCTIONS}`,
364
533
  display: true,
365
- });
534
+ }, { triggerTurn: true, deliverAs: "followUp" });
366
535
  schedulers.delete(squadId);
367
536
  forceWidgetUpdate();
368
537
  break;
@@ -375,7 +544,7 @@ export default function (pi: ExtensionAPI) {
375
544
  customType: "squad-failed",
376
545
  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("; ")}`,
377
546
  display: true,
378
- }, { triggerTurn: true });
547
+ }, { triggerTurn: true, deliverAs: "followUp" });
379
548
  forceWidgetUpdate();
380
549
  break;
381
550
  }
@@ -398,17 +567,32 @@ export default function (pi: ExtensionAPI) {
398
567
 
399
568
  const tasks = store.loadAllTasks(squadId);
400
569
  const done = tasks.filter(t => t.status === "done").length;
401
- return { content: [{ type: "text" as const, text: `Squad "${squadId}" resumed (${done}/${tasks.length} done). Agents restarting in background.` }] };
570
+ return { content: [{ type: "text" as const, text: `Squad "${squadId}" resumed (${done}/${tasks.length} done). Agents restarting in background.` }], details: undefined };
402
571
  }
403
572
 
573
+ const activeScheduler = getActiveScheduler();
404
574
  if (!activeScheduler || !activeSquadId) {
405
- 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." }] };
575
+ 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 };
406
576
  }
407
577
 
408
578
  switch (params.action) {
409
579
  case "add_task": {
410
580
  if (!params.task) {
411
- return { content: [{ type: "text" as const, text: "Provide a task definition for add_task." }] };
581
+ return { content: [{ type: "text" as const, text: "Provide a task definition for add_task." }], details: undefined };
582
+ }
583
+ // Validate against the live squad: deps must exist, agent must exist
584
+ const existing = store.loadAllTasks(activeSquadId);
585
+ const existingIds = new Set(existing.map((t) => t.id));
586
+ if (existingIds.has(params.task.id)) {
587
+ return { content: [{ type: "text" as const, text: `Task id '${params.task.id}' already exists in this squad.` }], details: undefined };
588
+ }
589
+ const badDeps = (params.task.depends || []).filter((d) => !existingIds.has(d));
590
+ if (badDeps.length > 0) {
591
+ return { content: [{ type: "text" as const, text: `Unknown dependency task(s): ${badDeps.join(", ")}. Existing tasks: ${[...existingIds].join(", ")}` }], details: undefined };
592
+ }
593
+ if (!store.loadAgentDef(params.task.agent, ctx.cwd)) {
594
+ const available = store.loadAllAgentDefs(ctx.cwd).filter((a) => !a.disabled).map((a) => a.name).join(", ");
595
+ return { content: [{ type: "text" as const, text: `Unknown agent '${params.task.agent}'. Available: ${available}` }], details: undefined };
412
596
  }
413
597
  const task: Task = {
414
598
  id: params.task.id,
@@ -417,6 +601,7 @@ export default function (pi: ExtensionAPI) {
417
601
  agent: params.task.agent,
418
602
  status: "pending",
419
603
  depends: params.task.depends || [],
604
+ ...(params.task.inheritContext ? { inheritContext: true } : {}),
420
605
  created: store.now(),
421
606
  started: null,
422
607
  completed: null,
@@ -426,27 +611,37 @@ export default function (pi: ExtensionAPI) {
426
611
  };
427
612
  store.createTask(activeSquadId, task);
428
613
  activeScheduler.updateContext();
429
- return { content: [{ type: "text" as const, text: `Task '${task.id}' added.` }] };
614
+ return { content: [{ type: "text" as const, text: `Task '${task.id}' added.` }], details: undefined };
430
615
  }
431
616
 
432
617
  case "cancel_task": {
433
- if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }] };
618
+ if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }], details: undefined };
434
619
  await activeScheduler.cancelTask(params.taskId);
435
- return { content: [{ type: "text" as const, text: `Task '${params.taskId}' cancelled.` }] };
620
+ return { content: [{ type: "text" as const, text: `Task '${params.taskId}' cancelled.` }], details: undefined };
436
621
  }
437
622
 
438
623
  case "pause_task": {
439
- if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }] };
624
+ if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }], details: undefined };
440
625
  await activeScheduler.pauseTask(params.taskId);
441
- return { content: [{ type: "text" as const, text: `Task '${params.taskId}' paused.` }] };
626
+ return { content: [{ type: "text" as const, text: `Task '${params.taskId}' paused.` }], details: undefined };
442
627
  }
443
628
 
444
629
  case "resume_task": {
445
- if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }] };
630
+ if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }], details: undefined };
446
631
  activeScheduler.resumeTask(params.taskId).catch((err) => {
447
632
  logError("squad", `Resume task error: ${(err as Error).message}`);
448
633
  });
449
- return { content: [{ type: "text" as const, text: `Task '${params.taskId}' resumed.` }] };
634
+ return { content: [{ type: "text" as const, text: `Task '${params.taskId}' resumed.` }], details: undefined };
635
+ }
636
+
637
+ case "complete_task": {
638
+ if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }], details: undefined };
639
+ try {
640
+ await activeScheduler.completeTask(params.taskId, params.output);
641
+ } catch (err) {
642
+ return { content: [{ type: "text" as const, text: `complete_task failed: ${(err as Error).message}` }], details: undefined };
643
+ }
644
+ return { content: [{ type: "text" as const, text: `Task '${params.taskId}' marked done — dependents unblocked and scheduled.` }], details: undefined };
450
645
  }
451
646
 
452
647
  case "pause": {
@@ -456,13 +651,10 @@ export default function (pi: ExtensionAPI) {
456
651
  store.saveSquad(squad);
457
652
  }
458
653
  await activeScheduler.stop();
459
- return { content: [{ type: "text" as const, text: "Squad paused. Use squad_modify with action 'resume' to continue." }] };
654
+ return { content: [{ type: "text" as const, text: "Squad paused. Use squad_modify with action 'resume' to continue." }], details: undefined };
460
655
  }
461
656
 
462
- case "resume": {
463
- // Handled above (before the activeScheduler guard)
464
- return { content: [{ type: "text" as const, text: "Squad resumed." }] };
465
- }
657
+ // Note: "resume" is handled above, before the activeScheduler guard.
466
658
 
467
659
  case "cancel": {
468
660
  await activeScheduler.stop();
@@ -473,11 +665,11 @@ export default function (pi: ExtensionAPI) {
473
665
  }
474
666
  schedulers.delete(activeSquadId);
475
667
  activeSquadId = null;
476
- return { content: [{ type: "text" as const, text: "Squad cancelled." }] };
668
+ return { content: [{ type: "text" as const, text: "Squad cancelled." }], details: undefined };
477
669
  }
478
670
 
479
671
  default:
480
- return { content: [{ type: "text" as const, text: `Unknown action: ${params.action}` }] };
672
+ return { content: [{ type: "text" as const, text: `Unknown action: ${params.action}` }], details: undefined };
481
673
  }
482
674
  },
483
675
  });
@@ -553,7 +745,7 @@ export default function (pi: ExtensionAPI) {
553
745
  }
554
746
 
555
747
  if (activeSquadId) {
556
- openPanel(ctx, schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths), activeSquadId);
748
+ openPanel(ctx, schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths, schedulerSpawnContext), activeSquadId);
557
749
  }
558
750
  return { consume: true };
559
751
  }
@@ -585,6 +777,8 @@ export default function (pi: ExtensionAPI) {
585
777
  { value: "all", label: "all", description: "List all squads, select to activate" },
586
778
  { value: "select", label: "select", description: "Pick a squad to view (interactive)" },
587
779
  { value: "agents", label: "agents", description: "List, view, or edit agent definitions" },
780
+ { value: "defaults", label: "defaults", description: "Default model/thinking for agents (follow main session, pi default, or fixed)" },
781
+ { value: "advisor", label: "advisor", description: "Advisor-first rescue for stuck agents (on/off, model, limits)" },
588
782
  { value: "msg", label: "msg", description: "Send message to agent: /squad msg [agent] text" },
589
783
  { value: "widget", label: "widget", description: "Toggle live widget" },
590
784
  { value: "panel", label: "panel", description: "Toggle overlay panel" },
@@ -677,7 +871,7 @@ export default function (pi: ExtensionAPI) {
677
871
  }
678
872
  }
679
873
  if (activeSquadId) {
680
- const sched = schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths);
874
+ const sched = schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths, schedulerSpawnContext);
681
875
  openPanel(ctx, sched, activeSquadId);
682
876
  }
683
877
  return;
@@ -884,6 +1078,92 @@ export default function (pi: ExtensionAPI) {
884
1078
  return;
885
1079
  }
886
1080
 
1081
+ case "defaults": {
1082
+ const settings = store.loadSquadSettings();
1083
+ const mainModel = getMainSessionModel() || "(unknown)";
1084
+ const mainThinking = getMainSessionThinking() || "(unknown)";
1085
+ const fmtPolicy = (v: string, live: string) =>
1086
+ v === "main" ? `follow main session (now: ${live})` : v === "pi-default" ? "pi default" : v;
1087
+
1088
+ const which = await ctx.ui.select(
1089
+ `Squad defaults — model: ${fmtPolicy(settings.defaultModel, mainModel)} | thinking: ${fmtPolicy(settings.defaultThinking, mainThinking)}`,
1090
+ ["Change default model", "Change default thinking", "Cancel"],
1091
+ );
1092
+ if (!which || which === "Cancel") return;
1093
+
1094
+ if (which === "Change default model") {
1095
+ const choice = await ctx.ui.select("Default model for squad agents", [
1096
+ `Follow main session (now: ${mainModel})`,
1097
+ "pi default (child pi resolves its own)",
1098
+ "Custom model…",
1099
+ ]);
1100
+ if (!choice) return;
1101
+ if (choice.startsWith("Follow")) settings.defaultModel = "main";
1102
+ else if (choice.startsWith("pi default")) settings.defaultModel = "pi-default";
1103
+ else {
1104
+ 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);
1105
+ if (!custom || !custom.trim()) return;
1106
+ settings.defaultModel = custom.trim();
1107
+ }
1108
+ store.saveSquadSettings(settings);
1109
+ ctx.ui.notify(`Squad default model → ${fmtPolicy(settings.defaultModel, mainModel)}`, "info");
1110
+ } else {
1111
+ const choice = await ctx.ui.select("Default thinking for squad agents", [
1112
+ `Follow main session (now: ${mainThinking})`,
1113
+ "pi default (child pi resolves its own)",
1114
+ ...THINKING_LEVELS,
1115
+ ]);
1116
+ if (!choice) return;
1117
+ if (choice.startsWith("Follow")) settings.defaultThinking = "main";
1118
+ else if (choice.startsWith("pi default")) settings.defaultThinking = "pi-default";
1119
+ else settings.defaultThinking = choice;
1120
+ store.saveSquadSettings(settings);
1121
+ ctx.ui.notify(`Squad default thinking → ${fmtPolicy(settings.defaultThinking, mainThinking)}`, "info");
1122
+ }
1123
+ return;
1124
+ }
1125
+
1126
+ case "advisor": {
1127
+ const settings = store.loadSquadSettings();
1128
+ const adv = settings.advisor;
1129
+ const mainModelLabel = getMainSessionModel() || "(unknown)";
1130
+ const modelLabel = adv.model === "main" ? `main session (now: ${mainModelLabel})` : adv.model;
1131
+
1132
+ const choice = await ctx.ui.select(
1133
+ `Squad advisor — ${adv.enabled ? "ON" : "OFF"} | model: ${modelLabel} | ${adv.maxCallsPerTask} calls/task, ${adv.reasoning} reasoning`,
1134
+ [adv.enabled ? "Disable advisor" : "Enable advisor", "Change advisor model", "Change max calls per task", "Change reasoning effort", "Cancel"],
1135
+ );
1136
+ if (!choice || choice === "Cancel") return;
1137
+
1138
+ if (choice.startsWith("Disable") || choice.startsWith("Enable")) {
1139
+ adv.enabled = !adv.enabled;
1140
+ ctx.ui.notify(`Squad advisor ${adv.enabled ? "enabled — stuck agents get a strong-model rescue before escalating" : "disabled — stuck agents escalate directly"}`, "info");
1141
+ } else if (choice === "Change advisor model") {
1142
+ const sel = await ctx.ui.select("Advisor model", [`Follow main session (now: ${mainModelLabel})`, "Custom model…"]);
1143
+ if (!sel) return;
1144
+ if (sel.startsWith("Follow")) adv.model = "main";
1145
+ else {
1146
+ const custom = await ctx.ui.input("Advisor model (provider/id)", adv.model === "main" ? "" : adv.model);
1147
+ if (!custom || !custom.trim()) return;
1148
+ adv.model = custom.trim();
1149
+ }
1150
+ ctx.ui.notify(`Advisor model → ${adv.model}`, "info");
1151
+ } else if (choice === "Change max calls per task") {
1152
+ const n = await ctx.ui.input("Max advisor calls per task", String(adv.maxCallsPerTask));
1153
+ const parsed = n ? Number.parseInt(n, 10) : NaN;
1154
+ if (!Number.isFinite(parsed) || parsed < 0) return;
1155
+ adv.maxCallsPerTask = parsed;
1156
+ ctx.ui.notify(`Advisor max calls/task → ${parsed}`, "info");
1157
+ } else {
1158
+ const lvl = await ctx.ui.select("Advisor reasoning effort", ["minimal", "low", "medium", "high", "xhigh"]);
1159
+ if (!lvl) return;
1160
+ adv.reasoning = lvl;
1161
+ ctx.ui.notify(`Advisor reasoning → ${lvl}`, "info");
1162
+ }
1163
+ store.saveSquadSettings(settings);
1164
+ return;
1165
+ }
1166
+
887
1167
  case "agents": {
888
1168
  const agentArg = parts[1];
889
1169
  const allAgents = store.loadAllAgentDefs(ctx.cwd);
@@ -895,7 +1175,7 @@ export default function (pi: ExtensionAPI) {
895
1175
  return;
896
1176
  }
897
1177
  const options = allAgents.map((a) => {
898
- const model = a.model ? ` [${a.model}]` : " [default]";
1178
+ const model = a.model ? ` [${a.model}${a.thinking ? `:${a.thinking}` : ""}]` : a.thinking ? ` [default:${a.thinking}]` : " [default]";
899
1179
  const status = a.disabled ? " ✗ disabled" : "";
900
1180
  return `${a.name} — ${a.role}${model}${status}`;
901
1181
  });
@@ -911,6 +1191,7 @@ export default function (pi: ExtensionAPI) {
911
1191
  "View details",
912
1192
  "Edit in editor",
913
1193
  "Change model",
1194
+ "Change thinking",
914
1195
  "Toggle tools (restrict/unrestrict)",
915
1196
  disableLabel,
916
1197
  "Cancel",
@@ -924,6 +1205,7 @@ export default function (pi: ExtensionAPI) {
924
1205
  `Role: ${agent.role}`,
925
1206
  `Description: ${agent.description}`,
926
1207
  `Model: ${agent.model || "(default)"}`,
1208
+ `Thinking: ${agent.thinking || "(default)"}`,
927
1209
  `Tools: ${agent.tools ? agent.tools.join(", ") : "(all)"}`,
928
1210
  `Tags: ${agent.tags.join(", ")}`,
929
1211
  ``,
@@ -953,6 +1235,14 @@ export default function (pi: ExtensionAPI) {
953
1235
  store.saveAgentDef(agent);
954
1236
  ctx.ui.notify(`${agent.name} model → ${agent.model || "(default)"}`, "info");
955
1237
  }
1238
+ } else if (action === "Change thinking") {
1239
+ const levels = ["(default)", ...THINKING_LEVELS];
1240
+ const level = await ctx.ui.select(`Thinking level for ${agent.name}`, levels);
1241
+ if (level !== undefined) {
1242
+ agent.thinking = level === "(default)" ? null : level;
1243
+ store.saveAgentDef(agent);
1244
+ ctx.ui.notify(`${agent.name} thinking → ${agent.thinking || "(default)"}`, "info");
1245
+ }
956
1246
  } else if (action === disableLabel) {
957
1247
  agent.disabled = !agent.disabled;
958
1248
  store.saveAgentDef(agent);
@@ -986,6 +1276,7 @@ export default function (pi: ExtensionAPI) {
986
1276
  `${agent.name} — ${agent.role}${status}`,
987
1277
  `${agent.description}`,
988
1278
  `Model: ${agent.model || "(default)"}`,
1279
+ `Thinking: ${agent.thinking || "(default)"}`,
989
1280
  `Tools: ${agent.tools ? agent.tools.join(", ") : "(all)"}`,
990
1281
  `Tags: ${agent.tags.join(", ")}`,
991
1282
  ].join("\n");
@@ -1003,7 +1294,7 @@ export default function (pi: ExtensionAPI) {
1003
1294
  activateSquadView(direct.id, ctx);
1004
1295
  return;
1005
1296
  }
1006
- ctx.ui.notify(`Unknown: /squad ${sub}. Try: list, all, select, widget, panel, cancel, clear`, "warning");
1297
+ ctx.ui.notify(`Unknown: /squad ${sub}. Try: list, all, select, agents, defaults, msg, widget, panel, cancel, clear, cleanup`, "warning");
1007
1298
  }
1008
1299
  },
1009
1300
  });
@@ -1019,7 +1310,7 @@ export default function (pi: ExtensionAPI) {
1019
1310
  * Returns the selected squad or undefined if cancelled.
1020
1311
  */
1021
1312
  async function pickSquad(
1022
- ctx: import("@mariozechner/pi-coding-agent").ExtensionContext | import("@mariozechner/pi-coding-agent").ExtensionCommandContext,
1313
+ ctx: import("@earendil-works/pi-coding-agent").ExtensionContext | import("@earendil-works/pi-coding-agent").ExtensionCommandContext,
1023
1314
  squads: Squad[],
1024
1315
  showProject = false,
1025
1316
  ): Promise<Squad | undefined> {
@@ -1046,7 +1337,7 @@ async function pickSquad(
1046
1337
  * Sets activeSquadId, starts widget, shows notification.
1047
1338
  * Does NOT start a scheduler (view-only unless squad needs resuming).
1048
1339
  */
1049
- function activateSquadView(squadId: string, ctx: import("@mariozechner/pi-coding-agent").ExtensionContext | import("@mariozechner/pi-coding-agent").ExtensionCommandContext): void {
1340
+ function activateSquadView(squadId: string, ctx: import("@earendil-works/pi-coding-agent").ExtensionContext | import("@earendil-works/pi-coding-agent").ExtensionCommandContext): void {
1050
1341
  const squad = store.loadSquad(squadId);
1051
1342
  if (!squad) {
1052
1343
  ctx.ui.notify(`Squad '${squadId}' not found`, "error");
@@ -1088,7 +1379,7 @@ function forceWidgetUpdate(): void {
1088
1379
  * that resolves when done() is called. The panel calls done() on close.
1089
1380
  */
1090
1381
  function openPanel(
1091
- ctx: import("@mariozechner/pi-coding-agent").ExtensionContext,
1382
+ ctx: import("@earendil-works/pi-coding-agent").ExtensionContext,
1092
1383
  scheduler: Scheduler,
1093
1384
  squadId: string,
1094
1385
  ): void {
@@ -1151,19 +1442,21 @@ async function startSquad(
1151
1442
  squadId: string,
1152
1443
  params: {
1153
1444
  goal: string;
1154
- agents?: Record<string, { model?: string }>;
1445
+ agents?: Record<string, { model?: string; thinking?: string }>;
1155
1446
  tasks?: Array<{
1156
1447
  id: string;
1157
1448
  title: string;
1158
1449
  description?: string;
1159
1450
  agent: string;
1160
1451
  depends?: string[];
1452
+ inheritContext?: boolean;
1161
1453
  }>;
1162
1454
  config?: { maxConcurrency?: number };
1163
1455
  },
1164
1456
  cwd: string,
1165
1457
  skillPaths: string[],
1166
1458
  pi: ExtensionAPI,
1459
+ sessionFile: string | null = null,
1167
1460
  ) {
1168
1461
  let plan: PlannerOutput;
1169
1462
 
@@ -1188,27 +1481,33 @@ async function startSquad(
1188
1481
  }
1189
1482
  }
1190
1483
  } else {
1191
- // Run planner to generate task breakdown
1484
+ // Run planner to generate task breakdown (squad default policy as fallback)
1192
1485
  try {
1193
- plan = await runPlanner({ goal: params.goal, cwd });
1486
+ const defaults = resolveSquadDefaults();
1487
+ plan = await runPlanner({ goal: params.goal, cwd, fallbackModel: defaults.model, fallbackThinking: defaults.thinking });
1194
1488
  } catch (error) {
1195
- return {
1196
- content: [
1197
- { type: "text" as const, text: `Failed to plan: ${(error as Error).message}` },
1198
- ],
1199
- isError: true,
1200
- };
1489
+ // Throwing marks the tool result as an error for the LLM (returning isError is ignored in current pi)
1490
+ throw new Error(`Failed to plan: ${(error as Error).message}`);
1201
1491
  }
1202
1492
  }
1203
1493
 
1204
1494
  // Merge agent roster
1205
- const agents: Record<string, { model?: string }> = { ...plan.agents };
1495
+ const agents: Record<string, { model?: string; thinking?: string }> = { ...plan.agents };
1206
1496
  if (params.agents) {
1207
1497
  for (const [name, entry] of Object.entries(params.agents)) {
1208
1498
  agents[name] = { ...agents[name], ...entry };
1209
1499
  }
1210
1500
  }
1211
1501
 
1502
+ // Validate the plan — same enforcement for main-session and planner plans.
1503
+ // Errors block squad creation; warnings are reported back to the plan author.
1504
+ const validation = validatePlan(plan.tasks);
1505
+ if (validation.errors.length > 0) {
1506
+ throw new Error(
1507
+ `Plan rejected:\n- ${validation.errors.join("\n- ")}\n\nFix the task list and call squad again.`,
1508
+ );
1509
+ }
1510
+
1212
1511
  // Create squad
1213
1512
  const config: SquadConfig = {
1214
1513
  ...DEFAULT_SQUAD_CONFIG,
@@ -1221,6 +1520,7 @@ async function startSquad(
1221
1520
  status: "running",
1222
1521
  created: store.now(),
1223
1522
  cwd,
1523
+ sessionFile,
1224
1524
  agents,
1225
1525
  config,
1226
1526
  };
@@ -1236,6 +1536,7 @@ async function startSquad(
1236
1536
  agent: taskDef.agent,
1237
1537
  status: taskDef.depends.length === 0 ? "pending" : "blocked",
1238
1538
  depends: taskDef.depends,
1539
+ ...(taskDef.inheritContext ? { inheritContext: true } : {}),
1239
1540
  created: store.now(),
1240
1541
  started: null,
1241
1542
  completed: null,
@@ -1243,22 +1544,14 @@ async function startSquad(
1243
1544
  error: null,
1244
1545
  usage: { inputTokens: 0, outputTokens: 0, cost: 0, turns: 0 },
1245
1546
  };
1246
-
1247
- // Mark tasks with unmet deps as blocked
1248
- if (task.depends.length > 0) {
1249
- const allDepsMet = task.depends.every((depId) =>
1250
- plan.tasks.some((t) => t.id === depId),
1251
- );
1252
- if (!allDepsMet) {
1253
- task.status = "pending"; // deps reference external tasks, treat as ready
1254
- }
1255
- }
1547
+ // Note: unknown dependency references are hard validation errors above,
1548
+ // so blocked tasks here always have resolvable deps.
1256
1549
 
1257
1550
  store.createTask(squadId, task);
1258
1551
  }
1259
1552
 
1260
1553
  // Start scheduler
1261
- const scheduler = new Scheduler(squadId, skillPaths);
1554
+ const scheduler = new Scheduler(squadId, skillPaths, schedulerSpawnContext);
1262
1555
  schedulers.set(squadId, scheduler);
1263
1556
  activeSquadId = squadId;
1264
1557
 
@@ -1286,13 +1579,16 @@ async function startSquad(
1286
1579
  completedSched.updateContext();
1287
1580
  }
1288
1581
 
1582
+ // followUp + triggerTurn: wake an idle main agent to review; if it's
1583
+ // mid-conversation with the human, deliver after it finishes
1289
1584
  pi.sendMessage({
1290
1585
  customType: "squad-completed",
1291
1586
  content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\n` +
1292
1587
  `Summary:\n${summary}\n\n` +
1293
- `Total cost: $${totalCost.toFixed(4)}`,
1588
+ `Total cost: $${totalCost.toFixed(4)}\n\n` +
1589
+ REVIEW_INSTRUCTIONS,
1294
1590
  display: true,
1295
- });
1591
+ }, { triggerTurn: true, deliverAs: "followUp" });
1296
1592
 
1297
1593
  // Clear scheduler but keep activeSquadId so squad_status still works
1298
1594
  schedulers.delete(squadId);
@@ -1312,7 +1608,7 @@ async function startSquad(
1312
1608
  `Failed: ${failed.map((t) => `${t.id}: ${t.error?.slice(0, 100)}`).join("; ")}\n` +
1313
1609
  `Use squad_status for details or squad_modify to adjust.`,
1314
1610
  display: true,
1315
- }, { triggerTurn: true });
1611
+ }, { triggerTurn: true, deliverAs: "followUp" });
1316
1612
  forceWidgetUpdate();
1317
1613
  break;
1318
1614
  }
@@ -1352,9 +1648,14 @@ async function startSquad(
1352
1648
  content: [
1353
1649
  {
1354
1650
  type: "text" as const,
1355
- text: `Squad "${squadId}" started with ${plan.tasks.length} tasks.\n\n${taskSummary}\n\nAgents are working in the background. Use squad_status to check progress.`,
1651
+ text: `Squad "${squadId}" started with ${plan.tasks.length} tasks.\n\n${taskSummary}${
1652
+ validation.warnings.length > 0
1653
+ ? `\n\n⚠️ Plan warnings (fix with squad_modify, or address at review):\n- ${validation.warnings.join("\n- ")}`
1654
+ : ""
1655
+ }\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.`,
1356
1656
  },
1357
1657
  ],
1658
+ details: undefined,
1358
1659
  };
1359
1660
  }
1360
1661