pi-squad 0.16.4 → 0.16.5
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 +6 -3
- package/package.json +1 -1
- package/src/index.ts +99 -47
- package/src/review.ts +15 -0
- package/src/scheduler.ts +56 -19
- package/src/types.ts +3 -1
package/README.md
CHANGED
|
@@ -52,8 +52,10 @@ Squad agents—including QA/reviewer agents—produce candidate work and evidenc
|
|
|
52
52
|
- Persisted status becomes `review`, never directly `done`.
|
|
53
53
|
- A persistent `<squad_review_required>` system reminder tells main Pi to re-read the original conversation contract, inspect the actual diff/source, rerun verification independently, and run integration/E2E where applicable.
|
|
54
54
|
- Main Pi must call `squad_review` with requirement-by-requirement contract checks, diff review, actual command/result evidence, integration/E2E evidence, and issues.
|
|
55
|
-
- Only `pass` or `pass_with_issues` changes the squad to `done`; `fail` leaves it review-blocked
|
|
56
|
-
-
|
|
55
|
+
- Only `pass` or `pass_with_issues` changes the squad to `done`; `fail` leaves it review-blocked and cannot be overwritten by another verdict.
|
|
56
|
+
- Failed review is reworked in the **same authoritative squad**: use `squad_modify` with that `squadId` and `add_task` or `resume_task` (or `resume` when interrupted work exists). These operations reconstruct the scheduler after restart. `/squad resume <squad-id>` provides the same resume path.
|
|
57
|
+
- When rework begins, the failed attempt moves to `reviewHistory`, the squad returns to `running`, and its evidence remains auditable. After every rework task settles, a fresh pending review becomes the active gate and `squad_review` is required again.
|
|
58
|
+
- Pending and failed review gates survive Pi restarts and are restored on the next session. A separate squad never links to, remediates, or accepts the failed gate.
|
|
57
59
|
|
|
58
60
|
The completion report is explicitly labeled **untrusted and not yet accepted**. Main Pi must never merely relay it or ask whether verification should be run.
|
|
59
61
|
|
|
@@ -188,6 +190,7 @@ Full overlay with task list, live activity preview, and scrollable message view.
|
|
|
188
190
|
| Command | Description |
|
|
189
191
|
|---|---|
|
|
190
192
|
| `/squad select` | Pick a squad to view |
|
|
193
|
+
| `/squad resume [squad-id]` | Reconstruct and resume an exact paused/failed/failed-review squad |
|
|
191
194
|
| `/squad list` | List project squads |
|
|
192
195
|
| `/squad all` | List all squads |
|
|
193
196
|
| `/squad agents` | Manage agent definitions |
|
|
@@ -206,7 +209,7 @@ Full overlay with task list, live activity preview, and scrollable message view.
|
|
|
206
209
|
| `squad` | Start a squad with goal + optional tasks/config |
|
|
207
210
|
| `squad_status` | Check progress, costs, task states |
|
|
208
211
|
| `squad_message` | Durably message an exact task; completed tasks reopen on their original session |
|
|
209
|
-
| `squad_modify` | Add/cancel/complete/pause/resume tasks or squads |
|
|
212
|
+
| `squad_modify` | Add/cancel/complete/pause/resume tasks or squads; accepts `squadId` for exact same-squad failed-review rework |
|
|
210
213
|
|
|
211
214
|
The main agent sees available agents in its system prompt and squad state when a squad is active.
|
|
212
215
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -176,6 +176,38 @@ function getActiveScheduler(): Scheduler | null {
|
|
|
176
176
|
return schedulers.get(activeSquadId) || null;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
/** Reconstruct and focus one exact persisted squad without creating/linking another. */
|
|
180
|
+
function ensureScheduler(pi: ExtensionAPI, squadId: string, skillPaths: string[]): Scheduler {
|
|
181
|
+
let scheduler = schedulers.get(squadId);
|
|
182
|
+
if (!scheduler) {
|
|
183
|
+
scheduler = new Scheduler(squadId, skillPaths, schedulerSpawnContext);
|
|
184
|
+
schedulers.set(squadId, scheduler);
|
|
185
|
+
wireSchedulerEvents(pi, scheduler, squadId);
|
|
186
|
+
}
|
|
187
|
+
activeSquadId = squadId;
|
|
188
|
+
widgetState.squadId = squadId;
|
|
189
|
+
widgetState.enabled = true;
|
|
190
|
+
widgetControls?.requestUpdate();
|
|
191
|
+
return scheduler;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isResumeCandidate(squad: Squad): boolean {
|
|
195
|
+
return squad.status === "paused" || squad.status === "failed" ||
|
|
196
|
+
(squad.status === "review" && squad.review?.status === "failed") ||
|
|
197
|
+
store.loadAllTasks(squad.id).some((task) => task.status === "suspended" || task.status === "failed");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveResumeSquad(cwd: string, explicitId?: string): Squad | null {
|
|
201
|
+
if (explicitId) return store.loadSquad(explicitId);
|
|
202
|
+
if (activeSquadId) {
|
|
203
|
+
const active = store.loadSquad(activeSquadId);
|
|
204
|
+
if (active?.cwd === cwd && isResumeCandidate(active)) return active;
|
|
205
|
+
}
|
|
206
|
+
return store.listSquadsForProject(cwd)
|
|
207
|
+
.filter(isResumeCandidate)
|
|
208
|
+
.sort((a, b) => b.created.localeCompare(a.created))[0] ?? null;
|
|
209
|
+
}
|
|
210
|
+
|
|
179
211
|
|
|
180
212
|
// ============================================================================
|
|
181
213
|
// Extension Entry
|
|
@@ -560,8 +592,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
560
592
|
pi.registerTool({
|
|
561
593
|
name: "squad_modify",
|
|
562
594
|
label: "Squad Modify",
|
|
563
|
-
description: "Modify
|
|
595
|
+
description: "Modify one exact squad: add_task, cancel_task, complete_task (mark done + schedule dependents), pause, resume_task, resume (including failed-review rework), cancel. add_task/resume_task/resume reconstruct the persisted scheduler after restart.",
|
|
564
596
|
parameters: Type.Object({
|
|
597
|
+
squadId: Type.Optional(Type.String({ description: "Exact squad to modify (recommended for failed-review rework; default: focused/recoverable project squad)" })),
|
|
565
598
|
action: Type.Union(
|
|
566
599
|
[
|
|
567
600
|
Type.Literal("add_task"),
|
|
@@ -590,45 +623,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
590
623
|
}),
|
|
591
624
|
|
|
592
625
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
593
|
-
// Resume can work without an active scheduler — it recreates one from disk
|
|
594
626
|
if (params.action === "resume") {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
return { content: [{ type: "text" as const, text: "No paused or failed squad found to resume." }], details: undefined };
|
|
627
|
+
const squad = resolveResumeSquad(ctx.cwd, params.squadId);
|
|
628
|
+
if (!squad) {
|
|
629
|
+
const text = params.squadId
|
|
630
|
+
? `Squad '${params.squadId}' not found.`
|
|
631
|
+
: "No paused, failed, or failed-review squad found to resume.";
|
|
632
|
+
return { content: [{ type: "text" as const, text }], details: undefined };
|
|
602
633
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
activeSquadId = squadId;
|
|
609
|
-
|
|
610
|
-
// Activate widget
|
|
611
|
-
widgetState.squadId = squadId;
|
|
612
|
-
widgetState.enabled = true;
|
|
613
|
-
widgetControls?.requestUpdate();
|
|
614
|
-
|
|
615
|
-
wireSchedulerEvents(pi, scheduler, squadId);
|
|
634
|
+
const resumeSched = ensureScheduler(pi, squad.id, squadSkillPaths);
|
|
635
|
+
try {
|
|
636
|
+
await resumeSched.resume();
|
|
637
|
+
} catch (err) {
|
|
638
|
+
return { content: [{ type: "text" as const, text: `Resume failed: ${(err as Error).message}` }], details: undefined };
|
|
616
639
|
}
|
|
617
|
-
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
logError("squad", `Resume error: ${(err as Error).message}`);
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
const tasks = store.loadAllTasks(squadId);
|
|
624
|
-
const done = tasks.filter(t => t.status === "done").length;
|
|
625
|
-
return { content: [{ type: "text" as const, text: `Squad "${squadId}" resumed (${done}/${tasks.length} done). Agents restarting in background.` }], details: undefined };
|
|
640
|
+
const tasks = store.loadAllTasks(squad.id);
|
|
641
|
+
const done = tasks.filter((task) => task.status === "done").length;
|
|
642
|
+
return { content: [{ type: "text" as const, text: `Squad "${squad.id}" resumed (${done}/${tasks.length} done). Agents restarting in background.` }], details: undefined };
|
|
626
643
|
}
|
|
627
644
|
|
|
628
|
-
const
|
|
629
|
-
if (!
|
|
630
|
-
return { content: [{ type: "text" as const, text:
|
|
645
|
+
const squadId = params.squadId || activeSquadId;
|
|
646
|
+
if (!squadId || !store.loadSquad(squadId)) {
|
|
647
|
+
return { content: [{ type: "text" as const, text: params.squadId ? `Squad '${params.squadId}' not found.` : "No active squad. Provide squadId, select the squad, or start a new one." }], details: undefined };
|
|
648
|
+
}
|
|
649
|
+
let activeScheduler = schedulers.get(squadId) || null;
|
|
650
|
+
if (!activeScheduler && (params.action === "add_task" || params.action === "resume_task")) {
|
|
651
|
+
activeScheduler = ensureScheduler(pi, squadId, squadSkillPaths);
|
|
652
|
+
}
|
|
653
|
+
if (!activeScheduler) {
|
|
654
|
+
return { content: [{ type: "text" as const, text: `Squad '${squadId}' has no active scheduler. Use resume, add_task, or resume_task to reconstruct it.` }], details: undefined };
|
|
631
655
|
}
|
|
656
|
+
activeSquadId = squadId;
|
|
632
657
|
|
|
633
658
|
switch (params.action) {
|
|
634
659
|
case "add_task": {
|
|
@@ -645,17 +670,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
645
670
|
if (badDeps.length > 0) {
|
|
646
671
|
return { content: [{ type: "text" as const, text: `Unknown dependency task(s): ${badDeps.join(", ")}. Existing tasks: ${[...existingIds].join(", ")}` }], details: undefined };
|
|
647
672
|
}
|
|
648
|
-
|
|
649
|
-
|
|
673
|
+
const targetCwd = store.loadSquad(squadId)!.cwd;
|
|
674
|
+
if (!store.loadAgentDef(params.task.agent, targetCwd)) {
|
|
675
|
+
const available = store.loadAllAgentDefs(targetCwd).filter((a) => !a.disabled).map((a) => a.name).join(", ");
|
|
650
676
|
return { content: [{ type: "text" as const, text: `Unknown agent '${params.task.agent}'. Available: ${available}` }], details: undefined };
|
|
651
677
|
}
|
|
678
|
+
const dependencies = params.task.depends || [];
|
|
652
679
|
const task: Task = {
|
|
653
680
|
id: params.task.id,
|
|
654
681
|
title: params.task.title,
|
|
655
682
|
description: params.task.description || "",
|
|
656
683
|
agent: params.task.agent,
|
|
657
|
-
status: "pending",
|
|
658
|
-
depends:
|
|
684
|
+
status: dependencies.every((dependency) => existing.find((candidate) => candidate.id === dependency)?.status === "done") ? "pending" : "blocked",
|
|
685
|
+
depends: dependencies,
|
|
659
686
|
...(params.task.inheritContext ? { inheritContext: true } : {}),
|
|
660
687
|
created: store.now(),
|
|
661
688
|
started: null,
|
|
@@ -664,9 +691,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
664
691
|
error: null,
|
|
665
692
|
usage: { inputTokens: 0, outputTokens: 0, cost: 0, turns: 0 },
|
|
666
693
|
};
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
694
|
+
try {
|
|
695
|
+
await activeScheduler.addTask(task);
|
|
696
|
+
} catch (err) {
|
|
697
|
+
return { content: [{ type: "text" as const, text: `add_task failed: ${(err as Error).message}` }], details: undefined };
|
|
698
|
+
}
|
|
699
|
+
return { content: [{ type: "text" as const, text: `Task '${task.id}' added to squad '${squadId}'.` }], details: undefined };
|
|
670
700
|
}
|
|
671
701
|
|
|
672
702
|
case "cancel_task": {
|
|
@@ -683,10 +713,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
683
713
|
|
|
684
714
|
case "resume_task": {
|
|
685
715
|
if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }], details: undefined };
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
})
|
|
689
|
-
|
|
716
|
+
try {
|
|
717
|
+
await activeScheduler.resumeTask(params.taskId);
|
|
718
|
+
} catch (err) {
|
|
719
|
+
return { content: [{ type: "text" as const, text: `resume_task failed: ${(err as Error).message}` }], details: undefined };
|
|
720
|
+
}
|
|
721
|
+
return { content: [{ type: "text" as const, text: `Task '${params.taskId}' resumed in squad '${squadId}'.` }], details: undefined };
|
|
690
722
|
}
|
|
691
723
|
|
|
692
724
|
case "complete_task": {
|
|
@@ -866,12 +898,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
866
898
|
// =========================================================================
|
|
867
899
|
|
|
868
900
|
pi.registerCommand("squad", {
|
|
869
|
-
description: "Browse, select, and manage squads. Usage: /squad [list|all|select|agents|msg|widget|panel|cancel|clear]",
|
|
901
|
+
description: "Browse, select, and manage squads. Usage: /squad [list|all|select|resume|agents|msg|widget|panel|cancel|clear]",
|
|
870
902
|
getArgumentCompletions: (prefix) => {
|
|
871
903
|
const subs = [
|
|
872
904
|
{ value: "list", label: "list", description: "List squads for current project" },
|
|
873
905
|
{ value: "all", label: "all", description: "List all squads, select to activate" },
|
|
874
906
|
{ value: "select", label: "select", description: "Pick a squad to view (interactive)" },
|
|
907
|
+
{ value: "resume", label: "resume", description: "Resume an exact paused/failed/failed-review squad" },
|
|
875
908
|
{ value: "agents", label: "agents", description: "List, view, or edit agent definitions" },
|
|
876
909
|
{ value: "defaults", label: "defaults", description: "Default model/thinking for agents (follow main session, pi default, or fixed)" },
|
|
877
910
|
{ value: "advisor", label: "advisor", description: "Advisor-first rescue for stuck agents (on/off, model, limits)" },
|
|
@@ -941,6 +974,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
941
974
|
return;
|
|
942
975
|
}
|
|
943
976
|
|
|
977
|
+
case "resume": {
|
|
978
|
+
const squad = resolveResumeSquad(ctx.cwd, parts[1]);
|
|
979
|
+
if (!squad) {
|
|
980
|
+
ctx.ui.notify(parts[1] ? `Squad '${parts[1]}' not found` : "No paused, failed, or failed-review squad found", "warning");
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const scheduler = ensureScheduler(pi, squad.id, squadSkillPaths);
|
|
984
|
+
try {
|
|
985
|
+
await scheduler.resume();
|
|
986
|
+
} catch (error) {
|
|
987
|
+
ctx.ui.notify(`Resume failed: ${(error as Error).message}`, "error");
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const tasks = store.loadAllTasks(squad.id);
|
|
991
|
+
const done = tasks.filter((task) => task.status === "done").length;
|
|
992
|
+
ctx.ui.notify(`Resumed: ${squad.id} (${done}/${tasks.length} done)`, "info");
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
944
996
|
case "widget": {
|
|
945
997
|
widgetState.enabled = !widgetState.enabled;
|
|
946
998
|
if (widgetState.enabled) {
|
|
@@ -1398,7 +1450,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1398
1450
|
activateSquadView(direct.id, ctx);
|
|
1399
1451
|
return;
|
|
1400
1452
|
}
|
|
1401
|
-
ctx.ui.notify(`Unknown: /squad ${sub}. Try: list, all, select, agents, defaults, msg, widget, panel, cancel, clear, cleanup`, "warning");
|
|
1453
|
+
ctx.ui.notify(`Unknown: /squad ${sub}. Try: list, all, select, resume, agents, defaults, msg, widget, panel, cancel, clear, cleanup`, "warning");
|
|
1402
1454
|
}
|
|
1403
1455
|
},
|
|
1404
1456
|
});
|
package/src/review.ts
CHANGED
|
@@ -11,6 +11,18 @@ export interface OrchestratorReviewInput {
|
|
|
11
11
|
issues: string[];
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Start same-squad rework without discarding completed review evidence.
|
|
16
|
+
* The next all-tasks-done transition creates a new active pending review.
|
|
17
|
+
*/
|
|
18
|
+
export function beginOrchestratorRework(squad: Squad): void {
|
|
19
|
+
if (squad.review && squad.review.status !== "pending") {
|
|
20
|
+
squad.reviewHistory = [...(squad.reviewHistory ?? []), { ...squad.review }];
|
|
21
|
+
}
|
|
22
|
+
delete squad.review;
|
|
23
|
+
squad.status = "running";
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
/** Move a squad from agent execution into mandatory independent main-session review. */
|
|
15
27
|
export function beginOrchestratorReview(squad: Squad): void {
|
|
16
28
|
squad.status = "review";
|
|
@@ -35,6 +47,9 @@ export function recordOrchestratorReview(squad: Squad, input: OrchestratorReview
|
|
|
35
47
|
if (squad.status !== "review" || !squad.review) {
|
|
36
48
|
throw new Error(`Squad '${squad.id}' is not awaiting orchestrator review`);
|
|
37
49
|
}
|
|
50
|
+
if (squad.review.status !== "pending") {
|
|
51
|
+
throw new Error(`Squad '${squad.id}' review attempt is already ${squad.review.status}; begin same-squad rework before submitting a fresh review`);
|
|
52
|
+
}
|
|
38
53
|
|
|
39
54
|
const contractChecks = cleanList(input.contractChecks);
|
|
40
55
|
const verificationEvidence = cleanList(input.verificationEvidence);
|
package/src/scheduler.ts
CHANGED
|
@@ -19,7 +19,7 @@ import * as store from "./store.js";
|
|
|
19
19
|
import { debug, logError } from "./logger.js";
|
|
20
20
|
import { buildAgentSystemPrompt } from "./protocol.js";
|
|
21
21
|
import { buildAdvisorConsultText, formatAdvisorSteerMessage, adviceNeedsHuman, type AdvisorConsultInput } from "./advisor.js";
|
|
22
|
-
import { beginOrchestratorReview } from "./review.js";
|
|
22
|
+
import { beginOrchestratorReview, beginOrchestratorRework } from "./review.js";
|
|
23
23
|
|
|
24
24
|
// ============================================================================
|
|
25
25
|
// Types
|
|
@@ -170,6 +170,10 @@ export class Scheduler {
|
|
|
170
170
|
|
|
171
171
|
/** Start the scheduler — begins scheduling ready tasks */
|
|
172
172
|
async start(): Promise<void> {
|
|
173
|
+
if (this.running) {
|
|
174
|
+
await this.reconcile();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
173
177
|
this.running = true;
|
|
174
178
|
this.monitor.start();
|
|
175
179
|
await this.reconcile();
|
|
@@ -200,13 +204,15 @@ export class Scheduler {
|
|
|
200
204
|
await this.pool.killAll();
|
|
201
205
|
}
|
|
202
206
|
|
|
203
|
-
/** Resume
|
|
204
|
-
*
|
|
207
|
+
/** Resume suspended/failed work. A failed review is archived only when
|
|
208
|
+
* resumable work actually exists; a bare resume cannot bypass the gate. */
|
|
205
209
|
async resume(): Promise<void> {
|
|
206
210
|
const tasks = store.loadAllTasks(this.squadId);
|
|
211
|
+
let resumedWork = false;
|
|
207
212
|
for (const task of tasks) {
|
|
208
213
|
if (task.status === "suspended") {
|
|
209
214
|
store.updateTaskStatus(this.squadId, task.id, "pending");
|
|
215
|
+
resumedWork = true;
|
|
210
216
|
} else if (task.status === "failed") {
|
|
211
217
|
store.updateTaskStatus(this.squadId, task.id, "pending", { error: null });
|
|
212
218
|
store.appendMessage(this.squadId, task.id, {
|
|
@@ -215,13 +221,19 @@ export class Scheduler {
|
|
|
215
221
|
type: "status",
|
|
216
222
|
text: "Reset failed → pending on squad resume",
|
|
217
223
|
});
|
|
224
|
+
resumedWork = true;
|
|
218
225
|
}
|
|
219
226
|
}
|
|
220
227
|
|
|
221
228
|
const squad = store.loadSquad(this.squadId);
|
|
222
|
-
if (squad
|
|
223
|
-
squad.status
|
|
224
|
-
|
|
229
|
+
if (squad) {
|
|
230
|
+
if (resumedWork && squad.status === "review" && squad.review?.status === "failed") {
|
|
231
|
+
beginOrchestratorRework(squad);
|
|
232
|
+
store.saveSquad(squad);
|
|
233
|
+
} else if (squad.status === "paused" || squad.status === "failed") {
|
|
234
|
+
squad.status = "running";
|
|
235
|
+
store.saveSquad(squad);
|
|
236
|
+
}
|
|
225
237
|
}
|
|
226
238
|
|
|
227
239
|
await this.start();
|
|
@@ -262,8 +274,7 @@ export class Scheduler {
|
|
|
262
274
|
recoveredMailbox = true;
|
|
263
275
|
}
|
|
264
276
|
if (recoveredMailbox && squad.status !== "running") {
|
|
265
|
-
squad
|
|
266
|
-
delete squad.review;
|
|
277
|
+
beginOrchestratorRework(squad);
|
|
267
278
|
store.saveSquad(squad);
|
|
268
279
|
}
|
|
269
280
|
|
|
@@ -1301,6 +1312,17 @@ export class Scheduler {
|
|
|
1301
1312
|
await Promise.all(kills);
|
|
1302
1313
|
}
|
|
1303
1314
|
|
|
1315
|
+
private reopenSquadForWork(): void {
|
|
1316
|
+
const squad = store.loadSquad(this.squadId);
|
|
1317
|
+
if (!squad) throw new Error(`Squad not found: ${this.squadId}`);
|
|
1318
|
+
if (squad.status === "review" || squad.status === "done" || squad.review) {
|
|
1319
|
+
beginOrchestratorRework(squad);
|
|
1320
|
+
} else if (squad.status !== "running") {
|
|
1321
|
+
squad.status = "running";
|
|
1322
|
+
}
|
|
1323
|
+
store.saveSquad(squad);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1304
1326
|
private async reopenTaskForMessage(task: Task): Promise<void> {
|
|
1305
1327
|
if (task.status === "done") await this.invalidateDescendants(task.id);
|
|
1306
1328
|
const tasks = store.loadAllTasks(this.squadId);
|
|
@@ -1313,14 +1335,7 @@ export class Scheduler {
|
|
|
1313
1335
|
completed: null,
|
|
1314
1336
|
});
|
|
1315
1337
|
|
|
1316
|
-
|
|
1317
|
-
if (squad && squad.status !== "running") {
|
|
1318
|
-
squad.status = "running";
|
|
1319
|
-
// Any prior acceptance/review applies to the pre-resume result. The
|
|
1320
|
-
// normal all-done path will require a fresh independent review.
|
|
1321
|
-
delete squad.review;
|
|
1322
|
-
store.saveSquad(squad);
|
|
1323
|
-
}
|
|
1338
|
+
this.reopenSquadForWork();
|
|
1324
1339
|
}
|
|
1325
1340
|
|
|
1326
1341
|
/** Send a main-orchestrator request to one exact task and await its next reply. */
|
|
@@ -1381,10 +1396,32 @@ export class Scheduler {
|
|
|
1381
1396
|
this.updateContext();
|
|
1382
1397
|
}
|
|
1383
1398
|
|
|
1384
|
-
/**
|
|
1399
|
+
/** Add work to this exact squad and schedule it, reconstructing safely after restart. */
|
|
1400
|
+
async addTask(task: Task): Promise<void> {
|
|
1401
|
+
if (store.loadTask(this.squadId, task.id)) throw new Error(`Task already exists: ${task.id}`);
|
|
1402
|
+
store.createTask(this.squadId, task);
|
|
1403
|
+
this.reopenSquadForWork();
|
|
1404
|
+
await this.start();
|
|
1405
|
+
this.updateContext();
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/** Resume one exact task. Reopening completed work invalidates descendants and
|
|
1409
|
+
* archives a completed active review before fresh scheduling begins. */
|
|
1385
1410
|
async resumeTask(taskId: string): Promise<void> {
|
|
1386
|
-
store.
|
|
1387
|
-
|
|
1411
|
+
const task = store.loadTask(this.squadId, taskId);
|
|
1412
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
1413
|
+
if (task.status === "done") await this.invalidateDescendants(taskId);
|
|
1414
|
+
const tasks = store.loadAllTasks(this.squadId);
|
|
1415
|
+
const dependenciesDone = task.depends.every(
|
|
1416
|
+
(depId) => tasks.find((candidate) => candidate.id === depId)?.status === "done",
|
|
1417
|
+
);
|
|
1418
|
+
store.updateTaskStatus(this.squadId, taskId, dependenciesDone ? "pending" : "blocked", {
|
|
1419
|
+
error: null,
|
|
1420
|
+
completed: null,
|
|
1421
|
+
});
|
|
1422
|
+
this.reopenSquadForWork();
|
|
1423
|
+
await this.start();
|
|
1424
|
+
this.updateContext();
|
|
1388
1425
|
}
|
|
1389
1426
|
|
|
1390
1427
|
/**
|
package/src/types.ts
CHANGED
|
@@ -112,8 +112,10 @@ export interface Squad {
|
|
|
112
112
|
/** Agent name → overrides. Keys must exist in .pi/squad/agents/ */
|
|
113
113
|
agents: Record<string, SquadAgentEntry>;
|
|
114
114
|
config: SquadConfig;
|
|
115
|
-
/** Mandatory independent main-session review; absent
|
|
115
|
+
/** Mandatory independent main-session review; absent while rework is running. */
|
|
116
116
|
review?: SquadReview;
|
|
117
|
+
/** Completed prior review attempts retained as same-squad audit evidence. */
|
|
118
|
+
reviewHistory?: SquadReview[];
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
// ============================================================================
|