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
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { chunkRanges, type PreparedSpec } from "./file-spec.js";
|
|
4
|
+
import { logError } from "./logger.js";
|
|
5
|
+
import { validatePlan } from "./plan-rules.js";
|
|
6
|
+
import { runPlanner } from "./planner.js";
|
|
7
|
+
import { Scheduler } from "./scheduler.js";
|
|
8
|
+
import { wireSchedulerEvents } from "./scheduler-runtime.js";
|
|
9
|
+
import * as store from "./store.js";
|
|
10
|
+
import type { PlannerOutput, Squad, SquadAgentEntry, SquadConfig, Task } from "./types.js";
|
|
11
|
+
import { DEFAULT_SQUAD_CONFIG } from "./types.js";
|
|
12
|
+
import { disabledToolResult, focusSquad, resolveSquadDefaults, runtime, schedulerSpawnContext, type InlineSquadStart } from "./runtime.js";
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Start Squad
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export async function startSquad(
|
|
19
|
+
squadId: string,
|
|
20
|
+
params: InlineSquadStart,
|
|
21
|
+
cwd: string,
|
|
22
|
+
skillPaths: string[],
|
|
23
|
+
pi: ExtensionAPI,
|
|
24
|
+
sessionFile: string | null = null,
|
|
25
|
+
preparedSpec?: PreparedSpec,
|
|
26
|
+
) {
|
|
27
|
+
let plan: PlannerOutput;
|
|
28
|
+
|
|
29
|
+
if (params.tasks && params.tasks.length > 0) {
|
|
30
|
+
// User provided a plan — use it directly
|
|
31
|
+
plan = {
|
|
32
|
+
agents: params.agents || {},
|
|
33
|
+
tasks: params.tasks.map((t) => ({
|
|
34
|
+
...t,
|
|
35
|
+
description: t.description || "",
|
|
36
|
+
depends: t.depends || [],
|
|
37
|
+
})),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Validate agent names — remap unknown agents to fullstack
|
|
41
|
+
for (const task of plan.tasks) {
|
|
42
|
+
const agentDef = store.loadAgentDef(task.agent, cwd);
|
|
43
|
+
if (!agentDef || agentDef.disabled) {
|
|
44
|
+
if (preparedSpec) throw new Error(`SPEC_MALFORMED: assigned agent '${task.agent}' is missing or disabled`);
|
|
45
|
+
const original = task.agent;
|
|
46
|
+
task.agent = "fullstack";
|
|
47
|
+
task.description = `[Note: agent "${original}" not found, remapped to fullstack]\n\n${task.description}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
// Run planner to generate task breakdown (squad default policy as fallback)
|
|
52
|
+
try {
|
|
53
|
+
const defaults = resolveSquadDefaults();
|
|
54
|
+
plan = await runPlanner({ goal: params.goal, cwd, fallbackModel: defaults.model, fallbackThinking: defaults.thinking });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// Throwing marks the tool result as an error for the LLM (returning isError is ignored in current pi)
|
|
57
|
+
throw new Error(`Failed to plan: ${(error as Error).message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A planner may take long enough for /squad disable to run concurrently.
|
|
62
|
+
// Re-check before publishing any squad or constructing a scheduler.
|
|
63
|
+
if (!runtime.squadEnabled) return disabledToolResult();
|
|
64
|
+
|
|
65
|
+
// Merge agent roster
|
|
66
|
+
const agents: Record<string, SquadAgentEntry> = { ...plan.agents };
|
|
67
|
+
if (params.agents) {
|
|
68
|
+
for (const [name, entry] of Object.entries(params.agents)) {
|
|
69
|
+
agents[name] = { ...agents[name], ...entry };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Validate the plan — same enforcement for main-session and planner plans.
|
|
74
|
+
// Errors block squad creation; warnings are reported back to the plan author.
|
|
75
|
+
const validation = validatePlan(plan.tasks);
|
|
76
|
+
if (validation.errors.length > 0) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Plan rejected:\n- ${validation.errors.join("\n- ")}\n\nFix the task list and call squad again.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Create squad
|
|
83
|
+
const config: SquadConfig = {
|
|
84
|
+
...DEFAULT_SQUAD_CONFIG,
|
|
85
|
+
...(params.config?.maxConcurrency ? { maxConcurrency: params.config.maxConcurrency } : {}),
|
|
86
|
+
...(typeof params.config?.autoUnblock === "boolean" ? { autoUnblock: params.config.autoUnblock } : {}),
|
|
87
|
+
...(typeof params.config?.maxRetries === "number" ? { maxRetries: params.config.maxRetries } : {}),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const squad: Squad = {
|
|
91
|
+
id: squadId,
|
|
92
|
+
goal: params.goal,
|
|
93
|
+
status: "running",
|
|
94
|
+
created: store.now(),
|
|
95
|
+
cwd,
|
|
96
|
+
sessionFile,
|
|
97
|
+
agents,
|
|
98
|
+
config,
|
|
99
|
+
...(preparedSpec ? { spec: {
|
|
100
|
+
schemaVersion: 1 as const,
|
|
101
|
+
sha256: preparedSpec.sha256,
|
|
102
|
+
bytes: preparedSpec.raw.length,
|
|
103
|
+
path: path.join(store.getSquadDir(squadId), "spec", "spec.v1.json"),
|
|
104
|
+
chunkBytes: 32768 as const,
|
|
105
|
+
chunkCount: chunkRanges(preparedSpec.raw).length,
|
|
106
|
+
} } : {}),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Materialize task state in memory so file squads can publish spec+squad+tasks atomically.
|
|
110
|
+
const initialTasks: Task[] = plan.tasks.map((taskDef) => {
|
|
111
|
+
const task: Task = {
|
|
112
|
+
id: taskDef.id,
|
|
113
|
+
title: taskDef.title,
|
|
114
|
+
description: taskDef.description,
|
|
115
|
+
agent: taskDef.agent,
|
|
116
|
+
status: taskDef.depends.length === 0 ? "pending" : "blocked",
|
|
117
|
+
depends: taskDef.depends,
|
|
118
|
+
...(taskDef.inheritContext ? { inheritContext: true } : {}),
|
|
119
|
+
created: store.now(),
|
|
120
|
+
started: null,
|
|
121
|
+
completed: null,
|
|
122
|
+
output: null,
|
|
123
|
+
error: null,
|
|
124
|
+
usage: { inputTokens: 0, outputTokens: 0, cost: 0, turns: 0 },
|
|
125
|
+
};
|
|
126
|
+
return task;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (preparedSpec) store.publishFileSquad(squad, initialTasks, preparedSpec.raw);
|
|
130
|
+
else {
|
|
131
|
+
store.saveSquad(squad);
|
|
132
|
+
for (const task of initialTasks) store.createTask(squadId, task);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Start scheduler
|
|
136
|
+
const scheduler = new Scheduler(squadId, skillPaths, schedulerSpawnContext);
|
|
137
|
+
runtime.schedulers.set(squadId, scheduler);
|
|
138
|
+
// Activate panel/widget/tool focus as one invariant.
|
|
139
|
+
focusSquad(squadId);
|
|
140
|
+
|
|
141
|
+
// Wire up completion/escalation notifications to main agent.
|
|
142
|
+
wireSchedulerEvents(pi, scheduler, squadId);
|
|
143
|
+
|
|
144
|
+
// Start scheduling — fire and forget, don't block the tool call.
|
|
145
|
+
// scheduler.start() spawns agents which can take seconds per agent.
|
|
146
|
+
// We must return immediately so the main agent's turn completes
|
|
147
|
+
// and the user regains interactive control.
|
|
148
|
+
scheduler.start().catch((err) => {
|
|
149
|
+
logError("squad", `Scheduler start error: ${(err as Error).message}`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Build response. File mode returns only the bounded descriptor; the canonical
|
|
153
|
+
// contract is never reflected back into the main model's transport.
|
|
154
|
+
const taskSummary = preparedSpec
|
|
155
|
+
? `Canonical spec: ${squad.spec!.path}\nSHA-256: ${squad.spec!.sha256}\nBytes: ${squad.spec!.bytes}\nTasks: ${plan.tasks.length}`
|
|
156
|
+
: plan.tasks
|
|
157
|
+
.map((t) => {
|
|
158
|
+
const deps = t.depends.length > 0 ? ` (depends: ${t.depends.join(", ")})` : "";
|
|
159
|
+
return `${t.id} → ${t.agent}: ${t.title}${deps}`;
|
|
160
|
+
})
|
|
161
|
+
.join("\n");
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
content: [
|
|
165
|
+
{
|
|
166
|
+
type: "text" as const,
|
|
167
|
+
text: `Squad "${squadId}" started with ${plan.tasks.length} tasks.\n\n${taskSummary}${
|
|
168
|
+
validation.warnings.length > 0
|
|
169
|
+
? `\n\n⚠️ Plan warnings (fix with squad_modify, or address at review):\n- ${validation.warnings.join("\n- ")}`
|
|
170
|
+
: ""
|
|
171
|
+
}\n\nAgents work in the background — you will be woken automatically when the squad completes, fails, or needs help. Report this plan to the user and END YOUR TURN now. Do NOT poll squad_status, do NOT sleep-wait, do NOT loop.`,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
details: undefined,
|
|
175
|
+
};
|
|
176
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -12,7 +12,6 @@ import * as os from "node:os";
|
|
|
12
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import type {
|
|
14
14
|
AgentDef,
|
|
15
|
-
KnowledgeEntry,
|
|
16
15
|
Squad,
|
|
17
16
|
SquadContext,
|
|
18
17
|
Task,
|
|
@@ -62,12 +61,14 @@ export function getSquadSettingsPath(): string {
|
|
|
62
61
|
return path.join(SQUAD_HOME, "settings.json");
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
/** Load global squad settings, merged over defaults (advisor merged deep) */
|
|
64
|
+
/** Load global squad settings, merged over defaults (advisor merged deep). */
|
|
66
65
|
export function loadSquadSettings(): SquadSettings {
|
|
67
66
|
const loaded = readJson<Partial<SquadSettings>>(getSquadSettingsPath());
|
|
68
67
|
return {
|
|
69
68
|
...DEFAULT_SQUAD_SETTINGS,
|
|
70
69
|
...(loaded || {}),
|
|
70
|
+
// Legacy, missing, and malformed values remain backward-compatible.
|
|
71
|
+
enabled: typeof loaded?.enabled === "boolean" ? loaded.enabled : DEFAULT_SQUAD_SETTINGS.enabled,
|
|
71
72
|
advisor: { ...DEFAULT_SQUAD_SETTINGS.advisor, ...(loaded?.advisor || {}) },
|
|
72
73
|
};
|
|
73
74
|
}
|
|
@@ -101,14 +102,6 @@ export function getContextFilePath(squadId: string): string {
|
|
|
101
102
|
return path.join(getSquadDir(squadId), "context.json");
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
export function getKnowledgeDir(squadId: string): string {
|
|
105
|
-
return path.join(getSquadDir(squadId), "knowledge");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function getMemoryFilePath(): string {
|
|
109
|
-
return path.join(getSquadRoot(), "memory.jsonl");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
105
|
/** Resolve task dir, supporting nested subtasks via parentPath */
|
|
113
106
|
export function getTaskDir(squadId: string, taskId: string, parentPath?: string): string {
|
|
114
107
|
const base = parentPath
|
|
@@ -647,28 +640,6 @@ export function saveContext(squadId: string, context: SquadContext): void {
|
|
|
647
640
|
writeJsonAtomic(getContextFilePath(squadId), context);
|
|
648
641
|
}
|
|
649
642
|
|
|
650
|
-
// ============================================================================
|
|
651
|
-
// Knowledge
|
|
652
|
-
// ============================================================================
|
|
653
|
-
|
|
654
|
-
export function appendKnowledge(squadId: string, type: KnowledgeEntry["type"], entry: KnowledgeEntry): void {
|
|
655
|
-
const file = path.join(getKnowledgeDir(squadId), `${type}s.jsonl`);
|
|
656
|
-
appendJsonl(file, entry);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
export function loadKnowledge(squadId: string, type: KnowledgeEntry["type"]): KnowledgeEntry[] {
|
|
660
|
-
const file = path.join(getKnowledgeDir(squadId), `${type}s.jsonl`);
|
|
661
|
-
return readJsonl<KnowledgeEntry>(file);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
export function loadAllKnowledge(squadId: string): KnowledgeEntry[] {
|
|
665
|
-
return [
|
|
666
|
-
...loadKnowledge(squadId, "decision"),
|
|
667
|
-
...loadKnowledge(squadId, "convention"),
|
|
668
|
-
...loadKnowledge(squadId, "finding"),
|
|
669
|
-
].sort((a, b) => a.ts.localeCompare(b.ts));
|
|
670
|
-
}
|
|
671
|
-
|
|
672
643
|
// ============================================================================
|
|
673
644
|
// Rework Helpers
|
|
674
645
|
// ============================================================================
|
|
@@ -686,18 +657,6 @@ export function getRetryCount(squadId: string, taskId: string): number {
|
|
|
686
657
|
return findRetries(squadId, taskId).length;
|
|
687
658
|
}
|
|
688
659
|
|
|
689
|
-
// ============================================================================
|
|
690
|
-
// Memory (cross-squad)
|
|
691
|
-
// ============================================================================
|
|
692
|
-
|
|
693
|
-
export function appendMemory(entry: KnowledgeEntry): void {
|
|
694
|
-
appendJsonl(getMemoryFilePath(), entry);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
export function loadMemory(): KnowledgeEntry[] {
|
|
698
|
-
return readJsonl<KnowledgeEntry>(getMemoryFilePath());
|
|
699
|
-
}
|
|
700
|
-
|
|
701
660
|
// ============================================================================
|
|
702
661
|
// Bootstrap — first-run agent initialization
|
|
703
662
|
// ============================================================================
|