@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,423 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { z } from "zod";
5
+ import { CONTEXT_BUNDLE_LIMITS, computeBundleManifestSha256, contextBundleManifestV1Schema, effectiveNodeContextV1Schema, } from "../../../shared/context-transfer.js";
6
+ import { redactSecretsWithFindings, redactSensitivePathsWithFindings, } from "../../../shared/preview.js";
7
+ import { evaluatePiSessionCapability, renderPiSessionJsonl, } from "../../../adapters/context-transfer/pi-session.js";
8
+ import { renderGenericHandoff, renderReadableTranscript, } from "./renderers.js";
9
+ import { computeCanonicalHash } from "../../../task/contract/hash.js";
10
+ import { readFileBoundarySafe } from "../path-safety.js";
11
+ function sha256(value) {
12
+ return createHash("sha256").update(value).digest("hex");
13
+ }
14
+ function canonicalJson(value) {
15
+ return `${JSON.stringify(value, null, 2)}\n`;
16
+ }
17
+ const sha256Hex = z.string().regex(/^[a-f0-9]{64}$/);
18
+ /** Strict shape of the materialized managed-task-contract component. */
19
+ const taskContractMaterialSchema = z
20
+ .object({
21
+ schemaVersion: z.literal(1),
22
+ taskId: z.string().min(1),
23
+ revision: z.number().int().nonnegative(),
24
+ projectionVersion: z.literal(1),
25
+ canonicalizerVersion: z.literal(1),
26
+ taskConfigSchemaVersion: z.literal(1),
27
+ sourceHashes: z
28
+ .object({
29
+ requirementSha256: sha256Hex,
30
+ constraintsSha256: sha256Hex,
31
+ referenceManifestSha256: sha256Hex.optional(),
32
+ requirementLedgerSha256: sha256Hex.optional(),
33
+ })
34
+ .strict(),
35
+ taskConfigSha256: sha256Hex,
36
+ canonicalHash: sha256Hex,
37
+ })
38
+ .strict();
39
+ /**
40
+ * Read a run-owned material with full path safety and TOCTOU protection:
41
+ * reject symlinked or hardlinked intermediates/final entry and non-regular
42
+ * files (device/FIFO), open with O_NOFOLLOW and read via the fd so a swapped
43
+ * path can never redirect the bundle to content outside the run. Hash/size
44
+ * verification still runs on the returned bytes.
45
+ */
46
+ async function readBundleMaterial(runDir, relativePath) {
47
+ try {
48
+ return await readFileBoundarySafe({
49
+ boundaryRoot: path.resolve(runDir),
50
+ targetPath: path.resolve(runDir, ...relativePath.split("/")),
51
+ label: `context bundle material ${relativePath}`,
52
+ });
53
+ }
54
+ catch (error) {
55
+ throw new Error(`CONTEXT_COMPONENT_UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`);
56
+ }
57
+ }
58
+ function isTextualMedia(mediaType) {
59
+ const normalized = mediaType.split(";", 1)[0].trim().toLowerCase();
60
+ return (normalized.startsWith("text/") ||
61
+ normalized.includes("json") ||
62
+ normalized.includes("xml") ||
63
+ normalized.includes("yaml") ||
64
+ normalized.includes("javascript"));
65
+ }
66
+ function applyBundleSecurityPass(files, profile) {
67
+ let detectedFindingCount = 0;
68
+ let redactedFindingCount = 0;
69
+ let credentialFindingCount = 0;
70
+ let sensitivePathFindingCount = 0;
71
+ const securedFiles = files.map((file) => {
72
+ if (file.redaction === "omitted" || !isTextualMedia(file.mediaType)) {
73
+ return file;
74
+ }
75
+ const sourceText = file.bytes.toString("utf8");
76
+ const credentialScan = redactSecretsWithFindings(sourceText);
77
+ const pathScan = redactSensitivePathsWithFindings(sourceText);
78
+ const fileFindingCount = credentialScan.findingCount + pathScan.findingCount;
79
+ credentialFindingCount += credentialScan.findingCount;
80
+ sensitivePathFindingCount += pathScan.findingCount;
81
+ detectedFindingCount += fileFindingCount;
82
+ if (profile === "raw" || fileFindingCount === 0)
83
+ return file;
84
+ redactedFindingCount += fileFindingCount;
85
+ const redacted = redactSensitivePathsWithFindings(credentialScan.redacted).redacted;
86
+ return {
87
+ ...file,
88
+ bytes: Buffer.from(redacted, "utf8"),
89
+ redaction: "redacted",
90
+ sourceSha256: sha256(file.bytes),
91
+ };
92
+ });
93
+ return {
94
+ files: securedFiles,
95
+ detectedFindingCount,
96
+ redactedFindingCount,
97
+ credentialFindingCount,
98
+ sensitivePathFindingCount,
99
+ };
100
+ }
101
+ export async function buildContextBundle(input) {
102
+ const profile = input.profile ?? "redacted";
103
+ const target = input.target ?? "generic";
104
+ if (profile === "raw" && !input.rawAcknowledgement) {
105
+ throw new Error("RAW_EXPORT_ACKNOWLEDGEMENT_REQUIRED");
106
+ }
107
+ const receiptPath = path.join(input.runDir, input.nodeId, "context-receipts", `attempt-${input.attempt}.json`);
108
+ let receiptBytes;
109
+ try {
110
+ receiptBytes = await readFileBoundarySafe({
111
+ boundaryRoot: path.resolve(input.runDir),
112
+ targetPath: receiptPath,
113
+ label: "context receipt",
114
+ });
115
+ }
116
+ catch (error) {
117
+ const message = error instanceof Error ? error.message : String(error);
118
+ if (/missing|ENOENT|no such/i.test(message)) {
119
+ throw new Error("CONTEXT_RECEIPT_MISSING");
120
+ }
121
+ throw new Error(`CONTEXT_COMPONENT_UNAVAILABLE: ${message}`);
122
+ }
123
+ const receipt = effectiveNodeContextV1Schema.parse(JSON.parse(receiptBytes.toString("utf8")));
124
+ if (receipt.identity.runId !== input.runId ||
125
+ receipt.identity.nodeId !== input.nodeId ||
126
+ receipt.identity.attempt !== input.attempt) {
127
+ throw new Error("CONTEXT_COMPONENT_UNAVAILABLE: receipt ownership mismatch");
128
+ }
129
+ let files = [];
130
+ for (const component of receipt.orderedComponents) {
131
+ if (!component.materializedPath || component.inclusion === "omitted")
132
+ continue;
133
+ const source = await readBundleMaterial(input.runDir, component.materializedPath);
134
+ if (source.byteLength !== component.bytes ||
135
+ sha256(source) !== component.sha256) {
136
+ throw new Error("CONTEXT_COMPONENT_UNAVAILABLE: material hash mismatch");
137
+ }
138
+ if (component.kind === "task-contract") {
139
+ // Independently verify the materialized canonical hash: strictly parse
140
+ // the canonical-hash material (versions, sha256s, sourceHashes), then
141
+ // recompute computeCanonicalHash and require it to match both the
142
+ // material's claim and the receipt's authoritative hash. Without strict
143
+ // parsing, malformed materials (e.g. non-object sourceHashes) could be
144
+ // accepted as long as they carry a self-consistent hash.
145
+ let parsed;
146
+ try {
147
+ parsed = taskContractMaterialSchema.safeParse(JSON.parse(source.toString("utf8")));
148
+ }
149
+ catch {
150
+ throw new Error("CONTEXT_COMPONENT_UNAVAILABLE: task contract material is not valid JSON");
151
+ }
152
+ if (!parsed.success) {
153
+ throw new Error(`CONTEXT_COMPONENT_UNAVAILABLE: task contract material failed strict validation: ${parsed.error.issues
154
+ .map((issue) => issue.path.join("."))
155
+ .join(", ")}`);
156
+ }
157
+ const material = parsed.data;
158
+ const recomputed = computeCanonicalHash({
159
+ sourceHashes: material.sourceHashes,
160
+ taskConfigSha256: material.taskConfigSha256,
161
+ projectionVersion: material.projectionVersion,
162
+ canonicalizerVersion: material.canonicalizerVersion,
163
+ taskConfigSchemaVersion: material.taskConfigSchemaVersion,
164
+ });
165
+ if (recomputed !== material.canonicalHash ||
166
+ material.canonicalHash !== component.authoritativeSource?.sha256) {
167
+ throw new Error("CONTEXT_COMPONENT_UNAVAILABLE: task contract canonical hash mismatch");
168
+ }
169
+ }
170
+ const bundlePath = component.kind === "system-prompt"
171
+ ? "context/system-prompt.md"
172
+ : component.kind === "user-prompt"
173
+ ? "context/user-message.md"
174
+ : `context/components/${String(component.index).padStart(4, "0")}.bin`;
175
+ files.push({
176
+ path: bundlePath,
177
+ bytes: source,
178
+ mediaType: component.mediaType,
179
+ origin: component.source.identity,
180
+ redaction: "none",
181
+ });
182
+ }
183
+ files.push({
184
+ path: "context/receipt.json",
185
+ bytes: receiptBytes,
186
+ mediaType: "application/json",
187
+ origin: `${input.nodeId}/context-receipts/attempt-${input.attempt}.json`,
188
+ redaction: "none",
189
+ });
190
+ let sessionEventLines = [];
191
+ if (target === "readable") {
192
+ try {
193
+ sessionEventLines = (await readBundleMaterial(input.runDir, `${input.nodeId}/session-events.jsonl`)).toString("utf8").split(/\r?\n/).filter(Boolean);
194
+ }
195
+ catch (error) {
196
+ // Only a genuinely absent event stream (missing file) is treated as "no
197
+ // events" for historical/non-Pi nodes. Any safety rejection (symlink /
198
+ // hardlink / non-regular material) must fail closed: an attacker must not
199
+ // be able to mask a tampered stream as "node had no events".
200
+ const message = error instanceof Error ? error.message : String(error);
201
+ if (!/missing|no such|ENOENT|not found/i.test(message)) {
202
+ throw error;
203
+ }
204
+ }
205
+ }
206
+ const handoff = renderGenericHandoff({
207
+ runId: input.runId,
208
+ nodeId: input.nodeId,
209
+ attempt: input.attempt,
210
+ receipt,
211
+ profile,
212
+ });
213
+ files.push({
214
+ path: target === "readable" ? "transcript.md" : "handoff.md",
215
+ bytes: Buffer.from(target === "readable"
216
+ ? renderReadableTranscript({ receipt, eventLines: sessionEventLines })
217
+ : handoff, "utf8"),
218
+ mediaType: "text/markdown; charset=utf-8",
219
+ origin: target === "readable"
220
+ ? "loop-agent/readable-transcript-v1"
221
+ : "loop-agent/generic-handoff-v1",
222
+ redaction: "none",
223
+ });
224
+ const piCapability = target === "pi-session"
225
+ ? evaluatePiSessionCapability(receipt)
226
+ : undefined;
227
+ if (target === "pi-session" && piCapability?.compatible) {
228
+ files.push({
229
+ path: "pi/session.jsonl",
230
+ bytes: Buffer.from(renderPiSessionJsonl({
231
+ receipt,
232
+ runId: input.runId,
233
+ nodeId: input.nodeId,
234
+ attempt: input.attempt,
235
+ }), "utf8"),
236
+ mediaType: "application/x-ndjson",
237
+ origin: "loop-agent/pi-session-adapter-v1",
238
+ redaction: "none",
239
+ });
240
+ }
241
+ const securityPass = applyBundleSecurityPass(files, profile);
242
+ files = securityPass.files;
243
+ files.sort((a, b) => a.path.localeCompare(b.path));
244
+ const totalBytes = files.reduce((total, file) => total + file.bytes.byteLength, 0);
245
+ if (files.length > CONTEXT_BUNDLE_LIMITS.fileCount ||
246
+ totalBytes > CONTEXT_BUNDLE_LIMITS.totalUncompressedBytes ||
247
+ files.some((file) => file.bytes.byteLength > CONTEXT_BUNDLE_LIMITS.singleFileBytes)) {
248
+ throw new Error("CONTEXT_BUNDLE_TOO_LARGE");
249
+ }
250
+ const runSpecBytes = await readBundleMaterial(input.runDir, "run.json");
251
+ const bundleSeed = {
252
+ runId: input.runId,
253
+ nodeId: input.nodeId,
254
+ attempt: input.attempt,
255
+ profile,
256
+ target,
257
+ contextDigest: receipt.contextDigest,
258
+ };
259
+ const manifestWithoutHash = {
260
+ schemaVersion: 1,
261
+ bundleId: `ctx-${sha256(JSON.stringify(bundleSeed)).slice(0, 24)}`,
262
+ createdAt: receipt.identity.capturedAt,
263
+ source: {
264
+ repoIdentity: sha256(path.resolve(input.runDir)).slice(0, 24),
265
+ runId: input.runId,
266
+ nodeId: input.nodeId,
267
+ attempt: input.attempt,
268
+ },
269
+ profile,
270
+ target,
271
+ reproducibility: {
272
+ level: target === "readable"
273
+ ? "readable-transcript"
274
+ : piCapability?.reproducibility ?? "semantic-handoff",
275
+ reasons: [
276
+ "dispatch messages and runtime capability facts captured before model call",
277
+ ...(piCapability?.reasonCodes ?? []),
278
+ ],
279
+ limitations: [
280
+ "model output and external tool state are not deterministic",
281
+ ...(piCapability?.limitations ?? []),
282
+ ],
283
+ },
284
+ bindings: {
285
+ frozenRunSpecSha256: sha256(runSpecBytes),
286
+ contextReceiptSha256: sha256(receiptBytes),
287
+ contextDigest: receipt.contextDigest,
288
+ },
289
+ runtime: {
290
+ executor: receipt.runtime.executor,
291
+ ...(receipt.runtime.resolvedModel
292
+ ? { model: receipt.runtime.resolvedModel }
293
+ : {}),
294
+ skills: receipt.runtime.skills.map((id) => ({ id })),
295
+ extensions: receipt.runtime.extensions.map((id) => ({
296
+ id,
297
+ status: "resolved",
298
+ })),
299
+ tools: receipt.runtime.activeTools.map((name) => ({
300
+ name,
301
+ available: true,
302
+ })),
303
+ },
304
+ files: files.map((file) => ({
305
+ path: file.path,
306
+ sha256: sha256(file.bytes),
307
+ bytes: file.bytes.byteLength,
308
+ mediaType: file.mediaType,
309
+ origin: file.origin,
310
+ redaction: file.redaction,
311
+ ...(file.sourceSha256 ? { sourceSha256: file.sourceSha256 } : {}),
312
+ })),
313
+ omissions: [],
314
+ security: {
315
+ secretScan: securityPass.detectedFindingCount > 0 ? "warning" : "pass",
316
+ findingCounts: {
317
+ detected: securityPass.detectedFindingCount,
318
+ credentials: securityPass.credentialFindingCount,
319
+ sensitivePaths: securityPass.sensitivePathFindingCount,
320
+ ...(profile === "redacted"
321
+ ? { redacted: securityPass.redactedFindingCount }
322
+ : {}),
323
+ },
324
+ ...(input.rawAcknowledgement
325
+ ? { rawAcknowledgement: input.rawAcknowledgement }
326
+ : {}),
327
+ },
328
+ };
329
+ const manifest = contextBundleManifestV1Schema.parse({
330
+ ...manifestWithoutHash,
331
+ bundleSha256: computeBundleManifestSha256(manifestWithoutHash),
332
+ });
333
+ const archiveFiles = [
334
+ ...files,
335
+ {
336
+ path: "manifest.json",
337
+ bytes: Buffer.from(canonicalJson(manifest), "utf8"),
338
+ mediaType: "application/json",
339
+ origin: "loop-agent/context-bundle-manifest-v1",
340
+ redaction: "none",
341
+ },
342
+ ].sort((a, b) => a.path.localeCompare(b.path));
343
+ return { manifest, files, zipBytes: deterministicZip(archiveFiles) };
344
+ }
345
+ export async function writeContextBundleDirectory(bundle, outputDir) {
346
+ await mkdir(outputDir, { recursive: true });
347
+ for (const file of bundle.files) {
348
+ const target = path.resolve(outputDir, ...file.path.split("/"));
349
+ if (!target.startsWith(`${path.resolve(outputDir)}${path.sep}`)) {
350
+ throw new Error("unsafe bundle output path");
351
+ }
352
+ await mkdir(path.dirname(target), { recursive: true });
353
+ await writeFile(target, file.bytes, { flag: "wx" });
354
+ }
355
+ await writeFile(path.join(outputDir, "manifest.json"), canonicalJson(bundle.manifest), { flag: "wx" });
356
+ }
357
+ const CRC_TABLE = Array.from({ length: 256 }, (_, index) => {
358
+ let value = index;
359
+ for (let bit = 0; bit < 8; bit += 1) {
360
+ value = (value & 1) !== 0 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
361
+ }
362
+ return value >>> 0;
363
+ });
364
+ function crc32(bytes) {
365
+ let crc = 0xffffffff;
366
+ for (const byte of bytes)
367
+ crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
368
+ return (crc ^ 0xffffffff) >>> 0;
369
+ }
370
+ function deterministicZip(files) {
371
+ const localParts = [];
372
+ const centralParts = [];
373
+ let offset = 0;
374
+ for (const file of files) {
375
+ const name = Buffer.from(file.path, "utf8");
376
+ const data = file.bytes;
377
+ const crc = crc32(data);
378
+ const local = Buffer.alloc(30);
379
+ local.writeUInt32LE(0x04034b50, 0);
380
+ local.writeUInt16LE(20, 4);
381
+ local.writeUInt16LE(0x0800, 6);
382
+ local.writeUInt16LE(0, 8);
383
+ local.writeUInt16LE(0, 10);
384
+ local.writeUInt16LE(33, 12);
385
+ local.writeUInt32LE(crc, 14);
386
+ local.writeUInt32LE(data.byteLength, 18);
387
+ local.writeUInt32LE(data.byteLength, 22);
388
+ local.writeUInt16LE(name.byteLength, 26);
389
+ local.writeUInt16LE(0, 28);
390
+ localParts.push(local, name, data);
391
+ const central = Buffer.alloc(46);
392
+ central.writeUInt32LE(0x02014b50, 0);
393
+ central.writeUInt16LE(20, 4);
394
+ central.writeUInt16LE(20, 6);
395
+ central.writeUInt16LE(0x0800, 8);
396
+ central.writeUInt16LE(0, 10);
397
+ central.writeUInt16LE(0, 12);
398
+ central.writeUInt16LE(33, 14);
399
+ central.writeUInt32LE(crc, 16);
400
+ central.writeUInt32LE(data.byteLength, 20);
401
+ central.writeUInt32LE(data.byteLength, 24);
402
+ central.writeUInt16LE(name.byteLength, 28);
403
+ central.writeUInt16LE(0, 30);
404
+ central.writeUInt16LE(0, 32);
405
+ central.writeUInt16LE(0, 34);
406
+ central.writeUInt16LE(0, 36);
407
+ central.writeUInt32LE(0, 38);
408
+ central.writeUInt32LE(offset, 42);
409
+ centralParts.push(central, name);
410
+ offset += local.byteLength + name.byteLength + data.byteLength;
411
+ }
412
+ const centralBytes = Buffer.concat(centralParts);
413
+ const end = Buffer.alloc(22);
414
+ end.writeUInt32LE(0x06054b50, 0);
415
+ end.writeUInt16LE(0, 4);
416
+ end.writeUInt16LE(0, 6);
417
+ end.writeUInt16LE(files.length, 8);
418
+ end.writeUInt16LE(files.length, 10);
419
+ end.writeUInt32LE(centralBytes.byteLength, 12);
420
+ end.writeUInt32LE(offset, 16);
421
+ end.writeUInt16LE(0, 20);
422
+ return Buffer.concat([...localParts, centralBytes, end]);
423
+ }
@@ -0,0 +1,61 @@
1
+ import path from "node:path";
2
+ import { buildArtifactBindingGraph } from "../artifact-bindings.js";
3
+ import { readFileBoundarySafe } from "../path-safety.js";
4
+ import { locateDagRun, readDagRunSpec, readDagRunState, } from "../lifecycle.js";
5
+ import { executeDagArtifactOverrideRerun, planDagArtifactOverride, } from "../rerun-run.js";
6
+ import { buildContextBundle, } from "./context-bundle.js";
7
+ export async function buildDagRunContextBundle(input) {
8
+ const located = await locateDagRun(input.cwd, input.runId);
9
+ if (!located)
10
+ throw new Error(`dag run not found: ${input.runId}`);
11
+ return buildContextBundle({
12
+ runDir: located.runDir,
13
+ runId: input.runId,
14
+ nodeId: input.nodeId,
15
+ attempt: input.attempt,
16
+ profile: input.profile,
17
+ target: input.target,
18
+ ...(input.rawAcknowledgement
19
+ ? { rawAcknowledgement: input.rawAcknowledgement }
20
+ : {}),
21
+ });
22
+ }
23
+ export async function readRevisionParentArtifact(input) {
24
+ const located = await locateDagRun(input.cwd, input.runId);
25
+ if (!located || located.lifecycle !== "completed") {
26
+ throw new Error("artifact revision requires a completed parent run");
27
+ }
28
+ const spec = await readDagRunSpec(located.runDir);
29
+ const state = await readDagRunState(located.runDir);
30
+ const declaration = buildArtifactBindingGraph(spec).artifacts.find((entry) => entry.artifactId === input.artifactId &&
31
+ entry.producerNodeId === input.producerNodeId);
32
+ if (!declaration)
33
+ throw new Error("ARTIFACT_BINDING_UNAVAILABLE");
34
+ const fact = state.nodes[input.producerNodeId]?.declaredArtifacts?.find((entry) => entry.artifactId === input.artifactId);
35
+ if (fact?.status !== "materialized" || !fact.sha256 || !fact.revisionEligible) {
36
+ throw new Error("ARTIFACT_NOT_REVISION_ELIGIBLE");
37
+ }
38
+ const parentPath = path.resolve(located.runDir, ...declaration.path.split("/"));
39
+ const resolvedRunDir = path.resolve(located.runDir);
40
+ if (!parentPath.startsWith(`${resolvedRunDir}${path.sep}`)) {
41
+ throw new Error("ARTIFACT_REVISION_PATH_INVALID");
42
+ }
43
+ // Final regular single-link check (rejects symlink/hardlink/device/FIFO)
44
+ // on top of the per-segment boundary walk, with a TOCTOU-safe fd read.
45
+ const parentBytes = await readFileBoundarySafe({
46
+ boundaryRoot: resolvedRunDir,
47
+ targetPath: parentPath,
48
+ label: `artifact parent ${input.artifactId}`,
49
+ });
50
+ return {
51
+ declaration,
52
+ parentSha256: fact.sha256,
53
+ parentBytes,
54
+ };
55
+ }
56
+ export async function planArtifactRevisionContinuation(input) {
57
+ return planDagArtifactOverride(input);
58
+ }
59
+ export async function executeArtifactRevisionContinuation(input) {
60
+ return executeDagArtifactOverrideRerun(input);
61
+ }
@@ -0,0 +1,93 @@
1
+ function textFromContent(content) {
2
+ if (typeof content === "string")
3
+ return content;
4
+ if (!Array.isArray(content))
5
+ return "";
6
+ return content
7
+ .map((part) => {
8
+ if (!part || typeof part !== "object")
9
+ return "";
10
+ const record = part;
11
+ if (record.type === "text" && typeof record.text === "string") {
12
+ return record.text;
13
+ }
14
+ if (record.type === "thinking" && typeof record.thinking === "string") {
15
+ return `[thinking captured]\n${record.thinking}`;
16
+ }
17
+ return "";
18
+ })
19
+ .filter(Boolean)
20
+ .join("\n");
21
+ }
22
+ export function renderGenericHandoff(input) {
23
+ const componentTable = input.receipt.orderedComponents.map((component) => `| ${component.index} | ${component.kind} | ${component.logicalName} | ${component.inclusion} | ${component.sha256} |`);
24
+ return [
25
+ "# Agent handoff",
26
+ "",
27
+ `- run: ${input.runId}`,
28
+ `- node: ${input.nodeId}`,
29
+ `- attempt: ${input.attempt}`,
30
+ `- context digest: ${input.receipt.contextDigest}`,
31
+ `- profile: ${input.profile}`,
32
+ "",
33
+ "## Resume instructions",
34
+ "",
35
+ "Use `context/system-prompt.md` as system/developer context and `context/user-message.md` as the user message. Read other materialized components in receipt order. Verify every payload hash against `manifest.json` before importing. The bundle restores captured inputs, not deterministic model output, credentials, or external tool state.",
36
+ "",
37
+ "## Ordered context index",
38
+ "",
39
+ "| # | kind | name | inclusion | source sha256 |",
40
+ "|---:|---|---|---|---|",
41
+ ...componentTable,
42
+ "",
43
+ ].join("\n");
44
+ }
45
+ export function renderReadableTranscript(input) {
46
+ const output = [
47
+ "# Node transcript",
48
+ "",
49
+ `Context digest: ${input.receipt.contextDigest}`,
50
+ "",
51
+ "The exact dispatched system and user messages are stored in `context/`. Missing thinking or tool events are not reconstructed.",
52
+ "",
53
+ ];
54
+ let turn = 0;
55
+ const seenMessages = new Set();
56
+ for (const line of input.eventLines) {
57
+ let event;
58
+ try {
59
+ event = JSON.parse(line);
60
+ }
61
+ catch {
62
+ continue;
63
+ }
64
+ if (event.type === "turn_start") {
65
+ turn += 1;
66
+ output.push(`## Turn ${turn}`, "");
67
+ continue;
68
+ }
69
+ if (event.type === "tool_execution_start") {
70
+ output.push(`### Tool call: ${String(event.toolName ?? "unknown")}`, "", "```json", JSON.stringify(event.args ?? null, null, 2), "```", "");
71
+ continue;
72
+ }
73
+ if (event.type === "tool_execution_end") {
74
+ output.push(`### Tool result: ${String(event.toolName ?? "unknown")}${event.isError === true ? " (error)" : ""}`, "", textFromContent(event.result?.content) || "[result not captured as text]", "");
75
+ continue;
76
+ }
77
+ if (event.type !== "message_end" || !event.message || typeof event.message !== "object") {
78
+ continue;
79
+ }
80
+ const message = event.message;
81
+ const fingerprint = JSON.stringify(message);
82
+ if (seenMessages.has(fingerprint))
83
+ continue;
84
+ seenMessages.add(fingerprint);
85
+ const role = typeof message.role === "string" ? message.role : "unknown";
86
+ const body = textFromContent(message.content);
87
+ output.push(`### ${role}`, "", body || "[message content not captured]", "");
88
+ }
89
+ if (turn === 0 && seenMessages.size === 0) {
90
+ output.push("No session events were captured for this attempt.", "");
91
+ }
92
+ return output.join("\n");
93
+ }
@@ -370,6 +370,38 @@ async function inspectPath(repoRoot, relPath, deps) {
370
370
  throw error;
371
371
  }
372
372
  }
373
+ /**
374
+ * Build the CAS attempt side of a rollback journal from the current worktree.
375
+ * This is used by high-risk artifact continuations, where the baseline is
376
+ * captured before the child starts and the complete attempt inventory is
377
+ * derived after the child settles (including changes from dynamic writers).
378
+ */
379
+ export async function recordWriterAttemptFromCurrent(snapshot, options) {
380
+ const deps = resolveDeps(options.dependencies);
381
+ const working = structuredClone(snapshot);
382
+ const baselineByPath = new Map(working.existingFiles.map((file) => [file.path, file]));
383
+ const currentMatches = [];
384
+ await walkMatchingEntries(options.repoRoot, "", working.writeSet, deps, (match) => currentMatches.push(match));
385
+ const candidatePaths = new Set([
386
+ ...baselineByPath.keys(),
387
+ ...currentMatches.map((match) => match.relPath),
388
+ ]);
389
+ const attemptChangedPaths = [];
390
+ const attemptFileHashes = {};
391
+ for (const relPath of [...candidatePaths].sort((left, right) => left.localeCompare(right))) {
392
+ const baseline = baselineByPath.get(relPath);
393
+ const current = await inspectPath(options.repoRoot, relPath, deps);
394
+ const currentHash = current.kind === "absent" ? "absent" : current.hash;
395
+ const baselineHash = baseline?.baselineSha256 ?? "absent";
396
+ if (currentHash === baselineHash)
397
+ continue;
398
+ attemptChangedPaths.push(relPath);
399
+ attemptFileHashes[relPath] = currentHash;
400
+ }
401
+ working.attemptChangedPaths = attemptChangedPaths;
402
+ working.attemptFileHashes = attemptFileHashes;
403
+ return frontendWriterRollbackV1Schema.parse(working);
404
+ }
373
405
  function ensureJournalFile(working, relPath, baselineSha256, attemptSha256) {
374
406
  let file = working.journal.files[relPath];
375
407
  if (!file) {