pi-messenger 0.13.0 → 0.13.1
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 +5 -0
- package/README.md +5 -3
- package/crew/handlers/review.ts +1 -1
- package/crew/handlers/work.ts +43 -1
- package/crew/types.ts +1 -0
- package/feed.ts +3 -0
- package/overlay-coordinator.ts +150 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.1] - 2026-03-14
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Auto-review after task completion** — Workers' completed tasks now get an automatic reviewer pass before being counted as done. Controlled by existing `config.review.enabled` (default: true) and `config.review.maxIterations` (default: 3). SHIP keeps the task done, NEEDS_WORK resets it to todo for retry with review feedback injected into the next worker's prompt, MAJOR_RETHINK blocks the task. Reviews run sequentially between worker completion and wave result reporting, and respect the abort signal. Adds `review_count` to the Task interface and `task.review` to the activity feed.
|
|
7
|
+
|
|
3
8
|
## [0.13.0] - 2026-03-02
|
|
4
9
|
|
|
5
10
|
### Added
|
package/README.md
CHANGED
|
@@ -92,11 +92,13 @@ Chat input supports `@Name msg` for DMs and `@all msg` for broadcasts. Text with
|
|
|
92
92
|
|
|
93
93
|
Crew turns a PRD into a dependency graph of tasks, then executes them in parallel waves.
|
|
94
94
|
|
|
95
|
+
Crew logs are per project, under that project's working directory: `.pi/messenger/crew/`. For example, if you run Crew from `/path/to/my-app`, the planner log lives at `/path/to/my-app/.pi/messenger/crew/planning-progress.md`.
|
|
96
|
+
|
|
95
97
|
### Workflow
|
|
96
98
|
|
|
97
99
|
1. **Plan** — Planner explores the codebase and PRD, drafts tasks with dependencies. A reviewer checks the plan; the planner refines until SHIP or `maxPasses` is reached. History is stored in `planning-progress.md`.
|
|
98
|
-
2. **Work** — Workers implement ready tasks (all dependencies met) in parallel waves. A single `work` call runs one wave. `autonomous: true` runs waves back-to-back until everything is done or blocked.
|
|
99
|
-
3. **Review** —
|
|
100
|
+
2. **Work** — Workers implement ready tasks (all dependencies met) in parallel waves. A single `work` call runs one wave. `autonomous: true` runs waves back-to-back until everything is done or blocked. Each completed task gets an automatic reviewer pass — SHIP keeps it done, NEEDS_WORK resets it for retry with feedback, MAJOR_RETHINK blocks it. Controlled by `review.enabled` and `review.maxIterations`.
|
|
101
|
+
3. **Review** — Manual review of a specific task or the plan: `pi_messenger({ action: "review", target: "task-1" })`. Returns SHIP, NEEDS_WORK, or MAJOR_RETHINK with detailed feedback.
|
|
100
102
|
|
|
101
103
|
No special PRD format required — the planner auto-discovers `PRD.md`, `SPEC.md`, `DESIGN.md`, etc. in your project root and `docs/`. Or skip the file entirely:
|
|
102
104
|
|
|
@@ -332,7 +334,7 @@ Incoming messages wake the receiving agent via `pi.sendMessage()` with `triggerT
|
|
|
332
334
|
|
|
333
335
|
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.
|
|
334
336
|
|
|
335
|
-
All coordination is file-based, no daemon required. Shared state (registry, inboxes, swarm claims/completions) lives in `~/.pi/agent/messenger/`. Activity feed and crew data are project-scoped under `.pi/messenger/` inside your project. Dead agents are detected via PID checks and cleaned up automatically.
|
|
337
|
+
All coordination is file-based, no daemon required. Shared state (registry, inboxes, swarm claims/completions) lives in `~/.pi/agent/messenger/`. Activity feed and crew data are project-scoped under `.pi/messenger/` inside your project, so Crew logs live at `<project>/.pi/messenger/crew/` and the shared activity feed lives at `<project>/.pi/messenger/feed.jsonl`. Dead agents are detected via PID checks and cleaned up automatically.
|
|
336
338
|
|
|
337
339
|
## Credits
|
|
338
340
|
|
package/crew/handlers/review.ts
CHANGED
|
@@ -55,7 +55,7 @@ export async function execute(
|
|
|
55
55
|
// Implementation Review
|
|
56
56
|
// =============================================================================
|
|
57
57
|
|
|
58
|
-
async function reviewImplementation(cwd: string, taskId: string, modelOverride?: string) {
|
|
58
|
+
export 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.`, {
|
package/crew/handlers/work.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { resolveModel, spawnAgents } from "../agents.js";
|
|
|
13
13
|
import { loadCrewConfig } from "../utils/config.js";
|
|
14
14
|
import { discoverCrewAgents, discoverCrewSkills } from "../utils/discover.js";
|
|
15
15
|
import { buildWorkerPrompt } from "../prompt.js";
|
|
16
|
+
import { reviewImplementation } from "./review.js";
|
|
16
17
|
import * as store from "../store.js";
|
|
17
18
|
import { getCrewDir } from "../store.js";
|
|
18
19
|
import { autonomousState, isAutonomousForCwd, startAutonomous, stopAutonomous, addWaveResult, clampConcurrency } from "../state.js";
|
|
@@ -220,6 +221,48 @@ export async function execute(
|
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
|
|
224
|
+
// Auto-review succeeded tasks
|
|
225
|
+
if (config.review.enabled && succeeded.length > 0) {
|
|
226
|
+
const hasReviewer = availableAgents.some(a => a.name === "crew-reviewer");
|
|
227
|
+
if (hasReviewer) {
|
|
228
|
+
for (const taskId of [...succeeded]) {
|
|
229
|
+
if (signal?.aborted) break;
|
|
230
|
+
const task = store.getTask(cwd, taskId);
|
|
231
|
+
if (!task || !task.base_commit) continue;
|
|
232
|
+
if ((task.review_count ?? 0) >= config.review.maxIterations) continue;
|
|
233
|
+
|
|
234
|
+
const rr = await reviewImplementation(cwd, taskId, config.models?.reviewer);
|
|
235
|
+
const verdict = rr.details?.verdict as string | undefined;
|
|
236
|
+
if (!verdict) {
|
|
237
|
+
store.appendTaskProgress(cwd, taskId, "system",
|
|
238
|
+
`Auto-review skipped: ${rr.details?.error ?? "unknown"}`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const reviewCount = (task.review_count ?? 0) + 1;
|
|
243
|
+
store.updateTask(cwd, taskId, { review_count: reviewCount });
|
|
244
|
+
|
|
245
|
+
if (verdict === "SHIP") {
|
|
246
|
+
logFeedEvent(cwd, "crew", "task.review", taskId, "SHIP");
|
|
247
|
+
} else if (verdict === "NEEDS_WORK") {
|
|
248
|
+
store.resetTask(cwd, taskId);
|
|
249
|
+
logFeedEvent(cwd, "crew", "task.review", taskId, "NEEDS_WORK — reset for retry");
|
|
250
|
+
succeeded.splice(succeeded.indexOf(taskId), 1);
|
|
251
|
+
failed.push(taskId);
|
|
252
|
+
} else {
|
|
253
|
+
const lastReview = store.getTask(cwd, taskId)?.last_review;
|
|
254
|
+
const summary = lastReview?.summary
|
|
255
|
+
? lastReview.summary.split("\n")[0].slice(0, 120)
|
|
256
|
+
: "Major issues found";
|
|
257
|
+
store.blockTask(cwd, taskId, `Reviewer: ${summary}`);
|
|
258
|
+
logFeedEvent(cwd, "crew", "task.review", taskId, "MAJOR_RETHINK — blocked");
|
|
259
|
+
succeeded.splice(succeeded.indexOf(taskId), 1);
|
|
260
|
+
blocked.push(taskId);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
223
266
|
syncCompletedCount(cwd);
|
|
224
267
|
|
|
225
268
|
// Save current wave number BEFORE addWaveResult increments it
|
|
@@ -327,4 +370,3 @@ function syncCompletedCount(cwd: string): void {
|
|
|
327
370
|
store.updatePlan(cwd, { completed_count: doneCount });
|
|
328
371
|
}
|
|
329
372
|
}
|
|
330
|
-
|
package/crew/types.ts
CHANGED
|
@@ -51,6 +51,7 @@ export interface Task {
|
|
|
51
51
|
evidence?: TaskEvidence; // Evidence from task.done
|
|
52
52
|
blocked_reason?: string; // Reason from task.block
|
|
53
53
|
attempt_count: number; // How many times attempted (for auto-block)
|
|
54
|
+
review_count?: number; // How many times reviewed
|
|
54
55
|
last_review?: ReviewFeedback; // Feedback from last review (for retry)
|
|
55
56
|
}
|
|
56
57
|
|
package/feed.ts
CHANGED
|
@@ -18,6 +18,7 @@ export type FeedEventType =
|
|
|
18
18
|
| "edit"
|
|
19
19
|
| "task.start"
|
|
20
20
|
| "task.done"
|
|
21
|
+
| "task.review"
|
|
21
22
|
| "task.block"
|
|
22
23
|
| "task.unblock"
|
|
23
24
|
| "task.reset"
|
|
@@ -128,6 +129,7 @@ export function pruneFeed(cwd: string, maxEvents: number): void {
|
|
|
128
129
|
const CREW_EVENT_TYPES = new Set<FeedEventType>([
|
|
129
130
|
"task.start",
|
|
130
131
|
"task.done",
|
|
132
|
+
"task.review",
|
|
131
133
|
"task.block",
|
|
132
134
|
"task.unblock",
|
|
133
135
|
"task.reset",
|
|
@@ -182,6 +184,7 @@ export function formatFeedLine(event: FeedEvent): string {
|
|
|
182
184
|
case "edit": line += ` editing ${target}`; break;
|
|
183
185
|
case "task.start": line += withPreview(` started ${target}`); break;
|
|
184
186
|
case "task.done": line += withPreview(` completed ${target}`); break;
|
|
187
|
+
case "task.review": line += withPreview(` reviewed ${target}`); break;
|
|
185
188
|
case "task.block": line += withPreview(` blocked ${target}`); break;
|
|
186
189
|
case "task.unblock": line += withPreview(` unblocked ${target}`); break;
|
|
187
190
|
case "task.reset": line += withPreview(` reset ${target}`); break;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { OverlayHandle, TUI } from "@mariozechner/pi-tui";
|
|
2
|
+
|
|
3
|
+
const QUIET_PERIOD_MS = 80;
|
|
4
|
+
const RENDER_THROTTLE_MS = 32;
|
|
5
|
+
const STDOUT_GUARD_MS = 32;
|
|
6
|
+
|
|
7
|
+
type StdoutWrite = typeof process.stdout.write;
|
|
8
|
+
|
|
9
|
+
function hasRenderableOutput(chunk: unknown): boolean {
|
|
10
|
+
if (typeof chunk === "string") return chunk.length > 0;
|
|
11
|
+
if (chunk instanceof Uint8Array) return chunk.length > 0;
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Coordinates overlay rendering with main agent stdout to prevent visual collision.
|
|
17
|
+
*
|
|
18
|
+
* Strategy: Keep overlay visible, but schedule a "repair" render after foreign
|
|
19
|
+
* output settles. Brief visual corruption is acceptable if it self-heals quickly.
|
|
20
|
+
*/
|
|
21
|
+
export class OverlayRenderCoordinator {
|
|
22
|
+
private tui: TUI | null = null;
|
|
23
|
+
private handle: OverlayHandle | null = null;
|
|
24
|
+
private originalRequestRender: TUI["requestRender"] | null = null;
|
|
25
|
+
private originalStdoutWrite: StdoutWrite | null = null;
|
|
26
|
+
private repairTimer: ReturnType<typeof setTimeout> | null = null;
|
|
27
|
+
private lastRenderAt = 0;
|
|
28
|
+
private stdoutGuardUntil = 0;
|
|
29
|
+
private foreignOutputDetected = false;
|
|
30
|
+
|
|
31
|
+
installStdoutInterceptor(): void {
|
|
32
|
+
if (this.originalStdoutWrite) return;
|
|
33
|
+
|
|
34
|
+
const original = process.stdout.write.bind(process.stdout) as StdoutWrite;
|
|
35
|
+
this.originalStdoutWrite = original;
|
|
36
|
+
|
|
37
|
+
const coordinator = this;
|
|
38
|
+
(process.stdout.write as StdoutWrite) = function writeIntercept(
|
|
39
|
+
chunk: Parameters<StdoutWrite>[0],
|
|
40
|
+
...args: Parameters<StdoutWrite> extends [unknown, ...infer Rest] ? Rest : never
|
|
41
|
+
) {
|
|
42
|
+
const result = original(chunk, ...args);
|
|
43
|
+
coordinator.handleStdoutWrite(chunk);
|
|
44
|
+
return result;
|
|
45
|
+
} as StdoutWrite;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
uninstallStdoutInterceptor(): void {
|
|
49
|
+
if (!this.originalStdoutWrite) return;
|
|
50
|
+
(process.stdout.write as StdoutWrite) = this.originalStdoutWrite;
|
|
51
|
+
this.originalStdoutWrite = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
attach(tui: TUI): void {
|
|
55
|
+
if (this.tui === tui && this.originalRequestRender) return;
|
|
56
|
+
|
|
57
|
+
this.detach();
|
|
58
|
+
this.tui = tui;
|
|
59
|
+
this.originalRequestRender = tui.requestRender.bind(tui);
|
|
60
|
+
tui.requestRender = ((force?: boolean) => {
|
|
61
|
+
this.requestRender(force);
|
|
62
|
+
}) as TUI["requestRender"];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setHandle(handle: OverlayHandle | null): void {
|
|
66
|
+
this.handle = handle;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
detach(): void {
|
|
70
|
+
if (this.repairTimer) {
|
|
71
|
+
clearTimeout(this.repairTimer);
|
|
72
|
+
this.repairTimer = null;
|
|
73
|
+
}
|
|
74
|
+
if (this.tui && this.originalRequestRender) {
|
|
75
|
+
this.tui.requestRender = this.originalRequestRender;
|
|
76
|
+
}
|
|
77
|
+
this.tui = null;
|
|
78
|
+
this.handle = null;
|
|
79
|
+
this.originalRequestRender = null;
|
|
80
|
+
this.lastRenderAt = 0;
|
|
81
|
+
this.stdoutGuardUntil = 0;
|
|
82
|
+
this.foreignOutputDetected = false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
dispose(): void {
|
|
86
|
+
this.detach();
|
|
87
|
+
this.uninstallStdoutInterceptor();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Called by hooks when main agent activity is expected */
|
|
91
|
+
noteForegroundActivity(): void {
|
|
92
|
+
if (!this.tui || !this.handle) return;
|
|
93
|
+
if (this.handle.isHidden()) return;
|
|
94
|
+
|
|
95
|
+
this.foreignOutputDetected = true;
|
|
96
|
+
this.scheduleRepair();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private handleStdoutWrite(chunk: unknown): void {
|
|
100
|
+
if (!this.tui || !this.handle) return;
|
|
101
|
+
if (!hasRenderableOutput(chunk)) return;
|
|
102
|
+
if (Date.now() <= this.stdoutGuardUntil) return;
|
|
103
|
+
if (this.handle.isHidden()) return;
|
|
104
|
+
|
|
105
|
+
this.foreignOutputDetected = true;
|
|
106
|
+
this.scheduleRepair();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private scheduleRepair(): void {
|
|
110
|
+
if (this.repairTimer) clearTimeout(this.repairTimer);
|
|
111
|
+
this.repairTimer = setTimeout(() => {
|
|
112
|
+
this.repairTimer = null;
|
|
113
|
+
this.repair();
|
|
114
|
+
}, QUIET_PERIOD_MS);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private repair(): void {
|
|
118
|
+
if (!this.tui || !this.handle) return;
|
|
119
|
+
if (this.handle.isHidden()) return;
|
|
120
|
+
if (!this.foreignOutputDetected) return;
|
|
121
|
+
|
|
122
|
+
this.foreignOutputDetected = false;
|
|
123
|
+
this.flushRender(true);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
requestRender(force = false): void {
|
|
127
|
+
if (!this.originalRequestRender) return;
|
|
128
|
+
if (this.handle?.isHidden()) return;
|
|
129
|
+
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
if (!force) {
|
|
132
|
+
const elapsed = now - this.lastRenderAt;
|
|
133
|
+
if (elapsed < RENDER_THROTTLE_MS) {
|
|
134
|
+
// Skip this render, repair timer will catch up
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.flushRender(force);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private flushRender(force: boolean): void {
|
|
143
|
+
if (!this.originalRequestRender) return;
|
|
144
|
+
if (this.handle?.isHidden()) return;
|
|
145
|
+
|
|
146
|
+
this.lastRenderAt = Date.now();
|
|
147
|
+
this.stdoutGuardUntil = this.lastRenderAt + STDOUT_GUARD_MS;
|
|
148
|
+
this.originalRequestRender(force);
|
|
149
|
+
}
|
|
150
|
+
}
|