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.
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/package.json +30 -0
- package/src/agent-pool.ts +445 -0
- package/src/agents/_defaults/architect.json +9 -0
- package/src/agents/_defaults/backend.json +9 -0
- package/src/agents/_defaults/debugger.json +9 -0
- package/src/agents/_defaults/devops.json +9 -0
- package/src/agents/_defaults/docs.json +9 -0
- package/src/agents/_defaults/frontend.json +9 -0
- package/src/agents/_defaults/fullstack.json +9 -0
- package/src/agents/_defaults/planner.json +9 -0
- package/src/agents/_defaults/qa.json +9 -0
- package/src/agents/_defaults/researcher.json +9 -0
- package/src/agents/_defaults/security.json +9 -0
- package/src/index.ts +1121 -0
- package/src/monitor.ts +204 -0
- package/src/panel/message-view.ts +232 -0
- package/src/panel/squad-panel.ts +383 -0
- package/src/panel/task-list.ts +264 -0
- package/src/planner.ts +275 -0
- package/src/protocol.ts +265 -0
- package/src/router.ts +207 -0
- package/src/scheduler.ts +732 -0
- package/src/skills/collaboration/SKILL.md +39 -0
- package/src/skills/squad-protocol/SKILL.md +65 -0
- package/src/skills/verification/SKILL.md +64 -0
- package/src/store.ts +458 -0
- package/src/supervisor.ts +143 -0
- package/src/types.ts +210 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: collaboration
|
|
3
|
+
description: Multi-agent collaboration patterns — how to build on others' work, ask questions, share knowledge, and work as a team.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Collaboration
|
|
7
|
+
|
|
8
|
+
## Build on others' work
|
|
9
|
+
Read what your dependencies produced. Reference their output explicitly.
|
|
10
|
+
"Based on the schema from the db-schema task, the users table has id, email, password_hash, created_at.
|
|
11
|
+
I'll add the JWT validation column and refresh_token table..."
|
|
12
|
+
|
|
13
|
+
## Ask real questions
|
|
14
|
+
When you @mention someone, ask something specific that needs an answer.
|
|
15
|
+
Don't send FYI messages that waste their context window.
|
|
16
|
+
|
|
17
|
+
Good: "@backend what's the token expiry? I need it for the refresh logic"
|
|
18
|
+
Good: "@frontend are you using React Router or Next.js routing? Affects how I set up the auth middleware"
|
|
19
|
+
Bad: "@backend FYI I'm working on the frontend" (no question, wastes their time)
|
|
20
|
+
|
|
21
|
+
## Share your reasoning
|
|
22
|
+
Don't just post conclusions. Explain why you made a choice, so others can course-correct early.
|
|
23
|
+
"I chose RS256 over HS256 because the frontend needs to verify tokens without the signing secret"
|
|
24
|
+
|
|
25
|
+
## Admit uncertainty
|
|
26
|
+
If you're not sure about something, say so and ask.
|
|
27
|
+
"I'm not sure if this migration is backwards-compatible — @backend can you verify?"
|
|
28
|
+
|
|
29
|
+
Better to ask than to silently introduce a breaking change.
|
|
30
|
+
|
|
31
|
+
## Respond when asked
|
|
32
|
+
If another agent @mentions you, always respond — even briefly.
|
|
33
|
+
The requesting agent may be blocked waiting for your answer.
|
|
34
|
+
"Got it, the payload is {sub, email, role}. Using 1h expiry."
|
|
35
|
+
|
|
36
|
+
## Share discoveries
|
|
37
|
+
If you learn something about the project that other agents should know, state it clearly.
|
|
38
|
+
"The project uses Drizzle ORM with PostgreSQL, not Prisma as I initially assumed."
|
|
39
|
+
The squad system captures these as shared knowledge.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: squad-protocol
|
|
3
|
+
description: Core communication protocol for multi-agent squad collaboration. Defines how agents talk to each other, signal status, and coordinate work.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Squad Protocol
|
|
7
|
+
|
|
8
|
+
You are part of a multi-agent squad. Other agents are working on related tasks in parallel.
|
|
9
|
+
|
|
10
|
+
## Communication
|
|
11
|
+
|
|
12
|
+
### Talking to other agents
|
|
13
|
+
Write @agentname followed by your message in your regular output.
|
|
14
|
+
The squad system parses @mentions and routes messages to the target agent in real-time.
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
- "@frontend what token format do you need for the login endpoint?"
|
|
18
|
+
- "@backend the schema needs a `role` column on the users table"
|
|
19
|
+
- "@qa the API is ready at /api/auth — here are the test endpoints..."
|
|
20
|
+
|
|
21
|
+
### Receiving messages
|
|
22
|
+
Messages from other agents and the human arrive as interruptions in your conversation.
|
|
23
|
+
They are prefixed with `[squad]`. Read them carefully, incorporate the information,
|
|
24
|
+
and continue your work. Don't ignore incoming messages.
|
|
25
|
+
|
|
26
|
+
### Completion
|
|
27
|
+
When you finish your task, clearly state your output in your last message.
|
|
28
|
+
Be specific about what you built, what files you changed, and how to verify it works.
|
|
29
|
+
This output gets passed to dependent tasks as context — vague summaries waste their time.
|
|
30
|
+
|
|
31
|
+
Good: "Created JWT middleware in src/middleware/auth.ts. Validates RS256 tokens,
|
|
32
|
+
extracts user from payload, attaches to req.user. Test: curl -H 'Authorization: Bearer ...' localhost:3000/api/me"
|
|
33
|
+
|
|
34
|
+
Bad: "Done with the auth middleware."
|
|
35
|
+
|
|
36
|
+
### Blocking
|
|
37
|
+
If you cannot proceed, clearly explain:
|
|
38
|
+
1. What you need
|
|
39
|
+
2. Who might have it (use @mention)
|
|
40
|
+
3. Why you can't continue without it
|
|
41
|
+
|
|
42
|
+
The squad system will detect this and route help to you.
|
|
43
|
+
|
|
44
|
+
## Rules
|
|
45
|
+
|
|
46
|
+
### Stay in scope
|
|
47
|
+
Your task description defines your scope. If you discover work that's outside
|
|
48
|
+
your task, mention it but don't do it. The squad system will create a new task if needed.
|
|
49
|
+
|
|
50
|
+
### Don't duplicate work
|
|
51
|
+
Your system prompt includes outputs from completed dependency tasks.
|
|
52
|
+
Read them before starting. Don't redo work that's already done.
|
|
53
|
+
|
|
54
|
+
### Coordinate on shared files
|
|
55
|
+
Your system prompt lists files modified by other agents.
|
|
56
|
+
If you need to edit a file another agent owns, message them first with @mention.
|
|
57
|
+
Don't silently overwrite their changes.
|
|
58
|
+
|
|
59
|
+
### Ask for help
|
|
60
|
+
If you're stuck for more than a few minutes, say so clearly.
|
|
61
|
+
The squad system monitors your activity and will intervene, but being explicit is faster.
|
|
62
|
+
|
|
63
|
+
### Read before writing
|
|
64
|
+
Before modifying any file, read it first. Another agent may have changed it
|
|
65
|
+
since the last time you saw it.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verification
|
|
3
|
+
description: Verify work before claiming completion — evidence-based discipline for multi-agent handoffs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Verification
|
|
7
|
+
|
|
8
|
+
Before claiming your task is done, verify that your work actually works.
|
|
9
|
+
Your output gets passed to dependent tasks. If it's wrong, the entire chain fails.
|
|
10
|
+
|
|
11
|
+
## The Protocol
|
|
12
|
+
|
|
13
|
+
1. **IDENTIFY**: What command or test proves the work is correct?
|
|
14
|
+
2. **RUN**: Execute it fresh — don't rely on earlier results
|
|
15
|
+
3. **READ**: Check the full output, not just the exit code
|
|
16
|
+
4. **VERIFY**: Does the output confirm your claim?
|
|
17
|
+
- If NO: fix it, then re-verify
|
|
18
|
+
- If YES: include the evidence in your output
|
|
19
|
+
|
|
20
|
+
## Red Flags — Stop If You Catch Yourself
|
|
21
|
+
|
|
22
|
+
- Using "should work", "probably correct", "looks right"
|
|
23
|
+
- Claiming done without running the code
|
|
24
|
+
- Assuming tests pass without executing them
|
|
25
|
+
- Expressing satisfaction before verification
|
|
26
|
+
- Saying "just this one time" about skipping verification
|
|
27
|
+
|
|
28
|
+
## Output Checklist
|
|
29
|
+
|
|
30
|
+
When completing a task, your final message must include:
|
|
31
|
+
|
|
32
|
+
1. **What you built/changed** — specific file paths
|
|
33
|
+
2. **How to verify** — exact command or steps
|
|
34
|
+
3. **Evidence** — actual output from running the verification
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
```
|
|
38
|
+
Created JWT middleware:
|
|
39
|
+
- src/lib/jwt.ts — token generation (RS256, 1h expiry)
|
|
40
|
+
- src/middleware/auth.ts — validation middleware
|
|
41
|
+
- test/auth.test.ts — 8 tests
|
|
42
|
+
|
|
43
|
+
Verification:
|
|
44
|
+
$ npm test -- test/auth.test.ts
|
|
45
|
+
✓ generates valid JWT (12ms)
|
|
46
|
+
✓ validates token signature (3ms)
|
|
47
|
+
✓ rejects expired tokens (5ms)
|
|
48
|
+
...
|
|
49
|
+
8 tests passed, 0 failed
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## For Implementation Tasks
|
|
53
|
+
|
|
54
|
+
- Run the code, don't just write it
|
|
55
|
+
- Check for TypeScript/lint errors
|
|
56
|
+
- Run related tests if they exist
|
|
57
|
+
- Verify the feature works end-to-end, not just that it compiles
|
|
58
|
+
|
|
59
|
+
## For Research/Analysis Tasks
|
|
60
|
+
|
|
61
|
+
- Cite specific files and line numbers
|
|
62
|
+
- Include relevant code snippets as evidence
|
|
63
|
+
- State confidence level on uncertain findings
|
|
64
|
+
- List what you checked AND what you didn't check
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* store.ts — JSON file I/O for squad state.
|
|
3
|
+
*
|
|
4
|
+
* All state lives in .pi/squad/ as JSON files.
|
|
5
|
+
* Writes are atomic (write to temp, rename).
|
|
6
|
+
* JSONL files are append-only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import type {
|
|
13
|
+
AgentDef,
|
|
14
|
+
KnowledgeEntry,
|
|
15
|
+
Squad,
|
|
16
|
+
SquadContext,
|
|
17
|
+
Task,
|
|
18
|
+
TaskMessage,
|
|
19
|
+
TaskUsage,
|
|
20
|
+
DEFAULT_SQUAD_CONFIG,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Paths
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Two-tier storage:
|
|
29
|
+
*
|
|
30
|
+
* Global: ~/.pi/squad/
|
|
31
|
+
* ├── agents/ — default agent definitions
|
|
32
|
+
* └── {squad-id}/ — all squad instances (each has cwd in squad.json)
|
|
33
|
+
*
|
|
34
|
+
* Local (project override): {project}/.pi/squad/
|
|
35
|
+
* └── agents/ — project-specific agent overrides (checked first)
|
|
36
|
+
*
|
|
37
|
+
* Squad instances are always global. Agents are merged (local overrides global).
|
|
38
|
+
* Each squad stores its project cwd in squad.json for agent execution.
|
|
39
|
+
* Listing/widget filters squads by current project cwd.
|
|
40
|
+
*/
|
|
41
|
+
const SQUAD_HOME = path.join(os.homedir(), ".pi", "squad");
|
|
42
|
+
|
|
43
|
+
export function getSquadRoot(): string {
|
|
44
|
+
return SQUAD_HOME;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Global agent directory */
|
|
48
|
+
export function getGlobalAgentsDir(): string {
|
|
49
|
+
return path.join(SQUAD_HOME, "agents");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Project-local agent directory (overrides global) */
|
|
53
|
+
export function getLocalAgentsDir(projectCwd: string): string {
|
|
54
|
+
return path.join(projectCwd, ".pi", "squad", "agents");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Effective agents directory. For writes (bootstrap), always use global.
|
|
59
|
+
* For reads, merge local over global via loadAllAgentDefs(projectCwd).
|
|
60
|
+
*/
|
|
61
|
+
export function getAgentsDir(): string {
|
|
62
|
+
return getGlobalAgentsDir();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getSquadDir(squadId: string): string {
|
|
66
|
+
return path.join(getSquadRoot(), squadId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getSquadFilePath(squadId: string): string {
|
|
70
|
+
return path.join(getSquadDir(squadId), "squad.json");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getContextFilePath(squadId: string): string {
|
|
74
|
+
return path.join(getSquadDir(squadId), "context.json");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getKnowledgeDir(squadId: string): string {
|
|
78
|
+
return path.join(getSquadDir(squadId), "knowledge");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getMemoryFilePath(): string {
|
|
82
|
+
return path.join(getSquadRoot(), "memory.jsonl");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Resolve task dir, supporting nested subtasks via parentPath */
|
|
86
|
+
export function getTaskDir(squadId: string, taskId: string, parentPath?: string): string {
|
|
87
|
+
const base = parentPath
|
|
88
|
+
? path.join(getSquadDir(squadId), parentPath, taskId)
|
|
89
|
+
: path.join(getSquadDir(squadId), taskId);
|
|
90
|
+
return base;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getTaskFilePath(squadId: string, taskId: string, parentPath?: string): string {
|
|
94
|
+
return path.join(getTaskDir(squadId, taskId, parentPath), "task.json");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getMessagesFilePath(squadId: string, taskId: string, parentPath?: string): string {
|
|
98
|
+
return path.join(getTaskDir(squadId, taskId, parentPath), "messages.jsonl");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// Atomic File Operations
|
|
103
|
+
// ============================================================================
|
|
104
|
+
|
|
105
|
+
function ensureDir(dirPath: string): void {
|
|
106
|
+
if (!fs.existsSync(dirPath)) {
|
|
107
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Write JSON atomically: write to temp file, then rename */
|
|
112
|
+
function writeJsonAtomic(filePath: string, data: unknown): void {
|
|
113
|
+
ensureDir(path.dirname(filePath));
|
|
114
|
+
const tmp = filePath + `.tmp.${process.pid}.${Date.now()}`;
|
|
115
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
116
|
+
fs.renameSync(tmp, filePath);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readJson<T>(filePath: string): T | null {
|
|
120
|
+
try {
|
|
121
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
122
|
+
return JSON.parse(content) as T;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Append a JSONL line */
|
|
129
|
+
function appendJsonl(filePath: string, entry: unknown): void {
|
|
130
|
+
ensureDir(path.dirname(filePath));
|
|
131
|
+
fs.appendFileSync(filePath, JSON.stringify(entry) + "\n", "utf-8");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Read all JSONL lines */
|
|
135
|
+
function readJsonl<T>(filePath: string): T[] {
|
|
136
|
+
try {
|
|
137
|
+
const content = fs.readFileSync(filePath, "utf-8").trim();
|
|
138
|
+
if (!content) return [];
|
|
139
|
+
return content.split("\n").map((line) => JSON.parse(line) as T);
|
|
140
|
+
} catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// Agent Definitions (global + local override)
|
|
147
|
+
// ============================================================================
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Load agent by name. Checks project-local first, then global.
|
|
151
|
+
*/
|
|
152
|
+
export function loadAgentDef(name: string, projectCwd?: string): AgentDef | null {
|
|
153
|
+
// Check local override first
|
|
154
|
+
if (projectCwd) {
|
|
155
|
+
const localFile = path.join(getLocalAgentsDir(projectCwd), `${name}.json`);
|
|
156
|
+
const local = readJson<AgentDef>(localFile);
|
|
157
|
+
if (local) return local;
|
|
158
|
+
}
|
|
159
|
+
// Fall back to global
|
|
160
|
+
return readJson<AgentDef>(path.join(getGlobalAgentsDir(), `${name}.json`));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Load all agents, merging local overrides on top of global.
|
|
165
|
+
* Local agents with the same name replace global ones.
|
|
166
|
+
*/
|
|
167
|
+
export function loadAllAgentDefs(projectCwd?: string): AgentDef[] {
|
|
168
|
+
const agents = new Map<string, AgentDef>();
|
|
169
|
+
|
|
170
|
+
// Load global first
|
|
171
|
+
const globalDir = getGlobalAgentsDir();
|
|
172
|
+
if (fs.existsSync(globalDir)) {
|
|
173
|
+
for (const f of fs.readdirSync(globalDir).filter((f) => f.endsWith(".json"))) {
|
|
174
|
+
const agent = readJson<AgentDef>(path.join(globalDir, f));
|
|
175
|
+
if (agent) agents.set(agent.name, agent);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Overlay local overrides
|
|
180
|
+
if (projectCwd) {
|
|
181
|
+
const localDir = getLocalAgentsDir(projectCwd);
|
|
182
|
+
if (fs.existsSync(localDir)) {
|
|
183
|
+
for (const f of fs.readdirSync(localDir).filter((f) => f.endsWith(".json"))) {
|
|
184
|
+
const agent = readJson<AgentDef>(path.join(localDir, f));
|
|
185
|
+
if (agent) agents.set(agent.name, agent);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return Array.from(agents.values());
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Save agent to global directory */
|
|
194
|
+
export function saveAgentDef(agent: AgentDef): void {
|
|
195
|
+
writeJsonAtomic(path.join(getGlobalAgentsDir(), `${agent.name}.json`), agent);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Save agent to project-local directory (override) */
|
|
199
|
+
export function saveLocalAgentDef(agent: AgentDef, projectCwd: string): void {
|
|
200
|
+
writeJsonAtomic(path.join(getLocalAgentsDir(projectCwd), `${agent.name}.json`), agent);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function deleteAgentDef(name: string): boolean {
|
|
204
|
+
const filePath = path.join(getGlobalAgentsDir(), `${name}.json`);
|
|
205
|
+
if (fs.existsSync(filePath)) {
|
|
206
|
+
fs.unlinkSync(filePath);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ============================================================================
|
|
213
|
+
// Squad
|
|
214
|
+
// ============================================================================
|
|
215
|
+
|
|
216
|
+
export function loadSquad(squadId: string): Squad | null {
|
|
217
|
+
return readJson<Squad>(getSquadFilePath(squadId));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function saveSquad(squad: Squad): void {
|
|
221
|
+
writeJsonAtomic(getSquadFilePath(squad.id), squad);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function listSquads(): string[] {
|
|
225
|
+
const root = getSquadRoot();
|
|
226
|
+
if (!fs.existsSync(root)) return [];
|
|
227
|
+
return fs
|
|
228
|
+
.readdirSync(root)
|
|
229
|
+
.filter((entry) => {
|
|
230
|
+
if (entry === "agents" || entry === "memory.jsonl") return false;
|
|
231
|
+
const squadFile = path.join(root, entry, "squad.json");
|
|
232
|
+
return fs.existsSync(squadFile);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function findActiveSquads(): Squad[] {
|
|
237
|
+
return listSquads()
|
|
238
|
+
.map((id) => loadSquad(id))
|
|
239
|
+
.filter((s): s is Squad => s !== null && (s.status === "running" || s.status === "paused"));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** List squads filtered by project cwd. If no cwd, returns all. */
|
|
243
|
+
export function listSquadsForProject(projectCwd?: string): Squad[] {
|
|
244
|
+
return listSquads()
|
|
245
|
+
.map((id) => loadSquad(id))
|
|
246
|
+
.filter((s): s is Squad => {
|
|
247
|
+
if (!s) return false;
|
|
248
|
+
if (!projectCwd) return true;
|
|
249
|
+
return s.cwd === projectCwd;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Find most recent squad for a project (by creation time) */
|
|
254
|
+
export function findLatestSquad(projectCwd?: string): Squad | null {
|
|
255
|
+
const squads = listSquadsForProject(projectCwd);
|
|
256
|
+
if (squads.length === 0) return null;
|
|
257
|
+
return squads.sort((a, b) => b.created.localeCompare(a.created))[0];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// Tasks
|
|
262
|
+
// ============================================================================
|
|
263
|
+
|
|
264
|
+
export function loadTask(squadId: string, taskId: string, parentPath?: string): Task | null {
|
|
265
|
+
return readJson<Task>(getTaskFilePath(squadId, taskId, parentPath));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function saveTask(squadId: string, task: Task, parentPath?: string): void {
|
|
269
|
+
writeJsonAtomic(getTaskFilePath(squadId, task.id, parentPath), task);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Load all tasks for a squad (flat list, scans top-level task folders) */
|
|
273
|
+
export function loadAllTasks(squadId: string): Task[] {
|
|
274
|
+
const squadDir = getSquadDir(squadId);
|
|
275
|
+
if (!fs.existsSync(squadDir)) return [];
|
|
276
|
+
|
|
277
|
+
const tasks: Task[] = [];
|
|
278
|
+
const seen = new Set<string>();
|
|
279
|
+
const entries = fs.readdirSync(squadDir, { withFileTypes: true });
|
|
280
|
+
|
|
281
|
+
for (const entry of entries) {
|
|
282
|
+
if (!entry.isDirectory()) continue;
|
|
283
|
+
if (entry.name === "knowledge") continue;
|
|
284
|
+
const taskFile = path.join(squadDir, entry.name, "task.json");
|
|
285
|
+
const task = readJson<Task>(taskFile);
|
|
286
|
+
if (task && !seen.has(task.id)) {
|
|
287
|
+
seen.add(task.id);
|
|
288
|
+
tasks.push(task);
|
|
289
|
+
// Scan for subtasks
|
|
290
|
+
collectSubtasks(squadDir, entry.name, tasks, seen);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return tasks;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function collectSubtasks(squadDir: string, parentPath: string, tasks: Task[], seen: Set<string>): void {
|
|
298
|
+
const parentDir = path.join(squadDir, parentPath);
|
|
299
|
+
let entries;
|
|
300
|
+
try {
|
|
301
|
+
entries = fs.readdirSync(parentDir, { withFileTypes: true });
|
|
302
|
+
} catch {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (const entry of entries) {
|
|
307
|
+
if (!entry.isDirectory()) continue;
|
|
308
|
+
const taskFile = path.join(parentDir, entry.name, "task.json");
|
|
309
|
+
const task = readJson<Task>(taskFile);
|
|
310
|
+
if (task && !seen.has(task.id)) {
|
|
311
|
+
seen.add(task.id);
|
|
312
|
+
tasks.push(task);
|
|
313
|
+
collectSubtasks(squadDir, path.join(parentPath, entry.name), tasks, seen);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function createTask(squadId: string, task: Task, parentPath?: string): void {
|
|
319
|
+
ensureDir(getTaskDir(squadId, task.id, parentPath));
|
|
320
|
+
saveTask(squadId, task, parentPath);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function updateTaskStatus(
|
|
324
|
+
squadId: string,
|
|
325
|
+
taskId: string,
|
|
326
|
+
status: Task["status"],
|
|
327
|
+
extra?: Partial<Pick<Task, "output" | "error" | "started" | "completed">>,
|
|
328
|
+
): void {
|
|
329
|
+
const task = loadTask(squadId, taskId);
|
|
330
|
+
if (!task) return;
|
|
331
|
+
task.status = status;
|
|
332
|
+
if (extra) {
|
|
333
|
+
if (extra.output !== undefined) task.output = extra.output;
|
|
334
|
+
if (extra.error !== undefined) task.error = extra.error;
|
|
335
|
+
if (extra.started !== undefined) task.started = extra.started;
|
|
336
|
+
if (extra.completed !== undefined) task.completed = extra.completed;
|
|
337
|
+
}
|
|
338
|
+
saveTask(squadId, task);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function updateTaskUsage(squadId: string, taskId: string, usage: Partial<TaskUsage>): void {
|
|
342
|
+
const task = loadTask(squadId, taskId);
|
|
343
|
+
if (!task) return;
|
|
344
|
+
if (usage.inputTokens !== undefined) task.usage.inputTokens += usage.inputTokens;
|
|
345
|
+
if (usage.outputTokens !== undefined) task.usage.outputTokens += usage.outputTokens;
|
|
346
|
+
if (usage.cost !== undefined) task.usage.cost += usage.cost;
|
|
347
|
+
if (usage.turns !== undefined) task.usage.turns += usage.turns;
|
|
348
|
+
saveTask(squadId, task);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ============================================================================
|
|
352
|
+
// Messages
|
|
353
|
+
// ============================================================================
|
|
354
|
+
|
|
355
|
+
export function appendMessage(squadId: string, taskId: string, message: TaskMessage, parentPath?: string): void {
|
|
356
|
+
appendJsonl(getMessagesFilePath(squadId, taskId, parentPath), message);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function loadMessages(squadId: string, taskId: string, parentPath?: string): TaskMessage[] {
|
|
360
|
+
return readJsonl<TaskMessage>(getMessagesFilePath(squadId, taskId, parentPath));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ============================================================================
|
|
364
|
+
// Context
|
|
365
|
+
// ============================================================================
|
|
366
|
+
|
|
367
|
+
export function loadContext(squadId: string): SquadContext | null {
|
|
368
|
+
return readJson<SquadContext>(getContextFilePath(squadId));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function saveContext(squadId: string, context: SquadContext): void {
|
|
372
|
+
writeJsonAtomic(getContextFilePath(squadId), context);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ============================================================================
|
|
376
|
+
// Knowledge
|
|
377
|
+
// ============================================================================
|
|
378
|
+
|
|
379
|
+
export function appendKnowledge(squadId: string, type: KnowledgeEntry["type"], entry: KnowledgeEntry): void {
|
|
380
|
+
const file = path.join(getKnowledgeDir(squadId), `${type}s.jsonl`);
|
|
381
|
+
appendJsonl(file, entry);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function loadKnowledge(squadId: string, type: KnowledgeEntry["type"]): KnowledgeEntry[] {
|
|
385
|
+
const file = path.join(getKnowledgeDir(squadId), `${type}s.jsonl`);
|
|
386
|
+
return readJsonl<KnowledgeEntry>(file);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function loadAllKnowledge(squadId: string): KnowledgeEntry[] {
|
|
390
|
+
return [
|
|
391
|
+
...loadKnowledge(squadId, "decision"),
|
|
392
|
+
...loadKnowledge(squadId, "convention"),
|
|
393
|
+
...loadKnowledge(squadId, "finding"),
|
|
394
|
+
].sort((a, b) => a.ts.localeCompare(b.ts));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ============================================================================
|
|
398
|
+
// Memory (cross-squad)
|
|
399
|
+
// ============================================================================
|
|
400
|
+
|
|
401
|
+
export function appendMemory(entry: KnowledgeEntry): void {
|
|
402
|
+
appendJsonl(getMemoryFilePath(), entry);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function loadMemory(): KnowledgeEntry[] {
|
|
406
|
+
return readJsonl<KnowledgeEntry>(getMemoryFilePath());
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ============================================================================
|
|
410
|
+
// Bootstrap — first-run agent initialization
|
|
411
|
+
// ============================================================================
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Copy default agents to .pi/squad/agents/ if they don't exist yet.
|
|
415
|
+
* Never overwrites user's existing files.
|
|
416
|
+
*/
|
|
417
|
+
export function bootstrapAgents(defaultsDir: string): { copied: string[]; skipped: string[] } {
|
|
418
|
+
const targetDir = getAgentsDir();
|
|
419
|
+
ensureDir(targetDir);
|
|
420
|
+
|
|
421
|
+
const copied: string[] = [];
|
|
422
|
+
const skipped: string[] = [];
|
|
423
|
+
|
|
424
|
+
if (!fs.existsSync(defaultsDir)) return { copied, skipped };
|
|
425
|
+
|
|
426
|
+
for (const file of fs.readdirSync(defaultsDir)) {
|
|
427
|
+
if (!file.endsWith(".json")) continue;
|
|
428
|
+
const target = path.join(targetDir, file);
|
|
429
|
+
if (fs.existsSync(target)) {
|
|
430
|
+
skipped.push(file);
|
|
431
|
+
} else {
|
|
432
|
+
fs.copyFileSync(path.join(defaultsDir, file), target);
|
|
433
|
+
copied.push(file);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return { copied, skipped };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ============================================================================
|
|
441
|
+
// Utility
|
|
442
|
+
// ============================================================================
|
|
443
|
+
|
|
444
|
+
export function now(): string {
|
|
445
|
+
return new Date().toISOString();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function makeTaskId(title: string): string {
|
|
449
|
+
return title
|
|
450
|
+
.toLowerCase()
|
|
451
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
452
|
+
.replace(/^-|-$/g, "")
|
|
453
|
+
.slice(0, 40);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function squadExists(squadId: string): boolean {
|
|
457
|
+
return fs.existsSync(getSquadFilePath(squadId));
|
|
458
|
+
}
|