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,289 @@
1
+ /**
2
+ * Crew - Work Handler
3
+ *
4
+ * Spawns workers for ready tasks with concurrency control.
5
+ * Simplified: works on 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, AppendEntryFn, Task } from "../types.js";
11
+ import { result } from "../utils/result.js";
12
+ import { spawnAgents } from "../agents.js";
13
+ import { loadCrewConfig } from "../utils/config.js";
14
+ import { discoverCrewAgents } from "../utils/discover.js";
15
+ import * as store from "../store.js";
16
+ import { getCrewDir } from "../store.js";
17
+ import { autonomousState, startAutonomous, stopAutonomous, addWaveResult } from "../state.js";
18
+
19
+ export async function execute(
20
+ params: CrewParams,
21
+ _state: MessengerState,
22
+ _dirs: Dirs,
23
+ ctx: ExtensionContext,
24
+ appendEntry: AppendEntryFn
25
+ ) {
26
+ const cwd = ctx.cwd ?? process.cwd();
27
+ const config = loadCrewConfig(getCrewDir(cwd));
28
+ const { autonomous, concurrency: concurrencyOverride } = params;
29
+
30
+ // Verify plan exists
31
+ const plan = store.getPlan(cwd);
32
+ if (!plan) {
33
+ return result("No plan found. Create one first:\n\n pi_messenger({ action: \"plan\" })\n pi_messenger({ action: \"plan\", prd: \"path/to/PRD.md\" })", {
34
+ mode: "work",
35
+ error: "no_plan"
36
+ });
37
+ }
38
+
39
+ // Check for worker agent
40
+ const availableAgents = discoverCrewAgents(cwd);
41
+ const hasWorker = availableAgents.some(a => a.name === "crew-worker");
42
+ if (!hasWorker) {
43
+ return result("Error: crew-worker agent not found. Required for task execution.", {
44
+ mode: "work",
45
+ error: "no_worker"
46
+ });
47
+ }
48
+
49
+ // Get ready tasks
50
+ const readyTasks = store.getReadyTasks(cwd);
51
+
52
+ if (readyTasks.length === 0) {
53
+ const tasks = store.getTasks(cwd);
54
+ const inProgress = tasks.filter(t => t.status === "in_progress");
55
+ const blocked = tasks.filter(t => t.status === "blocked");
56
+ const done = tasks.filter(t => t.status === "done");
57
+
58
+ let reason = "";
59
+ if (done.length === tasks.length) {
60
+ reason = "🎉 All tasks are done! Plan is complete.";
61
+ } else if (inProgress.length > 0) {
62
+ reason = `${inProgress.length} task(s) in progress: ${inProgress.map(t => t.id).join(", ")}`;
63
+ } else if (blocked.length > 0) {
64
+ reason = `${blocked.length} task(s) blocked: ${blocked.map(t => `${t.id} (${t.blocked_reason})`).join(", ")}`;
65
+ } else {
66
+ reason = "All remaining tasks have unmet dependencies.";
67
+ }
68
+
69
+ return result(`No ready tasks.\n\n${reason}`, {
70
+ mode: "work",
71
+ prd: plan.prd,
72
+ ready: [],
73
+ reason,
74
+ inProgress: inProgress.map(t => t.id),
75
+ blocked: blocked.map(t => t.id)
76
+ });
77
+ }
78
+
79
+ // Determine concurrency
80
+ const concurrency = concurrencyOverride ?? config.concurrency.workers;
81
+ const tasksToRun = readyTasks.slice(0, concurrency);
82
+
83
+ // If autonomous mode, set up state and persist (only on first wave or cwd change)
84
+ if (autonomous && (!autonomousState.active || autonomousState.cwd !== cwd)) {
85
+ startAutonomous(cwd);
86
+ appendEntry("crew-state", autonomousState);
87
+ }
88
+
89
+ // Spawn workers
90
+ const workerTasks = tasksToRun.map(task => ({
91
+ agent: "crew-worker",
92
+ task: buildWorkerPrompt(task, plan.prd, cwd)
93
+ }));
94
+
95
+ const workerResults = await spawnAgents(
96
+ workerTasks,
97
+ concurrency,
98
+ cwd
99
+ );
100
+
101
+ // Process results
102
+ const succeeded: string[] = [];
103
+ const failed: string[] = [];
104
+ const blocked: string[] = [];
105
+
106
+ for (let i = 0; i < workerResults.length; i++) {
107
+ const r = workerResults[i];
108
+ const taskId = tasksToRun[i].id;
109
+ const task = store.getTask(cwd, taskId);
110
+
111
+ if (r.exitCode === 0) {
112
+ // Check if task was completed (worker should call task.done)
113
+ if (task?.status === "done") {
114
+ succeeded.push(taskId);
115
+ } else if (task?.status === "blocked") {
116
+ blocked.push(taskId);
117
+ } else {
118
+ // Worker finished but didn't complete - treat as failure
119
+ failed.push(taskId);
120
+ }
121
+ } else {
122
+ // Auto-block on failure if in autonomous mode
123
+ if (autonomous && task?.status === "in_progress") {
124
+ store.blockTask(cwd, taskId, `Worker failed: ${r.error ?? "Unknown error"}`);
125
+ blocked.push(taskId);
126
+ } else {
127
+ failed.push(taskId);
128
+ }
129
+ }
130
+ }
131
+
132
+ // Save current wave number BEFORE addWaveResult increments it
133
+ const currentWave = autonomous ? autonomousState.waveNumber : 1;
134
+
135
+ if (autonomous) {
136
+ addWaveResult({
137
+ waveNumber: currentWave,
138
+ tasksAttempted: tasksToRun.map(t => t.id),
139
+ succeeded,
140
+ failed,
141
+ blocked,
142
+ timestamp: new Date().toISOString()
143
+ });
144
+
145
+ // Check if we should continue
146
+ const nextReady = store.getReadyTasks(cwd);
147
+ const allTasks = store.getTasks(cwd);
148
+ const allDone = allTasks.every(t => t.status === "done");
149
+ const allBlockedOrDone = allTasks.every(t => t.status === "done" || t.status === "blocked");
150
+
151
+ if (allDone) {
152
+ stopAutonomous("completed");
153
+ appendEntry("crew-state", autonomousState);
154
+ appendEntry("crew_wave_complete", {
155
+ prd: plan.prd,
156
+ status: "completed",
157
+ totalWaves: currentWave,
158
+ totalTasks: allTasks.length
159
+ });
160
+ } else if (allBlockedOrDone || nextReady.length === 0) {
161
+ stopAutonomous("blocked");
162
+ appendEntry("crew-state", autonomousState);
163
+ appendEntry("crew_wave_blocked", {
164
+ prd: plan.prd,
165
+ status: "blocked",
166
+ blockedTasks: allTasks.filter(t => t.status === "blocked").map(t => t.id)
167
+ });
168
+ } else {
169
+ // Persist state for session recovery and signal continuation
170
+ appendEntry("crew-state", autonomousState);
171
+ appendEntry("crew_wave_continue", {
172
+ prd: plan.prd,
173
+ nextWave: autonomousState.waveNumber,
174
+ readyTasks: nextReady.map(t => t.id)
175
+ });
176
+ }
177
+ }
178
+
179
+ // Build result
180
+ const updatedPlan = store.getPlan(cwd);
181
+ const progress = updatedPlan
182
+ ? `${updatedPlan.completed_count}/${updatedPlan.task_count}`
183
+ : "unknown";
184
+
185
+ let statusText = "";
186
+ if (succeeded.length > 0) statusText += `\n✅ Completed: ${succeeded.join(", ")}`;
187
+ if (failed.length > 0) statusText += `\n❌ Failed: ${failed.join(", ")}`;
188
+ if (blocked.length > 0) statusText += `\n🚫 Blocked: ${blocked.join(", ")}`;
189
+
190
+ const nextReady = store.getReadyTasks(cwd);
191
+ const nextText = nextReady.length > 0
192
+ ? `\n\n**Ready for next wave:** ${nextReady.map(t => t.id).join(", ")}`
193
+ : "";
194
+
195
+ const text = `# Work Wave ${currentWave}
196
+
197
+ **PRD:** ${plan.prd}
198
+ **Tasks attempted:** ${tasksToRun.length}
199
+ **Progress:** ${progress}
200
+ ${statusText}${nextText}
201
+
202
+ ${autonomous && nextReady.length > 0 ? "Autonomous mode: Continuing to next wave..." : ""}`;
203
+
204
+ return result(text, {
205
+ mode: "work",
206
+ prd: plan.prd,
207
+ wave: currentWave,
208
+ attempted: tasksToRun.map(t => t.id),
209
+ succeeded,
210
+ failed,
211
+ blocked,
212
+ nextReady: nextReady.map(t => t.id),
213
+ autonomous: !!autonomous
214
+ });
215
+ }
216
+
217
+ // =============================================================================
218
+ // Worker Prompt Builder
219
+ // =============================================================================
220
+
221
+ function buildWorkerPrompt(task: Task, prdPath: string, cwd: string): string {
222
+ const taskSpec = store.getTaskSpec(cwd, task.id);
223
+ const planSpec = store.getPlanSpec(cwd);
224
+
225
+ let prompt = `# Task Assignment
226
+
227
+ **Task ID:** ${task.id}
228
+ **Task Title:** ${task.title}
229
+ **PRD:** ${prdPath}
230
+ ${task.attempt_count >= 1 ? `**Attempt:** ${task.attempt_count + 1} (retry after previous attempt)` : ""}
231
+
232
+ ## Your Mission
233
+
234
+ Implement this task following the crew-worker protocol:
235
+ 1. Join the mesh
236
+ 2. Read task spec to understand requirements
237
+ 3. Start task and reserve files
238
+ 4. Implement the feature
239
+ 5. Commit your changes
240
+ 6. Release reservations and mark complete
241
+
242
+ `;
243
+
244
+ // Include previous review feedback if this is a retry
245
+ if (task.last_review) {
246
+ prompt += `## ⚠️ Previous Review Feedback
247
+
248
+ **Verdict:** ${task.last_review.verdict}
249
+
250
+ ${task.last_review.summary}
251
+
252
+ ${task.last_review.issues.length > 0 ? `**Issues to fix:**\n${task.last_review.issues.map(i => `- ${i}`).join("\n")}\n` : ""}
253
+ ${task.last_review.suggestions.length > 0 ? `**Suggestions:**\n${task.last_review.suggestions.map(s => `- ${s}`).join("\n")}\n` : ""}
254
+
255
+ **You MUST address the issues above in this attempt.**
256
+
257
+ `;
258
+ }
259
+
260
+ if (taskSpec && !taskSpec.includes("*Spec pending*")) {
261
+ prompt += `## Task Specification
262
+
263
+ ${taskSpec}
264
+
265
+ `;
266
+ }
267
+
268
+ if (task.depends_on.length > 0) {
269
+ prompt += `## Dependencies
270
+
271
+ This task depends on: ${task.depends_on.join(", ")}
272
+ These tasks are already complete - you can reference their implementations.
273
+
274
+ `;
275
+ }
276
+
277
+ if (planSpec && !planSpec.includes("*Spec pending*")) {
278
+ // Include truncated plan spec for context
279
+ const truncatedSpec = planSpec.length > 2000
280
+ ? planSpec.slice(0, 2000) + `\n\n[Spec truncated - read full spec from .pi/messenger/crew/plan.md]`
281
+ : planSpec;
282
+ prompt += `## Plan Context
283
+
284
+ ${truncatedSpec}
285
+ `;
286
+ }
287
+
288
+ return prompt;
289
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Crew - ID Allocator
3
+ *
4
+ * Simple task ID allocation: task-1, task-2, ...
5
+ */
6
+
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+
10
+ /**
11
+ * Scans existing tasks to determine the next sequence number.
12
+ * Returns task ID in format: task-N
13
+ */
14
+ export function allocateTaskId(cwd: string): string {
15
+ const tasksDir = path.join(cwd, ".pi", "messenger", "crew", "tasks");
16
+
17
+ let maxN = 0;
18
+ if (fs.existsSync(tasksDir)) {
19
+ for (const file of fs.readdirSync(tasksDir)) {
20
+ const match = file.match(/^task-(\d+)\.json$/);
21
+ if (match) {
22
+ const n = parseInt(match[1], 10);
23
+ if (n > maxN) maxN = n;
24
+ }
25
+ }
26
+ }
27
+
28
+ return `task-${maxN + 1}`;
29
+ }
30
+
31
+ /**
32
+ * Validates that an ID is a well-formed task ID.
33
+ */
34
+ export function isValidTaskId(id: string): boolean {
35
+ return /^task-\d+$/.test(id);
36
+ }
37
+
38
+ /**
39
+ * Extracts the task number from a task ID.
40
+ */
41
+ export function getTaskNumber(taskId: string): number | null {
42
+ const match = taskId.match(/^task-(\d+)$/);
43
+ return match ? parseInt(match[1], 10) : null;
44
+ }
package/crew/index.ts ADDED
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Crew - Action Router
3
+ *
4
+ * Routes crew actions to their respective handlers.
5
+ * Simplified: PRD → plan → tasks → work → done
6
+ */
7
+
8
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
9
+ import type { MessengerState, Dirs, AgentMailMessage } from "../lib.js";
10
+ import * as handlers from "../handlers.js";
11
+ import type { CrewParams, AppendEntryFn } from "./types.js";
12
+ import { result } from "./utils/result.js";
13
+ import { ensureAgentsInstalled, ensureSkillsInstalled } from "./utils/install.js";
14
+
15
+ type DeliverFn = (msg: AgentMailMessage) => void;
16
+ type UpdateStatusFn = (ctx: ExtensionContext) => void;
17
+
18
+ /** Ensure both agents and skills are installed */
19
+ function ensureCrewInstalled() {
20
+ ensureAgentsInstalled();
21
+ ensureSkillsInstalled();
22
+ }
23
+
24
+ /**
25
+ * Execute a crew action.
26
+ *
27
+ * Routes action strings like "task.show" to the appropriate handler.
28
+ */
29
+ export async function executeCrewAction(
30
+ action: string,
31
+ params: CrewParams,
32
+ state: MessengerState,
33
+ dirs: Dirs,
34
+ ctx: ExtensionContext,
35
+ deliverMessage: DeliverFn,
36
+ updateStatus: UpdateStatusFn,
37
+ appendEntry: AppendEntryFn
38
+ ) {
39
+ // Parse action: "task.show" → group="task", op="show"
40
+ const dotIndex = action.indexOf('.');
41
+ const group = dotIndex > 0 ? action.slice(0, dotIndex) : action;
42
+ const op = dotIndex > 0 ? action.slice(dotIndex + 1) : null;
43
+
44
+ // ═══════════════════════════════════════════════════════════════════════
45
+ // Actions that DON'T require registration
46
+ // ═══════════════════════════════════════════════════════════════════════
47
+
48
+ // join - this is how you register
49
+ if (group === 'join') {
50
+ return handlers.executeJoin(state, dirs, ctx, deliverMessage, updateStatus, params.spec);
51
+ }
52
+
53
+ // autoRegisterPath - config management, not agent operation
54
+ if (group === 'autoRegisterPath') {
55
+ if (!params.autoRegisterPath) {
56
+ return result("Error: autoRegisterPath requires value ('add', 'remove', or 'list').",
57
+ { mode: "autoRegisterPath", error: "missing_value" });
58
+ }
59
+ return handlers.executeAutoRegisterPath(params.autoRegisterPath);
60
+ }
61
+
62
+ // ═══════════════════════════════════════════════════════════════════════
63
+ // All other actions require registration
64
+ // ═══════════════════════════════════════════════════════════════════════
65
+ if (!state.registered) {
66
+ return handlers.notRegisteredError();
67
+ }
68
+
69
+ switch (group) {
70
+ // ═══════════════════════════════════════════════════════════════════════
71
+ // Coordination actions (delegate to existing handlers)
72
+ // ═══════════════════════════════════════════════════════════════════════
73
+ case 'status': {
74
+ // Check if this is a crew status request
75
+ try {
76
+ const statusHandler = await import("./handlers/status.js");
77
+ return statusHandler.execute(params, state, dirs, ctx);
78
+ } catch {
79
+ // Fall back to messenger status
80
+ return handlers.executeStatus(state, dirs);
81
+ }
82
+ }
83
+
84
+ case 'list':
85
+ return handlers.executeList(state, dirs);
86
+
87
+ case 'spec':
88
+ if (!params.spec) {
89
+ return result("Error: spec path required.", { mode: "spec", error: "missing_spec" });
90
+ }
91
+ return handlers.executeSetSpec(state, dirs, ctx, params.spec);
92
+
93
+ case 'send':
94
+ return handlers.executeSend(state, dirs, params.to, false, params.message, params.replyTo);
95
+
96
+ case 'broadcast':
97
+ return handlers.executeSend(state, dirs, undefined, true, params.message, params.replyTo);
98
+
99
+ case 'reserve':
100
+ if (!params.paths || params.paths.length === 0) {
101
+ return result("Error: paths required for reserve action.", { mode: "reserve", error: "missing_paths" });
102
+ }
103
+ return handlers.executeReserve(state, dirs, ctx, params.paths, params.reason);
104
+
105
+ case 'release':
106
+ return handlers.executeRelease(state, dirs, ctx, params.paths ?? true);
107
+
108
+ case 'rename':
109
+ if (!params.name) {
110
+ return result("Error: name required for rename action.", { mode: "rename", error: "missing_name" });
111
+ }
112
+ return handlers.executeRename(state, dirs, ctx, params.name, deliverMessage, updateStatus);
113
+
114
+ case 'swarm':
115
+ return handlers.executeSwarm(state, dirs, params.spec);
116
+
117
+ case 'claim':
118
+ if (!params.taskId) {
119
+ return result("Error: taskId required for claim action.", { mode: "claim", error: "missing_taskId" });
120
+ }
121
+ return handlers.executeClaim(state, dirs, ctx, params.taskId, params.spec, params.reason);
122
+
123
+ case 'unclaim':
124
+ if (!params.taskId) {
125
+ return result("Error: taskId required for unclaim action.", { mode: "unclaim", error: "missing_taskId" });
126
+ }
127
+ return handlers.executeUnclaim(state, dirs, params.taskId, params.spec);
128
+
129
+ case 'complete':
130
+ if (!params.taskId) {
131
+ return result("Error: taskId required for complete action.", { mode: "complete", error: "missing_taskId" });
132
+ }
133
+ return handlers.executeComplete(state, dirs, params.taskId, params.notes, params.spec);
134
+
135
+ // ═══════════════════════════════════════════════════════════════════════
136
+ // Crew actions - Simplified PRD-based workflow
137
+ // ═══════════════════════════════════════════════════════════════════════
138
+ case 'task': {
139
+ if (!op) {
140
+ return result("Error: task action requires operation (e.g., 'task.show', 'task.list').",
141
+ { mode: "task", error: "missing_operation" });
142
+ }
143
+ try {
144
+ const taskHandlers = await import("./handlers/task.js");
145
+ return taskHandlers.execute(op, params, state, dirs, ctx);
146
+ } catch (e) {
147
+ return result(`Error: task.${op} handler failed: ${e instanceof Error ? e.message : 'unknown'}`,
148
+ { mode: "task", error: "handler_error", operation: op });
149
+ }
150
+ }
151
+
152
+ case 'plan': {
153
+ // Auto-install agents if missing
154
+ ensureCrewInstalled();
155
+ try {
156
+ const planHandler = await import("./handlers/plan.js");
157
+ return planHandler.execute(params, state, dirs, ctx);
158
+ } catch (e) {
159
+ return result(`Error: plan handler failed: ${e instanceof Error ? e.message : 'unknown'}`,
160
+ { mode: "plan", error: "handler_error" });
161
+ }
162
+ }
163
+
164
+ case 'work': {
165
+ // Auto-install agents if missing
166
+ ensureCrewInstalled();
167
+ try {
168
+ const workHandler = await import("./handlers/work.js");
169
+ return workHandler.execute(params, state, dirs, ctx, appendEntry);
170
+ } catch (e) {
171
+ return result(`Error: work handler failed: ${e instanceof Error ? e.message : 'unknown'}`,
172
+ { mode: "work", error: "handler_error" });
173
+ }
174
+ }
175
+
176
+ case 'review': {
177
+ // Auto-install agents if missing
178
+ ensureCrewInstalled();
179
+ try {
180
+ const reviewHandler = await import("./handlers/review.js");
181
+ return reviewHandler.execute(params, state, dirs, ctx);
182
+ } catch (e) {
183
+ return result(`Error: review handler failed: ${e instanceof Error ? e.message : 'unknown'}`,
184
+ { mode: "review", error: "handler_error" });
185
+ }
186
+ }
187
+
188
+ case 'interview': {
189
+ // Auto-install agents if missing
190
+ ensureCrewInstalled();
191
+ try {
192
+ const interviewHandler = await import("./handlers/interview.js");
193
+ return interviewHandler.execute(params, state, dirs, ctx);
194
+ } catch (e) {
195
+ return result(`Error: interview handler failed: ${e instanceof Error ? e.message : 'unknown'}`,
196
+ { mode: "interview", error: "handler_error" });
197
+ }
198
+ }
199
+
200
+ case 'sync': {
201
+ // Auto-install agents if missing
202
+ ensureCrewInstalled();
203
+ try {
204
+ const syncHandler = await import("./handlers/sync.js");
205
+ return syncHandler.execute(params, state, dirs, ctx);
206
+ } catch (e) {
207
+ return result(`Error: sync handler failed: ${e instanceof Error ? e.message : 'unknown'}`,
208
+ { mode: "sync", error: "handler_error" });
209
+ }
210
+ }
211
+
212
+ case 'crew': {
213
+ if (!op) {
214
+ return result("Error: crew action requires operation (e.g., 'crew.status', 'crew.agents').",
215
+ { mode: "crew", error: "missing_operation" });
216
+ }
217
+ try {
218
+ const statusHandlers = await import("./handlers/status.js");
219
+ return statusHandlers.executeCrew(op, params, state, dirs, ctx);
220
+ } catch (e) {
221
+ return result(`Error: crew.${op} handler failed: ${e instanceof Error ? e.message : 'unknown'}`,
222
+ { mode: "crew", error: "handler_error", operation: op });
223
+ }
224
+ }
225
+
226
+ default:
227
+ return result(`Unknown action: ${action}`, { mode: "error", error: "unknown_action", action });
228
+ }
229
+ }
package/crew/state.ts ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Crew - Shared Autonomous State
3
+ *
4
+ * Tracks autonomous mode execution across turns.
5
+ */
6
+
7
+ export interface WaveResult {
8
+ waveNumber: number;
9
+ tasksAttempted: string[];
10
+ succeeded: string[];
11
+ failed: string[];
12
+ blocked: string[];
13
+ timestamp: string;
14
+ }
15
+
16
+ export interface AutonomousState {
17
+ active: boolean;
18
+ cwd: string | null;
19
+ waveNumber: number;
20
+ attemptsPerTask: Record<string, number>;
21
+ waveHistory: WaveResult[];
22
+ startedAt: string | null;
23
+ stoppedAt: string | null;
24
+ stopReason: "completed" | "blocked" | "manual" | null;
25
+ }
26
+
27
+ /**
28
+ * Shared state for autonomous mode.
29
+ * Persisted to session via appendEntry("crew-state", ...) and
30
+ * restored on session_start.
31
+ */
32
+ export const autonomousState: AutonomousState = {
33
+ active: false,
34
+ cwd: null,
35
+ waveNumber: 0,
36
+ attemptsPerTask: {},
37
+ waveHistory: [],
38
+ startedAt: null,
39
+ stoppedAt: null,
40
+ stopReason: null,
41
+ };
42
+
43
+ /**
44
+ * Reset autonomous state.
45
+ */
46
+ export function resetAutonomousState(): void {
47
+ autonomousState.active = false;
48
+ autonomousState.cwd = null;
49
+ autonomousState.waveNumber = 0;
50
+ autonomousState.attemptsPerTask = {};
51
+ autonomousState.waveHistory = [];
52
+ autonomousState.startedAt = null;
53
+ autonomousState.stoppedAt = null;
54
+ autonomousState.stopReason = null;
55
+ }
56
+
57
+ /**
58
+ * Start autonomous mode.
59
+ */
60
+ export function startAutonomous(cwd: string): void {
61
+ autonomousState.active = true;
62
+ autonomousState.cwd = cwd;
63
+ autonomousState.waveNumber = 1;
64
+ autonomousState.attemptsPerTask = {};
65
+ autonomousState.waveHistory = [];
66
+ autonomousState.startedAt = new Date().toISOString();
67
+ autonomousState.stoppedAt = null;
68
+ autonomousState.stopReason = null;
69
+ }
70
+
71
+ /**
72
+ * Stop autonomous mode.
73
+ */
74
+ export function stopAutonomous(reason: "completed" | "blocked" | "manual"): void {
75
+ autonomousState.active = false;
76
+ autonomousState.stoppedAt = new Date().toISOString();
77
+ autonomousState.stopReason = reason;
78
+ }
79
+
80
+ /**
81
+ * Add a wave result to history.
82
+ */
83
+ export function addWaveResult(result: WaveResult): void {
84
+ autonomousState.waveHistory.push(result);
85
+ autonomousState.waveNumber++;
86
+ }
87
+
88
+ /**
89
+ * Restore autonomous state from session data.
90
+ */
91
+ export function restoreAutonomousState(data: Partial<AutonomousState>): void {
92
+ if (data.active !== undefined) autonomousState.active = data.active;
93
+ if (data.cwd !== undefined) autonomousState.cwd = data.cwd;
94
+ if (data.waveNumber !== undefined) autonomousState.waveNumber = data.waveNumber;
95
+ if (data.attemptsPerTask !== undefined) autonomousState.attemptsPerTask = data.attemptsPerTask;
96
+ if (data.waveHistory !== undefined) autonomousState.waveHistory = data.waveHistory;
97
+ if (data.startedAt !== undefined) autonomousState.startedAt = data.startedAt;
98
+ if (data.stoppedAt !== undefined) autonomousState.stoppedAt = data.stoppedAt;
99
+ if (data.stopReason !== undefined) autonomousState.stopReason = data.stopReason;
100
+ }
101
+
102
+ /**
103
+ * Increment attempt count for a task.
104
+ */
105
+ export function incrementTaskAttempt(taskId: string): number {
106
+ const current = autonomousState.attemptsPerTask[taskId] ?? 0;
107
+ autonomousState.attemptsPerTask[taskId] = current + 1;
108
+ return current + 1;
109
+ }
110
+
111
+ /**
112
+ * Get attempt count for a task.
113
+ */
114
+ export function getTaskAttempts(taskId: string): number {
115
+ return autonomousState.attemptsPerTask[taskId] ?? 0;
116
+ }