@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,572 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readFile, realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
5
+ import { effectiveNodeContextV1Schema, } from "../../shared/context-transfer.js";
6
+ import { buildArtifactBindingGraph, } from "../../workflows/dag/artifact-bindings.js";
7
+ import { dagSpecSchema } from "../../workflows/dag/types.js";
8
+ import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
9
+ import { resolveDagRunArtifact, resolveDagRunJson, } from "./dag-run-artifacts.js";
10
+ export const NODE_TRANSPARENCY_SCHEMA_VERSION = 1;
11
+ export const DAG_ARTIFACT_PREVIEW_MAX_BYTES = 64 * 1024;
12
+ function failureForResolution(reason) {
13
+ if (reason === "invalid-id" || reason === "unsafe-path") {
14
+ return {
15
+ status: 400,
16
+ body: { error: "Invalid or unsafe DAG run identifier", reason },
17
+ };
18
+ }
19
+ if (reason === "ambiguous") {
20
+ return {
21
+ status: 409,
22
+ body: { error: "Ambiguous DAG run lifecycle", reason },
23
+ };
24
+ }
25
+ return { status: 404, body: { error: "DAG run not found", reason } };
26
+ }
27
+ async function readJsonObject(filePath) {
28
+ try {
29
+ const parsed = JSON.parse(await readFile(filePath, "utf8"));
30
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
31
+ ? parsed
32
+ : null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ async function loadRunFacts(repoRoot, dagRunId) {
39
+ const resolved = resolveDagRunJson(repoRoot, dagRunId);
40
+ if (!resolved.ok) {
41
+ return { ok: false, failure: failureForResolution(resolved.reason) };
42
+ }
43
+ const raw = await readJsonObject(resolved.result.absolutePath);
44
+ if (!raw) {
45
+ return {
46
+ ok: false,
47
+ failure: { status: 422, body: { error: "run.json is unreadable" } },
48
+ };
49
+ }
50
+ const parsed = dagSpecSchema.safeParse(raw);
51
+ if (!parsed.success) {
52
+ return {
53
+ ok: false,
54
+ failure: {
55
+ status: 422,
56
+ body: { error: "run.json does not satisfy the frozen DAG schema" },
57
+ },
58
+ };
59
+ }
60
+ const state = await readJsonObject(`${resolved.result.runDir}/state.json`);
61
+ return {
62
+ ok: true,
63
+ facts: {
64
+ lifecycle: resolved.result.lifecycle,
65
+ runDir: resolved.result.runDir,
66
+ spec: parsed.data,
67
+ state,
68
+ },
69
+ };
70
+ }
71
+ function stateNode(state, nodeId) {
72
+ const nodes = state?.nodes;
73
+ if (!nodes || typeof nodes !== "object" || Array.isArray(nodes))
74
+ return null;
75
+ const node = nodes[nodeId];
76
+ return node && typeof node === "object" && !Array.isArray(node)
77
+ ? node
78
+ : null;
79
+ }
80
+ function normalizedAttempt(rawAttempt, node) {
81
+ if (Number.isSafeInteger(rawAttempt) && (rawAttempt ?? 0) > 0)
82
+ return rawAttempt;
83
+ const current = node?.currentAttempt;
84
+ if (typeof current === "number" &&
85
+ Number.isSafeInteger(current) &&
86
+ current > 0) {
87
+ return current;
88
+ }
89
+ const attempts = node?.attempts;
90
+ return Array.isArray(attempts) && attempts.length > 0 ? attempts.length : 1;
91
+ }
92
+ function nodeStatus(node) {
93
+ return typeof node?.status === "string" ? node.status : null;
94
+ }
95
+ function wasDispatched(node) {
96
+ const status = nodeStatus(node)?.toUpperCase();
97
+ return Boolean((status && !["PENDING", "READY", "BLOCKED"].includes(status)) ||
98
+ typeof node?.startedAt === "string" ||
99
+ typeof node?.currentAttempt === "number");
100
+ }
101
+ export async function loadDagNodeContextSummary(repoRoot, dagRunId, nodeId, attempt) {
102
+ if (!isSafeObservabilityIdentifier(nodeId)) {
103
+ return {
104
+ status: 400,
105
+ body: { error: "Invalid node identifier", reason: "invalid-id" },
106
+ };
107
+ }
108
+ const loaded = await loadRunFacts(repoRoot, dagRunId);
109
+ if (!loaded.ok)
110
+ return loaded.failure;
111
+ const task = loaded.facts.spec.tasks.find((candidate) => candidate.id === nodeId);
112
+ if (!task) {
113
+ return {
114
+ status: 404,
115
+ body: { error: "DAG node not found", reason: "not-found" },
116
+ };
117
+ }
118
+ const runtimeNode = stateNode(loaded.facts.state, nodeId);
119
+ const selectedAttempt = normalizedAttempt(attempt, runtimeNode);
120
+ const receiptSegments = [
121
+ nodeId,
122
+ "context-receipts",
123
+ `attempt-${selectedAttempt}.json`,
124
+ ];
125
+ const receiptArtifact = receiptSegments.join("/");
126
+ const receiptResolved = resolveDagRunArtifact(repoRoot, dagRunId, receiptSegments);
127
+ let effectiveContext;
128
+ if (!receiptResolved.ok) {
129
+ effectiveContext = {
130
+ status: "not-captured",
131
+ reasonCode: "CONTEXT_RECEIPT_MISSING",
132
+ receiptArtifact,
133
+ };
134
+ }
135
+ else {
136
+ const rawReceipt = await readJsonObject(receiptResolved.result.absolutePath);
137
+ const parsedReceipt = effectiveNodeContextV1Schema.safeParse(rawReceipt);
138
+ if (!parsedReceipt.success ||
139
+ parsedReceipt.data.identity.runId !== dagRunId ||
140
+ parsedReceipt.data.identity.nodeId !== nodeId ||
141
+ parsedReceipt.data.identity.attempt !== selectedAttempt) {
142
+ effectiveContext = {
143
+ status: "invalid",
144
+ reasonCode: "CONTEXT_COMPONENT_UNAVAILABLE",
145
+ receiptArtifact,
146
+ };
147
+ }
148
+ else {
149
+ const receipt = parsedReceipt.data;
150
+ const componentKinds = {};
151
+ const inclusionCounts = {};
152
+ for (const component of receipt.orderedComponents) {
153
+ componentKinds[component.kind] =
154
+ (componentKinds[component.kind] ?? 0) + 1;
155
+ inclusionCounts[component.inclusion] =
156
+ (inclusionCounts[component.inclusion] ?? 0) + 1;
157
+ }
158
+ effectiveContext = {
159
+ status: "captured",
160
+ contextDigest: receipt.contextDigest,
161
+ capturedAt: receipt.identity.capturedAt,
162
+ componentCount: receipt.orderedComponents.length,
163
+ componentKinds,
164
+ inclusionCounts,
165
+ truncationCount: receipt.promptAssembly.truncations.length,
166
+ runtime: {
167
+ executor: receipt.runtime.executor,
168
+ model: receipt.runtime.resolvedModel ?? receipt.runtime.modelHint ?? null,
169
+ skillCount: receipt.runtime.skills.length,
170
+ extensionCount: receipt.runtime.extensions.length,
171
+ toolCount: receipt.runtime.activeTools.length,
172
+ },
173
+ };
174
+ }
175
+ }
176
+ return {
177
+ status: 200,
178
+ body: {
179
+ schemaVersion: NODE_TRANSPARENCY_SCHEMA_VERSION,
180
+ dagRunId,
181
+ nodeId,
182
+ attempt: selectedAttempt,
183
+ declared: { status: "available", source: "frozen-run-spec" },
184
+ dispatched: {
185
+ status: wasDispatched(runtimeNode) ? "dispatched" : "not-dispatched",
186
+ nodeStatus: nodeStatus(runtimeNode),
187
+ },
188
+ effectiveContext,
189
+ },
190
+ };
191
+ }
192
+ async function loadNodeRecord(repoRoot, dagRunId, nodeId) {
193
+ const resolved = resolveDagRunArtifact(repoRoot, dagRunId, [
194
+ `${nodeId}.json`,
195
+ ]);
196
+ return resolved.ok ? readJsonObject(resolved.result.absolutePath) : null;
197
+ }
198
+ function declaredArtifactFacts(record) {
199
+ const facts = new Map();
200
+ for (const value of Array.isArray(record?.declaredArtifacts)
201
+ ? record.declaredArtifacts
202
+ : []) {
203
+ if (!value || typeof value !== "object" || Array.isArray(value))
204
+ continue;
205
+ const fact = value;
206
+ if (typeof fact.artifactId === "string")
207
+ facts.set(fact.artifactId, fact);
208
+ }
209
+ return facts;
210
+ }
211
+ function projectOrigin(record, runtimeNode) {
212
+ const raw = record?.origin ?? runtimeNode?.origin;
213
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
214
+ const origin = raw;
215
+ if (origin.kind === "imported") {
216
+ return {
217
+ kind: "imported",
218
+ ...(typeof origin.parentRunId === "string"
219
+ ? { parentRunId: origin.parentRunId }
220
+ : {}),
221
+ };
222
+ }
223
+ if (origin.kind === "executed")
224
+ return { kind: "executed" };
225
+ }
226
+ if (!record && !wasDispatched(runtimeNode))
227
+ return { kind: "pending" };
228
+ return { kind: "unknown" };
229
+ }
230
+ export async function loadDagNodeArtifactInventory(repoRoot, dagRunId, nodeId) {
231
+ if (!isSafeObservabilityIdentifier(nodeId)) {
232
+ return {
233
+ status: 400,
234
+ body: { error: "Invalid node identifier", reason: "invalid-id" },
235
+ };
236
+ }
237
+ const loaded = await loadRunFacts(repoRoot, dagRunId);
238
+ if (!loaded.ok)
239
+ return loaded.failure;
240
+ const task = loaded.facts.spec.tasks.find((candidate) => candidate.id === nodeId);
241
+ if (!task) {
242
+ return {
243
+ status: 404,
244
+ body: { error: "DAG node not found", reason: "not-found" },
245
+ };
246
+ }
247
+ const graph = buildArtifactBindingGraph(loaded.facts.spec);
248
+ const byId = new Map(graph.artifacts.map((artifact) => [artifact.artifactId, artifact]));
249
+ const record = await loadNodeRecord(repoRoot, dagRunId, nodeId);
250
+ const materialized = declaredArtifactFacts(record);
251
+ const produces = graph.artifacts
252
+ .filter((artifact) => artifact.producerNodeId === nodeId)
253
+ .map((artifact) => {
254
+ const fact = materialized.get(artifact.artifactId);
255
+ const status = fact?.status === "materialized" ||
256
+ fact?.status === "missing" ||
257
+ fact?.status === "mismatch"
258
+ ? fact.status
259
+ : "not-recorded";
260
+ const runtimeEligible = status === "materialized" && fact?.revisionEligible === true;
261
+ const factOrigin = fact?.origin && typeof fact.origin === "object" && !Array.isArray(fact.origin)
262
+ ? fact.origin
263
+ : undefined;
264
+ return {
265
+ artifactId: artifact.artifactId,
266
+ kind: artifact.kind,
267
+ path: artifact.path,
268
+ mediaType: artifact.mediaType,
269
+ ...(artifact.schemaId ? { schemaId: artifact.schemaId } : {}),
270
+ revisionPolicy: artifact.revisionPolicy,
271
+ directConsumerNodeIds: artifact.directConsumerNodeIds,
272
+ materialization: {
273
+ status,
274
+ ...(typeof fact?.sha256 === "string" ? { sha256: fact.sha256 } : {}),
275
+ ...(typeof fact?.bytes === "number" ? { bytes: fact.bytes } : {}),
276
+ ...(typeof fact?.reasonCode === "string"
277
+ ? { reasonCode: fact.reasonCode }
278
+ : {}),
279
+ },
280
+ revision: {
281
+ eligible: runtimeEligible,
282
+ ...(!runtimeEligible
283
+ ? {
284
+ reasonCode: artifact.revisionPolicy !== "replace-file"
285
+ ? "ARTIFACT_NOT_REVISION_ELIGIBLE"
286
+ : typeof fact?.reasonCode === "string"
287
+ ? fact.reasonCode
288
+ : "ARTIFACT_NOT_MATERIALIZED",
289
+ }
290
+ : {}),
291
+ },
292
+ ...(factOrigin && ["executed", "imported", "overridden"].includes(String(factOrigin.kind))
293
+ ? {
294
+ origin: {
295
+ kind: factOrigin.kind,
296
+ ...(typeof factOrigin.parentRunId === "string"
297
+ ? { parentRunId: factOrigin.parentRunId }
298
+ : {}),
299
+ ...(typeof factOrigin.revisionId === "string"
300
+ ? { revisionId: factOrigin.revisionId }
301
+ : {}),
302
+ ...(typeof factOrigin.supersedes === "string"
303
+ ? { supersedes: factOrigin.supersedes }
304
+ : {}),
305
+ },
306
+ }
307
+ : {}),
308
+ };
309
+ });
310
+ const consumes = (task.consumesArtifacts ?? []).flatMap((binding) => {
311
+ const artifact = byId.get(binding.artifactId);
312
+ return artifact
313
+ ? [
314
+ {
315
+ artifactId: binding.artifactId,
316
+ required: binding.required,
317
+ producerNodeId: artifact.producerNodeId,
318
+ path: artifact.path,
319
+ mediaType: artifact.mediaType,
320
+ ...(artifact.schemaId ? { schemaId: artifact.schemaId } : {}),
321
+ },
322
+ ]
323
+ : [];
324
+ });
325
+ return {
326
+ status: 200,
327
+ body: {
328
+ schemaVersion: NODE_TRANSPARENCY_SCHEMA_VERSION,
329
+ dagRunId,
330
+ nodeId,
331
+ binding: {
332
+ available: graph.available,
333
+ ...(graph.reasonCode ? { reasonCode: graph.reasonCode } : {}),
334
+ artifactGraphSha256: graph.artifactGraphSha256,
335
+ },
336
+ origin: projectOrigin(record, stateNode(loaded.facts.state, nodeId)),
337
+ produces,
338
+ consumes,
339
+ },
340
+ };
341
+ }
342
+ function isTextPreviewMediaType(mediaType) {
343
+ const normalized = mediaType.toLowerCase().split(";", 1)[0]?.trim() ?? "";
344
+ return normalized.startsWith("text/") || [
345
+ "application/json",
346
+ "application/ld+json",
347
+ "application/x-ndjson",
348
+ "application/xml",
349
+ "application/yaml",
350
+ "application/x-yaml",
351
+ "application/toml",
352
+ "application/javascript",
353
+ "application/sql",
354
+ ].includes(normalized) || normalized.endsWith("+json") || normalized.endsWith("+xml");
355
+ }
356
+ function isInsideDirectory(root, candidate) {
357
+ const normalizedRoot = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
358
+ return candidate.startsWith(normalizedRoot);
359
+ }
360
+ export async function loadDagNodeArtifactPreview(repoRoot, dagRunId, nodeId, artifactId) {
361
+ if (!isSafeObservabilityIdentifier(nodeId) || !isSafeObservabilityIdentifier(artifactId)) {
362
+ return {
363
+ status: 400,
364
+ body: { error: "Invalid node or artifact identifier", reason: "invalid-id" },
365
+ };
366
+ }
367
+ const loaded = await loadRunFacts(repoRoot, dagRunId);
368
+ if (!loaded.ok)
369
+ return loaded.failure;
370
+ const task = loaded.facts.spec.tasks.find((candidate) => candidate.id === nodeId);
371
+ if (!task) {
372
+ return {
373
+ status: 404,
374
+ body: { error: "DAG node not found", reason: "not-found" },
375
+ };
376
+ }
377
+ const graph = buildArtifactBindingGraph(loaded.facts.spec);
378
+ if (!graph.available) {
379
+ return {
380
+ status: 422,
381
+ body: {
382
+ error: "Artifact preview requires frozen artifact bindings",
383
+ reason: graph.reasonCode ?? "ARTIFACT_BINDING_UNAVAILABLE",
384
+ },
385
+ };
386
+ }
387
+ const artifact = graph.artifacts.find((candidate) => candidate.artifactId === artifactId);
388
+ const isVisibleToNode = artifact && (artifact.producerNodeId === nodeId ||
389
+ (task.consumesArtifacts ?? []).some((binding) => binding.artifactId === artifactId));
390
+ if (!artifact || !isVisibleToNode) {
391
+ return {
392
+ status: 404,
393
+ body: { error: "Artifact is not bound to this node", reason: "not-found" },
394
+ };
395
+ }
396
+ const producerRecord = await loadNodeRecord(repoRoot, dagRunId, artifact.producerNodeId);
397
+ const fact = declaredArtifactFacts(producerRecord).get(artifactId);
398
+ if (fact?.status !== "materialized" || typeof fact.sha256 !== "string") {
399
+ return {
400
+ status: 404,
401
+ body: {
402
+ error: "Artifact file has not been materialized",
403
+ reason: typeof fact?.reasonCode === "string"
404
+ ? fact.reasonCode
405
+ : "ARTIFACT_NOT_MATERIALIZED",
406
+ },
407
+ };
408
+ }
409
+ const resolved = resolveDagRunArtifact(repoRoot, dagRunId, artifact.path.split("/").filter(Boolean));
410
+ if (!resolved.ok)
411
+ return failureForResolution(resolved.reason);
412
+ try {
413
+ const [runRoot, artifactRealPath, fileStat] = await Promise.all([
414
+ realpath(resolved.result.runDir),
415
+ realpath(resolved.result.absolutePath),
416
+ lstat(resolved.result.absolutePath),
417
+ ]);
418
+ if (!isInsideDirectory(runRoot, artifactRealPath) ||
419
+ !fileStat.isFile() ||
420
+ fileStat.isSymbolicLink() ||
421
+ fileStat.nlink !== 1) {
422
+ return {
423
+ status: 422,
424
+ body: { error: "Artifact is not a safe regular file", reason: "unsafe-path" },
425
+ };
426
+ }
427
+ const bytes = await readFile(artifactRealPath);
428
+ const actualSha256 = createHash("sha256").update(bytes).digest("hex");
429
+ if (actualSha256 !== fact.sha256 ||
430
+ (typeof fact.bytes === "number" && fact.bytes !== bytes.byteLength)) {
431
+ return {
432
+ status: 422,
433
+ body: {
434
+ error: "Artifact bytes no longer match the recorded materialization",
435
+ reason: "ARTIFACT_HASH_MISMATCH",
436
+ },
437
+ };
438
+ }
439
+ if (!isTextPreviewMediaType(artifact.mediaType)) {
440
+ return {
441
+ status: 200,
442
+ body: {
443
+ schemaVersion: NODE_TRANSPARENCY_SCHEMA_VERSION,
444
+ dagRunId,
445
+ nodeId,
446
+ producerNodeId: artifact.producerNodeId,
447
+ artifactId,
448
+ path: artifact.path,
449
+ mediaType: artifact.mediaType,
450
+ kind: artifact.kind,
451
+ sha256: actualSha256,
452
+ sourceBytes: bytes.byteLength,
453
+ maxBytes: DAG_ARTIFACT_PREVIEW_MAX_BYTES,
454
+ previewKind: "unsupported",
455
+ content: null,
456
+ truncated: false,
457
+ redacted: false,
458
+ },
459
+ };
460
+ }
461
+ const raw = bytes.toString("utf8");
462
+ const redacted = redactSecrets(raw);
463
+ const content = truncateUtf8Preview(redacted, DAG_ARTIFACT_PREVIEW_MAX_BYTES);
464
+ return {
465
+ status: 200,
466
+ body: {
467
+ schemaVersion: NODE_TRANSPARENCY_SCHEMA_VERSION,
468
+ dagRunId,
469
+ nodeId,
470
+ producerNodeId: artifact.producerNodeId,
471
+ artifactId,
472
+ path: artifact.path,
473
+ mediaType: artifact.mediaType,
474
+ kind: artifact.kind,
475
+ sha256: actualSha256,
476
+ sourceBytes: bytes.byteLength,
477
+ maxBytes: DAG_ARTIFACT_PREVIEW_MAX_BYTES,
478
+ previewKind: "text",
479
+ content,
480
+ truncated: content !== redacted,
481
+ redacted: redacted !== raw,
482
+ },
483
+ };
484
+ }
485
+ catch {
486
+ return {
487
+ status: 404,
488
+ body: { error: "Artifact file is unavailable", reason: "not-found" },
489
+ };
490
+ }
491
+ }
492
+ function stringField(value, key) {
493
+ return typeof value?.[key] === "string" ? value[key] : undefined;
494
+ }
495
+ export async function loadDagRunLineage(repoRoot, dagRunId) {
496
+ const loaded = await loadRunFacts(repoRoot, dagRunId);
497
+ if (!loaded.ok)
498
+ return loaded.failure;
499
+ const continuationRaw = loaded.facts.state?.continuation;
500
+ const continuation = continuationRaw &&
501
+ typeof continuationRaw === "object" &&
502
+ !Array.isArray(continuationRaw)
503
+ ? continuationRaw
504
+ : null;
505
+ const importResolved = resolveDagRunArtifact(repoRoot, dagRunId, [
506
+ ".runtime",
507
+ "import-manifest.json",
508
+ ]);
509
+ const importManifest = importResolved.ok
510
+ ? await readJsonObject(importResolved.result.absolutePath)
511
+ : null;
512
+ const importedNodeIds = (Array.isArray(importManifest?.nodes) ? importManifest.nodes : [])
513
+ .flatMap((entry) => entry &&
514
+ typeof entry === "object" &&
515
+ !Array.isArray(entry) &&
516
+ typeof entry.nodeId === "string"
517
+ ? [entry.nodeId]
518
+ : [])
519
+ .sort();
520
+ const rerunResolved = resolveDagRunArtifact(repoRoot, dagRunId, [
521
+ ".runtime",
522
+ "rerun-plan.json",
523
+ ]);
524
+ const rerunPlan = rerunResolved.ok
525
+ ? await readJsonObject(rerunResolved.result.absolutePath)
526
+ : null;
527
+ const promptOverriddenNodeIds = (Array.isArray(rerunPlan?.promptOverrides) ? rerunPlan.promptOverrides : [])
528
+ .flatMap((entry) => entry &&
529
+ typeof entry === "object" &&
530
+ !Array.isArray(entry) &&
531
+ typeof entry.nodeId === "string"
532
+ ? [entry.nodeId]
533
+ : [])
534
+ .sort();
535
+ const artifactOverriddenNodeIds = (Array.isArray(importManifest?.overrides) ? importManifest.overrides : []).flatMap((entry) => entry &&
536
+ typeof entry === "object" &&
537
+ !Array.isArray(entry) &&
538
+ typeof entry.producerNodeId === "string"
539
+ ? [entry.producerNodeId]
540
+ : []);
541
+ const overriddenNodeIds = [
542
+ ...new Set([...promptOverriddenNodeIds, ...artifactOverriddenNodeIds]),
543
+ ].sort();
544
+ const parentRunId = stringField(continuation, "parentRunId");
545
+ return {
546
+ status: 200,
547
+ body: {
548
+ schemaVersion: NODE_TRANSPARENCY_SCHEMA_VERSION,
549
+ dagRunId,
550
+ kind: parentRunId ? "continuation" : "root",
551
+ ...(parentRunId ? { parentRunId } : {}),
552
+ ...(stringField(rerunPlan, "trigger")
553
+ ? { trigger: stringField(rerunPlan, "trigger") }
554
+ : {}),
555
+ ...(stringField(continuation, "operatorSelectedNodeId")
556
+ ? {
557
+ operatorSelectedNodeId: stringField(continuation, "operatorSelectedNodeId"),
558
+ }
559
+ : {}),
560
+ ...(stringField(continuation, "effectiveFromNodeId")
561
+ ? {
562
+ effectiveFromNodeId: stringField(continuation, "effectiveFromNodeId"),
563
+ }
564
+ : {}),
565
+ ...(stringField(rerunPlan, "planHash")
566
+ ? { planHash: stringField(rerunPlan, "planHash") }
567
+ : {}),
568
+ importedNodeIds,
569
+ overriddenNodeIds,
570
+ },
571
+ };
572
+ }
@@ -14,13 +14,14 @@ import { dagSourceBindingSchema } from "../../workflows/dag/types.js";
14
14
  import { getTaskPaths } from "../../task/runtime.js";
15
15
  import { classifyLedgerGate, computeLedgerInputDigest, LedgerParseError, parseLedgerJson, recoverLedgerInputContract, summarizeLedger, } from "../../task/source-prepare/ledger.js";
16
16
  import { getTaskPoolRoot } from "../pool/run-store.js";
17
- import { resolveDagRunArtifact, } from "./dag-run-artifacts.js";
17
+ import { resolveDagRunArtifact } from "./dag-run-artifacts.js";
18
18
  import { frontendPrewriteResultV1Schema } from "../../workflows/dag/frontend-prewrite-gate.js";
19
19
  import { isAllowedArtifactTextPath, resolveArtifactPath, resolveRepoFilePreview, toRepoRelativeArtifactPath, } from "./paths.js";
20
20
  import { extractSpecEvidence, extractSpecReadContent, } from "./spec-evidence.js";
21
21
  import { buildDagNodeInput } from "./node-input.js";
22
22
  import { loadDagNodeExecutionOutput } from "./dag-node-execution-output.js";
23
23
  import { loadDagNodeProcess } from "./node-process.js";
24
+ import { loadDagNodeArtifactInventory, loadDagNodeArtifactPreview, loadDagNodeContextSummary, loadDagRunLineage, } from "./node-transparency.js";
24
25
  import { buildObserveHealthV1 } from "./health.js";
25
26
  import { buildNightJobDetail, buildNightJobsDoctor, buildNightJobsList, buildNightJobsMorning, } from "./night-jobs.js";
26
27
  const ARTIFACT_PREVIEW_MAX_BYTES = 64 * 1024;
@@ -109,6 +110,21 @@ export const ROUTES = [
109
110
  pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/input$/,
110
111
  handler: handleDagNodeInput,
111
112
  },
113
+ {
114
+ method: "GET",
115
+ pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/context$/,
116
+ handler: handleDagNodeContext,
117
+ },
118
+ {
119
+ method: "GET",
120
+ pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/artifacts\/preview$/,
121
+ handler: handleDagNodeArtifactPreview,
122
+ },
123
+ {
124
+ method: "GET",
125
+ pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/artifacts$/,
126
+ handler: handleDagNodeArtifacts,
127
+ },
112
128
  {
113
129
  method: "GET",
114
130
  pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/execution-output$/,
@@ -139,6 +155,11 @@ export const ROUTES = [
139
155
  pattern: /^\/api\/dag-runs\/([^/]+)\/prewrite-result$/,
140
156
  handler: handleDagRunPrewriteResult,
141
157
  },
158
+ {
159
+ method: "GET",
160
+ pattern: /^\/api\/dag-runs\/([^/]+)\/lineage$/,
161
+ handler: handleDagRunLineage,
162
+ },
142
163
  {
143
164
  method: "GET",
144
165
  pattern: /^\/api\/dag-runs\/([^/]+)$/,
@@ -754,6 +775,35 @@ async function handleDagNodeInput(_req, res, match, ctx) {
754
775
  const result = await buildDagNodeInput(ctx.repoRoot, dagRunId, nodeId);
755
776
  sendJson(res, result.status, result.body);
756
777
  }
778
+ async function handleDagNodeContext(_req, res, match, ctx) {
779
+ const attemptRaw = match.query.get("attempt");
780
+ let attempt;
781
+ if (attemptRaw != null) {
782
+ attempt = Number(attemptRaw);
783
+ if (!Number.isSafeInteger(attempt) || attempt < 1) {
784
+ sendJson(res, 400, {
785
+ error: "attempt must be a positive integer",
786
+ reason: "invalid-attempt",
787
+ });
788
+ return;
789
+ }
790
+ }
791
+ const result = await loadDagNodeContextSummary(ctx.repoRoot, match.params.id, match.params.sub, attempt);
792
+ sendJson(res, result.status, result.body);
793
+ }
794
+ async function handleDagNodeArtifacts(_req, res, match, ctx) {
795
+ const result = await loadDagNodeArtifactInventory(ctx.repoRoot, match.params.id, match.params.sub);
796
+ sendJson(res, result.status, result.body);
797
+ }
798
+ async function handleDagNodeArtifactPreview(_req, res, match, ctx) {
799
+ const artifactId = match.query.get("artifactId") ?? "";
800
+ const result = await loadDagNodeArtifactPreview(ctx.repoRoot, match.params.id, match.params.sub, artifactId);
801
+ sendJson(res, result.status, result.body);
802
+ }
803
+ async function handleDagRunLineage(_req, res, match, ctx) {
804
+ const result = await loadDagRunLineage(ctx.repoRoot, match.params.id);
805
+ sendJson(res, result.status, result.body);
806
+ }
757
807
  /**
758
808
  * Read-only execution body by attempt-N / pass-N key only.
759
809
  * Clients must never submit artifact paths; server maps key → run-dir segments.