pi-messenger 0.7.3
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/ARCHITECTURE.md +244 -0
- package/CHANGELOG.md +418 -0
- package/README.md +394 -0
- package/banner.png +0 -0
- package/config-overlay.ts +172 -0
- package/config.ts +178 -0
- package/crew/agents/crew-docs-scout.md +55 -0
- package/crew/agents/crew-gap-analyst.md +105 -0
- package/crew/agents/crew-github-scout.md +111 -0
- package/crew/agents/crew-interview-generator.md +79 -0
- package/crew/agents/crew-plan-sync.md +64 -0
- package/crew/agents/crew-practice-scout.md +62 -0
- package/crew/agents/crew-repo-scout.md +65 -0
- package/crew/agents/crew-reviewer.md +58 -0
- package/crew/agents/crew-web-scout.md +85 -0
- package/crew/agents/crew-worker.md +95 -0
- package/crew/agents.ts +200 -0
- package/crew/handlers/interview.ts +211 -0
- package/crew/handlers/plan.ts +358 -0
- package/crew/handlers/review.ts +341 -0
- package/crew/handlers/status.ts +257 -0
- package/crew/handlers/sync.ts +232 -0
- package/crew/handlers/task.ts +511 -0
- package/crew/handlers/work.ts +289 -0
- package/crew/id-allocator.ts +44 -0
- package/crew/index.ts +229 -0
- package/crew/state.ts +116 -0
- package/crew/store.ts +480 -0
- package/crew/types.ts +164 -0
- package/crew/utils/artifacts.ts +65 -0
- package/crew/utils/config.ts +104 -0
- package/crew/utils/discover.ts +170 -0
- package/crew/utils/install.ts +373 -0
- package/crew/utils/progress.ts +107 -0
- package/crew/utils/result.ts +16 -0
- package/crew/utils/truncate.ts +79 -0
- package/crew-overlay.ts +259 -0
- package/handlers.ts +799 -0
- package/index.ts +591 -0
- package/lib.ts +232 -0
- package/overlay.ts +687 -0
- package/package.json +20 -0
- package/skills/pi-messenger-crew/SKILL.md +140 -0
- package/store.ts +1068 -0
- package/tsconfig.json +19 -0
package/crew/store.ts
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew - Store Operations
|
|
3
|
+
*
|
|
4
|
+
* Simplified PRD-based storage: plan.json + tasks/*.json
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
10
|
+
import type { Plan, Task, TaskEvidence } from "./types.js";
|
|
11
|
+
import { allocateTaskId } from "./id-allocator.js";
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Directory Helpers
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
function ensureDir(dir: string): void {
|
|
18
|
+
if (!fs.existsSync(dir)) {
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getCrewDir(cwd: string): string {
|
|
24
|
+
return path.join(cwd, ".pi", "messenger", "crew");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getTasksDir(cwd: string): string {
|
|
28
|
+
return path.join(getCrewDir(cwd), "tasks");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getBlocksDir(cwd: string): string {
|
|
32
|
+
return path.join(getCrewDir(cwd), "blocks");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// JSON Helpers
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
function readJson<T>(filePath: string): T | null {
|
|
40
|
+
if (!fs.existsSync(filePath)) return null;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeJson(filePath: string, data: unknown): void {
|
|
49
|
+
ensureDir(path.dirname(filePath));
|
|
50
|
+
const temp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
51
|
+
fs.writeFileSync(temp, JSON.stringify(data, null, 2));
|
|
52
|
+
fs.renameSync(temp, filePath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readText(filePath: string): string | null {
|
|
56
|
+
if (!fs.existsSync(filePath)) return null;
|
|
57
|
+
try {
|
|
58
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeText(filePath: string, content: string): void {
|
|
65
|
+
ensureDir(path.dirname(filePath));
|
|
66
|
+
const temp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
67
|
+
fs.writeFileSync(temp, content);
|
|
68
|
+
fs.renameSync(temp, filePath);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// Plan Operations
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
export function getPlan(cwd: string): Plan | null {
|
|
76
|
+
return readJson<Plan>(path.join(getCrewDir(cwd), "plan.json"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createPlan(cwd: string, prdPath: string): Plan {
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
|
|
82
|
+
const plan: Plan = {
|
|
83
|
+
prd: prdPath,
|
|
84
|
+
created_at: now,
|
|
85
|
+
updated_at: now,
|
|
86
|
+
task_count: 0,
|
|
87
|
+
completed_count: 0,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
writeJson(path.join(getCrewDir(cwd), "plan.json"), plan);
|
|
91
|
+
return plan;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function updatePlan(cwd: string, updates: Partial<Plan>): Plan | null {
|
|
95
|
+
const plan = getPlan(cwd);
|
|
96
|
+
if (!plan) return null;
|
|
97
|
+
|
|
98
|
+
const updated: Plan = {
|
|
99
|
+
...plan,
|
|
100
|
+
...updates,
|
|
101
|
+
updated_at: new Date().toISOString(),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
writeJson(path.join(getCrewDir(cwd), "plan.json"), updated);
|
|
105
|
+
return updated;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function deletePlan(cwd: string): boolean {
|
|
109
|
+
const planPath = path.join(getCrewDir(cwd), "plan.json");
|
|
110
|
+
const planMdPath = path.join(getCrewDir(cwd), "plan.md");
|
|
111
|
+
const tasksDir = getTasksDir(cwd);
|
|
112
|
+
|
|
113
|
+
let deleted = false;
|
|
114
|
+
|
|
115
|
+
// Delete plan.json
|
|
116
|
+
if (fs.existsSync(planPath)) {
|
|
117
|
+
fs.unlinkSync(planPath);
|
|
118
|
+
deleted = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Delete plan.md
|
|
122
|
+
if (fs.existsSync(planMdPath)) {
|
|
123
|
+
fs.unlinkSync(planMdPath);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Delete all task files
|
|
127
|
+
if (fs.existsSync(tasksDir)) {
|
|
128
|
+
for (const file of fs.readdirSync(tasksDir)) {
|
|
129
|
+
fs.unlinkSync(path.join(tasksDir, file));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return deleted;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// =============================================================================
|
|
137
|
+
// Plan Spec Operations
|
|
138
|
+
// =============================================================================
|
|
139
|
+
|
|
140
|
+
export function getPlanSpec(cwd: string): string | null {
|
|
141
|
+
return readText(path.join(getCrewDir(cwd), "plan.md"));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function setPlanSpec(cwd: string, content: string): void {
|
|
145
|
+
writeText(path.join(getCrewDir(cwd), "plan.md"), content);
|
|
146
|
+
updatePlan(cwd, {}); // Touch updated_at
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// =============================================================================
|
|
150
|
+
// Task Operations
|
|
151
|
+
// =============================================================================
|
|
152
|
+
|
|
153
|
+
export function createTask(
|
|
154
|
+
cwd: string,
|
|
155
|
+
title: string,
|
|
156
|
+
description?: string,
|
|
157
|
+
dependsOn?: string[]
|
|
158
|
+
): Task {
|
|
159
|
+
const id = allocateTaskId(cwd);
|
|
160
|
+
const now = new Date().toISOString();
|
|
161
|
+
|
|
162
|
+
const task: Task = {
|
|
163
|
+
id,
|
|
164
|
+
title,
|
|
165
|
+
status: "todo",
|
|
166
|
+
depends_on: dependsOn ?? [],
|
|
167
|
+
created_at: now,
|
|
168
|
+
updated_at: now,
|
|
169
|
+
attempt_count: 0,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
writeJson(path.join(getTasksDir(cwd), `${id}.json`), task);
|
|
173
|
+
|
|
174
|
+
// Create task spec file
|
|
175
|
+
const specContent = description
|
|
176
|
+
? `# ${title}\n\n${description}\n`
|
|
177
|
+
: `# ${title}\n\n*Spec pending*\n`;
|
|
178
|
+
writeText(path.join(getTasksDir(cwd), `${id}.md`), specContent);
|
|
179
|
+
|
|
180
|
+
// Update plan task count
|
|
181
|
+
const plan = getPlan(cwd);
|
|
182
|
+
if (plan) {
|
|
183
|
+
updatePlan(cwd, { task_count: plan.task_count + 1 });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return task;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function getTask(cwd: string, taskId: string): Task | null {
|
|
190
|
+
return readJson<Task>(path.join(getTasksDir(cwd), `${taskId}.json`));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function updateTask(cwd: string, taskId: string, updates: Partial<Task>): Task | null {
|
|
194
|
+
const task = getTask(cwd, taskId);
|
|
195
|
+
if (!task) return null;
|
|
196
|
+
|
|
197
|
+
const updated: Task = {
|
|
198
|
+
...task,
|
|
199
|
+
...updates,
|
|
200
|
+
updated_at: new Date().toISOString(),
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
writeJson(path.join(getTasksDir(cwd), `${taskId}.json`), updated);
|
|
204
|
+
return updated;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function getTasks(cwd: string): Task[] {
|
|
208
|
+
const dir = getTasksDir(cwd);
|
|
209
|
+
if (!fs.existsSync(dir)) return [];
|
|
210
|
+
|
|
211
|
+
const tasks: Task[] = [];
|
|
212
|
+
for (const file of fs.readdirSync(dir)) {
|
|
213
|
+
if (!file.endsWith(".json")) continue;
|
|
214
|
+
const task = readJson<Task>(path.join(dir, file));
|
|
215
|
+
if (task) tasks.push(task);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Sort by ID number (task-1, task-2, ...)
|
|
219
|
+
return tasks.sort((a, b) => {
|
|
220
|
+
const aNum = parseInt(a.id.replace("task-", ""));
|
|
221
|
+
const bNum = parseInt(b.id.replace("task-", ""));
|
|
222
|
+
return aNum - bNum;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function getTaskSpec(cwd: string, taskId: string): string | null {
|
|
227
|
+
return readText(path.join(getTasksDir(cwd), `${taskId}.md`));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function setTaskSpec(cwd: string, taskId: string, content: string): void {
|
|
231
|
+
writeText(path.join(getTasksDir(cwd), `${taskId}.md`), content);
|
|
232
|
+
updateTask(cwd, taskId, {}); // Touch updated_at
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// =============================================================================
|
|
236
|
+
// Task Lifecycle Operations
|
|
237
|
+
// =============================================================================
|
|
238
|
+
|
|
239
|
+
export function startTask(cwd: string, taskId: string, agentName: string): Task | null {
|
|
240
|
+
const task = getTask(cwd, taskId);
|
|
241
|
+
if (!task || task.status !== "todo") return null;
|
|
242
|
+
|
|
243
|
+
// Capture current git commit
|
|
244
|
+
let baseCommit: string | undefined;
|
|
245
|
+
try {
|
|
246
|
+
baseCommit = execSync("git rev-parse HEAD", { cwd, encoding: "utf-8" }).trim();
|
|
247
|
+
} catch {
|
|
248
|
+
// Not a git repo or git not available
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return updateTask(cwd, taskId, {
|
|
252
|
+
status: "in_progress",
|
|
253
|
+
started_at: new Date().toISOString(),
|
|
254
|
+
base_commit: baseCommit,
|
|
255
|
+
assigned_to: agentName,
|
|
256
|
+
attempt_count: task.attempt_count + 1,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function completeTask(
|
|
261
|
+
cwd: string,
|
|
262
|
+
taskId: string,
|
|
263
|
+
summary: string,
|
|
264
|
+
evidence?: TaskEvidence
|
|
265
|
+
): Task | null {
|
|
266
|
+
const task = getTask(cwd, taskId);
|
|
267
|
+
if (!task || task.status !== "in_progress") return null;
|
|
268
|
+
|
|
269
|
+
const updated = updateTask(cwd, taskId, {
|
|
270
|
+
status: "done",
|
|
271
|
+
completed_at: new Date().toISOString(),
|
|
272
|
+
summary,
|
|
273
|
+
evidence,
|
|
274
|
+
assigned_to: undefined,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Update plan completed count
|
|
278
|
+
if (updated) {
|
|
279
|
+
const plan = getPlan(cwd);
|
|
280
|
+
if (plan) {
|
|
281
|
+
updatePlan(cwd, { completed_count: plan.completed_count + 1 });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return updated;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function blockTask(cwd: string, taskId: string, reason: string): Task | null {
|
|
289
|
+
const task = getTask(cwd, taskId);
|
|
290
|
+
if (!task) return null;
|
|
291
|
+
|
|
292
|
+
// Write block context to blocks directory
|
|
293
|
+
const blockPath = path.join(getBlocksDir(cwd), `${taskId}.md`);
|
|
294
|
+
writeText(blockPath, `# Blocked: ${task.title}\n\n**Reason:** ${reason}\n\n**Blocked at:** ${new Date().toISOString()}\n`);
|
|
295
|
+
|
|
296
|
+
return updateTask(cwd, taskId, {
|
|
297
|
+
status: "blocked",
|
|
298
|
+
blocked_reason: reason,
|
|
299
|
+
assigned_to: undefined,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function unblockTask(cwd: string, taskId: string): Task | null {
|
|
304
|
+
const task = getTask(cwd, taskId);
|
|
305
|
+
if (!task || task.status !== "blocked") return null;
|
|
306
|
+
|
|
307
|
+
// Remove block file if exists
|
|
308
|
+
const blockPath = path.join(getBlocksDir(cwd), `${taskId}.md`);
|
|
309
|
+
try {
|
|
310
|
+
fs.unlinkSync(blockPath);
|
|
311
|
+
} catch {
|
|
312
|
+
// Ignore if doesn't exist
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return updateTask(cwd, taskId, {
|
|
316
|
+
status: "todo",
|
|
317
|
+
blocked_reason: undefined,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function resetTask(cwd: string, taskId: string, cascade: boolean = false): Task[] {
|
|
322
|
+
const task = getTask(cwd, taskId);
|
|
323
|
+
if (!task) return [];
|
|
324
|
+
|
|
325
|
+
const resetTasks: Task[] = [];
|
|
326
|
+
const wasDone = task.status === "done";
|
|
327
|
+
|
|
328
|
+
// Reset this task
|
|
329
|
+
const updated = updateTask(cwd, taskId, {
|
|
330
|
+
status: "todo",
|
|
331
|
+
started_at: undefined,
|
|
332
|
+
completed_at: undefined,
|
|
333
|
+
base_commit: undefined,
|
|
334
|
+
assigned_to: undefined,
|
|
335
|
+
summary: undefined,
|
|
336
|
+
evidence: undefined,
|
|
337
|
+
blocked_reason: undefined,
|
|
338
|
+
// Keep attempt_count for tracking
|
|
339
|
+
});
|
|
340
|
+
if (updated) resetTasks.push(updated);
|
|
341
|
+
|
|
342
|
+
// If cascade, reset all tasks that depend on this one
|
|
343
|
+
if (cascade) {
|
|
344
|
+
const allTasks = getTasks(cwd);
|
|
345
|
+
for (const t of allTasks) {
|
|
346
|
+
if (t.depends_on.includes(taskId) && t.status !== "todo") {
|
|
347
|
+
const cascaded = resetTask(cwd, t.id, true);
|
|
348
|
+
resetTasks.push(...cascaded);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Update plan completed count if needed
|
|
354
|
+
if (wasDone && resetTasks.length > 0) {
|
|
355
|
+
const plan = getPlan(cwd);
|
|
356
|
+
if (plan) {
|
|
357
|
+
const doneTasks = getTasks(cwd).filter(t => t.status === "done");
|
|
358
|
+
updatePlan(cwd, { completed_count: doneTasks.length });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return resetTasks;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// =============================================================================
|
|
366
|
+
// Ready Tasks (Dependency Resolution)
|
|
367
|
+
// =============================================================================
|
|
368
|
+
|
|
369
|
+
export function getReadyTasks(cwd: string): Task[] {
|
|
370
|
+
const tasks = getTasks(cwd);
|
|
371
|
+
const doneIds = new Set(tasks.filter(t => t.status === "done").map(t => t.id));
|
|
372
|
+
|
|
373
|
+
return tasks.filter(task => {
|
|
374
|
+
// Must be in "todo" status
|
|
375
|
+
if (task.status !== "todo") return false;
|
|
376
|
+
|
|
377
|
+
// All dependencies must be done
|
|
378
|
+
return task.depends_on.every(depId => doneIds.has(depId));
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// =============================================================================
|
|
383
|
+
// Validation
|
|
384
|
+
// =============================================================================
|
|
385
|
+
|
|
386
|
+
export interface ValidationResult {
|
|
387
|
+
valid: boolean;
|
|
388
|
+
errors: string[];
|
|
389
|
+
warnings: string[];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function validatePlan(cwd: string): ValidationResult {
|
|
393
|
+
const errors: string[] = [];
|
|
394
|
+
const warnings: string[] = [];
|
|
395
|
+
|
|
396
|
+
const plan = getPlan(cwd);
|
|
397
|
+
if (!plan) {
|
|
398
|
+
return { valid: false, errors: ["No plan found"], warnings: [] };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const tasks = getTasks(cwd);
|
|
402
|
+
|
|
403
|
+
// Check for orphan dependencies
|
|
404
|
+
const taskIds = new Set(tasks.map(t => t.id));
|
|
405
|
+
for (const task of tasks) {
|
|
406
|
+
for (const depId of task.depends_on) {
|
|
407
|
+
if (!taskIds.has(depId)) {
|
|
408
|
+
errors.push(`Task ${task.id} depends on non-existent task ${depId}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Check for circular dependencies
|
|
414
|
+
const visited = new Set<string>();
|
|
415
|
+
const recursionStack = new Set<string>();
|
|
416
|
+
|
|
417
|
+
function hasCycle(taskId: string): boolean {
|
|
418
|
+
if (recursionStack.has(taskId)) return true;
|
|
419
|
+
if (visited.has(taskId)) return false;
|
|
420
|
+
|
|
421
|
+
visited.add(taskId);
|
|
422
|
+
recursionStack.add(taskId);
|
|
423
|
+
|
|
424
|
+
const task = tasks.find(t => t.id === taskId);
|
|
425
|
+
if (task) {
|
|
426
|
+
for (const depId of task.depends_on) {
|
|
427
|
+
if (hasCycle(depId)) return true;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
recursionStack.delete(taskId);
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
for (const task of tasks) {
|
|
436
|
+
visited.clear();
|
|
437
|
+
recursionStack.clear();
|
|
438
|
+
if (hasCycle(task.id)) {
|
|
439
|
+
errors.push(`Circular dependency detected involving task ${task.id}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Check for tasks without specs
|
|
444
|
+
for (const task of tasks) {
|
|
445
|
+
const spec = getTaskSpec(cwd, task.id);
|
|
446
|
+
if (!spec || spec.includes("*Spec pending*")) {
|
|
447
|
+
warnings.push(`Task ${task.id} has no detailed spec`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Check plan spec
|
|
452
|
+
const planSpec = getPlanSpec(cwd);
|
|
453
|
+
if (!planSpec || planSpec.includes("*Spec pending*")) {
|
|
454
|
+
warnings.push("Plan has no detailed spec");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Check task counts
|
|
458
|
+
if (plan.task_count !== tasks.length) {
|
|
459
|
+
warnings.push(`Plan task_count (${plan.task_count}) doesn't match actual tasks (${tasks.length})`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const actualDone = tasks.filter(t => t.status === "done").length;
|
|
463
|
+
if (plan.completed_count !== actualDone) {
|
|
464
|
+
warnings.push(`Plan completed_count (${plan.completed_count}) doesn't match actual (${actualDone})`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
valid: errors.length === 0,
|
|
469
|
+
errors,
|
|
470
|
+
warnings,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// =============================================================================
|
|
475
|
+
// Plan Existence Check
|
|
476
|
+
// =============================================================================
|
|
477
|
+
|
|
478
|
+
export function hasPlan(cwd: string): boolean {
|
|
479
|
+
return getPlan(cwd) !== null;
|
|
480
|
+
}
|
package/crew/types.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew - Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Simplified PRD-based workflow types.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MaxOutputConfig } from "./utils/truncate.js";
|
|
8
|
+
import type { AgentProgress } from "./utils/progress.js";
|
|
9
|
+
import type { CrewAgentConfig } from "./utils/discover.js";
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Plan Types
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export interface Plan {
|
|
16
|
+
prd: string; // Path to PRD file (relative to cwd)
|
|
17
|
+
created_at: string; // ISO timestamp
|
|
18
|
+
updated_at: string; // ISO timestamp
|
|
19
|
+
task_count: number; // Total tasks
|
|
20
|
+
completed_count: number; // Completed tasks
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// Task Types
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
export type TaskStatus = "todo" | "in_progress" | "done" | "blocked";
|
|
28
|
+
|
|
29
|
+
export interface TaskEvidence {
|
|
30
|
+
commits?: string[]; // Commit SHAs
|
|
31
|
+
tests?: string[]; // Test commands/files run
|
|
32
|
+
prs?: string[]; // PR URLs
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Task {
|
|
36
|
+
id: string; // task-N format
|
|
37
|
+
title: string;
|
|
38
|
+
status: TaskStatus;
|
|
39
|
+
depends_on: string[]; // Task IDs this depends on
|
|
40
|
+
created_at: string; // ISO timestamp
|
|
41
|
+
updated_at: string; // ISO timestamp
|
|
42
|
+
started_at?: string; // When task.start was called
|
|
43
|
+
completed_at?: string; // When task.done was called
|
|
44
|
+
base_commit?: string; // Git commit SHA at task.start
|
|
45
|
+
assigned_to?: string; // Agent name currently working on it
|
|
46
|
+
summary?: string; // Completion summary from task.done
|
|
47
|
+
evidence?: TaskEvidence; // Evidence from task.done
|
|
48
|
+
blocked_reason?: string; // Reason from task.block
|
|
49
|
+
attempt_count: number; // How many times attempted (for auto-block)
|
|
50
|
+
last_review?: ReviewFeedback; // Feedback from last review (for retry)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ReviewFeedback {
|
|
54
|
+
verdict: ReviewVerdict;
|
|
55
|
+
summary: string;
|
|
56
|
+
issues: string[];
|
|
57
|
+
suggestions: string[];
|
|
58
|
+
reviewed_at: string; // ISO timestamp
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// Crew Params (Tool Parameters)
|
|
63
|
+
// =============================================================================
|
|
64
|
+
|
|
65
|
+
export interface CrewParams {
|
|
66
|
+
// Action
|
|
67
|
+
action?: string;
|
|
68
|
+
|
|
69
|
+
// Plan
|
|
70
|
+
prd?: string; // PRD file path for plan action
|
|
71
|
+
|
|
72
|
+
// Task IDs
|
|
73
|
+
id?: string; // Task ID (task-N)
|
|
74
|
+
taskId?: string; // Swarm task ID (for claim/unclaim/complete)
|
|
75
|
+
|
|
76
|
+
// Creation
|
|
77
|
+
title?: string;
|
|
78
|
+
dependsOn?: string[];
|
|
79
|
+
|
|
80
|
+
// Completion
|
|
81
|
+
summary?: string;
|
|
82
|
+
evidence?: TaskEvidence;
|
|
83
|
+
|
|
84
|
+
// Content
|
|
85
|
+
content?: string; // Task description/spec content
|
|
86
|
+
|
|
87
|
+
// Review
|
|
88
|
+
target?: string; // Task ID to review
|
|
89
|
+
type?: "plan" | "impl";
|
|
90
|
+
|
|
91
|
+
// Work options
|
|
92
|
+
autonomous?: boolean;
|
|
93
|
+
concurrency?: number;
|
|
94
|
+
|
|
95
|
+
// Task reset
|
|
96
|
+
cascade?: boolean;
|
|
97
|
+
|
|
98
|
+
// Coordination (existing)
|
|
99
|
+
spec?: string;
|
|
100
|
+
to?: string | string[];
|
|
101
|
+
message?: string;
|
|
102
|
+
replyTo?: string;
|
|
103
|
+
paths?: string[];
|
|
104
|
+
reason?: string;
|
|
105
|
+
name?: string;
|
|
106
|
+
notes?: string;
|
|
107
|
+
release?: string[] | boolean;
|
|
108
|
+
autoRegisterPath?: "add" | "remove" | "list";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// Review Types
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
export type ReviewVerdict = "SHIP" | "NEEDS_WORK" | "MAJOR_RETHINK";
|
|
116
|
+
|
|
117
|
+
export interface ReviewResult {
|
|
118
|
+
verdict: ReviewVerdict;
|
|
119
|
+
summary: string;
|
|
120
|
+
issues?: string[];
|
|
121
|
+
suggestions?: string[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// =============================================================================
|
|
125
|
+
// Agent Spawning Types
|
|
126
|
+
// =============================================================================
|
|
127
|
+
|
|
128
|
+
export interface AgentTask {
|
|
129
|
+
agent: string;
|
|
130
|
+
task: string;
|
|
131
|
+
maxOutput?: MaxOutputConfig;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface AgentResult {
|
|
135
|
+
agent: string;
|
|
136
|
+
exitCode: number;
|
|
137
|
+
output: string;
|
|
138
|
+
truncated: boolean;
|
|
139
|
+
progress: AgentProgress;
|
|
140
|
+
config?: CrewAgentConfig;
|
|
141
|
+
error?: string;
|
|
142
|
+
artifactPaths?: {
|
|
143
|
+
input: string;
|
|
144
|
+
output: string;
|
|
145
|
+
jsonl: string;
|
|
146
|
+
metadata: string;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// =============================================================================
|
|
151
|
+
// Callback Types
|
|
152
|
+
// =============================================================================
|
|
153
|
+
|
|
154
|
+
export type AppendEntryFn = (type: string, data: unknown) => void;
|
|
155
|
+
|
|
156
|
+
// =============================================================================
|
|
157
|
+
// Generated Task (from plan phase)
|
|
158
|
+
// =============================================================================
|
|
159
|
+
|
|
160
|
+
export interface GeneratedTask {
|
|
161
|
+
title: string;
|
|
162
|
+
description: string;
|
|
163
|
+
dependsOn?: string[]; // Task titles (resolved to IDs during creation)
|
|
164
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew - Debug Artifacts
|
|
3
|
+
*
|
|
4
|
+
* Writes debug files for troubleshooting agent failures.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
|
|
10
|
+
export interface ArtifactPaths {
|
|
11
|
+
inputPath: string;
|
|
12
|
+
outputPath: string;
|
|
13
|
+
jsonlPath: string;
|
|
14
|
+
metadataPath: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getArtifactPaths(
|
|
18
|
+
artifactsDir: string,
|
|
19
|
+
runId: string,
|
|
20
|
+
agent: string,
|
|
21
|
+
index?: number
|
|
22
|
+
): ArtifactPaths {
|
|
23
|
+
const suffix = index !== undefined ? `_${index}` : "";
|
|
24
|
+
const safeAgent = agent.replace(/[^\w.-]/g, "_");
|
|
25
|
+
const base = `${runId}_${safeAgent}${suffix}`;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
inputPath: path.join(artifactsDir, `${base}_input.md`),
|
|
29
|
+
outputPath: path.join(artifactsDir, `${base}_output.md`),
|
|
30
|
+
jsonlPath: path.join(artifactsDir, `${base}.jsonl`),
|
|
31
|
+
metadataPath: path.join(artifactsDir, `${base}_meta.json`),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ensureArtifactsDir(dir: string): void {
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function writeArtifact(filePath: string, content: string): void {
|
|
40
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function writeMetadata(filePath: string, metadata: object): void {
|
|
44
|
+
fs.writeFileSync(filePath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function appendJsonl(filePath: string, line: string): void {
|
|
48
|
+
fs.appendFileSync(filePath, `${line}\n`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function cleanupOldArtifacts(dir: string, maxAgeDays: number): void {
|
|
52
|
+
if (!fs.existsSync(dir)) return;
|
|
53
|
+
const cutoff = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000);
|
|
54
|
+
|
|
55
|
+
for (const file of fs.readdirSync(dir)) {
|
|
56
|
+
const filePath = path.join(dir, file);
|
|
57
|
+
try {
|
|
58
|
+
if (fs.statSync(filePath).mtimeMs < cutoff) {
|
|
59
|
+
fs.unlinkSync(filePath);
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// Ignore errors
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|