pi-squad 0.17.2 → 0.19.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.
@@ -0,0 +1,139 @@
1
+ /**
2
+ * inline-input.ts — tolerant decoding for inline squad-start arguments.
3
+ *
4
+ * Some tool transports (MCP bridges, JSON-over-JSON encoders) deliver the
5
+ * structured `tasks`/`agents`/`config` fields as JSON-encoded strings instead
6
+ * of arrays/objects. The squad tool accepts both and coerces strings back to
7
+ * structures with precise errors, so a correct plan never fails on transport
8
+ * shape alone. Semantic plan validation (ids, dependencies, cycles, agents)
9
+ * still happens in plan-rules/startSquad after coercion.
10
+ */
11
+ import type { InlineSquadStart } from "./runtime.js";
12
+
13
+ export type InlineCoercion =
14
+ | { ok: true; value: InlineSquadStart }
15
+ | { ok: false; error: string };
16
+
17
+ type InlineTask = NonNullable<InlineSquadStart["tasks"]>[number];
18
+ type InlineAgents = NonNullable<InlineSquadStart["agents"]>;
19
+ type InlineConfig = NonNullable<InlineSquadStart["config"]>;
20
+
21
+ function parseIfString(value: unknown, field: string): { ok: true; value: unknown } | { ok: false; error: string } {
22
+ if (typeof value !== "string") return { ok: true, value };
23
+ try {
24
+ return { ok: true, value: JSON.parse(value) };
25
+ } catch (error) {
26
+ return { ok: false, error: `${field} arrived as a JSON string that is not valid JSON (${(error as Error).message}). Send a real ${field === "tasks" ? "array" : "object"}, or a JSON-encoded one.` };
27
+ }
28
+ }
29
+
30
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
31
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
32
+ }
33
+
34
+ function coerceTasks(input: unknown): { ok: true; value: InlineTask[] | undefined } | { ok: false; error: string } {
35
+ if (input === undefined) return { ok: true, value: undefined };
36
+ if (!Array.isArray(input)) return { ok: false, error: "tasks must be an array of task objects (or a JSON-encoded array)." };
37
+ const tasks: InlineTask[] = [];
38
+ for (let i = 0; i < input.length; i++) {
39
+ const raw = input[i];
40
+ if (!isPlainObject(raw)) return { ok: false, error: `tasks[${i}] must be an object.` };
41
+ for (const key of ["id", "title", "agent"] as const) {
42
+ if (typeof raw[key] !== "string" || raw[key].length === 0) {
43
+ return { ok: false, error: `tasks[${i}].${key} must be a nonempty string.` };
44
+ }
45
+ }
46
+ if (raw.description !== undefined && typeof raw.description !== "string") {
47
+ return { ok: false, error: `tasks[${i}].description must be a string when present.` };
48
+ }
49
+ if (raw.depends !== undefined && (!Array.isArray(raw.depends) || !raw.depends.every((d) => typeof d === "string"))) {
50
+ return { ok: false, error: `tasks[${i}].depends must be an array of task-id strings when present.` };
51
+ }
52
+ if (raw.inheritContext !== undefined && typeof raw.inheritContext !== "boolean") {
53
+ return { ok: false, error: `tasks[${i}].inheritContext must be a boolean when present.` };
54
+ }
55
+ tasks.push({
56
+ id: raw.id as string,
57
+ title: raw.title as string,
58
+ agent: raw.agent as string,
59
+ ...(raw.description !== undefined ? { description: raw.description as string } : {}),
60
+ ...(raw.depends !== undefined ? { depends: raw.depends as string[] } : {}),
61
+ ...(raw.inheritContext !== undefined ? { inheritContext: raw.inheritContext as boolean } : {}),
62
+ });
63
+ }
64
+ return { ok: true, value: tasks };
65
+ }
66
+
67
+ function coerceAgents(input: unknown): { ok: true; value: InlineAgents | undefined } | { ok: false; error: string } {
68
+ if (input === undefined) return { ok: true, value: undefined };
69
+ if (!isPlainObject(input)) return { ok: false, error: "agents must be an object mapping agent names to overrides (or a JSON-encoded object)." };
70
+ const agents: InlineAgents = {};
71
+ for (const [name, raw] of Object.entries(input)) {
72
+ if (!isPlainObject(raw)) return { ok: false, error: `agents.${name} must be an object.` };
73
+ if (raw.model !== undefined && typeof raw.model !== "string") return { ok: false, error: `agents.${name}.model must be a string when present.` };
74
+ if (raw.thinking !== undefined && typeof raw.thinking !== "string") return { ok: false, error: `agents.${name}.thinking must be a string when present.` };
75
+ agents[name] = {
76
+ ...(raw.model !== undefined ? { model: raw.model as string } : {}),
77
+ ...(raw.thinking !== undefined ? { thinking: raw.thinking as string } : {}),
78
+ };
79
+ }
80
+ return { ok: true, value: agents };
81
+ }
82
+
83
+ function coerceConfig(input: unknown): { ok: true; value: InlineConfig | undefined } | { ok: false; error: string } {
84
+ if (input === undefined) return { ok: true, value: undefined };
85
+ if (!isPlainObject(input)) return { ok: false, error: "config must be an object (or a JSON-encoded object)." };
86
+ if (input.maxConcurrency !== undefined && typeof input.maxConcurrency !== "number") {
87
+ return { ok: false, error: "config.maxConcurrency must be a number when present." };
88
+ }
89
+ if (input.autoUnblock !== undefined && typeof input.autoUnblock !== "boolean") {
90
+ return { ok: false, error: "config.autoUnblock must be a boolean when present." };
91
+ }
92
+ if (input.maxRetries !== undefined && typeof input.maxRetries !== "number") {
93
+ return { ok: false, error: "config.maxRetries must be a number when present." };
94
+ }
95
+ return {
96
+ ok: true,
97
+ value: {
98
+ ...(input.maxConcurrency !== undefined ? { maxConcurrency: input.maxConcurrency as number } : {}),
99
+ ...(input.autoUnblock !== undefined ? { autoUnblock: input.autoUnblock as boolean } : {}),
100
+ ...(input.maxRetries !== undefined ? { maxRetries: input.maxRetries as number } : {}),
101
+ },
102
+ };
103
+ }
104
+
105
+ /** Decode inline squad-start input, accepting JSON-encoded structured fields. */
106
+ export function coerceInlineSquadStart(raw: {
107
+ goal: string;
108
+ agents?: unknown;
109
+ tasks?: unknown;
110
+ config?: unknown;
111
+ }): InlineCoercion {
112
+ if (typeof raw.goal !== "string" || raw.goal.trim().length === 0) {
113
+ return { ok: false, error: "goal must be a nonempty string." };
114
+ }
115
+
116
+ const tasksParsed = parseIfString(raw.tasks, "tasks");
117
+ if (!tasksParsed.ok) return tasksParsed;
118
+ const agentsParsed = parseIfString(raw.agents, "agents");
119
+ if (!agentsParsed.ok) return agentsParsed;
120
+ const configParsed = parseIfString(raw.config, "config");
121
+ if (!configParsed.ok) return configParsed;
122
+
123
+ const tasks = coerceTasks(tasksParsed.value);
124
+ if (!tasks.ok) return tasks;
125
+ const agents = coerceAgents(agentsParsed.value);
126
+ if (!agents.ok) return agents;
127
+ const config = coerceConfig(configParsed.value);
128
+ if (!config.ok) return config;
129
+
130
+ return {
131
+ ok: true,
132
+ value: {
133
+ goal: raw.goal,
134
+ ...(agents.value !== undefined ? { agents: agents.value } : {}),
135
+ ...(tasks.value !== undefined ? { tasks: tasks.value } : {}),
136
+ ...(config.value !== undefined ? { config: config.value } : {}),
137
+ },
138
+ };
139
+ }
@@ -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
+ }
@@ -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/planner.ts CHANGED
@@ -251,8 +251,7 @@ Respond with a JSON object (and nothing else outside the JSON):
251
251
  \`\`\`json
252
252
  {
253
253
  "agents": {
254
- "agent-name": {},
255
- "agent-name": { "model": "override-model-if-needed", "thinking": "high" }
254
+ "agent-name": {}
256
255
  },
257
256
  "tasks": [
258
257
  {
@@ -269,7 +268,7 @@ Respond with a JSON object (and nothing else outside the JSON):
269
268
  ## Rules
270
269
  ${PLAN_STRUCTURE_RULES}
271
270
  - Only reference agents that exist in the Available Agents list
272
- - Agent overrides are optional: "model" (a pi model id) and "thinking" (one of: off, minimal, low, medium, high, xhigh, max). Omit both to use defaults. Raise "thinking" only for tasks needing deep reasoning (architecture, security)
271
+ - Agent entries stay empty ({}) so each agent uses its configured model/thinking (agent definition, then squad defaults). Set "model" (a pi model id) or "thinking" (off, minimal, low, medium, high, xhigh, max) ONLY when the goal explicitly requests a specific model or thinking level
273
272
 
274
273
  ## Task Descriptions
275
274
  ${TASK_DESCRIPTION_GUIDE}
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. Knowledge entries (decisions, conventions, findings)
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, KnowledgeEntry, Squad, Task, TaskMessage } from "./types.js";
14
- import { loadAllKnowledge, loadAllTasks, loadMessages } from "./store.js";
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
- return [completed, cancelled ? `CANCELLED TASKS (neutral; not successful output)\n${cancelled}` : ""]
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. */