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.
- package/CHANGELOG.md +225 -0
- package/README.md +70 -31
- package/docs/issue-29-analysis.md +189 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
- package/package.json +2 -2
- package/src/agents/agent-config.ts +2 -1
- package/src/benchmark/feedback-loop.ts +4 -2
- package/src/config/config.ts +106 -15
- package/src/errors.ts +107 -0
- package/src/extension/async-notifier.ts +6 -2
- package/src/extension/crew-cleanup.ts +8 -5
- package/src/extension/cross-extension-rpc.ts +48 -0
- package/src/extension/management.ts +464 -109
- package/src/extension/register.ts +194 -34
- package/src/extension/registration/commands.ts +4 -3
- package/src/extension/registration/subagent-helpers.ts +2 -2
- package/src/extension/registration/subagent-tools.ts +3 -1
- package/src/extension/registration/team-tool.ts +3 -1
- package/src/extension/registration/viewers.ts +3 -2
- package/src/extension/run-export.ts +16 -1
- package/src/extension/run-import.ts +16 -0
- package/src/extension/team-tool/anchor.ts +5 -1
- package/src/extension/team-tool/api.ts +12 -7
- package/src/extension/team-tool/cancel.ts +3 -3
- package/src/extension/team-tool/config-patch.ts +15 -1
- package/src/extension/team-tool/explain.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +40 -0
- package/src/extension/team-tool/inspect.ts +3 -3
- package/src/extension/team-tool/lifecycle-actions.ts +4 -4
- package/src/extension/team-tool/respond.ts +2 -2
- package/src/extension/team-tool/run.ts +60 -11
- package/src/extension/team-tool/status.ts +1 -1
- package/src/extension/team-tool.ts +175 -47
- package/src/hooks/registry.ts +84 -12
- package/src/hooks/types.ts +12 -3
- package/src/i18n.ts +15 -2
- package/src/observability/exporters/otlp-exporter.ts +73 -0
- package/src/plugins/plugin-define.ts +6 -0
- package/src/plugins/plugin-registry.ts +32 -0
- package/src/plugins/plugins/index.ts +3 -0
- package/src/plugins/plugins/nextjs.ts +19 -0
- package/src/plugins/plugins/vite.ts +14 -0
- package/src/plugins/plugins/vitest.ts +14 -0
- package/src/runtime/adaptive-plan.ts +24 -0
- package/src/runtime/agent-control.ts +6 -3
- package/src/runtime/async-runner.ts +86 -3
- package/src/runtime/background-runner.ts +529 -144
- package/src/runtime/chain-parser.ts +5 -1
- package/src/runtime/chain-runner.ts +67 -3
- package/src/runtime/checkpoint.ts +262 -180
- package/src/runtime/child-pi.ts +165 -38
- package/src/runtime/crash-recovery.ts +25 -14
- package/src/runtime/crew-agent-records.ts +6 -3
- package/src/runtime/cross-extension-rpc.ts +34 -8
- package/src/runtime/diagnostic-export.ts +4 -5
- package/src/runtime/dynamic-script-runner.ts +9 -6
- package/src/runtime/foreground-watchdog.ts +3 -3
- package/src/runtime/heartbeat-watcher.ts +19 -2
- package/src/runtime/intercom-bridge.ts +7 -0
- package/src/runtime/iteration-hooks.ts +1 -1
- package/src/runtime/live-agent-manager.ts +6 -3
- package/src/runtime/live-irc.ts +4 -2
- package/src/runtime/manifest-cache.ts +4 -0
- package/src/runtime/orphan-worker-registry.ts +444 -0
- package/src/runtime/parallel-utils.ts +2 -1
- package/src/runtime/parent-guard.ts +70 -16
- package/src/runtime/pi-args.ts +437 -14
- package/src/runtime/pi-spawn.ts +1 -0
- package/src/runtime/post-checks.ts +11 -4
- package/src/runtime/retry-runner.ts +9 -3
- package/src/runtime/{drift-detectors.ts → run-drift.ts} +1 -1
- package/src/runtime/run-tracker.ts +38 -10
- package/src/runtime/sandbox.ts +42 -21
- package/src/runtime/scheduler.ts +25 -2
- package/src/runtime/semaphore.ts +2 -1
- package/src/runtime/settings-store.ts +14 -2
- package/src/runtime/skill-effectiveness.ts +109 -64
- package/src/runtime/skill-instructions.ts +114 -33
- package/src/runtime/stale-reconciler.ts +310 -69
- package/src/runtime/subagent-manager.ts +265 -53
- package/src/runtime/subprocess-tool-registry.ts +2 -2
- package/src/runtime/task-health.ts +76 -0
- package/src/runtime/task-id.ts +20 -0
- package/src/runtime/task-packet.ts +13 -1
- package/src/runtime/task-runner/live-executor.ts +1 -1
- package/src/runtime/task-runner/progress.ts +1 -1
- package/src/runtime/task-runner/state-helpers.ts +110 -6
- package/src/runtime/task-runner.ts +98 -67
- package/src/runtime/team-runner.ts +186 -20
- package/src/runtime/usage-tracker.ts +4 -2
- package/src/runtime/verification-gates.ts +36 -9
- package/src/schema/team-tool-schema.ts +27 -9
- package/src/skills/discover-skills.ts +9 -3
- package/src/state/active-run-registry.ts +170 -38
- package/src/state/artifact-store.ts +25 -13
- package/src/state/atomic-write-v2.ts +86 -0
- package/src/state/atomic-write.ts +346 -55
- package/src/state/blob-store.ts +178 -10
- package/src/state/contracts.ts +2 -1
- package/src/state/crew-init.ts +161 -28
- package/src/state/decision-ledger.ts +172 -111
- package/src/state/event-log-rotation.ts +82 -52
- package/src/state/event-log.ts +270 -75
- package/src/state/health-store.ts +71 -0
- package/src/state/hook-instinct-bridge.ts +2 -1
- package/src/state/locks.ts +109 -20
- package/src/state/mailbox.ts +45 -7
- package/src/state/observation-store.ts +4 -1
- package/src/state/run-graph.ts +141 -130
- package/src/state/run-metrics.ts +24 -8
- package/src/state/state-store.ts +333 -44
- package/src/state/task-claims.ts +9 -2
- package/src/tools/safe-bash.ts +69 -20
- package/src/types/new-api-types.ts +10 -5
- package/src/ui/keybinding-map.ts +2 -1
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +4 -0
- package/src/ui/overlays/agent-picker-overlay.ts +1 -1
- package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
- package/src/ui/run-action-dispatcher.ts +4 -3
- package/src/ui/run-snapshot-cache.ts +1 -1
- package/src/ui/status-colors.ts +2 -1
- package/src/ui/syntax-highlight.ts +2 -1
- package/src/ui/tool-render.ts +13 -3
- package/src/utils/env-filter.ts +66 -0
- package/src/utils/file-coalescer.ts +9 -1
- package/src/utils/fs-watch.ts +4 -2
- package/src/utils/gh-protocol.ts +2 -1
- package/src/utils/paths.ts +80 -5
- package/src/utils/redaction.ts +6 -1
- package/src/utils/safe-paths.ts +287 -10
- package/src/worktree/cleanup.ts +117 -26
- package/src/worktree/worktree-manager.ts +128 -15
- package/test-bugs-all.mjs +10 -6
- 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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
task
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
571
|
-
|
|
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 (
|
|
1090
|
+
if (runtimeKind !== "scaffold" && taskPacket.verification?.commands?.length) {
|
|
1060
1091
|
try {
|
|
1061
1092
|
const commandResults = await executeVerificationCommands(
|
|
1062
1093
|
taskPacket.verification,
|