pi-squad 0.16.3 → 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 +24 -15
- package/package.json +1 -1
- package/src/agent-pool.ts +123 -44
- package/src/index.ts +258 -223
- package/src/panel/message-view.ts +58 -50
- package/src/panel/squad-widget.ts +35 -10
- package/src/panel/task-list.ts +13 -3
- package/src/review.ts +15 -0
- package/src/router.ts +37 -35
- package/src/scheduler.ts +300 -73
- package/src/store.ts +195 -1
- package/src/types.ts +27 -1
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
|
|
@@ -504,7 +536,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
504
536
|
pi.registerTool({
|
|
505
537
|
name: "squad_message",
|
|
506
538
|
label: "Squad Message",
|
|
507
|
-
description: "Send a request to a specific
|
|
539
|
+
description: "Send a durable request to a specific task. Existing tasks reopen their original Pi session; only a currently running task may be selected by agent name.",
|
|
508
540
|
parameters: Type.Object({
|
|
509
541
|
message: Type.String({ description: "Message to send" }),
|
|
510
542
|
taskId: Type.Optional(Type.String({ description: "Target task ID" })),
|
|
@@ -513,23 +545,40 @@ export default function (pi: ExtensionAPI) {
|
|
|
513
545
|
}),
|
|
514
546
|
|
|
515
547
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
516
|
-
|
|
517
|
-
if (!activeScheduler || !activeSquadId) {
|
|
548
|
+
if (!activeSquadId) {
|
|
518
549
|
return { content: [{ type: "text" as const, text: "No active squad." }], details: undefined };
|
|
519
550
|
}
|
|
520
551
|
|
|
552
|
+
let activeScheduler = getActiveScheduler();
|
|
521
553
|
let taskId = params.taskId;
|
|
522
554
|
|
|
523
|
-
//
|
|
524
|
-
|
|
525
|
-
|
|
555
|
+
// Agent name is safe shorthand only when it identifies one live task.
|
|
556
|
+
// Multiple concurrent tasks can share a role, so never guess between them.
|
|
557
|
+
if (!taskId && params.agent && activeScheduler) {
|
|
558
|
+
const liveMatches = store.loadAllTasks(activeSquadId).filter(
|
|
559
|
+
(task) => task.agent === params.agent && activeScheduler!.getPool().isRunning(task.id),
|
|
560
|
+
);
|
|
561
|
+
if (liveMatches.length === 1) taskId = liveMatches[0].id;
|
|
526
562
|
}
|
|
527
563
|
|
|
528
564
|
if (!taskId) {
|
|
529
565
|
return { content: [{ type: "text" as const, text: "Could not determine target task. Provide taskId or an agent name that is currently running." }], details: undefined };
|
|
530
566
|
}
|
|
567
|
+
if (!store.loadTask(activeSquadId, taskId)) {
|
|
568
|
+
return { content: [{ type: "text" as const, text: `Task not found: ${taskId}` }], details: undefined };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Review/done squads have no live scheduler after a process restart. An
|
|
572
|
+
// exact task ID is enough to reconstruct it from disk and reopen only that
|
|
573
|
+
// task; the task's immutable session binding supplies --session.
|
|
574
|
+
if (!activeScheduler) {
|
|
575
|
+
activeScheduler = new Scheduler(activeSquadId, squadSkillPaths, schedulerSpawnContext);
|
|
576
|
+
schedulers.set(activeSquadId, activeScheduler);
|
|
577
|
+
wireSchedulerEvents(pi, activeScheduler, activeSquadId);
|
|
578
|
+
await activeScheduler.start();
|
|
579
|
+
}
|
|
531
580
|
|
|
532
|
-
const sent = await activeScheduler
|
|
581
|
+
const sent = await activeScheduler.sendHumanMessage(taskId, params.message, params.expectReply ?? true);
|
|
533
582
|
const status = sent ? "delivered" : "queued for when the agent starts";
|
|
534
583
|
|
|
535
584
|
return { content: [{ type: "text" as const, text: `Message ${status}: "${params.message}"` }], details: undefined };
|
|
@@ -543,8 +592,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
543
592
|
pi.registerTool({
|
|
544
593
|
name: "squad_modify",
|
|
545
594
|
label: "Squad Modify",
|
|
546
|
-
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.",
|
|
547
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)" })),
|
|
548
598
|
action: Type.Union(
|
|
549
599
|
[
|
|
550
600
|
Type.Literal("add_task"),
|
|
@@ -573,95 +623,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
573
623
|
}),
|
|
574
624
|
|
|
575
625
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
576
|
-
// Resume can work without an active scheduler — it recreates one from disk
|
|
577
626
|
if (params.action === "resume") {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
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 };
|
|
585
633
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
activeSquadId = squadId;
|
|
592
|
-
|
|
593
|
-
// Activate widget
|
|
594
|
-
widgetState.squadId = squadId;
|
|
595
|
-
widgetState.enabled = true;
|
|
596
|
-
widgetControls?.requestUpdate();
|
|
597
|
-
|
|
598
|
-
// Wire up events (same as startSquad)
|
|
599
|
-
scheduler.onEvent((event: SchedulerEvent) => {
|
|
600
|
-
forceWidgetUpdate();
|
|
601
|
-
switch (event.type) {
|
|
602
|
-
case "squad_review_required": {
|
|
603
|
-
const tasks = store.loadAllTasks(squadId);
|
|
604
|
-
const summary = buildCompletionSummary(tasks);
|
|
605
|
-
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
606
|
-
const s = schedulers.get(squadId); if (s) s.updateContext();
|
|
607
|
-
const squad = store.loadSquad(squadId)!;
|
|
608
|
-
// Wake the main agent into an explicit acceptance gate. Agent reports
|
|
609
|
-
// are untrusted inputs; only independent orchestrator evidence can pass it.
|
|
610
|
-
pi.sendMessage({
|
|
611
|
-
customType: "squad-review-required",
|
|
612
|
-
content: `[squad] TASK EXECUTION FINISHED for "${squadId}" — WORK IS UNTRUSTED AND NOT YET ACCEPTED.\n\nSquad claims (review inputs only):\n${summary}\n\nTotal cost: $${totalCost.toFixed(4)}\n\n${buildOrchestratorReviewGate(squad, tasks)}`,
|
|
613
|
-
display: true,
|
|
614
|
-
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
615
|
-
schedulers.delete(squadId);
|
|
616
|
-
forceWidgetUpdate();
|
|
617
|
-
break;
|
|
618
|
-
}
|
|
619
|
-
case "squad_failed": {
|
|
620
|
-
const tasks = store.loadAllTasks(squadId);
|
|
621
|
-
const failed = tasks.filter((t) => t.status === "failed");
|
|
622
|
-
const done = tasks.filter((t) => t.status === "done");
|
|
623
|
-
pi.sendMessage({
|
|
624
|
-
customType: "squad-failed",
|
|
625
|
-
content: `[squad] Squad "${squadId}" has stalled. ${done.length}/${tasks.length} done, ${failed.length} failed.\nFailed: ${buildFailureSummary(tasks)}`,
|
|
626
|
-
display: true,
|
|
627
|
-
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
628
|
-
forceWidgetUpdate();
|
|
629
|
-
break;
|
|
630
|
-
}
|
|
631
|
-
case "orchestrator_reply": {
|
|
632
|
-
pi.sendMessage({
|
|
633
|
-
customType: "squad-agent-reply",
|
|
634
|
-
content: `[squad] Direct reply from '${event.agentName}' on task '${event.taskId}':\n${event.message}`,
|
|
635
|
-
display: true,
|
|
636
|
-
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
637
|
-
break;
|
|
638
|
-
}
|
|
639
|
-
case "escalation": {
|
|
640
|
-
pi.sendMessage({
|
|
641
|
-
customType: "squad-escalation",
|
|
642
|
-
content: `[squad] Agent '${event.agentName}' on task '${event.taskId}' needs attention:\n${event.message}`,
|
|
643
|
-
display: true,
|
|
644
|
-
}, { triggerTurn: true });
|
|
645
|
-
break;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
});
|
|
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 };
|
|
649
639
|
}
|
|
650
|
-
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
logError("squad", `Resume error: ${(err as Error).message}`);
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
const tasks = store.loadAllTasks(squadId);
|
|
657
|
-
const done = tasks.filter(t => t.status === "done").length;
|
|
658
|
-
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 };
|
|
659
643
|
}
|
|
660
644
|
|
|
661
|
-
const
|
|
662
|
-
if (!
|
|
663
|
-
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 };
|
|
664
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 };
|
|
655
|
+
}
|
|
656
|
+
activeSquadId = squadId;
|
|
665
657
|
|
|
666
658
|
switch (params.action) {
|
|
667
659
|
case "add_task": {
|
|
@@ -678,17 +670,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
678
670
|
if (badDeps.length > 0) {
|
|
679
671
|
return { content: [{ type: "text" as const, text: `Unknown dependency task(s): ${badDeps.join(", ")}. Existing tasks: ${[...existingIds].join(", ")}` }], details: undefined };
|
|
680
672
|
}
|
|
681
|
-
|
|
682
|
-
|
|
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(", ");
|
|
683
676
|
return { content: [{ type: "text" as const, text: `Unknown agent '${params.task.agent}'. Available: ${available}` }], details: undefined };
|
|
684
677
|
}
|
|
678
|
+
const dependencies = params.task.depends || [];
|
|
685
679
|
const task: Task = {
|
|
686
680
|
id: params.task.id,
|
|
687
681
|
title: params.task.title,
|
|
688
682
|
description: params.task.description || "",
|
|
689
683
|
agent: params.task.agent,
|
|
690
|
-
status: "pending",
|
|
691
|
-
depends:
|
|
684
|
+
status: dependencies.every((dependency) => existing.find((candidate) => candidate.id === dependency)?.status === "done") ? "pending" : "blocked",
|
|
685
|
+
depends: dependencies,
|
|
692
686
|
...(params.task.inheritContext ? { inheritContext: true } : {}),
|
|
693
687
|
created: store.now(),
|
|
694
688
|
started: null,
|
|
@@ -697,9 +691,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
697
691
|
error: null,
|
|
698
692
|
usage: { inputTokens: 0, outputTokens: 0, cost: 0, turns: 0 },
|
|
699
693
|
};
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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 };
|
|
703
700
|
}
|
|
704
701
|
|
|
705
702
|
case "cancel_task": {
|
|
@@ -716,10 +713,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
716
713
|
|
|
717
714
|
case "resume_task": {
|
|
718
715
|
if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }], details: undefined };
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
})
|
|
722
|
-
|
|
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 };
|
|
723
722
|
}
|
|
724
723
|
|
|
725
724
|
case "complete_task": {
|
|
@@ -795,6 +794,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
795
794
|
}
|
|
796
795
|
}
|
|
797
796
|
|
|
797
|
+
// Mailbox recovery is automatic after extension/main-process restart. Scan
|
|
798
|
+
// every project squad (including accepted done squads) because a crash can
|
|
799
|
+
// occur after the mailbox-first write but before the squad/task is reopened.
|
|
800
|
+
const pendingMailSquads = store.listSquadsForProject(ctx.cwd)
|
|
801
|
+
.filter((squad) => store.loadAllTasks(squad.id)
|
|
802
|
+
.some((task) => store.loadPendingTaskMessages(squad.id, task.id).length > 0))
|
|
803
|
+
.sort((a, b) => b.created.localeCompare(a.created));
|
|
804
|
+
for (const squad of pendingMailSquads) {
|
|
805
|
+
let scheduler = schedulers.get(squad.id);
|
|
806
|
+
if (!scheduler) {
|
|
807
|
+
scheduler = new Scheduler(squad.id, squadSkillPaths, schedulerSpawnContext);
|
|
808
|
+
schedulers.set(squad.id, scheduler);
|
|
809
|
+
wireSchedulerEvents(pi, scheduler, squad.id);
|
|
810
|
+
}
|
|
811
|
+
await scheduler.start();
|
|
812
|
+
}
|
|
813
|
+
if (pendingMailSquads.length > 0) {
|
|
814
|
+
activeSquadId = pendingMailSquads[0].id;
|
|
815
|
+
widgetState.squadId = activeSquadId;
|
|
816
|
+
widgetState.enabled = true;
|
|
817
|
+
widgetControls?.requestUpdate();
|
|
818
|
+
}
|
|
819
|
+
|
|
798
820
|
// Notify about paused squads only if they have real completed work
|
|
799
821
|
const paused = store.findActiveSquads()
|
|
800
822
|
.filter((s) => s.cwd === ctx.cwd && s.status === "paused");
|
|
@@ -851,7 +873,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
851
873
|
}
|
|
852
874
|
|
|
853
875
|
if (activeSquadId) {
|
|
854
|
-
openPanel(ctx, schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths, schedulerSpawnContext), activeSquadId);
|
|
876
|
+
openPanel(pi, ctx, schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths, schedulerSpawnContext), activeSquadId);
|
|
855
877
|
}
|
|
856
878
|
return { consume: true };
|
|
857
879
|
}
|
|
@@ -876,16 +898,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
876
898
|
// =========================================================================
|
|
877
899
|
|
|
878
900
|
pi.registerCommand("squad", {
|
|
879
|
-
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]",
|
|
880
902
|
getArgumentCompletions: (prefix) => {
|
|
881
903
|
const subs = [
|
|
882
904
|
{ value: "list", label: "list", description: "List squads for current project" },
|
|
883
905
|
{ value: "all", label: "all", description: "List all squads, select to activate" },
|
|
884
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" },
|
|
885
908
|
{ value: "agents", label: "agents", description: "List, view, or edit agent definitions" },
|
|
886
909
|
{ value: "defaults", label: "defaults", description: "Default model/thinking for agents (follow main session, pi default, or fixed)" },
|
|
887
910
|
{ value: "advisor", label: "advisor", description: "Advisor-first rescue for stuck agents (on/off, model, limits)" },
|
|
888
|
-
{ value: "msg", label: "msg", description: "
|
|
911
|
+
{ value: "msg", label: "msg", description: "Message a task: /squad msg [task-id|running-agent] text" },
|
|
889
912
|
{ value: "widget", label: "widget", description: "Toggle live widget" },
|
|
890
913
|
{ value: "panel", label: "panel", description: "Toggle overlay panel" },
|
|
891
914
|
{ value: "cancel", label: "cancel", description: "Cancel running squad" },
|
|
@@ -951,6 +974,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
951
974
|
return;
|
|
952
975
|
}
|
|
953
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
|
+
|
|
954
996
|
case "widget": {
|
|
955
997
|
widgetState.enabled = !widgetState.enabled;
|
|
956
998
|
if (widgetState.enabled) {
|
|
@@ -978,7 +1020,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
978
1020
|
}
|
|
979
1021
|
if (activeSquadId) {
|
|
980
1022
|
const sched = schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths, schedulerSpawnContext);
|
|
981
|
-
openPanel(ctx, sched, activeSquadId);
|
|
1023
|
+
openPanel(pi, ctx, sched, activeSquadId);
|
|
982
1024
|
}
|
|
983
1025
|
return;
|
|
984
1026
|
}
|
|
@@ -989,11 +1031,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
989
1031
|
return;
|
|
990
1032
|
}
|
|
991
1033
|
const msgSquad = store.loadSquad(activeSquadId);
|
|
992
|
-
if (!msgSquad
|
|
993
|
-
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
// Parse: /squad msg [agent] message text
|
|
1034
|
+
if (!msgSquad) return;
|
|
1035
|
+
// Parse: /squad msg [task-id|running-agent] message text
|
|
997
1036
|
const msgParts = parts.slice(1);
|
|
998
1037
|
let targetAgent: string | undefined;
|
|
999
1038
|
let msgText: string;
|
|
@@ -1014,40 +1053,51 @@ export default function (pi: ExtensionAPI) {
|
|
|
1014
1053
|
}
|
|
1015
1054
|
}
|
|
1016
1055
|
|
|
1017
|
-
// Find target task
|
|
1056
|
+
// Find one exact target task. A task ID can address completed or
|
|
1057
|
+
// stopped work; an agent name remains shorthand for a live task only.
|
|
1018
1058
|
const msgTasks = store.loadAllTasks(activeSquadId);
|
|
1019
1059
|
let targetTaskId: string | undefined;
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1060
|
+
const explicitTask = msgParts.length > 1
|
|
1061
|
+
? msgTasks.find((task) => task.id === msgParts[0])
|
|
1062
|
+
: undefined;
|
|
1063
|
+
if (explicitTask) {
|
|
1064
|
+
targetTaskId = explicitTask.id;
|
|
1065
|
+
targetAgent = explicitTask.agent;
|
|
1066
|
+
msgText = msgParts.slice(1).join(" ");
|
|
1067
|
+
} else if (targetAgent) {
|
|
1068
|
+
const liveMatches = msgTasks.filter(
|
|
1069
|
+
(task) => task.agent === targetAgent && getActiveScheduler()?.getPool().isRunning(task.id),
|
|
1070
|
+
);
|
|
1071
|
+
if (liveMatches.length === 1) targetTaskId = liveMatches[0].id;
|
|
1024
1072
|
if (!targetTaskId) {
|
|
1025
|
-
ctx.ui.notify(`Agent '${targetAgent}'
|
|
1073
|
+
ctx.ui.notify(`Agent '${targetAgent}' is not working on exactly one live task; provide an exact task ID`, "warning");
|
|
1026
1074
|
return;
|
|
1027
1075
|
}
|
|
1028
1076
|
} else {
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
if (
|
|
1033
|
-
|
|
1077
|
+
const liveTasks = msgTasks.filter((task) =>
|
|
1078
|
+
getActiveScheduler()?.getPool().isRunning(task.id),
|
|
1079
|
+
);
|
|
1080
|
+
if (liveTasks.length === 1) {
|
|
1081
|
+
targetTaskId = liveTasks[0].id;
|
|
1082
|
+
targetAgent = liveTasks[0].agent;
|
|
1083
|
+
} else {
|
|
1084
|
+
ctx.ui.notify("No unique live task to message; provide an exact task ID", "warning");
|
|
1034
1085
|
return;
|
|
1035
1086
|
}
|
|
1036
1087
|
}
|
|
1037
1088
|
|
|
1038
|
-
|
|
1039
|
-
if (msgSched) {
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
ts: store.now(),
|
|
1045
|
-
from: "human",
|
|
1046
|
-
type: "message",
|
|
1047
|
-
text: msgText,
|
|
1048
|
-
});
|
|
1049
|
-
ctx.ui.notify(`Logged to ${targetTaskId} (agent not running)`, "info");
|
|
1089
|
+
let msgSched = getActiveScheduler();
|
|
1090
|
+
if (!msgSched) {
|
|
1091
|
+
msgSched = new Scheduler(activeSquadId, squadSkillPaths, schedulerSpawnContext);
|
|
1092
|
+
schedulers.set(activeSquadId, msgSched);
|
|
1093
|
+
wireSchedulerEvents(pi, msgSched, activeSquadId);
|
|
1094
|
+
await msgSched.start();
|
|
1050
1095
|
}
|
|
1096
|
+
const delivered = await msgSched.sendHumanMessage(targetTaskId, msgText);
|
|
1097
|
+
ctx.ui.notify(
|
|
1098
|
+
delivered ? `Sent to ${targetAgent}: "${msgText}"` : `Queued durably for ${targetTaskId}`,
|
|
1099
|
+
"info",
|
|
1100
|
+
);
|
|
1051
1101
|
forceWidgetUpdate();
|
|
1052
1102
|
return;
|
|
1053
1103
|
}
|
|
@@ -1400,7 +1450,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1400
1450
|
activateSquadView(direct.id, ctx);
|
|
1401
1451
|
return;
|
|
1402
1452
|
}
|
|
1403
|
-
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");
|
|
1404
1454
|
}
|
|
1405
1455
|
},
|
|
1406
1456
|
});
|
|
@@ -1475,6 +1525,63 @@ function forceWidgetUpdate(): void {
|
|
|
1475
1525
|
widgetControls?.requestUpdate();
|
|
1476
1526
|
}
|
|
1477
1527
|
|
|
1528
|
+
/** Wire one scheduler to durable main-session notifications. */
|
|
1529
|
+
function wireSchedulerEvents(pi: ExtensionAPI, scheduler: Scheduler, squadId: string): void {
|
|
1530
|
+
scheduler.onEvent((event: SchedulerEvent) => {
|
|
1531
|
+
forceWidgetUpdate();
|
|
1532
|
+
switch (event.type) {
|
|
1533
|
+
case "squad_review_required": {
|
|
1534
|
+
const tasks = store.loadAllTasks(squadId);
|
|
1535
|
+
const summary = buildCompletionSummary(tasks);
|
|
1536
|
+
const totalCost = tasks.reduce((sum, task) => sum + task.usage.cost, 0);
|
|
1537
|
+
scheduler.updateContext();
|
|
1538
|
+
const squad = store.loadSquad(squadId);
|
|
1539
|
+
if (!squad) break;
|
|
1540
|
+
pi.sendMessage({
|
|
1541
|
+
customType: "squad-review-required",
|
|
1542
|
+
content: `[squad] TASK EXECUTION FINISHED for "${squadId}" — WORK IS UNTRUSTED AND NOT YET ACCEPTED.\n\n` +
|
|
1543
|
+
`Squad claims (review inputs only):\n${summary}\n\n` +
|
|
1544
|
+
`Total cost: $${totalCost.toFixed(4)}\n\n` +
|
|
1545
|
+
buildOrchestratorReviewGate(squad, tasks),
|
|
1546
|
+
display: true,
|
|
1547
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
1548
|
+
// Keep the settled scheduler addressable. A later exact-task message
|
|
1549
|
+
// can reopen the task immediately on its bound durable Pi session.
|
|
1550
|
+
break;
|
|
1551
|
+
}
|
|
1552
|
+
case "squad_failed": {
|
|
1553
|
+
const tasks = store.loadAllTasks(squadId);
|
|
1554
|
+
const failed = tasks.filter((task) => task.status === "failed");
|
|
1555
|
+
const done = tasks.filter((task) => task.status === "done");
|
|
1556
|
+
pi.sendMessage({
|
|
1557
|
+
customType: "squad-failed",
|
|
1558
|
+
content: `[squad] Squad "${squadId}" has stalled. ` +
|
|
1559
|
+
`${done.length}/${tasks.length} tasks done, ${failed.length} failed.\n` +
|
|
1560
|
+
`Failed: ${buildFailureSummary(tasks)}\n` +
|
|
1561
|
+
"Use squad_status for details or squad_modify to adjust.",
|
|
1562
|
+
display: true,
|
|
1563
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
1564
|
+
break;
|
|
1565
|
+
}
|
|
1566
|
+
case "orchestrator_reply":
|
|
1567
|
+
pi.sendMessage({
|
|
1568
|
+
customType: "squad-agent-reply",
|
|
1569
|
+
content: `[squad] Direct reply from '${event.agentName}' on task '${event.taskId}':\n${event.message}`,
|
|
1570
|
+
display: true,
|
|
1571
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
1572
|
+
break;
|
|
1573
|
+
case "escalation":
|
|
1574
|
+
pi.sendMessage({
|
|
1575
|
+
customType: "squad-escalation",
|
|
1576
|
+
content: `[squad] Agent '${event.agentName}' on task '${event.taskId}' needs attention:\n` +
|
|
1577
|
+
`${event.message}\n\nReply to me and I'll forward your answer, or use the squad panel.`,
|
|
1578
|
+
display: true,
|
|
1579
|
+
}, { triggerTurn: true });
|
|
1580
|
+
break;
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1478
1585
|
// ============================================================================
|
|
1479
1586
|
// Panel — overlay via ctx.ui.custom() with proper done() lifecycle
|
|
1480
1587
|
// ============================================================================
|
|
@@ -1485,6 +1592,7 @@ function forceWidgetUpdate(): void {
|
|
|
1485
1592
|
* that resolves when done() is called. The panel calls done() on close.
|
|
1486
1593
|
*/
|
|
1487
1594
|
function openPanel(
|
|
1595
|
+
pi: ExtensionAPI,
|
|
1488
1596
|
ctx: import("@earendil-works/pi-coding-agent").ExtensionContext,
|
|
1489
1597
|
scheduler: Scheduler,
|
|
1490
1598
|
squadId: string,
|
|
@@ -1502,18 +1610,19 @@ function openPanel(
|
|
|
1502
1610
|
const task = store.loadTask(squadId, taskId);
|
|
1503
1611
|
const agentName = task?.agent || taskId;
|
|
1504
1612
|
const input = await ctx.ui.input(`Message to ${agentName}`, "Type your message...");
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1613
|
+
if (input) {
|
|
1614
|
+
let panelSched = schedulers.get(squadId);
|
|
1615
|
+
if (!panelSched) {
|
|
1616
|
+
panelSched = scheduler;
|
|
1617
|
+
schedulers.set(squadId, panelSched);
|
|
1618
|
+
wireSchedulerEvents(pi, panelSched, squadId);
|
|
1619
|
+
await panelSched.start();
|
|
1620
|
+
}
|
|
1621
|
+
const delivered = await panelSched.sendHumanMessage(taskId, input);
|
|
1622
|
+
ctx.ui.notify(
|
|
1623
|
+
delivered ? `Sent to ${agentName}: "${input}"` : `Queued durably for ${taskId}`,
|
|
1624
|
+
"info",
|
|
1625
|
+
);
|
|
1517
1626
|
}
|
|
1518
1627
|
tui.requestRender();
|
|
1519
1628
|
};
|
|
@@ -1666,82 +1775,8 @@ async function startSquad(
|
|
|
1666
1775
|
widgetState.enabled = true;
|
|
1667
1776
|
widgetControls?.requestUpdate();
|
|
1668
1777
|
|
|
1669
|
-
// Wire up completion/escalation notifications to main agent
|
|
1670
|
-
|
|
1671
|
-
// Update widget on every scheduler event
|
|
1672
|
-
forceWidgetUpdate();
|
|
1673
|
-
switch (event.type) {
|
|
1674
|
-
case "squad_review_required": {
|
|
1675
|
-
const tasks = store.loadAllTasks(squadId);
|
|
1676
|
-
const summary = buildCompletionSummary(tasks);
|
|
1677
|
-
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
1678
|
-
|
|
1679
|
-
// Final context update before clearing scheduler
|
|
1680
|
-
const completedSched = schedulers.get(squadId);
|
|
1681
|
-
if (completedSched) {
|
|
1682
|
-
completedSched.updateContext();
|
|
1683
|
-
}
|
|
1684
|
-
const squad = store.loadSquad(squadId)!;
|
|
1685
|
-
|
|
1686
|
-
// Wake the main agent into an explicit acceptance gate. Never frame
|
|
1687
|
-
// agent execution or squad QA as trusted completion.
|
|
1688
|
-
pi.sendMessage({
|
|
1689
|
-
customType: "squad-review-required",
|
|
1690
|
-
content: `[squad] TASK EXECUTION FINISHED for "${squadId}" — WORK IS UNTRUSTED AND NOT YET ACCEPTED.\n\n` +
|
|
1691
|
-
`Squad claims (review inputs only):\n${summary}\n\n` +
|
|
1692
|
-
`Total cost: $${totalCost.toFixed(4)}\n\n` +
|
|
1693
|
-
buildOrchestratorReviewGate(squad, tasks),
|
|
1694
|
-
display: true,
|
|
1695
|
-
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
1696
|
-
|
|
1697
|
-
// Clear scheduler but keep activeSquadId so squad_status still works
|
|
1698
|
-
schedulers.delete(squadId);
|
|
1699
|
-
forceWidgetUpdate(); // Final update showing done state
|
|
1700
|
-
break;
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
case "squad_failed": {
|
|
1704
|
-
const tasks = store.loadAllTasks(squadId);
|
|
1705
|
-
const failed = tasks.filter((t) => t.status === "failed");
|
|
1706
|
-
const done = tasks.filter((t) => t.status === "done");
|
|
1707
|
-
|
|
1708
|
-
pi.sendMessage({
|
|
1709
|
-
customType: "squad-failed",
|
|
1710
|
-
content: `[squad] Squad "${squadId}" has stalled. ` +
|
|
1711
|
-
`${done.length}/${tasks.length} tasks done, ${failed.length} failed.\n` +
|
|
1712
|
-
`Failed: ${buildFailureSummary(tasks)}\n` +
|
|
1713
|
-
`Use squad_status for details or squad_modify to adjust.`,
|
|
1714
|
-
display: true,
|
|
1715
|
-
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
1716
|
-
forceWidgetUpdate();
|
|
1717
|
-
break;
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
case "orchestrator_reply": {
|
|
1721
|
-
// Push the complete requested response back into the main session and
|
|
1722
|
-
// wake it; ordinary task activity remains panel-only.
|
|
1723
|
-
pi.sendMessage({
|
|
1724
|
-
customType: "squad-agent-reply",
|
|
1725
|
-
content: `[squad] Direct reply from '${event.agentName}' on task '${event.taskId}':\n${event.message}`,
|
|
1726
|
-
display: true,
|
|
1727
|
-
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
1728
|
-
break;
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
case "escalation": {
|
|
1732
|
-
// Escalation — agent needs help. triggerTurn so the main agent
|
|
1733
|
-
// can respond and relay help.
|
|
1734
|
-
pi.sendMessage({
|
|
1735
|
-
customType: "squad-escalation",
|
|
1736
|
-
content: `[squad] Agent '${event.agentName}' on task '${event.taskId}' needs attention:\n` +
|
|
1737
|
-
`${event.message}\n\n` +
|
|
1738
|
-
`Reply to me and I'll forward your answer, or use the squad panel.`,
|
|
1739
|
-
display: true,
|
|
1740
|
-
}, { triggerTurn: true });
|
|
1741
|
-
break;
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
});
|
|
1778
|
+
// Wire up completion/escalation notifications to main agent.
|
|
1779
|
+
wireSchedulerEvents(pi, scheduler, squadId);
|
|
1745
1780
|
|
|
1746
1781
|
// Start scheduling — fire and forget, don't block the tool call.
|
|
1747
1782
|
// scheduler.start() spawns agents which can take seconds per agent.
|