pi-crew 0.6.0 → 0.6.3

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 (135) hide show
  1. package/CHANGELOG.md +225 -0
  2. package/README.md +70 -31
  3. package/docs/issue-29-analysis.md +189 -0
  4. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
  5. package/package.json +2 -2
  6. package/src/agents/agent-config.ts +2 -1
  7. package/src/benchmark/feedback-loop.ts +4 -2
  8. package/src/config/config.ts +106 -15
  9. package/src/errors.ts +107 -0
  10. package/src/extension/async-notifier.ts +6 -2
  11. package/src/extension/crew-cleanup.ts +8 -5
  12. package/src/extension/cross-extension-rpc.ts +48 -0
  13. package/src/extension/management.ts +464 -109
  14. package/src/extension/register.ts +194 -34
  15. package/src/extension/registration/commands.ts +4 -3
  16. package/src/extension/registration/subagent-helpers.ts +2 -2
  17. package/src/extension/registration/subagent-tools.ts +3 -1
  18. package/src/extension/registration/team-tool.ts +3 -1
  19. package/src/extension/registration/viewers.ts +3 -2
  20. package/src/extension/run-export.ts +16 -1
  21. package/src/extension/run-import.ts +16 -0
  22. package/src/extension/team-tool/anchor.ts +5 -1
  23. package/src/extension/team-tool/api.ts +12 -7
  24. package/src/extension/team-tool/cancel.ts +3 -3
  25. package/src/extension/team-tool/config-patch.ts +15 -1
  26. package/src/extension/team-tool/explain.ts +1 -1
  27. package/src/extension/team-tool/handle-schedule.ts +40 -0
  28. package/src/extension/team-tool/inspect.ts +3 -3
  29. package/src/extension/team-tool/lifecycle-actions.ts +4 -4
  30. package/src/extension/team-tool/respond.ts +2 -2
  31. package/src/extension/team-tool/run.ts +60 -11
  32. package/src/extension/team-tool/status.ts +1 -1
  33. package/src/extension/team-tool.ts +175 -47
  34. package/src/hooks/registry.ts +84 -12
  35. package/src/hooks/types.ts +12 -3
  36. package/src/i18n.ts +15 -2
  37. package/src/observability/exporters/otlp-exporter.ts +73 -0
  38. package/src/plugins/plugin-define.ts +6 -0
  39. package/src/plugins/plugin-registry.ts +32 -0
  40. package/src/plugins/plugins/index.ts +3 -0
  41. package/src/plugins/plugins/nextjs.ts +19 -0
  42. package/src/plugins/plugins/vite.ts +14 -0
  43. package/src/plugins/plugins/vitest.ts +14 -0
  44. package/src/runtime/adaptive-plan.ts +24 -0
  45. package/src/runtime/agent-control.ts +6 -3
  46. package/src/runtime/async-runner.ts +86 -3
  47. package/src/runtime/background-runner.ts +529 -144
  48. package/src/runtime/chain-parser.ts +5 -1
  49. package/src/runtime/chain-runner.ts +67 -3
  50. package/src/runtime/checkpoint.ts +262 -180
  51. package/src/runtime/child-pi.ts +165 -38
  52. package/src/runtime/crash-recovery.ts +25 -14
  53. package/src/runtime/crew-agent-records.ts +6 -3
  54. package/src/runtime/cross-extension-rpc.ts +34 -8
  55. package/src/runtime/diagnostic-export.ts +4 -5
  56. package/src/runtime/dynamic-script-runner.ts +9 -6
  57. package/src/runtime/foreground-watchdog.ts +3 -3
  58. package/src/runtime/heartbeat-watcher.ts +19 -2
  59. package/src/runtime/intercom-bridge.ts +7 -0
  60. package/src/runtime/iteration-hooks.ts +1 -1
  61. package/src/runtime/live-agent-manager.ts +6 -3
  62. package/src/runtime/live-irc.ts +4 -2
  63. package/src/runtime/manifest-cache.ts +4 -0
  64. package/src/runtime/orphan-worker-registry.ts +444 -0
  65. package/src/runtime/parallel-utils.ts +2 -1
  66. package/src/runtime/parent-guard.ts +70 -16
  67. package/src/runtime/pi-args.ts +437 -14
  68. package/src/runtime/pi-spawn.ts +1 -0
  69. package/src/runtime/post-checks.ts +11 -4
  70. package/src/runtime/retry-runner.ts +9 -3
  71. package/src/runtime/{drift-detectors.ts → run-drift.ts} +1 -1
  72. package/src/runtime/run-tracker.ts +38 -10
  73. package/src/runtime/sandbox.ts +42 -21
  74. package/src/runtime/scheduler.ts +25 -2
  75. package/src/runtime/semaphore.ts +2 -1
  76. package/src/runtime/settings-store.ts +14 -2
  77. package/src/runtime/skill-effectiveness.ts +109 -64
  78. package/src/runtime/skill-instructions.ts +114 -33
  79. package/src/runtime/stale-reconciler.ts +310 -69
  80. package/src/runtime/subagent-manager.ts +265 -53
  81. package/src/runtime/subprocess-tool-registry.ts +2 -2
  82. package/src/runtime/task-health.ts +76 -0
  83. package/src/runtime/task-id.ts +20 -0
  84. package/src/runtime/task-packet.ts +13 -1
  85. package/src/runtime/task-runner/live-executor.ts +1 -1
  86. package/src/runtime/task-runner/progress.ts +1 -1
  87. package/src/runtime/task-runner/state-helpers.ts +110 -6
  88. package/src/runtime/task-runner.ts +98 -67
  89. package/src/runtime/team-runner.ts +186 -20
  90. package/src/runtime/usage-tracker.ts +4 -2
  91. package/src/runtime/verification-gates.ts +36 -9
  92. package/src/schema/team-tool-schema.ts +27 -9
  93. package/src/skills/discover-skills.ts +9 -3
  94. package/src/state/active-run-registry.ts +170 -38
  95. package/src/state/artifact-store.ts +25 -13
  96. package/src/state/atomic-write-v2.ts +86 -0
  97. package/src/state/atomic-write.ts +346 -55
  98. package/src/state/blob-store.ts +178 -10
  99. package/src/state/contracts.ts +2 -1
  100. package/src/state/crew-init.ts +161 -28
  101. package/src/state/decision-ledger.ts +172 -111
  102. package/src/state/event-log-rotation.ts +82 -52
  103. package/src/state/event-log.ts +270 -75
  104. package/src/state/health-store.ts +71 -0
  105. package/src/state/hook-instinct-bridge.ts +2 -1
  106. package/src/state/locks.ts +109 -20
  107. package/src/state/mailbox.ts +45 -7
  108. package/src/state/observation-store.ts +4 -1
  109. package/src/state/run-graph.ts +141 -130
  110. package/src/state/run-metrics.ts +24 -8
  111. package/src/state/state-store.ts +333 -44
  112. package/src/state/task-claims.ts +9 -2
  113. package/src/tools/safe-bash.ts +69 -20
  114. package/src/types/new-api-types.ts +10 -5
  115. package/src/ui/keybinding-map.ts +2 -1
  116. package/src/ui/live-run-sidebar.ts +1 -1
  117. package/src/ui/loaders.ts +4 -0
  118. package/src/ui/overlays/agent-picker-overlay.ts +1 -1
  119. package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
  120. package/src/ui/run-action-dispatcher.ts +4 -3
  121. package/src/ui/run-snapshot-cache.ts +1 -1
  122. package/src/ui/status-colors.ts +2 -1
  123. package/src/ui/syntax-highlight.ts +2 -1
  124. package/src/ui/tool-render.ts +13 -3
  125. package/src/utils/env-filter.ts +66 -0
  126. package/src/utils/file-coalescer.ts +9 -1
  127. package/src/utils/fs-watch.ts +4 -2
  128. package/src/utils/gh-protocol.ts +2 -1
  129. package/src/utils/paths.ts +80 -5
  130. package/src/utils/redaction.ts +6 -1
  131. package/src/utils/safe-paths.ts +287 -10
  132. package/src/worktree/cleanup.ts +117 -26
  133. package/src/worktree/worktree-manager.ts +128 -15
  134. package/test-bugs-all.mjs +10 -6
  135. package/test-integration-check.ts +4 -0
@@ -1,22 +1,126 @@
1
+ import * as fs from "node:fs";
1
2
  import type { TaskCheckpointState, TeamRunManifest, TeamTaskState } from "../../state/types.ts";
2
3
  import { loadRunManifestById, saveRunTasks } from "../../state/state-store.ts";
3
4
  import { recordFromTask, upsertCrewAgent } from "../crew-agent-records.ts";
5
+ import { logInternalError } from "../../utils/internal-error.ts";
6
+ import { withRunLockSync } from "../../state/locks.ts";
4
7
 
5
8
  export function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
6
9
  return tasks.map((task) => task.id === updated.id ? updated : task);
7
10
  }
8
11
 
9
- export function persistSingleTaskUpdate(manifest: TeamRunManifest, fallbackTasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
10
- const latest = loadRunManifestById(manifest.cwd, manifest.runId)?.tasks ?? fallbackTasks;
11
- const merged = updateTask(latest, updated);
12
- saveRunTasks(manifest, merged);
13
- return merged;
12
+ /**
13
+ * Persist a single task update using compare-and-swap under the run lock.
14
+ *
15
+ * Problem: The naive read-merge-write pattern is vulnerable to a read-modify-write
16
+ * race. When two parallel task completions race:
17
+ * 1. Task A loads tasks [A(running), B(running)], writes [A(completed), B(running)]
18
+ * 2. Task B loads [A(running), B(running)] (stale, before A's write), writes [A(running), B(completed)]
19
+ * Result: Task A's completed status is clobbered.
20
+ *
21
+ * Solution: Use mtime-based CAS under the run lock. Before writing, stat the tasks file
22
+ * to record its mtime. After merging, re-stat — if mtime changed, another writer
23
+ * committed first; retry with the fresh state. This is O(retry) under contention but
24
+ * converges in the normal single-writer case.
25
+ *
26
+ * @param checkpointPhase - Optional checkpoint phase to include in the task state alongside the update.
27
+ */
28
+ export function persistSingleTaskUpdate(manifest: TeamRunManifest, fallbackTasks: TeamTaskState[], updated: TeamTaskState, checkpointPhase?: TaskCheckpointState["phase"]): TeamTaskState[] {
29
+ let baseMtime = 0;
30
+ try {
31
+ baseMtime = fs.statSync(manifest.tasksPath).mtimeMs;
32
+ } catch {
33
+ // File doesn't exist yet — baseMtime=0 means "anything is fine"
34
+ baseMtime = 0;
35
+ }
36
+
37
+ let merged: TeamTaskState[] | undefined;
38
+
39
+ // Build the task with optional checkpoint phase
40
+ const taskWithCheckpoint = checkpointPhase
41
+ ? { ...updated, checkpoint: { phase: checkpointPhase, updatedAt: new Date().toISOString() } }
42
+ : updated;
43
+
44
+ try {
45
+ return withRunLockSync(manifest, () => {
46
+ retryLoop: for (let attempt = 0; attempt < 100; attempt++) {
47
+ const latest = loadRunManifestById(manifest.cwd, manifest.runId)?.tasks ?? fallbackTasks;
48
+ merged = updateTask(latest, taskWithCheckpoint);
49
+
50
+ // Re-stat to detect concurrent writes
51
+ let currentMtime: number;
52
+ try {
53
+ currentMtime = fs.statSync(manifest.tasksPath).mtimeMs;
54
+ } catch {
55
+ currentMtime = 0;
56
+ }
57
+
58
+ if (currentMtime !== baseMtime) {
59
+ // Another writer committed — their update is in latest, re-merge on top
60
+ baseMtime = currentMtime;
61
+ continue retryLoop;
62
+ }
63
+
64
+ // No concurrent writer — check that our merged result is based on the
65
+ // same base we observed (no intermediate writer between our load and check)
66
+ let recheckMtime: number;
67
+ try {
68
+ recheckMtime = fs.statSync(manifest.tasksPath).mtimeMs;
69
+ } catch {
70
+ // Run state deleted (prune/forget) — nothing to persist.
71
+ return fallbackTasks;
72
+ }
73
+ if (recheckMtime !== baseMtime) {
74
+ baseMtime = recheckMtime;
75
+ continue retryLoop;
76
+ }
77
+
78
+ // Final pre-write mtime check to catch any concurrent writer that completed
79
+ // between the recheck and saveRunTasks
80
+ let preWriteMtime: number;
81
+ try {
82
+ preWriteMtime = fs.statSync(manifest.tasksPath).mtimeMs;
83
+ } catch {
84
+ preWriteMtime = 0;
85
+ }
86
+ if (preWriteMtime !== baseMtime) {
87
+ // Another writer committed — retry
88
+ baseMtime = preWriteMtime;
89
+ continue retryLoop;
90
+ }
91
+
92
+ break retryLoop;
93
+ }
94
+
95
+ if (merged === undefined) {
96
+ logInternalError("persistSingleTaskUpdate", new Error("failed to converge after 50 attempts"));
97
+ throw new Error("persistSingleTaskUpdate: failed to converge after 50 attempts");
98
+ }
99
+
100
+ try {
101
+ saveRunTasks(manifest, merged);
102
+ } catch (err) {
103
+ logInternalError("persistSingleTaskUpdate", err);
104
+ throw err;
105
+ }
106
+ return merged;
107
+ });
108
+ } catch (err) {
109
+ if (merged === undefined) {
110
+ logInternalError("persistSingleTaskUpdate", err);
111
+ }
112
+ throw err;
113
+ }
14
114
  }
15
115
 
16
116
  export function checkpointTask(manifest: TeamRunManifest, tasks: TeamTaskState[], task: TeamTaskState, phase: TaskCheckpointState["phase"], childPid?: number): { task: TeamTaskState; tasks: TeamTaskState[] } {
17
117
  const checkpoint: TaskCheckpointState = { phase, updatedAt: new Date().toISOString(), ...(childPid ? { childPid } : task.checkpoint?.childPid ? { childPid: task.checkpoint.childPid } : {}) };
18
118
  const nextTask = { ...task, checkpoint };
19
119
  const nextTasks = persistSingleTaskUpdate(manifest, updateTask(tasks, nextTask), nextTask);
20
- upsertCrewAgent(manifest, recordFromTask(manifest, nextTask, "child-process"));
120
+ try {
121
+ upsertCrewAgent(manifest, recordFromTask(manifest, nextTask, "child-process"));
122
+ } catch (err) {
123
+ logInternalError("checkpointTask", err);
124
+ }
21
125
  return { task: nextTask, tasks: nextTasks };
22
126
  }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import * as path from "node:path";
2
3
  import type { AgentConfig } from "../agents/agent-config.ts";
3
4
  import type { CrewLimitsConfig, CrewRuntimeConfig } from "../config/config.ts";
4
5
  import type {
@@ -219,7 +220,7 @@ export async function runTeamTask(
219
220
  tasks: updateTask(tasks, cancelledTask),
220
221
  };
221
222
  }
222
- tasks = persistSingleTaskUpdate(manifest, tasks, task);
223
+ tasks = persistSingleTaskUpdate(manifest, tasks, task, "started");
223
224
  if (runtimeKind === "child-process")
224
225
  ({ task, tasks } = checkpointTask(
225
226
  manifest,
@@ -260,7 +261,7 @@ export async function runTeamTask(
260
261
  teamRole: { skills: input.teamRoleSkills },
261
262
  step: input.step,
262
263
  override: input.skillOverride,
263
- runId: manifest.runId, // ECC INSTINCT: Enable skill confidence tracking
264
+ runId: manifest.runId,
264
265
  })
265
266
  : undefined;
266
267
  const skillBlock = input.skillBlock ?? renderedSkills?.block;
@@ -272,6 +273,11 @@ export async function runTeamTask(
272
273
  if (input.step.preStepScript) {
273
274
  const scriptTimeout = input.step.preStepTimeout ?? 30_000;
274
275
  const scriptArgs = input.step.preStepArgs ?? [];
276
+ // SECURITY: Validate preStepScript path is contained within cwd
277
+ const resolved = path.resolve(manifest.cwd, input.step.preStepScript);
278
+ if (!resolved.startsWith(path.resolve(manifest.cwd) + path.sep) && resolved !== path.resolve(manifest.cwd)) {
279
+ throw new Error(`Security: preStepScript path escapes working directory: ${input.step.preStepScript}`);
280
+ }
275
281
  try {
276
282
  const { execFileSync } = await import("node:child_process");
277
283
  preStepOutput = execFileSync(input.step.preStepScript, scriptArgs, {
@@ -380,15 +386,29 @@ export async function runTeamTask(
380
386
  let lastRunProgressSummary: ProgressEventSummary | undefined;
381
387
  const persistHeartbeat = (force = false): void => {
382
388
  const now = Date.now();
389
+ // Skip disk write if throttled (unless forced).
383
390
  if (!force && now - lastHeartbeatPersistedAt < 1000) return;
384
- lastHeartbeatPersistedAt = now;
391
+ try {
392
+ // Write to disk first, then update in-memory.
393
+ // Disk state is always <= in-memory state, so a crash never produces
394
+ // a fresher in-memory heartbeat than what's on disk. This prevents the
395
+ // stale reconciler from seeing a live heartbeat paired with stale task state
396
+ // (which could cause false zombie detection).
397
+ tasks = persistSingleTaskUpdate(manifest, tasks, task);
398
+ } catch (err) {
399
+ // Run state may have been deleted by prune/forget/cleanup.
400
+ // This is not fatal — the run is gone, no point persisting.
401
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return;
402
+ throw err;
403
+ }
404
+ // Now update in-memory heartbeat so it is always >= persisted state.
385
405
  task = {
386
406
  ...task,
387
407
  heartbeat: touchWorkerHeartbeat(
388
408
  task.heartbeat ?? createWorkerHeartbeat(task.id),
389
409
  ),
390
410
  };
391
- tasks = persistSingleTaskUpdate(manifest, tasks, task);
411
+ lastHeartbeatPersistedAt = now;
392
412
  };
393
413
  const persistChildProgress = (
394
414
  event: unknown,
@@ -432,6 +452,10 @@ export async function runTeamTask(
432
452
  for (let i = 0; i < attemptModels.length; i++) {
433
453
  // M1 fix: set transcript path per attempt to avoid mixing across fallback attempts.
434
454
  transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.attempt-${i}.jsonl`;
455
+ // Ensure transcripts/ subdirectory exists before child-pi appends
456
+ // to it. appendTranscript uses O_APPEND (no mkdir) for security,
457
+ // so the caller must create the directory.
458
+ fs.mkdirSync(path.join(manifest.artifactsRoot, "transcripts"), { recursive: true });
435
459
  const model = attemptModels[i];
436
460
  const attemptStartedAt = new Date();
437
461
  const pendingAttempt: ModelAttemptSummary = {
@@ -466,6 +490,7 @@ export async function runTeamTask(
466
490
  role: task.role,
467
491
  runId: manifest.runId,
468
492
  agentId: task.id,
493
+ artifactsRoot: manifest.artifactsRoot,
469
494
  onSpawn: (pid) => {
470
495
  try {
471
496
  ({ task, tasks } = checkpointTask(
@@ -515,86 +540,90 @@ export async function runTeamTask(
515
540
  // Errors are logged but processing continues so subsequent events still update state.
516
541
  try {
517
542
  appendCrewAgentEvent(manifest, task.id, event);
518
- } catch (err) {
519
- logInternalError("task-runner.append-crew-agent-event", err, `taskId=${task.id}`);
520
- }
521
- if (
522
- event &&
523
- typeof event === "object" &&
524
- !Array.isArray(event)
525
- )
526
- collectedJsonEvents.push(
527
- event as Record<string, unknown>,
528
- );
529
- // Accumulate lifetime usage via message_end events (survives compaction)
530
- if (event && typeof event === "object" && (event as Record<string, unknown>).type === "message_end") {
531
- const msg = (event as Record<string, unknown>).message as Record<string, unknown> | undefined;
532
- if (msg?.role === "assistant") {
533
- const usage = msg.usage as Record<string, number> | undefined;
534
- if (usage) {
535
- task.lifetimeUsage = {
536
- input: (task.lifetimeUsage?.input ?? 0) + (usage.input ?? 0),
537
- output: (task.lifetimeUsage?.output ?? 0) + (usage.output ?? 0),
538
- cacheWrite: (task.lifetimeUsage?.cacheWrite ?? 0) + (usage.cacheWrite ?? 0),
539
- };
543
+ if (
544
+ event &&
545
+ typeof event === "object" &&
546
+ !Array.isArray(event)
547
+ )
548
+ collectedJsonEvents.push(
549
+ event as Record<string, unknown>,
550
+ );
551
+ if (collectedJsonEvents.length > 1000) {
552
+ collectedJsonEvents.splice(0, collectedJsonEvents.length - 1000);
553
+ }
554
+ // Accumulate lifetime usage via message_end events (survives compaction)
555
+ if (event && typeof event === "object" && (event as Record<string, unknown>).type === "message_end") {
556
+ const msg = (event as Record<string, unknown>).message as Record<string, unknown> | undefined;
557
+ if (msg?.role === "assistant") {
558
+ const usage = msg.usage as Record<string, number> | undefined;
559
+ if (usage) {
560
+ task.lifetimeUsage = {
561
+ input: (task.lifetimeUsage?.input ?? 0) + (usage.input ?? 0),
562
+ output: (task.lifetimeUsage?.output ?? 0) + (usage.output ?? 0),
563
+ cacheWrite: (task.lifetimeUsage?.cacheWrite ?? 0) + (usage.cacheWrite ?? 0),
564
+ };
565
+ }
540
566
  }
541
567
  }
542
- }
543
- persistHeartbeat();
544
- // Bug #3 fix: Write worker JSON events to background.log for debugging when running in background mode.
545
- // This supplements the event log so developers can see what the child Pi worker produced.
546
- if (process.env.PI_CREW_BACKGROUND_MODE === "1" && event) {
547
- try {
568
+ persistHeartbeat();
569
+ // Bug #3 fix: Write worker JSON events to background.log for debugging when running in background mode.
570
+ // This supplements the event log so developers can see what the child Pi worker produced.
571
+ if (process.env.PI_CREW_BACKGROUND_MODE === "1" && event) {
548
572
  const bgLogPath = `${manifest.stateRoot}/background.log`;
549
573
  const eventLine = typeof event === "object" && !Array.isArray(event) ? JSON.stringify(event) : String(event);
550
574
  fs.appendFileSync(bgLogPath, `${eventLine}\n`);
551
- } catch { /* background log write failures should not affect task */ }
552
- }
553
- task = {
554
- ...task,
555
- agentProgress: applyAgentProgressEvent(
556
- task.agentProgress ?? emptyCrewAgentProgress(),
557
- event,
558
- task.startedAt,
559
- ),
560
- };
561
- tasks = updateTask(tasks, task);
562
- // Bridge event to UI event bus for near-instant updates
563
- try {
575
+ }
576
+ // Apply agentProgress update first, then persist, then update in-memory array.
577
+ // This ensures disk state is always >= in-memory state, preventing
578
+ // fresher in-memory state from being lost on crash.
579
+ tasks = persistSingleTaskUpdate(manifest, tasks, {
580
+ ...task,
581
+ agentProgress: applyAgentProgressEvent(
582
+ task.agentProgress ?? emptyCrewAgentProgress(),
583
+ event,
584
+ task.startedAt,
585
+ ),
586
+ });
587
+ task = {
588
+ ...task,
589
+ agentProgress: applyAgentProgressEvent(
590
+ task.agentProgress ?? emptyCrewAgentProgress(),
591
+ event,
592
+ task.startedAt,
593
+ ),
594
+ };
595
+ tasks = updateTask(tasks, task);
596
+ // Bridge event to UI event bus for near-instant updates
564
597
  const bridgeEvent = bridgeEventFromJsonEvent(
565
598
  manifest.runId,
566
599
  task.id,
567
600
  event,
568
601
  );
569
602
  if (bridgeEvent) streamBridge?.handler(bridgeEvent);
570
- } catch {
571
- /* bridge errors should not affect task */
572
- }
573
- // Feed overflow recovery tracker
574
- if (input.onJsonEvent) {
575
- try {
603
+ // Feed overflow recovery tracker
604
+ if (input.onJsonEvent) {
576
605
  input.onJsonEvent(
577
606
  task.id,
578
607
  manifest.runId,
579
608
  event,
580
609
  );
581
- } catch {
582
- /* overflow tracking errors should not affect task */
583
610
  }
611
+ if (
612
+ !finalCheckpointWritten &&
613
+ isFinalChildEvent(event)
614
+ ) {
615
+ finalCheckpointWritten = true;
616
+ ({ task, tasks } = checkpointTask(
617
+ manifest,
618
+ tasks,
619
+ task,
620
+ "child-stdout-final",
621
+ ));
622
+ }
623
+ persistChildProgress(event);
624
+ } catch (err) {
625
+ logInternalError("task-runner.on-json-event", err as Error, `taskId=${task.id}`);
584
626
  }
585
- if (
586
- !finalCheckpointWritten &&
587
- isFinalChildEvent(event)
588
- ) {
589
- finalCheckpointWritten = true;
590
- ({ task, tasks } = checkpointTask(
591
- manifest,
592
- tasks,
593
- task,
594
- "child-stdout-final",
595
- ));
596
- }
597
- persistChildProgress(event);
598
627
  },
599
628
  });
600
629
  const evidenceStatus = childResult.exitStatus?.cancelled
@@ -872,6 +901,8 @@ export async function runTeamTask(
872
901
  } else {
873
902
  resultArtifact = live.resultArtifact;
874
903
  }
904
+ // Sync task.resultArtifact with the re-written artifact (if liveText was truthy)
905
+ task = { ...task, resultArtifact };
875
906
  logArtifact = live.logArtifact;
876
907
  transcriptArtifact = live.transcriptArtifact;
877
908
  } else {
@@ -1056,7 +1087,7 @@ export async function runTeamTask(
1056
1087
  // 2. Verification contract has commands
1057
1088
  // 3. Not in scaffold mode (scaffold mode intentionally skips execution)
1058
1089
  let verificationEvidence: VerificationEvidence = baseEvidence;
1059
- if (!error && runtimeKind !== "scaffold" && taskPacket.verification?.commands?.length) {
1090
+ if (runtimeKind !== "scaffold" && taskPacket.verification?.commands?.length) {
1060
1091
  try {
1061
1092
  const commandResults = await executeVerificationCommands(
1062
1093
  taskPacket.verification,