pi-squad 0.19.3 → 0.19.4

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/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.19.4] - 2026-07-24
11
+
12
+ ### Fixed
13
+
14
+ - The mandatory review-required report can no longer be silently lost or invisibly stalled. Successful delivery is durably recorded as `review.notifiedAt`; the 60s reconcile loop re-raises an unrecorded pending gate (covers delivery exceptions and disabled-mode drops), and an immediate TUI notification surfaces the pending review even while a long or stalled main-session run delays the queued follow-up report. Delivered gates are never re-notified while a human review is in progress.
15
+ - `squad_failed` stall notifications now fire only on the actual transition to `failed`. Repeated reconciles over an already-failed squad no longer queue duplicate notifications, each of which previously triggered its own main-session turn.
16
+ - Review-required delivery failures are now logged to `~/.pi/squad/debug.log` instead of being swallowed.
17
+
10
18
  ## [0.19.3] - 2026-07-19
11
19
 
12
20
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squad",
3
- "version": "0.19.3",
3
+ "version": "0.19.4",
4
4
  "description": "Multi-agent collaboration extension for pi — task decomposition, dependency management, parallel execution, TUI panel",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,14 +22,32 @@ export function wireSchedulerEvents(pi: ExtensionAPI, scheduler: Scheduler, squa
22
22
  const totalCost = tasks.reduce((sum, task) => sum + task.usage.cost, 0);
23
23
  scheduler.updateContext();
24
24
  if (!squad) break;
25
- pi.sendMessage({
26
- customType: "squad-review-required",
27
- content: `[squad] TASK EXECUTION FINISHED for "${squadId}" — WORK IS UNTRUSTED AND NOT YET ACCEPTED.\n\n` +
28
- `Squad claims (review inputs only):\n${summary}\n\n` +
29
- `Total cost: $${totalCost.toFixed(4)}\n\n` +
30
- buildOrchestratorReviewGate(squad, tasks),
31
- display: true,
32
- }, { triggerTurn: true, deliverAs: "followUp" });
25
+ try {
26
+ pi.sendMessage({
27
+ customType: "squad-review-required",
28
+ content: `[squad] TASK EXECUTION FINISHED for "${squadId}" — WORK IS UNTRUSTED AND NOT YET ACCEPTED.\n\n` +
29
+ `Squad claims (review inputs only):\n${summary}\n\n` +
30
+ `Total cost: $${totalCost.toFixed(4)}\n\n` +
31
+ buildOrchestratorReviewGate(squad, tasks),
32
+ display: true,
33
+ }, { triggerTurn: true, deliverAs: "followUp" });
34
+ // Record durable delivery so reconcile stops re-raising the gate.
35
+ // An unrecorded emission (throw here, or the disabled-mode drop
36
+ // above) is re-emitted by the next reconcile pass.
37
+ const fresh = store.loadSquad(squadId);
38
+ if (fresh?.review?.status === "pending" && !fresh.review.notifiedAt) {
39
+ fresh.review.notifiedAt = store.now();
40
+ store.saveSquad(fresh);
41
+ }
42
+ } catch (error) {
43
+ logError("squad-scheduler", `review-required delivery failed for ${squadId}: ${(error as Error).message}`);
44
+ }
45
+ // The followUp above waits for the current main run to settle. Surface
46
+ // the pending gate immediately in the TUI so a long/stalled run cannot
47
+ // hide it for hours.
48
+ try {
49
+ if (runtime.uiCtx?.hasUI) runtime.uiCtx.ui.notify(`[squad] "${squadId}" finished — awaiting your independent review (report queued for end of current turn)`, "info");
50
+ } catch { /* toast is best-effort */ }
33
51
  // Keep the settled scheduler addressable. A later exact-task message
34
52
  // can reopen the task immediately on its bound durable Pi session.
35
53
  break;
package/src/scheduler.ts CHANGED
@@ -403,6 +403,15 @@ export class Scheduler {
403
403
  if (freshSquad && (freshSquad.status === "running" || freshSquad.status === "failed")) {
404
404
  this.checkSquadCompletion(store.loadAllTasks(this.squadId), freshSquad);
405
405
  }
406
+
407
+ // A pending review whose notification never reached the main session
408
+ // (delivery threw, or disabled mode dropped the event) is re-raised until
409
+ // the delivery handler durably records review.notifiedAt. Successful
410
+ // delivery stops the re-raise, so a slow human review never re-notifies.
411
+ const reviewSquad = freshSquad?.status === "review" ? freshSquad : store.loadSquad(this.squadId);
412
+ if (reviewSquad?.status === "review" && reviewSquad.review?.status === "pending" && !reviewSquad.review.notifiedAt) {
413
+ this.emit({ type: "squad_review_required", squadId: this.squadId });
414
+ }
406
415
  }
407
416
 
408
417
  // =========================================================================
@@ -1328,7 +1337,11 @@ export class Scheduler {
1328
1337
  store.saveSquad(squad);
1329
1338
  this.emit({ type: "squad_review_required", squadId: this.squadId });
1330
1339
  } else if (anyFailed && !anyInProgress) {
1331
- // All remaining tasks are blocked/failed with no way forward
1340
+ // All remaining tasks are blocked/failed with no way forward.
1341
+ // Emit only on the transition: repeated reconciles over an already-failed
1342
+ // squad must not queue duplicate stall notifications (each would trigger
1343
+ // its own main-session turn).
1344
+ if (squad.status === "failed") return;
1332
1345
  const blockedCount = relevant.filter((task) => task.status === "blocked").length;
1333
1346
  const failedCount = relevant.filter((task) => task.status === "failed").length;
1334
1347
  if (blockedCount + failedCount === relevant.filter((task) => task.status !== "done").length) {
package/src/types.ts CHANGED
@@ -78,6 +78,9 @@ export type SquadStatus = "planning" | "running" | "paused" | "review" | "done"
78
78
  export interface SquadReview {
79
79
  status: "pending" | "passed" | "failed";
80
80
  requestedAt: string;
81
+ /** When the review-required notification was handed to the main session.
82
+ * Absent/null = not yet delivered; reconcile re-raises the gate. */
83
+ notifiedAt?: string | null;
81
84
  completedAt: string | null;
82
85
  verdict: "pass" | "pass_with_issues" | "fail" | null;
83
86
  contractChecks: string[];