pi-squad 0.17.1 → 0.18.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/CHANGELOG.md +74 -0
- package/README.md +12 -2
- package/docs/persistent-master-switch-contract.md +148 -0
- package/package.json +2 -1
- package/src/commands.ts +585 -0
- package/src/index.ts +16 -1921
- package/src/lifecycle.ts +164 -0
- package/src/panel/squad-panel.ts +16 -0
- package/src/panel-runtime.ts +157 -0
- package/src/protocol.ts +3 -47
- package/src/report.ts +40 -2
- package/src/runtime.ts +196 -0
- package/src/scheduler-runtime.ts +117 -0
- package/src/scheduler.ts +149 -135
- package/src/start-squad.ts +176 -0
- package/src/store.ts +3 -44
- package/src/tools-registration.ts +609 -0
- package/src/types.ts +2 -26
- package/src/supervisor.ts +0 -142
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { setupSquadWidget } from "./panel/squad-widget.js";
|
|
3
|
+
import { activateSquadView, openPanel } from "./panel-runtime.js";
|
|
4
|
+
import { buildOrchestratorReviewGate } from "./review.js";
|
|
5
|
+
import { reviveScheduler } from "./scheduler-runtime.js";
|
|
6
|
+
import * as store from "./store.js";
|
|
7
|
+
import type { Squad } from "./types.js";
|
|
8
|
+
import { DISABLED_GUIDANCE, focusSquad, formatTaskProgress, runtime } from "./runtime.js";
|
|
9
|
+
|
|
10
|
+
export function registerLifecycle(pi: ExtensionAPI, squadSkillPaths: string[]): void {
|
|
11
|
+
/** Register a dynamically gated Ctrl+Q path; disabled mode can only show guidance. */
|
|
12
|
+
const registerTerminalPanelToggle = (ctx: ExtensionContext): void => {
|
|
13
|
+
if (!ctx.hasUI) return;
|
|
14
|
+
ctx.ui.onTerminalInput((data) => {
|
|
15
|
+
if (data !== "\x11") return undefined;
|
|
16
|
+
if (!runtime.squadEnabled) {
|
|
17
|
+
ctx.ui.notify(DISABLED_GUIDANCE, "warning");
|
|
18
|
+
return { consume: true };
|
|
19
|
+
}
|
|
20
|
+
// If overlay is already open, let the panel's own handler deal with it.
|
|
21
|
+
if (runtime.overlayOpen) return undefined;
|
|
22
|
+
if (!runtime.activeSquadId) {
|
|
23
|
+
const latest = store.findLatestSquad(ctx.cwd)
|
|
24
|
+
|| store.listSquads().map((id) => store.loadSquad(id)).filter((s): s is Squad => s !== null).sort((a, b) => b.created.localeCompare(a.created))[0];
|
|
25
|
+
if (latest) activateSquadView(latest.id, ctx);
|
|
26
|
+
else {
|
|
27
|
+
ctx.ui.notify("No squads found. Use /squad or the squad tool.", "info");
|
|
28
|
+
return { consume: true };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (runtime.activeSquadId) {
|
|
32
|
+
openPanel(pi, ctx, reviveScheduler(pi, runtime.activeSquadId, squadSkillPaths), runtime.activeSquadId, squadSkillPaths);
|
|
33
|
+
}
|
|
34
|
+
return { consume: true };
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// =========================================================================
|
|
39
|
+
// Session Lifecycle
|
|
40
|
+
// =========================================================================
|
|
41
|
+
|
|
42
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
43
|
+
// Re-read before touching focus, widgets, runtime.schedulers, or persisted work.
|
|
44
|
+
runtime.squadEnabled = store.loadSquadSettings().enabled;
|
|
45
|
+
runtime.uiCtx = ctx;
|
|
46
|
+
|
|
47
|
+
// A session replacement may not deliver shutdown first. Never carry a
|
|
48
|
+
// focused squad across projects, and never seed a new widget from stale state.
|
|
49
|
+
runtime.widgetControls?.dispose();
|
|
50
|
+
runtime.widgetControls = null;
|
|
51
|
+
registerTerminalPanelToggle(ctx);
|
|
52
|
+
if (!runtime.squadEnabled) {
|
|
53
|
+
runtime.widgetState.enabled = false;
|
|
54
|
+
focusSquad(null);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
runtime.widgetState.enabled = true;
|
|
58
|
+
const focused = runtime.activeSquadId ? store.loadSquad(runtime.activeSquadId) : null;
|
|
59
|
+
focusSquad(focused?.cwd === ctx.cwd ? runtime.activeSquadId : null);
|
|
60
|
+
|
|
61
|
+
// Install component-based widget
|
|
62
|
+
if (ctx.hasUI) {
|
|
63
|
+
runtime.widgetControls = setupSquadWidget(ctx, runtime.widgetState);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Clean up orphaned squads from crashed sessions:
|
|
67
|
+
// If a squad is "running" but has no live scheduler, its parent died.
|
|
68
|
+
// Suspend in-progress tasks and mark the squad as paused so it doesn't
|
|
69
|
+
// block new squads or trigger confusing followUp messages.
|
|
70
|
+
const orphaned = store.findActiveSquads()
|
|
71
|
+
.filter((s) => s.cwd === ctx.cwd && s.status === "running");
|
|
72
|
+
for (const squad of orphaned) {
|
|
73
|
+
const tasks = store.loadAllTasks(squad.id);
|
|
74
|
+
let hadInProgress = false;
|
|
75
|
+
for (const task of tasks) {
|
|
76
|
+
if (task.status === "in_progress") {
|
|
77
|
+
store.updateTaskStatus(squad.id, task.id, "suspended");
|
|
78
|
+
hadInProgress = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (hadInProgress) {
|
|
82
|
+
squad.status = "paused";
|
|
83
|
+
store.saveSquad(squad);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Audit file-spec evidence on restart. Review/running/failed work is reopened;
|
|
88
|
+
// an explicitly paused squad remains paused and suspended/cancelled tasks stay untouched.
|
|
89
|
+
for (const squad of store.listSquadsForProject(ctx.cwd).filter((candidate) => candidate.spec && ["running", "failed", "paused", "review"].includes(candidate.status))) {
|
|
90
|
+
const scheduler = reviveScheduler(pi, squad.id, squadSkillPaths);
|
|
91
|
+
const invalid = await scheduler.auditSpecAttestations();
|
|
92
|
+
if (invalid.length > 0 && squad.status !== "paused") await scheduler.start();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Reconstruct runtime.schedulers for explicit-suspension stalls without resuming any
|
|
96
|
+
// task. Reconcile derives/persists a missing outbox record and emits only a
|
|
97
|
+
// pending fingerprint; delivered attention remains durable and silent.
|
|
98
|
+
const suspensionCandidates = store.listSquadsForProject(ctx.cwd)
|
|
99
|
+
.filter((squad) => Boolean(squad.suspendedStallAttention) || store.loadAllTasks(squad.id).some((task) => task.status === "suspended"));
|
|
100
|
+
for (const squad of suspensionCandidates) {
|
|
101
|
+
const scheduler = reviveScheduler(pi, squad.id, squadSkillPaths);
|
|
102
|
+
await scheduler.start();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Mailbox recovery is automatic after extension/main-process restart. Scan
|
|
106
|
+
// every project squad (including accepted done squads) because a crash can
|
|
107
|
+
// occur after the mailbox-first write but before the squad/task is reopened.
|
|
108
|
+
const pendingMailSquads = store.listSquadsForProject(ctx.cwd)
|
|
109
|
+
.filter((squad) => store.loadAllTasks(squad.id)
|
|
110
|
+
.some((task) => task.status !== "cancelled" && store.loadPendingTaskMessages(squad.id, task.id).length > 0))
|
|
111
|
+
.sort((a, b) => b.created.localeCompare(a.created));
|
|
112
|
+
for (const squad of pendingMailSquads) {
|
|
113
|
+
const scheduler = reviveScheduler(pi, squad.id, squadSkillPaths);
|
|
114
|
+
await scheduler.start();
|
|
115
|
+
}
|
|
116
|
+
if (pendingMailSquads.length > 0) focusSquad(pendingMailSquads[0].id);
|
|
117
|
+
|
|
118
|
+
// Notify about paused squads only if they have real completed work
|
|
119
|
+
const paused = store.findActiveSquads()
|
|
120
|
+
.filter((s) => s.cwd === ctx.cwd && s.status === "paused");
|
|
121
|
+
if (paused.length > 0) {
|
|
122
|
+
const squad = paused[0];
|
|
123
|
+
const tasks = store.loadAllTasks(squad.id);
|
|
124
|
+
const done = tasks.filter(t => t.status === "done").length;
|
|
125
|
+
// Only notify if at least 1 task completed — worth resuming
|
|
126
|
+
if (done > 0) {
|
|
127
|
+
pi.sendMessage({
|
|
128
|
+
customType: "squad-paused",
|
|
129
|
+
content: `[squad] Found paused squad "${squad.id}" (${squad.spec ? `file spec sha256=${squad.spec.sha256}` : squad.goal}) — ${formatTaskProgress(tasks)}. ` +
|
|
130
|
+
`Use squad_modify with action "resume" to continue, or start a new squad.`,
|
|
131
|
+
display: true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Restore pending acceptance gates after a main-session restart. Review is
|
|
137
|
+
// persisted state, not a one-shot completion message that can be missed.
|
|
138
|
+
const pendingReviews = store.findActiveSquads()
|
|
139
|
+
.filter((s) => s.cwd === ctx.cwd && s.status === "review");
|
|
140
|
+
if (pendingReviews.length > 0) {
|
|
141
|
+
const squad = pendingReviews.sort((a, b) => b.created.localeCompare(a.created))[0];
|
|
142
|
+
focusSquad(squad.id);
|
|
143
|
+
const tasks = store.loadAllTasks(squad.id);
|
|
144
|
+
pi.sendMessage({
|
|
145
|
+
customType: "squad-review-required",
|
|
146
|
+
content: `[squad] Restored mandatory orchestrator review for "${squad.id}" after session restart. The work remains untrusted and not accepted.\n\n${buildOrchestratorReviewGate(squad, tasks)}`,
|
|
147
|
+
display: true,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
pi.on("session_shutdown", async () => {
|
|
154
|
+
focusSquad(null);
|
|
155
|
+
runtime.widgetControls?.dispose();
|
|
156
|
+
runtime.widgetControls = null;
|
|
157
|
+
for (const [id, sched] of runtime.schedulers) {
|
|
158
|
+
await sched.stop();
|
|
159
|
+
}
|
|
160
|
+
runtime.schedulers.clear();
|
|
161
|
+
runtime.uiCtx = null;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
}
|
package/src/panel/squad-panel.ts
CHANGED
|
@@ -41,6 +41,8 @@ export class SquadPanel implements Component, Focusable {
|
|
|
41
41
|
private done: (result: SquadPanelResult) => void;
|
|
42
42
|
private scheduler: Scheduler;
|
|
43
43
|
private squadId: string;
|
|
44
|
+
private isEnabled: () => boolean;
|
|
45
|
+
private onDisabled: () => void;
|
|
44
46
|
|
|
45
47
|
/** Callback for sending messages — set by the extension */
|
|
46
48
|
onSendMessage?: (taskId: string, message: string) => Promise<void>;
|
|
@@ -69,12 +71,16 @@ export class SquadPanel implements Component, Focusable {
|
|
|
69
71
|
scheduler: Scheduler,
|
|
70
72
|
squadId: string,
|
|
71
73
|
done: (result: SquadPanelResult) => void,
|
|
74
|
+
isEnabled: () => boolean = () => true,
|
|
75
|
+
onDisabled: () => void = () => {},
|
|
72
76
|
) {
|
|
73
77
|
this.tui = tui;
|
|
74
78
|
this.theme = theme;
|
|
75
79
|
this.scheduler = scheduler;
|
|
76
80
|
this.squadId = squadId;
|
|
77
81
|
this.done = done;
|
|
82
|
+
this.isEnabled = isEnabled;
|
|
83
|
+
this.onDisabled = onDisabled;
|
|
78
84
|
|
|
79
85
|
this.taskListView = new TaskListView(theme, squadId);
|
|
80
86
|
this.messageView = new MessageView(theme, squadId);
|
|
@@ -112,6 +118,12 @@ export class SquadPanel implements Component, Focusable {
|
|
|
112
118
|
// =========================================================================
|
|
113
119
|
|
|
114
120
|
handleInput(data: string): void {
|
|
121
|
+
if (!this.isEnabled()) {
|
|
122
|
+
this.onDisabled();
|
|
123
|
+
this.finish({ action: "close" });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
115
127
|
// Ctrl+Q or q: close panel
|
|
116
128
|
if (data === "\x11") {
|
|
117
129
|
this.finish({ action: "close" });
|
|
@@ -316,6 +328,10 @@ export class SquadPanel implements Component, Focusable {
|
|
|
316
328
|
// Lifecycle
|
|
317
329
|
// =========================================================================
|
|
318
330
|
|
|
331
|
+
close(): void {
|
|
332
|
+
this.finish({ action: "close" });
|
|
333
|
+
}
|
|
334
|
+
|
|
319
335
|
dispose(): void {
|
|
320
336
|
if (this.renderTimeout) {
|
|
321
337
|
clearTimeout(this.renderTimeout);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { SquadPanel, type SquadPanelResult } from "./panel/squad-panel.js";
|
|
3
|
+
import { getReviewPresentation } from "./presentation.js";
|
|
4
|
+
import type { Scheduler } from "./scheduler.js";
|
|
5
|
+
import * as store from "./store.js";
|
|
6
|
+
import type { Squad } from "./types.js";
|
|
7
|
+
import { DISABLED_GUIDANCE, focusSquad, forceWidgetUpdate, formatTaskProgress, runtime } from "./runtime.js";
|
|
8
|
+
import { reviveScheduler } from "./scheduler-runtime.js";
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Squad Selection & Activation
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Show an interactive selector to pick a squad.
|
|
16
|
+
* Returns the selected squad or undefined if cancelled.
|
|
17
|
+
*/
|
|
18
|
+
export async function pickSquad(
|
|
19
|
+
ctx: import("@earendil-works/pi-coding-agent").ExtensionContext | import("@earendil-works/pi-coding-agent").ExtensionCommandContext,
|
|
20
|
+
squads: Squad[],
|
|
21
|
+
showProject = false,
|
|
22
|
+
): Promise<Squad | undefined> {
|
|
23
|
+
if (squads.length === 0) return undefined;
|
|
24
|
+
|
|
25
|
+
const options = squads.map((s) => {
|
|
26
|
+
const tasks = store.loadAllTasks(s.id);
|
|
27
|
+
const cost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
28
|
+
const review = getReviewPresentation(s);
|
|
29
|
+
const icon = review?.icon ?? (s.status === "done" ? "✓" : s.status === "running" ? "⏳" : s.status === "failed" ? "✗" : "·");
|
|
30
|
+
const acceptance = review ? ` ${review.label}` : ` [${s.status}]`;
|
|
31
|
+
const project = showProject ? ` — ${s.cwd.split("/").pop()}` : "";
|
|
32
|
+
return `${icon} ${s.id}${acceptance} · ${formatTaskProgress(tasks)} $${cost.toFixed(2)}${project}`;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const choice = await ctx.ui.select("Select a squad", options);
|
|
36
|
+
if (choice === undefined) return undefined;
|
|
37
|
+
|
|
38
|
+
const idx = options.indexOf(choice);
|
|
39
|
+
return idx >= 0 ? squads[idx] : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Activate a squad for viewing in this session.
|
|
44
|
+
* Sets runtime.activeSquadId, starts widget, shows notification.
|
|
45
|
+
* Does NOT start a scheduler (view-only unless squad needs resuming).
|
|
46
|
+
*/
|
|
47
|
+
export function activateSquadView(squadId: string, ctx: import("@earendil-works/pi-coding-agent").ExtensionContext | import("@earendil-works/pi-coding-agent").ExtensionCommandContext): void {
|
|
48
|
+
if (!runtime.squadEnabled) {
|
|
49
|
+
ctx.ui.notify(DISABLED_GUIDANCE, "warning");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const squad = store.loadSquad(squadId);
|
|
53
|
+
if (!squad) {
|
|
54
|
+
ctx.ui.notify(`Squad '${squadId}' not found`, "error");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Selection is one atomic focus operation: panel, widget, status, and tools
|
|
59
|
+
// must all target this exact squad before control returns to the caller.
|
|
60
|
+
focusSquad(squadId);
|
|
61
|
+
|
|
62
|
+
// Compact notification — widget already shows full task details.
|
|
63
|
+
// Avoid large multi-line notifications that can break TUI layout.
|
|
64
|
+
const tasks = store.loadAllTasks(squadId);
|
|
65
|
+
const cost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
66
|
+
ctx.ui.notify(`Viewing: ${squad.id} [${squad.status}] ${formatTaskProgress(tasks)} $${cost.toFixed(2)}`, "info");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Panel — overlay via ctx.ui.custom() with proper done() lifecycle
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Open the squad panel overlay.
|
|
75
|
+
* Uses the pi-interactive-shell pattern: ctx.ui.custom() returns a Promise
|
|
76
|
+
* that resolves when done() is called. The panel calls done() on close.
|
|
77
|
+
*/
|
|
78
|
+
export function openPanel(
|
|
79
|
+
pi: ExtensionAPI,
|
|
80
|
+
ctx: import("@earendil-works/pi-coding-agent").ExtensionContext,
|
|
81
|
+
scheduler: Scheduler,
|
|
82
|
+
squadId: string,
|
|
83
|
+
skillPaths: string[],
|
|
84
|
+
): void {
|
|
85
|
+
if (!runtime.squadEnabled) {
|
|
86
|
+
ctx.ui.notify(DISABLED_GUIDANCE, "warning");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (runtime.overlayOpen) return;
|
|
90
|
+
// Opening details also establishes authoritative focus. This covers callers
|
|
91
|
+
// that reconstructed or targeted a scheduler without going through selector UI.
|
|
92
|
+
focusSquad(squadId);
|
|
93
|
+
runtime.overlayOpen = true;
|
|
94
|
+
|
|
95
|
+
// The promise resolves when the panel calls done()
|
|
96
|
+
const panelPromise = ctx.ui.custom<SquadPanelResult>(
|
|
97
|
+
(tui, theme, _kb, done) => {
|
|
98
|
+
const panel = new SquadPanel(
|
|
99
|
+
tui,
|
|
100
|
+
theme,
|
|
101
|
+
scheduler,
|
|
102
|
+
squadId,
|
|
103
|
+
done,
|
|
104
|
+
() => runtime.squadEnabled,
|
|
105
|
+
() => ctx.ui.notify(DISABLED_GUIDANCE, "warning"),
|
|
106
|
+
);
|
|
107
|
+
runtime.closeOverlay = () => panel.close();
|
|
108
|
+
|
|
109
|
+
// Wire up message sending from panel
|
|
110
|
+
panel.onSendMessage = async (taskId: string, _prefill: string) => {
|
|
111
|
+
if (!runtime.squadEnabled) {
|
|
112
|
+
ctx.ui.notify(DISABLED_GUIDANCE, "warning");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const task = store.loadTask(squadId, taskId);
|
|
116
|
+
const agentName = task?.agent || taskId;
|
|
117
|
+
const input = await ctx.ui.input(`Message to ${agentName}`, "Type your message...");
|
|
118
|
+
if (!runtime.squadEnabled) {
|
|
119
|
+
ctx.ui.notify(DISABLED_GUIDANCE, "warning");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (input) {
|
|
123
|
+
const panelSched = reviveScheduler(pi, squadId, skillPaths);
|
|
124
|
+
await panelSched.start();
|
|
125
|
+
const delivered = await panelSched.sendHumanMessage(taskId, input);
|
|
126
|
+
ctx.ui.notify(
|
|
127
|
+
delivered ? `Sent to ${agentName}: "${input}"` : `Queued durably for ${taskId}`,
|
|
128
|
+
"info",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
tui.requestRender();
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return panel;
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
overlay: true,
|
|
138
|
+
overlayOptions: {
|
|
139
|
+
anchor: "center" as const,
|
|
140
|
+
width: "80%" as const,
|
|
141
|
+
maxHeight: "80%" as const,
|
|
142
|
+
margin: 2,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// When panel closes (done() called), clean up
|
|
148
|
+
panelPromise.then(() => {
|
|
149
|
+
runtime.overlayOpen = false;
|
|
150
|
+
runtime.closeOverlay = null;
|
|
151
|
+
forceWidgetUpdate();
|
|
152
|
+
}).catch(() => {
|
|
153
|
+
runtime.overlayOpen = false;
|
|
154
|
+
runtime.closeOverlay = null;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
package/src/protocol.ts
CHANGED
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
* 2. Agent identity (role, custom prompt)
|
|
7
7
|
* 3. Chain context (completed dependency outputs)
|
|
8
8
|
* 4. Sibling awareness (parallel tasks, file map)
|
|
9
|
-
* 5.
|
|
10
|
-
* 6. Queued messages (received while agent wasn't running)
|
|
9
|
+
* 5. Queued messages (received while agent wasn't running)
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
|
-
import type { AgentDef,
|
|
14
|
-
import {
|
|
12
|
+
import type { AgentDef, Squad, Task, TaskMessage } from "./types.js";
|
|
13
|
+
import { loadAllTasks, loadMessages } from "./store.js";
|
|
15
14
|
|
|
16
15
|
// ============================================================================
|
|
17
16
|
// Squad Protocol (injected into every agent)
|
|
@@ -186,47 +185,6 @@ function buildSiblingAwareness(
|
|
|
186
185
|
return lines.join("\n") + "\n";
|
|
187
186
|
}
|
|
188
187
|
|
|
189
|
-
// ============================================================================
|
|
190
|
-
// Knowledge
|
|
191
|
-
// ============================================================================
|
|
192
|
-
|
|
193
|
-
function buildKnowledgeSection(squadId: string): string {
|
|
194
|
-
const entries = loadAllKnowledge(squadId);
|
|
195
|
-
if (entries.length === 0) return "";
|
|
196
|
-
|
|
197
|
-
const decisions = entries.filter((e) => e.type === "decision");
|
|
198
|
-
const conventions = entries.filter((e) => e.type === "convention");
|
|
199
|
-
const findings = entries.filter((e) => e.type === "finding");
|
|
200
|
-
|
|
201
|
-
const lines: string[] = ["# Squad Knowledge\n"];
|
|
202
|
-
|
|
203
|
-
if (decisions.length > 0) {
|
|
204
|
-
lines.push("## Decisions");
|
|
205
|
-
for (const d of decisions) {
|
|
206
|
-
lines.push(`- ${d.text} (${d.from})`);
|
|
207
|
-
}
|
|
208
|
-
lines.push("");
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (conventions.length > 0) {
|
|
212
|
-
lines.push("## Project Conventions");
|
|
213
|
-
for (const c of conventions) {
|
|
214
|
-
lines.push(`- ${c.text} (${c.from})`);
|
|
215
|
-
}
|
|
216
|
-
lines.push("");
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (findings.length > 0) {
|
|
220
|
-
lines.push("## Findings");
|
|
221
|
-
for (const f of findings) {
|
|
222
|
-
lines.push(`- ${f.text} (${f.from})`);
|
|
223
|
-
}
|
|
224
|
-
lines.push("");
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return lines.join("\n");
|
|
228
|
-
}
|
|
229
|
-
|
|
230
188
|
// ============================================================================
|
|
231
189
|
// Queued Messages
|
|
232
190
|
// ============================================================================
|
|
@@ -326,7 +284,6 @@ export function buildAgentSystemPrompt(options: ProtocolBuildOptions): string {
|
|
|
326
284
|
buildAgentIdentity(agentDef),
|
|
327
285
|
...(task.fileSpecDelta ? [buildTaskSection(task), buildReworkContext(task, squadId)] : []),
|
|
328
286
|
buildChainContext(task, allTasks, squadId),
|
|
329
|
-
buildKnowledgeSection(squadId),
|
|
330
287
|
buildQueuedMessages(queuedMessages),
|
|
331
288
|
].filter((section) => section.length > 0);
|
|
332
289
|
return fileSections.join("\n---\n\n");
|
|
@@ -339,7 +296,6 @@ export function buildAgentSystemPrompt(options: ProtocolBuildOptions): string {
|
|
|
339
296
|
buildReworkContext(task, squadId),
|
|
340
297
|
buildChainContext(task, allTasks, squadId),
|
|
341
298
|
buildSiblingAwareness(task, allTasks, modifiedFiles),
|
|
342
|
-
buildKnowledgeSection(squadId),
|
|
343
299
|
buildQueuedMessages(queuedMessages),
|
|
344
300
|
].filter((s) => s.length > 0);
|
|
345
301
|
|
package/src/report.ts
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
|
+
import { execFileSync, type ExecFileSyncOptionsWithStringEncoding } from "node:child_process";
|
|
1
2
|
import type { Task } from "./types.js";
|
|
2
3
|
|
|
4
|
+
const SNAPSHOT_COMMAND_OPTIONS: ExecFileSyncOptionsWithStringEncoding = {
|
|
5
|
+
encoding: "utf8",
|
|
6
|
+
timeout: 2_000,
|
|
7
|
+
maxBuffer: 256 * 1024,
|
|
8
|
+
windowsHide: true,
|
|
9
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function buildWorkingTreeSnapshot(cwd: string): string {
|
|
13
|
+
try {
|
|
14
|
+
const insideWorkTree = execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
15
|
+
...SNAPSHOT_COMMAND_OPTIONS,
|
|
16
|
+
cwd,
|
|
17
|
+
}).trim();
|
|
18
|
+
if (insideWorkTree !== "true") return "";
|
|
19
|
+
|
|
20
|
+
const diffStat = execFileSync("git", ["diff", "--stat"], {
|
|
21
|
+
...SNAPSHOT_COMMAND_OPTIONS,
|
|
22
|
+
cwd,
|
|
23
|
+
}).trimEnd();
|
|
24
|
+
const status = execFileSync("git", ["status", "--short"], {
|
|
25
|
+
...SNAPSHOT_COMMAND_OPTIONS,
|
|
26
|
+
cwd,
|
|
27
|
+
});
|
|
28
|
+
const untrackedCount = status.match(/^\?\? /gm)?.length ?? 0;
|
|
29
|
+
|
|
30
|
+
return ["Working Tree Snapshot", diffStat, `Untracked files: ${untrackedCount}`]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join("\n");
|
|
33
|
+
} catch {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
3
38
|
/**
|
|
4
39
|
* Build the full task handoff included in squad completion notifications.
|
|
5
40
|
* This is durable report data, not a UI preview: never shorten task output.
|
|
6
41
|
*/
|
|
7
|
-
export function buildCompletionSummary(tasks: Task[]): string {
|
|
42
|
+
export function buildCompletionSummary(tasks: Task[], cwd?: string): string {
|
|
8
43
|
const completed = tasks
|
|
9
44
|
.filter((task) => task.status === "done")
|
|
10
45
|
.map((task) => `- ${task.id} (${task.agent}): ${task.output || "done"}`)
|
|
@@ -14,9 +49,12 @@ export function buildCompletionSummary(tasks: Task[]): string {
|
|
|
14
49
|
.map((task) => `- ${task.id} (${task.agent}): cancelled`)
|
|
15
50
|
.join("\n");
|
|
16
51
|
|
|
17
|
-
|
|
52
|
+
const legacySummary = [completed, cancelled ? `CANCELLED TASKS (neutral; not successful output)\n${cancelled}` : ""]
|
|
18
53
|
.filter(Boolean)
|
|
19
54
|
.join("\n\n");
|
|
55
|
+
const snapshot = cwd ? buildWorkingTreeSnapshot(cwd) : "";
|
|
56
|
+
|
|
57
|
+
return [legacySummary, snapshot].filter(Boolean).join("\n\n");
|
|
20
58
|
}
|
|
21
59
|
|
|
22
60
|
/** Build the full failure handoff without shortening diagnostics. */
|