pi-messenger 0.13.1 → 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 CHANGED
@@ -1,5 +1,22 @@
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
+
3
20
  ## [0.13.1] - 2026-03-14
4
21
 
5
22
  ### Added
package/README.md CHANGED
@@ -268,6 +268,7 @@ Agent definitions live in `crew/agents/` within the extension. To customize one
268
268
  |--------|-------------|
269
269
  | `plan` | Create plan from PRD or inline prompt (`prd`, `prompt` optional — auto-discovers PRD if omitted, auto-starts workers unless `autoWork: false`) |
270
270
  | `work` | Run ready tasks (`autonomous`, `concurrency` optional) |
271
+ | `work.stop` | Stop autonomous work for the current project |
271
272
  | `review` | Review implementation (`target` task ID required) |
272
273
  | `task.list` | List all tasks |
273
274
  | `task.show` | Show task details (`id` required) |
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);
@@ -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/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) return;
997
+ if (!autonomousState.active) {
998
+ resetAutonomousContinueGuard();
999
+ return;
1000
+ }
959
1001
 
960
- const cwd = autonomousState.cwd ?? ctx.cwd ?? process.cwd();
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-messenger",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "description": "Inter-agent messaging and file reservation system for pi coding agent",
5
5
  "type": "module",
6
6
  "author": "Nico Bailon",