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.
Files changed (45) hide show
  1. package/ARCHITECTURE.md +244 -0
  2. package/CHANGELOG.md +418 -0
  3. package/README.md +394 -0
  4. package/banner.png +0 -0
  5. package/config-overlay.ts +172 -0
  6. package/config.ts +178 -0
  7. package/crew/agents/crew-docs-scout.md +55 -0
  8. package/crew/agents/crew-gap-analyst.md +105 -0
  9. package/crew/agents/crew-github-scout.md +111 -0
  10. package/crew/agents/crew-interview-generator.md +79 -0
  11. package/crew/agents/crew-plan-sync.md +64 -0
  12. package/crew/agents/crew-practice-scout.md +62 -0
  13. package/crew/agents/crew-repo-scout.md +65 -0
  14. package/crew/agents/crew-reviewer.md +58 -0
  15. package/crew/agents/crew-web-scout.md +85 -0
  16. package/crew/agents/crew-worker.md +95 -0
  17. package/crew/agents.ts +200 -0
  18. package/crew/handlers/interview.ts +211 -0
  19. package/crew/handlers/plan.ts +358 -0
  20. package/crew/handlers/review.ts +341 -0
  21. package/crew/handlers/status.ts +257 -0
  22. package/crew/handlers/sync.ts +232 -0
  23. package/crew/handlers/task.ts +511 -0
  24. package/crew/handlers/work.ts +289 -0
  25. package/crew/id-allocator.ts +44 -0
  26. package/crew/index.ts +229 -0
  27. package/crew/state.ts +116 -0
  28. package/crew/store.ts +480 -0
  29. package/crew/types.ts +164 -0
  30. package/crew/utils/artifacts.ts +65 -0
  31. package/crew/utils/config.ts +104 -0
  32. package/crew/utils/discover.ts +170 -0
  33. package/crew/utils/install.ts +373 -0
  34. package/crew/utils/progress.ts +107 -0
  35. package/crew/utils/result.ts +16 -0
  36. package/crew/utils/truncate.ts +79 -0
  37. package/crew-overlay.ts +259 -0
  38. package/handlers.ts +799 -0
  39. package/index.ts +591 -0
  40. package/lib.ts +232 -0
  41. package/overlay.ts +687 -0
  42. package/package.json +20 -0
  43. package/skills/pi-messenger-crew/SKILL.md +140 -0
  44. package/store.ts +1068 -0
  45. package/tsconfig.json +19 -0
@@ -0,0 +1,511 @@
1
+ /**
2
+ * Crew - Task Handlers
3
+ *
4
+ * Operations: create, show, list, start, done, block, unblock, ready, reset
5
+ * Simplified: tasks belong to the plan, not an epic
6
+ */
7
+
8
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
9
+ import type { MessengerState, Dirs } from "../../lib.js";
10
+ import type { CrewParams, TaskEvidence } from "../types.js";
11
+ import { result } from "../utils/result.js";
12
+ import * as store from "../store.js";
13
+
14
+ export async function execute(
15
+ op: string,
16
+ params: CrewParams,
17
+ state: MessengerState,
18
+ _dirs: Dirs,
19
+ ctx: ExtensionContext
20
+ ) {
21
+ const cwd = ctx.cwd ?? process.cwd();
22
+
23
+ switch (op) {
24
+ case "create":
25
+ return taskCreate(cwd, params);
26
+ case "show":
27
+ return taskShow(cwd, params);
28
+ case "list":
29
+ return taskList(cwd);
30
+ case "start":
31
+ return taskStart(cwd, params, state);
32
+ case "done":
33
+ return taskDone(cwd, params);
34
+ case "block":
35
+ return taskBlock(cwd, params);
36
+ case "unblock":
37
+ return taskUnblock(cwd, params);
38
+ case "ready":
39
+ return taskReady(cwd);
40
+ case "reset":
41
+ return taskReset(cwd, params);
42
+ default:
43
+ return result(`Unknown task operation: ${op}`, { mode: "task", error: "unknown_operation", operation: op });
44
+ }
45
+ }
46
+
47
+ // =============================================================================
48
+ // task.create
49
+ // =============================================================================
50
+
51
+ function taskCreate(cwd: string, params: CrewParams) {
52
+ if (!params.title) {
53
+ return result("Error: title required for task.create", { mode: "task.create", error: "missing_title" });
54
+ }
55
+
56
+ // Verify plan exists
57
+ const plan = store.getPlan(cwd);
58
+ if (!plan) {
59
+ return result("Error: No plan exists. Create one first with pi_messenger({ action: \"plan\" })", {
60
+ mode: "task.create", error: "no_plan"
61
+ });
62
+ }
63
+
64
+ // Validate dependencies exist
65
+ if (params.dependsOn && params.dependsOn.length > 0) {
66
+ for (const depId of params.dependsOn) {
67
+ const dep = store.getTask(cwd, depId);
68
+ if (!dep) {
69
+ return result(`Error: Dependency ${depId} not found`, { mode: "task.create", error: "dependency_not_found", dependency: depId });
70
+ }
71
+ }
72
+ }
73
+
74
+ const task = store.createTask(cwd, params.title, params.content, params.dependsOn);
75
+
76
+ const depsText = task.depends_on.length > 0
77
+ ? `\n**Depends on:** ${task.depends_on.join(", ")}`
78
+ : "";
79
+
80
+ const text = `✅ Created task **${task.id}**
81
+
82
+ **Title:** ${task.title}
83
+ **Status:** ${task.status}${depsText}
84
+
85
+ Start with: \`pi_messenger({ action: "task.start", id: "${task.id}" })\``;
86
+
87
+ return result(text, {
88
+ mode: "task.create",
89
+ task: {
90
+ id: task.id,
91
+ title: task.title,
92
+ status: task.status,
93
+ depends_on: task.depends_on,
94
+ }
95
+ });
96
+ }
97
+
98
+ // =============================================================================
99
+ // task.show
100
+ // =============================================================================
101
+
102
+ function taskShow(cwd: string, params: CrewParams) {
103
+ const id = params.id;
104
+ if (!id) {
105
+ return result("Error: id required for task.show", { mode: "task.show", error: "missing_id" });
106
+ }
107
+
108
+ const task = store.getTask(cwd, id);
109
+ if (!task) {
110
+ return result(`Error: Task ${id} not found`, { mode: "task.show", error: "not_found", id });
111
+ }
112
+
113
+ const spec = store.getTaskSpec(cwd, id);
114
+
115
+ // Build status details
116
+ let statusDetails = "";
117
+ switch (task.status) {
118
+ case "in_progress":
119
+ statusDetails = `\n**Assigned to:** ${task.assigned_to ?? "unknown"}\n**Started:** ${task.started_at}`;
120
+ if (task.base_commit) statusDetails += `\n**Base commit:** ${task.base_commit.slice(0, 8)}`;
121
+ break;
122
+ case "done":
123
+ statusDetails = `\n**Completed:** ${task.completed_at}`;
124
+ if (task.summary) statusDetails += `\n**Summary:** ${task.summary}`;
125
+ break;
126
+ case "blocked":
127
+ statusDetails = `\n**Blocked reason:** ${task.blocked_reason ?? "unknown"}`;
128
+ break;
129
+ }
130
+
131
+ const depsText = task.depends_on.length > 0
132
+ ? `\n**Depends on:** ${task.depends_on.join(", ")}`
133
+ : "";
134
+
135
+ // Spec preview
136
+ let specPreview = "";
137
+ if (spec && !spec.includes("*Spec pending*")) {
138
+ const truncated = spec.length > 800 ? spec.slice(0, 800) + "..." : spec;
139
+ specPreview = `\n\n## Spec\n\`\`\`\n${truncated}\n\`\`\``;
140
+ }
141
+
142
+ const statusIcon = {
143
+ todo: "⬜",
144
+ in_progress: "🔄",
145
+ done: "✅",
146
+ blocked: "🚫",
147
+ }[task.status];
148
+
149
+ const text = `# Task ${task.id}: ${task.title}
150
+
151
+ ${statusIcon} **Status:** ${task.status}${statusDetails}
152
+ **Attempts:** ${task.attempt_count}${depsText}${specPreview}`;
153
+
154
+ return result(text, {
155
+ mode: "task.show",
156
+ task,
157
+ hasSpec: spec && !spec.includes("*Spec pending*"),
158
+ });
159
+ }
160
+
161
+ // =============================================================================
162
+ // task.list
163
+ // =============================================================================
164
+
165
+ function taskList(cwd: string) {
166
+ const plan = store.getPlan(cwd);
167
+ if (!plan) {
168
+ return result("No plan found. Create one with: pi_messenger({ action: \"plan\" })", {
169
+ mode: "task.list", tasks: [], hasPlan: false
170
+ });
171
+ }
172
+
173
+ const tasks = store.getTasks(cwd);
174
+ if (tasks.length === 0) {
175
+ return result(`No tasks in plan. Create with: \`pi_messenger({ action: "task.create", title: "..." })\``, {
176
+ mode: "task.list",
177
+ tasks: [],
178
+ prd: plan.prd,
179
+ });
180
+ }
181
+
182
+ const lines: string[] = [`# Tasks for ${plan.prd}\n`];
183
+
184
+ for (const task of tasks) {
185
+ const icon = { todo: "⬜", in_progress: "🔄", done: "✅", blocked: "🚫" }[task.status];
186
+ const deps = task.depends_on.length > 0 ? ` → deps: ${task.depends_on.join(", ")}` : "";
187
+ const assignee = task.assigned_to ? ` [${task.assigned_to}]` : "";
188
+ lines.push(`${icon} **${task.id}**: ${task.title}${assignee}${deps}`);
189
+ }
190
+
191
+ const done = tasks.filter(t => t.status === "done").length;
192
+ lines.push(`\n**Progress:** ${done}/${tasks.length}`);
193
+
194
+ return result(lines.join("\n"), {
195
+ mode: "task.list",
196
+ prd: plan.prd,
197
+ tasks: tasks.map(t => ({
198
+ id: t.id,
199
+ title: t.title,
200
+ status: t.status,
201
+ depends_on: t.depends_on,
202
+ })),
203
+ });
204
+ }
205
+
206
+ // =============================================================================
207
+ // task.start
208
+ // =============================================================================
209
+
210
+ function taskStart(cwd: string, params: CrewParams, state: MessengerState) {
211
+ const id = params.id;
212
+ if (!id) {
213
+ return result("Error: id required for task.start", { mode: "task.start", error: "missing_id" });
214
+ }
215
+
216
+ const task = store.getTask(cwd, id);
217
+ if (!task) {
218
+ return result(`Error: Task ${id} not found`, { mode: "task.start", error: "not_found", id });
219
+ }
220
+
221
+ if (task.status !== "todo") {
222
+ return result(`Error: Task ${id} is ${task.status}, not todo`, {
223
+ mode: "task.start", error: "invalid_status", id, status: task.status
224
+ });
225
+ }
226
+
227
+ // Check dependencies are done
228
+ if (task.depends_on.length > 0) {
229
+ const notDone: string[] = [];
230
+ for (const depId of task.depends_on) {
231
+ const dep = store.getTask(cwd, depId);
232
+ if (dep && dep.status !== "done") {
233
+ notDone.push(`${depId} (${dep.status})`);
234
+ }
235
+ }
236
+ if (notDone.length > 0) {
237
+ return result(`Error: Task ${id} has unmet dependencies: ${notDone.join(", ")}`, {
238
+ mode: "task.start",
239
+ error: "unmet_dependencies",
240
+ id,
241
+ unmetDependencies: notDone,
242
+ });
243
+ }
244
+ }
245
+
246
+ const agentName = state.agentName || "unknown";
247
+ const started = store.startTask(cwd, id, agentName);
248
+
249
+ if (!started) {
250
+ return result(`Error: Failed to start task ${id}`, { mode: "task.start", error: "start_failed", id });
251
+ }
252
+
253
+ const spec = store.getTaskSpec(cwd, id);
254
+ const specPreview = spec && !spec.includes("*Spec pending*")
255
+ ? `\n\n**Spec:**\n\`\`\`\n${spec.length > 1000 ? spec.slice(0, 1000) + "..." : spec}\n\`\`\``
256
+ : "";
257
+
258
+ const text = `🔄 Started task **${id}**
259
+
260
+ **Title:** ${started.title}
261
+ **Assigned to:** ${agentName}
262
+ **Attempt:** ${started.attempt_count}
263
+ ${started.base_commit ? `**Base commit:** ${started.base_commit.slice(0, 8)}` : ""}${specPreview}
264
+
265
+ When done: \`pi_messenger({ action: "task.done", id: "${id}", summary: "..." })\`
266
+ If blocked: \`pi_messenger({ action: "task.block", id: "${id}", reason: "..." })\``;
267
+
268
+ return result(text, {
269
+ mode: "task.start",
270
+ task: {
271
+ id: started.id,
272
+ title: started.title,
273
+ status: started.status,
274
+ assigned_to: started.assigned_to,
275
+ attempt_count: started.attempt_count,
276
+ base_commit: started.base_commit,
277
+ }
278
+ });
279
+ }
280
+
281
+ // =============================================================================
282
+ // task.done
283
+ // =============================================================================
284
+
285
+ function taskDone(cwd: string, params: CrewParams) {
286
+ const id = params.id;
287
+ if (!id) {
288
+ return result("Error: id required for task.done", { mode: "task.done", error: "missing_id" });
289
+ }
290
+
291
+ const task = store.getTask(cwd, id);
292
+ if (!task) {
293
+ return result(`Error: Task ${id} not found`, { mode: "task.done", error: "not_found", id });
294
+ }
295
+
296
+ if (task.status !== "in_progress") {
297
+ return result(`Error: Task ${id} is ${task.status}, not in_progress`, {
298
+ mode: "task.done", error: "invalid_status", id, status: task.status
299
+ });
300
+ }
301
+
302
+ const summary = params.summary ?? "Task completed";
303
+ const evidence: TaskEvidence | undefined = params.evidence;
304
+
305
+ const completed = store.completeTask(cwd, id, summary, evidence);
306
+ if (!completed) {
307
+ return result(`Error: Failed to complete task ${id}`, { mode: "task.done", error: "complete_failed", id });
308
+ }
309
+
310
+ // Check progress
311
+ const plan = store.getPlan(cwd);
312
+ const tasks = store.getTasks(cwd);
313
+ const remaining = tasks.filter(t => t.status !== "done");
314
+
315
+ let nextSteps = "";
316
+ if (remaining.length === 0) {
317
+ nextSteps = `\n\n🎉 **All tasks complete!** Plan is finished.`;
318
+ } else {
319
+ const ready = store.getReadyTasks(cwd);
320
+ if (ready.length > 0) {
321
+ nextSteps = `\n\n**Ready tasks:** ${ready.map(t => t.id).join(", ")}`;
322
+ }
323
+ }
324
+
325
+ const text = `✅ Completed task **${id}**
326
+
327
+ **Summary:** ${summary}
328
+ **Progress:** ${plan?.completed_count}/${plan?.task_count}${nextSteps}`;
329
+
330
+ return result(text, {
331
+ mode: "task.done",
332
+ task: {
333
+ id: completed.id,
334
+ title: completed.title,
335
+ status: completed.status,
336
+ summary: completed.summary,
337
+ },
338
+ progress: {
339
+ completed: plan?.completed_count ?? 0,
340
+ total: plan?.task_count ?? 0,
341
+ }
342
+ });
343
+ }
344
+
345
+ // =============================================================================
346
+ // task.block
347
+ // =============================================================================
348
+
349
+ function taskBlock(cwd: string, params: CrewParams) {
350
+ const id = params.id;
351
+ if (!id) {
352
+ return result("Error: id required for task.block", { mode: "task.block", error: "missing_id" });
353
+ }
354
+
355
+ if (!params.reason) {
356
+ return result("Error: reason required for task.block", { mode: "task.block", error: "missing_reason" });
357
+ }
358
+
359
+ const task = store.getTask(cwd, id);
360
+ if (!task) {
361
+ return result(`Error: Task ${id} not found`, { mode: "task.block", error: "not_found", id });
362
+ }
363
+
364
+ const blocked = store.blockTask(cwd, id, params.reason);
365
+ if (!blocked) {
366
+ return result(`Error: Failed to block task ${id}`, { mode: "task.block", error: "block_failed", id });
367
+ }
368
+
369
+ const text = `🚫 Blocked task **${id}**
370
+
371
+ **Reason:** ${params.reason}
372
+
373
+ Unblock with: \`pi_messenger({ action: "task.unblock", id: "${id}" })\``;
374
+
375
+ return result(text, {
376
+ mode: "task.block",
377
+ task: {
378
+ id: blocked.id,
379
+ title: blocked.title,
380
+ status: blocked.status,
381
+ blocked_reason: blocked.blocked_reason,
382
+ }
383
+ });
384
+ }
385
+
386
+ // =============================================================================
387
+ // task.unblock
388
+ // =============================================================================
389
+
390
+ function taskUnblock(cwd: string, params: CrewParams) {
391
+ const id = params.id;
392
+ if (!id) {
393
+ return result("Error: id required for task.unblock", { mode: "task.unblock", error: "missing_id" });
394
+ }
395
+
396
+ const task = store.getTask(cwd, id);
397
+ if (!task) {
398
+ return result(`Error: Task ${id} not found`, { mode: "task.unblock", error: "not_found", id });
399
+ }
400
+
401
+ if (task.status !== "blocked") {
402
+ return result(`Error: Task ${id} is ${task.status}, not blocked`, {
403
+ mode: "task.unblock", error: "invalid_status", id, status: task.status
404
+ });
405
+ }
406
+
407
+ const unblocked = store.unblockTask(cwd, id);
408
+ if (!unblocked) {
409
+ return result(`Error: Failed to unblock task ${id}`, { mode: "task.unblock", error: "unblock_failed", id });
410
+ }
411
+
412
+ const text = `⬜ Unblocked task **${id}**
413
+
414
+ Task is now ready to start: \`pi_messenger({ action: "task.start", id: "${id}" })\``;
415
+
416
+ return result(text, {
417
+ mode: "task.unblock",
418
+ task: {
419
+ id: unblocked.id,
420
+ title: unblocked.title,
421
+ status: unblocked.status,
422
+ }
423
+ });
424
+ }
425
+
426
+ // =============================================================================
427
+ // task.ready
428
+ // =============================================================================
429
+
430
+ function taskReady(cwd: string) {
431
+ const plan = store.getPlan(cwd);
432
+ if (!plan) {
433
+ return result("No plan found. Create one with: pi_messenger({ action: \"plan\" })", {
434
+ mode: "task.ready", ready: [], hasPlan: false
435
+ });
436
+ }
437
+
438
+ const ready = store.getReadyTasks(cwd);
439
+
440
+ if (ready.length === 0) {
441
+ const tasks = store.getTasks(cwd);
442
+ const inProgress = tasks.filter(t => t.status === "in_progress");
443
+ const blocked = tasks.filter(t => t.status === "blocked");
444
+ const done = tasks.filter(t => t.status === "done");
445
+
446
+ let reason = "";
447
+ if (done.length === tasks.length) {
448
+ reason = "All tasks are done!";
449
+ } else if (inProgress.length > 0) {
450
+ reason = `${inProgress.length} task(s) in progress: ${inProgress.map(t => t.id).join(", ")}`;
451
+ } else if (blocked.length > 0) {
452
+ reason = `${blocked.length} task(s) blocked: ${blocked.map(t => t.id).join(", ")}`;
453
+ } else {
454
+ reason = "All remaining tasks have unmet dependencies.";
455
+ }
456
+
457
+ return result(`No ready tasks.\n\n${reason}`, {
458
+ mode: "task.ready",
459
+ ready: [],
460
+ reason,
461
+ });
462
+ }
463
+
464
+ const lines: string[] = [`# Ready Tasks\n`];
465
+ for (const task of ready) {
466
+ lines.push(`⬜ **${task.id}**: ${task.title}`);
467
+ }
468
+ lines.push(`\nStart one: \`pi_messenger({ action: "task.start", id: "${ready[0].id}" })\``);
469
+ lines.push(`Or run all: \`pi_messenger({ action: "work" })\``);
470
+
471
+ return result(lines.join("\n"), {
472
+ mode: "task.ready",
473
+ ready: ready.map(t => ({
474
+ id: t.id,
475
+ title: t.title,
476
+ })),
477
+ });
478
+ }
479
+
480
+ // =============================================================================
481
+ // task.reset
482
+ // =============================================================================
483
+
484
+ function taskReset(cwd: string, params: CrewParams) {
485
+ const id = params.id;
486
+ if (!id) {
487
+ return result("Error: id required for task.reset", { mode: "task.reset", error: "missing_id" });
488
+ }
489
+
490
+ const task = store.getTask(cwd, id);
491
+ if (!task) {
492
+ return result(`Error: Task ${id} not found`, { mode: "task.reset", error: "not_found", id });
493
+ }
494
+
495
+ const cascade = params.cascade ?? false;
496
+ const resetTasks = store.resetTask(cwd, id, cascade);
497
+
498
+ if (resetTasks.length === 0) {
499
+ return result(`Error: Failed to reset task ${id}`, { mode: "task.reset", error: "reset_failed", id });
500
+ }
501
+
502
+ const text = cascade && resetTasks.length > 1
503
+ ? `🔄 Reset ${resetTasks.length} tasks:\n${resetTasks.map(t => ` - ${t.id}`).join("\n")}`
504
+ : `🔄 Reset task **${id}**`;
505
+
506
+ return result(text + `\n\nStart with: \`pi_messenger({ action: "task.start", id: "${id}" })\``, {
507
+ mode: "task.reset",
508
+ reset: resetTasks.map(t => t.id),
509
+ cascade,
510
+ });
511
+ }