pi-crew 0.10.3 → 0.10.4

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.
Files changed (62) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +5 -1
  3. package/dist/index.mjs +10685 -6882
  4. package/docs/architecture.md +4 -4
  5. package/docs/commands-reference.md +3 -0
  6. package/docs/publishing.md +15 -3
  7. package/install.mjs +90 -39
  8. package/package.json +8 -3
  9. package/scripts/README.md +4 -3
  10. package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +1 -0
  11. package/skills/real-test-pi-crew/SKILL.md +153 -6
  12. package/src/config/migration-validator.ts +113 -0
  13. package/src/extension/cross-extension-rpc.ts +3 -7
  14. package/src/extension/register.ts +13 -0
  15. package/src/extension/registration/observability.ts +3 -7
  16. package/src/extension/registration/subagent-tools.ts +3 -7
  17. package/src/extension/registration/team-tool.ts +3 -7
  18. package/src/extension/registration/ui.ts +3 -8
  19. package/src/extension/registration/viewers.ts +3 -10
  20. package/src/extension/team-manager-command.ts +3 -7
  21. package/src/extension/team-tool/api/agent-control.ts +17 -10
  22. package/src/extension/team-tool/api/heartbeat.ts +4 -3
  23. package/src/extension/team-tool/api/mailbox.ts +33 -20
  24. package/src/extension/team-tool/api/plan-approval.ts +5 -5
  25. package/src/extension/team-tool/api/task-claims.ts +8 -7
  26. package/src/extension/team-tool/cancel.ts +6 -0
  27. package/src/extension/team-tool/handle-settings.ts +4 -1
  28. package/src/extension/team-tool/run.ts +3 -7
  29. package/src/extension/team-tool/status.ts +5 -0
  30. package/src/extension/team-tool.ts +6 -14
  31. package/src/hooks/registry.ts +3 -0
  32. package/src/prompt/scratchpad-lifecycle.ts +3 -3
  33. package/src/runtime/background-runner.ts +30 -35
  34. package/src/runtime/broker/crew-broker.ts +112 -441
  35. package/src/runtime/broker/delegate/delegate-event.ts +37 -0
  36. package/src/runtime/broker/mailbox-observer/mailbox-fanout.ts +59 -0
  37. package/src/runtime/broker/protocol/connection-state.ts +103 -0
  38. package/src/runtime/broker/protocol/events-replay.ts +68 -0
  39. package/src/runtime/broker/protocol/manifest-loader.ts +20 -0
  40. package/src/runtime/broker/protocol/msg-inbox.ts +69 -0
  41. package/src/runtime/broker/protocol/request-parsers.ts +175 -0
  42. package/src/runtime/broker/protocol/wait-auth.ts +46 -0
  43. package/src/runtime/child-pi/child-pi.ts +15 -0
  44. package/src/runtime/finalize-run.ts +15 -7
  45. package/src/runtime/foreground-control.ts +19 -6
  46. package/src/runtime/goal-workflow/dynamic-workflow-context.ts +6 -0
  47. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -0
  48. package/src/runtime/goal-workflow/goal-loop-runner.ts +29 -27
  49. package/src/runtime/goal-workflow/goal-state-store.ts +3 -0
  50. package/src/runtime/heartbeat/heartbeat-watcher.ts +3 -3
  51. package/src/runtime/model/pi-args.ts +6 -1
  52. package/src/runtime/plan-replan.ts +3 -0
  53. package/src/runtime/stale-reconciler.ts +28 -3
  54. package/src/runtime/supervisor-contact.ts +3 -0
  55. package/src/runtime/task-runner/child-executor.ts +33 -0
  56. package/src/runtime/team-runner.ts +3 -3
  57. package/src/state/stores/ownership-map.ts +5 -4
  58. package/src/state/stores/plan-store.ts +12 -0
  59. package/src/state/stores/state-store.ts +5 -0
  60. package/src/ui/powerbar-publisher.ts +3 -7
  61. package/src/ui/run-action-dispatcher.ts +7 -10
  62. package/src/ui/settings-overlay.ts +4 -1
@@ -23,14 +23,10 @@ import { listRecentRuns } from "./run-index.ts";
23
23
  import type { PiTeamsToolResult } from "./tool-result.ts";
24
24
 
25
25
  type ExecuteTeamRunFn = typeof _executeTeamRunFn;
26
- let _cachedExecuteTeamRun: ExecuteTeamRunFn | undefined;
27
26
  async function executeTeamRun(...args: Parameters<ExecuteTeamRunFn>): Promise<Awaited<ReturnType<ExecuteTeamRunFn>>> {
28
- if (_cachedExecuteTeamRun === undefined) {
29
- // LAZY: heavy runtime — defer 1.4s import cost until team run actually executes.
30
- const mod = await import("../runtime/team-runner.ts");
31
- _cachedExecuteTeamRun = mod.executeTeamRun;
32
- }
33
- return _cachedExecuteTeamRun(...args);
27
+ // LAZY: heavy runtime — defer 1.4s import cost until team run actually executes.
28
+ const mod = await import("../runtime/team-runner.ts");
29
+ return mod.executeTeamRun(...args);
34
30
  }
35
31
 
36
32
  import { directTeamAndWorkflowFromRun } from "../runtime/direct-run.ts";
@@ -44,14 +40,10 @@ import { buildParentContext, formatScoped, result, type TeamContext } from "./te
44
40
  import type { handleRun as _handleRunFn } from "./team-tool/run.ts";
45
41
 
46
42
  type HandleRunFn = typeof _handleRunFn;
47
- let _cachedHandleRun: HandleRunFn | undefined;
48
43
  async function handleRun(...args: Parameters<HandleRunFn>): Promise<Awaited<ReturnType<HandleRunFn>>> {
49
- if (_cachedHandleRun === undefined) {
50
- // LAZY: run.ts pulls in spawnBackgroundTeamRun + resolveCrewRuntime; also avoids jiti import race in child-process contexts.
51
- const mod = await import("./team-tool/run.ts");
52
- _cachedHandleRun = mod.handleRun;
53
- }
54
- return _cachedHandleRun(...args);
44
+ // LAZY: run.ts pulls in spawnBackgroundTeamRun + resolveCrewRuntime; also avoids jiti import race in child-process contexts.
45
+ const mod = await import("./team-tool/run.ts");
46
+ return mod.handleRun(...args);
55
47
  }
56
48
 
57
49
  import { t } from "../i18n.ts";
@@ -179,6 +179,9 @@ export async function executeHook(name: HookName, ctx: HookContext): Promise<Hoo
179
179
  }
180
180
 
181
181
  export function appendHookEvent(manifest: TeamRunManifest, report: HookExecutionReport): void {
182
+ // REVIEW FIX (2026-09-10): reverted M2b buffered conversion — hook.executed
183
+ // events are read back synchronously (recovery-hooks tests, hooks audit
184
+ // display) and are low-frequency (per hook execution).
182
185
  appendEvent(manifest.eventsPath, {
183
186
  type: "hook.executed",
184
187
  runId: manifest.runId,
@@ -32,7 +32,7 @@ import * as path from "node:path";
32
32
  import { type Static, Type } from "@sinclair/typebox";
33
33
  import { defineTool, type ExtensionAPI, type ToolDefinition } from "../extension/pi-api.ts";
34
34
  import { EngineManager, type ExecuteResult } from "../runtime/scratchpad/engine.ts";
35
- import { appendEventFireAndForget } from "../state/event-log/event-log.ts";
35
+ import { appendEventBuffered, appendEventFireAndForget } from "../state/event-log/event-log.ts";
36
36
  // D5/MAJOR-S1: PI_CREW_PARENT_PID + PI_CREW_GUEST build the guest's zombie-
37
37
  // backstop env (PI_CREW_KIND_ENV is already exported above).
38
38
  import { type ArtifactWriteOptions, writeArtifact } from "../state/stores/artifact-store.ts";
@@ -84,12 +84,12 @@ function emitScratchpadMetric(
84
84
  // never break the cell. The real appendEventFireAndForget already catches
85
85
  // async; this guard also absorbs a sync throw from a bad writer.
86
86
  try {
87
- appendEvent(eventsPath, {
87
+ appendEventBuffered(eventsPath, {
88
88
  type,
89
89
  runId,
90
90
  taskId: env[PI_CREW_TASK_ID_ENV],
91
91
  data,
92
- });
92
+ }).catch((e) => logInternalError("scratchpad_lifecycle.buffered", e, `type=${type}`));
93
93
  } catch {
94
94
  // Metric is best-effort — drop the event, never the cell.
95
95
  }
@@ -5,7 +5,7 @@ import { loadConfig } from "../config/config.ts";
5
5
  import { getCrewEnv } from "../config/env-vars.ts";
6
6
  import { atomicWriteFile } from "../state/atomic-write.ts";
7
7
  import { withRunLockSync } from "../state/coordination/locks.ts";
8
- import { appendEvent, appendEventFireAndForget } from "../state/event-log/event-log.ts";
8
+ import { appendEvent, appendEventBuffered, appendEventFireAndForget } from "../state/event-log/event-log.ts";
9
9
  import { createRunPaths, loadRunManifestById, saveRunManifestAsync, updateRunStatus } from "../state/stores/state-store.ts";
10
10
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
11
11
  import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
@@ -18,8 +18,6 @@ import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows
18
18
  import { primePeerDep } from "./peer-dep.ts";
19
19
  import type { executeTeamRun as ExecuteTeamRunFn } from "./team-runner.ts";
20
20
 
21
- let _cachedExecuteTeamRun: typeof ExecuteTeamRunFn | undefined;
22
-
23
21
  /** Maximum runtime for a single background run before the watchdog force-aborts
24
22
  * it. Prevents zombie background-runner processes when a team run hangs forever
25
23
  * (e.g. a hung child Pi process, a stuck lock, or a test that spawns a run
@@ -33,16 +31,13 @@ const MAX_BACKGROUND_RUN_MS = (() => {
33
31
  return Number.isFinite(env) && env > 0 ? env : 2 * 60 * 60 * 1000;
34
32
  })();
35
33
  async function executeTeamRun(...args: Parameters<typeof ExecuteTeamRunFn>): Promise<Awaited<ReturnType<typeof ExecuteTeamRunFn>>> {
36
- if (!_cachedExecuteTeamRun) {
37
- // FIX (split-scope install): prime the ESM peer dep BEFORE team-runner is
38
- // imported, so its transitive skill-instructions.ts can read getAgentDir()
39
- // from the primed cache instead of crashing on `Cannot find module`.
40
- await primePeerDep().catch(() => undefined);
41
- // LAZY: avoid pulling team-runner into background-runner at module load time.
42
- const mod = await import("./team-runner.ts");
43
- _cachedExecuteTeamRun = mod.executeTeamRun;
44
- }
45
- return _cachedExecuteTeamRun(...args);
34
+ // FIX (split-scope install): prime the ESM peer dep BEFORE team-runner is
35
+ // imported, so its transitive skill-instructions.ts can read getAgentDir()
36
+ // from the primed cache instead of crashing on `Cannot find module`.
37
+ await primePeerDep().catch(() => undefined);
38
+ // LAZY: avoid pulling team-runner into background-runner at module load time.
39
+ const mod = await import("./team-runner.ts");
40
+ return mod.executeTeamRun(...args);
46
41
  }
47
42
 
48
43
  import { logInternalError } from "../utils/internal-error.ts";
@@ -256,11 +251,11 @@ export function startInterruptGuard(
256
251
  /* best-effort ack — interruptHandled gate prevents re-fire */
257
252
  }
258
253
 
259
- appendEvent(manifest.eventsPath, {
254
+ appendEventBuffered(manifest.eventsPath, {
260
255
  type: "async.interrupt_detected",
261
256
  runId: manifest.runId,
262
257
  message: "Background runner detected foreground interrupt — killing child processes and exiting.",
263
- });
258
+ }).catch((e) => logInternalError("background-runner.buffered", e, "type=async.interrupt_detected"));
264
259
  // FIX: Terminate ALL child-pi processes IMMEDIATELY before exiting.
265
260
  // Previously this was missing, causing orphaned child processes to run forever
266
261
  // after the background-runner exited. terminateActiveChildPiProcesses sends
@@ -311,7 +306,7 @@ function setupUnhandledRejectionGuard(
311
306
  console.error("[background-runner] Stack:", reason instanceof Error ? reason.stack : "N/A");
312
307
  try {
313
308
  if (state.eventsPath && state.runId) {
314
- appendEvent(state.eventsPath, {
309
+ appendEventBuffered(state.eventsPath, {
315
310
  type: "async.failed",
316
311
  runId: state.runId,
317
312
  message: `Unhandled rejection: ${message}`,
@@ -320,7 +315,7 @@ function setupUnhandledRejectionGuard(
320
315
  stack: reason instanceof Error ? reason.stack : undefined,
321
316
  handled: false,
322
317
  },
323
- });
318
+ }).catch((e) => logInternalError("background-runner.buffered", e, "type=async.failed source=unhandledRejection"));
324
319
  }
325
320
  } catch (appendErr) {
326
321
  console.error("[background-runner] Failed to write async.failed event:", appendErr);
@@ -377,12 +372,12 @@ function runCleanup(
377
372
  console.log(`[background-runner] runCleanup: unregisterWorker error: ${errorMessage(error)}`);
378
373
  if (eventsPath) {
379
374
  try {
380
- appendEvent(eventsPath, {
375
+ appendEventBuffered(eventsPath, {
381
376
  type: "background.unregister_worker_failed",
382
377
  runId: argValue("--run-id") ?? "unknown",
383
378
  message: `unregisterWorker failed: ${errorMessage(error)}`,
384
379
  data: { pid: process.pid },
385
- });
380
+ }).catch((e) => logInternalError("background-runner.buffered", e, "type=background.unregister_worker_failed"));
386
381
  } catch {
387
382
  /* best-effort */
388
383
  }
@@ -538,12 +533,12 @@ async function main(): Promise<void> {
538
533
  const runId = argValue("--run-id");
539
534
  if (runId && manifest.eventsPath) {
540
535
  try {
541
- appendEvent(manifest.eventsPath, {
536
+ appendEventBuffered(manifest.eventsPath, {
542
537
  type: "async.sigterm_received_graceful_shutdown",
543
538
  runId,
544
539
  message: `SIGTERM received, graceful shutdown via abort pid=${process.pid}`,
545
540
  data: { pid: process.pid, ppid: process.ppid },
546
- });
541
+ }).catch((e) => logInternalError("background-runner.buffered", e, "type=async.sigterm_received_graceful_shutdown"));
547
542
  } catch {
548
543
  /* best-effort */
549
544
  }
@@ -596,12 +591,12 @@ async function main(): Promise<void> {
596
591
  const codeStr = code === undefined ? "<none>" : String(code);
597
592
  if (runId2 && manifest.eventsPath) {
598
593
  try {
599
- appendEvent(manifest.eventsPath, {
594
+ appendEventBuffered(manifest.eventsPath, {
600
595
  type: "async.exit",
601
596
  runId: runId2,
602
597
  message: `Background runner exit(${codeStr}) pid=${process.pid}`,
603
598
  data: { code, pid: process.pid },
604
- });
599
+ }).catch((e) => logInternalError("background-runner.buffered", e, "type=async.exit"));
605
600
  } catch {
606
601
  /* best-effort */
607
602
  }
@@ -635,11 +630,11 @@ async function main(): Promise<void> {
635
630
  // NOTE: intentionally no unref() — the guard keeps the event loop alive
636
631
  // to prevent premature worker exit. See parent-guard.ts:86 for rationale.
637
632
 
638
- appendEvent(manifest.eventsPath, {
633
+ appendEventBuffered(manifest.eventsPath, {
639
634
  type: "async.started",
640
635
  runId: manifest.runId,
641
636
  data: { pid: process.pid },
642
- });
637
+ }).catch((e) => logInternalError("background-runner.buffered", e, "type=async.started"));
643
638
  debugLog(`[background-runner] async.started written, pid=${process.pid}`);
644
639
  writeAsyncStartMarker(manifest, {
645
640
  pid: process.pid,
@@ -663,12 +658,12 @@ async function main(): Promise<void> {
663
658
  const watchdogTimer = setTimeout(() => {
664
659
  console.error(`[background-runner] WATCHDOG: run ${runId} exceeded ${MAX_BACKGROUND_RUN_MS}ms — aborting (zombie prevention)`);
665
660
  try {
666
- appendEvent(manifest.eventsPath, {
661
+ appendEventBuffered(manifest.eventsPath, {
667
662
  type: "async.watchdog_fired",
668
663
  runId,
669
664
  message: `Run exceeded ${MAX_BACKGROUND_RUN_MS}ms and was force-aborted to prevent a zombie background-runner process.`,
670
665
  data: { maxRunMs: MAX_BACKGROUND_RUN_MS },
671
- });
666
+ }).catch((e) => logInternalError("background-runner.buffered", e, "type=async.watchdog_fired"));
672
667
  } catch {
673
668
  /* best-effort event log */
674
669
  }
@@ -825,12 +820,12 @@ async function main(): Promise<void> {
825
820
  updatedAt: new Date().toISOString(),
826
821
  };
827
822
  await saveRunManifestAsync(manifest);
828
- appendEvent(manifest.eventsPath, {
823
+ appendEventBuffered(manifest.eventsPath, {
829
824
  type: "runtime.resolved",
830
825
  runId: manifest.runId,
831
826
  message: `Runtime resolved: ${runtime.kind} safety=${runtime.safety}`,
832
827
  data: { runtimeResolution, async: true },
833
- });
828
+ }).catch((e) => logInternalError("background-runner.buffered", e, "type=runtime.resolved"));
834
829
  if (runtime.safety === "blocked")
835
830
  throw new Error(runtime.reason ?? "Child worker execution is disabled; refusing to create no-op scaffold subagents.");
836
831
  const executeWorkers = runtime.kind !== "scaffold";
@@ -890,11 +885,11 @@ async function main(): Promise<void> {
890
885
  } // close if (!earlyResult) — team-run setup+execute done; earlyResult path skips to here
891
886
  manifest = result!.manifest;
892
887
  tasks = result!.tasks;
893
- appendEvent(manifest.eventsPath, {
888
+ appendEventBuffered(manifest.eventsPath, {
894
889
  type: "async.completed",
895
890
  runId: manifest.runId,
896
891
  data: { status: manifest.status, tasks: tasks.length },
897
- });
892
+ }).catch((e) => logInternalError("background-runner.buffered", e, "type=async.completed"));
898
893
  console.log(`[background-runner] async.completed written, status=${manifest.status}`);
899
894
  if (manifest.status === "failed" || manifest.status === "cancelled" || manifest.status === "blocked") process.exitCode = 1;
900
895
  } catch (error) {
@@ -915,11 +910,11 @@ async function main(): Promise<void> {
915
910
  const fresh = loaded?.manifest ?? manifest;
916
911
  if (fresh) {
917
912
  manifest = updateRunStatus(fresh, "failed", message);
918
- appendEvent(manifest.eventsPath, {
913
+ appendEventBuffered(manifest.eventsPath, {
919
914
  type: "async.failed",
920
915
  runId: manifest.runId,
921
916
  message,
922
- });
917
+ }).catch((e) => logInternalError("background-runner.buffered", e, "type=async.failed source=updateFailed"));
923
918
  }
924
919
  return fresh;
925
920
  },
@@ -988,12 +983,12 @@ try {
988
983
  const mRunId = argValue("--run-id");
989
984
  if (mCwd && mRunId) {
990
985
  const mEventsPath = createRunPaths(mCwd, mRunId).eventsPath;
991
- appendEvent(mEventsPath, {
986
+ appendEventBuffered(mEventsPath, {
992
987
  type: "async.failed",
993
988
  runId: mRunId,
994
989
  message: errorMessage(err),
995
990
  data: { stack: err instanceof Error ? err.stack : undefined },
996
- });
991
+ }).catch((e) => logInternalError("background-runner.buffered", e, "type=async.failed source=catch"));
997
992
  }
998
993
  } catch {
999
994
  /* best-effort — don't let event-write failure mask the original error */