@tea-agent/loop-agent 0.39.3 → 0.40.0-beta.2

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 (223) hide show
  1. package/CHANGELOG.md +135 -92
  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 +108 -19
  11. package/dist/executors/pi-executor.js +4 -2
  12. package/dist/executors/pi-sdk-executor.js +49 -1
  13. package/dist/executors/pi-writer-tool-policy.js +57 -1
  14. package/dist/executors/shell-executor.js +42 -2
  15. package/dist/executors/workspace-write-snapshot.js +68 -0
  16. package/dist/{worker → infrastructure}/console/app-data.js +4 -0
  17. package/dist/infrastructure/console/artifact-revision-store.js +430 -0
  18. package/dist/infrastructure/console/context-export-store.js +160 -0
  19. package/dist/infrastructure/console/dir-lock.js +132 -0
  20. package/dist/infrastructure/harness/artifact-store.js +10 -1
  21. package/dist/infrastructure/harness/atomic-write.js +35 -2
  22. package/dist/shared/context-transfer/artifact-revision.js +172 -0
  23. package/dist/shared/context-transfer.js +418 -0
  24. package/dist/shared/operator/capabilities.js +14 -0
  25. package/dist/shared/operator/safe-run-summary.js +1 -0
  26. package/dist/shared/path-safety.js +93 -0
  27. package/dist/shared/preview.js +28 -4
  28. package/dist/task/config-types.js +12 -1
  29. package/dist/task/source-prepare/fragment-inventory.js +1 -14
  30. package/dist/task/task-demand-routing.js +3 -0
  31. package/dist/worker/console/chat/chat-event-store.js +77 -7
  32. package/dist/worker/console/chat/context-panel.js +7 -0
  33. package/dist/worker/console/chat/deferred-turn.js +12 -0
  34. package/dist/worker/console/chat/pi-runtime.js +331 -18
  35. package/dist/worker/console/chat/provider-error.js +98 -0
  36. package/dist/worker/console/chat/resource-preferences-store.js +1 -1
  37. package/dist/worker/console/chat/routes.js +389 -56
  38. package/dist/worker/console/chat/session-stats.js +179 -0
  39. package/dist/worker/console/chat/session-store.js +94 -64
  40. package/dist/worker/console/chat/todos.js +115 -0
  41. package/dist/worker/console/chat/tool-preview.js +42 -0
  42. package/dist/worker/console/chat/turn-process.js +20 -55
  43. package/dist/worker/console/chat/user-questions.js +266 -0
  44. package/dist/worker/console/chat/workspace-landing.js +79 -0
  45. package/dist/worker/console/console-update-runtime.js +1 -1
  46. package/dist/worker/console/context-transfer-diagnostics.js +198 -0
  47. package/dist/worker/console/dag-confirmation.js +1 -1
  48. package/dist/worker/console/dag-execution-receipt.js +1 -1
  49. package/dist/worker/console/doctor.js +1 -1
  50. package/dist/worker/console/draft-store.js +1 -1
  51. package/dist/worker/console/human-gate-token.js +1 -1
  52. package/dist/worker/console/index.js +3 -3
  53. package/dist/worker/console/interview/assessment.js +1 -1
  54. package/dist/worker/console/interview/session.js +1 -1
  55. package/dist/worker/console/native-directory-picker.js +13 -0
  56. package/dist/worker/console/operation-runner.js +45 -4
  57. package/dist/worker/console/operation-sse.js +1 -1
  58. package/dist/worker/console/operation-wait.js +1 -1
  59. package/dist/worker/console/operator-actions.js +104 -3
  60. package/dist/worker/console/operator-surface-health.js +1 -1
  61. package/dist/worker/console/operator-user-error.js +128 -0
  62. package/dist/worker/console/routes.js +927 -8
  63. package/dist/worker/console/security.js +39 -4
  64. package/dist/worker/console/server.js +532 -187
  65. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CpWvwARc.js} +1 -1
  66. package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-DctXgO-g.js} +1 -1
  67. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-D4mIkoQl.js} +1 -1
  68. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-ClG0--KO.js} +1 -1
  69. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-CwdLk78T.js} +1 -1
  70. package/dist/worker/console/static/assets/channel-C9VQZ-ru.js +1 -0
  71. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DKczcu-O.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-BV6F-vBI.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtojVnvA.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-BjDQRGnO.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-I0fV6ngU.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-BM8C-mmC.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-BEK-a15e.js} +1 -1
  78. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-CQsg_fSR.js} +1 -1
  79. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-B-BcaUm3.js} +1 -1
  80. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D1Bu08fg.js} +1 -1
  81. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BsCkw5MF.js +1 -0
  82. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BsCkw5MF.js +1 -0
  83. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-q8nwtxBR.js} +1 -1
  84. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-CruhpURw.js} +1 -1
  85. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-CJne6iYF.js} +1 -1
  86. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-CbeLvBU5.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-DmCxH1Pe.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-DJ5OYhtC.js} +1 -1
  89. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-B-UN-hln.js} +1 -1
  90. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-CcEro8Ey.js} +1 -1
  91. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BBBzDden.js} +1 -1
  92. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-xKEyH-oo.js} +1 -1
  93. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-C7xl63VC.js} +1 -1
  94. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-nthQQ9nR.js} +1 -1
  95. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-CWN5mE33.js} +1 -1
  96. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-Wn86tQ6N.js} +1 -1
  97. package/dist/worker/console/static/assets/index-D-0NVyMk.css +1 -0
  98. package/dist/worker/console/static/assets/index-gwyOxQiS.js +407 -0
  99. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-_WQymGtW.js} +1 -1
  100. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-COBeivBm.js} +1 -1
  101. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-CwGOJl3Q.js} +1 -1
  102. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-Ci0CJ5Vi.js} +1 -1
  103. package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CPjMq6vN.js} +1 -1
  104. package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-Z0wGoe0b.js} +5 -5
  105. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-dISked0d.js} +1 -1
  106. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-BJxgof7Y.js} +1 -1
  107. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-CIblU8eW.js} +1 -1
  108. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-vwYiqh8E.js} +1 -1
  109. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-_iOsVeFm.js} +1 -1
  110. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Dk0MLxO_.js} +1 -1
  111. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-Dx7HtLVW.js} +1 -1
  112. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-DRuWjlpL.js} +1 -1
  113. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Bhy9DQAA.js} +1 -1
  114. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-DAx9twnU.js} +1 -1
  115. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dmdf8cGy.js +1 -0
  116. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-okMlvVrM.js} +2 -2
  117. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-W1IcAFff.js +8 -0
  118. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DXqCKe37.js} +1 -1
  119. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CeIUkwBh.js} +1 -1
  120. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-BP76BKTN.js} +1 -1
  121. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-BpVI0OpF.js} +1 -1
  122. package/dist/worker/console/static/index.html +7 -2
  123. package/dist/worker/console/static-src/app/console-types.js +29 -4
  124. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  125. package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
  126. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  127. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  128. package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
  129. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  130. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
  131. package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
  132. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  133. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  134. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  135. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  136. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
  137. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  138. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
  139. package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
  140. package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
  141. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  142. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  143. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  144. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  145. package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
  146. package/dist/worker/console/workspace-context.js +260 -0
  147. package/dist/worker/console/workspace-registry.js +214 -0
  148. package/dist/worker/continuation/worker-continuation.js +278 -0
  149. package/dist/worker/observe/health.js +1 -1
  150. package/dist/worker/observe/node-transparency.js +572 -0
  151. package/dist/worker/observe/routes.js +51 -1
  152. package/dist/worker/observe/server.js +4 -0
  153. package/dist/worker/observe/static/api.js +94 -10
  154. package/dist/worker/observe/static/app.js +18 -70
  155. package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
  156. package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
  157. package/dist/worker/observe/static/index.html +1 -1
  158. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  159. package/dist/worker/observe/static/operator-chrome.css +145 -20
  160. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  161. package/dist/worker/observe/static/operator-chrome.js +330 -100
  162. package/dist/worker/observe/static/router.d.ts +46 -0
  163. package/dist/worker/observe/static/router.js +25 -1
  164. package/dist/worker/observe/static/state.d.ts +50 -0
  165. package/dist/worker/observe/static/state.js +175 -1
  166. package/dist/worker/observe/static/styles.css +1039 -128
  167. package/dist/worker/observe/static/views/dag-inspector.js +1218 -60
  168. package/dist/worker/observe/static/views/dag.js +2 -1
  169. package/dist/worker/observe/static/views/dashboard.js +6 -4
  170. package/dist/worker/observe/static/views/night.js +2 -2
  171. package/dist/worker/observe/static/views/pool.js +1 -0
  172. package/dist/worker/observe/static/views/run.js +3 -1
  173. package/dist/worker/observe/static/views/session-timeline.js +75 -10
  174. package/dist/worker/observe/static/views/task.js +1 -1
  175. package/dist/worker/outcomes/adapters.js +19 -0
  176. package/dist/worker/outcomes/types.js +1 -0
  177. package/dist/worker/pool/attempt-lease.js +97 -66
  178. package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
  179. package/dist/worker/pool/reconcile.js +46 -1
  180. package/dist/worker/pool/run-store.js +2 -0
  181. package/dist/worker/pool/state-projection.js +2 -0
  182. package/dist/worker/task-spec/workflow-routing.js +8 -3
  183. package/dist/workflows/dag/artifact-bindings.js +149 -0
  184. package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
  185. package/dist/workflows/dag/backend-test-pytest-collection.js +11 -10
  186. package/dist/workflows/dag/backend-test-writer-completeness.js +7 -12
  187. package/dist/workflows/dag/context-receipt.js +305 -0
  188. package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
  189. package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
  190. package/dist/workflows/dag/context-transfer/renderers.js +93 -0
  191. package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
  192. package/dist/workflows/dag/frontend-implementation-contract.js +1 -89
  193. package/dist/workflows/dag/init-hybrid.js +259 -47
  194. package/dist/workflows/dag/interrupt-request.js +3 -3
  195. package/dist/workflows/dag/lifecycle.js +3 -3
  196. package/dist/workflows/dag/node-execution.js +112 -27
  197. package/dist/workflows/dag/path-safety.js +1 -0
  198. package/dist/workflows/dag/prompt.js +29 -4
  199. package/dist/workflows/dag/rerun-plan.js +272 -0
  200. package/dist/workflows/dag/rerun-run.js +390 -12
  201. package/dist/workflows/dag/skill-snapshot.js +1 -0
  202. package/dist/workflows/dag/types.js +203 -8
  203. package/docs/README.md +1 -0
  204. package/docs/architecture/runtime-boundaries.md +6 -6
  205. package/docs/architecture/worker-and-feature.md +1 -1
  206. package/docs/operations/local-development-environment.md +21 -0
  207. package/docs/templates/agent-dag.schema.json +50 -0
  208. package/docs/templates/backend-test-dag.json +265 -237
  209. package/docs/templates/product-line/task.yaml +1 -1
  210. package/harness.json +3 -3
  211. package/package.json +5 -1
  212. package/skills/loop-agent/references/command-reference.md +6 -0
  213. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
  214. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
  215. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
  216. package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
  217. package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
  218. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
  219. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
  220. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  221. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
  222. /package/dist/{worker → infrastructure}/console/operation-store.js +0 -0
  223. /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
@@ -0,0 +1,149 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { canonicalSha256 } from "../../shared/context-transfer.js";
5
+ export function buildArtifactBindingGraph(spec) {
6
+ const consumers = new Map();
7
+ for (const task of spec.tasks) {
8
+ for (const binding of task.consumesArtifacts ?? []) {
9
+ const nodeIds = consumers.get(binding.artifactId) ?? [];
10
+ nodeIds.push(task.id);
11
+ consumers.set(binding.artifactId, nodeIds);
12
+ }
13
+ }
14
+ const artifacts = spec.tasks
15
+ .flatMap((task) => (task.producesArtifacts ?? []).map((artifact) => ({
16
+ ...artifact,
17
+ producerNodeId: task.id,
18
+ directConsumerNodeIds: [...(consumers.get(artifact.artifactId) ?? [])].sort(),
19
+ })))
20
+ .sort((a, b) => a.artifactId.localeCompare(b.artifactId));
21
+ const available = artifacts.length > 0;
22
+ const payload = { schemaVersion: 1, artifacts };
23
+ return {
24
+ ...payload,
25
+ available,
26
+ ...(available ? {} : { reasonCode: "ARTIFACT_BINDING_UNAVAILABLE" }),
27
+ artifactGraphSha256: canonicalSha256(payload),
28
+ };
29
+ }
30
+ function collectDescendants(spec, roots) {
31
+ const children = new Map();
32
+ for (const task of spec.tasks) {
33
+ for (const parent of task.depends_on) {
34
+ const values = children.get(parent) ?? [];
35
+ values.push(task.id);
36
+ children.set(parent, values);
37
+ }
38
+ }
39
+ const queue = [...roots];
40
+ const seen = new Set();
41
+ while (queue.length > 0) {
42
+ const nodeId = queue.shift();
43
+ if (seen.has(nodeId))
44
+ continue;
45
+ seen.add(nodeId);
46
+ queue.push(...(children.get(nodeId) ?? []));
47
+ }
48
+ return spec.tasks.map((task) => task.id).filter((nodeId) => seen.has(nodeId));
49
+ }
50
+ export function computeArtifactImpact(spec, artifactId) {
51
+ const graph = buildArtifactBindingGraph(spec);
52
+ const artifact = graph.artifacts.find((entry) => entry.artifactId === artifactId);
53
+ if (!artifact || artifact.directConsumerNodeIds.length === 0)
54
+ return undefined;
55
+ const affectedNodeIds = collectDescendants(spec, artifact.directConsumerNodeIds);
56
+ const affectedSet = new Set(affectedNodeIds);
57
+ const importedNodeIds = spec.tasks
58
+ .map((task) => task.id)
59
+ .filter((nodeId) => !affectedSet.has(nodeId));
60
+ return {
61
+ artifactId,
62
+ producerNodeId: artifact.producerNodeId,
63
+ directConsumerNodeIds: artifact.directConsumerNodeIds,
64
+ effectiveFromNodeId: affectedNodeIds[0],
65
+ affectedNodeIds,
66
+ importedNodeIds,
67
+ };
68
+ }
69
+ function runtimeArtifactFields(node, kind) {
70
+ if (kind === "structured") {
71
+ return {
72
+ path: node.structuredArtifactPath,
73
+ sha256: node.structuredArtifactSha256,
74
+ schemaId: node.structuredArtifactSchemaId,
75
+ };
76
+ }
77
+ if (kind === "assistant") {
78
+ return { path: node.assistantArtifactPath, sha256: node.assistantSha256 };
79
+ }
80
+ return { path: node.stdoutArtifactPath, sha256: node.stdoutSha256 };
81
+ }
82
+ export async function materializeDeclaredArtifactFacts(input) {
83
+ const facts = [];
84
+ for (const declaration of input.task.producesArtifacts ?? []) {
85
+ const runtime = runtimeArtifactFields(input.node, declaration.kind);
86
+ const base = { ...declaration, revisionEligible: false };
87
+ if (!runtime.path || !runtime.sha256) {
88
+ facts.push({
89
+ ...base,
90
+ status: "missing",
91
+ reasonCode: "ARTIFACT_NOT_MATERIALIZED",
92
+ });
93
+ continue;
94
+ }
95
+ const relativePath = path
96
+ .relative(input.runDir, runtime.path)
97
+ .split(path.sep)
98
+ .join("/");
99
+ if (relativePath !== declaration.path) {
100
+ facts.push({
101
+ ...base,
102
+ status: "mismatch",
103
+ sha256: runtime.sha256,
104
+ reasonCode: "ARTIFACT_PATH_MISMATCH",
105
+ });
106
+ continue;
107
+ }
108
+ if (declaration.schemaId !== runtime.schemaId) {
109
+ facts.push({
110
+ ...base,
111
+ status: "mismatch",
112
+ sha256: runtime.sha256,
113
+ reasonCode: "ARTIFACT_SCHEMA_MISMATCH",
114
+ });
115
+ continue;
116
+ }
117
+ let bytes;
118
+ try {
119
+ bytes = await readFile(runtime.path);
120
+ }
121
+ catch {
122
+ facts.push({
123
+ ...base,
124
+ status: "missing",
125
+ reasonCode: "ARTIFACT_NOT_MATERIALIZED",
126
+ });
127
+ continue;
128
+ }
129
+ const actualSha256 = createHash("sha256").update(bytes).digest("hex");
130
+ if (actualSha256 !== runtime.sha256) {
131
+ facts.push({
132
+ ...base,
133
+ status: "mismatch",
134
+ sha256: actualSha256,
135
+ bytes: bytes.byteLength,
136
+ reasonCode: "ARTIFACT_HASH_MISMATCH",
137
+ });
138
+ continue;
139
+ }
140
+ facts.push({
141
+ ...base,
142
+ status: "materialized",
143
+ sha256: actualSha256,
144
+ bytes: bytes.byteLength,
145
+ revisionEligible: declaration.revisionPolicy === "replace-file",
146
+ });
147
+ }
148
+ return facts;
149
+ }
@@ -0,0 +1,31 @@
1
+ import { backendPytestCollectionFactsSchema, backendTestExecutionReadinessSchema } from "./backend-test-pytest-collection.js";
2
+ import { backendTestCaseManifestSchema } from "./backend-test-case-manifest.js";
3
+ import { backendTestAnalysisContractSchema } from "./backend-test-analysis-contract.js";
4
+ import { backendTestExecutionContractSchema } from "./backend-test-execution-contract.js";
5
+ import { backendTestCaseCoverageFactsSchema } from "./backend-test-case-coverage-analysis.js";
6
+ import { frontendImplementationContractSchema } from "./frontend-implementation-contract.js";
7
+ /**
8
+ * Standalone artifact revision schema registry (exec-plan
9
+ * 2026-08-25-console-node-context-artifact-rerun W4.3 / M4 exit criteria).
10
+ *
11
+ * Maps a frozen produced-artifact `schemaId` to a deterministic zod validator
12
+ * that can check candidate revision bytes without run context. Declared
13
+ * schemaIds without an entry here are rejected fail-closed with
14
+ * ARTIFACT_REVISION_SCHEMA_INVALID — an artifact cannot be staged until its
15
+ * schema is registered. Never guessed, never silently passed.
16
+ */
17
+ const ARTIFACT_REVISION_SCHEMAS = Object.freeze({
18
+ "backend-test-pytest-collection-v3": backendPytestCollectionFactsSchema,
19
+ "backend-test-execution-readiness-v2": backendTestExecutionReadinessSchema,
20
+ "backend-test-case-manifest-v1": backendTestCaseManifestSchema,
21
+ "backend-test-analysis-v2": backendTestAnalysisContractSchema,
22
+ "backend-test-execution-v1": backendTestExecutionContractSchema,
23
+ "backend-test-case-coverage-facts-v4": backendTestCaseCoverageFactsSchema,
24
+ "frontend-implementation-contract-v1": frontendImplementationContractSchema,
25
+ });
26
+ export function resolveArtifactRevisionSchema(schemaId) {
27
+ return ARTIFACT_REVISION_SCHEMAS[schemaId];
28
+ }
29
+ export function listArtifactRevisionSchemaIds() {
30
+ return Object.keys(ARTIFACT_REVISION_SCHEMAS).sort();
31
+ }
@@ -258,13 +258,14 @@ function bounded(value) {
258
258
  ? normalized
259
259
  : `${normalized.slice(0, MAX_DIAGNOSTIC_CHARS)}\n...[truncated]`;
260
260
  }
261
- function collectionItems(stdout) {
261
+ function collectionItems(stdout, layout) {
262
+ const rootPrefix = `${(layout ?? resolveBackendTestLayout(undefined)).testRoot}/`;
262
263
  return [...new Set(stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => {
263
264
  if (!line.includes("::"))
264
265
  return false;
265
266
  if (/^(ERROR|FAILED|E\s+)/.test(line))
266
267
  return false;
267
- return /^testcase\//.test(line.replaceAll("\\", "/"));
268
+ return line.replaceAll("\\", "/").startsWith(rootPrefix);
268
269
  }).map((line) => line.replaceAll("\\", "/")))];
269
270
  }
270
271
  function escapeRegExp(value) {
@@ -416,7 +417,7 @@ function classifyCollectionFailure(output, evidence) {
416
417
  return { status: "BLOCKED", kind: "unclassified-collection-error", detail: "collection failure cannot be safely attributed to generated testcase assets" };
417
418
  }
418
419
  export function assessBackendPytestCollection(input) {
419
- const items = collectionItems(input.stdout);
420
+ const items = collectionItems(input.stdout, input.layout);
420
421
  const fixtureResolution = input.fixtureResolution ?? { exitCode: 0, stdout: "fixture resolution assumed by direct assessor", stderr: "" };
421
422
  if (input.exitCode === 0 && fixtureResolution.exitCode === 0) {
422
423
  return backendPytestCollectionFactsSchema.parse({
@@ -740,7 +741,7 @@ export async function materializeBackendTestExecutionReadiness(input) {
740
741
  if (input.effective.phase !== "effective" || input.effective.status !== "PASS" || input.effective.fixtureResolutionStatus !== "PASS") {
741
742
  throw new Error("backend-test execution readiness requires effective collection and fixture-resolution PASS");
742
743
  }
743
- const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.effective.mappedScripts);
744
+ const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.effective.mappedScripts, { layout: input.layout });
744
745
  assertSameInventory(input.effective, current);
745
746
  const eligibility = input.eligibility ?? { eligibleItemIds: input.effective.collectedItemIds, excludedItems: [] };
746
747
  const status = eligibility.eligibleItemIds.length === 0
@@ -839,7 +840,7 @@ async function firstUnreadableAssetPath(workspaceRoot, readiness) {
839
840
  }
840
841
  return undefined;
841
842
  }
842
- export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, readiness) {
843
+ export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, readiness, layout) {
843
844
  if (!["PASS", "PARTIAL"].includes(readiness.status)) {
844
845
  const counts = formatReadinessCounts(readiness);
845
846
  const reasonTally = readiness.eligibleItemIds.length === 0 && readiness.excludedItems.length > 0
@@ -862,7 +863,7 @@ export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, re
862
863
  }
863
864
  let current;
864
865
  try {
865
- current = await buildBackendPytestAssetInventory(workspaceRoot, readiness.mappedScripts);
866
+ current = await buildBackendPytestAssetInventory(workspaceRoot, readiness.mappedScripts, { layout });
866
867
  }
867
868
  catch {
868
869
  const fallbackPath = [...readiness.mappedScripts].sort()[0] ?? Object.keys(readiness.assetHashes).sort()[0] ?? "unknown";
@@ -897,11 +898,11 @@ export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, re
897
898
  }
898
899
  }
899
900
  }
900
- export async function assertBackendPytestCollectionFresh(workspaceRoot, effective) {
901
+ export async function assertBackendPytestCollectionFresh(workspaceRoot, effective, layout) {
901
902
  if (effective.phase !== "effective" || effective.status !== "PASS") {
902
903
  throw new Error("backend pytest execution requires effective collection PASS facts");
903
904
  }
904
- const current = await buildBackendPytestAssetInventory(workspaceRoot, effective.mappedScripts);
905
+ const current = await buildBackendPytestAssetInventory(workspaceRoot, effective.mappedScripts, { layout });
905
906
  const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
906
907
  if (priorityOnly.length > 0)
907
908
  throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
@@ -909,7 +910,7 @@ export async function assertBackendPytestCollectionFresh(workspaceRoot, effectiv
909
910
  }
910
911
  export async function materializeEffectiveBackendPytestCollection(input) {
911
912
  if (input.initial.status === "PASS") {
912
- const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.initial.mappedScripts);
913
+ const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.initial.mappedScripts, { layout: input.layout });
913
914
  const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
914
915
  if (priorityOnly.length > 0)
915
916
  throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
@@ -927,7 +928,7 @@ export async function materializeEffectiveBackendPytestCollection(input) {
927
928
  if (input.final.status !== "PASS")
928
929
  throw new Error("backend pytest collection still fails after bounded repair");
929
930
  await assertBackendPytestRepairSafety(input.workspaceRoot, input.initial, input.final);
930
- const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.final.mappedScripts);
931
+ const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.final.mappedScripts, { layout: input.layout });
931
932
  const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
932
933
  if (priorityOnly.length > 0)
933
934
  throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
@@ -884,28 +884,23 @@ export function isBackendTestShardChildTask(task) {
884
884
  task.id?.startsWith("generate-backend-pytest-case-"));
885
885
  }
886
886
  /**
887
- * Is this task the README-only plan node of the map-sharded md topology?
888
- * The plan node writes testcase/md/README.md alone; module case cards are
889
- * produced by the downstream map_agent children, so the Completeness Gate
890
- * must NOT require module files to exist at plan time.
887
+ * Historical classifier for the Markdown plan node. Current backend-test N2
888
+ * is read-only and persists a run-owned plan.md, so writer completeness never
889
+ * invokes the workspace README assessment below for newly generated DAGs.
891
890
  */
892
891
  export function isBackendTestMdPlanTask(task) {
893
892
  return task.id === "generate-backend-md-plan-pi";
894
893
  }
895
894
  /**
896
- * Is this task the shared-resources plan node of the map-sharded pytest
897
- * topology? The plan node writes helpers/factories only; per-module pytest
898
- * scripts are produced by the downstream map_agent children, so the Gate
899
- * must NOT require mapped pytest scripts to exist at plan time.
895
+ * Historical classifier for the former shared-resources pytest writer. Current
896
+ * N7 is static and every map child writes one self-contained pytest module.
900
897
  */
901
898
  export function isBackendTestPytestPlanTask(task) {
902
899
  return task.id === "generate-backend-pytest-plan-pi";
903
900
  }
904
901
  /**
905
- * Plan-node completeness for the md topology: verify the README itself is
906
- * structurally complete (Coverage Scope, Coverage Matrix, a non-empty module
907
- * index), WITHOUT requiring the indexed module files to exist yet — those are
908
- * written by the downstream map_agent shard children.
902
+ * Legacy workspace-plan completeness helper retained for isolated historical
903
+ * fixtures. New DAGs validate the run-owned plan artifact in N3 instead.
909
904
  */
910
905
  export async function assessBackendTestMdPlanCompleteness(workspaceRoot, layout) {
911
906
  const resolvedPlan = layout ?? resolveBackendTestLayout(undefined);
@@ -0,0 +1,305 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { computeContextDigest, effectiveNodeContextV1Schema, } from "../../shared/context-transfer.js";
4
+ import { writeDagNodeBufferArtifact, writeDagNodeJsonArtifact, writeDagNodeTextArtifact, } from "../../infrastructure/harness/artifact-store.js";
5
+ import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
6
+ import { getTaskPaths } from "../../task/runtime.js";
7
+ import { readStoredContractRef } from "../../task/contract/observe.js";
8
+ import { computeCanonicalHash } from "../../task/contract/hash.js";
9
+ import { readFileBoundarySafe } from "./path-safety.js";
10
+ const CONTEXT_FACT_WRITE_OPTIONS = Object.freeze({
11
+ mode: 0o600,
12
+ directoryMode: 0o700,
13
+ });
14
+ function sha256(value) {
15
+ return createHash("sha256").update(value).digest("hex");
16
+ }
17
+ function bytes(value) {
18
+ return Buffer.byteLength(value, "utf8");
19
+ }
20
+ function sourceDocumentMediaType(filePath) {
21
+ const ext = path.extname(filePath).toLowerCase();
22
+ if (ext === ".md" || ext === ".markdown") {
23
+ return "text/markdown; charset=utf-8";
24
+ }
25
+ if (ext === ".txt")
26
+ return "text/plain; charset=utf-8";
27
+ if (ext === ".json")
28
+ return "application/json";
29
+ return "application/octet-stream";
30
+ }
31
+ /**
32
+ * Managed Task Contract facts, materialized so a portable bundle can verify
33
+ * the canonical hash without the run directory. The material carries the full
34
+ * canonical-hash material (including the source hashes the canonical hash is
35
+ * computed from), so a bundle consumer can recompute `computeCanonicalHash`
36
+ * and compare it against the recorded canonicalHash — the JSON is no longer a
37
+ * self-describing blob. The contract prose itself is not re-inlined here: it
38
+ * reached the model through the assembled system prompt (component 0), and
39
+ * declared sources below carry the per-document provenance.
40
+ */
41
+ async function captureTaskContractComponent(input, materialRoot) {
42
+ const binding = input.spec?.taskContractBinding;
43
+ if (!binding)
44
+ return null;
45
+ if (!input.repoRoot) {
46
+ throw new Error("context receipt requires repoRoot to verify managed task contract");
47
+ }
48
+ const contractPath = getTaskPaths(path.resolve(input.repoRoot), binding.taskId).contractPath;
49
+ const ref = await readStoredContractRef(contractPath);
50
+ if (!ref) {
51
+ throw new Error(`context task contract unavailable: ${binding.taskId} (no stored contract.json)`);
52
+ }
53
+ if (ref.canonicalHash !== binding.canonicalHash) {
54
+ throw new Error(`context task contract drift: ${binding.taskId}`);
55
+ }
56
+ const material = {
57
+ schemaVersion: binding.schemaVersion,
58
+ taskId: binding.taskId,
59
+ revision: binding.revision,
60
+ projectionVersion: binding.projectionVersion,
61
+ canonicalizerVersion: binding.canonicalizerVersion,
62
+ taskConfigSchemaVersion: binding.taskConfigSchemaVersion,
63
+ sourceHashes: ref.sourceHashes,
64
+ taskConfigSha256: binding.taskConfigSha256,
65
+ canonicalHash: binding.canonicalHash,
66
+ };
67
+ const recomputed = computeCanonicalHash({
68
+ sourceHashes: material.sourceHashes,
69
+ taskConfigSha256: material.taskConfigSha256,
70
+ projectionVersion: binding.projectionVersion,
71
+ canonicalizerVersion: binding.canonicalizerVersion,
72
+ taskConfigSchemaVersion: binding.taskConfigSchemaVersion,
73
+ });
74
+ if (recomputed !== binding.canonicalHash) {
75
+ throw new Error(`context task contract canonical hash mismatch: ${binding.taskId}`);
76
+ }
77
+ const materialBytes = Buffer.from(`${JSON.stringify(material, null, 2)}\n`, "utf8");
78
+ const materializedPath = `${input.nodeId}/${materialRoot}/task-contract-binding.json`;
79
+ await writeDagNodeBufferArtifact(input.runDir, input.nodeId, `${materialRoot}/task-contract-binding.json`, materialBytes, CONTEXT_FACT_WRITE_OPTIONS);
80
+ return {
81
+ kind: "task-contract",
82
+ logicalName: "managed-task-contract",
83
+ source: {
84
+ type: "managed-task-contract-binding",
85
+ identity: "run.json#taskContractBinding",
86
+ revision: binding.canonicalHash,
87
+ },
88
+ materializedPath,
89
+ sha256: createHash("sha256").update(materialBytes).digest("hex"),
90
+ bytes: materialBytes.byteLength,
91
+ mediaType: "application/json",
92
+ inclusion: "full",
93
+ authoritativeSource: {
94
+ identity: "run.json#taskContractBinding",
95
+ sha256: binding.canonicalHash,
96
+ },
97
+ };
98
+ }
99
+ /**
100
+ * Declared source bindings are contract-level provenance facts, NOT model
101
+ * inputs: at dispatch only the assembled system prompt and user message are
102
+ * sent (dag-pi-executor attaches no files). Their prose is embedded into the
103
+ * system prompt during prompt assembly. Components are therefore recorded as
104
+ * reference-only with hash-verified integrity (drift fails closed) and paths
105
+ * resolved through the authoritative task-source boundary — never as
106
+ * full/excerpt "effective context".
107
+ */
108
+ async function captureSourceContextComponents(input, _materialRoot, startIndex) {
109
+ const binding = input.spec?.sourceBinding;
110
+ if (!binding || binding.sources.length === 0) {
111
+ return { components: [], truncations: [] };
112
+ }
113
+ if (!input.repoRoot) {
114
+ throw new Error("context receipt requires repoRoot to verify declared source documents");
115
+ }
116
+ const components = [];
117
+ const truncations = [];
118
+ for (let i = 0; i < binding.sources.length; i += 1) {
119
+ const declared = binding.sources[i];
120
+ const sourcePath = resolveDagTaskSourcePath({
121
+ workspaceRoot: path.resolve(input.repoRoot),
122
+ taskId: binding.taskId,
123
+ sourcePath: declared.path,
124
+ });
125
+ const sourceDir = getTaskPaths(path.resolve(input.repoRoot), binding.taskId).sourceDir;
126
+ const sourceBytes = await readFileBoundarySafe({
127
+ boundaryRoot: sourceDir,
128
+ targetPath: sourcePath,
129
+ label: `context source ${declared.path}`,
130
+ });
131
+ const sourceSha256 = createHash("sha256").update(sourceBytes).digest("hex");
132
+ if (sourceSha256 !== declared.sha256) {
133
+ throw new Error(`context source drift detected: ${declared.path}`);
134
+ }
135
+ components.push({
136
+ kind: declared.kind === "constraint" ? "execution-constraint" : "context-file",
137
+ logicalName: declared.path,
138
+ source: {
139
+ type: "declared-source-binding",
140
+ identity: `run.json#sourceBinding/sources/${String(i)}`,
141
+ revision: declared.sha256,
142
+ },
143
+ sha256: sourceSha256,
144
+ bytes: sourceBytes.byteLength,
145
+ mediaType: sourceDocumentMediaType(declared.path),
146
+ inclusion: "reference-only",
147
+ authoritativeSource: {
148
+ identity: declared.path,
149
+ sha256: sourceSha256,
150
+ },
151
+ omissionReason: "declared source is contract provenance; its prose was embedded into the assembled system prompt at dispatch and the bytes are not copied into the v1 receipt",
152
+ });
153
+ }
154
+ return { components, truncations };
155
+ }
156
+ async function captureConsumedArtifactComponents(input, _materialRoot, _startIndex) {
157
+ if (!input.spec || (input.task.consumesArtifacts?.length ?? 0) === 0) {
158
+ return { components: [], truncations: [] };
159
+ }
160
+ const components = [];
161
+ const resolvedRunDir = path.resolve(input.runDir);
162
+ for (const binding of input.task.consumesArtifacts ?? []) {
163
+ const producers = input.spec.tasks.flatMap((task) => (task.producesArtifacts ?? [])
164
+ .filter((artifact) => artifact.artifactId === binding.artifactId)
165
+ .map((artifact) => ({ task, artifact })));
166
+ if (producers.length !== 1) {
167
+ if (binding.required) {
168
+ throw new Error(`context artifact binding unavailable: ${binding.artifactId}`);
169
+ }
170
+ continue;
171
+ }
172
+ const producer = producers[0];
173
+ const sourcePath = path.resolve(resolvedRunDir, ...producer.artifact.path.split("/"));
174
+ const sourceBytes = await readFileBoundarySafe({
175
+ boundaryRoot: resolvedRunDir,
176
+ targetPath: sourcePath,
177
+ label: `context artifact ${binding.artifactId}`,
178
+ }).catch(async (error) => {
179
+ const message = error instanceof Error ? error.message : String(error);
180
+ if (/missing|no such|ENOENT|not found/i.test(message) && !binding.required) {
181
+ return undefined;
182
+ }
183
+ throw error;
184
+ });
185
+ if (sourceBytes === undefined)
186
+ continue;
187
+ const sourceSha256 = createHash("sha256").update(sourceBytes).digest("hex");
188
+ components.push({
189
+ kind: "upstream-artifact",
190
+ logicalName: binding.artifactId,
191
+ source: {
192
+ type: "frozen-artifact-binding",
193
+ identity: `run.json#tasks/${producer.task.id}/producesArtifacts/${binding.artifactId}`,
194
+ revision: sourceSha256,
195
+ },
196
+ sha256: sourceSha256,
197
+ bytes: sourceBytes.byteLength,
198
+ mediaType: producer.artifact.mediaType,
199
+ inclusion: "reference-only",
200
+ authoritativeSource: {
201
+ identity: `${producer.task.id}:${producer.artifact.path}`,
202
+ sha256: sourceSha256,
203
+ },
204
+ omissionReason: "upstream artifact is dispatched as a read-tool pointer (path + sha256 + use-read instruction), not as inline content; the consumer must read it through the frozen binding and the bytes are not copied into the v1 receipt",
205
+ });
206
+ }
207
+ return { components, truncations: [] };
208
+ }
209
+ /**
210
+ * Persist the exact Pi dispatch messages before the provider is called. Material
211
+ * bytes are committed first and the validated receipt last, so a crash can only
212
+ * leave an ignorable partial material directory — never a receipt that points at
213
+ * missing bytes.
214
+ */
215
+ export async function writeEffectiveContextReceipt(input) {
216
+ if (!Number.isSafeInteger(input.attempt) || input.attempt < 1) {
217
+ throw new Error("context receipt attempt must be a positive integer");
218
+ }
219
+ const materialRoot = `context-materials/attempt-${input.attempt}`;
220
+ const systemPath = `${input.nodeId}/${materialRoot}/system-prompt.md`;
221
+ const userPath = `${input.nodeId}/${materialRoot}/user-message.md`;
222
+ await writeDagNodeTextArtifact(input.runDir, input.nodeId, `${materialRoot}/system-prompt.md`, input.systemPrompt, CONTEXT_FACT_WRITE_OPTIONS);
223
+ await writeDagNodeTextArtifact(input.runDir, input.nodeId, `${materialRoot}/user-message.md`, input.userMessage, CONTEXT_FACT_WRITE_OPTIONS);
224
+ const taskContractComponent = await captureTaskContractComponent(input, materialRoot);
225
+ const sourceCapture = await captureSourceContextComponents(input, materialRoot, 2 + (taskContractComponent ? 1 : 0));
226
+ const upstreamCapture = await captureConsumedArtifactComponents(input, materialRoot, 2 +
227
+ (taskContractComponent ? 1 : 0) +
228
+ sourceCapture.components.length);
229
+ const extraComponents = [
230
+ ...(taskContractComponent ? [taskContractComponent] : []),
231
+ ...sourceCapture.components,
232
+ ...upstreamCapture.components,
233
+ ];
234
+ const truncations = [
235
+ ...sourceCapture.truncations,
236
+ ...upstreamCapture.truncations,
237
+ ];
238
+ const contextWithoutDigest = {
239
+ schemaVersion: 1,
240
+ identity: {
241
+ runId: input.runId,
242
+ nodeId: input.nodeId,
243
+ attempt: input.attempt,
244
+ capturedAt: input.capturedAt ?? new Date().toISOString(),
245
+ },
246
+ orderedComponents: [
247
+ {
248
+ index: 0,
249
+ kind: "system-prompt",
250
+ logicalName: "dag-pi-system-prompt",
251
+ source: {
252
+ type: "assembled-dag-prompt",
253
+ identity: `run.json#tasks/${input.nodeId}`,
254
+ },
255
+ materializedPath: systemPath,
256
+ sha256: sha256(input.systemPrompt),
257
+ bytes: bytes(input.systemPrompt),
258
+ mediaType: "text/markdown; charset=utf-8",
259
+ inclusion: "full",
260
+ },
261
+ {
262
+ index: 1,
263
+ kind: "user-prompt",
264
+ logicalName: "dag-pi-user-message",
265
+ source: {
266
+ type: "executor-message",
267
+ identity: `dag-pi-user-message/${input.nodeId}`,
268
+ },
269
+ materializedPath: userPath,
270
+ sha256: sha256(input.userMessage),
271
+ bytes: bytes(input.userMessage),
272
+ mediaType: "text/plain; charset=utf-8",
273
+ inclusion: "full",
274
+ },
275
+ ...extraComponents.map((component, offset) => ({
276
+ ...component,
277
+ index: 2 + offset,
278
+ })),
279
+ ],
280
+ promptAssembly: {
281
+ strategy: "dag-pi-system-plus-user-v1",
282
+ finalPromptSha256: sha256(input.systemPrompt),
283
+ truncations,
284
+ },
285
+ boundaries: {
286
+ allowedPaths: [...(input.task.allowedPaths ?? [])],
287
+ forbiddenPaths: [...(input.task.forbiddenPaths ?? [])],
288
+ writeSet: [...(input.task.writeSet ?? [])],
289
+ },
290
+ runtime: {
291
+ executor: "pi",
292
+ resolvedModel: input.resolvedModel,
293
+ skills: [...input.resolvedSkills],
294
+ extensions: [...input.resolvedExtensions],
295
+ activeTools: [...input.activeTools],
296
+ },
297
+ };
298
+ const receipt = effectiveNodeContextV1Schema.parse({
299
+ ...contextWithoutDigest,
300
+ contextDigest: computeContextDigest(contextWithoutDigest),
301
+ });
302
+ const receiptFile = `context-receipts/attempt-${input.attempt}.json`;
303
+ const receiptPath = await writeDagNodeJsonArtifact(input.runDir, input.nodeId, receiptFile, receipt, CONTEXT_FACT_WRITE_OPTIONS);
304
+ return { receipt, receiptPath };
305
+ }