pi-crew 0.11.0 → 0.11.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.
Files changed (174) hide show
  1. package/CHANGELOG.md +155 -9
  2. package/README.md +161 -1037
  3. package/agents/verifier.md +18 -7
  4. package/dist/index.mjs +744 -90644
  5. package/docs/README.md +57 -46
  6. package/docs/architecture.md +87 -33
  7. package/docs/commands-reference.md +9 -5
  8. package/docs/troubleshooting.md +3 -2
  9. package/package.json +1 -3
  10. package/schema.json +39 -0
  11. package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
  12. package/skills/real-test-pi-crew/SKILL.md +371 -34
  13. package/src/agents/agent-config.ts +1 -1
  14. package/src/agents/discover-agents.ts +1 -1
  15. package/src/config/config-validation.ts +15 -2
  16. package/src/config/config.ts +47 -13
  17. package/src/config/defaults.ts +0 -1
  18. package/src/config/env-vars.ts +35 -0
  19. package/src/config/types.ts +19 -5
  20. package/src/errors.ts +2 -2
  21. package/src/extension/async-notifier.ts +23 -0
  22. package/src/extension/crew-vibes/config.ts +0 -21
  23. package/src/extension/crew-vibes/index.ts +0 -2
  24. package/src/extension/crew-vibes/render.ts +1 -50
  25. package/src/extension/help.ts +21 -12
  26. package/src/extension/knowledge-injection.ts +2 -1
  27. package/src/extension/management.ts +8 -3
  28. package/src/extension/notification-sink.ts +17 -0
  29. package/src/extension/register.ts +7 -2
  30. package/src/extension/registration/command-utils.ts +28 -2
  31. package/src/extension/registration/commands/dashboard.ts +11 -1
  32. package/src/extension/registration/commands/manage.ts +36 -19
  33. package/src/extension/registration/commands/run.ts +24 -2
  34. package/src/extension/registration/commands/shared.ts +23 -1
  35. package/src/extension/registration/commands/status.ts +25 -2
  36. package/src/extension/registration/context-builder.ts +8 -2
  37. package/src/extension/registration/health-notify-policy.ts +100 -0
  38. package/src/extension/registration/lazy-configurers.ts +35 -0
  39. package/src/extension/registration/lifecycle-handlers.ts +91 -30
  40. package/src/extension/registration/lifecycle.ts +75 -10
  41. package/src/extension/registration/observability.ts +98 -35
  42. package/src/extension/registration/registration-types.ts +7 -5
  43. package/src/extension/registration/runtime-cleanup.ts +9 -3
  44. package/src/extension/registration/subagent-helpers.ts +38 -0
  45. package/src/extension/registration/subagent-tools.ts +16 -6
  46. package/src/extension/registration/team-tool.ts +10 -3
  47. package/src/extension/registration/terminal-status-wiring.ts +172 -0
  48. package/src/extension/registration/viewers.ts +6 -0
  49. package/src/extension/registration/wire-cross-extension.ts +28 -0
  50. package/src/extension/run-compare.ts +220 -0
  51. package/src/extension/run-export.ts +37 -5
  52. package/src/extension/run-maintenance.ts +155 -5
  53. package/src/extension/team-tool/dispatch/index.ts +3 -2
  54. package/src/extension/team-tool/dispatch/manage.ts +5 -2
  55. package/src/extension/team-tool/goal.ts +4 -1
  56. package/src/extension/team-tool/handle-settings.ts +33 -4
  57. package/src/extension/team-tool/health-monitor.ts +21 -7
  58. package/src/extension/team-tool/lifecycle-actions.ts +49 -1
  59. package/src/extension/team-tool/plan.ts +10 -0
  60. package/src/extension/team-tool/routing-hint.ts +63 -0
  61. package/src/extension/team-tool/status.ts +4 -0
  62. package/src/extension/team-tool.ts +52 -6
  63. package/src/extension/webhook-notify.ts +382 -0
  64. package/src/observability/metric-sink.ts +12 -2
  65. package/src/prompt/prompt-runtime.ts +82 -31
  66. package/src/prompt/worker-events-channel.ts +12 -0
  67. package/src/runtime/README.md +1 -1
  68. package/src/runtime/async-runner.ts +87 -1
  69. package/src/runtime/background-runner.ts +313 -234
  70. package/src/runtime/broker/crew-broker.ts +17 -11
  71. package/src/runtime/broker/delegate/shadow-lifecycle.ts +92 -0
  72. package/src/runtime/broker/wait-status-cache.ts +1 -1
  73. package/src/runtime/child-pi/child-pi-timers.ts +1 -1
  74. package/src/runtime/child-pi/mock-fixtures.ts +48 -0
  75. package/src/runtime/crew-agent-records.ts +337 -45
  76. package/src/runtime/deadletter.ts +43 -1
  77. package/src/runtime/delegate-spawn.ts +5 -1
  78. package/src/runtime/dispatch-batch.ts +72 -5
  79. package/src/runtime/goal-workflow/goal-loop-runner.ts +73 -4
  80. package/src/runtime/heartbeat/heartbeat-watcher.ts +7 -0
  81. package/src/runtime/model/model-fallback.ts +21 -1
  82. package/src/runtime/model/pi-args.ts +8 -10
  83. package/src/runtime/recovery/crash-recovery.ts +25 -1
  84. package/src/runtime/run-worker.ts +12 -1
  85. package/src/runtime/scheduling/global-worker-cap.ts +13 -6
  86. package/src/runtime/scheduling/run-coalesced-task-group.ts +27 -1
  87. package/src/runtime/scheduling/scheduler.ts +49 -13
  88. package/src/runtime/scheduling/semaphore.ts +148 -20
  89. package/src/runtime/scratchpad/README.md +1 -1
  90. package/src/runtime/scratchpad/protocol.ts +1 -1
  91. package/src/runtime/settings-store.ts +1 -1
  92. package/src/runtime/skill-instructions.ts +22 -0
  93. package/src/runtime/stale-reconciler.ts +85 -13
  94. package/src/runtime/task-display.ts +1 -1
  95. package/src/runtime/task-runner/pre-execution.ts +26 -2
  96. package/src/runtime/task-runner/prompt-builder.ts +142 -45
  97. package/src/runtime/task-runner.ts +21 -1
  98. package/src/runtime/team-runner.ts +38 -1
  99. package/src/runtime/workspace-lock.ts +4 -1
  100. package/src/schema/config-schema.ts +18 -0
  101. package/src/schema/team-tool-schema.ts +17 -0
  102. package/src/state/atomic-write.ts +53 -0
  103. package/src/state/contracts.ts +109 -0
  104. package/src/state/coordination/locks.ts +191 -33
  105. package/src/state/coordination/mailbox.ts +140 -15
  106. package/src/state/crew-init.ts +87 -12
  107. package/src/state/event-log/cursor.ts +37 -1
  108. package/src/state/event-log/event-log-rotation.ts +72 -7
  109. package/src/state/stores/active-run-registry.ts +13 -1
  110. package/src/state/stores/state-store.ts +112 -22
  111. package/src/state/types.ts +4 -0
  112. package/src/ui/adaptive-card.ts +65 -0
  113. package/src/ui/agents-jobs-browser.ts +70 -64
  114. package/src/ui/card-colors.ts +36 -7
  115. package/src/ui/dashboard-panes/agents-pane.ts +55 -14
  116. package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
  117. package/src/ui/dashboard-panes/health-pane.ts +7 -5
  118. package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
  119. package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
  120. package/src/ui/dashboard-panes/pane-theme.ts +21 -0
  121. package/src/ui/dashboard-panes/plan-pane.ts +63 -30
  122. package/src/ui/dashboard-panes/progress-pane.ts +3 -2
  123. package/src/ui/dashboard-panes/schedules-pane.ts +44 -21
  124. package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
  125. package/src/ui/dwf-phase-display.ts +3 -20
  126. package/src/ui/format-helpers.ts +22 -0
  127. package/src/ui/heartbeat-aggregator.ts +34 -0
  128. package/src/ui/inline-panel/crew-editor.ts +13 -3
  129. package/src/ui/inline-panel/index.ts +60 -4
  130. package/src/ui/keybinding-map.ts +251 -35
  131. package/src/ui/live-conversation-overlay.ts +180 -47
  132. package/src/ui/live-run-sidebar.ts +134 -55
  133. package/src/ui/mascot.ts +32 -16
  134. package/src/ui/overlays/agent-picker-overlay.ts +81 -26
  135. package/src/ui/overlays/confirm-overlay.ts +55 -29
  136. package/src/ui/overlays/help-overlay.ts +108 -53
  137. package/src/ui/overlays/mailbox-compose-overlay.ts +89 -50
  138. package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
  139. package/src/ui/powerbar-publisher.ts +0 -1
  140. package/src/ui/rail.ts +333 -0
  141. package/src/ui/run-dashboard.ts +193 -79
  142. package/src/ui/run-snapshot-cache.ts +18 -1
  143. package/src/ui/settings-overlay.ts +81 -39
  144. package/src/ui/spinner.ts +26 -2
  145. package/src/ui/terminal-status.ts +7 -1
  146. package/src/ui/theme-adapter.ts +0 -45
  147. package/src/ui/theme-discovery.ts +12 -6
  148. package/src/ui/tool-progress-formatter.ts +128 -9
  149. package/src/ui/tool-renderers/brief-mode.ts +10 -67
  150. package/src/ui/tool-renderers/index.ts +374 -523
  151. package/src/ui/transcript-viewer.ts +30 -12
  152. package/src/ui/widget/index.ts +32 -52
  153. package/src/ui/widget/task-list.ts +64 -32
  154. package/src/ui/widget/widget-formatters.ts +3 -402
  155. package/src/ui/widget/widget-model.ts +28 -7
  156. package/src/ui/widget/widget-renderer.ts +201 -128
  157. package/src/ui/widget/widget-types.ts +0 -2
  158. package/src/utils/incremental-reader.ts +11 -3
  159. package/src/utils/paths.ts +94 -12
  160. package/src/utils/project-markers.ts +40 -0
  161. package/src/utils/visual.ts +0 -4
  162. package/src/worktree/worktree-manager.ts +206 -26
  163. package/workflows/distill.workflow.md +3 -3
  164. package/workflows/fast-fix.workflow.md +1 -1
  165. package/workflows/plan-execute.workflow.md +1 -1
  166. package/workflows/review.workflow.md +1 -1
  167. package/workflows/strict-fast-fix.workflow.md +1 -1
  168. package/docs/migration-v0.4-v0.5.md +0 -208
  169. package/docs/runtime-flow.md +0 -148
  170. package/src/extension/crew-vibes/figures.ts +0 -22
  171. package/src/extension/crew-vibes/font-detect.ts +0 -71
  172. package/src/ui/dynamic-border.ts +0 -35
  173. package/src/ui/loaders.ts +0 -6
  174. package/src/ui/overlay-stack.ts +0 -148
@@ -82,7 +82,11 @@ const SEEN_STEER_ID_CAP = 1024;
82
82
  * Factory-shaped so multiple prompt-runtime instances (tests, parallel
83
83
  * workers) get independent sets.
84
84
  */
85
- export function createSeenSteerIdSet(): { markOrSkip: (id?: string) => boolean; size: () => number } {
85
+ export function createSeenSteerIdSet(): {
86
+ markOrSkip: (id?: string) => boolean;
87
+ unmark: (id?: string) => void;
88
+ size: () => number;
89
+ } {
86
90
  const seen: string[] = [];
87
91
  const set = new Set<string>();
88
92
  return {
@@ -98,6 +102,15 @@ export function createSeenSteerIdSet(): { markOrSkip: (id?: string) => boolean;
98
102
  }
99
103
  return true;
100
104
  },
105
+ unmark(id?: string): void {
106
+ // F-L2: a steer whose DELIVERY failed must release its dedup mark so
107
+ // the retry (after the poll's offset rewind) is not skipped as a
108
+ // duplicate — that would silently drop the steer instead.
109
+ if (id === undefined) return;
110
+ if (!set.delete(id)) return;
111
+ const idx = seen.indexOf(id);
112
+ if (idx !== -1) seen.splice(idx, 1);
113
+ },
101
114
  size: () => set.size,
102
115
  };
103
116
  }
@@ -861,42 +874,80 @@ export default function registerPiTeamsPromptRuntime(pi: ExtensionAPI): void {
861
874
  try {
862
875
  const buf = Buffer.alloc(stat.size - lastOffset);
863
876
  fs.readSync(fd, buf, 0, buf.length, lastOffset);
864
- lastOffset = stat.size;
865
- const lines = buf.toString("utf8").split("\n").filter(Boolean);
866
- for (const line of lines) {
877
+ const offsetBefore = lastOffset;
878
+ let cursor = 0;
879
+ // F-L2 (live battery 2026-09-21, run team_20260921035840): a steer
880
+ // written while the extension is still LOADING makes pi.sendMessage
881
+ // throw ("Extension runtime not initialized" — pi's loader gates
882
+ // action methods until bindExtensions). The old code advanced
883
+ // lastOffset to stat.size BEFORE the line loop and its per-line
884
+ // catch swallowed that throw as "malformed line" — the entry was
885
+ // then silently lost forever (the offset was already past it).
886
+ // Walk the buffer by byte offset and advance only past lines with
887
+ // a terminal verdict (delivered / non-steer / rejected / malformed);
888
+ // a DELIVERY failure rewinds to the failing line's start so the
889
+ // next tick re-reads it (with its dedup id unmarked).
890
+ while (cursor < buf.length) {
891
+ const nl = buf.indexOf(10, cursor);
892
+ const end = nl === -1 ? buf.length : nl;
893
+ const line = buf.subarray(cursor, end).toString("utf8");
894
+ if (line.length === 0) {
895
+ cursor = end + 1;
896
+ continue;
897
+ }
867
898
  try {
868
899
  const entry = JSON.parse(line) as SteerEntry;
869
- if (entry.type !== "steer") continue;
870
- // FIX-S1: cross-channel dedup. The broker writes the
871
- // same steer to both the mailbox (live fanout via the
872
- // onSteer callback below) and this JSONL file. A
873
- // connected worker receives it via the broker first
874
- // and via this poll second; the seen-id set ensures
875
- // only the first arrival reaches pi.sendMessage.
876
- const entryId =
877
- typeof (entry as { id?: unknown }).id === "string" ? (entry as { id: string }).id : undefined;
878
- if (!seenSteers.markOrSkip(entryId)) continue;
879
- // FIX-02: sanitize each steer entry before forwarding
880
- // to pi.sendMessage. Reject oversized payloads,
881
- // excessive newlines, and control characters.
882
- const sanitized = sanitizeSteerMessage(entry);
883
- if (!sanitized.valid || sanitized.message === undefined) {
884
- logInternalError(
885
- "prompt-runtime.steer-rejected",
886
- new Error(sanitized.reason ?? "steer-sanitization-failed"),
887
- `line-preview=${line.slice(0, 64)}`,
888
- "warn",
889
- );
890
- continue;
900
+ if (entry.type === "steer") {
901
+ // FIX-S1: cross-channel dedup. The broker writes the
902
+ // same steer to both the mailbox (live fanout via the
903
+ // onSteer callback below) and this JSONL file. A
904
+ // connected worker receives it via the broker first
905
+ // and via this poll second; the seen-id set ensures
906
+ // only the first arrival reaches pi.sendMessage.
907
+ const entryId =
908
+ typeof (entry as { id?: unknown }).id === "string" ? (entry as { id: string }).id : undefined;
909
+ if (seenSteers.markOrSkip(entryId)) {
910
+ // FIX-02: sanitize each steer entry before forwarding
911
+ // to pi.sendMessage. Reject oversized payloads,
912
+ // excessive newlines, and control characters.
913
+ const sanitized = sanitizeSteerMessage(entry);
914
+ if (sanitized.valid && sanitized.message !== undefined) {
915
+ try {
916
+ pi.sendMessage(
917
+ { customType: "crew-steer", content: sanitized.message, display: false },
918
+ { deliverAs: "steer" },
919
+ );
920
+ } catch (sendErr) {
921
+ // Delivery failed (e.g. pre-bind notInitialized).
922
+ // Release the dedup mark so the retry after the
923
+ // offset rewind is not skipped as a duplicate, then
924
+ // rewind: cursor stays at this line's start so the
925
+ // next tick re-reads the entry.
926
+ seenSteers.unmark(entryId);
927
+ logInternalError(
928
+ "prompt-runtime.steer-delivery-failed",
929
+ sendErr instanceof Error ? sendErr : new Error(String(sendErr)),
930
+ `line-preview=${line.slice(0, 64)}`,
931
+ "warn",
932
+ );
933
+ break;
934
+ }
935
+ } else {
936
+ logInternalError(
937
+ "prompt-runtime.steer-rejected",
938
+ new Error(sanitized.reason ?? "steer-sanitization-failed"),
939
+ `line-preview=${line.slice(0, 64)}`,
940
+ "warn",
941
+ );
942
+ }
943
+ }
891
944
  }
892
- pi.sendMessage(
893
- { customType: "crew-steer", content: sanitized.message, display: false },
894
- { deliverAs: "steer" },
895
- );
896
945
  } catch {
897
- // Malformed line — skip
946
+ // Malformed line — skip (terminal verdict — advance past)
898
947
  }
948
+ cursor = end + 1;
899
949
  }
950
+ lastOffset = offsetBefore + Math.min(cursor, buf.length);
900
951
  } finally {
901
952
  try {
902
953
  fs.closeSync(fd);
@@ -107,6 +107,18 @@ export function createWorkerEventsChannel(options: WorkerEventsChannelOptions =
107
107
  const write = (item: { type: string; data: Record<string, unknown>; droppedSinceLast: number }): boolean => {
108
108
  try {
109
109
  appendEvent(eventsPath as string, {
110
+ // TeamEvent contract: `time` (ISO string) is REQUIRED — the import
111
+ // bundle validator (run-bundle-schema.ts validateEvent) rejects the
112
+ // whole bundle when any event lacks it, and event readers sort by it.
113
+ // The channel's default appender is a raw O_APPEND writer (NOT
114
+ // event-log.ts appendEvent, which stamps time itself), so the stamp
115
+ // must happen HERE — once, at the single choke point both emit()
116
+ // and emitTerminal() pass through. Found live 2026-09-21: every
117
+ // worker.started/completed of a tmux-surface run was written
118
+ // time-less, and exporting that run produced an un-importable bundle
119
+ // ("events[i].time must be a string"). Uses the injected clock so
120
+ // tests stay deterministic.
121
+ time: new Date(now()).toISOString(),
110
122
  type: item.type,
111
123
  runId,
112
124
  taskId,
@@ -32,4 +32,4 @@ clusters are extracted into subdirectories; the remaining files stay at the root
32
32
  - **observability** — `effectiveness.ts`, `agent-observability.ts`, `command-trace.ts`
33
33
  - **misc** — `stale-reconciler.ts`, `parent-guard.ts`, `manifest-cache.ts`, `event-stream-bridge.ts`, `policy-engine.ts`, `role-permission.ts`, `workspace-lock.ts`, `workspace-tree.ts`, `session-*.ts`, ...
34
34
 
35
- See `docs/COVERAGE-ASSESSMENT-2026-08-01.md` § "Source reorganization" for the reorg history + remaining-cluster plan.
35
+ See `docs/archive/COVERAGE-ASSESSMENT-2026-08-01.md` § "Source reorganization" for the reorg history + remaining-cluster plan.
@@ -209,6 +209,9 @@ export const BACKGROUND_RUNNER_ENV_ALLOWLIST: string[] = [
209
209
  "PI_TEAMS_PI_BIN",
210
210
  "PI_TEAMS_MOCK_CHILD_PI",
211
211
  "PI_CREW_ALLOW_MOCK",
212
+ // Entry guard for the detached runner main() (background-runner.ts skips main()
213
+ // on plain imports — the inline async test seam lazy-imports it).
214
+ "PI_CREW_BACKGROUND_RUNNER_ENTRY",
212
215
  // Phase 1.5: worker-thread atomic writer opt-in (RFC 15).
213
216
  "PI_CREW_WORKER_ATOMIC_WRITER",
214
217
  "PI_TEAMS_WORKER_ATOMIC_WRITER",
@@ -262,7 +265,87 @@ export function buildBrokerStdinLine(runId: string, socketPath: string, tasks: R
262
265
  return `${JSON.stringify({ v: 2, runId, socketPath, tasks })}\n`;
263
266
  }
264
267
 
268
+ /**
269
+ * IN-PROCESS ASYNC TEST SEAM (kills two classes of test pain):
270
+ * 1. Windows Defender first-spawn stall — CI ground truth (run 36093514388
271
+ * attempt 3): the detached background-runner spawn stalled >300s with the
272
+ * task left "queued" while the event loop stayed healthy. No second
273
+ * process to come up = no per-file-hash AV scan inside a test deadline.
274
+ * 2. Orphan-tmpdir leak — a detached runner survives the test's rmSync and
275
+ * keeps writing run state into a deleted tree (source of the ~3.4k
276
+ * pi-crew-* debris, 2026-09-26 triage). In-process, the run dies with the
277
+ * test process.
278
+ *
279
+ * Gated by BOTH PI_CREW_TEST_ASYNC_INLINE=1 AND the existing test-fixture
280
+ * gate PI_CREW_ALLOW_MOCK=1 (defense-in-depth: production never sets either).
281
+ * The inline host reuses background-runner's executeBackgroundRun core —
282
+ * same code path as the detached runner, no drift.
283
+ */
284
+ export function isInlineAsyncTestSeamActive(): boolean {
285
+ return getCrewEnv("PI_CREW_TEST_ASYNC_INLINE") === "1" && getCrewEnv("PI_CREW_ALLOW_MOCK") === "1";
286
+ }
287
+
288
+ async function runInlineBackgroundTeamRun(manifest: TeamRunManifest, logPath: string): Promise<SpawnBackgroundTeamRunResult> {
289
+ const log = (message: string): void => {
290
+ try {
291
+ fs.appendFileSync(logPath, `${message}\n`, "utf-8");
292
+ } catch {
293
+ /* best-effort */
294
+ }
295
+ };
296
+ log(`[pi-crew] inline test seam: executing run ${manifest.runId} in-process (no detached runner) pid=${process.pid}`);
297
+ await appendEventAsync(manifest.eventsPath, {
298
+ type: "async.spawned",
299
+ runId: manifest.runId,
300
+ data: { pid: process.pid, logPath, inline: true },
301
+ });
302
+ // Fire-and-forget — spawn() semantics: the caller must NOT await the run.
303
+ // Catch-all: a late rejection after the test ended must not become an
304
+ // unhandledRejection that fails the whole file.
305
+ void (async () => {
306
+ try {
307
+ // LAZY: defer the core import — background-runner must not load in the
308
+ // normal spawn path (and its module-scope main() is entry-guarded).
309
+ const { executeBackgroundRun } = await import("./background-runner.ts"); // LAZY: defer the core import — background-runner must not load in the normal spawn path
310
+ const { loadRunManifestById } = await import("../state/stores/state-store.ts"); // LAZY: state-store pulls the whole stores chain
311
+ const { withRunLockSync } = await import("../state/coordination/locks.ts"); // LAZY: coordination locks only needed on the inline path
312
+ const loaded = withRunLockSync(manifest, () => loadRunManifestById(manifest.cwd, manifest.runId), { staleMs: 30_000 });
313
+ const effectiveManifest = loaded?.manifest ?? manifest;
314
+ const tasks = loaded?.tasks ?? [];
315
+ await executeBackgroundRun({ manifest: effectiveManifest, tasks }, { log: (message) => log(`[inline] ${message}`) });
316
+ } catch (error) {
317
+ const message = error instanceof Error ? error.message : String(error);
318
+ log(`[pi-crew] inline test seam run ${manifest.runId} FAILED: ${message}`);
319
+ try {
320
+ const { loadRunManifestByIdAsync: reload, updateRunStatus } = await import("../state/stores/state-store.ts"); // LAZY: failure-path imports only
321
+ const fresh = (await reload(manifest.cwd, manifest.runId))?.manifest;
322
+ if (fresh) updateRunStatus(fresh, "failed", `inline test seam: ${message}`);
323
+ else
324
+ await appendEventAsync(manifest.eventsPath, {
325
+ type: "async.failed",
326
+ runId: manifest.runId,
327
+ message: `inline test seam: ${message}`,
328
+ });
329
+ if (fresh)
330
+ await appendEventAsync(fresh.eventsPath, {
331
+ type: "async.failed",
332
+ runId: fresh.runId,
333
+ message: `inline test seam: ${message}`,
334
+ });
335
+ } catch {
336
+ /* best-effort */
337
+ }
338
+ }
339
+ })();
340
+ return { pid: process.pid, logPath };
341
+ }
342
+
265
343
  export async function spawnBackgroundTeamRun(manifest: TeamRunManifest): Promise<SpawnBackgroundTeamRunResult> {
344
+ if (isInlineAsyncTestSeamActive()) {
345
+ const logPath = path.join(manifest.stateRoot, "background.log");
346
+ fs.mkdirSync(manifest.stateRoot, { recursive: true });
347
+ return runInlineBackgroundTeamRun(manifest, logPath);
348
+ }
266
349
  // FIX (2026-07-02, perf review F-critical): use packageRoot() instead of
267
350
  // import.meta.url-relative path. The previous path.resolve walks
268
351
  // <bundleDir>/background-runner.ts, which is correct in src/ but BROKEN
@@ -291,7 +374,10 @@ export async function spawnBackgroundTeamRun(manifest: TeamRunManifest): Promise
291
374
  // `npm root -g` probe. No-op when pi-crew and pi are co-located. See
292
375
  // src/runtime/peer-dep.ts.
293
376
  const peerDepDir = resolvePeerDepDir();
294
- const childEnv = buildBackgroundRunnerEnv(peerDepDir ? { ...filteredEnv, [PEER_DEP_DIR_ENV]: peerDepDir } : filteredEnv);
377
+ const childEnv = buildBackgroundRunnerEnv({
378
+ ...(peerDepDir ? { ...filteredEnv, [PEER_DEP_DIR_ENV]: peerDepDir } : filteredEnv),
379
+ PI_CREW_BACKGROUND_RUNNER_ENTRY: "1",
380
+ });
295
381
 
296
382
  const loader = resolveTypeScriptLoader();
297
383
  if (!loader) {