@agwab/pi-workflow 0.2.1 → 0.4.0

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 (119) hide show
  1. package/README.md +3 -1
  2. package/dist/artifact-graph-runtime.d.ts +1 -1
  3. package/dist/artifact-graph-runtime.js +10 -5
  4. package/dist/artifact-graph-schema.js +127 -5
  5. package/dist/compiler.js +52 -19
  6. package/dist/dynamic-generated-task-runtime.js +3 -1
  7. package/dist/dynamic-profiles.d.ts +1 -1
  8. package/dist/engine-run-graph.d.ts +3 -0
  9. package/dist/engine-run-graph.js +194 -4
  10. package/dist/engine.d.ts +5 -0
  11. package/dist/engine.js +389 -41
  12. package/dist/extension.d.ts +2 -1
  13. package/dist/extension.js +30 -8
  14. package/dist/index.d.ts +11 -3
  15. package/dist/index.js +6 -1
  16. package/dist/prompt-json.d.ts +7 -0
  17. package/dist/prompt-json.js +13 -0
  18. package/dist/roles.d.ts +1 -1
  19. package/dist/roles.js +5 -8
  20. package/dist/store.d.ts +20 -1
  21. package/dist/store.js +139 -35
  22. package/dist/strings.d.ts +11 -0
  23. package/dist/strings.js +24 -0
  24. package/dist/subagent-backend.js +710 -40
  25. package/dist/types.d.ts +107 -1
  26. package/dist/verification-ontology.d.ts +31 -0
  27. package/dist/verification-ontology.js +66 -0
  28. package/dist/workflow-artifact-tool.js +5 -6
  29. package/dist/workflow-artifacts.d.ts +7 -0
  30. package/dist/workflow-artifacts.js +55 -4
  31. package/dist/workflow-fetch-cache-extension.d.ts +1 -0
  32. package/dist/workflow-fetch-cache-extension.js +57 -9
  33. package/dist/workflow-metrics.d.ts +113 -0
  34. package/dist/workflow-metrics.js +272 -0
  35. package/dist/workflow-output-artifacts.js +5 -3
  36. package/dist/workflow-partial-output.d.ts +45 -0
  37. package/dist/workflow-partial-output.js +205 -0
  38. package/dist/workflow-progress-health.js +42 -10
  39. package/dist/workflow-runtime.js +10 -1
  40. package/dist/workflow-view.js +3 -1
  41. package/dist/workflow-web-source-extension.js +194 -52
  42. package/dist/workflow-web-source.d.ts +2 -1
  43. package/dist/workflow-web-source.js +109 -30
  44. package/docs/usage.md +76 -29
  45. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  46. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  47. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  48. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  49. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  50. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  51. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  52. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  53. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  54. package/node_modules/@agwab/pi-subagent/src/index.ts +1046 -576
  55. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  56. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  57. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  58. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  59. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  60. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  61. package/node_modules/@agwab/pi-subagent/src/panel.ts +1356 -560
  62. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  63. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  64. package/package.json +2 -2
  65. package/skills/workflow-guide/SKILL.md +1 -0
  66. package/src/artifact-graph-runtime.ts +19 -13
  67. package/src/artifact-graph-schema.ts +143 -3
  68. package/src/cli.mjs +52 -0
  69. package/src/compiler.ts +63 -18
  70. package/src/dynamic-generated-task-runtime.ts +3 -1
  71. package/src/dynamic-profiles.ts +1 -1
  72. package/src/engine-run-graph.ts +246 -4
  73. package/src/engine.ts +545 -38
  74. package/src/extension.ts +36 -6
  75. package/src/index.ts +52 -1
  76. package/src/prompt-json.ts +13 -0
  77. package/src/roles.ts +6 -9
  78. package/src/store.ts +194 -42
  79. package/src/strings.ts +38 -0
  80. package/src/subagent-backend.ts +921 -62
  81. package/src/types.ts +116 -2
  82. package/src/verification-ontology.ts +88 -0
  83. package/src/workflow-artifact-tool.ts +5 -7
  84. package/src/workflow-artifacts.ts +83 -3
  85. package/src/workflow-fetch-cache-extension.ts +78 -13
  86. package/src/workflow-metrics.ts +478 -0
  87. package/src/workflow-output-artifacts.ts +5 -3
  88. package/src/workflow-partial-output.ts +299 -0
  89. package/src/workflow-progress-health.ts +47 -15
  90. package/src/workflow-runtime.ts +18 -2
  91. package/src/workflow-view.ts +2 -1
  92. package/src/workflow-web-source-extension.ts +654 -232
  93. package/src/workflow-web-source.ts +153 -39
  94. package/workflows/README.md +7 -25
  95. package/workflows/deep-research/batched-verification.spec.json +253 -0
  96. package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
  97. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +229 -36
  98. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  99. package/workflows/deep-research/helpers/normalize-input-packet.mjs +81 -2
  100. package/workflows/deep-research/helpers/render-executive.mjs +40 -26
  101. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  102. package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
  103. package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
  104. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -3
  105. package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
  106. package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
  107. package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
  108. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +13 -3
  109. package/workflows/deep-research/spec.json +32 -12
  110. package/workflows/impact-review/spec.json +3 -3
  111. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  112. package/dist/dynamic-loader.d.ts +0 -25
  113. package/dist/dynamic-loader.js +0 -13
  114. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
  115. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stdout +0 -13
  116. package/src/dynamic-loader.ts +0 -49
  117. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  118. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  119. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
@@ -1,364 +1,680 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
2
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
3
3
  import {
4
- readRunEvents,
5
- readRunRecord,
6
- relativeRunEventsPath,
7
- relativeRunRecordPath,
8
- type ArtifactRef,
9
- type CompletionMetadata,
10
- type ResultEnvelope,
11
- type ResultMetadata,
12
- type RunAttemptRecord,
13
- type RunEvent,
14
- type RunRecord,
4
+ readRunEvents,
5
+ readRunRecord,
6
+ relativeRunEventsPath,
7
+ relativeRunRecordPath,
8
+ type ArtifactRef,
9
+ type CompletionMetadata,
10
+ type ResultEnvelope,
11
+ type ResultMetadata,
12
+ type RunAttemptRecord,
13
+ type RunEvent,
14
+ type RunRecord,
15
15
  } from "../artifacts/index.ts";
16
- import { STATUSES, type AsyncDependency, type ExecutionMode, type FailureKind, type ResolvedBackend, type Status } from "../core/constants.ts";
16
+ import {
17
+ STATUSES,
18
+ type AsyncDependency,
19
+ type ExecutionMode,
20
+ type FailureKind,
21
+ type ResolvedBackend,
22
+ type Status,
23
+ } from "../core/constants.ts";
24
+ import { resolveRunRef } from "./run-ref.ts";
17
25
 
18
26
  const DEFAULT_RUNS_DIR = ".pi/agent/runs";
19
27
  const SAFE_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
20
28
  const EVENT_TAIL_LIMIT = 20;
29
+ const CHILD_EVENT_SCAN_LIMIT = Number.POSITIVE_INFINITY;
21
30
 
22
31
  export interface RunStatusRef {
23
- runId: string;
24
- attemptId?: string;
25
- /** @deprecated v1 compatibility only. */
26
- taskId?: string;
27
- cwd?: string;
28
- runsDir?: string;
32
+ runId: string;
33
+ attemptId?: string;
34
+ /** @deprecated v1 compatibility only. */
35
+ taskId?: string;
36
+ cwd?: string;
37
+ runsDir?: string;
29
38
  }
30
39
 
31
40
  export interface RunLogRef extends ArtifactRef {
32
- type: "stdout" | "stderr" | "output" | "result";
33
- artifactCwd?: string;
41
+ type: "stdout" | "stderr" | "output" | "result";
42
+ artifactCwd?: string;
34
43
  }
35
44
 
36
45
  export interface RunAttemptStatusSnapshot {
37
- attemptId: string;
38
- status: Status;
39
- backend: ResolvedBackend | null;
40
- failureKind: FailureKind | null;
41
- startedAt: string;
42
- completedAt: string | null;
43
- heartbeatAt?: string;
44
- resultPath: string | null;
45
- outputPath: string | null;
46
- stdoutPath: string | null;
47
- stderrPath: string | null;
48
- artifactCwd?: string;
49
- pid?: number;
50
- processGroupId?: number;
51
- workerPid?: number;
52
- workerProcessGroupId?: number;
53
- }
54
-
55
- export type RunTaskStatusSnapshot = RunAttemptStatusSnapshot & { taskId?: string };
46
+ attemptId: string;
47
+ status: Status;
48
+ backend: ResolvedBackend | null;
49
+ failureKind: FailureKind | null;
50
+ startedAt: string;
51
+ completedAt: string | null;
52
+ heartbeatAt?: string;
53
+ resultPath: string | null;
54
+ outputPath: string | null;
55
+ stdoutPath: string | null;
56
+ stderrPath: string | null;
57
+ artifactCwd?: string;
58
+ pid?: number;
59
+ processGroupId?: number;
60
+ workerPid?: number;
61
+ workerProcessGroupId?: number;
62
+ }
63
+
64
+ export type RunTaskStatusSnapshot = RunAttemptStatusSnapshot & {
65
+ taskId?: string;
66
+ };
67
+
68
+ export interface RunChildFailureSummary {
69
+ childRunId: string;
70
+ workflowRunId?: string;
71
+ taskId?: string;
72
+ status: Status;
73
+ failureKind: FailureKind | string | null;
74
+ message?: string;
75
+ timestamp: string;
76
+ }
77
+
78
+ export interface RunChildSummary {
79
+ total: number;
80
+ pending: number;
81
+ running: number;
82
+ completed: number;
83
+ failed: number;
84
+ cancelled: number;
85
+ activeChildRunIds: string[];
86
+ latestFailure: RunChildFailureSummary | null;
87
+ worstDescendantStatus: Status | null;
88
+ }
56
89
 
57
90
  export interface RunStatusSnapshot {
58
- runId: string;
59
- attemptId: string;
60
- /** @deprecated v1 compatibility only. */
61
- taskId?: string;
62
- correlationId?: string;
63
- backend: ResolvedBackend;
64
- status: Status;
65
- failureKind: FailureKind | null;
66
- startedAt: string;
67
- completedAt: string | null;
68
- durationMs: number | null;
69
- logs: RunLogRef[];
70
- resultPath: string | null;
71
- completion?: CompletionMetadata;
72
- metadata: ResultMetadata;
73
- mode?: ExecutionMode;
74
- dependency?: AsyncDependency | null;
75
- registryPath?: string;
76
- eventsPath?: string;
77
- eventTail?: RunEvent[];
78
- attempts?: RunAttemptStatusSnapshot[];
79
- /** @deprecated v1 compatibility only. */
80
- tasks?: RunTaskStatusSnapshot[];
91
+ runId: string;
92
+ attemptId: string;
93
+ /** @deprecated v1 compatibility only. */
94
+ taskId?: string;
95
+ correlationId?: string;
96
+ backend: ResolvedBackend;
97
+ status: Status;
98
+ failureKind: FailureKind | null;
99
+ startedAt: string;
100
+ completedAt: string | null;
101
+ durationMs: number | null;
102
+ logs: RunLogRef[];
103
+ resultPath: string | null;
104
+ completion?: CompletionMetadata;
105
+ metadata: ResultMetadata;
106
+ mode?: ExecutionMode;
107
+ dependency?: AsyncDependency | null;
108
+ registryPath?: string;
109
+ eventsPath?: string;
110
+ eventTail?: RunEvent[];
111
+ childSummary?: RunChildSummary;
112
+ attempts?: RunAttemptStatusSnapshot[];
113
+ /** @deprecated v1 compatibility only. */
114
+ tasks?: RunTaskStatusSnapshot[];
81
115
  }
82
116
 
83
117
  export interface RunLogsSnapshot extends RunStatusSnapshot {
84
- logText: Partial<Record<RunLogRef["type"] | "events", string>>;
118
+ logText: Partial<Record<RunLogRef["type"] | "events", string>>;
85
119
  }
86
120
 
87
121
  export interface WaitForRunOptions extends RunStatusRef {
88
- timeoutMs?: number;
89
- pollIntervalMs?: number;
122
+ timeoutMs?: number;
123
+ pollIntervalMs?: number;
90
124
  }
91
125
 
92
126
  export interface WaitForRunResult {
93
- status: "completed" | "timeout";
94
- snapshot: RunStatusSnapshot | null;
127
+ status: "completed" | "timeout";
128
+ snapshot: RunStatusSnapshot | null;
95
129
  }
96
130
 
97
131
  function assertSafeId(name: string, value: string): void {
98
- if (!SAFE_ID_PATTERN.test(value)) {
99
- throw new Error(`${name} must contain only letters, numbers, dots, underscores, or dashes.`);
100
- }
132
+ if (!SAFE_ID_PATTERN.test(value)) {
133
+ throw new Error(
134
+ `${name} must contain only letters, numbers, dots, underscores, or dashes.`,
135
+ );
136
+ }
101
137
  }
102
138
 
103
139
  function isInsideOrEqual(parent: string, child: string): boolean {
104
- const childRelative = relative(parent, child);
105
- return childRelative === "" || (!childRelative.startsWith("..") && !isAbsolute(childRelative));
106
- }
107
-
108
- function pathsFor(ref: RunStatusRef): { cwd: string; runsDir: string; runDir: string } {
109
- assertSafeId("runId", ref.runId);
110
- if (ref.attemptId !== undefined) assertSafeId("attemptId", ref.attemptId);
111
- if (ref.taskId !== undefined) assertSafeId("taskId", ref.taskId);
112
- const cwd = resolve(ref.cwd ?? process.cwd());
113
- const runsDir = resolve(cwd, ref.runsDir ?? DEFAULT_RUNS_DIR);
114
- if (!isInsideOrEqual(cwd, runsDir)) {
115
- throw new Error("runsDir must be inside cwd so lifecycle refs remain relative and safe.");
116
- }
117
- return { cwd, runsDir, runDir: join(runsDir, ref.runId) };
118
- }
119
-
120
- function safeArtifactPath(cwd: string, artifact: Pick<RunLogRef, "path" | "artifactCwd">): string {
121
- if (isAbsolute(artifact.path) || artifact.path.split("/").includes("..")) throw new Error("artifact path must be a safe relative path.");
122
- const artifactCwd = resolve(artifact.artifactCwd ?? cwd);
123
- const path = resolve(artifactCwd, artifact.path.split("/").join(sep));
124
- if (!isInsideOrEqual(artifactCwd, path)) throw new Error("artifact path must stay inside its artifact cwd.");
125
- return path;
140
+ const childRelative = relative(parent, child);
141
+ return (
142
+ childRelative === "" ||
143
+ (!childRelative.startsWith("..") && !isAbsolute(childRelative))
144
+ );
145
+ }
146
+
147
+ function pathsFor(ref: RunStatusRef): {
148
+ cwd: string;
149
+ runsDir: string;
150
+ runDir: string;
151
+ } {
152
+ assertSafeId("runId", ref.runId);
153
+ if (ref.attemptId !== undefined) assertSafeId("attemptId", ref.attemptId);
154
+ if (ref.taskId !== undefined) assertSafeId("taskId", ref.taskId);
155
+ const cwd = resolve(ref.cwd ?? process.cwd());
156
+ const runsDir = resolve(cwd, ref.runsDir ?? DEFAULT_RUNS_DIR);
157
+ if (!isInsideOrEqual(cwd, runsDir)) {
158
+ throw new Error(
159
+ "runsDir must be inside cwd so lifecycle refs remain relative and safe.",
160
+ );
161
+ }
162
+ return { cwd, runsDir, runDir: join(runsDir, ref.runId) };
163
+ }
164
+
165
+ function safeArtifactPath(
166
+ cwd: string,
167
+ artifact: Pick<RunLogRef, "path" | "artifactCwd">,
168
+ ): string {
169
+ if (isAbsolute(artifact.path) || artifact.path.split("/").includes(".."))
170
+ throw new Error("artifact path must be a safe relative path.");
171
+ const artifactCwd = resolve(artifact.artifactCwd ?? cwd);
172
+ const path = resolve(artifactCwd, artifact.path.split("/").join(sep));
173
+ if (!isInsideOrEqual(artifactCwd, path))
174
+ throw new Error("artifact path must stay inside its artifact cwd.");
175
+ return path;
126
176
  }
127
177
 
128
178
  function sleep(ms: number): Promise<void> {
129
- return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
179
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
130
180
  }
131
181
 
132
- function artifactFromAttempt(attempt: RunAttemptRecord, type: RunLogRef["type"], path: string | undefined): RunLogRef | null {
133
- if (path === undefined) return null;
134
- return { type, path, artifactCwd: attempt.artifactCwd };
182
+ function artifactFromAttempt(
183
+ attempt: RunAttemptRecord,
184
+ type: RunLogRef["type"],
185
+ path: string | undefined,
186
+ ): RunLogRef | null {
187
+ if (path === undefined) return null;
188
+ return { type, path, artifactCwd: attempt.artifactCwd };
135
189
  }
136
190
 
137
191
  function attemptSnapshot(attempt: RunAttemptRecord): RunAttemptStatusSnapshot {
138
- return {
139
- attemptId: attempt.attemptId,
140
- status: attempt.status,
141
- backend: attempt.backend ?? null,
142
- failureKind: attempt.failureKind,
143
- startedAt: attempt.startedAt,
144
- completedAt: attempt.completedAt,
145
- ...(attempt.heartbeatAt === undefined ? {} : { heartbeatAt: attempt.heartbeatAt }),
146
- resultPath: attempt.resultPath ?? null,
147
- outputPath: attempt.outputPath ?? null,
148
- stdoutPath: attempt.stdoutPath ?? null,
149
- stderrPath: attempt.stderrPath ?? null,
150
- ...(attempt.artifactCwd === undefined ? {} : { artifactCwd: attempt.artifactCwd }),
151
- ...(attempt.process?.pid === undefined ? {} : { pid: attempt.process.pid }),
152
- ...(attempt.process?.processGroupId === undefined ? {} : { processGroupId: attempt.process.processGroupId }),
153
- ...(attempt.process?.workerPid === undefined ? {} : { workerPid: attempt.process.workerPid }),
154
- ...(attempt.process?.workerProcessGroupId === undefined ? {} : { workerProcessGroupId: attempt.process.workerProcessGroupId }),
155
- };
192
+ return {
193
+ attemptId: attempt.attemptId,
194
+ status: attempt.status,
195
+ backend: attempt.backend ?? null,
196
+ failureKind: attempt.failureKind,
197
+ startedAt: attempt.startedAt,
198
+ completedAt: attempt.completedAt,
199
+ ...(attempt.heartbeatAt === undefined
200
+ ? {}
201
+ : { heartbeatAt: attempt.heartbeatAt }),
202
+ resultPath: attempt.resultPath ?? null,
203
+ outputPath: attempt.outputPath ?? null,
204
+ stdoutPath: attempt.stdoutPath ?? null,
205
+ stderrPath: attempt.stderrPath ?? null,
206
+ ...(attempt.artifactCwd === undefined
207
+ ? {}
208
+ : { artifactCwd: attempt.artifactCwd }),
209
+ ...(attempt.process?.pid === undefined ? {} : { pid: attempt.process.pid }),
210
+ ...(attempt.process?.processGroupId === undefined
211
+ ? {}
212
+ : { processGroupId: attempt.process.processGroupId }),
213
+ ...(attempt.process?.workerPid === undefined
214
+ ? {}
215
+ : { workerPid: attempt.process.workerPid }),
216
+ ...(attempt.process?.workerProcessGroupId === undefined
217
+ ? {}
218
+ : { workerProcessGroupId: attempt.process.workerProcessGroupId }),
219
+ };
156
220
  }
157
221
 
158
222
  function resultLogs(result: ResultEnvelope): RunLogRef[] {
159
- return result.artifacts
160
- .filter((artifact): artifact is RunLogRef => artifact.type === "stdout" || artifact.type === "stderr" || artifact.type === "output" || artifact.type === "result")
161
- .map((artifact) => ({ ...artifact, artifactCwd: result.cwd }));
223
+ return result.artifacts
224
+ .filter(
225
+ (artifact): artifact is RunLogRef =>
226
+ artifact.type === "stdout" ||
227
+ artifact.type === "stderr" ||
228
+ artifact.type === "output" ||
229
+ artifact.type === "result",
230
+ )
231
+ .map((artifact) => ({ ...artifact, artifactCwd: result.cwd }));
162
232
  }
163
233
 
164
234
  export function isStatus(value: unknown): value is Status {
165
- return typeof value === "string" && (STATUSES as readonly string[]).includes(value);
235
+ return (
236
+ typeof value === "string" && (STATUSES as readonly string[]).includes(value)
237
+ );
166
238
  }
167
239
 
168
240
  export function isTerminalStatus(status: Status): boolean {
169
- return status === "completed" || status === "failed" || status === "cancelled";
241
+ return (
242
+ status === "completed" || status === "failed" || status === "cancelled"
243
+ );
170
244
  }
171
245
 
172
246
  export function statusSucceeded(status: Status): boolean {
173
- return status === "completed";
174
- }
175
-
176
- export function statusFailedClosed(status: Status, failureKind: FailureKind | null): boolean {
177
- return (status === "failed" || status === "cancelled") && failureKind !== null;
178
- }
179
-
180
- export function createRunStatusSnapshot(result: ResultEnvelope): RunStatusSnapshot {
181
- const logs = resultLogs(result);
182
- const resultArtifact = logs.find((artifact) => artifact.type === "result") ?? null;
183
-
184
- return {
185
- runId: result.runId,
186
- attemptId: result.attemptId,
187
- ...(result.taskId === undefined ? {} : { taskId: result.taskId }),
188
- ...(result.correlationId === undefined ? {} : { correlationId: result.correlationId }),
189
- backend: result.backend,
190
- status: result.status,
191
- failureKind: result.failureKind,
192
- startedAt: result.startedAt,
193
- completedAt: result.completedAt,
194
- durationMs: result.durationMs,
195
- logs,
196
- resultPath: resultArtifact?.path ?? null,
197
- metadata: result.metadata,
198
- ...(result.completion === undefined ? {} : { completion: result.completion }),
199
- };
247
+ return status === "completed";
248
+ }
249
+
250
+ export function statusFailedClosed(
251
+ status: Status,
252
+ failureKind: FailureKind | null,
253
+ ): boolean {
254
+ return (
255
+ (status === "failed" || status === "cancelled") && failureKind !== null
256
+ );
257
+ }
258
+
259
+ function dataString(
260
+ data: Record<string, unknown> | undefined,
261
+ key: string,
262
+ ): string | undefined {
263
+ const value = data?.[key];
264
+ return typeof value === "string" && value.length > 0 ? value : undefined;
265
+ }
266
+
267
+ function statusFromChildEvent(event: RunEvent): Status | undefined {
268
+ if (isStatus(event.status)) return event.status;
269
+ if (event.type === "child.started" || event.type === "child.updated")
270
+ return "running";
271
+ if (event.type === "child.completed") return "completed";
272
+ if (event.type === "child.failed") return "failed";
273
+ if (event.type === "child.cancelled") return "cancelled";
274
+ return undefined;
275
+ }
276
+
277
+ export function summarizeChildEvents(
278
+ events: readonly RunEvent[],
279
+ ): RunChildSummary | undefined {
280
+ const children = new Map<
281
+ string,
282
+ {
283
+ status: Status;
284
+ failureKind: FailureKind | string | null;
285
+ workflowRunId?: string;
286
+ taskId?: string;
287
+ message?: string;
288
+ timestamp: string;
289
+ }
290
+ >();
291
+ let latestFailure: RunChildFailureSummary | null = null;
292
+ let anonymous = 0;
293
+
294
+ for (const event of events) {
295
+ if (!event.type.startsWith("child.")) continue;
296
+ const status = statusFromChildEvent(event);
297
+ if (status === undefined) continue;
298
+ const data = event.data;
299
+ const childRunId =
300
+ dataString(data, "childRunId") ??
301
+ dataString(data, "childId") ??
302
+ dataString(data, "descendantRunId") ??
303
+ `anonymous-child-${anonymous++}`;
304
+ const failureKind = dataString(data, "failureKind") ?? null;
305
+ const child = {
306
+ status,
307
+ failureKind,
308
+ ...(dataString(data, "workflowRunId") === undefined
309
+ ? {}
310
+ : { workflowRunId: dataString(data, "workflowRunId") }),
311
+ ...(dataString(data, "taskId") === undefined
312
+ ? {}
313
+ : { taskId: dataString(data, "taskId") }),
314
+ ...(event.message === undefined ? {} : { message: event.message }),
315
+ timestamp: event.timestamp,
316
+ };
317
+ children.set(childRunId, child);
318
+ if (status === "failed" || status === "cancelled") {
319
+ latestFailure = {
320
+ childRunId,
321
+ ...(child.workflowRunId === undefined
322
+ ? {}
323
+ : { workflowRunId: child.workflowRunId }),
324
+ ...(child.taskId === undefined ? {} : { taskId: child.taskId }),
325
+ status,
326
+ failureKind,
327
+ ...(event.message === undefined ? {} : { message: event.message }),
328
+ timestamp: event.timestamp,
329
+ };
330
+ }
331
+ }
332
+
333
+ if (children.size === 0) return undefined;
334
+ let pending = 0;
335
+ let running = 0;
336
+ let completed = 0;
337
+ let failed = 0;
338
+ let cancelled = 0;
339
+ const activeChildRunIds: string[] = [];
340
+ for (const [childRunId, child] of children) {
341
+ if (child.status === "pending") pending += 1;
342
+ else if (child.status === "running") running += 1;
343
+ else if (child.status === "completed") completed += 1;
344
+ else if (child.status === "failed") failed += 1;
345
+ else if (child.status === "cancelled") cancelled += 1;
346
+ if (child.status === "pending" || child.status === "running")
347
+ activeChildRunIds.push(childRunId);
348
+ }
349
+ return {
350
+ total: children.size,
351
+ pending,
352
+ running,
353
+ completed,
354
+ failed,
355
+ cancelled,
356
+ activeChildRunIds,
357
+ latestFailure,
358
+ worstDescendantStatus:
359
+ failed > 0
360
+ ? "failed"
361
+ : cancelled > 0
362
+ ? "cancelled"
363
+ : running > 0
364
+ ? "running"
365
+ : pending > 0
366
+ ? "pending"
367
+ : children.size > 0
368
+ ? "completed"
369
+ : null,
370
+ };
371
+ }
372
+
373
+ function withChildSummary<T extends RunStatusSnapshot>(
374
+ snapshot: T,
375
+ childSummary: RunChildSummary | undefined,
376
+ ): T {
377
+ return childSummary === undefined ? snapshot : { ...snapshot, childSummary };
378
+ }
379
+
380
+ export function createRunStatusSnapshot(
381
+ result: ResultEnvelope,
382
+ ): RunStatusSnapshot {
383
+ const logs = resultLogs(result);
384
+ const resultArtifact =
385
+ logs.find((artifact) => artifact.type === "result") ?? null;
386
+
387
+ return {
388
+ runId: result.runId,
389
+ attemptId: result.attemptId,
390
+ ...(result.taskId === undefined ? {} : { taskId: result.taskId }),
391
+ ...(result.correlationId === undefined
392
+ ? {}
393
+ : { correlationId: result.correlationId }),
394
+ backend: result.backend,
395
+ status: result.status,
396
+ failureKind: result.failureKind,
397
+ startedAt: result.startedAt,
398
+ completedAt: result.completedAt,
399
+ durationMs: result.durationMs,
400
+ logs,
401
+ resultPath: resultArtifact?.path ?? null,
402
+ metadata: result.metadata,
403
+ ...(result.completion === undefined
404
+ ? {}
405
+ : { completion: result.completion }),
406
+ };
200
407
  }
201
408
 
202
409
  async function readJsonFile(path: string): Promise<unknown | null> {
203
- try {
204
- return JSON.parse(await readFile(path, "utf8"));
205
- } catch (error) {
206
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return null;
207
- throw error;
208
- }
410
+ try {
411
+ return JSON.parse(await readFile(path, "utf8"));
412
+ } catch (error) {
413
+ if (
414
+ error &&
415
+ typeof error === "object" &&
416
+ "code" in error &&
417
+ error.code === "ENOENT"
418
+ )
419
+ return null;
420
+ throw error;
421
+ }
209
422
  }
210
423
 
211
424
  function coerceResultEnvelope(value: unknown): ResultEnvelope | null {
212
- if (typeof value !== "object" || value === null) return null;
213
- const raw = value as Partial<ResultEnvelope> & { taskId?: string };
214
- if (typeof raw.runId !== "string" || typeof raw.backend !== "string" || typeof raw.status !== "string" || typeof raw.cwd !== "string") return null;
215
- const attemptId = typeof raw.attemptId === "string" ? raw.attemptId : typeof raw.taskId === "string" ? raw.taskId : "task-1";
216
- return {
217
- schemaVersion: 2,
218
- runId: raw.runId,
219
- attemptId,
220
- ...(raw.taskId === undefined ? {} : { taskId: raw.taskId }),
221
- ...(raw.correlationId === undefined ? {} : { correlationId: raw.correlationId }),
222
- backend: raw.backend,
223
- status: raw.status,
224
- failureKind: raw.failureKind ?? null,
225
- cwd: raw.cwd,
226
- startedAt: raw.startedAt ?? new Date(0).toISOString(),
227
- completedAt: raw.completedAt ?? null,
228
- durationMs: raw.durationMs ?? null,
229
- workspace: raw.workspace ?? { mode: "shared", cwd: raw.cwd, worktreePath: null },
230
- sandbox: raw.sandbox ?? { enabled: false },
231
- exitCode: raw.exitCode ?? null,
232
- signal: raw.signal ?? null,
233
- artifacts: Array.isArray(raw.artifacts) ? raw.artifacts : [],
234
- metadata: raw.metadata ?? { contextLengthExceeded: false },
235
- ...(raw.tmux === undefined ? {} : { tmux: raw.tmux }),
236
- ...(raw.completion === undefined ? {} : { completion: raw.completion }),
237
- } as ResultEnvelope;
425
+ if (typeof value !== "object" || value === null) return null;
426
+ const raw = value as Partial<ResultEnvelope> & { taskId?: string };
427
+ if (
428
+ typeof raw.runId !== "string" ||
429
+ typeof raw.backend !== "string" ||
430
+ typeof raw.status !== "string" ||
431
+ typeof raw.cwd !== "string"
432
+ )
433
+ return null;
434
+ const attemptId =
435
+ typeof raw.attemptId === "string"
436
+ ? raw.attemptId
437
+ : typeof raw.taskId === "string"
438
+ ? raw.taskId
439
+ : "task-1";
440
+ return {
441
+ schemaVersion: 2,
442
+ runId: raw.runId,
443
+ attemptId,
444
+ ...(raw.taskId === undefined ? {} : { taskId: raw.taskId }),
445
+ ...(raw.correlationId === undefined
446
+ ? {}
447
+ : { correlationId: raw.correlationId }),
448
+ backend: raw.backend,
449
+ status: raw.status,
450
+ failureKind: raw.failureKind ?? null,
451
+ cwd: raw.cwd,
452
+ startedAt: raw.startedAt ?? new Date(0).toISOString(),
453
+ completedAt: raw.completedAt ?? null,
454
+ durationMs: raw.durationMs ?? null,
455
+ workspace: raw.workspace ?? {
456
+ mode: "shared",
457
+ cwd: raw.cwd,
458
+ worktreePath: null,
459
+ },
460
+ sandbox: raw.sandbox ?? { enabled: false },
461
+ exitCode: raw.exitCode ?? null,
462
+ signal: raw.signal ?? null,
463
+ artifacts: Array.isArray(raw.artifacts) ? raw.artifacts : [],
464
+ metadata: raw.metadata ?? { contextLengthExceeded: false },
465
+ ...(raw.tmux === undefined ? {} : { tmux: raw.tmux }),
466
+ ...(raw.completion === undefined ? {} : { completion: raw.completion }),
467
+ } as ResultEnvelope;
238
468
  }
239
469
 
240
470
  async function readResultFile(path: string): Promise<ResultEnvelope | null> {
241
- return coerceResultEnvelope(await readJsonFile(path));
242
- }
243
-
244
- async function readRunResultFromAttempt(cwd: string, attempt: RunAttemptRecord | undefined): Promise<ResultEnvelope | null> {
245
- if (attempt?.resultPath === undefined || attempt.artifactCwd === undefined) return null;
246
- return await readResultFile(safeArtifactPath(attempt.artifactCwd, { path: attempt.resultPath }));
247
- }
248
-
249
- async function readRunResultFallback(ref: RunStatusRef): Promise<ResultEnvelope | null> {
250
- const { runDir } = pathsFor(ref);
251
- const attemptId = ref.attemptId ?? ref.taskId;
252
- if (attemptId !== undefined) {
253
- return (await readResultFile(join(runDir, "attempts", attemptId, "result.json")))
254
- ?? (await readResultFile(join(runDir, attemptId, "result.json")));
255
- }
256
- const attemptsDir = join(runDir, "attempts");
257
- const entries = await readdir(attemptsDir, { withFileTypes: true }).catch(() => []);
258
- const attemptDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
259
- const latest = attemptDirs.at(-1);
260
- if (latest !== undefined) return await readResultFile(join(attemptsDir, latest, "result.json"));
261
- // v1 fallback
262
- return await readResultFile(join(runDir, ref.taskId ?? "task-1", "result.json"));
471
+ return coerceResultEnvelope(await readJsonFile(path));
472
+ }
473
+
474
+ async function readRunResultFromAttempt(
475
+ attempt: RunAttemptRecord | undefined,
476
+ ): Promise<ResultEnvelope | null> {
477
+ if (attempt?.resultPath === undefined || attempt.artifactCwd === undefined)
478
+ return null;
479
+ return await readResultFile(
480
+ safeArtifactPath(attempt.artifactCwd, { path: attempt.resultPath }),
481
+ );
482
+ }
483
+
484
+ async function readRunResultFallback(
485
+ ref: RunStatusRef,
486
+ ): Promise<ResultEnvelope | null> {
487
+ const { runDir } = pathsFor(ref);
488
+ const attemptId = ref.attemptId ?? ref.taskId;
489
+ if (attemptId !== undefined) {
490
+ return (
491
+ (await readResultFile(
492
+ join(runDir, "attempts", attemptId, "result.json"),
493
+ )) ?? (await readResultFile(join(runDir, attemptId, "result.json")))
494
+ );
495
+ }
496
+ const attemptsDir = join(runDir, "attempts");
497
+ const entries = await readdir(attemptsDir, { withFileTypes: true }).catch(
498
+ () => [],
499
+ );
500
+ const attemptDirs = entries
501
+ .filter((entry) => entry.isDirectory())
502
+ .map((entry) => entry.name)
503
+ .sort();
504
+ const latest = attemptDirs.at(-1);
505
+ if (latest !== undefined)
506
+ return await readResultFile(join(attemptsDir, latest, "result.json"));
507
+ // v1 fallback
508
+ return await readResultFile(
509
+ join(runDir, ref.taskId ?? "task-1", "result.json"),
510
+ );
263
511
  }
264
512
 
265
513
  function recordLogs(attempt: RunAttemptRecord): RunLogRef[] {
266
- return [
267
- artifactFromAttempt(attempt, "stdout", attempt.stdoutPath),
268
- artifactFromAttempt(attempt, "stderr", attempt.stderrPath),
269
- artifactFromAttempt(attempt, "output", attempt.outputPath),
270
- artifactFromAttempt(attempt, "result", attempt.resultPath),
271
- ].filter((artifact): artifact is RunLogRef => artifact !== null);
272
- }
273
-
274
- function selectedAttempt(record: RunRecord, ref: RunStatusRef): RunAttemptRecord | undefined {
275
- const requested = ref.attemptId ?? ref.taskId ?? record.latestAttemptId ?? record.activeAttemptId ?? undefined;
276
- return requested === undefined ? record.attempts.at(-1) : record.attempts.find((attempt) => attempt.attemptId === requested) ?? record.attempts.at(-1);
277
- }
278
-
279
- function snapshotFromRecord(record: RunRecord, ref: RunStatusRef, events: RunEvent[]): RunStatusSnapshot {
280
- const attempt = selectedAttempt(record, ref);
281
- return {
282
- runId: record.runId,
283
- attemptId: attempt?.attemptId ?? ref.attemptId ?? ref.taskId ?? record.latestAttemptId ?? "unknown",
284
- correlationId: record.correlationId,
285
- backend: attempt?.backend ?? record.backend ?? "headless",
286
- status: record.status,
287
- failureKind: record.failureKind,
288
- startedAt: attempt?.startedAt ?? record.startedAt,
289
- completedAt: record.completedAt,
290
- durationMs: null,
291
- logs: attempt === undefined ? [] : recordLogs(attempt),
292
- resultPath: attempt?.resultPath ?? null,
293
- metadata: { contextLengthExceeded: false },
294
- mode: record.mode,
295
- dependency: record.dependency,
296
- registryPath: relativeRunRecordPath(ref),
297
- eventsPath: relativeRunEventsPath(ref),
298
- eventTail: events,
299
- attempts: record.attempts.map(attemptSnapshot),
300
- };
301
- }
302
-
303
- function mergeRecordSnapshot(snapshot: RunStatusSnapshot, record: RunRecord, ref: RunStatusRef, events: RunEvent[]): RunStatusSnapshot {
304
- const attempt = record.attempts.find((candidate) => candidate.attemptId === snapshot.attemptId);
305
- return {
306
- ...snapshot,
307
- correlationId: snapshot.correlationId ?? record.correlationId,
308
- status: record.status,
309
- failureKind: record.failureKind,
310
- completedAt: record.completedAt,
311
- mode: record.mode,
312
- dependency: record.dependency,
313
- registryPath: relativeRunRecordPath(ref),
314
- eventsPath: relativeRunEventsPath(ref),
315
- eventTail: events,
316
- attempts: record.attempts.map(attemptSnapshot),
317
- logs: snapshot.logs.length > 0 ? snapshot.logs : attempt === undefined ? snapshot.logs : recordLogs(attempt),
318
- };
319
- }
320
-
321
- export async function readRunResult(ref: RunStatusRef): Promise<ResultEnvelope | null> {
322
- const record = await readRunRecord(ref);
323
- const resultFromRecord = record === null ? null : await readRunResultFromAttempt(pathsFor(ref).cwd, selectedAttempt(record, ref));
324
- return resultFromRecord ?? await readRunResultFallback(ref);
325
- }
326
-
327
- export async function getRunStatus(ref: RunStatusRef): Promise<RunStatusSnapshot | null> {
328
- const record = await readRunRecord(ref);
329
- const events = await readRunEvents(ref, EVENT_TAIL_LIMIT).catch(() => []);
330
- const result = await readRunResult(ref);
331
- if (result !== null) {
332
- const snapshot = createRunStatusSnapshot(result);
333
- return record === null ? snapshot : mergeRecordSnapshot(snapshot, record, ref, events);
334
- }
335
- return record === null ? null : snapshotFromRecord(record, ref, events);
336
- }
337
-
338
- export async function getRunLogs(ref: RunStatusRef): Promise<RunLogsSnapshot | null> {
339
- const { cwd } = pathsFor(ref);
340
- const snapshot = await getRunStatus(ref);
341
- if (snapshot === null) return null;
342
-
343
- const logText: RunLogsSnapshot["logText"] = {};
344
- for (const log of snapshot.logs) {
345
- logText[log.type] = await readFile(safeArtifactPath(cwd, log), "utf8").catch(() => "");
346
- }
347
- if (snapshot.eventsPath !== undefined) {
348
- logText.events = await readFile(safeArtifactPath(cwd, { path: snapshot.eventsPath }), "utf8").catch(() => "");
349
- }
350
- return { ...snapshot, logText };
351
- }
352
-
353
- export async function waitForRun(options: WaitForRunOptions): Promise<WaitForRunResult> {
354
- const timeoutMs = options.timeoutMs ?? 60_000;
355
- const pollIntervalMs = options.pollIntervalMs ?? 500;
356
- const deadline = Date.now() + timeoutMs;
357
- let snapshot = await getRunStatus(options);
358
- while (Date.now() <= deadline) {
359
- snapshot = await getRunStatus(options);
360
- if (snapshot !== null && isTerminalStatus(snapshot.status)) return { status: "completed", snapshot };
361
- await sleep(pollIntervalMs);
362
- }
363
- return { status: "timeout", snapshot };
514
+ return [
515
+ artifactFromAttempt(attempt, "stdout", attempt.stdoutPath),
516
+ artifactFromAttempt(attempt, "stderr", attempt.stderrPath),
517
+ artifactFromAttempt(attempt, "output", attempt.outputPath),
518
+ artifactFromAttempt(attempt, "result", attempt.resultPath),
519
+ ].filter((artifact): artifact is RunLogRef => artifact !== null);
520
+ }
521
+
522
+ function selectedAttempt(
523
+ record: RunRecord,
524
+ ref: RunStatusRef,
525
+ ): RunAttemptRecord | undefined {
526
+ const requested =
527
+ ref.attemptId ??
528
+ ref.taskId ??
529
+ record.latestAttemptId ??
530
+ record.activeAttemptId ??
531
+ undefined;
532
+ return requested === undefined
533
+ ? record.attempts.at(-1)
534
+ : (record.attempts.find((attempt) => attempt.attemptId === requested) ??
535
+ record.attempts.at(-1));
536
+ }
537
+
538
+ function snapshotFromRecord(
539
+ record: RunRecord,
540
+ ref: RunStatusRef,
541
+ events: RunEvent[],
542
+ ): RunStatusSnapshot {
543
+ const attempt = selectedAttempt(record, ref);
544
+ return {
545
+ runId: record.runId,
546
+ attemptId:
547
+ attempt?.attemptId ??
548
+ ref.attemptId ??
549
+ ref.taskId ??
550
+ record.latestAttemptId ??
551
+ "unknown",
552
+ correlationId: record.correlationId,
553
+ backend: attempt?.backend ?? record.backend ?? "headless",
554
+ status: record.status,
555
+ failureKind: record.failureKind,
556
+ startedAt: attempt?.startedAt ?? record.startedAt,
557
+ completedAt: record.completedAt,
558
+ durationMs: null,
559
+ logs: attempt === undefined ? [] : recordLogs(attempt),
560
+ resultPath: attempt?.resultPath ?? null,
561
+ metadata: { contextLengthExceeded: false },
562
+ mode: record.mode,
563
+ dependency: record.dependency,
564
+ registryPath: relativeRunRecordPath(ref),
565
+ eventsPath: relativeRunEventsPath(ref),
566
+ eventTail: events,
567
+ attempts: record.attempts.map(attemptSnapshot),
568
+ };
569
+ }
570
+
571
+ function mergeRecordSnapshot(
572
+ snapshot: RunStatusSnapshot,
573
+ record: RunRecord,
574
+ ref: RunStatusRef,
575
+ events: RunEvent[],
576
+ ): RunStatusSnapshot {
577
+ const attempt = record.attempts.find(
578
+ (candidate) => candidate.attemptId === snapshot.attemptId,
579
+ );
580
+ return {
581
+ ...snapshot,
582
+ correlationId: snapshot.correlationId ?? record.correlationId,
583
+ status: record.status,
584
+ failureKind: record.failureKind,
585
+ completedAt: record.completedAt,
586
+ mode: record.mode,
587
+ dependency: record.dependency,
588
+ registryPath: relativeRunRecordPath(ref),
589
+ eventsPath: relativeRunEventsPath(ref),
590
+ eventTail: events,
591
+ attempts: record.attempts.map(attemptSnapshot),
592
+ logs:
593
+ snapshot.logs.length > 0
594
+ ? snapshot.logs
595
+ : attempt === undefined
596
+ ? snapshot.logs
597
+ : recordLogs(attempt),
598
+ };
599
+ }
600
+
601
+ export async function readRunResult(
602
+ ref: RunStatusRef,
603
+ ): Promise<ResultEnvelope | null> {
604
+ const resolvedRef = await resolveRunRef(ref);
605
+ const record = await readRunRecord(resolvedRef);
606
+ const resultFromRecord =
607
+ record === null
608
+ ? null
609
+ : await readRunResultFromAttempt(selectedAttempt(record, resolvedRef));
610
+ return resultFromRecord ?? (await readRunResultFallback(resolvedRef));
611
+ }
612
+
613
+ export async function getRunStatus(
614
+ ref: RunStatusRef,
615
+ ): Promise<RunStatusSnapshot | null> {
616
+ const resolvedRef = await resolveRunRef(ref);
617
+ const record = await readRunRecord(resolvedRef);
618
+ const scannedEvents = await readRunEvents(
619
+ resolvedRef,
620
+ CHILD_EVENT_SCAN_LIMIT,
621
+ ).catch(() => []);
622
+ const events = scannedEvents.slice(-EVENT_TAIL_LIMIT);
623
+ const childSummary = summarizeChildEvents(scannedEvents);
624
+ const result = await readRunResult(resolvedRef);
625
+ if (result !== null) {
626
+ const snapshot = createRunStatusSnapshot(result);
627
+ return withChildSummary(
628
+ record === null
629
+ ? snapshot
630
+ : mergeRecordSnapshot(snapshot, record, resolvedRef, events),
631
+ childSummary,
632
+ );
633
+ }
634
+ return record === null
635
+ ? null
636
+ : withChildSummary(
637
+ snapshotFromRecord(record, resolvedRef, events),
638
+ childSummary,
639
+ );
640
+ }
641
+
642
+ export async function getRunLogs(
643
+ ref: RunStatusRef,
644
+ ): Promise<RunLogsSnapshot | null> {
645
+ const resolvedRef = await resolveRunRef(ref);
646
+ const { cwd } = pathsFor(resolvedRef);
647
+ const snapshot = await getRunStatus(resolvedRef);
648
+ if (snapshot === null) return null;
649
+
650
+ const logText: RunLogsSnapshot["logText"] = {};
651
+ for (const log of snapshot.logs) {
652
+ logText[log.type] = await readFile(
653
+ safeArtifactPath(cwd, log),
654
+ "utf8",
655
+ ).catch(() => "");
656
+ }
657
+ if (snapshot.eventsPath !== undefined) {
658
+ logText.events = await readFile(
659
+ safeArtifactPath(cwd, { path: snapshot.eventsPath }),
660
+ "utf8",
661
+ ).catch(() => "");
662
+ }
663
+ return { ...snapshot, logText };
664
+ }
665
+
666
+ export async function waitForRun(
667
+ options: WaitForRunOptions,
668
+ ): Promise<WaitForRunResult> {
669
+ const timeoutMs = options.timeoutMs ?? 60_000;
670
+ const pollIntervalMs = options.pollIntervalMs ?? 500;
671
+ const deadline = Date.now() + timeoutMs;
672
+ let snapshot = await getRunStatus(options);
673
+ while (Date.now() <= deadline) {
674
+ snapshot = await getRunStatus(options);
675
+ if (snapshot !== null && isTerminalStatus(snapshot.status))
676
+ return { status: "completed", snapshot };
677
+ await sleep(pollIntervalMs);
678
+ }
679
+ return { status: "timeout", snapshot };
364
680
  }