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/monitor.ts CHANGED
@@ -11,16 +11,23 @@
11
11
 
12
12
  import type { AgentPool } from "./agent-pool.js";
13
13
  import type { HealthStatus } from "./types.js";
14
+ import { debug } from "./logger.js";
14
15
 
15
16
  // ============================================================================
16
17
  // Config
17
18
  // ============================================================================
18
19
 
19
- const IDLE_WARNING_MS = 3 * 60 * 1000; // 3 min no output → warn
20
- const STUCK_TIMEOUT_MS = 5 * 60 * 1000; // 5 min no output → intervene
21
- const HARD_CEILING_MS = 30 * 60 * 1000; // 30 min total → force stop
20
+ /** Env-overridable for testing (PI_SQUAD_IDLE_MS etc.) */
21
+ function envMs(name: string, fallback: number): number {
22
+ const v = Number(process.env[name]);
23
+ return Number.isFinite(v) && v > 0 ? v : fallback;
24
+ }
25
+
26
+ const IDLE_WARNING_MS = envMs("PI_SQUAD_IDLE_MS", 3 * 60 * 1000); // no output → warn
27
+ const STUCK_TIMEOUT_MS = envMs("PI_SQUAD_STUCK_MS", 5 * 60 * 1000); // no output → intervene
28
+ const HARD_CEILING_MS = envMs("PI_SQUAD_CEILING_MS", 30 * 60 * 1000); // total → force stop
22
29
  const LOOP_THRESHOLD = 5; // same tool call 5x → looping
23
- const POLL_INTERVAL_MS = 30 * 1000; // check every 30s
30
+ const POLL_INTERVAL_MS = envMs("PI_SQUAD_POLL_MS", 30 * 1000); // check interval
24
31
 
25
32
  // ============================================================================
26
33
  // Types
@@ -51,6 +58,8 @@ export class Monitor {
51
58
  private warned = new Set<string>();
52
59
  /** Track which agents have been steered for stuck (to avoid repeated steers) */
53
60
  private stuckSteered = new Set<string>();
61
+ /** Track which tasks have been escalated (one escalation per stuck episode) */
62
+ private escalated = new Set<string>();
54
63
 
55
64
  constructor(pool: AgentPool, squadId: string) {
56
65
  this.pool = pool;
@@ -90,6 +99,7 @@ export class Monitor {
90
99
  }
91
100
  this.warned.clear();
92
101
  this.stuckSteered.clear();
102
+ this.escalated.clear();
93
103
  }
94
104
 
95
105
  /** Check all running agents */
@@ -132,11 +142,13 @@ export class Monitor {
132
142
 
133
143
  /** Take action based on health status */
134
144
  private handleHealth(taskId: string, agentName: string, status: HealthStatus): void {
145
+ if (status !== "healthy") debug("squad-monitor", `${taskId} (${agentName}): ${status}`);
135
146
  switch (status) {
136
147
  case "healthy":
137
- // Clear warning flags
148
+ // Clear warning flags — a new stuck episode escalates again
138
149
  this.warned.delete(taskId);
139
150
  this.stuckSteered.delete(taskId);
151
+ this.escalated.delete(taskId);
140
152
  break;
141
153
 
142
154
  case "idle_warning":
@@ -166,8 +178,9 @@ export class Monitor {
166
178
  "Summarize what you've done and what's blocking you. " +
167
179
  "If you can't proceed, state what you need.",
168
180
  });
169
- } else {
170
- // Already steered once, escalate
181
+ } else if (!this.escalated.has(taskId)) {
182
+ // Already steered once — escalate, but only once per stuck episode
183
+ this.escalated.add(taskId);
171
184
  this.emit({
172
185
  type: "escalate",
173
186
  taskId,
@@ -4,8 +4,8 @@
4
4
  * TUI corruption from large message histories.
5
5
  */
6
6
 
7
- import type { Theme } from "@mariozechner/pi-coding-agent";
8
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
7
+ import type { Theme } from "@earendil-works/pi-coding-agent";
8
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
9
  import type { TaskMessage } from "../types.js";
10
10
  import * as store from "../store.js";
11
11
 
@@ -9,9 +9,9 @@
9
9
  * - done() closes the overlay cleanly
10
10
  */
11
11
 
12
- import type { Component, Focusable, TUI } from "@mariozechner/pi-tui";
13
- import { matchesKey, visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
14
- import type { Theme } from "@mariozechner/pi-coding-agent";
12
+ import type { Component, Focusable, TUI } from "@earendil-works/pi-tui";
13
+ import { matchesKey, visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
14
+ import type { Theme } from "@earendil-works/pi-coding-agent";
15
15
  import type { PanelState, PanelView } from "../types.js";
16
16
  import type { Scheduler } from "../scheduler.js";
17
17
  import { TaskListView } from "./task-list.js";
@@ -10,9 +10,9 @@
10
10
  * lines and calls setWidget() again.
11
11
  */
12
12
 
13
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
14
- import type { Component, TUI } from "@mariozechner/pi-tui";
15
- import type { Theme } from "@mariozechner/pi-coding-agent";
13
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
14
+ import type { Component, TUI } from "@earendil-works/pi-tui";
15
+ import type { Theme } from "@earendil-works/pi-coding-agent";
16
16
  import type { TaskStatus } from "../types.js";
17
17
  import * as store from "../store.js";
18
18
 
@@ -2,8 +2,8 @@
2
2
  * task-list.ts — Task tree view with status icons, live activity preview.
3
3
  */
4
4
 
5
- import type { Theme } from "@mariozechner/pi-coding-agent";
6
- import { truncateToWidth } from "@mariozechner/pi-tui";
5
+ import type { Theme } from "@earendil-works/pi-coding-agent";
6
+ import { truncateToWidth } from "@earendil-works/pi-tui";
7
7
  import type { Task, TaskStatus } from "../types.js";
8
8
  import type { Scheduler } from "../scheduler.js";
9
9
  import * as store from "../store.js";
@@ -0,0 +1,134 @@
1
+ /**
2
+ * plan-rules.ts — Single source of truth for squad planning behavior.
3
+ *
4
+ * Used by BOTH planning paths so they behave identically:
5
+ * - The planner agent (planner.ts injects these into its prompt)
6
+ * - The main pi session (index.ts injects these into the squad tool
7
+ * description / hint, and validatePlan() enforces them at submission)
8
+ */
9
+
10
+ // ============================================================================
11
+ // Shared prompt fragments
12
+ // ============================================================================
13
+
14
+ /** How to structure task descriptions (Goal/Context/Output/Boundaries/Verify) */
15
+ export const TASK_DESCRIPTION_GUIDE = `Structure each task description around these parts (include only the parts that help):
16
+ - **Goal**: the outcome, stated first. Describe the result, not step-by-step process — prescribe steps only when the process itself matters
17
+ - **Context**: the specific files, contracts, or dependency outputs the agent should read — name where to look, don't dump everything
18
+ - **Output**: the expected deliverable — files, format, level of detail
19
+ - **Boundaries**: what must stay unchanged (public APIs, schemas, config), what the agent must not do, and anything requiring escalation instead of guessing
20
+ - **Verify**: the exact command or check that proves the task is done (e.g. "npm test -- auth", "curl /api/health returns 200")`;
21
+
22
+ /** Structural rules for a good plan */
23
+ export const PLAN_STRUCTURE_RULES = `- Task IDs must be short kebab-case (e.g., "setup-db", "auth-middleware")
24
+ - Dependencies must reference task IDs from the same plan
25
+ - First task(s) should have empty depends: []
26
+ - When tasks share an interface (API endpoints, database schema, data formats), create a design/contract task first that defines the contract, and make consuming tasks depend on it
27
+ - Include a final QA/verification task if there are user-facing changes
28
+ - Don't over-decompose — 3-7 tasks is usually right for most goals
29
+ - Scope tasks to required work only — no optional polish or nice-to-haves unless the goal asks for them`;
30
+
31
+ // ============================================================================
32
+ // Plan validation (enforcement — applies to planner AND main-session plans)
33
+ // ============================================================================
34
+
35
+ export interface PlanTaskInput {
36
+ id: string;
37
+ title: string;
38
+ description: string;
39
+ agent: string;
40
+ depends: string[];
41
+ }
42
+
43
+ export interface PlanValidation {
44
+ /** Hard failures — squad must not start */
45
+ errors: string[];
46
+ /** Rule violations worth reporting back to the plan author */
47
+ warnings: string[];
48
+ }
49
+
50
+ /** Does this task look like a QA/verification/review task? */
51
+ function isQaLikeTask(task: PlanTaskInput): boolean {
52
+ const hay = `${task.id} ${task.title} ${task.agent}`.toLowerCase();
53
+ return /\b(qa|test|tests|testing|verif\w*|review|audit)\b/.test(hay);
54
+ }
55
+
56
+ /**
57
+ * Validate a plan's structure. Errors block squad creation;
58
+ * warnings are returned to the plan author for correction.
59
+ */
60
+ export function validatePlan(tasks: PlanTaskInput[]): PlanValidation {
61
+ const errors: string[] = [];
62
+ const warnings: string[] = [];
63
+
64
+ // Duplicate IDs
65
+ const ids = new Set<string>();
66
+ for (const t of tasks) {
67
+ if (ids.has(t.id)) errors.push(`Duplicate task id: "${t.id}"`);
68
+ ids.add(t.id);
69
+ }
70
+
71
+ // Unknown dependency references
72
+ for (const t of tasks) {
73
+ for (const dep of t.depends) {
74
+ if (!ids.has(dep)) {
75
+ errors.push(`Task "${t.id}" depends on unknown task "${dep}"`);
76
+ }
77
+ }
78
+ }
79
+
80
+ // Cycle detection (DFS, only over known deps)
81
+ const visiting = new Set<string>();
82
+ const visited = new Set<string>();
83
+ const byId = new Map(tasks.map((t) => [t.id, t]));
84
+ const inCycle: string[] = [];
85
+ const visit = (id: string): boolean => {
86
+ if (visited.has(id)) return false;
87
+ if (visiting.has(id)) return true;
88
+ visiting.add(id);
89
+ const task = byId.get(id);
90
+ if (task) {
91
+ for (const dep of task.depends) {
92
+ if (byId.has(dep) && visit(dep)) {
93
+ inCycle.push(id);
94
+ visiting.delete(id);
95
+ return true;
96
+ }
97
+ }
98
+ }
99
+ visiting.delete(id);
100
+ visited.add(id);
101
+ return false;
102
+ };
103
+ for (const t of tasks) {
104
+ if (visit(t.id) && inCycle.length > 0) {
105
+ errors.push(`Dependency cycle involving: ${[...new Set(inCycle)].join(", ")}`);
106
+ break;
107
+ }
108
+ }
109
+
110
+ // No entry point (every task has deps) — only meaningful without errors
111
+ if (tasks.length > 0 && errors.length === 0 && !tasks.some((t) => t.depends.length === 0)) {
112
+ errors.push("No task has empty depends — nothing can start");
113
+ }
114
+
115
+ // --- Warnings (planner rules) ---
116
+
117
+ if (tasks.length > 9) {
118
+ warnings.push(`${tasks.length} tasks — over-decomposed (3-7 is usually right). Consider merging related tasks.`);
119
+ }
120
+
121
+ if (tasks.length >= 3 && !tasks.some(isQaLikeTask)) {
122
+ warnings.push("No QA/verification task in the plan. Add a final task that tests the integrated result, or verify it yourself when the squad completes.");
123
+ }
124
+
125
+ for (const t of tasks) {
126
+ if (!t.description || t.description.trim().length === 0) {
127
+ warnings.push(`Task "${t.id}" has no description — the agent only gets the title.`);
128
+ } else if (!/verif|test|check|curl|run\b|npm |pnpm |cargo |pytest|tsc\b/i.test(t.description)) {
129
+ warnings.push(`Task "${t.id}" description has no Verify criterion — the agent won't know how to prove it's done.`);
130
+ }
131
+ }
132
+
133
+ return { errors, warnings };
134
+ }
package/src/planner.ts CHANGED
@@ -11,6 +11,7 @@ import * as os from "node:os";
11
11
  import * as path from "node:path";
12
12
  import type { AgentDef, PlannerOutput } from "./types.js";
13
13
  import { loadAllAgentDefs, loadAgentDef } from "./store.js";
14
+ import { PLAN_STRUCTURE_RULES, TASK_DESCRIPTION_GUIDE } from "./plan-rules.js";
14
15
 
15
16
  // ============================================================================
16
17
  // Planner
@@ -21,6 +22,10 @@ export interface PlannerOptions {
21
22
  cwd: string;
22
23
  /** If provided, use this model for planning instead of the planner agent's default */
23
24
  model?: string;
25
+ /** Fallback model when neither `model` nor the planner agent def specifies one (squad default policy) */
26
+ fallbackModel?: string;
27
+ /** Fallback thinking level when the planner agent def doesn't specify one (squad default policy) */
28
+ fallbackThinking?: string;
24
29
  }
25
30
 
26
31
  /**
@@ -28,7 +33,7 @@ export interface PlannerOptions {
28
33
  * Returns the parsed plan or throws on failure.
29
34
  */
30
35
  export async function runPlanner(options: PlannerOptions): Promise<PlannerOutput> {
31
- const { goal, cwd, model } = options;
36
+ const { goal, cwd, model, fallbackModel, fallbackThinking } = options;
32
37
 
33
38
  const plannerDef = loadAgentDef("planner", cwd);
34
39
  const allAgents = loadAllAgentDefs(cwd).filter((a) => a.name !== "planner" && !a.disabled);
@@ -62,7 +67,8 @@ export async function runPlanner(options: PlannerOptions): Promise<PlannerOutput
62
67
  cwd,
63
68
  prompt: `Read the prompt file at ${promptFile} and follow the instructions.`,
64
69
  systemPromptFile: systemFile,
65
- model: model || plannerDef?.model || undefined,
70
+ model: model || plannerDef?.model || fallbackModel || undefined,
71
+ thinking: plannerDef?.thinking || fallbackThinking || undefined,
66
72
  });
67
73
 
68
74
  const agentNames = new Set(allAgents.map((a) => a.name));
@@ -87,13 +93,15 @@ interface PiJsonOptions {
87
93
  prompt: string;
88
94
  systemPromptFile?: string;
89
95
  model?: string;
96
+ thinking?: string;
90
97
  }
91
98
 
92
99
  async function runPiJson(options: PiJsonOptions): Promise<string> {
93
- const { cwd, prompt, systemPromptFile, model } = options;
100
+ const { cwd, prompt, systemPromptFile, model, thinking } = options;
94
101
 
95
102
  const args: string[] = ["--mode", "json", "-p", "--no-session"];
96
103
  if (model) args.push("--model", model);
104
+ if (thinking) args.push("--thinking", thinking);
97
105
  if (systemPromptFile) args.push("--append-system-prompt", systemPromptFile);
98
106
  args.push(prompt);
99
107
 
@@ -244,13 +252,13 @@ Respond with a JSON object (and nothing else outside the JSON):
244
252
  {
245
253
  "agents": {
246
254
  "agent-name": {},
247
- "agent-name": { "model": "override-model-if-needed" }
255
+ "agent-name": { "model": "override-model-if-needed", "thinking": "high" }
248
256
  },
249
257
  "tasks": [
250
258
  {
251
259
  "id": "short-kebab-id",
252
260
  "title": "Human-readable task title",
253
- "description": "Detailed description of what to implement",
261
+ "description": "Goal: the outcome to achieve. Context: relevant files/contracts to read. Output: expected deliverable and format. Boundaries: what must NOT change. Verify: command or check that proves it works.",
254
262
  "agent": "agent-name",
255
263
  "depends": ["id-of-dependency", "another-dependency"]
256
264
  }
@@ -259,16 +267,12 @@ Respond with a JSON object (and nothing else outside the JSON):
259
267
  \`\`\`
260
268
 
261
269
  ## Rules
262
- - Task IDs must be short kebab-case (e.g., "setup-db", "auth-middleware")
270
+ ${PLAN_STRUCTURE_RULES}
263
271
  - Only reference agents that exist in the Available Agents list
264
- - Dependencies must reference task IDs from your own plan
265
- - First task(s) should have empty depends: []
266
- - Include a final QA/verification task if there are user-facing changes
267
- - Keep descriptions actionable — agent should know exactly what to build
268
- - Don't over-decompose — 3-7 tasks is usually right for most goals
269
-
270
- ## Shared Contracts
271
- - When tasks share an interface (e.g., API endpoints, database schema, data formats), create a design/architecture task first that defines the contract, and make consuming tasks depend on it
272
+ - Agent overrides are optional: "model" (a pi model id) and "thinking" (one of: off, minimal, low, medium, high, xhigh, max). Omit both to use defaults. Raise "thinking" only for tasks needing deep reasoning (architecture, security)
273
+
274
+ ## Task Descriptions
275
+ ${TASK_DESCRIPTION_GUIDE}
272
276
  - If tasks run in parallel, they MUST depend on a shared spec task that defines the interface between them
273
277
  - Frontend tasks should depend on backend API tasks — the frontend agent needs a running API to test against
274
278
  - In each task description, specify the exact API paths, data schemas, and conventions that the agent should follow or create
package/src/protocol.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import type { AgentDef, KnowledgeEntry, Squad, Task, TaskMessage } from "./types.js";
14
- import { loadAllKnowledge, loadAllTasks, loadMessages, loadOverview } from "./store.js";
14
+ import { loadAllKnowledge, loadAllTasks, loadMessages } from "./store.js";
15
15
 
16
16
  // ============================================================================
17
17
  // Squad Protocol (injected into every agent)
@@ -54,11 +54,15 @@ The squad system will detect this and route help.
54
54
  ## Rules
55
55
  - Stay focused on YOUR task — don't do work assigned to other agents
56
56
  - Read the dependency outputs below — don't redo completed work
57
- - **Follow the Design Contract in the Squad Progress Document** — use the exact API paths, ports, schemas, and file names specified. Do NOT invent alternatives
58
57
  - Check the modified files list — coordinate before editing shared files
59
- - When creating APIs, clearly document all endpoints, request/response shapes, and status codes in your completion output
60
58
  - Ask for help if stuck — don't spin for more than a few minutes
61
- - Verify your work before claiming done
59
+ - Verify your work before claiming done — report the commands you ran and their results
60
+
61
+ ## Boundaries
62
+ - If required information is missing or ambiguous, ask (@mention or escalate) — flag gaps instead of guessing or inventing
63
+ - Keep changes minimal and within your task's scope — don't refactor unrelated code or add unrequested polish
64
+ - Respect your task's stated boundaries: keep public APIs, schemas, and configs unchanged unless the task says otherwise
65
+ - Never take externally visible actions (git push, deploy, publish, send messages/emails) unless your task explicitly instructs it — prepare, don't ship
62
66
  `;
63
67
  }
64
68
 
@@ -116,20 +120,6 @@ ${sections.join("\n---\n\n")}
116
120
  `;
117
121
  }
118
122
 
119
- // ============================================================================
120
- // Squad Progress Overview
121
- // ============================================================================
122
-
123
- export function buildOverviewSection(squadId: string): string {
124
- const content = loadOverview(squadId);
125
- if (!content.trim()) return "";
126
-
127
- return `# Squad Progress Document
128
-
129
- ${content.trim()}
130
- `;
131
- }
132
-
133
123
  // ============================================================================
134
124
  // Sibling Awareness
135
125
  // ============================================================================
@@ -278,10 +268,12 @@ function buildReworkContext(task: Task, squadId: string): string {
278
268
 
279
269
  lines.push("## Instructions");
280
270
  lines.push("- Read the QA feedback carefully — fix ONLY the reported issues");
271
+ lines.push("- FIRST reproduce each reported issue (run the failing test or repro steps) — confirm you see the failure before changing code");
281
272
  lines.push("- Do NOT rewrite everything from scratch");
282
273
  lines.push("- Make targeted, minimal fixes");
283
274
  lines.push("- Re-run the failing tests to verify your fixes");
284
- lines.push("- Include test output as evidence in your completion message\n");
275
+ lines.push("- Include test output as evidence in your completion message");
276
+ lines.push("- Follow the squad-debugging skill for the full repro-first rework discipline\n");
285
277
 
286
278
  return lines.join("\n");
287
279
  }
@@ -309,7 +301,6 @@ export function buildAgentSystemPrompt(options: ProtocolBuildOptions): string {
309
301
  buildTaskSection(task),
310
302
  buildReworkContext(task, squadId),
311
303
  buildChainContext(task, allTasks, squadId),
312
- buildOverviewSection(squadId),
313
304
  buildSiblingAwareness(task, allTasks, modifiedFiles),
314
305
  buildKnowledgeSection(squadId),
315
306
  buildQueuedMessages(queuedMessages),