pi-squad 0.1.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,143 @@
1
+ /**
2
+ * supervisor.ts — On-demand planner agent calls for quality review,
3
+ * plan revision, and conflict resolution.
4
+ *
5
+ * Spawns a short-lived planner agent (one-shot, --mode json) for
6
+ * decisions that need LLM judgment rather than mechanical rules.
7
+ */
8
+
9
+ import type { Squad, SupervisorResult, Task } from "./types.js";
10
+ import * as store from "./store.js";
11
+
12
+ // ============================================================================
13
+ // Supervisor Calls
14
+ // ============================================================================
15
+
16
+ /**
17
+ * Review a completed task's output.
18
+ * Returns approve/revise/escalate verdict.
19
+ */
20
+ export async function reviewTaskCompletion(
21
+ squadId: string,
22
+ task: Task,
23
+ ): Promise<SupervisorResult> {
24
+ const squad = store.loadSquad(squadId);
25
+ if (!squad) {
26
+ return { verdict: "approve", reason: "Squad not found, auto-approving" };
27
+ }
28
+
29
+ const dependents = store
30
+ .loadAllTasks(squadId)
31
+ .filter((t) => t.depends.includes(task.id));
32
+
33
+ const prompt = `Review this completed task for the squad "${squad.goal}":
34
+
35
+ Task: ${task.title}
36
+ Description: ${task.description}
37
+ Agent: ${task.agent}
38
+ Output: ${task.output || "(no output)"}
39
+
40
+ Dependent tasks waiting on this:
41
+ ${dependents.map((t) => `- ${t.title} (${t.agent})`).join("\n") || "(none)"}
42
+
43
+ Questions:
44
+ 1. Does the output satisfy the task description?
45
+ 2. Will dependent tasks have enough context from this output?
46
+ 3. Any concerns before proceeding?
47
+
48
+ Respond with JSON only:
49
+ {"verdict":"approve"|"revise"|"escalate","reason":"...","feedback":"..."}`;
50
+
51
+ try {
52
+ // For now, auto-approve. Full implementation would spawn planner in json mode.
53
+ // This avoids blocking the scheduler on a review for MVP.
54
+ return { verdict: "approve", reason: "Auto-approved (supervisor review not yet enabled)" };
55
+ } catch (error) {
56
+ return {
57
+ verdict: "approve",
58
+ reason: `Supervisor error: ${(error as Error).message}. Auto-approving.`,
59
+ };
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Analyze a stuck agent and suggest next steps.
65
+ */
66
+ export async function analyzeStuckAgent(
67
+ squadId: string,
68
+ taskId: string,
69
+ agentName: string,
70
+ ): Promise<{ action: "retry" | "reassign" | "escalate"; reason: string; suggestion?: string }> {
71
+ const task = store.loadTask(squadId, taskId);
72
+ const messages = store.loadMessages(squadId, taskId);
73
+ const recentMessages = messages.slice(-10);
74
+
75
+ // Simple heuristic for now
76
+ const errorMessages = recentMessages.filter((m) => m.type === "error");
77
+ if (errorMessages.length >= 3) {
78
+ return {
79
+ action: "escalate",
80
+ reason: `Agent ${agentName} has ${errorMessages.length} errors on task ${taskId}`,
81
+ suggestion: "Check the error messages and consider a different approach",
82
+ };
83
+ }
84
+
85
+ return {
86
+ action: "retry",
87
+ reason: `Agent ${agentName} appears stuck on ${taskId}, attempting retry`,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Determine if a blocked agent's request warrants a new subtask.
93
+ */
94
+ export async function analyzeBlockRequest(
95
+ squadId: string,
96
+ taskId: string,
97
+ agentName: string,
98
+ blockReason: string,
99
+ ): Promise<{
100
+ action: "create_subtask" | "route_message" | "escalate";
101
+ subtask?: { title: string; agent: string; description: string };
102
+ targetAgent?: string;
103
+ message?: string;
104
+ }> {
105
+ // Simple pattern matching for common block reasons
106
+ const lower = blockReason.toLowerCase();
107
+
108
+ // If they mention a specific agent, route to them
109
+ const mentionMatch = lower.match(/@(\w+)/);
110
+ if (mentionMatch) {
111
+ const target = mentionMatch[1];
112
+ const projectCwd = store.loadSquad(squadId)?.cwd;
113
+ const agentDef = store.loadAgentDef(target, projectCwd);
114
+ if (agentDef) {
115
+ return {
116
+ action: "route_message",
117
+ targetAgent: target,
118
+ message: blockReason,
119
+ };
120
+ }
121
+ }
122
+
123
+ // If they mention needing something built/created, suggest a subtask
124
+ if (
125
+ lower.includes("need") &&
126
+ (lower.includes("create") || lower.includes("build") || lower.includes("implement"))
127
+ ) {
128
+ return {
129
+ action: "create_subtask",
130
+ subtask: {
131
+ title: `Support: ${blockReason.slice(0, 60)}`,
132
+ agent: "fullstack",
133
+ description: `Created from block request by ${agentName} on task ${taskId}: ${blockReason}`,
134
+ },
135
+ };
136
+ }
137
+
138
+ // Default: escalate to human
139
+ return {
140
+ action: "escalate",
141
+ message: `Agent ${agentName} blocked on ${taskId}: ${blockReason}`,
142
+ };
143
+ }
package/src/types.ts ADDED
@@ -0,0 +1,210 @@
1
+ // ============================================================================
2
+ // Agent Definitions
3
+ // ============================================================================
4
+
5
+ export interface AgentDef {
6
+ /** Agent identifier, matches filename */
7
+ name: string;
8
+ /** One-line role title */
9
+ role: string;
10
+ /** What this agent does (used by planner to pick agents) */
11
+ description: string;
12
+ /** Override model (null = squad default or pi default) */
13
+ model: string | null;
14
+ /** Override tool list (null = all standard tools) */
15
+ tools: string[] | null;
16
+ /** Tags for planner's automatic agent matching */
17
+ tags: string[];
18
+ /** System prompt injected via --append-system-prompt */
19
+ prompt: string;
20
+ }
21
+
22
+ /** Agent entry in squad.json — just overrides, references an AgentDef by key */
23
+ export interface SquadAgentEntry {
24
+ model?: string | null;
25
+ }
26
+
27
+ // ============================================================================
28
+ // Squad
29
+ // ============================================================================
30
+
31
+ export type SquadStatus = "planning" | "running" | "paused" | "done" | "failed";
32
+
33
+ export interface SquadConfig {
34
+ maxConcurrency: number;
35
+ autoUnblock: boolean;
36
+ reviewOnComplete: boolean;
37
+ }
38
+
39
+ export const DEFAULT_SQUAD_CONFIG: SquadConfig = {
40
+ maxConcurrency: 2,
41
+ autoUnblock: true,
42
+ reviewOnComplete: false,
43
+ };
44
+
45
+ export interface Squad {
46
+ id: string;
47
+ goal: string;
48
+ status: SquadStatus;
49
+ created: string;
50
+ cwd: string;
51
+ /** Agent name → overrides. Keys must exist in .pi/squad/agents/ */
52
+ agents: Record<string, SquadAgentEntry>;
53
+ config: SquadConfig;
54
+ }
55
+
56
+ // ============================================================================
57
+ // Tasks
58
+ // ============================================================================
59
+
60
+ export type TaskStatus = "pending" | "blocked" | "in_progress" | "done" | "failed" | "suspended";
61
+
62
+ export interface TaskUsage {
63
+ inputTokens: number;
64
+ outputTokens: number;
65
+ cost: number;
66
+ turns: number;
67
+ }
68
+
69
+ export interface Task {
70
+ id: string;
71
+ title: string;
72
+ description: string;
73
+ agent: string;
74
+ status: TaskStatus;
75
+ depends: string[];
76
+ created: string;
77
+ started: string | null;
78
+ completed: string | null;
79
+ output: string | null;
80
+ error: string | null;
81
+ usage: TaskUsage;
82
+ }
83
+
84
+ // ============================================================================
85
+ // Messages (JSONL entries)
86
+ // ============================================================================
87
+
88
+ export type MessageType = "status" | "text" | "tool" | "mention" | "reply" | "message" | "done" | "error";
89
+
90
+ export interface TaskMessage {
91
+ ts: string;
92
+ from: string;
93
+ type: MessageType;
94
+ text: string;
95
+ to?: string;
96
+ name?: string;
97
+ args?: Record<string, unknown>;
98
+ }
99
+
100
+ // ============================================================================
101
+ // Context (extension-maintained live state)
102
+ // ============================================================================
103
+
104
+ export interface ContextAgentState {
105
+ role: string;
106
+ status: "working" | "idle";
107
+ task: string | null;
108
+ }
109
+
110
+ export interface ContextTaskState {
111
+ status: TaskStatus;
112
+ agent: string;
113
+ title: string;
114
+ output?: string;
115
+ blockedBy?: string[];
116
+ subtasks?: Record<string, ContextTaskState>;
117
+ }
118
+
119
+ export interface ContextActivity {
120
+ ts: string;
121
+ agent: string;
122
+ action: string;
123
+ }
124
+
125
+ export interface SquadContext {
126
+ goal: string;
127
+ status: SquadStatus;
128
+ elapsed: string;
129
+ costs: {
130
+ total: number;
131
+ byAgent: Record<string, number>;
132
+ };
133
+ agents: Record<string, ContextAgentState>;
134
+ tasks: Record<string, ContextTaskState>;
135
+ recentActivity: ContextActivity[];
136
+ modifiedFiles: Record<string, string[]>;
137
+ }
138
+
139
+ // ============================================================================
140
+ // Knowledge (JSONL entries)
141
+ // ============================================================================
142
+
143
+ export type KnowledgeType = "decision" | "convention" | "finding";
144
+
145
+ export interface KnowledgeEntry {
146
+ ts: string;
147
+ from: string;
148
+ squad?: string;
149
+ type: KnowledgeType;
150
+ text: string;
151
+ }
152
+
153
+ // ============================================================================
154
+ // Scheduler
155
+ // ============================================================================
156
+
157
+ export interface AgentActivity {
158
+ taskId: string;
159
+ agentName: string;
160
+ lastOutputTs: number;
161
+ startedAt: number;
162
+ turnCount: number;
163
+ /** Ring buffer of recent tool call signatures for loop detection */
164
+ recentToolCalls: string[];
165
+ /** Set of file paths this agent has modified */
166
+ modifiedFiles: Set<string>;
167
+ }
168
+
169
+ export type HealthStatus = "healthy" | "idle_warning" | "stuck" | "looping" | "exceeded_ceiling";
170
+
171
+ // ============================================================================
172
+ // Supervisor
173
+ // ============================================================================
174
+
175
+ export type SupervisorVerdict = "approve" | "revise" | "escalate";
176
+
177
+ export interface SupervisorResult {
178
+ verdict: SupervisorVerdict;
179
+ reason: string;
180
+ feedback?: string;
181
+ }
182
+
183
+ // ============================================================================
184
+ // Planner
185
+ // ============================================================================
186
+
187
+ export interface PlannerOutput {
188
+ agents: Record<string, { model?: string }>;
189
+ tasks: Array<{
190
+ id: string;
191
+ title: string;
192
+ description: string;
193
+ agent: string;
194
+ depends: string[];
195
+ }>;
196
+ }
197
+
198
+ // ============================================================================
199
+ // Panel
200
+ // ============================================================================
201
+
202
+ export type PanelView = "tasks" | "messages" | "agents";
203
+
204
+ export interface PanelState {
205
+ view: PanelView;
206
+ selectedTaskIndex: number;
207
+ selectedTaskId: string | null;
208
+ scrollOffset: number;
209
+ agentSelectedIndex: number;
210
+ }