pi-messenger 0.10.0 → 0.11.0
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/CHANGELOG.md +21 -0
- package/README.md +20 -2
- package/crew/agents/crew-worker.md +9 -0
- package/crew/agents.ts +140 -11
- package/crew/handlers/plan.ts +4 -5
- package/crew/handlers/review.ts +11 -9
- package/crew/handlers/work.ts +79 -45
- package/crew/index.ts +5 -4
- package/crew/live-progress.ts +57 -0
- package/crew/state.ts +0 -16
- package/crew/types.ts +6 -0
- package/crew/utils/config.ts +26 -11
- package/crew/utils/progress.ts +4 -1
- package/crew-overlay.ts +51 -14
- package/index.ts +23 -3
- package/overlay.ts +47 -2
- package/package.json +9 -3
- package/vitest.config.ts +10 -0
- package/ARCHITECTURE.md +0 -220
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.0] - 2026-02-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Test suite** — 53 tests across 7 Vitest suites covering store CRUD, state machine, config merging, agent discovery, model resolution, graceful shutdown, and live progress (including cwd isolation). Includes test helpers for temp directories and mock contexts.
|
|
7
|
+
- **Per-agent runtime config** — Model override with 4-level priority: per-task > per-wave `model` param > config `crew.models.worker` > agent `.md` frontmatter. Environment variable override via `crew.work.env` config (not exposed as tool param to prevent API keys in logs).
|
|
8
|
+
- **Graceful shutdown** — `AbortSignal` threaded from tool execute through to spawned workers. On abort: discovers worker name via PID-based registry scan, writes shutdown message to worker inbox, waits 30s grace period, SIGTERM, waits 5s, SIGKILL. Tasks reset to `todo` for retry. Workers instructed to release reservations and exit without committing.
|
|
9
|
+
- **Live crew progress** — In-memory pub/sub store fed by worker JSONL events. Overlay Crew tab shows Active Workers section with tool name, call count, tokens, and elapsed time updating every second. Status bar shows active worker count during autonomous mode (`🔨N`).
|
|
10
|
+
- **`shutdownGracePeriodMs` config** — Configurable grace period before SIGTERM (default: 30000).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **Dynamic overlay height** — Content area scales with terminal size (8-25 lines) instead of hardcoded 10. On a standard 24-row terminal, visible content goes from 10 to 15 lines.
|
|
14
|
+
- **Handler signatures simplified** — Removed unused `state` and `dirs` parameters from plan and review handlers.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **`deepMerge` crash** — Merging config with `models` key crashed when the target didn't have the key. Hardened for undefined target keys.
|
|
18
|
+
- **Result/task association** — Worker results now matched by `taskId` field instead of array index, since `spawnAgents` returns in completion order not submission order.
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
- `attemptsPerTask` field from `AutonomousState` — declared but never populated by any code.
|
|
22
|
+
- `ARCHITECTURE.md` from repo (moved to external docs).
|
|
23
|
+
|
|
3
24
|
## [0.10.0] - 2026-02-05
|
|
4
25
|
|
|
5
26
|
### Fixed
|
package/README.md
CHANGED
|
@@ -122,13 +122,31 @@ Add to `~/.pi/agent/pi-messenger.json`:
|
|
|
122
122
|
{
|
|
123
123
|
"crew": {
|
|
124
124
|
"concurrency": { "workers": 2 },
|
|
125
|
+
"models": { "worker": "claude-sonnet-4-20250514" },
|
|
125
126
|
"review": { "enabled": true, "maxIterations": 3 },
|
|
126
127
|
"planning": { "maxPasses": 3 },
|
|
127
|
-
"work": {
|
|
128
|
+
"work": {
|
|
129
|
+
"maxAttemptsPerTask": 5,
|
|
130
|
+
"maxWaves": 50,
|
|
131
|
+
"shutdownGracePeriodMs": 30000,
|
|
132
|
+
"env": { "NODE_ENV": "test" }
|
|
133
|
+
}
|
|
128
134
|
}
|
|
129
135
|
}
|
|
130
136
|
```
|
|
131
137
|
|
|
138
|
+
| Setting | Description | Default |
|
|
139
|
+
|---------|-------------|---------|
|
|
140
|
+
| `concurrency.workers` | Max parallel workers per wave | `2` |
|
|
141
|
+
| `models.worker` | Model for spawned workers (overridden by per-task or per-wave `model` param) | agent `.md` frontmatter |
|
|
142
|
+
| `review.enabled` | Auto-review after task completion | `true` |
|
|
143
|
+
| `review.maxIterations` | Max review/fix cycles per task | `3` |
|
|
144
|
+
| `planning.maxPasses` | Max planner/reviewer refinement passes | `3` |
|
|
145
|
+
| `work.maxAttemptsPerTask` | Auto-block after N failures | `5` |
|
|
146
|
+
| `work.maxWaves` | Max autonomous waves | `50` |
|
|
147
|
+
| `work.shutdownGracePeriodMs` | Grace period before SIGTERM on abort | `30000` |
|
|
148
|
+
| `work.env` | Environment variables passed to spawned workers | `{}` |
|
|
149
|
+
|
|
132
150
|
Crew agents (planner, worker, reviewer, interview-generator, plan-sync) are **auto-installed** on first use. Run `npx pi-messenger --crew-install` to manually install or update.
|
|
133
151
|
|
|
134
152
|
## API Reference
|
|
@@ -216,7 +234,7 @@ Pi-messenger is a [pi extension](https://github.com/badlogic/pi-mono) that hooks
|
|
|
216
234
|
|
|
217
235
|
Incoming messages wake the receiving agent via `pi.sendMessage()` with `triggerTurn: true` and `deliverAs: "steer"`, which injects the message as a steering prompt that resumes the agent. File reservations are enforced by returning `{ block: true }` from a `tool_call` hook on write/edit operations. The `/messenger` overlay uses `ctx.ui.custom()` for the chat TUI, and `ctx.ui.setStatus()` keeps the status bar updated with peer count and unread messages.
|
|
218
236
|
|
|
219
|
-
Crew workers are spawned as `pi --mode json` subprocesses with the agent's system prompt, model, and tool restrictions from their `.md` definitions. Progress is tracked via JSONL streaming. The planner and reviewer work the same way — just pi instances with different agent configs.
|
|
237
|
+
Crew workers are spawned as `pi --mode json` subprocesses with the agent's system prompt, model, and tool restrictions from their `.md` definitions. Progress is tracked via JSONL streaming — the overlay subscribes to a live progress store that shows each worker's current tool, call count, and token usage in real time. Aborting a work run triggers graceful shutdown: each worker receives an inbox message asking it to stop, followed by a grace period before SIGTERM. The planner and reviewer work the same way — just pi instances with different agent configs.
|
|
220
238
|
|
|
221
239
|
All coordination is file-based, no daemon required. Global state (registry, inboxes, activity feed) lives in `~/.pi/agent/messenger/`. Per-project crew data (plan, tasks, artifacts) lives in `.pi/messenger/crew/` inside your project. Dead agents are detected via PID checks and cleaned up automatically.
|
|
222
240
|
|
|
@@ -85,6 +85,15 @@ pi_messenger({
|
|
|
85
85
|
})
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
## Shutdown Handling
|
|
89
|
+
|
|
90
|
+
If you receive a message saying "SHUTDOWN REQUESTED":
|
|
91
|
+
1. Stop what you're doing
|
|
92
|
+
2. Release reservations: `pi_messenger({ action: "release" })`
|
|
93
|
+
3. Do NOT mark the task as done — leave it as in_progress for retry
|
|
94
|
+
4. Do NOT commit anything
|
|
95
|
+
5. Exit immediately
|
|
96
|
+
|
|
88
97
|
## Important Rules
|
|
89
98
|
|
|
90
99
|
- ALWAYS join first, before any other pi_messenger calls
|
package/crew/agents.ts
CHANGED
|
@@ -11,13 +11,14 @@ import * as os from "node:os";
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { discoverCrewAgents, type CrewAgentConfig } from "./utils/discover.js";
|
|
14
|
-
import { truncateOutput
|
|
14
|
+
import { truncateOutput } from "./utils/truncate.js";
|
|
15
15
|
import {
|
|
16
16
|
createProgress,
|
|
17
17
|
parseJsonlLine,
|
|
18
18
|
updateProgress,
|
|
19
19
|
getFinalOutput,
|
|
20
|
-
type AgentProgress
|
|
20
|
+
type AgentProgress,
|
|
21
|
+
type PiEvent,
|
|
21
22
|
} from "./utils/progress.js";
|
|
22
23
|
import {
|
|
23
24
|
getArtifactPaths,
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
appendJsonl
|
|
28
29
|
} from "./utils/artifacts.js";
|
|
29
30
|
import { loadCrewConfig, getTruncationForRole, type CrewConfig } from "./utils/config.js";
|
|
31
|
+
import { removeLiveWorker, updateLiveWorker } from "./live-progress.js";
|
|
30
32
|
import type { AgentTask, AgentResult } from "./types.js";
|
|
31
33
|
|
|
32
34
|
// Extension directory (parent of crew/) - passed to subagents so they can use pi_messenger
|
|
@@ -39,8 +41,55 @@ export interface SpawnOptions {
|
|
|
39
41
|
onProgress?: (results: AgentResult[]) => void;
|
|
40
42
|
crewDir?: string;
|
|
41
43
|
signal?: AbortSignal;
|
|
44
|
+
messengerDirs?: { registry: string; inbox: string };
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
export function resolveModel(
|
|
48
|
+
taskModel?: string,
|
|
49
|
+
paramModel?: string,
|
|
50
|
+
configModel?: string,
|
|
51
|
+
agentModel?: string,
|
|
52
|
+
): string | undefined {
|
|
53
|
+
return taskModel ?? paramModel ?? configModel ?? agentModel;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function raceTimeout(promise: Promise<void>, ms: number): Promise<boolean> {
|
|
57
|
+
return new Promise<boolean>(resolve => {
|
|
58
|
+
const timer = setTimeout(() => resolve(false), ms);
|
|
59
|
+
promise.then(
|
|
60
|
+
() => {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
resolve(true);
|
|
63
|
+
},
|
|
64
|
+
() => {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
resolve(false);
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function discoverWorkerName(
|
|
73
|
+
pid: number | undefined,
|
|
74
|
+
registryDir: string | undefined
|
|
75
|
+
): string | null {
|
|
76
|
+
if (!pid || !registryDir || !fs.existsSync(registryDir)) return null;
|
|
77
|
+
try {
|
|
78
|
+
for (const file of fs.readdirSync(registryDir)) {
|
|
79
|
+
if (!file.endsWith(".json")) continue;
|
|
80
|
+
const reg = JSON.parse(fs.readFileSync(path.join(registryDir, file), "utf-8"));
|
|
81
|
+
if (reg.pid === pid) return reg.name;
|
|
82
|
+
}
|
|
83
|
+
} catch {}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const SHUTDOWN_MESSAGE = `⚠️ SHUTDOWN REQUESTED: Please wrap up your current work.
|
|
88
|
+
1. Release any file reservations
|
|
89
|
+
2. If the task is not complete, leave it as in_progress (do NOT mark done)
|
|
90
|
+
3. Do NOT commit anything
|
|
91
|
+
4. Exit`;
|
|
92
|
+
|
|
44
93
|
/**
|
|
45
94
|
* Spawn multiple agents in parallel with concurrency limit.
|
|
46
95
|
*/
|
|
@@ -66,7 +115,10 @@ export async function spawnAgents(
|
|
|
66
115
|
const running: Promise<void>[] = [];
|
|
67
116
|
|
|
68
117
|
while (queue.length > 0 || running.length > 0) {
|
|
118
|
+
if (options.signal?.aborted && running.length === 0) break;
|
|
119
|
+
|
|
69
120
|
while (running.length < concurrency && queue.length > 0) {
|
|
121
|
+
if (options.signal?.aborted) break;
|
|
70
122
|
const { task, index } = queue.shift()!;
|
|
71
123
|
const promise = runAgent(task, index, cwd, agents, config, runId, artifactsDir, options)
|
|
72
124
|
.then(result => {
|
|
@@ -78,6 +130,7 @@ export async function spawnAgents(
|
|
|
78
130
|
}
|
|
79
131
|
if (running.length > 0) {
|
|
80
132
|
await Promise.race(running);
|
|
133
|
+
if (options.signal?.aborted) continue;
|
|
81
134
|
}
|
|
82
135
|
}
|
|
83
136
|
|
|
@@ -117,7 +170,8 @@ async function runAgent(
|
|
|
117
170
|
return new Promise((resolve) => {
|
|
118
171
|
// Build args for pi command
|
|
119
172
|
const args = ["--mode", "json", "--no-session", "-p"];
|
|
120
|
-
|
|
173
|
+
const model = task.modelOverride ?? agentConfig?.model;
|
|
174
|
+
if (model) args.push("--model", model);
|
|
121
175
|
|
|
122
176
|
if (agentConfig?.tools?.length) {
|
|
123
177
|
const builtinTools: string[] = [];
|
|
@@ -151,10 +205,21 @@ async function runAgent(
|
|
|
151
205
|
|
|
152
206
|
args.push(task.task);
|
|
153
207
|
|
|
154
|
-
const
|
|
208
|
+
const envOverrides = config.work.env ?? {};
|
|
209
|
+
const env = Object.keys(envOverrides).length > 0
|
|
210
|
+
? { ...process.env, ...envOverrides }
|
|
211
|
+
: undefined;
|
|
212
|
+
|
|
213
|
+
const proc = spawn("pi", args, {
|
|
214
|
+
cwd,
|
|
215
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
216
|
+
...(env ? { env } : {}),
|
|
217
|
+
});
|
|
218
|
+
let gracefulShutdownRequested = false;
|
|
219
|
+
let discoveredWorkerName: string | null = null;
|
|
155
220
|
|
|
156
221
|
let jsonlBuffer = "";
|
|
157
|
-
const events:
|
|
222
|
+
const events: PiEvent[] = [];
|
|
158
223
|
|
|
159
224
|
proc.stdout?.on("data", (data) => {
|
|
160
225
|
jsonlBuffer += data.toString();
|
|
@@ -167,6 +232,17 @@ async function runAgent(
|
|
|
167
232
|
events.push(event);
|
|
168
233
|
updateProgress(progress, event, startTime);
|
|
169
234
|
if (artifactPaths) appendJsonl(artifactPaths.jsonlPath, line);
|
|
235
|
+
if (task.taskId) {
|
|
236
|
+
updateLiveWorker(cwd, task.taskId, {
|
|
237
|
+
taskId: task.taskId,
|
|
238
|
+
agent: task.agent,
|
|
239
|
+
progress: {
|
|
240
|
+
...progress,
|
|
241
|
+
recentTools: progress.recentTools.map(tool => ({ ...tool })),
|
|
242
|
+
},
|
|
243
|
+
startedAt: startTime,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
170
246
|
}
|
|
171
247
|
}
|
|
172
248
|
});
|
|
@@ -175,12 +251,13 @@ async function runAgent(
|
|
|
175
251
|
proc.stderr?.on("data", (data) => { stderr += data.toString(); });
|
|
176
252
|
|
|
177
253
|
proc.on("close", (code) => {
|
|
254
|
+
if (task.taskId) removeLiveWorker(cwd, task.taskId);
|
|
178
255
|
progress.status = code === 0 ? "completed" : "failed";
|
|
179
256
|
progress.durationMs = Date.now() - startTime;
|
|
180
257
|
if (stderr && code !== 0) progress.error = stderr;
|
|
181
258
|
|
|
182
259
|
// Get final output from events
|
|
183
|
-
const fullOutput = getFinalOutput(events
|
|
260
|
+
const fullOutput = getFinalOutput(events);
|
|
184
261
|
const truncation = truncateOutput(fullOutput, maxOutput, artifactPaths?.outputPath);
|
|
185
262
|
|
|
186
263
|
// Write output artifact (untruncated)
|
|
@@ -209,6 +286,8 @@ async function runAgent(
|
|
|
209
286
|
truncated: truncation.truncated,
|
|
210
287
|
progress,
|
|
211
288
|
config: agentConfig,
|
|
289
|
+
taskId: task.taskId,
|
|
290
|
+
wasGracefullyShutdown: gracefulShutdownRequested,
|
|
212
291
|
error: progress.error,
|
|
213
292
|
artifactPaths: artifactPaths ? {
|
|
214
293
|
input: artifactPaths.inputPath,
|
|
@@ -217,16 +296,66 @@ async function runAgent(
|
|
|
217
296
|
metadata: artifactPaths.metadataPath,
|
|
218
297
|
} : undefined,
|
|
219
298
|
});
|
|
299
|
+
|
|
300
|
+
if (gracefulShutdownRequested && discoveredWorkerName && options.messengerDirs?.registry) {
|
|
301
|
+
try {
|
|
302
|
+
fs.unlinkSync(path.join(options.messengerDirs.registry, `${discoveredWorkerName}.json`));
|
|
303
|
+
} catch {}
|
|
304
|
+
}
|
|
220
305
|
});
|
|
221
306
|
|
|
222
307
|
// Handle abort signal
|
|
223
308
|
if (options.signal) {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
309
|
+
const gracefulShutdown = async () => {
|
|
310
|
+
gracefulShutdownRequested = true;
|
|
311
|
+
|
|
312
|
+
let messageSent = false;
|
|
313
|
+
discoveredWorkerName = discoverWorkerName(proc.pid, options.messengerDirs?.registry);
|
|
314
|
+
if (discoveredWorkerName && options.messengerDirs) {
|
|
315
|
+
try {
|
|
316
|
+
const inboxDir = path.join(options.messengerDirs.inbox, discoveredWorkerName);
|
|
317
|
+
if (fs.existsSync(inboxDir)) {
|
|
318
|
+
const msgFile = path.join(inboxDir, `${Date.now()}-shutdown.json`);
|
|
319
|
+
fs.writeFileSync(msgFile, JSON.stringify({
|
|
320
|
+
id: randomUUID(),
|
|
321
|
+
from: "crew-orchestrator",
|
|
322
|
+
to: discoveredWorkerName,
|
|
323
|
+
text: SHUTDOWN_MESSAGE,
|
|
324
|
+
timestamp: new Date().toISOString(),
|
|
325
|
+
replyTo: null,
|
|
326
|
+
}));
|
|
327
|
+
messageSent = true;
|
|
328
|
+
}
|
|
329
|
+
} catch {}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (messageSent) {
|
|
333
|
+
const graceMs = config.work.shutdownGracePeriodMs ?? 30000;
|
|
334
|
+
const exitPromise = new Promise<void>(r => proc.once("exit", () => r()));
|
|
335
|
+
const exited = await raceTimeout(exitPromise, graceMs);
|
|
336
|
+
if (exited) return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!proc.killed && proc.exitCode === null) {
|
|
340
|
+
proc.kill("SIGTERM");
|
|
341
|
+
const termPromise = new Promise<void>(r => proc.once("exit", () => r()));
|
|
342
|
+
const killed = await raceTimeout(termPromise, 5000);
|
|
343
|
+
if (killed) return;
|
|
344
|
+
} else {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!proc.killed && proc.exitCode === null) {
|
|
349
|
+
proc.kill("SIGKILL");
|
|
350
|
+
}
|
|
227
351
|
};
|
|
228
|
-
if (options.signal.aborted)
|
|
229
|
-
|
|
352
|
+
if (options.signal.aborted) {
|
|
353
|
+
gracefulShutdown().catch(() => {});
|
|
354
|
+
} else {
|
|
355
|
+
options.signal.addEventListener("abort", () => {
|
|
356
|
+
gracefulShutdown().catch(() => {});
|
|
357
|
+
}, { once: true });
|
|
358
|
+
}
|
|
230
359
|
}
|
|
231
360
|
});
|
|
232
361
|
}
|
package/crew/handlers/plan.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
11
|
-
import type { MessengerState, Dirs } from "../../lib.js";
|
|
12
11
|
import type { CrewParams } from "../types.js";
|
|
13
12
|
import { result } from "../utils/result.js";
|
|
14
13
|
import { spawnAgents } from "../agents.js";
|
|
@@ -108,8 +107,6 @@ function appendReviewToProgress(
|
|
|
108
107
|
|
|
109
108
|
export async function execute(
|
|
110
109
|
params: CrewParams,
|
|
111
|
-
_state: MessengerState,
|
|
112
|
-
_dirs: Dirs,
|
|
113
110
|
ctx: ExtensionContext
|
|
114
111
|
) {
|
|
115
112
|
const cwd = ctx.cwd ?? process.cwd();
|
|
@@ -185,7 +182,8 @@ export async function execute(
|
|
|
185
182
|
|
|
186
183
|
const [plannerResult] = await spawnAgents([{
|
|
187
184
|
agent: PLANNER_AGENT,
|
|
188
|
-
task: plannerPrompt
|
|
185
|
+
task: plannerPrompt,
|
|
186
|
+
modelOverride: config.models?.planner,
|
|
189
187
|
}], 1, cwd);
|
|
190
188
|
|
|
191
189
|
if (plannerResult.exitCode !== 0) {
|
|
@@ -219,7 +217,8 @@ export async function execute(
|
|
|
219
217
|
|
|
220
218
|
const [reviewResult] = await spawnAgents([{
|
|
221
219
|
agent: "crew-reviewer",
|
|
222
|
-
task: reviewPrompt
|
|
220
|
+
task: reviewPrompt,
|
|
221
|
+
modelOverride: config.models?.reviewer,
|
|
223
222
|
}], 1, cwd);
|
|
224
223
|
|
|
225
224
|
if (reviewResult.exitCode !== 0) {
|
package/crew/handlers/review.ts
CHANGED
|
@@ -7,22 +7,22 @@
|
|
|
7
7
|
|
|
8
8
|
import { execSync } from "node:child_process";
|
|
9
9
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
10
|
-
import type { MessengerState, Dirs } from "../../lib.js";
|
|
11
10
|
import type { CrewParams } from "../types.js";
|
|
12
11
|
import { result } from "../utils/result.js";
|
|
13
12
|
import { spawnAgents } from "../agents.js";
|
|
14
13
|
import { discoverCrewAgents } from "../utils/discover.js";
|
|
14
|
+
import { loadCrewConfig } from "../utils/config.js";
|
|
15
15
|
import { parseVerdict, type ParsedReview } from "../utils/verdict.js";
|
|
16
16
|
import * as store from "../store.js";
|
|
17
17
|
|
|
18
18
|
export async function execute(
|
|
19
19
|
params: CrewParams,
|
|
20
|
-
_state: MessengerState,
|
|
21
|
-
_dirs: Dirs,
|
|
22
20
|
ctx: ExtensionContext
|
|
23
21
|
) {
|
|
24
22
|
const cwd = ctx.cwd ?? process.cwd();
|
|
25
23
|
const { target, type } = params;
|
|
24
|
+
const config = loadCrewConfig(store.getCrewDir(cwd));
|
|
25
|
+
const reviewerModel = config.models?.reviewer;
|
|
26
26
|
|
|
27
27
|
if (!target) {
|
|
28
28
|
return result("Error: target (task ID) required for review action.\n\nUsage: pi_messenger({ action: \"review\", target: \"task-1\" })", {
|
|
@@ -45,9 +45,9 @@ export async function execute(
|
|
|
45
45
|
const reviewType = type ?? (target.startsWith("task-") ? "impl" : "plan");
|
|
46
46
|
|
|
47
47
|
if (reviewType === "impl") {
|
|
48
|
-
return reviewImplementation(cwd, target);
|
|
48
|
+
return reviewImplementation(cwd, target, reviewerModel);
|
|
49
49
|
} else {
|
|
50
|
-
return reviewPlan(cwd);
|
|
50
|
+
return reviewPlan(cwd, reviewerModel);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -55,7 +55,7 @@ export async function execute(
|
|
|
55
55
|
// Implementation Review
|
|
56
56
|
// =============================================================================
|
|
57
57
|
|
|
58
|
-
async function reviewImplementation(cwd: string, taskId: string) {
|
|
58
|
+
async function reviewImplementation(cwd: string, taskId: string, modelOverride?: string) {
|
|
59
59
|
const task = store.getTask(cwd, taskId);
|
|
60
60
|
if (!task) {
|
|
61
61
|
return result(`Error: Task ${taskId} not found.`, {
|
|
@@ -120,7 +120,8 @@ Output your verdict as SHIP, NEEDS_WORK, or MAJOR_RETHINK with detailed feedback
|
|
|
120
120
|
// Spawn reviewer
|
|
121
121
|
const [reviewResult] = await spawnAgents([{
|
|
122
122
|
agent: "crew-reviewer",
|
|
123
|
-
task: prompt
|
|
123
|
+
task: prompt,
|
|
124
|
+
modelOverride,
|
|
124
125
|
}], 1, cwd);
|
|
125
126
|
|
|
126
127
|
if (reviewResult.exitCode !== 0) {
|
|
@@ -170,7 +171,7 @@ ${verdict.verdict === "SHIP" ? "✅ Ready to merge!" : verdict.verdict === "NEED
|
|
|
170
171
|
// Plan Review
|
|
171
172
|
// =============================================================================
|
|
172
173
|
|
|
173
|
-
async function reviewPlan(cwd: string) {
|
|
174
|
+
async function reviewPlan(cwd: string, modelOverride?: string) {
|
|
174
175
|
const plan = store.getPlan(cwd);
|
|
175
176
|
if (!plan) {
|
|
176
177
|
return result("Error: No plan found.", {
|
|
@@ -224,7 +225,8 @@ Output your verdict as SHIP (plan is solid), NEEDS_WORK (minor adjustments), or
|
|
|
224
225
|
// Spawn reviewer
|
|
225
226
|
const [reviewResult] = await spawnAgents([{
|
|
226
227
|
agent: "crew-reviewer",
|
|
227
|
-
task: prompt
|
|
228
|
+
task: prompt,
|
|
229
|
+
modelOverride,
|
|
228
230
|
}], 1, cwd);
|
|
229
231
|
|
|
230
232
|
if (reviewResult.exitCode !== 0) {
|
package/crew/handlers/work.ts
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import type {
|
|
9
|
+
import type { Dirs } from "../../lib.js";
|
|
10
10
|
import type { CrewParams, AppendEntryFn, Task } from "../types.js";
|
|
11
11
|
import { result } from "../utils/result.js";
|
|
12
|
-
import { spawnAgents } from "../agents.js";
|
|
12
|
+
import { resolveModel, spawnAgents } from "../agents.js";
|
|
13
13
|
import { loadCrewConfig } from "../utils/config.js";
|
|
14
14
|
import { discoverCrewAgents } from "../utils/discover.js";
|
|
15
15
|
import * as store from "../store.js";
|
|
@@ -18,10 +18,10 @@ import { autonomousState, startAutonomous, stopAutonomous, addWaveResult } from
|
|
|
18
18
|
|
|
19
19
|
export async function execute(
|
|
20
20
|
params: CrewParams,
|
|
21
|
-
|
|
22
|
-
_dirs: Dirs,
|
|
21
|
+
dirs: Dirs,
|
|
23
22
|
ctx: ExtensionContext,
|
|
24
|
-
appendEntry: AppendEntryFn
|
|
23
|
+
appendEntry: AppendEntryFn,
|
|
24
|
+
signal?: AbortSignal
|
|
25
25
|
) {
|
|
26
26
|
const cwd = ctx.cwd ?? process.cwd();
|
|
27
27
|
const config = loadCrewConfig(getCrewDir(cwd));
|
|
@@ -87,15 +87,30 @@ export async function execute(
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// Spawn workers
|
|
90
|
-
const workerTasks = tasksToRun.map(task =>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
const workerTasks = tasksToRun.map(task => {
|
|
91
|
+
const taskModel = resolveModel(
|
|
92
|
+
task.model,
|
|
93
|
+
params.model,
|
|
94
|
+
config.models?.worker,
|
|
95
|
+
undefined,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
agent: "crew-worker",
|
|
100
|
+
task: buildWorkerPrompt(task, plan.prd, cwd),
|
|
101
|
+
taskId: task.id,
|
|
102
|
+
modelOverride: taskModel,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
94
105
|
|
|
95
106
|
const workerResults = await spawnAgents(
|
|
96
107
|
workerTasks,
|
|
97
108
|
concurrency,
|
|
98
|
-
cwd
|
|
109
|
+
cwd,
|
|
110
|
+
{
|
|
111
|
+
signal,
|
|
112
|
+
messengerDirs: { registry: dirs.registry, inbox: dirs.inbox },
|
|
113
|
+
}
|
|
99
114
|
);
|
|
100
115
|
|
|
101
116
|
// Process results
|
|
@@ -105,22 +120,33 @@ export async function execute(
|
|
|
105
120
|
|
|
106
121
|
for (let i = 0; i < workerResults.length; i++) {
|
|
107
122
|
const r = workerResults[i];
|
|
108
|
-
const taskId =
|
|
123
|
+
const taskId = r.taskId;
|
|
124
|
+
if (!taskId) {
|
|
125
|
+
failed.push(`unknown-result-${i}`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
109
128
|
const task = store.getTask(cwd, taskId);
|
|
110
129
|
|
|
111
130
|
if (r.exitCode === 0) {
|
|
112
|
-
// Check if task was completed (worker should call task.done)
|
|
113
131
|
if (task?.status === "done") {
|
|
114
132
|
succeeded.push(taskId);
|
|
115
133
|
} else if (task?.status === "blocked") {
|
|
116
134
|
blocked.push(taskId);
|
|
135
|
+
} else if (r.wasGracefullyShutdown && task?.status === "in_progress") {
|
|
136
|
+
store.updateTask(cwd, taskId, { status: "todo", assigned_to: undefined });
|
|
117
137
|
} else {
|
|
118
|
-
// Worker finished but didn't complete - treat as failure
|
|
119
138
|
failed.push(taskId);
|
|
120
139
|
}
|
|
121
140
|
} else {
|
|
122
|
-
|
|
123
|
-
|
|
141
|
+
if (r.wasGracefullyShutdown) {
|
|
142
|
+
if (task?.status === "done") {
|
|
143
|
+
succeeded.push(taskId);
|
|
144
|
+
} else if (task?.status === "blocked") {
|
|
145
|
+
blocked.push(taskId);
|
|
146
|
+
} else if (task?.status === "in_progress") {
|
|
147
|
+
store.updateTask(cwd, taskId, { status: "todo", assigned_to: undefined });
|
|
148
|
+
}
|
|
149
|
+
} else if (autonomous && task?.status === "in_progress") {
|
|
124
150
|
store.blockTask(cwd, taskId, `Worker failed: ${r.error ?? "Unknown error"}`);
|
|
125
151
|
blocked.push(taskId);
|
|
126
152
|
} else {
|
|
@@ -142,37 +168,40 @@ export async function execute(
|
|
|
142
168
|
timestamp: new Date().toISOString()
|
|
143
169
|
});
|
|
144
170
|
|
|
145
|
-
|
|
146
|
-
|
|
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");
|
|
171
|
+
if (signal?.aborted) {
|
|
172
|
+
stopAutonomous("manual");
|
|
153
173
|
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
174
|
} else {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
const nextReady = store.getReadyTasks(cwd);
|
|
176
|
+
const allTasks = store.getTasks(cwd);
|
|
177
|
+
const allDone = allTasks.every(t => t.status === "done");
|
|
178
|
+
const allBlockedOrDone = allTasks.every(t => t.status === "done" || t.status === "blocked");
|
|
179
|
+
|
|
180
|
+
if (allDone) {
|
|
181
|
+
stopAutonomous("completed");
|
|
182
|
+
appendEntry("crew-state", autonomousState);
|
|
183
|
+
appendEntry("crew_wave_complete", {
|
|
184
|
+
prd: plan.prd,
|
|
185
|
+
status: "completed",
|
|
186
|
+
totalWaves: currentWave,
|
|
187
|
+
totalTasks: allTasks.length
|
|
188
|
+
});
|
|
189
|
+
} else if (allBlockedOrDone || nextReady.length === 0) {
|
|
190
|
+
stopAutonomous("blocked");
|
|
191
|
+
appendEntry("crew-state", autonomousState);
|
|
192
|
+
appendEntry("crew_wave_blocked", {
|
|
193
|
+
prd: plan.prd,
|
|
194
|
+
status: "blocked",
|
|
195
|
+
blockedTasks: allTasks.filter(t => t.status === "blocked").map(t => t.id)
|
|
196
|
+
});
|
|
197
|
+
} else {
|
|
198
|
+
appendEntry("crew-state", autonomousState);
|
|
199
|
+
appendEntry("crew_wave_continue", {
|
|
200
|
+
prd: plan.prd,
|
|
201
|
+
nextWave: autonomousState.waveNumber,
|
|
202
|
+
readyTasks: nextReady.map(t => t.id)
|
|
203
|
+
});
|
|
204
|
+
}
|
|
176
205
|
}
|
|
177
206
|
}
|
|
178
207
|
|
|
@@ -191,6 +220,11 @@ export async function execute(
|
|
|
191
220
|
const nextText = nextReady.length > 0
|
|
192
221
|
? `\n\n**Ready for next wave:** ${nextReady.map(t => t.id).join(", ")}`
|
|
193
222
|
: "";
|
|
223
|
+
const continueText = autonomous && !signal?.aborted && nextReady.length > 0
|
|
224
|
+
? "Autonomous mode: Continuing to next wave..."
|
|
225
|
+
: signal?.aborted && autonomous
|
|
226
|
+
? "Autonomous mode stopped (cancelled)."
|
|
227
|
+
: "";
|
|
194
228
|
|
|
195
229
|
const text = `# Work Wave ${currentWave}
|
|
196
230
|
|
|
@@ -199,7 +233,7 @@ export async function execute(
|
|
|
199
233
|
**Progress:** ${progress}
|
|
200
234
|
${statusText}${nextText}
|
|
201
235
|
|
|
202
|
-
${
|
|
236
|
+
${continueText}`;
|
|
203
237
|
|
|
204
238
|
return result(text, {
|
|
205
239
|
mode: "work",
|
package/crew/index.ts
CHANGED
|
@@ -42,7 +42,8 @@ export async function executeCrewAction(
|
|
|
42
42
|
deliverMessage: DeliverFn,
|
|
43
43
|
updateStatus: UpdateStatusFn,
|
|
44
44
|
appendEntry: AppendEntryFn,
|
|
45
|
-
config?: CrewActionConfig
|
|
45
|
+
config?: CrewActionConfig,
|
|
46
|
+
signal?: AbortSignal
|
|
46
47
|
) {
|
|
47
48
|
// Parse action: "task.show" → group="task", op="show"
|
|
48
49
|
const dotIndex = action.indexOf('.');
|
|
@@ -177,7 +178,7 @@ export async function executeCrewAction(
|
|
|
177
178
|
ensureCrewInstalled();
|
|
178
179
|
try {
|
|
179
180
|
const planHandler = await import("./handlers/plan.js");
|
|
180
|
-
return planHandler.execute(params,
|
|
181
|
+
return planHandler.execute(params, ctx);
|
|
181
182
|
} catch (e) {
|
|
182
183
|
return result(`Error: plan handler failed: ${e instanceof Error ? e.message : 'unknown'}`,
|
|
183
184
|
{ mode: "plan", error: "handler_error" });
|
|
@@ -189,7 +190,7 @@ export async function executeCrewAction(
|
|
|
189
190
|
ensureCrewInstalled();
|
|
190
191
|
try {
|
|
191
192
|
const workHandler = await import("./handlers/work.js");
|
|
192
|
-
return workHandler.execute(params,
|
|
193
|
+
return workHandler.execute(params, dirs, ctx, appendEntry, signal);
|
|
193
194
|
} catch (e) {
|
|
194
195
|
return result(`Error: work handler failed: ${e instanceof Error ? e.message : 'unknown'}`,
|
|
195
196
|
{ mode: "work", error: "handler_error" });
|
|
@@ -201,7 +202,7 @@ export async function executeCrewAction(
|
|
|
201
202
|
ensureCrewInstalled();
|
|
202
203
|
try {
|
|
203
204
|
const reviewHandler = await import("./handlers/review.js");
|
|
204
|
-
return reviewHandler.execute(params,
|
|
205
|
+
return reviewHandler.execute(params, ctx);
|
|
205
206
|
} catch (e) {
|
|
206
207
|
return result(`Error: review handler failed: ${e instanceof Error ? e.message : 'unknown'}`,
|
|
207
208
|
{ mode: "review", error: "handler_error" });
|