pi-squad 0.7.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -317
- package/package.json +18 -2
- package/src/advisor.ts +145 -0
- package/src/agent-pool.ts +39 -15
- package/src/agents/_defaults/architect.json +8 -1
- package/src/agents/_defaults/backend.json +9 -1
- package/src/agents/_defaults/debugger.json +8 -1
- package/src/agents/_defaults/devops.json +9 -1
- package/src/agents/_defaults/docs.json +8 -1
- package/src/agents/_defaults/frontend.json +10 -1
- package/src/agents/_defaults/fullstack.json +8 -1
- package/src/agents/_defaults/planner.json +7 -1
- package/src/agents/_defaults/qa.json +8 -1
- package/src/agents/_defaults/researcher.json +8 -1
- package/src/agents/_defaults/reviewer.json +10 -0
- package/src/agents/_defaults/security.json +8 -1
- package/src/index.ts +379 -78
- package/src/monitor.ts +54 -20
- package/src/panel/message-view.ts +2 -2
- package/src/panel/squad-panel.ts +3 -3
- package/src/panel/squad-widget.ts +3 -3
- package/src/panel/task-list.ts +2 -2
- package/src/plan-rules.ts +134 -0
- package/src/planner.ts +18 -14
- package/src/protocol.ts +10 -2
- package/src/scheduler.ts +293 -13
- package/src/skills/squad-code-review/SKILL.md +89 -0
- package/src/skills/squad-collaboration/SKILL.md +5 -1
- package/src/skills/squad-debugging/SKILL.md +61 -0
- package/src/skills/squad-protocol/SKILL.md +19 -0
- package/src/skills/squad-qa-testing/SKILL.md +19 -8
- package/src/skills/squad-supervisor/SKILL.md +85 -11
- package/src/store.ts +27 -1
- package/src/types.ts +49 -2
package/src/scheduler.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - Detects squad completion
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
12
14
|
import type { AgentDef, Squad, SquadConfig, Task, TaskMessage, TaskStatus } from "./types.js";
|
|
13
15
|
import { AgentPool, type AgentEvent } from "./agent-pool.js";
|
|
14
16
|
import { Monitor } from "./monitor.js";
|
|
@@ -16,6 +18,7 @@ import { Router } from "./router.js";
|
|
|
16
18
|
import * as store from "./store.js";
|
|
17
19
|
import { debug, logError } from "./logger.js";
|
|
18
20
|
import { buildAgentSystemPrompt } from "./protocol.js";
|
|
21
|
+
import { buildAdvisorConsultText, formatAdvisorSteerMessage, adviceNeedsHuman, type AdvisorConsultInput } from "./advisor.js";
|
|
19
22
|
|
|
20
23
|
// ============================================================================
|
|
21
24
|
// Types
|
|
@@ -44,6 +47,16 @@ export interface SchedulerEvent {
|
|
|
44
47
|
|
|
45
48
|
export type SchedulerEventListener = (event: SchedulerEvent) => void;
|
|
46
49
|
|
|
50
|
+
/** Host-session capabilities passed in by the extension (index.ts) */
|
|
51
|
+
export interface SchedulerSpawnContext {
|
|
52
|
+
/** Resolve a model string (or null = default model) to its context window in tokens */
|
|
53
|
+
resolveContextWindow?: (model: string | null) => number | undefined;
|
|
54
|
+
/** Resolve the squad default model/thinking policy (settings.json + main session state) */
|
|
55
|
+
getDefaultModelThinking?: () => { model?: string; thinking?: string };
|
|
56
|
+
/** Consult the advisor model with a curated digest. Returns advice text, or null when disabled/unavailable. */
|
|
57
|
+
consultAdvisor?: (input: AdvisorConsultInput) => Promise<string | null>;
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
// ============================================================================
|
|
48
61
|
// Scheduler
|
|
49
62
|
// ============================================================================
|
|
@@ -55,18 +68,22 @@ export class Scheduler {
|
|
|
55
68
|
private router: Router;
|
|
56
69
|
private listeners: SchedulerEventListener[] = [];
|
|
57
70
|
private skillPaths: string[] = [];
|
|
71
|
+
private spawnContext?: SchedulerSpawnContext;
|
|
58
72
|
private running = false;
|
|
59
73
|
/** Track spawn retries to allow one retry per task */
|
|
60
74
|
private spawnRetries = new Set<string>();
|
|
75
|
+
/** Periodic level-triggered reconcile (heals missed events / out-of-band store edits) */
|
|
76
|
+
private reconcileTimer: ReturnType<typeof setInterval> | null = null;
|
|
61
77
|
|
|
62
78
|
/** Get the project cwd for this squad (from squad.json) */
|
|
63
79
|
getProjectCwd(): string | undefined {
|
|
64
80
|
return store.loadSquad(this.squadId)?.cwd;
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
constructor(squadId: string, skillPaths: string[]) {
|
|
83
|
+
constructor(squadId: string, skillPaths: string[], spawnContext?: SchedulerSpawnContext) {
|
|
68
84
|
this.squadId = squadId;
|
|
69
85
|
this.skillPaths = skillPaths;
|
|
86
|
+
this.spawnContext = spawnContext;
|
|
70
87
|
this.pool = new AgentPool();
|
|
71
88
|
this.monitor = new Monitor(this.pool, squadId);
|
|
72
89
|
this.router = new Router(this.pool, squadId);
|
|
@@ -78,15 +95,28 @@ export class Scheduler {
|
|
|
78
95
|
this.monitor.onAction((action) => {
|
|
79
96
|
if (action.type === "steer") {
|
|
80
97
|
this.pool.steer(action.taskId, action.message);
|
|
81
|
-
} else if (action.type === "
|
|
82
|
-
|
|
83
|
-
|
|
98
|
+
} else if (action.type === "notify") {
|
|
99
|
+
// Informational only — tell the main Pi session directly (no advisor
|
|
100
|
+
// detour, no kill). The agent keeps working.
|
|
84
101
|
this.emit({
|
|
85
102
|
type: "escalation",
|
|
86
103
|
squadId: this.squadId,
|
|
87
104
|
taskId: action.taskId,
|
|
105
|
+
agentName: action.agentName,
|
|
88
106
|
message: action.reason,
|
|
89
107
|
});
|
|
108
|
+
} else if (action.type === "escalate") {
|
|
109
|
+
// Advisor-first: try a strong-model rescue before interrupting the human
|
|
110
|
+
void this.tryAdvisorRescue(action.taskId, action.agentName, action.reason).then((rescued) => {
|
|
111
|
+
if (rescued) return;
|
|
112
|
+
this.emit({
|
|
113
|
+
type: "escalation",
|
|
114
|
+
squadId: this.squadId,
|
|
115
|
+
taskId: action.taskId,
|
|
116
|
+
agentName: action.agentName,
|
|
117
|
+
message: action.reason,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
90
120
|
}
|
|
91
121
|
});
|
|
92
122
|
|
|
@@ -140,13 +170,22 @@ export class Scheduler {
|
|
|
140
170
|
async start(): Promise<void> {
|
|
141
171
|
this.running = true;
|
|
142
172
|
this.monitor.start();
|
|
143
|
-
await this.
|
|
173
|
+
await this.reconcile();
|
|
174
|
+
// Level-triggered safety net: periodically re-derive scheduling decisions
|
|
175
|
+
// from persisted state so missed in-memory events (crashes, out-of-band
|
|
176
|
+
// store edits, external recovery) cannot strand ready tasks.
|
|
177
|
+
this.reconcileTimer = setInterval(() => void this.reconcile(), 60_000);
|
|
178
|
+
(this.reconcileTimer as { unref?: () => void }).unref?.();
|
|
144
179
|
}
|
|
145
180
|
|
|
146
181
|
/** Stop the scheduler — kills all agents, saves state */
|
|
147
182
|
async stop(): Promise<void> {
|
|
148
183
|
this.running = false;
|
|
149
184
|
this.monitor.stop();
|
|
185
|
+
if (this.reconcileTimer) {
|
|
186
|
+
clearInterval(this.reconcileTimer);
|
|
187
|
+
this.reconcileTimer = null;
|
|
188
|
+
}
|
|
150
189
|
|
|
151
190
|
// Suspend in-progress tasks
|
|
152
191
|
const tasks = store.loadAllTasks(this.squadId);
|
|
@@ -159,17 +198,26 @@ export class Scheduler {
|
|
|
159
198
|
await this.pool.killAll();
|
|
160
199
|
}
|
|
161
200
|
|
|
162
|
-
/** Resume from suspended state
|
|
201
|
+
/** Resume from suspended OR failed state. Failure is never terminal:
|
|
202
|
+
* failed tasks are reset to pending and a failed squad becomes running. */
|
|
163
203
|
async resume(): Promise<void> {
|
|
164
204
|
const tasks = store.loadAllTasks(this.squadId);
|
|
165
205
|
for (const task of tasks) {
|
|
166
206
|
if (task.status === "suspended") {
|
|
167
207
|
store.updateTaskStatus(this.squadId, task.id, "pending");
|
|
208
|
+
} else if (task.status === "failed") {
|
|
209
|
+
store.updateTaskStatus(this.squadId, task.id, "pending", { error: null });
|
|
210
|
+
store.appendMessage(this.squadId, task.id, {
|
|
211
|
+
ts: store.now(),
|
|
212
|
+
from: "system",
|
|
213
|
+
type: "status",
|
|
214
|
+
text: "Reset failed → pending on squad resume",
|
|
215
|
+
});
|
|
168
216
|
}
|
|
169
217
|
}
|
|
170
218
|
|
|
171
219
|
const squad = store.loadSquad(this.squadId);
|
|
172
|
-
if (squad && squad.status === "paused") {
|
|
220
|
+
if (squad && (squad.status === "paused" || squad.status === "failed")) {
|
|
173
221
|
squad.status = "running";
|
|
174
222
|
store.saveSquad(squad);
|
|
175
223
|
}
|
|
@@ -177,6 +225,52 @@ export class Scheduler {
|
|
|
177
225
|
await this.start();
|
|
178
226
|
}
|
|
179
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Level-triggered reconciliation — derive scheduling from persisted state:
|
|
230
|
+
* 1. Unblock blocked tasks whose deps are all done (missed autoUnblock events)
|
|
231
|
+
* 2. Self-heal a "failed" squad when runnable work exists (out-of-band recovery)
|
|
232
|
+
* 3. Schedule ready tasks
|
|
233
|
+
* Safe to call any time; no-ops when nothing changed.
|
|
234
|
+
*/
|
|
235
|
+
async reconcile(): Promise<void> {
|
|
236
|
+
if (!this.running) return;
|
|
237
|
+
const squad = store.loadSquad(this.squadId);
|
|
238
|
+
if (!squad) return;
|
|
239
|
+
|
|
240
|
+
const tasks = store.loadAllTasks(this.squadId);
|
|
241
|
+
|
|
242
|
+
// 1. Blocked → pending when all deps are done (respects autoUnblock config)
|
|
243
|
+
if (squad.config.autoUnblock) {
|
|
244
|
+
for (const task of tasks) {
|
|
245
|
+
if (task.status !== "blocked") continue;
|
|
246
|
+
const allDepsDone = task.depends.every((depId) => {
|
|
247
|
+
const dep = tasks.find((t) => t.id === depId);
|
|
248
|
+
return dep?.status === "done";
|
|
249
|
+
});
|
|
250
|
+
if (allDepsDone) {
|
|
251
|
+
task.status = "pending";
|
|
252
|
+
store.updateTaskStatus(this.squadId, task.id, "pending");
|
|
253
|
+
this.emit({ type: "task_unblocked", squadId: this.squadId, taskId: task.id });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 2. A "failed" squad with runnable work self-heals to running. This makes
|
|
259
|
+
// the terminal status derivable from task state instead of a one-way latch.
|
|
260
|
+
const hasRunnable = tasks.some((t) => {
|
|
261
|
+
if (t.status === "in_progress") return true;
|
|
262
|
+
if (t.status !== "pending") return false;
|
|
263
|
+
return t.depends.every((depId) => tasks.find((x) => x.id === depId)?.status === "done");
|
|
264
|
+
});
|
|
265
|
+
if (squad.status === "failed" && hasRunnable) {
|
|
266
|
+
squad.status = "running";
|
|
267
|
+
store.saveSquad(squad);
|
|
268
|
+
debug("squad-scheduler", "reconcile: failed squad has runnable work — healing to running");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await this.scheduleReadyTasks();
|
|
272
|
+
}
|
|
273
|
+
|
|
180
274
|
// =========================================================================
|
|
181
275
|
// Task Scheduling
|
|
182
276
|
// =========================================================================
|
|
@@ -248,11 +342,25 @@ export class Scheduler {
|
|
|
248
342
|
return;
|
|
249
343
|
}
|
|
250
344
|
|
|
251
|
-
// Apply squad-level model
|
|
345
|
+
// Apply squad-level model/thinking overrides
|
|
252
346
|
const squadAgentEntry = squad.agents[task.agent];
|
|
253
347
|
if (squadAgentEntry?.model) {
|
|
254
348
|
agentDef.model = squadAgentEntry.model;
|
|
255
349
|
}
|
|
350
|
+
if (squadAgentEntry?.thinking) {
|
|
351
|
+
agentDef.thinking = squadAgentEntry.thinking;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Apply squad defaults for anything still unset (policy resolved by the
|
|
355
|
+
// extension host: "main" = main session's model/thinking, "pi-default" =
|
|
356
|
+
// leave unset, or an explicit value from ~/.pi/squad/settings.json)
|
|
357
|
+
const defaults = this.spawnContext?.getDefaultModelThinking?.();
|
|
358
|
+
if (!agentDef.model && defaults?.model) {
|
|
359
|
+
agentDef.model = defaults.model;
|
|
360
|
+
}
|
|
361
|
+
if (!agentDef.thinking && defaults?.thinking) {
|
|
362
|
+
agentDef.thinking = defaults.thinking;
|
|
363
|
+
}
|
|
256
364
|
|
|
257
365
|
// Build modified files map from all running agents
|
|
258
366
|
const modifiedFiles: Record<string, string[]> = {};
|
|
@@ -285,6 +393,9 @@ export class Scheduler {
|
|
|
285
393
|
agentName: task.agent,
|
|
286
394
|
});
|
|
287
395
|
|
|
396
|
+
// Decide whether to fork the main session for context inheritance
|
|
397
|
+
const forkSessionFile = this.resolveForkSession(task, squad, agentDef);
|
|
398
|
+
|
|
288
399
|
try {
|
|
289
400
|
await this.pool.spawn({
|
|
290
401
|
taskId: task.id,
|
|
@@ -299,6 +410,14 @@ export class Scheduler {
|
|
|
299
410
|
},
|
|
300
411
|
cwd: squad.cwd,
|
|
301
412
|
skillPaths: this.skillPaths,
|
|
413
|
+
...(forkSessionFile
|
|
414
|
+
? {
|
|
415
|
+
forkSession: {
|
|
416
|
+
file: forkSessionFile,
|
|
417
|
+
sessionDir: path.join(store.getSquadDir(this.squadId), "sessions"),
|
|
418
|
+
},
|
|
419
|
+
}
|
|
420
|
+
: {}),
|
|
302
421
|
});
|
|
303
422
|
} catch (error) {
|
|
304
423
|
this.handleTaskFailed(task.id, (error as Error).message);
|
|
@@ -307,6 +426,123 @@ export class Scheduler {
|
|
|
307
426
|
this.updateContext();
|
|
308
427
|
}
|
|
309
428
|
|
|
429
|
+
/** Advisor consultations per task (advisor-first escalation) */
|
|
430
|
+
private advisorAttempts = new Map<string, number>();
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Consult the advisor for a stuck agent and steer it with the advice.
|
|
434
|
+
* Returns true when the agent was steered (escalation suppressed).
|
|
435
|
+
* Returns false when the advisor is disabled, exhausted, unavailable,
|
|
436
|
+
* failed, or explicitly said the problem needs human input.
|
|
437
|
+
*/
|
|
438
|
+
private async tryAdvisorRescue(taskId: string, agentName: string | undefined, reason: string): Promise<boolean> {
|
|
439
|
+
try {
|
|
440
|
+
const consult = this.spawnContext?.consultAdvisor;
|
|
441
|
+
if (!consult) return false;
|
|
442
|
+
|
|
443
|
+
const settings = store.loadSquadSettings();
|
|
444
|
+
if (!settings.advisor.enabled) return false;
|
|
445
|
+
|
|
446
|
+
const attempts = this.advisorAttempts.get(taskId) || 0;
|
|
447
|
+
if (attempts >= settings.advisor.maxCallsPerTask) {
|
|
448
|
+
debug("squad-advisor", `${taskId}: advisor exhausted (${attempts}/${settings.advisor.maxCallsPerTask}), escalating`);
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
if (!this.pool.isRunning(taskId)) return false;
|
|
452
|
+
|
|
453
|
+
const task = store.loadTask(this.squadId, taskId);
|
|
454
|
+
const squad = store.loadSquad(this.squadId);
|
|
455
|
+
if (!task || !squad) return false;
|
|
456
|
+
const agentDef = store.loadAgentDef(task.agent, squad.cwd);
|
|
457
|
+
const activity = this.pool.getActivity(taskId);
|
|
458
|
+
|
|
459
|
+
this.advisorAttempts.set(taskId, attempts + 1);
|
|
460
|
+
|
|
461
|
+
const input: AdvisorConsultInput = {
|
|
462
|
+
taskId,
|
|
463
|
+
taskTitle: task.title,
|
|
464
|
+
taskDescription: task.description,
|
|
465
|
+
agentName: task.agent,
|
|
466
|
+
agentRole: agentDef?.role || task.agent,
|
|
467
|
+
reason,
|
|
468
|
+
recentMessages: store.loadMessages(this.squadId, taskId).slice(-12).map((m) => ({ from: m.from, type: m.type, text: m.text })),
|
|
469
|
+
recentToolCalls: activity ? [...activity.recentToolCalls] : [],
|
|
470
|
+
turnCount: activity?.turnCount || 0,
|
|
471
|
+
elapsedMinutes: activity ? (Date.now() - activity.startedAt) / 60000 : 0,
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const advice = await consult(input);
|
|
475
|
+
if (!advice) return false;
|
|
476
|
+
|
|
477
|
+
store.appendMessage(this.squadId, taskId, {
|
|
478
|
+
ts: store.now(),
|
|
479
|
+
from: "advisor",
|
|
480
|
+
type: "message",
|
|
481
|
+
text: advice,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Advisor says a human decision is required — escalate with the advice attached
|
|
485
|
+
if (adviceNeedsHuman(advice)) {
|
|
486
|
+
this.emit({
|
|
487
|
+
type: "escalation",
|
|
488
|
+
squadId: this.squadId,
|
|
489
|
+
taskId,
|
|
490
|
+
agentName,
|
|
491
|
+
message: `${reason}\n\nAdvisor assessment:\n${advice.slice(0, 800)}`,
|
|
492
|
+
});
|
|
493
|
+
return true; // escalation already emitted with richer context
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const delivered = await this.pool.steer(taskId, formatAdvisorSteerMessage(advice, reason));
|
|
497
|
+
debug("squad-advisor", `${taskId}: advisor steered agent (attempt ${attempts + 1}, delivered=${delivered})`);
|
|
498
|
+
return delivered;
|
|
499
|
+
} catch (error) {
|
|
500
|
+
logError("squad-advisor", `rescue failed for ${taskId}: ${(error as Error).message}`);
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Decide whether this task's agent should be spawned as a fork of the main
|
|
507
|
+
* pi session (context inheritance). Guards against blowing the child model's
|
|
508
|
+
* context window: forks only when the estimated session tokens fit within
|
|
509
|
+
* 50% of the agent model's context window.
|
|
510
|
+
*/
|
|
511
|
+
private resolveForkSession(task: Task, squad: Squad, agentDef: AgentDef): string | undefined {
|
|
512
|
+
if (!task.inheritContext) return undefined;
|
|
513
|
+
|
|
514
|
+
const skip = (reason: string): undefined => {
|
|
515
|
+
logError("squad-scheduler", `inheritContext skipped for ${task.id}: ${reason}`);
|
|
516
|
+
store.appendMessage(this.squadId, task.id, {
|
|
517
|
+
ts: store.now(),
|
|
518
|
+
from: "system",
|
|
519
|
+
type: "status",
|
|
520
|
+
text: `Context inheritance skipped: ${reason}. Agent starts with standard squad context only.`,
|
|
521
|
+
});
|
|
522
|
+
return undefined;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const sessionFile = squad.sessionFile;
|
|
526
|
+
if (!sessionFile) return skip("main session has no session file (ephemeral --no-session run)");
|
|
527
|
+
if (!fs.existsSync(sessionFile)) return skip(`session file not found: ${sessionFile}`);
|
|
528
|
+
|
|
529
|
+
// Rough token estimate: JSONL bytes / 4. Overestimates (JSON overhead), which is safe.
|
|
530
|
+
const estTokens = Math.ceil(fs.statSync(sessionFile).size / 4);
|
|
531
|
+
|
|
532
|
+
const window = this.spawnContext?.resolveContextWindow?.(agentDef.model ?? null);
|
|
533
|
+
if (!window) {
|
|
534
|
+
return skip(`cannot determine context window for model "${agentDef.model || "(default)"}"`);
|
|
535
|
+
}
|
|
536
|
+
if (estTokens > window * 0.5) {
|
|
537
|
+
return skip(
|
|
538
|
+
`estimated session context (~${Math.round(estTokens / 1000)}k tokens) exceeds 50% of ${agentDef.model || "default model"}'s ${Math.round(window / 1000)}k window — restate key context in the task description instead`,
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
debug("squad-scheduler", `inheritContext: forking ${sessionFile} for ${task.id} (~${Math.round(estTokens / 1000)}k tokens, window ${Math.round(window / 1000)}k)`);
|
|
543
|
+
return sessionFile;
|
|
544
|
+
}
|
|
545
|
+
|
|
310
546
|
// =========================================================================
|
|
311
547
|
// Event Handlers
|
|
312
548
|
// =========================================================================
|
|
@@ -599,7 +835,7 @@ export class Scheduler {
|
|
|
599
835
|
*/
|
|
600
836
|
private checkForRework(task: Task, output: string): boolean {
|
|
601
837
|
// Only trigger rework for QA/test agent tasks
|
|
602
|
-
const qaAgents = ["qa", "tester", "security"];
|
|
838
|
+
const qaAgents = ["qa", "tester", "security", "reviewer"];
|
|
603
839
|
if (!qaAgents.includes(task.agent)) return false;
|
|
604
840
|
|
|
605
841
|
// Parse verdict from output
|
|
@@ -610,7 +846,7 @@ export class Scheduler {
|
|
|
610
846
|
const allTasks = store.loadAllTasks(this.squadId);
|
|
611
847
|
const implDeps = task.depends
|
|
612
848
|
.map((depId) => allTasks.find((t) => t.id === depId))
|
|
613
|
-
.filter((t): t is Task => t !==
|
|
849
|
+
.filter((t): t is Task => t !== undefined && !qaAgents.includes(t.agent));
|
|
614
850
|
|
|
615
851
|
if (implDeps.length === 0) return false;
|
|
616
852
|
|
|
@@ -649,6 +885,7 @@ export class Scheduler {
|
|
|
649
885
|
agent: implTask.agent,
|
|
650
886
|
status: "pending",
|
|
651
887
|
depends: [],
|
|
888
|
+
...(implTask.inheritContext ? { inheritContext: true } : {}),
|
|
652
889
|
created: store.now(),
|
|
653
890
|
started: null,
|
|
654
891
|
completed: null,
|
|
@@ -910,10 +1147,53 @@ export class Scheduler {
|
|
|
910
1147
|
this.updateContext();
|
|
911
1148
|
}
|
|
912
1149
|
|
|
913
|
-
/** Resume a suspended task */
|
|
1150
|
+
/** Resume a suspended/failed task. Reconciles so a failed squad heals too. */
|
|
914
1151
|
async resumeTask(taskId: string): Promise<void> {
|
|
915
|
-
store.updateTaskStatus(this.squadId, taskId, "pending");
|
|
916
|
-
await this.
|
|
1152
|
+
store.updateTaskStatus(this.squadId, taskId, "pending", { error: null });
|
|
1153
|
+
await this.reconcile();
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Mark a task done through the normal completion flow (admin/recovery path).
|
|
1158
|
+
* Unlike editing the store directly, this fires auto-unblock and scheduling,
|
|
1159
|
+
* so dependents transition pending → running. Skips the QA rework check —
|
|
1160
|
+
* a human/main agent marking a task done is an explicit override.
|
|
1161
|
+
*/
|
|
1162
|
+
async completeTask(taskId: string, output?: string): Promise<void> {
|
|
1163
|
+
const task = store.loadTask(this.squadId, taskId);
|
|
1164
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
1165
|
+
if (task.status === "done") return;
|
|
1166
|
+
|
|
1167
|
+
if (this.pool.isRunning(taskId)) {
|
|
1168
|
+
await this.pool.kill(taskId);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
store.updateTaskStatus(this.squadId, taskId, "done", {
|
|
1172
|
+
output: output ?? task.output ?? "Marked done (recovered/admin)",
|
|
1173
|
+
error: null,
|
|
1174
|
+
completed: store.now(),
|
|
1175
|
+
});
|
|
1176
|
+
store.appendMessage(this.squadId, taskId, {
|
|
1177
|
+
ts: store.now(),
|
|
1178
|
+
from: "system",
|
|
1179
|
+
type: "done",
|
|
1180
|
+
text: "Task marked done (recovered/admin)",
|
|
1181
|
+
});
|
|
1182
|
+
this.emit({
|
|
1183
|
+
type: "task_completed",
|
|
1184
|
+
squadId: this.squadId,
|
|
1185
|
+
taskId,
|
|
1186
|
+
agentName: task.agent,
|
|
1187
|
+
message: output ?? "",
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
this.autoUnblock(taskId);
|
|
1191
|
+
await this.reconcile();
|
|
1192
|
+
|
|
1193
|
+
const freshTasks = store.loadAllTasks(this.squadId);
|
|
1194
|
+
const freshSquad = store.loadSquad(this.squadId);
|
|
1195
|
+
if (freshSquad) this.checkSquadCompletion(freshTasks, freshSquad);
|
|
1196
|
+
this.updateContext();
|
|
917
1197
|
}
|
|
918
1198
|
|
|
919
1199
|
/** Cancel a task */
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: squad-code-review
|
|
3
|
+
description: >
|
|
4
|
+
Code review methodology — two-pass review (correctness/scope, then
|
|
5
|
+
over-engineering), surgical-change discipline, complexity findings with
|
|
6
|
+
delete/stdlib/native/yagni/shrink tags, machine-parsed verdicts. Use when
|
|
7
|
+
reviewing diffs, implementations, or pull requests.
|
|
8
|
+
version: 1.0.0
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Code Review
|
|
12
|
+
|
|
13
|
+
Two passes, in order. Findings from pass 1 gate the verdict; pass 2 findings
|
|
14
|
+
are usually non-blocking. Derived from the karpathy-guidelines and ponytail
|
|
15
|
+
review disciplines.
|
|
16
|
+
|
|
17
|
+
## Pass 1 — Correctness & Scope
|
|
18
|
+
|
|
19
|
+
**Every changed line must trace to the task.**
|
|
20
|
+
- Flag unrequested refactors, drive-by "improvements", formatting churn in
|
|
21
|
+
untouched code, and deleted code the task didn't ask to remove
|
|
22
|
+
- Orphan check: did the change leave now-unused imports/variables/functions
|
|
23
|
+
it created? (Pre-existing dead code is a mention, not a finding)
|
|
24
|
+
|
|
25
|
+
**Verify with evidence, not by reading.**
|
|
26
|
+
- Run the task's Verify criterion yourself (tests, build, curl). Your review
|
|
27
|
+
is only as strong as the commands you ran
|
|
28
|
+
- Reproduce at least one happy path and one failure path where practical
|
|
29
|
+
- If you can't run something, say so explicitly — don't imply you did
|
|
30
|
+
|
|
31
|
+
**Assumptions and interpretations.**
|
|
32
|
+
- If the implementation silently picked one of several readings of the task,
|
|
33
|
+
name the choice and whether it matches the task's Goal/Boundaries
|
|
34
|
+
- Check the task's Boundaries were respected (unchanged APIs, schemas, configs)
|
|
35
|
+
|
|
36
|
+
**Error handling in the right places.**
|
|
37
|
+
- Trust boundaries (user input, network, file I/O) must handle failure
|
|
38
|
+
- Impossible-scenario handling is noise — flag it in pass 2 as yagni
|
|
39
|
+
|
|
40
|
+
## Pass 2 — Complexity Hunt
|
|
41
|
+
|
|
42
|
+
Climb the ladder for each addition; stop at the first rung that holds:
|
|
43
|
+
need to exist? → already in codebase? → stdlib? → native platform? →
|
|
44
|
+
installed dep? → one line? → minimum that works.
|
|
45
|
+
|
|
46
|
+
One line per finding — location, what to cut, what replaces it:
|
|
47
|
+
|
|
48
|
+
- `delete:` dead code, unused flexibility, speculative feature → nothing
|
|
49
|
+
- `stdlib:` hand-rolled thing the standard library ships → name the function
|
|
50
|
+
- `native:` dep/code doing what the platform does → name the feature
|
|
51
|
+
- `yagni:` abstraction with one implementation, config nobody sets, layer with one caller
|
|
52
|
+
- `shrink:` same logic, fewer lines → show the shorter form
|
|
53
|
+
|
|
54
|
+
Example: `api.ts:L88: yagni: RepositoryFactory with one product. Inline it until a second exists.`
|
|
55
|
+
|
|
56
|
+
**Never flag as bloat**: trust-boundary validation, data-loss handling,
|
|
57
|
+
security, accessibility, or a minimal test. Lean is not careless.
|
|
58
|
+
|
|
59
|
+
If nothing to cut: `Lean already.` — don't invent findings to look thorough.
|
|
60
|
+
|
|
61
|
+
## Verdict (machine-parsed — REQUIRED)
|
|
62
|
+
|
|
63
|
+
End your final message with exactly:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
## Verdict: PASS | FAIL | PASS WITH ISSUES
|
|
67
|
+
|
|
68
|
+
## Issues
|
|
69
|
+
1. **[file:line]** (Critical/High/Medium/Low) description
|
|
70
|
+
- Expected: X
|
|
71
|
+
- Got: Y
|
|
72
|
+
- Fix: specific change
|
|
73
|
+
|
|
74
|
+
## Evidence
|
|
75
|
+
[commands run + output]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- **FAIL**: pass-1 problems (correctness, scope violations, broken Verify).
|
|
79
|
+
Creates a rework task — the fixer only sees your Issues section
|
|
80
|
+
- **PASS WITH ISSUES**: works and in-scope, but pass-2 cuts are worth making
|
|
81
|
+
- **PASS**: correct, in scope, lean. Say what you verified
|
|
82
|
+
- Severity: correctness/security → Critical/High; complexity → Medium/Low
|
|
83
|
+
- Do not rename the sections — `## Issues` is extracted verbatim as fix feedback
|
|
84
|
+
|
|
85
|
+
## You Don't Fix
|
|
86
|
+
|
|
87
|
+
You report; the squad routes fixes to the original agent. Applying fixes
|
|
88
|
+
yourself would bypass the rework loop and confuse file ownership. If a fix
|
|
89
|
+
is truly one character, still report it — with the exact edit in the Fix line.
|
|
@@ -22,10 +22,14 @@ Bad: "@backend FYI I'm working on the frontend" (no question, wastes their time)
|
|
|
22
22
|
Don't just post conclusions. Explain why you made a choice, so others can course-correct early.
|
|
23
23
|
"I chose RS256 over HS256 because the frontend needs to verify tokens without the signing secret"
|
|
24
24
|
|
|
25
|
-
## Admit uncertainty
|
|
25
|
+
## Admit uncertainty — flag gaps, never guess
|
|
26
26
|
If you're not sure about something, say so and ask.
|
|
27
27
|
"I'm not sure if this migration is backwards-compatible — @backend can you verify?"
|
|
28
28
|
|
|
29
|
+
When required information is missing from your task or dependency outputs, flag the gap
|
|
30
|
+
and ask — don't invent values, endpoints, or formats to fill it. A wrong guess propagates
|
|
31
|
+
to every dependent task; a question costs one message.
|
|
32
|
+
|
|
29
33
|
Better to ask than to silently introduce a breaking change.
|
|
30
34
|
|
|
31
35
|
## Respond when asked
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: squad-debugging
|
|
3
|
+
description: >
|
|
4
|
+
Debugging and rework discipline — reproduce-first workflow, hypothesis testing,
|
|
5
|
+
minimal fixes, regression tests. Use when fixing bugs, handling QA feedback,
|
|
6
|
+
working on rework/fix tasks, or investigating failures.
|
|
7
|
+
version: 1.0.0
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Debugging & Rework
|
|
11
|
+
|
|
12
|
+
## The Iron Rule: Reproduce First
|
|
13
|
+
|
|
14
|
+
Never change code before you've seen the failure with your own eyes.
|
|
15
|
+
|
|
16
|
+
1. **REPRODUCE**: Run the failing test, repro steps, or command from the QA
|
|
17
|
+
feedback. Confirm you see the same failure. If you can't reproduce it,
|
|
18
|
+
say so explicitly — don't "fix" what you can't observe.
|
|
19
|
+
2. **LOCALIZE**: Read the error, stack trace, or diff. Form ONE hypothesis
|
|
20
|
+
about the cause. State it before changing anything.
|
|
21
|
+
3. **FIX**: Make the smallest change that addresses the hypothesized cause.
|
|
22
|
+
4. **VERIFY**: Re-run the exact reproduction from step 1 — it must now pass.
|
|
23
|
+
Then run the surrounding test suite to catch regressions.
|
|
24
|
+
5. **PROTECT**: Add a regression test if none exists for this failure mode.
|
|
25
|
+
|
|
26
|
+
## Hypothesis Discipline
|
|
27
|
+
|
|
28
|
+
- One hypothesis at a time. If the fix doesn't work, REVERT it before trying
|
|
29
|
+
the next hypothesis — never stack speculative changes
|
|
30
|
+
- If two hypotheses fail, stop and gather more evidence (add logging, isolate
|
|
31
|
+
with a minimal test case) instead of guessing a third time
|
|
32
|
+
- Distinguish "the symptom moved" from "the cause is fixed" — re-run the
|
|
33
|
+
ORIGINAL repro, not just your new test
|
|
34
|
+
|
|
35
|
+
## Rework Tasks (QA sent your work back)
|
|
36
|
+
|
|
37
|
+
- Fix ONLY the issues listed in the QA feedback — no rewrites, no opportunistic
|
|
38
|
+
refactoring, no unrelated improvements
|
|
39
|
+
- Reproduce each reported issue before touching code (the feedback includes
|
|
40
|
+
Expected/Got/Repro for each — use them)
|
|
41
|
+
- If a reported issue looks wrong or is not reproducible, say so explicitly
|
|
42
|
+
with evidence instead of silently skipping it
|
|
43
|
+
- Your completion message must show each issue → what changed → re-run evidence
|
|
44
|
+
|
|
45
|
+
## Common Traps
|
|
46
|
+
|
|
47
|
+
- **Fixing the test instead of the code** — only adjust a test when you can
|
|
48
|
+
argue the test itself was wrong, and say so
|
|
49
|
+
- **Shotgun debugging** — many small changes at once; you won't know which
|
|
50
|
+
one mattered and you'll introduce new breakage
|
|
51
|
+
- **Environment blame** — "works on my machine" requires evidence: show the
|
|
52
|
+
environment difference, don't assume it
|
|
53
|
+
- **Silent scope creep** — noticing other bugs is good; fixing them inside a
|
|
54
|
+
rework task is not. Mention them so the squad can create tasks
|
|
55
|
+
|
|
56
|
+
## Output Checklist
|
|
57
|
+
|
|
58
|
+
Your completion message must include:
|
|
59
|
+
1. Each issue: root cause (one sentence) + the fix (file:line)
|
|
60
|
+
2. Evidence: the original failing command now passing (actual output)
|
|
61
|
+
3. Regression scope: which suites you ran and their results
|
|
@@ -23,6 +23,17 @@ Messages from other agents and the human arrive as interruptions in your convers
|
|
|
23
23
|
They are prefixed with `[squad]`. Read them carefully, incorporate the information,
|
|
24
24
|
and continue your work. Don't ignore incoming messages.
|
|
25
25
|
|
|
26
|
+
### Advisor messages
|
|
27
|
+
If you appear stuck, a senior advisor model may review your situation. Its guidance
|
|
28
|
+
arrives as `[squad advisor]` with a verdict and numbered action items.
|
|
29
|
+
Execute the action items unless your evidence contradicts them — in that case,
|
|
30
|
+
state the conflict explicitly instead of silently ignoring the advice.
|
|
31
|
+
|
|
32
|
+
### Reading your task
|
|
33
|
+
Task descriptions are structured as: Goal (the outcome), Context (where to look),
|
|
34
|
+
Output (deliverable), Boundaries (what must NOT change), Verify (the command that
|
|
35
|
+
proves you're done). Honor the Boundaries; run the Verify command before claiming done.
|
|
36
|
+
|
|
26
37
|
### Completion
|
|
27
38
|
When you finish your task, clearly state your output in your last message.
|
|
28
39
|
Be specific about what you built, what files you changed, and how to verify it works.
|
|
@@ -63,3 +74,11 @@ The squad system monitors your activity and will intervene, but being explicit i
|
|
|
63
74
|
### Read before writing
|
|
64
75
|
Before modifying any file, read it first. Another agent may have changed it
|
|
65
76
|
since the last time you saw it.
|
|
77
|
+
|
|
78
|
+
### Boundaries
|
|
79
|
+
- If required information is missing or ambiguous, ask (@mention or escalate) —
|
|
80
|
+
flag gaps instead of guessing or inventing
|
|
81
|
+
- Keep changes minimal and within your task's scope — no unrequested refactors or polish
|
|
82
|
+
- Keep public APIs, schemas, and configs unchanged unless your task says otherwise
|
|
83
|
+
- Never take externally visible actions (git push, deploy, publish, send messages)
|
|
84
|
+
unless your task explicitly instructs it — prepare, don't ship
|
|
@@ -40,19 +40,30 @@ Every claim must have evidence. Don't just say "it works" — show it:
|
|
|
40
40
|
- **UI tests**: Describe what you see, or use screenshots
|
|
41
41
|
- **Error tests**: Show the error response for invalid input
|
|
42
42
|
|
|
43
|
-
## Verdict Format
|
|
43
|
+
## Verdict Format (machine-parsed — use EXACTLY this structure)
|
|
44
|
+
|
|
45
|
+
Your final message MUST end with this structure. The squad system parses the
|
|
46
|
+
`## Verdict:` line to decide pass/fail, and extracts the `## Issues` section
|
|
47
|
+
(exactly `## Issues`, two hashes) as feedback for the fixing agent:
|
|
48
|
+
|
|
44
49
|
```
|
|
45
|
-
## Verdict: PASS | FAIL
|
|
50
|
+
## Verdict: PASS | FAIL | PASS WITH ISSUES
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
## Issues
|
|
53
|
+
1. **[file:line or endpoint]** (Critical/High/Medium/Low) Description
|
|
54
|
+
- Expected: X
|
|
55
|
+
- Got: Y
|
|
56
|
+
- Repro: exact command or steps
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
[test output, curl commands,
|
|
58
|
+
## Evidence
|
|
59
|
+
[test output, curl commands and responses, build output]
|
|
54
60
|
```
|
|
55
61
|
|
|
62
|
+
- `FAIL` automatically creates a rework task for the original agent — it only
|
|
63
|
+
sees your `## Issues` section, so make every issue specific and reproducible
|
|
64
|
+
- `PASS WITH ISSUES` = working but with non-blocking concerns (listed under `## Issues`)
|
|
65
|
+
- Do NOT rename the sections (`### Issues Found` etc. breaks feedback extraction)
|
|
66
|
+
|
|
56
67
|
## Rework Flow
|
|
57
68
|
If issues are found:
|
|
58
69
|
1. Document each issue with severity, location, and reproduction steps
|