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,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
+ }