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,257 @@
1
+ /**
2
+ * Crew - Status Handler
3
+ *
4
+ * Shows plan progress and task status.
5
+ */
6
+
7
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
8
+ import type { MessengerState, Dirs } from "../../lib.js";
9
+ import type { CrewParams } from "../types.js";
10
+ import { result } from "../utils/result.js";
11
+ import { discoverCrewAgents } from "../utils/discover.js";
12
+ import {
13
+ ensureAgentsInstalled,
14
+ uninstallAgents,
15
+ ensureSkillsInstalled,
16
+ uninstallSkills
17
+ } from "../utils/install.js";
18
+ import * as store from "../store.js";
19
+ import { autonomousState } from "../state.js";
20
+
21
+ /**
22
+ * Execute status action - shows plan progress.
23
+ */
24
+ export async function execute(
25
+ _params: CrewParams,
26
+ _state: MessengerState,
27
+ _dirs: Dirs,
28
+ ctx: ExtensionContext
29
+ ) {
30
+ const cwd = ctx.cwd ?? process.cwd();
31
+ const plan = store.getPlan(cwd);
32
+
33
+ if (!plan) {
34
+ return result(`# Crew Status
35
+
36
+ **No active plan.**
37
+
38
+ Create a plan from your PRD:
39
+ pi_messenger({ action: "plan" }) # Auto-discovers PRD.md
40
+ pi_messenger({ action: "plan", prd: "docs/PRD.md" }) # Explicit path`, {
41
+ mode: "status",
42
+ hasPlan: false
43
+ });
44
+ }
45
+
46
+ const tasks = store.getTasks(cwd);
47
+ const done = tasks.filter(t => t.status === "done");
48
+ const inProgress = tasks.filter(t => t.status === "in_progress");
49
+ const blocked = tasks.filter(t => t.status === "blocked");
50
+ const ready = store.getReadyTasks(cwd);
51
+ const waiting = tasks.filter(t =>
52
+ t.status === "todo" && !ready.some(r => r.id === t.id)
53
+ );
54
+
55
+ const pct = tasks.length > 0 ? Math.round((done.length / tasks.length) * 100) : 0;
56
+
57
+ let text = `# Crew Status
58
+
59
+ **Plan:** ${plan.prd}
60
+ **Progress:** ${done.length}/${tasks.length} tasks (${pct}%)
61
+
62
+ ## Tasks
63
+ `;
64
+
65
+ if (done.length > 0) {
66
+ text += `\n✅ **Done**\n`;
67
+ for (const t of done) {
68
+ text += ` - ${t.id}: ${t.title}\n`;
69
+ }
70
+ }
71
+
72
+ if (inProgress.length > 0) {
73
+ text += `\n🔄 **In Progress**\n`;
74
+ for (const t of inProgress) {
75
+ const agent = t.assigned_to ? ` (${t.assigned_to}` : "";
76
+ const attempt = t.attempt_count > 1 ? `, attempt ${t.attempt_count}` : "";
77
+ text += ` - ${t.id}: ${t.title}${agent}${attempt}${t.assigned_to ? ")" : ""}\n`;
78
+ }
79
+ }
80
+
81
+ if (ready.length > 0) {
82
+ text += `\n⬜ **Ready**\n`;
83
+ for (const t of ready) {
84
+ text += ` - ${t.id}: ${t.title}\n`;
85
+ }
86
+ }
87
+
88
+ if (waiting.length > 0) {
89
+ text += `\n⏸️ **Waiting** (dependencies not met)\n`;
90
+ for (const t of waiting) {
91
+ const deps = t.depends_on.join(", ");
92
+ text += ` - ${t.id}: ${t.title} → needs: ${deps}\n`;
93
+ }
94
+ }
95
+
96
+ if (blocked.length > 0) {
97
+ text += `\n🚫 **Blocked**\n`;
98
+ for (const t of blocked) {
99
+ const reason = t.blocked_reason ? ` (${t.blocked_reason.slice(0, 40)}...)` : "";
100
+ text += ` - ${t.id}: ${t.title}${reason}\n`;
101
+ }
102
+ }
103
+
104
+ // Add autonomous status if active
105
+ if (autonomousState.active) {
106
+ text += `\n## Autonomous Mode\n`;
107
+ text += `Wave ${autonomousState.waveNumber} running...\n`;
108
+ if (autonomousState.startedAt) {
109
+ const startTime = new Date(autonomousState.startedAt).getTime();
110
+ const elapsedMs = Date.now() - startTime;
111
+ const minutes = Math.floor(elapsedMs / 60000);
112
+ const seconds = Math.floor((elapsedMs % 60000) / 1000);
113
+ text += `Elapsed: ${minutes}:${seconds.toString().padStart(2, "0")}\n`;
114
+ }
115
+ }
116
+
117
+ // Add next steps
118
+ text += `\n## Next`;
119
+ if (done.length === tasks.length) {
120
+ text += `\n🎉 All tasks complete!`;
121
+ } else if (ready.length > 0) {
122
+ text += `\nRun \`pi_messenger({ action: "work" })\` to execute ${ready.map(t => t.id).join(", ")}`;
123
+ } else if (blocked.length > 0) {
124
+ text += `\nUnblock tasks with \`pi_messenger({ action: "task.unblock", id: "..." })\``;
125
+ } else if (inProgress.length > 0) {
126
+ text += `\nWaiting for in-progress tasks to complete.`;
127
+ }
128
+
129
+ return result(text, {
130
+ mode: "status",
131
+ hasPlan: true,
132
+ prd: plan.prd,
133
+ progress: { done: done.length, total: tasks.length, pct },
134
+ tasks: {
135
+ done: done.map(t => t.id),
136
+ inProgress: inProgress.map(t => t.id),
137
+ ready: ready.map(t => t.id),
138
+ waiting: waiting.map(t => t.id),
139
+ blocked: blocked.map(t => t.id)
140
+ },
141
+ autonomous: autonomousState.active
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Execute crew.* actions (crew.status, crew.agents, crew.install, crew.uninstall)
147
+ */
148
+ export async function executeCrew(
149
+ op: string,
150
+ _params: CrewParams,
151
+ _state: MessengerState,
152
+ _dirs: Dirs,
153
+ ctx: ExtensionContext
154
+ ) {
155
+ const cwd = ctx.cwd ?? process.cwd();
156
+
157
+ switch (op) {
158
+ case "status": {
159
+ // Same as main status
160
+ return execute(_params, _state, _dirs, ctx);
161
+ }
162
+
163
+ case "agents": {
164
+ const agents = discoverCrewAgents(cwd);
165
+ if (agents.length === 0) {
166
+ return result("No crew agents found. Run crew.install to set up agents.", {
167
+ mode: "crew.agents",
168
+ agents: []
169
+ });
170
+ }
171
+
172
+ const byRole: Record<string, string[]> = {};
173
+ for (const a of agents) {
174
+ const role = a.crewRole ?? "other";
175
+ if (!byRole[role]) byRole[role] = [];
176
+ byRole[role].push(`${a.name} (${a.model ?? "default"})`);
177
+ }
178
+
179
+ let text = "# Crew Agents\n";
180
+ for (const [role, names] of Object.entries(byRole)) {
181
+ text += `\n**${role}s:** ${names.join(", ")}\n`;
182
+ }
183
+
184
+ return result(text, {
185
+ mode: "crew.agents",
186
+ agents: agents.map(a => ({ name: a.name, role: a.crewRole, model: a.model }))
187
+ });
188
+ }
189
+
190
+ case "install": {
191
+ ensureAgentsInstalled();
192
+ ensureSkillsInstalled();
193
+ const agents = discoverCrewAgents(cwd);
194
+ return result(`✅ Crew installed:\n- Agents: ${agents.map(a => a.name).join(", ")}\n- Skills: pi-messenger-crew`, {
195
+ mode: "crew.install",
196
+ agents: agents.map(a => a.name),
197
+ skills: ["pi-messenger-crew"]
198
+ });
199
+ }
200
+
201
+ case "uninstall": {
202
+ const agentResult = uninstallAgents();
203
+ const skillResult = uninstallSkills();
204
+ const errors = [...agentResult.errors, ...skillResult.errors];
205
+ const removed = { agents: agentResult.removed, skills: skillResult.removed };
206
+
207
+ if (errors.length > 0) {
208
+ return result(`⚠️ Removed with ${errors.length} error(s):\n${errors.join("\n")}`, {
209
+ mode: "crew.uninstall",
210
+ removed,
211
+ errors
212
+ });
213
+ }
214
+ return result(`✅ Removed:\n- ${agentResult.removed.length} agent(s)\n- ${skillResult.removed.length} skill(s)`, {
215
+ mode: "crew.uninstall",
216
+ removed
217
+ });
218
+ }
219
+
220
+ case "validate": {
221
+ const validation = store.validatePlan(cwd);
222
+
223
+ if (validation.valid && validation.warnings.length === 0) {
224
+ return result("✅ Plan is valid with no warnings.", {
225
+ mode: "crew.validate",
226
+ valid: true,
227
+ errors: [],
228
+ warnings: []
229
+ });
230
+ }
231
+
232
+ let text = validation.valid ? "✅ Plan is valid" : "❌ Plan has errors";
233
+
234
+ if (validation.errors.length > 0) {
235
+ text += "\n\n**Errors:**\n" + validation.errors.map(e => `- ${e}`).join("\n");
236
+ }
237
+
238
+ if (validation.warnings.length > 0) {
239
+ text += "\n\n**Warnings:**\n" + validation.warnings.map(w => `- ${w}`).join("\n");
240
+ }
241
+
242
+ return result(text, {
243
+ mode: "crew.validate",
244
+ valid: validation.valid,
245
+ errors: validation.errors,
246
+ warnings: validation.warnings
247
+ });
248
+ }
249
+
250
+ default:
251
+ return result(`Unknown crew operation: ${op}`, {
252
+ mode: "crew",
253
+ error: "unknown_operation",
254
+ operation: op
255
+ });
256
+ }
257
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Crew - Sync Handler
3
+ *
4
+ * Updates downstream specs after task completion.
5
+ * Works with current plan's tasks.
6
+ */
7
+
8
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
9
+ import type { MessengerState, Dirs } from "../../lib.js";
10
+ import type { CrewParams } from "../types.js";
11
+ import { result } from "../utils/result.js";
12
+ import { spawnAgents } from "../agents.js";
13
+ import { discoverCrewAgents } from "../utils/discover.js";
14
+ import * as store from "../store.js";
15
+
16
+ export async function execute(
17
+ params: CrewParams,
18
+ _state: MessengerState,
19
+ _dirs: Dirs,
20
+ ctx: ExtensionContext
21
+ ) {
22
+ const cwd = ctx.cwd ?? process.cwd();
23
+ const { target } = params;
24
+
25
+ if (!target) {
26
+ return result("Error: target (completed task ID) required for sync action.", {
27
+ mode: "sync",
28
+ error: "missing_target"
29
+ });
30
+ }
31
+
32
+ // Verify plan exists
33
+ const plan = store.getPlan(cwd);
34
+ if (!plan) {
35
+ return result("Error: No plan found.", {
36
+ mode: "sync",
37
+ error: "no_plan"
38
+ });
39
+ }
40
+
41
+ // Verify task exists and is completed
42
+ const task = store.getTask(cwd, target);
43
+ if (!task) {
44
+ return result(`Error: Task ${target} not found.`, {
45
+ mode: "sync",
46
+ error: "task_not_found",
47
+ target
48
+ });
49
+ }
50
+
51
+ if (task.status !== "done") {
52
+ return result(`Error: Task ${target} is ${task.status}, not done. Sync is for completed tasks.`, {
53
+ mode: "sync",
54
+ error: "task_not_done",
55
+ status: task.status
56
+ });
57
+ }
58
+
59
+ // Check for plan-sync agent
60
+ const availableAgents = discoverCrewAgents(cwd);
61
+ const hasSyncAgent = availableAgents.some(a => a.name === "crew-plan-sync");
62
+ if (!hasSyncAgent) {
63
+ return result("Error: crew-plan-sync agent not found.", {
64
+ mode: "sync",
65
+ error: "no_sync_agent"
66
+ });
67
+ }
68
+
69
+ const allTasks = store.getTasks(cwd);
70
+
71
+ // Find dependent tasks (tasks that depend on the completed task)
72
+ const dependentTasks = allTasks.filter(t =>
73
+ t.depends_on.includes(target) && t.status === "todo"
74
+ );
75
+
76
+ if (dependentTasks.length === 0) {
77
+ return result(`No downstream tasks depend on ${target}. No sync needed.`, {
78
+ mode: "sync",
79
+ taskId: target,
80
+ dependentTasks: [],
81
+ synced: false
82
+ });
83
+ }
84
+
85
+ // Get completed task details for context
86
+ const taskSpec = store.getTaskSpec(cwd, target);
87
+ const taskSummary = task.summary ?? "No summary";
88
+
89
+ // Build task overview for dependent tasks
90
+ const dependentOverview = dependentTasks.map(t => {
91
+ const spec = store.getTaskSpec(cwd, t.id);
92
+ return `### ${t.id}: ${t.title}
93
+
94
+ ${spec || "*No spec*"}
95
+ `;
96
+ }).join("\n");
97
+
98
+ // Build sync prompt
99
+ const prompt = `# Spec Sync Request
100
+
101
+ ## Completed Task
102
+
103
+ **Task ID:** ${target}
104
+ **Title:** ${task.title}
105
+ **Summary:** ${taskSummary}
106
+
107
+ ### Implementation Details
108
+
109
+ ${taskSpec || "*No detailed spec*"}
110
+
111
+ ## Dependent Tasks to Update
112
+
113
+ These tasks depend on the completed task and may need spec updates:
114
+
115
+ ${dependentOverview}
116
+
117
+ ## Your Task
118
+
119
+ 1. Review what was implemented in the completed task
120
+ 2. Check if any dependent task specs need updating based on the implementation
121
+ 3. Update specs with relevant information (file locations, API details, etc.)
122
+ 4. Output which specs were updated and why
123
+
124
+ Follow the output format in your instructions.`;
125
+
126
+ // Spawn sync agent
127
+ const [syncResult] = await spawnAgents([{
128
+ agent: "crew-plan-sync",
129
+ task: prompt
130
+ }], 1, cwd);
131
+
132
+ if (syncResult.exitCode !== 0) {
133
+ return result(`Error: Sync agent failed: ${syncResult.error ?? "Unknown error"}`, {
134
+ mode: "sync",
135
+ error: "sync_failed"
136
+ });
137
+ }
138
+
139
+ // Parse sync results
140
+ const updates = parseSyncUpdates(syncResult.output);
141
+
142
+ // Apply updates to task specs
143
+ let updatedCount = 0;
144
+ for (const update of updates) {
145
+ const matchingTask = dependentTasks.find(t =>
146
+ t.id === update.taskId ||
147
+ t.title.toLowerCase().includes(update.taskId.toLowerCase())
148
+ );
149
+
150
+ if (matchingTask && update.newContent) {
151
+ const currentSpec = store.getTaskSpec(cwd, matchingTask.id) ?? "";
152
+
153
+ // Append update to spec (don't replace)
154
+ const updatedSpec = `${currentSpec}
155
+
156
+ ---
157
+ *Updated after ${target} completion:*
158
+
159
+ ${update.newContent}`;
160
+
161
+ store.setTaskSpec(cwd, matchingTask.id, updatedSpec);
162
+ updatedCount++;
163
+ }
164
+ }
165
+
166
+ const readyTasks = store.getReadyTasks(cwd);
167
+ const text = `# Sync Complete: ${target}
168
+
169
+ **Dependent tasks checked:** ${dependentTasks.length}
170
+ **Specs updated:** ${updatedCount}
171
+
172
+ ${updates.length > 0 ? `## Updates\n${updates.map(u => `- **${u.taskId}**: ${u.reason}`).join("\n")}` : "## No Updates Needed\n\nDependent task specs are already up to date."}
173
+
174
+ ${updatedCount > 0 ? `\n**Ready tasks:** ${readyTasks.map(t => t.id).join(", ") || "none"}` : ""}`;
175
+
176
+ return result(text, {
177
+ mode: "sync",
178
+ taskId: target,
179
+ dependentTasks: dependentTasks.map(t => t.id),
180
+ updatedCount,
181
+ updates: updates.map(u => ({ taskId: u.taskId, reason: u.reason }))
182
+ });
183
+ }
184
+
185
+ // =============================================================================
186
+ // Sync Update Parsing
187
+ // =============================================================================
188
+
189
+ interface SyncUpdate {
190
+ taskId: string;
191
+ reason: string;
192
+ newContent?: string;
193
+ }
194
+
195
+ /**
196
+ * Parses sync updates from the sync agent output.
197
+ *
198
+ * Expected format:
199
+ * ### Updated: [task-id]
200
+ *
201
+ * Changes made:
202
+ * - Updated section X to reflect...
203
+ *
204
+ * New content:
205
+ * [content to add to spec]
206
+ */
207
+ function parseSyncUpdates(output: string): SyncUpdate[] {
208
+ const updates: SyncUpdate[] = [];
209
+
210
+ // Match update blocks
211
+ const updateRegex = /###\s*Updated:\s*(.+?)\n([\s\S]*?)(?=###|$)/gi;
212
+ let match;
213
+
214
+ while ((match = updateRegex.exec(output)) !== null) {
215
+ const taskId = match[1].trim();
216
+ const body = match[2].trim();
217
+
218
+ // Extract reason (Changes made section)
219
+ const reasonMatch = body.match(/Changes made:?\s*([\s\S]*?)(?=New content:|$)/i);
220
+ const reason = reasonMatch
221
+ ? reasonMatch[1].trim().replace(/^[-*]\s*/gm, "").split("\n")[0].trim()
222
+ : "Updated based on implementation";
223
+
224
+ // Extract new content
225
+ const contentMatch = body.match(/New content:?\s*([\s\S]*?)$/i);
226
+ const newContent = contentMatch ? contentMatch[1].trim() : undefined;
227
+
228
+ updates.push({ taskId, reason, newContent });
229
+ }
230
+
231
+ return updates;
232
+ }