@tea-agent/loop-agent 0.20.1-beta.0 → 0.20.1

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 (50) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/application/dag/args.js +29 -0
  3. package/dist/application/dag/run-dag.js +3 -1
  4. package/dist/cli/command-definitions.js +15 -1
  5. package/dist/cli/program.js +11 -1
  6. package/dist/commands/dag-rerun-task.js +19 -0
  7. package/dist/commands/dag-rerun.js +111 -0
  8. package/dist/shared/operator/capabilities.js +54 -0
  9. package/dist/worker/console/index.js +1 -1
  10. package/dist/worker/console/inspect-split.js +82 -0
  11. package/dist/worker/console/operation-runner.js +3 -1
  12. package/dist/worker/console/operation-store.js +1 -0
  13. package/dist/worker/console/operator-actions.js +153 -2
  14. package/dist/worker/console/operator-user-error.js +10 -0
  15. package/dist/worker/console/pi-readiness.js +4 -0
  16. package/dist/worker/console/recovery-cta.js +116 -5
  17. package/dist/worker/console/recovery-selection.js +107 -0
  18. package/dist/worker/console/resolve-dag-run-for-task.js +50 -11
  19. package/dist/worker/console/routes.js +20 -0
  20. package/dist/worker/console/sibling-controller.js +12 -7
  21. package/dist/worker/console/static/assets/index-CUDke82y.js +18 -0
  22. package/dist/worker/console/static/assets/index-wSEksVSO.css +1 -0
  23. package/dist/worker/console/static/index.html +2 -2
  24. package/dist/worker/observability/read-model.js +60 -0
  25. package/dist/worker/observe/static/index.html +1 -1
  26. package/dist/worker/run-task/run-task.js +7 -0
  27. package/dist/workflows/dag/frontend-test-result-contract.js +64 -0
  28. package/dist/workflows/dag/init-hybrid.js +90 -48
  29. package/dist/workflows/dag/node-execution.js +40 -6
  30. package/dist/workflows/dag/output-protocol.js +76 -0
  31. package/dist/workflows/dag/rerun-plan.js +611 -0
  32. package/dist/workflows/dag/rerun-run.js +497 -0
  33. package/dist/workflows/dag/rerun-task.js +284 -0
  34. package/dist/workflows/dag/retry-policy.js +20 -1
  35. package/dist/workflows/dag/runner.js +50 -0
  36. package/dist/workflows/dag/skill-snapshot.js +22 -3
  37. package/dist/workflows/dag/types.js +7 -0
  38. package/dist/workflows/dag/validate.js +11 -0
  39. package/dist/workflows/dag/workspace-checkpoint.js +163 -0
  40. package/docs/README.md +1 -0
  41. package/docs/templates/agent-dag.schema.json +17 -2
  42. package/docs/templates/frontend-test-case-checklist.md +16 -1
  43. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +26 -2
  44. package/docs/templates/frontend-test-dag.json +65 -6
  45. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +4 -1
  46. package/package.json +1 -1
  47. package/skills/loop-agent/references/command-reference.md +3 -0
  48. package/skills/playwright-cli-case-generator/SKILL.md +35 -7
  49. package/dist/worker/console/static/assets/index-3vsjZJHq.js +0 -16
  50. package/dist/worker/console/static/assets/index-i1wV4LrY.css +0 -1
@@ -0,0 +1,497 @@
1
+ import { createHash } from "node:crypto";
2
+ import { access, copyFile, mkdir, readdir, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
5
+ import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
6
+ import { readControllerIdentityArtifact, captureControllerIdentity } from "./controller-identity.js";
7
+ import { DAG_RUNS_DIR, getDagRunDir, locateDagRun, readDagRunSpec, readDagRunState, } from "./lifecycle.js";
8
+ import { buildDagRunId, runDagContinuation, } from "./runner.js";
9
+ import { evaluateDagRerunPlan, evaluateLiveDagRerunBindings, } from "./rerun-plan.js";
10
+ import { prepareActiveRunDir, writeNodeRecord, writeRunSpec, writeRunState, } from "./run-store.js";
11
+ import { cloneParentSkillSnapshotForContinuation, readSkillSnapshot, } from "./skill-snapshot.js";
12
+ import { topoSortToRanks } from "./topo.js";
13
+ import { captureWorkspaceCheckpoint, readWorkspaceCheckpoint, WORKSPACE_CHECKPOINT_START_REL, WORKSPACE_CHECKPOINT_TERMINAL_REL, writeWorkspaceCheckpoint, } from "./workspace-checkpoint.js";
14
+ export const RERUN_PLAN_REL_PATH = ".runtime/rerun-plan.json";
15
+ export const IMPORT_MANIFEST_REL_PATH = ".runtime/import-manifest.json";
16
+ export const WORKER_ASSOCIATION_REL_PATH = ".runtime/worker-association.json";
17
+ function sha256Hex(content) {
18
+ return createHash("sha256").update(content).digest("hex");
19
+ }
20
+ async function fileExists(filePath) {
21
+ try {
22
+ await access(filePath);
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ function operatorRequestPath(cwd, requestId) {
30
+ return path.join(cwd, DAG_RUNS_DIR, ".operator-requests", `${requestId}.json`);
31
+ }
32
+ async function readExistingOperatorRequest(cwd, requestId) {
33
+ try {
34
+ const raw = JSON.parse(await readFile(operatorRequestPath(cwd, requestId), "utf-8"));
35
+ if (raw.result && typeof raw.result === "object") {
36
+ return { ...raw.result, idempotentReplay: true };
37
+ }
38
+ }
39
+ catch {
40
+ // not found or unreadable
41
+ }
42
+ return undefined;
43
+ }
44
+ async function persistOperatorRequest(cwd, result, requestId) {
45
+ await writeJsonAtomic(operatorRequestPath(cwd, requestId), {
46
+ schemaVersion: 1,
47
+ kind: "dag-rerun-from-node-request",
48
+ requestId,
49
+ parentRunId: result.plan.parentRunId,
50
+ newRunId: result.newRunId,
51
+ createdAt: new Date().toISOString(),
52
+ result,
53
+ }, { repoRoot: cwd });
54
+ }
55
+ function readOptionalString(record, key) {
56
+ const value = record[key];
57
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
58
+ }
59
+ async function detectWorkerManagedRun(parentRunDir, parentState, parentSpec) {
60
+ const association = parentState.workerAssociation;
61
+ if (association?.workerRunId) {
62
+ return {
63
+ workerManaged: true,
64
+ featureId: association.featureId,
65
+ taskId: association.taskId,
66
+ workerRunId: association.workerRunId,
67
+ };
68
+ }
69
+ const stateRecord = parentState;
70
+ const specRecord = parentSpec;
71
+ const workerRunId = readOptionalString(stateRecord, "workerRunId")
72
+ ?? readOptionalString(specRecord, "workerRunId");
73
+ const featureId = readOptionalString(stateRecord, "featureId")
74
+ ?? readOptionalString(specRecord, "featureId");
75
+ const taskId = readOptionalString(stateRecord, "taskId")
76
+ ?? readOptionalString(specRecord, "taskId")
77
+ ?? parentSpec.taskContractBinding?.taskId
78
+ ?? parentSpec.sourceBinding?.taskId;
79
+ if (workerRunId) {
80
+ return {
81
+ workerManaged: true,
82
+ ...(featureId ? { featureId } : {}),
83
+ ...(taskId ? { taskId } : {}),
84
+ workerRunId,
85
+ };
86
+ }
87
+ if (await fileExists(path.join(parentRunDir, WORKER_ASSOCIATION_REL_PATH))) {
88
+ return {
89
+ workerManaged: true,
90
+ ...(featureId ? { featureId } : {}),
91
+ ...(taskId ? { taskId } : {}),
92
+ };
93
+ }
94
+ return { workerManaged: false };
95
+ }
96
+ async function resolveControllerFingerprints(parentRunDir) {
97
+ const current = resolveRunningControllerIdentity();
98
+ const parentArtifact = await readControllerIdentityArtifact(parentRunDir);
99
+ return {
100
+ currentControllerFingerprint: current?.packageFingerprint.value,
101
+ parentControllerFingerprint: parentArtifact?.identity?.packageFingerprint.value,
102
+ };
103
+ }
104
+ async function verifyParentSkillSnapshot(parentRunDir, parentState, parentRunId) {
105
+ if (!parentState.skillSnapshotRef)
106
+ return false;
107
+ try {
108
+ await readSkillSnapshot(parentRunDir, parentState.skillSnapshotRef, {
109
+ expectedRunId: parentRunId,
110
+ });
111
+ return true;
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ }
117
+ export async function planDagRerun(input) {
118
+ const located = await locateDagRun(input.cwd, input.parentRunId);
119
+ if (!located) {
120
+ throw new Error(`dag run not found: ${input.parentRunId}`);
121
+ }
122
+ if (located.lifecycle !== "completed") {
123
+ const nonCompletedSpec = await readDagRunSpec(located.runDir);
124
+ const nonCompletedState = await readDagRunState(located.runDir);
125
+ const nonCompletedWorkerHints = await detectWorkerManagedRun(located.runDir, nonCompletedState, nonCompletedSpec);
126
+ return evaluateDagRerunPlan({
127
+ cwd: input.cwd,
128
+ parentRunId: input.parentRunId,
129
+ selectedNodeId: input.selectedNodeId,
130
+ parentRunDir: located.runDir,
131
+ parentSpec: nonCompletedSpec,
132
+ parentState: nonCompletedState,
133
+ parentLifecycle: located.lifecycle,
134
+ workerManaged: nonCompletedWorkerHints.workerManaged,
135
+ workerAssociation: nonCompletedWorkerHints.workerManaged
136
+ ? {
137
+ kind: "worker-managed",
138
+ ...(nonCompletedWorkerHints.featureId ? { featureId: nonCompletedWorkerHints.featureId } : {}),
139
+ ...(nonCompletedWorkerHints.taskId ? { taskId: nonCompletedWorkerHints.taskId } : {}),
140
+ ...(nonCompletedWorkerHints.workerRunId ? { workerRunId: nonCompletedWorkerHints.workerRunId } : {}),
141
+ }
142
+ : { kind: "standalone" },
143
+ bindingStatus: await evaluateLiveDagRerunBindings({
144
+ cwd: input.cwd,
145
+ parentSpec: nonCompletedSpec,
146
+ }),
147
+ });
148
+ }
149
+ const parentSpec = await readDagRunSpec(located.runDir);
150
+ const parentState = await readDagRunState(located.runDir);
151
+ const currentWorkspace = await captureWorkspaceCheckpoint(input.cwd).catch(() => undefined);
152
+ const parentTerminalWorkspace = await readWorkspaceCheckpoint(located.runDir, WORKSPACE_CHECKPOINT_TERMINAL_REL);
153
+ const fingerprints = await resolveControllerFingerprints(located.runDir);
154
+ const skillSnapshotOk = await verifyParentSkillSnapshot(located.runDir, parentState, input.parentRunId);
155
+ const workerHints = await detectWorkerManagedRun(located.runDir, parentState, parentSpec);
156
+ const bindingStatus = await evaluateLiveDagRerunBindings({
157
+ cwd: input.cwd,
158
+ parentSpec,
159
+ });
160
+ return evaluateDagRerunPlan({
161
+ cwd: input.cwd,
162
+ parentRunId: input.parentRunId,
163
+ selectedNodeId: input.selectedNodeId,
164
+ parentRunDir: located.runDir,
165
+ parentSpec,
166
+ parentState,
167
+ parentLifecycle: "completed",
168
+ currentControllerFingerprint: fingerprints.currentControllerFingerprint,
169
+ parentControllerFingerprint: fingerprints.parentControllerFingerprint,
170
+ parentTerminalWorkspace,
171
+ currentWorkspace,
172
+ skillSnapshotOk,
173
+ workerManaged: workerHints.workerManaged,
174
+ workerAssociation: workerHints.workerManaged
175
+ ? {
176
+ kind: "worker-managed",
177
+ ...(workerHints.featureId ? { featureId: workerHints.featureId } : {}),
178
+ ...(workerHints.taskId ? { taskId: workerHints.taskId } : {}),
179
+ ...(workerHints.workerRunId ? { workerRunId: workerHints.workerRunId } : {}),
180
+ }
181
+ : { kind: "standalone" },
182
+ bindingStatus,
183
+ });
184
+ }
185
+ function normalizeRunRelativePath(relPath) {
186
+ return relPath.replace(/\\/g, "/");
187
+ }
188
+ function assertArtifactPathWithinParentRun(parentRunDir, artifactPath) {
189
+ if (!path.isAbsolute(artifactPath)) {
190
+ return normalizeRunRelativePath(artifactPath);
191
+ }
192
+ const resolved = path.resolve(artifactPath);
193
+ const parentResolved = path.resolve(parentRunDir);
194
+ if (resolved !== parentResolved
195
+ && !resolved.startsWith(`${parentResolved}${path.sep}`)) {
196
+ throw new Error(`artifact path outside parent run directory: ${artifactPath}`);
197
+ }
198
+ return normalizeRunRelativePath(path.relative(parentResolved, resolved));
199
+ }
200
+ function collectNodeArtifactRelativePaths(record) {
201
+ const paths = new Set([`${record.id}.json`]);
202
+ const add = (value) => {
203
+ if (value?.trim())
204
+ paths.add(normalizeRunRelativePath(value.trim()));
205
+ };
206
+ add(record.stdoutArtifactPath);
207
+ add(record.assistantArtifactPath);
208
+ add(record.structuredArtifactPath);
209
+ add(record.nodeRecordPath);
210
+ add(record.escalationArtifactPath);
211
+ add(record.humanApprovalArtifactPath);
212
+ add(record.humanRejectionArtifactPath);
213
+ for (const attempt of record.attempts ?? []) {
214
+ add(attempt.artifactPath);
215
+ }
216
+ return [...paths];
217
+ }
218
+ async function listNodeDirectoryArtifacts(parentRunDir, nodeId) {
219
+ const nodeDir = path.join(parentRunDir, nodeId);
220
+ if (!(await fileExists(nodeDir)))
221
+ return [];
222
+ const entries = await readdir(nodeDir, { withFileTypes: true });
223
+ const artifacts = [];
224
+ for (const entry of entries) {
225
+ if (!entry.isFile())
226
+ continue;
227
+ artifacts.push(normalizeRunRelativePath(path.join(nodeId, entry.name)));
228
+ }
229
+ return artifacts;
230
+ }
231
+ async function copyArtifactIntoRun(input) {
232
+ const normalized = assertArtifactPathWithinParentRun(input.parentRunDir, input.relativePath);
233
+ const sourcePath = path.join(input.parentRunDir, ...normalized.split("/"));
234
+ if (!(await fileExists(sourcePath))) {
235
+ throw new Error(`missing imported artifact: ${normalized}`);
236
+ }
237
+ const destinationPath = path.join(input.newRunDir, ...normalized.split("/"));
238
+ await mkdir(path.dirname(destinationPath), { recursive: true });
239
+ await copyFile(sourcePath, destinationPath);
240
+ const bytes = await readFile(destinationPath);
241
+ return {
242
+ destinationRelativePath: normalized,
243
+ sha256: sha256Hex(bytes),
244
+ };
245
+ }
246
+ async function importNodeFacts(input) {
247
+ const nodeJsonRel = `${input.nodeId}.json`;
248
+ const parentNodeJsonPath = path.join(input.parentRunDir, nodeJsonRel);
249
+ const sourceNodeRecordSha256 = (await fileExists(parentNodeJsonPath))
250
+ ? sha256Hex(await readFile(parentNodeJsonPath))
251
+ : sha256Hex(`${JSON.stringify(input.parentRecord, null, 2)}\n`);
252
+ const importedRecord = buildImportedNodeRecord(input.parentRecord, input.parentRunId, sourceNodeRecordSha256, input.parentRunDir, input.newRunDir);
253
+ await writeNodeRecord(input.newRunDir, input.nodeId, importedRecord);
254
+ const relativePaths = [
255
+ ...new Set([
256
+ ...collectNodeArtifactRelativePaths(input.parentRecord).filter((relativePath) => relativePath !== nodeJsonRel),
257
+ ...(await listNodeDirectoryArtifacts(input.parentRunDir, input.nodeId)),
258
+ ]),
259
+ ];
260
+ const importedArtifacts = [
261
+ {
262
+ sourceRelativePath: nodeJsonRel,
263
+ destinationRelativePath: nodeJsonRel,
264
+ sha256: sha256Hex(await readFile(path.join(input.newRunDir, nodeJsonRel))),
265
+ },
266
+ ];
267
+ for (const relativePath of relativePaths.sort()) {
268
+ const copied = await copyArtifactIntoRun({
269
+ parentRunDir: input.parentRunDir,
270
+ newRunDir: input.newRunDir,
271
+ relativePath,
272
+ });
273
+ importedArtifacts.push({
274
+ sourceRelativePath: relativePath,
275
+ destinationRelativePath: copied.destinationRelativePath,
276
+ sha256: copied.sha256,
277
+ });
278
+ }
279
+ return {
280
+ manifestNode: {
281
+ nodeId: input.nodeId,
282
+ sourceNodeRecordSha256,
283
+ importedArtifacts,
284
+ },
285
+ importedRecord,
286
+ };
287
+ }
288
+ function normalizeImportedArtifactPath(parentRunDir, newRunDir, artifactPath) {
289
+ if (!artifactPath?.trim())
290
+ return artifactPath;
291
+ const relative = assertArtifactPathWithinParentRun(parentRunDir, artifactPath);
292
+ return path.join(newRunDir, ...relative.split("/"));
293
+ }
294
+ function buildImportedNodeRecord(parentRecord, parentRunId, sourceNodeRecordSha256, parentRunDir, newRunDir) {
295
+ const { origin: _origin, ...rest } = parentRecord;
296
+ void _origin;
297
+ const imported = structuredClone(rest);
298
+ imported.nodeRecordPath = path.join(newRunDir, `${parentRecord.id}.json`);
299
+ for (const key of [
300
+ "stdoutArtifactPath",
301
+ "assistantArtifactPath",
302
+ "structuredArtifactPath",
303
+ "escalationArtifactPath",
304
+ "humanApprovalArtifactPath",
305
+ "humanRejectionArtifactPath",
306
+ ]) {
307
+ imported[key] = normalizeImportedArtifactPath(parentRunDir, newRunDir, imported[key]);
308
+ }
309
+ return {
310
+ ...imported,
311
+ origin: {
312
+ kind: "imported",
313
+ parentRunId,
314
+ sourceNodeRecordSha256,
315
+ },
316
+ };
317
+ }
318
+ function buildPendingNodeRecord(task) {
319
+ return {
320
+ id: task.id,
321
+ status: "PENDING",
322
+ executor: task.executor,
323
+ complexity: task.complexity,
324
+ origin: { kind: "executed" },
325
+ ...(task.outputMode ? { outputMode: task.outputMode } : {}),
326
+ };
327
+ }
328
+ async function stageContinuationRun(input) {
329
+ const { ranks } = topoSortToRanks(input.parentSpec);
330
+ const runId = await buildDagRunId(input.parentSpec, { cwd: input.cwd });
331
+ const runDir = getDagRunDir(input.cwd, "active", runId);
332
+ await prepareActiveRunDir(runDir);
333
+ await writeRunSpec(runDir, input.parentSpec);
334
+ const resetSet = new Set(input.plan.resetNodeIds);
335
+ const importedSet = new Set(input.plan.importedNodeIds);
336
+ const tasksById = new Map(input.parentSpec.tasks.map((task) => [task.id, task]));
337
+ const importManifestNodes = [];
338
+ const nodes = {};
339
+ for (const nodeId of input.plan.importedNodeIds) {
340
+ const parentRecord = input.parentState.nodes[nodeId];
341
+ if (!parentRecord) {
342
+ throw new Error(`parent node record missing for imported node: ${nodeId}`);
343
+ }
344
+ const { manifestNode, importedRecord } = await importNodeFacts({
345
+ parentRunDir: input.parentRunDir,
346
+ newRunDir: runDir,
347
+ parentRunId: input.parentRunId,
348
+ nodeId,
349
+ parentRecord,
350
+ });
351
+ importManifestNodes.push(manifestNode);
352
+ nodes[nodeId] = importedRecord;
353
+ }
354
+ for (const nodeId of input.plan.resetNodeIds) {
355
+ const task = tasksById.get(nodeId);
356
+ if (!task) {
357
+ throw new Error(`reset node missing from spec: ${nodeId}`);
358
+ }
359
+ nodes[nodeId] = buildPendingNodeRecord(task);
360
+ }
361
+ for (const task of input.parentSpec.tasks) {
362
+ if (!resetSet.has(task.id) && !importedSet.has(task.id)) {
363
+ throw new Error(`node ${task.id} missing from rerun plan partition`);
364
+ }
365
+ }
366
+ const createdAt = new Date().toISOString();
367
+ const continuation = {
368
+ schemaVersion: 1,
369
+ kind: "rerun-from-node",
370
+ parentRunId: input.parentRunId,
371
+ operatorSelectedNodeId: input.plan.selectedNodeId,
372
+ effectiveFromNodeId: input.plan.effectiveFromNodeId,
373
+ rewriteApplied: input.plan.rewriteApplied,
374
+ reason: input.reason,
375
+ requestId: input.requestId,
376
+ planHash: input.plan.planHash,
377
+ createdAt,
378
+ };
379
+ const state = {
380
+ version: 1,
381
+ title: input.parentSpec.title,
382
+ runId,
383
+ cwd: input.cwd,
384
+ startedAt: createdAt,
385
+ status: "running",
386
+ ranks,
387
+ nodes,
388
+ continuation,
389
+ ...(input.parentState.evaluation
390
+ ? { evaluation: structuredClone(input.parentState.evaluation) }
391
+ : {}),
392
+ ...(input.parentState.budget
393
+ ? { budget: structuredClone(input.parentState.budget) }
394
+ : {}),
395
+ };
396
+ if (!input.parentState.skillSnapshotRef) {
397
+ throw new Error("parent run is missing skill snapshot ref");
398
+ }
399
+ state.skillSnapshotRef = await cloneParentSkillSnapshotForContinuation({
400
+ parentRunDir: input.parentRunDir,
401
+ parentRef: input.parentState.skillSnapshotRef,
402
+ parentRunId: input.parentRunId,
403
+ newRunId: runId,
404
+ newRunDir: runDir,
405
+ });
406
+ state.controllerIdentityRef = await captureControllerIdentity({ runDir });
407
+ try {
408
+ const startCheckpoint = await captureWorkspaceCheckpoint(input.cwd);
409
+ await writeWorkspaceCheckpoint(runDir, WORKSPACE_CHECKPOINT_START_REL, startCheckpoint);
410
+ }
411
+ catch (error) {
412
+ console.warn(`[dag rerun] warning: failed to write workspace start checkpoint: ${error instanceof Error ? error.message : String(error)}`);
413
+ }
414
+ const importManifest = {
415
+ schemaVersion: 1,
416
+ parentRunId: input.parentRunId,
417
+ createdAt,
418
+ nodes: importManifestNodes,
419
+ };
420
+ await writeJsonAtomic(path.join(runDir, IMPORT_MANIFEST_REL_PATH), importManifest);
421
+ await writeJsonAtomic(path.join(runDir, RERUN_PLAN_REL_PATH), input.plan);
422
+ await writeRunState(runDir, state);
423
+ return { runId, runDir, state };
424
+ }
425
+ export async function executeDagRerun(input) {
426
+ const existing = await readExistingOperatorRequest(input.cwd, input.requestId);
427
+ if (existing) {
428
+ return existing;
429
+ }
430
+ const plan = await planDagRerun({
431
+ cwd: input.cwd,
432
+ parentRunId: input.parentRunId,
433
+ selectedNodeId: input.selectedNodeId,
434
+ });
435
+ if (plan.planHash !== input.planHash) {
436
+ return {
437
+ ok: false,
438
+ plan,
439
+ errorCode: "PLAN_DRIFT",
440
+ message: "plan hash mismatch; re-run plan before executing",
441
+ };
442
+ }
443
+ if (!plan.eligible) {
444
+ return {
445
+ ok: false,
446
+ plan,
447
+ errorCode: "INELIGIBLE",
448
+ message: plan.blockedReasons.join(", ") || "rerun plan is not eligible",
449
+ };
450
+ }
451
+ const located = await locateDagRun(input.cwd, input.parentRunId);
452
+ if (!located || located.lifecycle !== "completed") {
453
+ return {
454
+ ok: false,
455
+ plan,
456
+ errorCode: "PARENT_LIFECYCLE",
457
+ message: `parent run must be completed: ${input.parentRunId}`,
458
+ };
459
+ }
460
+ const parentSpec = await readDagRunSpec(located.runDir);
461
+ const parentState = await readDagRunState(located.runDir);
462
+ const staged = await stageContinuationRun({
463
+ cwd: input.cwd,
464
+ parentRunDir: located.runDir,
465
+ parentRunId: input.parentRunId,
466
+ parentSpec,
467
+ parentState,
468
+ plan,
469
+ reason: input.reason,
470
+ requestId: input.requestId,
471
+ });
472
+ const continuationOptions = {
473
+ cwd: input.cwd,
474
+ spec: parentSpec,
475
+ state: staged.state,
476
+ runDir: staged.runDir,
477
+ ...(input.maxConcurrent !== undefined
478
+ ? { maxConcurrent: input.maxConcurrent }
479
+ : {}),
480
+ ...(input.executeNode ? { executeNode: input.executeNode } : {}),
481
+ };
482
+ const summary = await runDagContinuation(continuationOptions);
483
+ const result = {
484
+ ok: summary.status === "finished",
485
+ plan,
486
+ newRunId: summary.runId,
487
+ runDir: summary.runDir,
488
+ summary,
489
+ };
490
+ if (await fileExists(operatorRequestPath(input.cwd, input.requestId))) {
491
+ const replay = await readExistingOperatorRequest(input.cwd, input.requestId);
492
+ if (replay)
493
+ return replay;
494
+ }
495
+ await persistOperatorRequest(input.cwd, result, input.requestId);
496
+ return result;
497
+ }