@tea-agent/loop-agent 0.41.0 → 0.42.0-next.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 (162) hide show
  1. package/CHANGELOG.md +102 -14
  2. package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
  3. package/dist/adapters/context-transfer/pi-session.js +61 -0
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/command-definitions.js +12 -0
  6. package/dist/cli/program.js +23 -1
  7. package/dist/commands/dag-artifact.js +284 -0
  8. package/dist/commands/dag-context.js +184 -0
  9. package/dist/commands/dag-rerun.js +206 -1
  10. package/dist/executors/dag-pi-executor.js +39 -12
  11. package/dist/executors/pi-sdk-executor.js +3 -0
  12. package/dist/{worker → infrastructure}/console/app-data.js +4 -0
  13. package/dist/infrastructure/console/artifact-revision-store.js +430 -0
  14. package/dist/infrastructure/console/context-export-store.js +160 -0
  15. package/dist/infrastructure/console/dir-lock.js +132 -0
  16. package/dist/{worker → infrastructure}/console/operation-store.js +28 -0
  17. package/dist/infrastructure/harness/artifact-store.js +10 -1
  18. package/dist/infrastructure/harness/atomic-write.js +12 -2
  19. package/dist/shared/context-transfer/artifact-revision.js +172 -0
  20. package/dist/shared/context-transfer.js +418 -0
  21. package/dist/shared/operator/capabilities.js +14 -0
  22. package/dist/shared/operator/safe-run-summary.js +1 -0
  23. package/dist/shared/path-safety.js +93 -0
  24. package/dist/shared/preview.js +28 -4
  25. package/dist/task/config-types.js +2 -1
  26. package/dist/task/task-demand-routing.js +3 -0
  27. package/dist/worker/console/chat/chat-event-store.js +2 -2
  28. package/dist/worker/console/chat/resource-preferences-store.js +1 -1
  29. package/dist/worker/console/chat/routes.js +1 -1
  30. package/dist/worker/console/chat/session-store.js +1 -1
  31. package/dist/worker/console/chat/user-questions.js +1 -1
  32. package/dist/worker/console/console-update-runtime.js +1 -1
  33. package/dist/worker/console/context-transfer-diagnostics.js +198 -0
  34. package/dist/worker/console/dag-confirmation.js +1 -1
  35. package/dist/worker/console/dag-execution-receipt.js +1 -1
  36. package/dist/worker/console/doctor.js +1 -1
  37. package/dist/worker/console/draft-store.js +1 -1
  38. package/dist/worker/console/human-gate-token.js +1 -1
  39. package/dist/worker/console/index.js +3 -3
  40. package/dist/worker/console/interview/assessment.js +1 -1
  41. package/dist/worker/console/interview/session.js +1 -1
  42. package/dist/worker/console/operation-runner.js +45 -4
  43. package/dist/worker/console/operation-sse.js +1 -1
  44. package/dist/worker/console/operation-wait.js +1 -1
  45. package/dist/worker/console/operator-actions.js +104 -3
  46. package/dist/worker/console/operator-surface-health.js +1 -1
  47. package/dist/worker/console/operator-user-error.js +169 -0
  48. package/dist/worker/console/pi-plugins.js +57 -0
  49. package/dist/worker/console/routes.js +637 -8
  50. package/dist/worker/console/security.js +35 -0
  51. package/dist/worker/console/server.js +6 -3
  52. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DwVJxUxz.js → abnfDiagram-N423BO3Z-DZZ8m3PO.js} +1 -1
  53. package/dist/worker/console/static/assets/{arc-DqfCcXdu.js → arc-D6PvaVd-.js} +1 -1
  54. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-qMVtggRv.js → architectureDiagram-T3A2C74G-B_OTOiI8.js} +1 -1
  55. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-ECrV38dh.js → blockDiagram-VBNYF7ZC-Bv6rqHBg.js} +1 -1
  56. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DBaaLqQv.js → c4Diagram-5PPSVZJV-B8eHr0oz.js} +1 -1
  57. package/dist/worker/console/static/assets/channel-BU5gOilw.js +1 -0
  58. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BvfNHkAz.js → chunk-2GRJ4B5K-DYwR0im2.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-C5StiIbc.js → chunk-2Q5K7J3B-D2WPGqXt.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-HabGiJkS.js → chunk-5RXB4S5H-CQISJ_I7.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BiTg127i.js → chunk-5VM5RSS4-C0o2Du1e.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CZsXBPuR.js → chunk-6Q2QTUOP-4f8kr-U7.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D-r1MxLK.js → chunk-GF5L2VYU-D_OWvXzX.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DQxb9plr.js → chunk-JWPE2WC7-CasdPz5X.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DMndGvCT.js → chunk-KBJHAD2P-Cj8lRrla.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ByL4BgOJ.js → chunk-RYQCIY6F-CUvD1FSd.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BVe1b1gL.js → chunk-XXDRQBXY-CqLw_eqB.js} +1 -1
  68. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +1 -0
  69. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +1 -0
  70. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BAyGzN8j.js → cose-bilkent-JH36ORCC-ku-WqJPx.js} +1 -1
  71. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C5yVYU7E.js → cynefin-VYW2F7L2-Bq_PYDhl.js} +1 -1
  72. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-x-MwsEm9.js → cynefinDiagram-MW4NZA55-BiGZV641.js} +1 -1
  73. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CT5Tle5F.js → dagre-VZM6K2ZE-C4JRlglF.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BARERhlS.js → diagram-7IWD3JNH-B0dNeWYG.js} +1 -1
  75. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BwfUz0VM.js → diagram-B4RE2ZJO-Cj35YvYM.js} +1 -1
  76. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Mk4r2WvJ.js → diagram-LBJQPF4R-uzQoJ2-8.js} +1 -1
  77. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-KUrxuBs0.js → diagram-Q27KOJAE-D1a-Buoz.js} +1 -1
  78. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CjZOceZn.js → diagram-UB23O5K3-XjRrLRSs.js} +1 -1
  79. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-rnwUCiEI.js → ebnfDiagram-BXEA7PRR-Da_O24cW.js} +1 -1
  80. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DM4j23FM.js → erDiagram-JOGREHBK-BLJ8jrYU.js} +1 -1
  81. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DD5HdDEs.js → flowDiagram-UKHOOZJN-B9GrLjM0.js} +1 -1
  82. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CIAGSF47.js → ganttDiagram-PKOTCBZU-CeJ0TqiK.js} +1 -1
  83. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DbwFbVEL.js → gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js} +1 -1
  84. package/dist/worker/console/static/assets/index-H9rFJiGL.css +1 -0
  85. package/dist/worker/console/static/assets/{index-Bih9w9h9.js → index-xwu9GxEc.js} +80 -80
  86. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-B-Pb0T66.js → infoDiagram-6WML65LV-DB26i3d8.js} +1 -1
  87. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B9Unk-YX.js → ishikawaDiagram-WSZJBQD7-BOGRyeZT.js} +1 -1
  88. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C2F3JkPt.js → journeyDiagram-NVQOT4AX-CZRoBO_6.js} +1 -1
  89. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Bl8-Xcoz.js → kanban-definition-27J2QSJJ-qlWhJyoB.js} +1 -1
  90. package/dist/worker/console/static/assets/{linear-yK4uZmC5.js → linear-CfUiDDB3.js} +1 -1
  91. package/dist/worker/console/static/assets/{mermaid.core-Bpa6PmuE.js → mermaid.core-BQe6fpqj.js} +5 -5
  92. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BfGRoCSe.js → mindmap-definition-FAOFIHXS-zFzWHw64.js} +1 -1
  93. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Bz6_HtST.js → pegDiagram-VL7TDLO6-DFGUqjZO.js} +1 -1
  94. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-i0Umbsby.js → pieDiagram-7S7Q4E2Y-BB5i0l8Z.js} +1 -1
  95. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DaVQBobe.js → quadrantDiagram-CIZ2JOQS-obbVZ_X5.js} +1 -1
  96. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DMTGnSq7.js → railroadDiagram-AXF67PYL-BwdaNzc1.js} +1 -1
  97. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-2ao_DaSq.js → requirementDiagram-LRYGKXZP-CkTJaugh.js} +1 -1
  98. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-2SG1lTB9.js → sankeyDiagram-W5VNT64P-BwJ-hIgq.js} +1 -1
  99. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BQYOLybR.js → sequenceDiagram-SI44F4Z6-BzSgmm7w.js} +1 -1
  100. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-C8g3yjuI.js → sizeCapture-X5ZJPWSS-Bn3oj5Q1.js} +1 -1
  101. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-q_F8nAQG.js → stateDiagram-OKZ733FA-dxhGtL8K.js} +1 -1
  102. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +1 -0
  103. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-8LrWpH2s.js → swimlanes-SLNWSIFB-BLpEInya.js} +2 -2
  104. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +8 -0
  105. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BQoY3qzl.js → timeline-definition-Z64GVDOM-ChJGYcXy.js} +1 -1
  106. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BDu1UjSp.js → vennDiagram-T6HMQDX7-DK-qjRer.js} +1 -1
  107. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C48M7Y0a.js → wardleyDiagram-T6FBY63Y-3qeaWAg-.js} +1 -1
  108. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YU6FTl_F.js → xychartDiagram-ELKLHX3M-CootlyP9.js} +1 -1
  109. package/dist/worker/console/static/index.html +7 -2
  110. package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
  111. package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
  112. package/dist/worker/console/workspace-context.js +28 -2
  113. package/dist/worker/console/workspace-registry.js +2 -2
  114. package/dist/worker/continuation/worker-continuation.js +278 -0
  115. package/dist/worker/observe/health.js +1 -1
  116. package/dist/worker/observe/node-transparency.js +572 -0
  117. package/dist/worker/observe/routes.js +51 -1
  118. package/dist/worker/observe/static/api.js +69 -5
  119. package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
  120. package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
  121. package/dist/worker/observe/static/state.js +3 -1
  122. package/dist/worker/observe/static/styles.css +1037 -126
  123. package/dist/worker/observe/static/views/dag-inspector.js +1215 -58
  124. package/dist/worker/observe/static/views/session-timeline.js +75 -10
  125. package/dist/worker/outcomes/adapters.js +19 -0
  126. package/dist/worker/outcomes/types.js +1 -0
  127. package/dist/worker/pool/attempt-lease.js +97 -66
  128. package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
  129. package/dist/worker/pool/reconcile.js +46 -1
  130. package/dist/worker/pool/run-store.js +2 -0
  131. package/dist/worker/pool/state-projection.js +2 -0
  132. package/dist/worker/task-spec/workflow-routing.js +8 -3
  133. package/dist/workflows/dag/artifact-bindings.js +149 -0
  134. package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
  135. package/dist/workflows/dag/budget-enforcement.js +10 -0
  136. package/dist/workflows/dag/context-receipt.js +305 -0
  137. package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
  138. package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
  139. package/dist/workflows/dag/context-transfer/renderers.js +93 -0
  140. package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
  141. package/dist/workflows/dag/init-hybrid.js +146 -0
  142. package/dist/workflows/dag/node-execution.js +112 -27
  143. package/dist/workflows/dag/path-safety.js +1 -0
  144. package/dist/workflows/dag/prompt.js +29 -4
  145. package/dist/workflows/dag/rerun-plan.js +555 -1
  146. package/dist/workflows/dag/rerun-run.js +601 -15
  147. package/dist/workflows/dag/runner.js +20 -1
  148. package/dist/workflows/dag/skill-snapshot.js +17 -0
  149. package/dist/workflows/dag/types.js +183 -7
  150. package/docs/architecture/runtime-boundaries.md +6 -6
  151. package/docs/architecture/worker-and-feature.md +1 -1
  152. package/docs/templates/agent-dag.schema.json +32 -0
  153. package/docs/templates/product-line/task.yaml +1 -1
  154. package/package.json +3 -1
  155. package/skills/loop-agent/references/command-reference.md +6 -0
  156. package/dist/worker/console/static/assets/channel-CL1NRAmS.js +0 -1
  157. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-PBGzzjZA.js +0 -1
  158. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-PBGzzjZA.js +0 -1
  159. package/dist/worker/console/static/assets/index-BfleX-qj.css +0 -1
  160. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrxGBDJu.js +0 -1
  161. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BhpDxzAf.js +0 -8
  162. /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
@@ -0,0 +1,284 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { readRevisionParentArtifact } from "../workflows/dag/context-transfer/operator-actions.js";
5
+ import { readFileBoundarySafe } from "../shared/path-safety.js";
6
+ import { openConsoleAppData } from "../infrastructure/console/app-data.js";
7
+ import { artifactRevisionUploadPath, createArtifactRevision, discardArtifactRevision, acquireArtifactRevisionLease, releaseArtifactRevisionLease, } from "../infrastructure/console/artifact-revision-store.js";
8
+ import { OperationStore } from "../infrastructure/console/operation-store.js";
9
+ import { buildArtifactBindingGraph } from "../workflows/dag/artifact-bindings.js";
10
+ import { resolveArtifactRevisionSchema } from "../workflows/dag/artifact-revision-schema-registry.js";
11
+ import { locateDagRun, readDagRunSpec } from "../workflows/dag/lifecycle.js";
12
+ function parseArtifactArgs(args) {
13
+ const [first, second] = args;
14
+ const action = first === "consumers"
15
+ ? "consumers"
16
+ : first === "revision" && second === "stage"
17
+ ? "stage"
18
+ : first === "revision" && second === "discard"
19
+ ? "discard"
20
+ : undefined;
21
+ if (!action) {
22
+ throw new Error("usage: dag artifact consumers --run-id <run> --node-id <producer> --artifact <path> [--json] | dag artifact revision <stage|discard> ...");
23
+ }
24
+ let runId;
25
+ let nodeId;
26
+ let artifactPath;
27
+ let artifactId;
28
+ let file;
29
+ let candidateUpload;
30
+ let revisionId;
31
+ let requestId;
32
+ let json = false;
33
+ for (let index = action === "consumers" ? 1 : 2; index < args.length; index += 1) {
34
+ const arg = args[index];
35
+ const next = () => args[++index];
36
+ if (arg === "--run-id")
37
+ runId = next();
38
+ else if (arg.startsWith("--run-id="))
39
+ runId = arg.slice(9);
40
+ else if (arg === "--node-id")
41
+ nodeId = next();
42
+ else if (arg.startsWith("--node-id="))
43
+ nodeId = arg.slice(10);
44
+ else if (arg === "--artifact")
45
+ artifactPath = next();
46
+ else if (arg.startsWith("--artifact="))
47
+ artifactPath = arg.slice(11);
48
+ else if (arg === "--artifact-id")
49
+ artifactId = next();
50
+ else if (arg.startsWith("--artifact-id="))
51
+ artifactId = arg.slice(14);
52
+ else if (arg === "--file")
53
+ file = next();
54
+ else if (arg.startsWith("--file="))
55
+ file = arg.slice(7);
56
+ else if (arg === "--candidate-upload")
57
+ candidateUpload = next();
58
+ else if (arg.startsWith("--candidate-upload="))
59
+ candidateUpload = arg.slice(19);
60
+ else if (arg === "--revision-id")
61
+ revisionId = next();
62
+ else if (arg.startsWith("--revision-id="))
63
+ revisionId = arg.slice(14);
64
+ else if (arg === "--request-id")
65
+ requestId = next();
66
+ else if (arg.startsWith("--request-id="))
67
+ requestId = arg.slice(13);
68
+ else if (arg === "--json")
69
+ json = true;
70
+ else
71
+ throw new Error(`unknown dag artifact flag: ${arg}`);
72
+ }
73
+ return { action, runId, nodeId, artifactPath, artifactId, file, candidateUpload, revisionId, requestId, json };
74
+ }
75
+ function sha256(bytes) {
76
+ return createHash("sha256").update(bytes).digest("hex");
77
+ }
78
+ function print(value, json, fallback) {
79
+ process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : `${fallback}\n`);
80
+ }
81
+ export async function runDagArtifactCore(repoRoot, args, cliOptions) {
82
+ const input = parseArtifactArgs(args);
83
+ if (input.action === "discard") {
84
+ if (!input.revisionId || !input.requestId) {
85
+ throw new Error("dag artifact revision discard requires --revision-id and --request-id");
86
+ }
87
+ const appData = openConsoleAppData({ repoRoot, ...(cliOptions?.appDataRoot ? { appDataRoot: cliOptions.appDataRoot } : {}) });
88
+ const operations = new OperationStore(appData);
89
+ const created = await operations.create({
90
+ clientRequestId: input.requestId,
91
+ action: "cliArtifactRevisionDiscard",
92
+ actionParams: { revisionId: input.revisionId },
93
+ });
94
+ if (created.kind === "conflict")
95
+ throw new Error(created.code);
96
+ if (created.kind === "idempotent" && created.operation.state === "succeeded") {
97
+ return asRecord(created.operation.result);
98
+ }
99
+ const leaseHolder = `cli-discard:${input.requestId}`;
100
+ const revision = await acquireArtifactRevisionLease({
101
+ appData,
102
+ revisionId: input.revisionId,
103
+ holder: leaseHolder,
104
+ })
105
+ .then(() => discardArtifactRevision({
106
+ appData,
107
+ revisionId: input.revisionId,
108
+ holder: leaseHolder,
109
+ }))
110
+ .finally(() => releaseArtifactRevisionLease({
111
+ appData,
112
+ revisionId: input.revisionId,
113
+ holder: leaseHolder,
114
+ }).catch(() => { }));
115
+ const result = { ok: true, revisionId: revision.revisionId, state: revision.state };
116
+ await operations.update(created.operation.operationId, {
117
+ state: "succeeded",
118
+ startedAt: created.operation.createdAt,
119
+ finishedAt: new Date().toISOString(),
120
+ result,
121
+ });
122
+ return result;
123
+ }
124
+ if (!input.runId || !input.nodeId || (!input.artifactPath && !input.artifactId)) {
125
+ throw new Error("dag artifact requires --run-id, --node-id, and --artifact <relative-path> or --artifact-id <id>");
126
+ }
127
+ const located = await locateDagRun(repoRoot, input.runId);
128
+ if (!located)
129
+ throw new Error(`dag run not found: ${input.runId}`);
130
+ const graph = buildArtifactBindingGraph(await readDagRunSpec(located.runDir));
131
+ const declaration = graph.artifacts.find((item) => item.producerNodeId === input.nodeId &&
132
+ (input.artifactId
133
+ ? item.artifactId === input.artifactId
134
+ : item.path === input.artifactPath));
135
+ if (!declaration)
136
+ throw new Error("ARTIFACT_BINDING_UNAVAILABLE");
137
+ if (input.action === "consumers") {
138
+ return {
139
+ schemaVersion: 1,
140
+ runId: input.runId,
141
+ nodeId: input.nodeId,
142
+ artifactId: declaration.artifactId,
143
+ artifactPath: declaration.path,
144
+ directConsumerNodeIds: declaration.directConsumerNodeIds,
145
+ graphSha256: graph.artifactGraphSha256,
146
+ };
147
+ }
148
+ if (!input.requestId || (!input.file && !input.candidateUpload)) {
149
+ throw new Error("dag artifact revision stage requires --file or --candidate-upload, and --request-id");
150
+ }
151
+ const appData = openConsoleAppData({
152
+ repoRoot,
153
+ ...(cliOptions?.appDataRoot ? { appDataRoot: cliOptions.appDataRoot } : {}),
154
+ });
155
+ // The candidate upload lives in app-data and is cross-process mutable;
156
+ // resolve + read it TOCTOU-safe (O_NOFOLLOW fd read) instead of
157
+ // lstat-then-readFile, so a swapped symlink can never feed an unintended
158
+ // file into the stage. Both the path resolution (assertSafeAppDataPath
159
+ // rejects symlinks/hardlinks) and the fd read map to the stable
160
+ // ARTIFACT_REVISION_PATH_INVALID code.
161
+ let candidateBytes;
162
+ if (input.candidateUpload) {
163
+ try {
164
+ if (!/^[a-f0-9]{64}$/.test(input.candidateUpload)) {
165
+ throw new Error("invalid --candidate-upload hash");
166
+ }
167
+ const candidatePath = artifactRevisionUploadPath(appData, input.candidateUpload);
168
+ candidateBytes = await readFileBoundarySafe({
169
+ boundaryRoot: path.dirname(candidatePath),
170
+ targetPath: candidatePath,
171
+ label: "candidate upload",
172
+ });
173
+ }
174
+ catch (error) {
175
+ const message = error instanceof Error ? error.message : String(error);
176
+ if (/symlink|hardlink|single-link|regular|boundary|escap|app-data/i.test(message)) {
177
+ throw new Error("ARTIFACT_REVISION_PATH_INVALID");
178
+ }
179
+ throw error;
180
+ }
181
+ }
182
+ else {
183
+ const candidatePath = path.resolve(repoRoot, input.file);
184
+ const stat = await lstat(candidatePath);
185
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
186
+ throw new Error("candidate must be an owned regular file");
187
+ }
188
+ candidateBytes = await readFile(candidatePath);
189
+ }
190
+ if (candidateBytes.byteLength > 10 * 1024 * 1024)
191
+ throw new Error("candidate exceeds 10 MiB");
192
+ const operations = new OperationStore(appData);
193
+ const actionParams = {
194
+ runId: input.runId,
195
+ nodeId: input.nodeId,
196
+ artifactPath: declaration.path,
197
+ candidateSha256: sha256(candidateBytes),
198
+ candidateBytes: candidateBytes.byteLength,
199
+ };
200
+ const created = await operations.create({
201
+ clientRequestId: input.requestId,
202
+ action: "cliArtifactRevisionStage",
203
+ actionParams,
204
+ });
205
+ if (created.kind === "conflict")
206
+ throw new Error(created.code);
207
+ if (created.kind === "idempotent" && created.operation.state === "succeeded") {
208
+ return asRecord(created.operation.result);
209
+ }
210
+ const parent = await readRevisionParentArtifact({
211
+ cwd: repoRoot,
212
+ runId: input.runId,
213
+ producerNodeId: input.nodeId,
214
+ artifactId: declaration.artifactId,
215
+ });
216
+ const revision = await createArtifactRevision({
217
+ appData,
218
+ parentRunId: input.runId,
219
+ producerNodeId: input.nodeId,
220
+ declaration: parent.declaration,
221
+ parentSha256: parent.parentSha256,
222
+ parentBytes: parent.parentBytes,
223
+ candidateBytes,
224
+ mediaType: declaration.mediaType,
225
+ artifactSchemaResolver: resolveArtifactRevisionSchema,
226
+ });
227
+ const projected = {
228
+ revisionId: revision.revisionId,
229
+ state: revision.state,
230
+ parent: revision.parent,
231
+ candidate: {
232
+ sha256: revision.candidate.sha256,
233
+ bytes: revision.candidate.bytes,
234
+ mediaType: revision.candidate.mediaType,
235
+ uploadedAt: revision.candidate.uploadedAt,
236
+ },
237
+ validation: revision.validation,
238
+ diff: {
239
+ kind: revision.diff.kind,
240
+ summary: revision.diff.summary,
241
+ truncated: revision.diff.truncated,
242
+ },
243
+ expiresAt: revision.expiresAt,
244
+ };
245
+ const result = { ok: true, revision: projected };
246
+ await operations.update(created.operation.operationId, {
247
+ state: "succeeded",
248
+ startedAt: created.operation.createdAt,
249
+ finishedAt: new Date().toISOString(),
250
+ result,
251
+ });
252
+ return result;
253
+ }
254
+ function asRecord(value) {
255
+ return value && typeof value === "object" ? value : { ok: true };
256
+ }
257
+ export async function runDagArtifact(repoRoot, args) {
258
+ const input = parseArtifactArgs(args);
259
+ try {
260
+ const result = await runDagArtifactCore(repoRoot, args);
261
+ if (input.action === "discard") {
262
+ print(result, input.json, `Discarded ${String(result.revisionId ?? "")}`);
263
+ }
264
+ else if (input.action === "consumers") {
265
+ const consumers = result;
266
+ print(result, input.json, `${consumers.artifactPath}: ${(consumers.directConsumerNodeIds ?? []).join(", ") || "no consumers"}`);
267
+ }
268
+ else {
269
+ print(result, input.json, `Staged revision ${String(result.revisionId ?? "")}`);
270
+ }
271
+ }
272
+ catch (error) {
273
+ if (!input.json)
274
+ throw error;
275
+ const message = error instanceof Error ? error.message : String(error);
276
+ const candidateCode = message.split(":", 1)[0]?.trim() ?? "";
277
+ const code = /^[A-Z][A-Z0-9_]+$/.test(candidateCode)
278
+ ? candidateCode
279
+ : "INTERNAL_ERROR";
280
+ process.stdout.write(`${JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message } }, null, 2)}\n`);
281
+ process.stderr.write(`${message}\n`);
282
+ process.exitCode = 1;
283
+ }
284
+ }
@@ -0,0 +1,184 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { link, lstat, readFile, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { buildContextBundle } from "../workflows/dag/context-transfer/context-bundle.js";
5
+ import { effectiveNodeContextV1Schema } from "../shared/context-transfer.js";
6
+ import { locateDagRun } from "../workflows/dag/lifecycle.js";
7
+ function parseContextArgs(args) {
8
+ const action = args[0];
9
+ if (action !== "show" && action !== "export") {
10
+ throw new Error("usage: dag context <show|export> --run-id <id> --node-id <id> --attempt <n> [--json]");
11
+ }
12
+ let runId = "";
13
+ let nodeId = "";
14
+ let attempt = 0;
15
+ let json = false;
16
+ let target = "generic";
17
+ let profile = "redacted";
18
+ let output;
19
+ let acknowledgeSensitiveData = false;
20
+ for (let index = 1; index < args.length; index += 1) {
21
+ const arg = args[index];
22
+ if (arg === "--run-id")
23
+ runId = args[++index] ?? "";
24
+ else if (arg.startsWith("--run-id="))
25
+ runId = arg.slice(9);
26
+ else if (arg === "--node-id")
27
+ nodeId = args[++index] ?? "";
28
+ else if (arg.startsWith("--node-id="))
29
+ nodeId = arg.slice(10);
30
+ else if (arg === "--attempt")
31
+ attempt = Number(args[++index]);
32
+ else if (arg.startsWith("--attempt="))
33
+ attempt = Number(arg.slice(10));
34
+ else if (arg === "--target")
35
+ target = (args[++index] ?? "");
36
+ else if (arg.startsWith("--target="))
37
+ target = arg.slice(9);
38
+ else if (arg === "--redaction")
39
+ profile = (args[++index] ?? "");
40
+ else if (arg.startsWith("--redaction="))
41
+ profile = arg.slice(12);
42
+ else if (arg === "--output")
43
+ output = args[++index];
44
+ else if (arg.startsWith("--output="))
45
+ output = arg.slice(9);
46
+ else if (arg === "--acknowledge-sensitive-data")
47
+ acknowledgeSensitiveData = true;
48
+ else if (arg === "--json")
49
+ json = true;
50
+ else
51
+ throw new Error(`unknown dag context flag: ${arg}`);
52
+ }
53
+ if (!runId || !nodeId || !Number.isSafeInteger(attempt) || attempt < 1) {
54
+ throw new Error("dag context requires --run-id, --node-id, and positive --attempt");
55
+ }
56
+ if (!["generic", "pi-session", "pi-handoff", "readable"].includes(target)) {
57
+ throw new Error(`unsupported context target: ${target}`);
58
+ }
59
+ if (profile !== "redacted" && profile !== "raw") {
60
+ throw new Error(`unsupported redaction profile: ${profile}`);
61
+ }
62
+ if (action === "export" && !output) {
63
+ throw new Error("dag context export requires --output <path>");
64
+ }
65
+ if (action === "export" && profile === "raw" && !acknowledgeSensitiveData) {
66
+ throw new Error("raw context export requires --acknowledge-sensitive-data");
67
+ }
68
+ return {
69
+ action,
70
+ runId,
71
+ nodeId,
72
+ attempt,
73
+ json,
74
+ target,
75
+ profile,
76
+ ...(output ? { output } : {}),
77
+ acknowledgeSensitiveData,
78
+ };
79
+ }
80
+ function receiptPath(runDir, nodeId, attempt) {
81
+ return path.join(runDir, nodeId, "context-receipts", `attempt-${attempt}.json`);
82
+ }
83
+ function outputLabel(repoRoot, outputPath) {
84
+ const relative = path
85
+ .relative(repoRoot, outputPath)
86
+ .split(path.sep)
87
+ .join("/");
88
+ return relative && !relative.startsWith("../") && relative !== ".."
89
+ ? relative
90
+ : path.basename(outputPath);
91
+ }
92
+ export async function runDagContextCore(repoRoot, args) {
93
+ const input = parseContextArgs(args);
94
+ const located = await locateDagRun(repoRoot, input.runId);
95
+ if (!located)
96
+ throw new Error(`dag run not found: ${input.runId}`);
97
+ if (input.action === "show") {
98
+ let receipt;
99
+ try {
100
+ receipt = effectiveNodeContextV1Schema.parse(JSON.parse(await readFile(receiptPath(located.runDir, input.nodeId, input.attempt), "utf8")));
101
+ }
102
+ catch {
103
+ throw new Error("CONTEXT_RECEIPT_MISSING");
104
+ }
105
+ return {
106
+ schemaVersion: 1,
107
+ runId: input.runId,
108
+ nodeId: input.nodeId,
109
+ attempt: input.attempt,
110
+ capturedAt: receipt.identity.capturedAt,
111
+ contextDigest: receipt.contextDigest,
112
+ componentCount: receipt.orderedComponents.length,
113
+ truncationCount: receipt.promptAssembly.truncations.length,
114
+ runtime: {
115
+ executor: receipt.runtime.executor,
116
+ model: receipt.runtime.resolvedModel ?? receipt.runtime.modelHint ?? null,
117
+ skills: receipt.runtime.skills,
118
+ extensions: receipt.runtime.extensions,
119
+ tools: receipt.runtime.activeTools,
120
+ },
121
+ };
122
+ }
123
+ const outputPath = path.resolve(repoRoot, input.output);
124
+ const parent = path.dirname(outputPath);
125
+ const parentStat = await lstat(parent).catch(() => undefined);
126
+ if (parentStat?.isSymbolicLink())
127
+ throw new Error("unsafe output parent symlink");
128
+ const existing = await lstat(outputPath).catch(() => undefined);
129
+ if (existing)
130
+ throw new Error("context export output already exists");
131
+ const bundle = await buildContextBundle({
132
+ runDir: located.runDir,
133
+ runId: input.runId,
134
+ nodeId: input.nodeId,
135
+ attempt: input.attempt,
136
+ profile: input.profile,
137
+ target: input.target,
138
+ ...(input.profile === "raw" && input.acknowledgeSensitiveData
139
+ ? {
140
+ rawAcknowledgement: {
141
+ requestId: `cli-${bundleRequestIdentity(input)}`,
142
+ acknowledgedAt: new Date().toISOString(),
143
+ },
144
+ }
145
+ : {}),
146
+ });
147
+ const temporaryPath = `${outputPath}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
148
+ try {
149
+ await writeFile(temporaryPath, bundle.zipBytes, { flag: "wx", mode: 0o600 });
150
+ await link(temporaryPath, outputPath);
151
+ await rm(temporaryPath, { force: true });
152
+ }
153
+ catch (error) {
154
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
155
+ throw error;
156
+ }
157
+ return {
158
+ schemaVersion: 1,
159
+ bundleId: bundle.manifest.bundleId,
160
+ bundleSha256: bundle.manifest.bundleSha256,
161
+ profile: bundle.manifest.profile,
162
+ target: bundle.manifest.target,
163
+ bytes: bundle.zipBytes.byteLength,
164
+ output: outputLabel(repoRoot, outputPath),
165
+ };
166
+ }
167
+ export async function runDagContext(repoRoot, args) {
168
+ const input = parseContextArgs(args);
169
+ const result = await runDagContextCore(repoRoot, args);
170
+ if (input.action === "show") {
171
+ const summary = result;
172
+ process.stdout.write(input.json
173
+ ? `${JSON.stringify(result, null, 2)}\n`
174
+ : `Context ${summary.runId}/${summary.nodeId} attempt ${summary.attempt}\nDigest: ${summary.contextDigest}\nComponents: ${summary.componentCount}\n`);
175
+ return;
176
+ }
177
+ const exported = result;
178
+ process.stdout.write(input.json
179
+ ? `${JSON.stringify(result, null, 2)}\n`
180
+ : `Exported ${exported.bundleId} to ${exported.output} (${exported.bytes} bytes)\n`);
181
+ }
182
+ function bundleRequestIdentity(input) {
183
+ return `${input.runId}-${input.nodeId}-${input.attempt}`.replace(/[^a-zA-Z0-9._-]/g, "-");
184
+ }
@@ -1,6 +1,13 @@
1
+ import { randomBytes } from "node:crypto";
1
2
  import { readFile } from "node:fs/promises";
3
+ import { executeArtifactRevisionContinuation, planArtifactRevisionContinuation, } from "../workflows/dag/context-transfer/operator-actions.js";
2
4
  import { isDagRerunPromptOverrideEnabled } from "../shared/dag-prompt-override.js";
3
- import { executeDagRerun, planDagRerun, } from "../workflows/dag/rerun-run.js";
5
+ import { workerContinuationAuthorityV1Schema, } from "../shared/context-transfer.js";
6
+ import { openConsoleAppData } from "../infrastructure/console/app-data.js";
7
+ import { buildArtifactOverrideSet, consumeArtifactRevision, readArtifactRevision, readArtifactRevisionCandidate, acquireArtifactRevisionLease, releaseArtifactRevisionLease, EXECUTE_REVISION_LEASE_TTL_MS, } from "../infrastructure/console/artifact-revision-store.js";
8
+ import { buildArtifactBindingGraph } from "../workflows/dag/artifact-bindings.js";
9
+ import { locateDagRun, readDagRunSpec } from "../workflows/dag/lifecycle.js";
10
+ import { executeDagRerun, planDagRerun, readOperatorRequestReplay, } from "../workflows/dag/rerun-run.js";
4
11
  export function parseDagRerunArgs(args) {
5
12
  if (args.length === 0) {
6
13
  throw new Error("usage: dag rerun --run-id <id> --from-node <node-id> --plan [--json] | dag rerun --run-id <id> --from-node <node-id> --plan-hash <hash> --request-id <key> --reason <text> [--json]");
@@ -111,6 +118,10 @@ async function loadPromptOverrides(specs, mode) {
111
118
  return overrides;
112
119
  }
113
120
  export async function runDagRerun(repoRoot, rawArgs) {
121
+ if (rawArgs.some((arg) => arg === "--artifact-revision" || arg.startsWith("--artifact-revision="))) {
122
+ await runArtifactRevisionRerun(repoRoot, rawArgs);
123
+ return;
124
+ }
114
125
  const parsed = parseDagRerunArgs(rawArgs);
115
126
  let promptOverrides;
116
127
  const hasOverrides = parsed.promptOverrideSpecs.length > 0 ||
@@ -163,3 +174,197 @@ export async function runDagRerun(repoRoot, rawArgs) {
163
174
  }
164
175
  }
165
176
  }
177
+ function readFlag(args, name) {
178
+ const exact = args.indexOf(name);
179
+ if (exact >= 0)
180
+ return args[exact + 1];
181
+ const prefix = `${name}=`;
182
+ return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
183
+ }
184
+ function parseWorkerContinuationAuthority(raw) {
185
+ if (raw === undefined)
186
+ return undefined;
187
+ let parsed;
188
+ try {
189
+ parsed = JSON.parse(raw);
190
+ }
191
+ catch {
192
+ throw new Error("dag rerun --worker-continuation must be valid JSON");
193
+ }
194
+ const result = workerContinuationAuthorityV1Schema.safeParse(parsed);
195
+ if (!result.success) {
196
+ throw new Error(`dag rerun --worker-continuation is invalid: ${result.error.issues[0]?.message ?? "schema mismatch"}`);
197
+ }
198
+ return result.data;
199
+ }
200
+ function executeLeaseHolder(requestId) {
201
+ // The request id identifies the idempotency envelope, not a live process.
202
+ // A unique invocation holder prevents two concurrent direct CLI invocations
203
+ // with the same request id from treating each other as a lease renewal.
204
+ return `cli-execute:${requestId}:${randomBytes(12).toString("hex")}`;
205
+ }
206
+ /**
207
+ * Hold the staging lease across candidate read + continuation execution so a
208
+ * concurrent discard/second execute fails with LEASE_HELD and a crashed
209
+ * process leaves a stale, TTL-takeover-able lease instead of silent races.
210
+ * The acquire/renew here also refreshes the sliding window before the long
211
+ * child run starts.
212
+ */
213
+ async function withExecuteRevisionLease(appData, requestId, revisionId, operation) {
214
+ const holder = executeLeaseHolder(requestId);
215
+ await acquireArtifactRevisionLease({
216
+ appData,
217
+ revisionId,
218
+ holder,
219
+ ttlMs: EXECUTE_REVISION_LEASE_TTL_MS,
220
+ });
221
+ try {
222
+ return await operation(holder);
223
+ }
224
+ finally {
225
+ await releaseArtifactRevisionLease({
226
+ appData,
227
+ revisionId,
228
+ holder,
229
+ }).catch(() => { });
230
+ }
231
+ }
232
+ export async function runArtifactRevisionRerunCore(repoRoot, rawArgs, cliOptions) {
233
+ const allowed = new Set([
234
+ "--run-id",
235
+ "--artifact-revision",
236
+ "--plan-hash",
237
+ "--request-id",
238
+ "--reason",
239
+ "--worker-continuation",
240
+ ]);
241
+ for (let index = 0; index < rawArgs.length; index += 1) {
242
+ const arg = rawArgs[index];
243
+ if (arg === "--plan" || arg === "--json")
244
+ continue;
245
+ const name = arg.split("=", 1)[0];
246
+ if (!allowed.has(name))
247
+ throw new Error(`unknown artifact revision rerun flag: ${arg}`);
248
+ if (!arg.includes("="))
249
+ index += 1;
250
+ }
251
+ const parentRunId = readFlag(rawArgs, "--run-id")?.trim();
252
+ const revisionId = readFlag(rawArgs, "--artifact-revision")?.trim();
253
+ const planOnly = rawArgs.includes("--plan");
254
+ if (!parentRunId || !revisionId) {
255
+ throw new Error("dag rerun artifact revision requires --run-id and --artifact-revision");
256
+ }
257
+ const appData = openConsoleAppData({
258
+ repoRoot,
259
+ ...(cliOptions?.appDataRoot ? { appDataRoot: cliOptions.appDataRoot } : {}),
260
+ });
261
+ const planHash = readFlag(rawArgs, "--plan-hash")?.trim();
262
+ const requestId = readFlag(rawArgs, "--request-id")?.trim();
263
+ const reason = readFlag(rawArgs, "--reason")?.trim();
264
+ const workerContinuation = parseWorkerContinuationAuthority(readFlag(rawArgs, "--worker-continuation"));
265
+ if (!planOnly && (!planHash || !requestId || !reason)) {
266
+ throw new Error("dag rerun artifact revision execute requires --plan-hash, --request-id, and --reason");
267
+ }
268
+ if (!planOnly) {
269
+ // Replay-first (B4): a prior execute may have consumed the revision and
270
+ // the staging record may already have been reconciled away. The persisted
271
+ // operator request is the authoritative idempotent result and must be
272
+ // honored before any dependency on the revision metadata. A replay that
273
+ // does not match the requested parent/planHash is a reuse conflict, not a
274
+ // valid idempotent answer (SF2).
275
+ const replay = await readOperatorRequestReplay(repoRoot, requestId, {
276
+ parentRunId,
277
+ planHash,
278
+ });
279
+ if (replay) {
280
+ return {
281
+ payload: replay,
282
+ exitCode: replay.ok
283
+ ? 0
284
+ : replay.errorCode === "RERUN_PLAN_DRIFT"
285
+ ? 3
286
+ : replay.errorCode === "INELIGIBLE"
287
+ ? 2
288
+ : 1,
289
+ };
290
+ }
291
+ }
292
+ const revision = await readArtifactRevision(appData, revisionId);
293
+ if (!revision || revision.parent.runId !== parentRunId) {
294
+ throw new Error("ARTIFACT_REVISION_EXPIRED");
295
+ }
296
+ const located = await locateDagRun(repoRoot, parentRunId);
297
+ if (!located)
298
+ throw new Error(`dag run not found: ${parentRunId}`);
299
+ const declaration = buildArtifactBindingGraph(await readDagRunSpec(located.runDir)).artifacts.find((item) => item.producerNodeId === revision.parent.nodeId &&
300
+ item.path === revision.parent.artifactPath);
301
+ if (!declaration)
302
+ throw new Error("ARTIFACT_BINDING_UNAVAILABLE");
303
+ const artifactOverrides = buildArtifactOverrideSet(revision);
304
+ if (planOnly) {
305
+ if (planHash || requestId || reason) {
306
+ throw new Error("dag rerun --plan cannot be combined with execute flags");
307
+ }
308
+ const plan = await planArtifactRevisionContinuation({
309
+ cwd: repoRoot,
310
+ parentRunId,
311
+ artifactId: declaration.artifactId,
312
+ artifactOverrides,
313
+ stagingRevisionSha256: revision.candidate.sha256,
314
+ ...(workerContinuation
315
+ ? { workerContinuationAuthority: workerContinuation }
316
+ : {}),
317
+ });
318
+ return { payload: { ok: plan.eligible, plan }, exitCode: plan.eligible ? 0 : 2 };
319
+ }
320
+ if (revision.state === "consumed") {
321
+ // Replay was not found above, so this is a crashed consume without a
322
+ // persisted result — refuse rather than re-execute a consumed revision.
323
+ throw new Error("ARTIFACT_REVISION_EXPIRED: revision already consumed and no operator replay exists for this request");
324
+ }
325
+ const result = await withExecuteRevisionLease(appData, requestId, revision.revisionId, async (leaseHolder) => {
326
+ const candidateBytes = await readArtifactRevisionCandidate({ appData, revision });
327
+ return executeArtifactRevisionContinuation({
328
+ cwd: repoRoot,
329
+ parentRunId,
330
+ artifactId: declaration.artifactId,
331
+ artifactOverrides,
332
+ stagingRevisionSha256: revision.candidate.sha256,
333
+ candidateBytes,
334
+ planHash: planHash,
335
+ requestId: requestId,
336
+ reason: reason,
337
+ ...(workerContinuation
338
+ ? { workerContinuationAuthority: workerContinuation }
339
+ : {}),
340
+ consumeRevision: async ({ revisionId: consumedId, expectedSha256 }) => {
341
+ await consumeArtifactRevision({
342
+ appData,
343
+ revisionId: consumedId,
344
+ expectedSha256,
345
+ holder: leaseHolder,
346
+ });
347
+ },
348
+ });
349
+ });
350
+ return {
351
+ payload: result,
352
+ exitCode: result.ok
353
+ ? 0
354
+ : result.errorCode === "RERUN_PLAN_DRIFT"
355
+ ? 3
356
+ : result.errorCode === "INELIGIBLE"
357
+ ? 2
358
+ : 1,
359
+ };
360
+ }
361
+ async function runArtifactRevisionRerun(repoRoot, rawArgs) {
362
+ const { payload, exitCode } = await runArtifactRevisionRerunCore(repoRoot, rawArgs);
363
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
364
+ if (exitCode !== 0) {
365
+ const failure = payload;
366
+ const message = failure.message ?? failure.errorCode ?? `artifact rerun failed with exit ${exitCode}`;
367
+ process.stderr.write(`${message}\n`);
368
+ process.exitCode = exitCode;
369
+ }
370
+ }