pi-squad 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,9 +41,13 @@ The planner agent reads your codebase and creates a task breakdown automatically
41
41
  2. A **live widget** appears above the editor showing task progress
42
42
  3. **Specialist agents** spawn as separate pi processes, working in parallel where dependencies allow
43
43
  4. QA agents can trigger **automatic rework loops** when they find bugs
44
- 5. On completion, pi receives a summary with each task's output
44
+ 5. On completion, pi receives a summary with each task's **complete, untruncated output**
45
45
  6. Multiple squads can run concurrently across different projects
46
46
 
47
+ ### No-Truncation Contract
48
+
49
+ Task messages, task outputs, dependency/rework handoffs, QA feedback, advisor handoffs, completion reports, failure diagnostics, and planner errors are persisted and forwarded in full. There is no character or task-count limit on report data. TUI widgets may show width/height-limited **views** to fit the terminal, but the underlying data and agent/main-session handoffs remain complete.
50
+
47
51
  ## Features
48
52
 
49
53
  ### Dependency-Aware Scheduling
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squad",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Multi-agent collaboration extension for pi — task decomposition, dependency management, parallel execution, TUI panel",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/advisor.ts CHANGED
@@ -78,18 +78,10 @@ Output format:
78
78
 
79
79
  Keep it short. The agent reads your advice and immediately acts on it.`;
80
80
 
81
- const MAX_MSG_CHARS = 400;
82
- const MAX_MESSAGES = 12;
83
- const MAX_TOOL_CALLS = 10;
84
-
85
- function clamp(text: string, max: number): string {
86
- const t = text.trim();
87
- return t.length > max ? `${t.slice(0, max).trimEnd()}…` : t;
88
- }
89
-
90
81
  /**
91
82
  * Build the user-message digest sent to the advisor model.
92
- * Curated and bounded — summaries, not full transcripts.
83
+ * Preserve complete descriptions, messages, and tool history: advisor handoffs
84
+ * follow the same no-truncation contract as task and completion reports.
93
85
  */
94
86
  export function buildAdvisorConsultText(input: AdvisorConsultInput): string {
95
87
  const lines: string[] = [];
@@ -102,22 +94,21 @@ export function buildAdvisorConsultText(input: AdvisorConsultInput): string {
102
94
  lines.push(`Progress: ${input.turnCount} turns, ~${Math.round(input.elapsedMinutes)} min elapsed`);
103
95
  lines.push("");
104
96
  lines.push(`## Task Description`);
105
- lines.push(clamp(input.taskDescription || "(no description)", 1500));
97
+ lines.push((input.taskDescription || "(no description)").trim());
106
98
 
107
99
  if (input.recentToolCalls.length > 0) {
108
100
  lines.push("");
109
101
  lines.push(`## Recent Tool Activity (newest last)`);
110
- for (const call of input.recentToolCalls.slice(-MAX_TOOL_CALLS)) {
111
- lines.push(`- ${clamp(call, 160)}`);
102
+ for (const call of input.recentToolCalls) {
103
+ lines.push(`- ${call.trim()}`);
112
104
  }
113
105
  }
114
106
 
115
- const messages = input.recentMessages.slice(-MAX_MESSAGES);
116
- if (messages.length > 0) {
107
+ if (input.recentMessages.length > 0) {
117
108
  lines.push("");
118
109
  lines.push(`## Recent Messages (newest last)`);
119
- for (const msg of messages) {
120
- lines.push(`[${msg.from}/${msg.type}] ${clamp(msg.text, MAX_MSG_CHARS)}`);
110
+ for (const msg of input.recentMessages) {
111
+ lines.push(`[${msg.from}/${msg.type}] ${msg.text.trim()}`);
121
112
  }
122
113
  }
123
114
 
@@ -141,5 +132,5 @@ export function formatAdvisorSteerMessage(advice: string, reason: string): strin
141
132
 
142
133
  /** True when the advisor's verdict says a human decision is required. */
143
134
  export function adviceNeedsHuman(advice: string): boolean {
144
- return /needs human input/i.test(advice.slice(0, 200));
135
+ return /needs human input/i.test(advice);
145
136
  }
package/src/agent-pool.ts CHANGED
@@ -217,7 +217,7 @@ export class AgentPool {
217
217
  proc.on("exit", (code, signal) => {
218
218
  // Log diagnostic info for debugging spawn failures
219
219
  if (code !== 0 && code !== null) {
220
- logError("squad-pool", `${agentDef.name} exited: code=${code} signal=${signal} pid=${proc.pid} stdoutLines=${stdoutLines} stderr=${stderr.slice(0, 300) || "(empty)"}`);
220
+ logError("squad-pool", `${agentDef.name} exited: code=${code} signal=${signal} pid=${proc.pid} stdoutLines=${stdoutLines} stderr=${stderr || "(empty)"}`);
221
221
  }
222
222
  // Capture activity stats BEFORE deleting the agent
223
223
  const finalActivity = agentProc.activity;
@@ -232,7 +232,8 @@ export class AgentPool {
232
232
  agentName: agentDef.name,
233
233
  data: {
234
234
  exitCode: code,
235
- stderr: stderr.slice(-2000),
235
+ // Preserve complete diagnostics for task failure reports and recovery.
236
+ stderr,
236
237
  turnCount: finalActivity.turnCount,
237
238
  toolCallCount: finalActivity.recentToolCalls.length,
238
239
  filesModified: finalActivity.modifiedFiles.size,
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ import { SquadPanel, type SquadPanelResult } from "./panel/squad-panel.js";
25
25
  import { setupSquadWidget, type SquadWidgetState } from "./panel/squad-widget.js";
26
26
  import * as store from "./store.js";
27
27
  import { debug, logError } from "./logger.js";
28
+ import { buildCompletionSummary, buildFailureSummary } from "./report.js";
28
29
 
29
30
  // ============================================================================
30
31
  // State
@@ -229,8 +230,8 @@ export default function (pi: ExtensionAPI) {
229
230
  const taskLines = tasks.map((t) => {
230
231
  const icon = t.status === "done" ? "✓" : t.status === "in_progress" ? "⏳" : t.status === "failed" ? "✗" : t.status === "blocked" ? "◻" : "·";
231
232
  let line = ` ${icon} ${t.id} (${t.agent}) [${t.status}]`;
232
- if (t.output) line += ` — ${t.output.split("\n")[0].slice(0, 80)}`;
233
- if (t.error) line += ` ERROR: ${t.error.slice(0, 60)}`;
233
+ if (t.output) line += ` — ${t.output}`;
234
+ if (t.error) line += ` ERROR: ${t.error}`;
234
235
  return line;
235
236
  }).join("\n");
236
237
 
@@ -519,10 +520,7 @@ export default function (pi: ExtensionAPI) {
519
520
  switch (event.type) {
520
521
  case "squad_completed": {
521
522
  const tasks = store.loadAllTasks(squadId);
522
- const summary = tasks
523
- .filter((t) => t.status === "done")
524
- .map((t) => `- ${t.id} (${t.agent}): ${t.output?.slice(0, 150) || "done"}`)
525
- .join("\n");
523
+ const summary = buildCompletionSummary(tasks);
526
524
  const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
527
525
  const s = schedulers.get(squadId); if (s) s.updateContext();
528
526
  // followUp + triggerTurn: wake an idle main agent to review;
@@ -542,7 +540,7 @@ export default function (pi: ExtensionAPI) {
542
540
  const done = tasks.filter((t) => t.status === "done");
543
541
  pi.sendMessage({
544
542
  customType: "squad-failed",
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("; ")}`,
543
+ content: `[squad] Squad "${squadId}" has stalled. ${done.length}/${tasks.length} done, ${failed.length} failed.\nFailed: ${buildFailureSummary(tasks)}`,
546
544
  display: true,
547
545
  }, { triggerTurn: true, deliverAs: "followUp" });
548
546
  forceWidgetUpdate();
@@ -932,7 +930,7 @@ export default function (pi: ExtensionAPI) {
932
930
  const msgSched = getActiveScheduler();
933
931
  if (msgSched) {
934
932
  await msgSched.sendHumanMessage(targetTaskId, msgText);
935
- ctx.ui.notify(`Sent to ${targetAgent}: "${msgText.slice(0, 50)}"`, "info");
933
+ ctx.ui.notify(`Sent to ${targetAgent}: "${msgText}"`, "info");
936
934
  } else {
937
935
  store.appendMessage(activeSquadId, targetTaskId, {
938
936
  ts: store.now(),
@@ -1210,7 +1208,7 @@ export default function (pi: ExtensionAPI) {
1210
1208
  `Tags: ${agent.tags.join(", ")}`,
1211
1209
  ``,
1212
1210
  `Prompt:`,
1213
- `${agent.prompt.slice(0, 300)}${agent.prompt.length > 300 ? "..." : ""}`,
1211
+ agent.prompt,
1214
1212
  ``,
1215
1213
  `File: ${store.getGlobalAgentsDir()}/${agent.name}.json`,
1216
1214
  ].join("\n");
@@ -1399,7 +1397,7 @@ function openPanel(
1399
1397
  const panelSched = schedulers.get(squadId);
1400
1398
  if (input && panelSched) {
1401
1399
  await panelSched.sendHumanMessage(taskId, input);
1402
- ctx.ui.notify(`Sent to ${agentName}: "${input.slice(0, 50)}"`, "info");
1400
+ ctx.ui.notify(`Sent to ${agentName}: "${input}"`, "info");
1403
1401
  } else if (input) {
1404
1402
  store.appendMessage(squadId, taskId, {
1405
1403
  ts: store.now(),
@@ -1567,10 +1565,7 @@ async function startSquad(
1567
1565
  switch (event.type) {
1568
1566
  case "squad_completed": {
1569
1567
  const tasks = store.loadAllTasks(squadId);
1570
- const summary = tasks
1571
- .filter((t) => t.status === "done")
1572
- .map((t) => `- ${t.id} (${t.agent}): ${t.output?.slice(0, 150) || "done"}`)
1573
- .join("\n");
1568
+ const summary = buildCompletionSummary(tasks);
1574
1569
  const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
1575
1570
 
1576
1571
  // Final context update before clearing scheduler
@@ -1605,7 +1600,7 @@ async function startSquad(
1605
1600
  customType: "squad-failed",
1606
1601
  content: `[squad] Squad "${squadId}" has stalled. ` +
1607
1602
  `${done.length}/${tasks.length} tasks done, ${failed.length} failed.\n` +
1608
- `Failed: ${failed.map((t) => `${t.id}: ${t.error?.slice(0, 100)}`).join("; ")}\n` +
1603
+ `Failed: ${buildFailureSummary(tasks)}\n` +
1609
1604
  `Use squad_status for details or squad_modify to adjust.`,
1610
1605
  display: true,
1611
1606
  }, { triggerTurn: true, deliverAs: "followUp" });
package/src/planner.ts CHANGED
@@ -152,7 +152,7 @@ async function runPiJson(options: PiJsonOptions): Promise<string> {
152
152
  }
153
153
 
154
154
  if (code !== 0 && messages.length === 0) {
155
- reject(new Error(`Planner failed (code ${code}): ${stderr.slice(0, 500)}`));
155
+ reject(new Error(`Planner failed (code ${code}): ${stderr}`));
156
156
  return;
157
157
  }
158
158
 
@@ -217,7 +217,7 @@ function parsePlannerOutput(text: string, validAgents?: Set<string>): PlannerOut
217
217
  return parsed as PlannerOutput;
218
218
  } catch (error) {
219
219
  if (error instanceof SyntaxError) {
220
- throw new Error(`Planner output is not valid JSON: ${text.slice(0, 200)}`);
220
+ throw new Error(`Planner output is not valid JSON: ${text}`);
221
221
  }
222
222
  throw error;
223
223
  }
package/src/protocol.ts CHANGED
@@ -102,7 +102,6 @@ function buildChainContext(task: Task, allTasks: Task[], squadId: string): strin
102
102
  const messages = loadMessages(squadId, dep.id);
103
103
  const lastText = messages
104
104
  .filter((m) => m.from === dep.agent && (m.type === "text" || m.type === "done"))
105
- .slice(-3)
106
105
  .map((m) => m.text)
107
106
  .join("\n");
108
107
  if (lastText) {
@@ -155,12 +154,9 @@ function buildSiblingAwareness(
155
154
  for (const [agent, files] of fileEntries) {
156
155
  if (files.length > 0) {
157
156
  lines.push(`**${agent}:**`);
158
- for (const f of files.slice(0, 10)) {
157
+ for (const f of files) {
159
158
  lines.push(` - ${f}`);
160
159
  }
161
- if (files.length > 10) {
162
- lines.push(` - ...and ${files.length - 10} more`);
163
- }
164
160
  }
165
161
  }
166
162
  lines.push(
@@ -187,7 +183,7 @@ function buildKnowledgeSection(squadId: string): string {
187
183
 
188
184
  if (decisions.length > 0) {
189
185
  lines.push("## Decisions");
190
- for (const d of decisions.slice(-10)) {
186
+ for (const d of decisions) {
191
187
  lines.push(`- ${d.text} (${d.from})`);
192
188
  }
193
189
  lines.push("");
@@ -195,7 +191,7 @@ function buildKnowledgeSection(squadId: string): string {
195
191
 
196
192
  if (conventions.length > 0) {
197
193
  lines.push("## Project Conventions");
198
- for (const c of conventions.slice(-10)) {
194
+ for (const c of conventions) {
199
195
  lines.push(`- ${c.text} (${c.from})`);
200
196
  }
201
197
  lines.push("");
@@ -203,7 +199,7 @@ function buildKnowledgeSection(squadId: string): string {
203
199
 
204
200
  if (findings.length > 0) {
205
201
  lines.push("## Findings");
206
- for (const f of findings.slice(-10)) {
202
+ for (const f of findings) {
207
203
  lines.push(`- ${f.text} (${f.from})`);
208
204
  }
209
205
  lines.push("");
@@ -256,7 +252,8 @@ function buildReworkContext(task: Task, squadId: string): string {
256
252
 
257
253
  if (originalTask?.output) {
258
254
  lines.push("## What Was Built (Previous Attempt)");
259
- lines.push(originalTask.output.slice(0, 2000));
255
+ // Rework agents need the complete prior handoff, not an arbitrary prefix.
256
+ lines.push(originalTask.output);
260
257
  lines.push("");
261
258
  }
262
259
 
package/src/report.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { Task } from "./types.js";
2
+
3
+ /**
4
+ * Build the full task handoff included in squad completion notifications.
5
+ * This is durable report data, not a UI preview: never shorten task output.
6
+ */
7
+ export function buildCompletionSummary(tasks: Task[]): string {
8
+ return tasks
9
+ .filter((task) => task.status === "done")
10
+ .map((task) => `- ${task.id} (${task.agent}): ${task.output || "done"}`)
11
+ .join("\n");
12
+ }
13
+
14
+ /** Build the full failure handoff without shortening diagnostics. */
15
+ export function buildFailureSummary(tasks: Task[]): string {
16
+ return tasks
17
+ .filter((task) => task.status === "failed")
18
+ .map((task) => `${task.id}: ${task.error || "unknown error"}`)
19
+ .join("; ");
20
+ }
package/src/router.ts CHANGED
@@ -202,6 +202,6 @@ export class Router {
202
202
  return line.trim();
203
203
  }
204
204
  }
205
- return text.slice(0, 200);
205
+ return text;
206
206
  }
207
207
  }
package/src/scheduler.ts CHANGED
@@ -465,7 +465,7 @@ export class Scheduler {
465
465
  agentName: task.agent,
466
466
  agentRole: agentDef?.role || task.agent,
467
467
  reason,
468
- recentMessages: store.loadMessages(this.squadId, taskId).slice(-12).map((m) => ({ from: m.from, type: m.type, text: m.text })),
468
+ recentMessages: store.loadMessages(this.squadId, taskId).map((m) => ({ from: m.from, type: m.type, text: m.text })),
469
469
  recentToolCalls: activity ? [...activity.recentToolCalls] : [],
470
470
  turnCount: activity?.turnCount || 0,
471
471
  elapsedMinutes: activity ? (Date.now() - activity.startedAt) / 60000 : 0,
@@ -488,7 +488,7 @@ export class Scheduler {
488
488
  squadId: this.squadId,
489
489
  taskId,
490
490
  agentName,
491
- message: `${reason}\n\nAdvisor assessment:\n${advice.slice(0, 800)}`,
491
+ message: `${reason}\n\nAdvisor assessment:\n${advice}`,
492
492
  });
493
493
  return true; // escalation already emitted with richer context
494
494
  }
@@ -563,7 +563,9 @@ export class Scheduler {
563
563
  ts: store.now(),
564
564
  from: event.agentName,
565
565
  type: "text",
566
- text: text.slice(0, 2000),
566
+ // Persist the complete handoff. Reports can be arbitrarily long;
567
+ // presentation layers may viewport them, but source data must never truncate.
568
+ text,
567
569
  });
568
570
  }
569
571
 
@@ -636,7 +638,7 @@ export class Scheduler {
636
638
  const reason = turnCount === 0
637
639
  ? `exited with 0 turns (likely rate limit or API error)`
638
640
  : `exited with ${turnCount} turns but 0 tool calls (no work done)`;
639
- logError("squad-scheduler", `Agent ${event.agentName} ${reason}, code=${exitCode}. Retrying in 2s... stderr: ${stderr.slice(0, 200)}`);
641
+ logError("squad-scheduler", `Agent ${event.agentName} ${reason}, code=${exitCode}. Retrying in 2s... stderr: ${stderr}`);
640
642
  store.updateTaskStatus(this.squadId, event.taskId, "pending");
641
643
  store.appendMessage(this.squadId, event.taskId, {
642
644
  ts: store.now(),
@@ -650,7 +652,7 @@ export class Scheduler {
650
652
  }, 2000);
651
653
  } else {
652
654
  const stderr = event.data?.stderr || "";
653
- this.handleTaskFailed(event.taskId, `Agent exited with code ${exitCode} (retry exhausted). ${stderr.slice(0, 500)}`);
655
+ this.handleTaskFailed(event.taskId, `Agent exited with code ${exitCode} (retry exhausted). ${stderr}`);
654
656
  }
655
657
  this.updateContext();
656
658
  }
@@ -682,10 +684,9 @@ export class Scheduler {
682
684
 
683
685
  // Extract output from last messages
684
686
  const messages = store.loadMessages(this.squadId, taskId);
685
- const lastAgentMessages = messages
686
- .filter((m) => m.from === task.agent && (m.type === "text" || m.type === "done"))
687
- .slice(-3);
688
- const output = lastAgentMessages.map((m) => m.text).join("\n");
687
+ const agentMessages = messages
688
+ .filter((m) => m.from === task.agent && (m.type === "text" || m.type === "done"));
689
+ const output = agentMessages.map((m) => m.text).join("\n");
689
690
 
690
691
  store.updateTaskStatus(this.squadId, taskId, "done", {
691
692
  output: output || "Task completed",
@@ -869,7 +870,7 @@ export class Scheduler {
869
870
  squadId: this.squadId,
870
871
  taskId: task.id,
871
872
  agentName: task.agent,
872
- message: `QA failed ${originalId} ${retryCount} times. Retry limit reached.\nLatest feedback:\n${feedback.slice(0, 500)}`,
873
+ message: `QA failed ${originalId} ${retryCount} times. Retry limit reached.\nLatest feedback:\n${feedback}`,
873
874
  });
874
875
  continue;
875
876
  }
@@ -974,12 +975,11 @@ export class Scheduler {
974
975
 
975
976
  // Try to extract lines containing "FAIL", "Error", "✗"
976
977
  const failLines = output.split("\n")
977
- .filter((line) => /fail|error|✗|✘|broken|bug/i.test(line))
978
- .slice(0, 20);
978
+ .filter((line) => /fail|error|✗|✘|broken|bug/i.test(line));
979
979
  if (failLines.length > 0) return failLines.join("\n");
980
980
 
981
- // Fallback: last 500 chars
982
- return output.slice(-500);
981
+ // Preserve the complete QA handoff when no structured failure section exists.
982
+ return output;
983
983
  }
984
984
 
985
985
  // =========================================================================
@@ -1047,7 +1047,7 @@ export class Scheduler {
1047
1047
  status: task.status,
1048
1048
  agent: task.agent,
1049
1049
  title: task.title,
1050
- ...(task.output ? { output: task.output.slice(0, 500) } : {}),
1050
+ ...(task.output ? { output: task.output } : {}),
1051
1051
  ...(task.status === "blocked"
1052
1052
  ? {
1053
1053
  blockedBy: task.depends.filter((d) => {
@@ -1089,7 +1089,7 @@ export class Scheduler {
1089
1089
  action:
1090
1090
  msg.type === "tool"
1091
1091
  ? `→ ${msg.name} ${msg.args?.path || msg.args?.command || ""}`.trim()
1092
- : msg.text.slice(0, 80),
1092
+ : msg.text,
1093
1093
  });
1094
1094
  }
1095
1095
  }
package/src/supervisor.ts CHANGED
@@ -70,10 +70,9 @@ export async function analyzeStuckAgent(
70
70
  ): Promise<{ action: "retry" | "reassign" | "escalate"; reason: string; suggestion?: string }> {
71
71
  const task = store.loadTask(squadId, taskId);
72
72
  const messages = store.loadMessages(squadId, taskId);
73
- const recentMessages = messages.slice(-10);
74
73
 
75
- // Simple heuristic for now
76
- const errorMessages = recentMessages.filter((m) => m.type === "error");
74
+ // Simple heuristic for now — inspect the complete task history.
75
+ const errorMessages = messages.filter((m) => m.type === "error");
77
76
  if (errorMessages.length >= 3) {
78
77
  return {
79
78
  action: "escalate",
@@ -128,7 +127,7 @@ export async function analyzeBlockRequest(
128
127
  return {
129
128
  action: "create_subtask",
130
129
  subtask: {
131
- title: `Support: ${blockReason.slice(0, 60)}`,
130
+ title: `Support: ${blockReason}`,
132
131
  agent: "fullstack",
133
132
  description: `Created from block request by ${agentName} on task ${taskId}: ${blockReason}`,
134
133
  },