pi-messenger 0.13.0 → 0.13.2
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 +22 -0
- package/README.md +6 -3
- package/crew/handlers/review.ts +1 -1
- package/crew/handlers/work.ts +43 -1
- package/crew/index.ts +11 -1
- package/crew/state-autonomous.ts +30 -0
- package/crew/types.ts +1 -0
- package/feed.ts +3 -0
- package/index.ts +77 -5
- package/overlay-coordinator.ts +150 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.13.2] - 2026-03-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`work.stop` action** — `pi_messenger({ action: "work.stop" })` now stops autonomous Crew work for the current project and persists the stop state.
|
|
9
|
+
- **Autonomous guard coverage tests** — Added targeted tests for `work.stop` routing and `agent_end` autonomous continuation guard behavior.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **Autonomous continuation retry loop guard** — Autonomous Crew mode now stops itself after repeated identical `crew_continue` retries without wave progress (for example when steer turns keep aborting). The extension persists the stopped state and emits a warning instead of looping indefinitely.
|
|
13
|
+
- **Persisted autonomous stop state from index-level stop paths** — `max waves` and `no ready tasks` stop paths now append `crew-state` so restored sessions do not revive stale active autonomous state.
|
|
14
|
+
- **Autonomous continuation no longer runs in unregistered sessions** — If Crew autonomous state is restored but the current session is not joined to pi-messenger, the extension now stops autonomous mode and persists that stop instead of emitting repeated continuation steer messages that cannot execute.
|
|
15
|
+
- **Autonomous continuation now runs only in orchestrator sessions** — Worker and lobby processes (`PI_CREW_WORKER`/`PI_LOBBY_ID`) now skip `agent_end` continuation emission to prevent repeated duplicate `crew_continue` steer loops from non-orchestrator agents.
|
|
16
|
+
- **Autonomous continuation is now cwd-scoped** — `agent_end` continuation now requires the session cwd to match the active autonomous cwd (`isAutonomousForCwd`), preventing stale autonomous state from unrelated projects from triggering unexpected `crew_continue` messages.
|
|
17
|
+
- **Autonomous state restore now validates owner process** — Restored active autonomous state now requires a live owner PID matching the current process; missing/dead/foreign ownership is treated as stale and auto-stopped to avoid unexpected continuation after session restore.
|
|
18
|
+
- **Runtime artifact commit noise** — Added `.pi/` to `.gitignore` to avoid accidentally committing runtime files like feed artifacts.
|
|
19
|
+
|
|
20
|
+
## [0.13.1] - 2026-03-14
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **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.
|
|
24
|
+
|
|
3
25
|
## [0.13.0] - 2026-03-02
|
|
4
26
|
|
|
5
27
|
### 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
|
|
|
@@ -266,6 +268,7 @@ Agent definitions live in `crew/agents/` within the extension. To customize one
|
|
|
266
268
|
|--------|-------------|
|
|
267
269
|
| `plan` | Create plan from PRD or inline prompt (`prd`, `prompt` optional — auto-discovers PRD if omitted, auto-starts workers unless `autoWork: false`) |
|
|
268
270
|
| `work` | Run ready tasks (`autonomous`, `concurrency` optional) |
|
|
271
|
+
| `work.stop` | Stop autonomous work for the current project |
|
|
269
272
|
| `review` | Review implementation (`target` task ID required) |
|
|
270
273
|
| `task.list` | List all tasks |
|
|
271
274
|
| `task.show` | Show task details (`id` required) |
|
|
@@ -332,7 +335,7 @@ Incoming messages wake the receiving agent via `pi.sendMessage()` with `triggerT
|
|
|
332
335
|
|
|
333
336
|
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
337
|
|
|
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.
|
|
338
|
+
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
339
|
|
|
337
340
|
## Credits
|
|
338
341
|
|
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/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { MessengerState, Dirs, AgentMailMessage, NameThemeConfig } from "..
|
|
|
10
10
|
import * as handlers from "../handlers.js";
|
|
11
11
|
import type { CrewParams, AppendEntryFn } from "./types.js";
|
|
12
12
|
import { result } from "./utils/result.js";
|
|
13
|
-
import { isPlanningForCwd, cancelPlanningRun } from "./state.js";
|
|
13
|
+
import { isPlanningForCwd, cancelPlanningRun, autonomousState, isAutonomousForCwd, stopAutonomous } from "./state.js";
|
|
14
14
|
import { logFeedEvent } from "../feed.js";
|
|
15
15
|
|
|
16
16
|
type DeliverFn = (msg: AgentMailMessage) => void;
|
|
@@ -180,6 +180,16 @@ export async function executeCrewAction(
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
case 'work': {
|
|
183
|
+
if (op === 'stop') {
|
|
184
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
185
|
+
if (!isAutonomousForCwd(cwd)) {
|
|
186
|
+
return result("No autonomous work running for this project.", { mode: "work.stop" });
|
|
187
|
+
}
|
|
188
|
+
stopAutonomous("manual");
|
|
189
|
+
appendEntry("crew-state", autonomousState);
|
|
190
|
+
return result("Autonomous work stopped.", { mode: "work.stop", autonomous: false });
|
|
191
|
+
}
|
|
192
|
+
|
|
183
193
|
try {
|
|
184
194
|
const workHandler = await import("./handlers/work.js");
|
|
185
195
|
return workHandler.execute(params, dirs, ctx, appendEntry, signal);
|
package/crew/state-autonomous.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface AutonomousState {
|
|
|
25
25
|
stopReason: "completed" | "blocked" | "manual" | null;
|
|
26
26
|
concurrency: number;
|
|
27
27
|
autoOverlayPending: boolean;
|
|
28
|
+
pid: number | null;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export const autonomousState: AutonomousState = {
|
|
@@ -37,11 +38,21 @@ export const autonomousState: AutonomousState = {
|
|
|
37
38
|
stopReason: null,
|
|
38
39
|
concurrency: 2,
|
|
39
40
|
autoOverlayPending: false,
|
|
41
|
+
pid: null,
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
export const MIN_CONCURRENCY = 1;
|
|
43
45
|
export const MAX_CONCURRENCY = 10;
|
|
44
46
|
|
|
47
|
+
function isProcessAlive(pid: number): boolean {
|
|
48
|
+
try {
|
|
49
|
+
process.kill(pid, 0);
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
export function clampConcurrency(value: number, configMax?: number): number {
|
|
46
57
|
if (!Number.isFinite(value)) return MIN_CONCURRENCY;
|
|
47
58
|
const whole = Math.trunc(value);
|
|
@@ -79,6 +90,7 @@ export function startAutonomous(cwd: string, concurrency: number): void {
|
|
|
79
90
|
autonomousState.stopReason = null;
|
|
80
91
|
autonomousState.concurrency = clampConcurrency(concurrency);
|
|
81
92
|
autonomousState.autoOverlayPending = true;
|
|
93
|
+
autonomousState.pid = process.pid;
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
export function stopAutonomous(reason: "completed" | "blocked" | "manual"): void {
|
|
@@ -86,6 +98,7 @@ export function stopAutonomous(reason: "completed" | "blocked" | "manual"): void
|
|
|
86
98
|
autonomousState.autoOverlayPending = false;
|
|
87
99
|
autonomousState.stoppedAt = new Date().toISOString();
|
|
88
100
|
autonomousState.stopReason = reason;
|
|
101
|
+
autonomousState.pid = null;
|
|
89
102
|
}
|
|
90
103
|
|
|
91
104
|
export function addWaveResult(result: WaveResult): void {
|
|
@@ -106,6 +119,23 @@ export function restoreAutonomousState(data: Partial<AutonomousState>): void {
|
|
|
106
119
|
if (data.concurrency !== undefined) {
|
|
107
120
|
autonomousState.concurrency = clampConcurrency(Number(data.concurrency));
|
|
108
121
|
}
|
|
122
|
+
if (data.pid !== undefined) {
|
|
123
|
+
autonomousState.pid = typeof data.pid === "number" ? data.pid : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!autonomousState.active) return;
|
|
127
|
+
|
|
128
|
+
const ownerPid = autonomousState.pid;
|
|
129
|
+
const sameProcess = ownerPid === process.pid;
|
|
130
|
+
const ownerAlive = typeof ownerPid === "number" && isProcessAlive(ownerPid);
|
|
131
|
+
|
|
132
|
+
if (!sameProcess || !ownerAlive) {
|
|
133
|
+
autonomousState.active = false;
|
|
134
|
+
autonomousState.autoOverlayPending = false;
|
|
135
|
+
autonomousState.stopReason = autonomousState.stopReason ?? "manual";
|
|
136
|
+
autonomousState.stoppedAt = autonomousState.stoppedAt ?? new Date().toISOString();
|
|
137
|
+
autonomousState.pid = null;
|
|
138
|
+
}
|
|
109
139
|
}
|
|
110
140
|
|
|
111
141
|
export function isAutonomousForCwd(cwd: string): boolean {
|
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;
|
package/index.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
restoreAutonomousState,
|
|
59
59
|
restorePlanningState,
|
|
60
60
|
stopAutonomous,
|
|
61
|
+
isAutonomousForCwd,
|
|
61
62
|
} from "./crew/state.js";
|
|
62
63
|
import { loadCrewConfig } from "./crew/utils/config.js";
|
|
63
64
|
import * as crewStore from "./crew/store.js";
|
|
@@ -295,6 +296,24 @@ export default function piMessengerExtension(pi: ExtensionAPI) {
|
|
|
295
296
|
const STATUS_HEARTBEAT_MS = 15_000;
|
|
296
297
|
let latestCtx: ExtensionContext | null = null;
|
|
297
298
|
let statusHeartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
299
|
+
const AUTONOMOUS_CONTINUE_REPEAT_LIMIT = 3;
|
|
300
|
+
let autonomousContinueSignature: string | null = null;
|
|
301
|
+
let autonomousContinueRepeats = 0;
|
|
302
|
+
|
|
303
|
+
function resetAutonomousContinueGuard(): void {
|
|
304
|
+
autonomousContinueSignature = null;
|
|
305
|
+
autonomousContinueRepeats = 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function trackAutonomousContinue(signature: string): number {
|
|
309
|
+
if (autonomousContinueSignature === signature) {
|
|
310
|
+
autonomousContinueRepeats += 1;
|
|
311
|
+
} else {
|
|
312
|
+
autonomousContinueSignature = signature;
|
|
313
|
+
autonomousContinueRepeats = 1;
|
|
314
|
+
}
|
|
315
|
+
return autonomousContinueRepeats;
|
|
316
|
+
}
|
|
298
317
|
|
|
299
318
|
function startStatusHeartbeat(): void {
|
|
300
319
|
if (statusHeartbeatTimer) return;
|
|
@@ -358,6 +377,7 @@ Usage (action-based API - preferred):
|
|
|
358
377
|
// Crew: Work through tasks
|
|
359
378
|
pi_messenger({ action: "work" }) → Run ready tasks
|
|
360
379
|
pi_messenger({ action: "work", autonomous: true }) → Run until done/blocked
|
|
380
|
+
pi_messenger({ action: "work.stop" }) → Stop autonomous work for this project
|
|
361
381
|
|
|
362
382
|
// Crew: Tasks
|
|
363
383
|
pi_messenger({ action: "task.show", id: "task-1" }) → Show task
|
|
@@ -749,6 +769,7 @@ Usage (action-based API - preferred):
|
|
|
749
769
|
|
|
750
770
|
pi.on("session_start", async (_event, ctx) => {
|
|
751
771
|
latestCtx = ctx;
|
|
772
|
+
resetAutonomousContinueGuard();
|
|
752
773
|
startStatusHeartbeat();
|
|
753
774
|
for (const entry of ctx.sessionManager.getEntries()) {
|
|
754
775
|
if (entry.type === "custom" && entry.customType === "crew-state") {
|
|
@@ -870,6 +891,7 @@ Usage (action-based API - preferred):
|
|
|
870
891
|
|
|
871
892
|
pi.on("session_switch", async (_event, ctx) => {
|
|
872
893
|
latestCtx = ctx;
|
|
894
|
+
resetAutonomousContinueGuard();
|
|
873
895
|
const { staleCleared } = restorePlanningState(ctx.cwd ?? process.cwd());
|
|
874
896
|
if (staleCleared && ctx.hasUI) {
|
|
875
897
|
ctx.ui.notify("Stale planning state cleared (planner process exited)", "warning");
|
|
@@ -880,6 +902,7 @@ Usage (action-based API - preferred):
|
|
|
880
902
|
});
|
|
881
903
|
pi.on("session_fork", async (_event, ctx) => {
|
|
882
904
|
latestCtx = ctx;
|
|
905
|
+
resetAutonomousContinueGuard();
|
|
883
906
|
const { staleCleared } = restorePlanningState(ctx.cwd ?? process.cwd());
|
|
884
907
|
if (staleCleared && ctx.hasUI) {
|
|
885
908
|
ctx.ui.notify("Stale planning state cleared (planner process exited)", "warning");
|
|
@@ -936,6 +959,22 @@ Usage (action-based API - preferred):
|
|
|
936
959
|
// ===========================================================================
|
|
937
960
|
|
|
938
961
|
pi.on("agent_end", async (_event, ctx) => {
|
|
962
|
+
if (process.env.PI_CREW_WORKER === "1" || process.env.PI_LOBBY_ID) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (!state.registered) {
|
|
967
|
+
if (autonomousState.active) {
|
|
968
|
+
stopAutonomous("manual");
|
|
969
|
+
pi.appendEntry("crew-state", autonomousState);
|
|
970
|
+
resetAutonomousContinueGuard();
|
|
971
|
+
if (ctx.hasUI) {
|
|
972
|
+
ctx.ui.notify("Autonomous stopped: this session is not registered in pi-messenger.", "warning");
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
|
|
939
978
|
// --- Auto-work after plan completion ---
|
|
940
979
|
const autoWork = consumePendingAutoWork();
|
|
941
980
|
if (autoWork && !overlayTui) {
|
|
@@ -955,15 +994,26 @@ Usage (action-based API - preferred):
|
|
|
955
994
|
}
|
|
956
995
|
|
|
957
996
|
// --- Existing autonomous continuation ---
|
|
958
|
-
if (!autonomousState.active)
|
|
997
|
+
if (!autonomousState.active) {
|
|
998
|
+
resetAutonomousContinueGuard();
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
959
1001
|
|
|
960
|
-
const
|
|
1002
|
+
const currentCwd = ctx.cwd ?? process.cwd();
|
|
1003
|
+
if (!isAutonomousForCwd(currentCwd)) {
|
|
1004
|
+
resetAutonomousContinueGuard();
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const cwd = autonomousState.cwd ?? currentCwd;
|
|
961
1009
|
const crewDir = join(cwd, ".pi", "messenger", "crew");
|
|
962
1010
|
const crewConfig = loadCrewConfig(crewDir);
|
|
963
1011
|
|
|
964
1012
|
// Check max waves limit
|
|
965
1013
|
if (autonomousState.waveNumber >= crewConfig.work.maxWaves) {
|
|
966
1014
|
stopAutonomous("manual");
|
|
1015
|
+
pi.appendEntry("crew-state", autonomousState);
|
|
1016
|
+
resetAutonomousContinueGuard();
|
|
967
1017
|
if (ctx.hasUI) {
|
|
968
1018
|
ctx.ui.notify(`Autonomous stopped: max waves (${crewConfig.work.maxWaves}) reached`, "warning");
|
|
969
1019
|
}
|
|
@@ -972,14 +1022,16 @@ Usage (action-based API - preferred):
|
|
|
972
1022
|
|
|
973
1023
|
// Check for ready tasks
|
|
974
1024
|
const readyTasks = crewStore.getReadyTasks(cwd, { advisory: crewConfig.dependencies === "advisory" });
|
|
975
|
-
|
|
1025
|
+
|
|
976
1026
|
if (readyTasks.length === 0) {
|
|
977
1027
|
// No ready tasks - check if all done or blocked
|
|
978
1028
|
const allTasks = crewStore.getTasks(cwd);
|
|
979
1029
|
const allDone = allTasks.every(t => t.status === "done");
|
|
980
|
-
|
|
1030
|
+
|
|
981
1031
|
stopAutonomous(allDone ? "completed" : "blocked");
|
|
982
|
-
|
|
1032
|
+
pi.appendEntry("crew-state", autonomousState);
|
|
1033
|
+
resetAutonomousContinueGuard();
|
|
1034
|
+
|
|
983
1035
|
const plan = crewStore.getPlan(cwd);
|
|
984
1036
|
if (ctx.hasUI) {
|
|
985
1037
|
if (allDone) {
|
|
@@ -992,6 +1044,26 @@ Usage (action-based API - preferred):
|
|
|
992
1044
|
return;
|
|
993
1045
|
}
|
|
994
1046
|
|
|
1047
|
+
const continueSignature = `${cwd}:${autonomousState.waveNumber}:${readyTasks.map(task => task.id).sort().join(",")}`;
|
|
1048
|
+
const continueRepeatCount = trackAutonomousContinue(continueSignature);
|
|
1049
|
+
if (continueRepeatCount >= AUTONOMOUS_CONTINUE_REPEAT_LIMIT) {
|
|
1050
|
+
stopAutonomous("manual");
|
|
1051
|
+
pi.appendEntry("crew-state", autonomousState);
|
|
1052
|
+
resetAutonomousContinueGuard();
|
|
1053
|
+
|
|
1054
|
+
const plan = crewStore.getPlan(cwd);
|
|
1055
|
+
const message = `Autonomous work on ${plan?.prd ?? "plan"} stopped after ${continueRepeatCount} repeated continuation retries without wave progress. Resolve the abort condition, then run pi_messenger({ action: "work", autonomous: true }).`;
|
|
1056
|
+
if (ctx.hasUI) {
|
|
1057
|
+
ctx.ui.notify(message, "warning");
|
|
1058
|
+
}
|
|
1059
|
+
pi.sendMessage({
|
|
1060
|
+
customType: "crew_continue_stopped",
|
|
1061
|
+
content: message,
|
|
1062
|
+
display: true,
|
|
1063
|
+
});
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
995
1067
|
// Continue to next wave
|
|
996
1068
|
// Note: waveNumber was already incremented by addWaveResult() in work.ts
|
|
997
1069
|
const plan = crewStore.getPlan(cwd);
|
|
@@ -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
|
+
}
|