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