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.
Files changed (39) hide show
  1. package/README.md +200 -318
  2. package/package.json +21 -3
  3. package/src/advisor.ts +145 -0
  4. package/src/agent-pool.ts +39 -15
  5. package/src/agents/_defaults/architect.json +8 -1
  6. package/src/agents/_defaults/backend.json +9 -1
  7. package/src/agents/_defaults/debugger.json +8 -1
  8. package/src/agents/_defaults/devops.json +9 -1
  9. package/src/agents/_defaults/docs.json +8 -1
  10. package/src/agents/_defaults/frontend.json +10 -1
  11. package/src/agents/_defaults/fullstack.json +8 -1
  12. package/src/agents/_defaults/planner.json +7 -1
  13. package/src/agents/_defaults/qa.json +8 -1
  14. package/src/agents/_defaults/researcher.json +8 -1
  15. package/src/agents/_defaults/reviewer.json +10 -0
  16. package/src/agents/_defaults/security.json +8 -1
  17. package/src/index.ts +375 -212
  18. package/src/monitor.ts +20 -7
  19. package/src/panel/message-view.ts +2 -2
  20. package/src/panel/squad-panel.ts +3 -3
  21. package/src/panel/squad-widget.ts +3 -3
  22. package/src/panel/task-list.ts +2 -2
  23. package/src/plan-rules.ts +134 -0
  24. package/src/planner.ts +18 -14
  25. package/src/protocol.ts +11 -20
  26. package/src/scheduler.ts +172 -95
  27. package/src/skills/squad-architecture/SKILL.md +53 -0
  28. package/src/skills/squad-backend-dev/SKILL.md +45 -0
  29. package/src/skills/squad-code-review/SKILL.md +89 -0
  30. package/src/skills/{collaboration → squad-collaboration}/SKILL.md +6 -2
  31. package/src/skills/squad-debugging/SKILL.md +61 -0
  32. package/src/skills/squad-frontend-dev/SKILL.md +51 -0
  33. package/src/skills/squad-protocol/SKILL.md +19 -0
  34. package/src/skills/squad-qa-testing/SKILL.md +72 -0
  35. package/src/skills/squad-security-audit/SKILL.md +43 -0
  36. package/src/skills/squad-supervisor/SKILL.md +85 -11
  37. package/src/skills/{verification → squad-verification}/SKILL.md +1 -1
  38. package/src/store.ts +24 -26
  39. 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 "@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
 
@@ -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
- const overview = store.loadOverview(squadId);
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.\n` +
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
- case "resume": {
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("@mariozechner/pi-coding-agent").ExtensionContext | import("@mariozechner/pi-coding-agent").ExtensionCommandContext,
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("@mariozechner/pi-coding-agent").ExtensionContext | import("@mariozechner/pi-coding-agent").ExtensionCommandContext): void {
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("@mariozechner/pi-coding-agent").ExtensionContext,
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
- plan = await runPlanner({ goal: params.goal, cwd });
1474
+ const defaults = resolveSquadDefaults();
1475
+ plan = await runPlanner({ goal: params.goal, cwd, fallbackModel: defaults.model, fallbackThinking: defaults.thinking });
1196
1476
  } catch (error) {
1197
- return {
1198
- content: [
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
- // Mark tasks with unmet deps as blocked
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
- // Load the full overview document contains the narrative of
1313
- // each task's output, decisions, issues, and files modified.
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
- (overview ? `## Squad Overview\n\n${overview}\n\n` : "") +
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}\n\nAgents are working in the background. Use squad_status to check progress.`,
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
- }