pi-crew 0.9.41 → 0.9.44

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 (170) hide show
  1. package/CHANGELOG.md +253 -0
  2. package/README.md +14 -0
  3. package/dist/build-meta.json +6450 -12419
  4. package/dist/index.mjs +57998 -68417
  5. package/dist/index.mjs.map +4 -4
  6. package/docs/migration/atomic-write-v2-migration.md +2 -0
  7. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +2 -0
  8. package/package.json +4 -2
  9. package/scripts/README.md +6 -0
  10. package/scripts/build-bundle.mjs +2 -1
  11. package/scripts/check-all-skills.ts +3 -3
  12. package/scripts/dev-runner.mjs +106 -0
  13. package/scripts/postinstall.mjs +6 -2
  14. package/scripts/verify-skill.ts +2 -2
  15. package/src/agents/agent-config.ts +1 -1
  16. package/src/config/config.ts +20 -1
  17. package/src/config/drift-detector.ts +97 -15
  18. package/src/config/resilient-parser.ts +4 -4
  19. package/src/config/types.ts +0 -2
  20. package/src/extension/crew-cleanup.ts +17 -3
  21. package/src/extension/crew-vibes/config.ts +1 -1
  22. package/src/extension/crew-vibes/font-detect.ts +16 -3
  23. package/src/extension/crew-vibes/index.ts +1 -1
  24. package/src/extension/cross-extension-rpc.ts +26 -9
  25. package/src/extension/management.ts +6 -5
  26. package/src/extension/register.ts +58 -1573
  27. package/src/extension/registration/command-registration.ts +57 -0
  28. package/src/extension/registration/commands.ts +7 -2
  29. package/src/extension/registration/context-builder.ts +131 -0
  30. package/src/extension/registration/crash-recovery-cache.ts +54 -0
  31. package/src/extension/registration/foreground-run-controller.ts +238 -0
  32. package/src/extension/registration/hook-registration.ts +119 -0
  33. package/src/extension/registration/lazy-configurers.ts +124 -0
  34. package/src/extension/registration/lifecycle-handlers.ts +731 -0
  35. package/src/extension/registration/observability.ts +33 -4
  36. package/src/extension/registration/registration-types.ts +149 -0
  37. package/src/extension/registration/runtime-cleanup.ts +230 -0
  38. package/src/extension/registration/subagent-manager-setup.ts +211 -0
  39. package/src/extension/registration/tool-registration.ts +50 -0
  40. package/src/extension/registration/wire-cross-extension.ts +37 -0
  41. package/src/extension/rpc-hmac.ts +21 -1
  42. package/src/extension/run-import.ts +3 -3
  43. package/src/extension/team-tool/auto-summarize.ts +4 -4
  44. package/src/extension/team-tool/cancel.ts +5 -5
  45. package/src/extension/team-tool/chain-executor.ts +5 -4
  46. package/src/extension/team-tool/context.ts +11 -1
  47. package/src/extension/team-tool/doctor.ts +2 -1
  48. package/src/extension/team-tool/goal-wrap.ts +2 -2
  49. package/src/extension/team-tool/goal.ts +1 -1
  50. package/src/extension/team-tool/handle-schedule.ts +25 -36
  51. package/src/extension/team-tool/orchestrate.ts +0 -1
  52. package/src/extension/team-tool/parallel-dispatch.ts +5 -0
  53. package/src/extension/team-tool/run.ts +220 -214
  54. package/src/extension/team-tool/workflow-manage.ts +1 -1
  55. package/src/extension/team-tool.ts +69 -16
  56. package/src/hooks/registry.ts +7 -2
  57. package/src/observability/correlation.ts +4 -3
  58. package/src/observability/event-bus.ts +10 -1
  59. package/src/observability/event-to-metric.ts +8 -7
  60. package/src/observability/exporters/adapter.ts +6 -9
  61. package/src/observability/exporters/otlp-exporter.ts +81 -0
  62. package/src/observability/metric-retention.ts +4 -0
  63. package/src/observability/metric-sink.ts +14 -4
  64. package/src/prompt/prompt-runtime.ts +15 -0
  65. package/src/runtime/adaptive-plan.ts +1 -1
  66. package/src/runtime/async-runner.ts +13 -4
  67. package/src/runtime/background-runner.ts +32 -23
  68. package/src/runtime/capability-inventory.ts +1 -1
  69. package/src/runtime/chain-runner.ts +5 -5
  70. package/src/runtime/checkpoint.ts +6 -16
  71. package/src/runtime/child-pi-constants.ts +42 -0
  72. package/src/runtime/child-pi-kill.ts +180 -0
  73. package/src/runtime/child-pi-spawn.ts +234 -0
  74. package/src/runtime/child-pi-steering.ts +128 -0
  75. package/src/runtime/child-pi-streams.ts +296 -0
  76. package/src/runtime/child-pi-transcript.ts +169 -0
  77. package/src/runtime/child-pi.ts +80 -822
  78. package/src/runtime/coalesce-tasks.ts +1 -1
  79. package/src/runtime/compact-stages/tail-capture-stage.ts +1 -1
  80. package/src/runtime/crash-recovery.ts +63 -47
  81. package/src/runtime/delivery-coordinator.ts +1 -1
  82. package/src/runtime/deterministic-ast.ts +2 -2
  83. package/src/runtime/diagnostic-export.ts +2 -1
  84. package/src/runtime/dwf-state-store.ts +5 -0
  85. package/src/runtime/dynamic-workflow-context.ts +78 -18
  86. package/src/runtime/dynamic-workflow-runner.ts +18 -2
  87. package/src/runtime/foreground-control.ts +3 -2
  88. package/src/runtime/goal-evaluator.ts +59 -27
  89. package/src/runtime/goal-loop-runner.ts +46 -13
  90. package/src/runtime/goal-state-store.ts +1 -1
  91. package/src/runtime/handoff-manager.ts +0 -2
  92. package/src/runtime/intercom-bridge.ts +0 -2
  93. package/src/runtime/live-agent-manager.ts +30 -5
  94. package/src/runtime/live-extension-bridge.ts +0 -2
  95. package/src/runtime/live-session-runtime.ts +65 -57
  96. package/src/runtime/manifest-cache.ts +59 -0
  97. package/src/runtime/mcp-proxy.ts +1 -1
  98. package/src/runtime/orphan-worker-registry.ts +3 -3
  99. package/src/runtime/output-validator.ts +1 -1
  100. package/src/runtime/path-overlap.ts +1 -1
  101. package/src/runtime/pi-args.ts +3 -3
  102. package/src/runtime/pi-json-output.ts +4 -3
  103. package/src/runtime/pi-spawn.ts +7 -1
  104. package/src/runtime/pipeline-runner.ts +1 -4
  105. package/src/runtime/post-checks.ts +0 -1
  106. package/src/runtime/retry-runner.ts +1 -1
  107. package/src/runtime/run-coalesced-task-group.ts +18 -23
  108. package/src/runtime/scheduler.ts +2 -0
  109. package/src/runtime/session-resources.ts +1 -1
  110. package/src/runtime/settings-store.ts +31 -1
  111. package/src/runtime/skill-effectiveness.ts +2 -2
  112. package/src/runtime/stale-reconciler.ts +23 -53
  113. package/src/runtime/subagent-manager.ts +19 -3
  114. package/src/runtime/task-output-context.ts +2 -1
  115. package/src/runtime/task-runner/prompt-builder.ts +3 -3
  116. package/src/runtime/task-runner/run-projection.ts +3 -1
  117. package/src/runtime/task-runner/state-helpers.ts +13 -1
  118. package/src/runtime/task-runner.ts +31 -6
  119. package/src/runtime/team-runner.ts +8 -2
  120. package/src/runtime/verification-gates.ts +54 -5
  121. package/src/schema/config-schema.ts +19 -1
  122. package/src/schema/team-tool-schema.ts +2 -0
  123. package/src/skills/discover-skills.ts +25 -15
  124. package/src/state/active-run-registry.ts +0 -1
  125. package/src/state/atomic-write.ts +37 -11
  126. package/src/state/blob-store.ts +3 -9
  127. package/src/state/contracts.ts +5 -1
  128. package/src/state/crew-init.ts +3 -2
  129. package/src/state/decision-ledger.ts +2 -2
  130. package/src/state/event-log-rotation.ts +1 -1
  131. package/src/state/event-log.ts +4 -3
  132. package/src/state/gitignore-manager.ts +2 -1
  133. package/src/state/health-store.ts +5 -2
  134. package/src/state/instinct-store.ts +11 -2
  135. package/src/state/locks.ts +132 -49
  136. package/src/state/mailbox.ts +3 -4
  137. package/src/state/observation-store.ts +1 -13
  138. package/src/state/run-cache.ts +12 -12
  139. package/src/state/run-graph.ts +2 -1
  140. package/src/state/run-metrics.ts +1 -1
  141. package/src/state/schedule.ts +7 -1
  142. package/src/state/state-store.ts +9 -3
  143. package/src/state/tiered-eval.ts +1 -1
  144. package/src/state/worker-atomic-writer.ts +1 -0
  145. package/src/ui/dynamic-border.ts +13 -3
  146. package/src/ui/live-conversation-overlay.ts +7 -27
  147. package/src/ui/live-duration.ts +2 -2
  148. package/src/ui/live-run-sidebar.ts +56 -32
  149. package/src/ui/mascot.ts +19 -13
  150. package/src/ui/powerbar-publisher.ts +244 -178
  151. package/src/ui/render-coalescer.ts +44 -3
  152. package/src/ui/render-diff.ts +2 -2
  153. package/src/ui/run-dashboard.ts +122 -21
  154. package/src/ui/run-event-bus.ts +66 -0
  155. package/src/ui/run-snapshot-cache.ts +84 -81
  156. package/src/ui/settings-overlay.ts +3 -3
  157. package/src/ui/status-colors.ts +24 -0
  158. package/src/ui/tool-progress-formatter.ts +1 -1
  159. package/src/ui/tool-render.ts +3 -3
  160. package/src/ui/transcript-cache.ts +5 -10
  161. package/src/ui/transcript-entries.ts +68 -72
  162. package/src/ui/transcript-viewer.ts +54 -14
  163. package/src/ui/widget/index.ts +26 -15
  164. package/src/ui/widget/widget-model.ts +18 -9
  165. package/src/ui/widget/widget-renderer.ts +2 -5
  166. package/src/utils/fingerprint.ts +1 -1
  167. package/src/utils/paths.ts +1 -1
  168. package/src/utils/project-detector.ts +1 -1
  169. package/src/worktree/worktree-manager.ts +47 -19
  170. package/src/state/atomic-write-v2.ts +0 -108
@@ -16,6 +16,7 @@
16
16
 
17
17
  import { randomBytes } from "node:crypto";
18
18
  import { existsSync, readdirSync } from "node:fs";
19
+ import { createRequire } from "node:module";
19
20
  import type { AgentConfig } from "../agents/agent-config.ts";
20
21
  import { loadConfig } from "../config/config.ts";
21
22
  import { effectiveRunConfig } from "../extension/team-tool/config-patch.ts";
@@ -65,21 +66,29 @@ export const stubGoalEvaluator = async (
65
66
  _m?: import("../state/types.ts").TeamRunManifest,
66
67
  _t?: import("../state/types.ts").TeamTaskState[],
67
68
  _s?: AbortSignal,
68
- ): Promise<GoalVerdict> => ({
69
- turn: goal.turnsUsed,
70
- achieved: false,
71
- reason: `not-achieved: stub evaluator (P0). Turn ${goal.turnsUsed}/${goal.maxTurns} completed; P1 will judge against objective + verification.`,
72
- evaluatorModel: "stub",
73
- evaluatedAt: new Date().toISOString(),
69
+ ): Promise<GoalEvaluatorResult> => ({
70
+ verdict: {
71
+ turn: goal.turnsUsed,
72
+ achieved: false,
73
+ reason: `not-achieved: stub evaluator (P0). Turn ${goal.turnsUsed}/${goal.maxTurns} completed; P1 will judge against objective + verification.`,
74
+ evaluatorModel: "stub",
75
+ evaluatedAt: new Date().toISOString(),
76
+ },
77
+ judgeUsage: undefined,
74
78
  });
75
79
 
80
+ export type GoalEvaluatorResult = {
81
+ verdict: GoalVerdict;
82
+ judgeUsage?: { totalTokens: number; inputTokens?: number; outputTokens?: number };
83
+ };
84
+
76
85
  export type GoalEvaluatorFn = (
77
86
  goal: GoalLoopState,
78
87
  turnRunId: string,
79
88
  turnManifest: import("../state/types.ts").TeamRunManifest,
80
89
  turnTasks: import("../state/types.ts").TeamTaskState[],
81
90
  signal: AbortSignal,
82
- ) => Promise<GoalVerdict>;
91
+ ) => Promise<GoalEvaluatorResult>;
83
92
 
84
93
  /**
85
94
  * Production evaluator (P1): bundles turn evidence + calls the LLM judge.
@@ -93,7 +102,7 @@ export const realGoalEvaluator = async (
93
102
  turnManifest: import("../state/types.ts").TeamRunManifest,
94
103
  turnTasks: import("../state/types.ts").TeamTaskState[],
95
104
  signal: AbortSignal,
96
- ): Promise<GoalVerdict> => {
105
+ ): Promise<GoalEvaluatorResult> => {
97
106
  const transcriptPath = deriveTranscriptPath(turnManifest.artifactsRoot, turnTasks);
98
107
  // Fix round-7 F1: execute verification commands (if configured) so the judge has real evidence.
99
108
  // Previously bundleEvidence received `undefined` — the judge was told commands "MUST pass"
@@ -260,8 +269,11 @@ function resolveGoalTurnWorkflow(goal: GoalLoopState): WorkflowConfig {
260
269
  const wrapName = (goal as GoalLoopState & { goalWrapWorkflow?: string }).goalWrapWorkflow;
261
270
  if (!wrapName) return buildTurnWorkflow();
262
271
  try {
263
- const { discoverWorkflows, allWorkflows } =
264
- require("../workflows/discover-workflows.ts") as typeof import("../workflows/discover-workflows.ts");
272
+ // LAZY: createRequire for synchronous module resolution in ESM context
273
+ const requireFromHere = createRequire(import.meta.url);
274
+ const { discoverWorkflows, allWorkflows } = requireFromHere(
275
+ "../workflows/discover-workflows.ts",
276
+ ) as typeof import("../workflows/discover-workflows.ts");
265
277
  const found = allWorkflows(discoverWorkflows(goal.cwd)).find((w) => w.name === wrapName && w.source === "builtin");
266
278
  if (found) return found;
267
279
  logInternalError(
@@ -672,17 +684,38 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
672
684
  logInternalError("goal-loop.saveTurnTasks", error, `turnRunId=${created.manifest.runId}`);
673
685
  }
674
686
 
687
+ // GL-1: if the turn ended in blocked/failed status, stop the loop without spawning the judge.
688
+ // Previously the loop continued to evaluateGoal on blocked/failed turns, burning maxTurns.
689
+ const turnStatus = turnResult.manifest.status;
690
+ if (turnStatus === "blocked" || turnStatus === "failed") {
691
+ goal = safeSetStatus(store, goal.goalId, "blocked", goal, eventsPath);
692
+ appendEvent(eventsPath, {
693
+ type: "goal.turn_terminal_status",
694
+ runId: manifest.runId,
695
+ data: {
696
+ goalId: goal.goalId,
697
+ turn: turnIndex,
698
+ turnRunId: created.manifest.runId,
699
+ turnStatus,
700
+ },
701
+ });
702
+ break;
703
+ }
704
+
675
705
  // ── BUDGET accumulation (§0c C2: collectRunMetrics) ──────────────────────
676
- const updatedBudget = accumulateBudget(goal, created.manifest.runId);
706
+ const turnBudget = accumulateBudget(goal, created.manifest.runId);
677
707
 
678
708
  // ── EVALUATE (P1: real LLM judge; pass turn manifest + tasks for transcript lookup) ──
679
- const verdict = await evaluator(
680
- { ...goal, budgetUsed: updatedBudget },
709
+ const { verdict, judgeUsage } = await evaluator(
710
+ { ...goal, budgetUsed: turnBudget },
681
711
  created.manifest.runId,
682
712
  turnResult.manifest,
683
713
  turnResult.tasks,
684
714
  signal,
685
715
  );
716
+ // BDG-1: accumulate judge token usage into budget alongside the turn's worker tokens.
717
+ const judgeTokens = judgeUsage?.totalTokens ?? 0;
718
+ const updatedBudget = turnBudget + judgeTokens;
686
719
  const historyEntry = {
687
720
  runId: created.manifest.runId,
688
721
  outcome: verdict.achieved ? "achieved" : "not-achieved",
@@ -13,7 +13,7 @@
13
13
  * per-turn usage stays on each turn's TeamRunManifest/tasks.json.
14
14
  */
15
15
 
16
- import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
16
+ import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
17
17
  import { dirname } from "node:path";
18
18
  import { atomicWriteJson } from "../state/atomic-write.ts";
19
19
  import { appendEvent } from "../state/event-log.ts";
@@ -54,8 +54,6 @@ export function isValidHandoffSummary(value: unknown): value is HandoffSummary {
54
54
  * @see docs/pi-boomerang-integration-plan.md
55
55
  */
56
56
 
57
- import type { TeamEvent } from "../state/event-log.ts";
58
-
59
57
  /**
60
58
  * Represents a key decision made during task execution.
61
59
  */
@@ -8,8 +8,6 @@
8
8
  * The actual tool registration happens in task-runner.ts.
9
9
  */
10
10
 
11
- import { logInternalError } from "../utils/internal-error.ts";
12
-
13
11
  // ── Types ────────────────────────────────────────────────────────────────
14
12
 
15
13
  export type IntercomUrgency = "low" | "medium" | "high" | "critical";
@@ -2,6 +2,7 @@ import type { appendEvent } from "../state/event-log.ts";
2
2
  import { logInternalError } from "../utils/internal-error.ts";
3
3
  import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
4
4
  import type { IrcMessage } from "./live-irc.ts";
5
+ import { checkProcessLiveness } from "./process-status.ts";
5
6
 
6
7
  const MAX_PENDING_MESSAGES = 1000;
7
8
 
@@ -263,10 +264,34 @@ export function evictStaleLiveAgentHandles(now = Date.now()): number {
263
264
  evicted++;
264
265
  }
265
266
  } else if (age > STALE_RUNNING_HANDLE_MS) {
266
- // Active-status handle with no update for 30min — presumed dead
267
- liveAgents.delete(agentId);
268
- safeDisposeLiveSession(handle);
269
- evicted++;
267
+ // Active-status handle with no update for 30min — check process liveness
268
+ // before evicting. Only evict if the process is dead, to avoid evicting
269
+ // slow-but-alive agents.
270
+ const sessionPid = (handle.session as Record<string, unknown>)?.pid as number | undefined;
271
+ if (sessionPid === undefined) {
272
+ // In-process live-session agents have no pid — the Pi SDK returns
273
+ // in-process objects, not child processes. checkProcessLiveness(undefined)
274
+ // returns {alive:false} which would wrongly evict a running agent.
275
+ // Check session-level activity signals first. If the session is
276
+ // actively streaming or has pending messages, it is alive — skip.
277
+ // Otherwise, rely on the idle timeout alone: do NOT dispose just
278
+ // because pid is undefined.
279
+ const session = handle.session as Record<string, unknown>;
280
+ const isStreaming = session?.isStreaming === true;
281
+ const pendingCount = (session?.pendingMessageCount as number | undefined) ?? 0;
282
+ if (isStreaming || pendingCount > 0) {
283
+ // Session is active — skip eviction.
284
+ continue;
285
+ }
286
+ // No activity signals and no pid — do NOT evict based on PID liveness.
287
+ continue;
288
+ }
289
+ const liveness = checkProcessLiveness(sessionPid);
290
+ if (!liveness.alive) {
291
+ liveAgents.delete(agentId);
292
+ safeDisposeLiveSession(handle);
293
+ evicted++;
294
+ }
270
295
  }
271
296
  }
272
297
  return evicted;
@@ -282,7 +307,7 @@ export function listActiveLiveAgents(): LiveAgentHandle[] {
282
307
 
283
308
  export function getLiveAgentContextPercent(agentIdOrTaskId: string): number | null {
284
309
  const handle = getLiveAgent(agentIdOrTaskId);
285
- if (!handle || handle.status !== "running") return null;
310
+ if (handle?.status !== "running") return null;
286
311
  try {
287
312
  return handle.session.getSessionStats?.().contextUsage?.percent ?? null;
288
313
  } catch {
@@ -29,8 +29,6 @@
29
29
  * - setActiveTools / getActiveTools / getAllTools
30
30
  */
31
31
 
32
- import type { YieldResult } from "./yield-handler.ts";
33
-
34
32
  export interface ExtensionBridgeApis {
35
33
  sendMessage: (message: unknown, options?: Record<string, unknown>) => void;
36
34
  sendUserMessage: (content: unknown, options?: Record<string, unknown>) => void;
@@ -27,7 +27,7 @@ import {
27
27
  } from "./live-agent-manager.ts";
28
28
  import { subscribeLiveControlRealtime } from "./live-control-realtime.ts";
29
29
  import { buildExtensionBridge } from "./live-extension-bridge.ts";
30
- import { collectLiveSessionHealth, formatLiveSessionDiagnostics, type LiveSessionHealth } from "./live-session-health.ts";
30
+ import { collectLiveSessionHealth, formatLiveSessionDiagnostics } from "./live-session-health.ts";
31
31
  import { buildMcpProxyFromSession } from "./mcp-proxy.ts";
32
32
  import { buildConfiguredModelRouting } from "./model-fallback.ts";
33
33
  import { readEnabledModelsPatterns } from "./model-scope.ts";
@@ -231,7 +231,7 @@ function eventText(event: unknown): string[] {
231
231
 
232
232
  function finalAssistantText(event: unknown): string[] {
233
233
  const obj = asRecord(event);
234
- if (!obj || obj.type !== "message_end") return [];
234
+ if (obj?.type !== "message_end") return [];
235
235
  const message = asRecord(obj.message);
236
236
  if (message?.role !== "assistant") return [];
237
237
  return textFromContent(message.content);
@@ -266,7 +266,7 @@ async function resolveScopeModelsPatterns(cwd: string, agentDir?: string): Promi
266
266
  }
267
267
 
268
268
  function modelFromRegistry(modelRegistry: unknown, modelId: string | undefined): unknown {
269
- if (!modelId || !modelId.includes("/")) return undefined;
269
+ if (!modelId?.includes("/")) return undefined;
270
270
  const registry = asRecord(modelRegistry);
271
271
  const find = registry?.find;
272
272
  if (typeof find !== "function") return undefined;
@@ -450,7 +450,7 @@ function filterActiveTools(session: LiveSessionLike, agent: AgentConfig, role?:
450
450
  const allowed = policy.tools?.length ? new Set(policy.tools) : undefined;
451
451
  const active = session
452
452
  .getActiveToolNames()
453
- .filter((name) => !recursiveTools.has(name) && (!disallowed || !disallowed.has(name)) && (!allowed || allowed.has(name)));
453
+ .filter((name) => !recursiveTools.has(name) && !disallowed?.has(name) && (!allowed || allowed.has(name)));
454
454
  session.setActiveToolsByName(active);
455
455
  }
456
456
 
@@ -822,61 +822,69 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
822
822
  });
823
823
  if (typeof session.subscribe === "function") {
824
824
  unsubscribe = session.subscribe((event) => {
825
- if (!isCurrent()) return;
826
- jsonEvents += 1;
827
- appendTranscript(input.transcriptPath, event);
828
- const sidechainType = eventToSidechainType(event);
829
- if (sidechainType)
830
- writeSidechainEntry(sidechainPath, {
831
- agentId,
832
- type: sidechainType,
833
- message: event,
834
- cwd: input.task.cwd,
835
- });
836
- const obj = asRecord(event);
837
- if (obj?.type === "turn_end") {
838
- turnCount += 1;
839
- trackLiveAgentTurnEnd(agentId);
840
- if (maxTurns !== undefined && !softLimitReached && turnCount >= maxTurns) {
841
- softLimitReached = true;
842
- void session?.steer?.("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
843
- } else if (maxTurns !== undefined && softLimitReached && turnCount >= maxTurns + graceTurns) {
844
- void session?.abort?.();
845
- }
846
- }
847
- // Accumulate lifetime usage that survives compaction
848
- if (obj?.type === "message_end" && extractMessageRole(obj) === "assistant") {
849
- const u = extractMessageUsage(obj);
850
- if (u) {
851
- trackTaskUsage(input.task.id, {
852
- input: typeof u.input === "number" ? u.input : 0,
853
- output: typeof u.output === "number" ? u.output : 0,
854
- cacheWrite: typeof u.cacheWrite === "number" ? u.cacheWrite : 0,
825
+ // Wrap callback body in try/catch to prevent unguarded exceptions from
826
+ // propagating into the session internals and crashing the live session.
827
+ try {
828
+ if (!isCurrent()) return;
829
+ jsonEvents += 1;
830
+ appendTranscript(input.transcriptPath, event);
831
+ const sidechainType = eventToSidechainType(event);
832
+ if (sidechainType)
833
+ writeSidechainEntry(sidechainPath, {
834
+ agentId,
835
+ type: sidechainType,
836
+ message: event,
837
+ cwd: input.task.cwd,
855
838
  });
839
+ const obj = asRecord(event);
840
+ if (obj?.type === "turn_end") {
841
+ turnCount += 1;
842
+ trackLiveAgentTurnEnd(agentId);
843
+ if (maxTurns !== undefined && !softLimitReached && turnCount >= maxTurns) {
844
+ softLimitReached = true;
845
+ void session?.steer?.("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
846
+ } else if (maxTurns !== undefined && softLimitReached && turnCount >= maxTurns + graceTurns) {
847
+ void session?.abort?.();
848
+ }
856
849
  }
857
- }
858
- input.onEvent?.(event);
859
- const text = [...eventText(event), ...finalAssistantText(event)].join("\n");
860
- if (text.trim()) {
861
- stdout += `${text}\n`;
862
- streamOut?.write(text + "\n");
863
- trackLiveAgentResponseText(agentId, text);
864
- input.onOutput?.(text);
865
- }
866
- // G2: Track tool start/end for activity display
867
- if (obj?.type === "tool_use" || obj?.type === "tool_execution_start") {
868
- const toolName = extractToolName(obj) ?? "unknown";
869
- trackLiveAgentToolStart(agentId, toolName);
870
- }
871
- if (obj?.type === "tool_result" || obj?.type === "tool_execution_end") {
872
- const toolName = extractToolName(obj) ?? "unknown";
873
- trackLiveAgentToolEnd(agentId, toolName);
874
- }
875
- // Phase 1: collect events for yield detection
876
- if (collectedJsonEvents && event && typeof event === "object" && !Array.isArray(event)) {
877
- collectedJsonEvents.push(event as Record<string, unknown>);
878
- if (collectedJsonEvents.length > maxCollectedJsonEvents)
879
- collectedJsonEvents.splice(0, collectedJsonEvents.length - maxCollectedJsonEvents);
850
+ // Accumulate lifetime usage that survives compaction
851
+ if (obj?.type === "message_end" && extractMessageRole(obj) === "assistant") {
852
+ const u = extractMessageUsage(obj);
853
+ if (u) {
854
+ trackTaskUsage(input.task.id, {
855
+ input: typeof u.input === "number" ? u.input : 0,
856
+ output: typeof u.output === "number" ? u.output : 0,
857
+ cacheWrite: typeof u.cacheWrite === "number" ? u.cacheWrite : 0,
858
+ });
859
+ }
860
+ }
861
+ input.onEvent?.(event);
862
+ const text = [...eventText(event), ...finalAssistantText(event)].join("\n");
863
+ if (text.trim()) {
864
+ stdout += `${text}\n`;
865
+ streamOut?.write(text + "\n");
866
+ trackLiveAgentResponseText(agentId, text);
867
+ input.onOutput?.(text);
868
+ }
869
+ // G2: Track tool start/end for activity display
870
+ if (obj?.type === "tool_use" || obj?.type === "tool_execution_start") {
871
+ const toolName = extractToolName(obj) ?? "unknown";
872
+ trackLiveAgentToolStart(agentId, toolName);
873
+ }
874
+ if (obj?.type === "tool_result" || obj?.type === "tool_execution_end") {
875
+ const toolName = extractToolName(obj) ?? "unknown";
876
+ trackLiveAgentToolEnd(agentId, toolName);
877
+ }
878
+ // Phase 1: collect events for yield detection
879
+ if (collectedJsonEvents && event && typeof event === "object" && !Array.isArray(event)) {
880
+ collectedJsonEvents.push(event as Record<string, unknown>);
881
+ if (collectedJsonEvents.length > maxCollectedJsonEvents)
882
+ collectedJsonEvents.splice(0, collectedJsonEvents.length - maxCollectedJsonEvents);
883
+ }
884
+ } catch (error) {
885
+ // Prevent unguarded exceptions in the subscribe callback from crashing
886
+ // the live session or propagating into the session internals.
887
+ logInternalError("live-session.subscribe-callback", error, `agentId=${agentId}`);
880
888
  }
881
889
  });
882
890
  }
@@ -9,6 +9,15 @@ import { isSafePathId, resolveContainedRelativePath, resolveRealContainedPath }
9
9
 
10
10
  export interface ManifestCache {
11
11
  list(limit?: number): TeamRunManifest[];
12
+ /**
13
+ * Return ONLY running manifests, capped only by `limit`.
14
+ *
15
+ * RT-F3: unlike `list(limit)`, this filters `status === "running"` BEFORE
16
+ * the limit is applied so stale orphaned runs that have been pushed past
17
+ * the top-N by recent activity are not silently hidden from crash-recovery
18
+ * and zombie-detection scans.
19
+ */
20
+ listActive(limit: number): TeamRunManifest[];
12
21
  get(runId: string): TeamRunManifest | undefined;
13
22
  clear(runId?: string): void;
14
23
  dispose(): void;
@@ -203,6 +212,15 @@ export function createManifestCache(cwd: string, options: ManifestCacheOptions =
203
212
  return undefined;
204
213
  }
205
214
 
215
+ /**
216
+ * NOTE (RT-F10): This function performs a full FS scan + JSON.parse of all
217
+ * manifests on every TTL expiry (default 500ms). This is a known performance
218
+ * tradeoff: fs.watch provides real-time invalidation (see constructor above)
219
+ * so the TTL cache is only consulted when the watcher fires, but the list()
220
+ * call itself still re-reads all entries to pick up changes. Incremental
221
+ * updates via fs.watch-driven delta tracking are planned for a future
222
+ * iteration to avoid the full scan on each expiry.
223
+ */
206
224
  function list(limit = DEFAULT_CACHE.manifestMaxEntries): TeamRunManifest[] {
207
225
  const now = Date.now();
208
226
  const cached = listCache.get(limit);
@@ -248,6 +266,46 @@ export function createManifestCache(cwd: string, options: ManifestCacheOptions =
248
266
  return undefined;
249
267
  }
250
268
 
269
+ /**
270
+ * RT-F3: filter to `status === "running"` BEFORE applying the limit so an
271
+ * orphaned run that has been pushed past the top-N by recent successful
272
+ * runs is still surfaced to crash-recovery. The cap is the LIMIT, not the
273
+ * "top-N most recent"; callers (e.g. zombie detection) explicitly want
274
+ * EVERY currently-running run, in any order.
275
+ *
276
+ * Internally performs a full scan (NOT via list()) so the cap doesn't
277
+ * silently drop "running" runs that fell past the top-N createdAt cutoff.
278
+ * Still goes through parseManifestIfChanged for stat+size memoization, so
279
+ * the per-run I/O cost is the same as list().
280
+ */
281
+ function listActive(limit: number): TeamRunManifest[] {
282
+ const cap = Math.max(0, limit);
283
+ const parsedEntries = [
284
+ ...roots.flatMap((root) => collectRoots(root)),
285
+ ...activeRunEntries().map((entry) => ({
286
+ runId: entry.runId,
287
+ path: entry.manifestPath,
288
+ })),
289
+ ];
290
+ const unique = new Map<string, CachedManifest | undefined>();
291
+ for (const entry of parsedEntries) {
292
+ if (entry.runId.length === 0) continue;
293
+ let cached = manifestIndex.get(entry.runId);
294
+ const root = path.dirname(path.dirname(entry.path));
295
+ const parsed = parseManifestIfChanged(root, entry.runId, entry.path, cached);
296
+ if (parsed) {
297
+ cached = parsed;
298
+ manifestIndex.set(entry.runId, cached);
299
+ }
300
+ if (cached) unique.set(entry.runId, cached);
301
+ }
302
+ const running = [...unique.values()]
303
+ .filter((value): value is CachedManifest => value !== undefined)
304
+ .map((value) => value.manifest)
305
+ .filter((manifest) => manifest.status === "running");
306
+ return running.slice(0, cap);
307
+ }
308
+
251
309
  if (options.watch ?? true) {
252
310
  for (const root of roots) {
253
311
  const watcher = watchWithErrorHandler(
@@ -268,6 +326,7 @@ export function createManifestCache(cwd: string, options: ManifestCacheOptions =
268
326
 
269
327
  return {
270
328
  list,
329
+ listActive,
271
330
  get,
272
331
  clear(runId) {
273
332
  invalidate(runId);
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
20
- import type { Static, TSchema } from "@sinclair/typebox";
20
+ import type { TSchema } from "@sinclair/typebox";
21
21
 
22
22
  export interface McpProxyConfig {
23
23
  /** Whether to enable MCP in the child session. */
@@ -22,10 +22,10 @@
22
22
  * writes from causing lost updates.
23
23
  */
24
24
 
25
- import { execFileSync, execSync } from "node:child_process";
25
+ import { execFileSync } from "node:child_process";
26
26
  import * as fs from "node:fs";
27
27
  import * as path from "node:path";
28
- import { isSymlinkSafePath } from "../state/atomic-write.ts";
28
+ import { atomicWriteFile, isSymlinkSafePath } from "../state/atomic-write.ts";
29
29
  import { withFileLockSync } from "../state/locks.ts";
30
30
  import { logInternalError } from "../utils/internal-error.ts";
31
31
  import { userPiRoot } from "../utils/paths.ts";
@@ -252,7 +252,7 @@ function writeRegistry(entries: OrphanWorkerEntry[]): void {
252
252
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
253
253
  }
254
254
  try {
255
- fs.writeFileSync(p, JSON.stringify(entries, null, 2), { mode: 0o600 });
255
+ atomicWriteFile(p, JSON.stringify(entries, null, 2), { mode: 0o600 });
256
256
  } catch (error) {
257
257
  logInternalError("orphan-worker-registry.write", error, `path=${p} entries=${entries.length}`);
258
258
  }
@@ -42,7 +42,7 @@ export function validateWorkerOutput(role: string, output: string): OutputValida
42
42
  const issues: string[] = [];
43
43
 
44
44
  // Empty output always fails
45
- if (!output || !output.trim()) {
45
+ if (!output?.trim()) {
46
46
  return {
47
47
  valid: false,
48
48
  formatMatch: false,
@@ -124,7 +124,7 @@ export function filterReadyByWriteOverlap(
124
124
  for (const taskId of readyTaskIds) {
125
125
  if (pickedTaskIds.length >= maxCount) break;
126
126
  const task = taskById.get(taskId);
127
- if (!task || !task.stepId) {
127
+ if (!task?.stepId) {
128
128
  // No step reference: treat as no declared write path → never conflicts.
129
129
  pickedTaskIds.push(taskId);
130
130
  continue;
@@ -1,9 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
4
  import type { AgentConfig } from "../agents/agent-config.ts";
6
5
  import { resolveToolPolicy } from "../agents/agent-config.ts";
6
+ import { atomicWriteFile } from "../state/atomic-write.ts";
7
7
  import { packageRoot, userPiRoot } from "../utils/paths.ts";
8
8
 
9
9
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
@@ -323,7 +323,7 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
323
323
  const tmpBase = getPiTempBase();
324
324
  tempDir = createSafeTempDir(tmpBase, `pi-crew-${process.pid}-`);
325
325
  const promptPath = path.join(tempDir, `${input.agent.name.replace(/[^\w.-]/g, "_")}.md`);
326
- fs.writeFileSync(promptPath, input.agent.systemPrompt, { mode: 0o600 });
326
+ atomicWriteFile(promptPath, input.agent.systemPrompt, { mode: 0o600 });
327
327
  args.push(input.agent.systemPromptMode === "append" ? "--append-system-prompt" : "--system-prompt", promptPath);
328
328
  }
329
329
 
@@ -333,7 +333,7 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
333
333
  tempDir = createSafeTempDir(tmpBase, `pi-crew-${process.pid}-`);
334
334
  }
335
335
  const taskPath = path.join(tempDir, "task.md");
336
- fs.writeFileSync(taskPath, input.task, { mode: 0o600 });
336
+ atomicWriteFile(taskPath, input.task, { mode: 0o600 });
337
337
  args.push(`@${taskPath}`);
338
338
  } else {
339
339
  args.push(`Task: ${input.task}`);
@@ -55,7 +55,8 @@ function extractUsage(value: unknown): ParsedPiUsage | undefined {
55
55
  turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
56
56
  };
57
57
  if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
58
- for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
58
+ // Pi --mode json nests usage under message for message_end / turn_end events.
59
+ for (const key of ["usage", "message", "tokenUsage", "tokens", "stats"]) {
59
60
  const nested = extractUsage(obj[key]);
60
61
  if (nested) return nested;
61
62
  }
@@ -151,14 +152,14 @@ function extractErrorMessage(event: unknown): string | undefined {
151
152
  */
152
153
  function extractPatch(event: unknown, patches: string[]): void {
153
154
  const obj = asRecord(event);
154
- if (!obj || obj.type !== "tool_result") return;
155
+ if (obj?.type !== "tool_result") return;
155
156
 
156
157
  const content = obj.content;
157
158
  if (!Array.isArray(content)) return;
158
159
 
159
160
  for (const item of content) {
160
161
  const part = asRecord(item);
161
- if (!part || part.type !== "text") continue;
162
+ if (part?.type !== "text") continue;
162
163
  const text = typeof part.text === "string" ? part.text : "";
163
164
 
164
165
  // Check if this looks like a unified patch (starts with "---" or "+++")
@@ -189,7 +189,13 @@ function resolvePiCliScript(): string | undefined {
189
189
  const argv1 = process.argv[1];
190
190
  if (argv1) {
191
191
  const argvPath = path.isAbsolute(argv1) ? argv1 : path.resolve(argv1);
192
- if (isRunnableNodeScript(argvPath)) return argvPath;
192
+ // Only trust argv1 if we can confirm the current process is running from
193
+ // the pi-coding-agent package directory. Otherwise, when pi-crew is invoked
194
+ // from a standalone test script (process.argv[1] = test script path),
195
+ // resolvePiCliScript would incorrectly return the test script as the pi
196
+ // CLI. resolvePiPackageRoot walks up from argv1 looking for a package.json
197
+ // named @earendil-works/pi-coding-agent or @mariozechner/pi-coding-agent.
198
+ if (resolvePiPackageRoot() && isRunnableNodeScript(argvPath)) return argvPath;
193
199
  }
194
200
 
195
201
  // npm-global package dirs derived from `npm root -g` — placed BEFORE the
@@ -1,9 +1,6 @@
1
- import type { AgentConfig } from "../agents/agent-config.ts";
2
1
  import { errors } from "../errors.ts";
3
2
  import { appendEventAsync, flushEventLogBuffer } from "../state/event-log.ts";
4
- import type { TeamTaskState } from "../state/types.ts";
5
- import type { TeamConfig } from "../teams/team-config.ts";
6
- import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
3
+ import type { WorkflowConfig } from "../workflows/workflow-config.ts";
7
4
  import { mapConcurrent } from "./parallel-utils.ts";
8
5
 
9
6
  /**
@@ -5,7 +5,6 @@
5
5
  * Distilled from pi-autoresearch's post-check / backpressure pattern.
6
6
  */
7
7
  import { execFileSync } from "node:child_process";
8
- import * as path from "node:path";
9
8
  import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
10
9
  import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
11
10
  import { resolveShellForScript } from "../utils/resolve-shell.ts";
@@ -70,10 +70,10 @@ export interface TaskRunnerLike {
70
70
  */
71
71
  export class RetryRunner {
72
72
  private _disposed = false;
73
- private _handoffs: HandoffSummary[] = [];
74
73
 
75
74
  private taskRunner: TaskRunnerLike;
76
75
  private handoffManager: HandoffManager;
76
+ private _handoffs: HandoffSummary[] = [];
77
77
 
78
78
  constructor(taskRunner: TaskRunnerLike, handoffManager: HandoffManager) {
79
79
  this.taskRunner = taskRunner;