@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.
- package/CHANGELOG.md +135 -92
- package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
- package/dist/adapters/context-transfer/pi-session.js +61 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +12 -0
- package/dist/cli/program.js +23 -1
- package/dist/commands/dag-artifact.js +284 -0
- package/dist/commands/dag-context.js +184 -0
- package/dist/commands/dag-rerun.js +206 -1
- package/dist/executors/dag-pi-executor.js +108 -19
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +49 -1
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +42 -2
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/{worker → infrastructure}/console/app-data.js +4 -0
- package/dist/infrastructure/console/artifact-revision-store.js +430 -0
- package/dist/infrastructure/console/context-export-store.js +160 -0
- package/dist/infrastructure/console/dir-lock.js +132 -0
- package/dist/infrastructure/harness/artifact-store.js +10 -1
- package/dist/infrastructure/harness/atomic-write.js +35 -2
- package/dist/shared/context-transfer/artifact-revision.js +172 -0
- package/dist/shared/context-transfer.js +418 -0
- package/dist/shared/operator/capabilities.js +14 -0
- package/dist/shared/operator/safe-run-summary.js +1 -0
- package/dist/shared/path-safety.js +93 -0
- package/dist/shared/preview.js +28 -4
- package/dist/task/config-types.js +12 -1
- package/dist/task/source-prepare/fragment-inventory.js +1 -14
- package/dist/task/task-demand-routing.js +3 -0
- package/dist/worker/console/chat/chat-event-store.js +77 -7
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/pi-runtime.js +331 -18
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/resource-preferences-store.js +1 -1
- package/dist/worker/console/chat/routes.js +389 -56
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +94 -64
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +79 -0
- package/dist/worker/console/console-update-runtime.js +1 -1
- package/dist/worker/console/context-transfer-diagnostics.js +198 -0
- package/dist/worker/console/dag-confirmation.js +1 -1
- package/dist/worker/console/dag-execution-receipt.js +1 -1
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/draft-store.js +1 -1
- package/dist/worker/console/human-gate-token.js +1 -1
- package/dist/worker/console/index.js +3 -3
- package/dist/worker/console/interview/assessment.js +1 -1
- package/dist/worker/console/interview/session.js +1 -1
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-runner.js +45 -4
- package/dist/worker/console/operation-sse.js +1 -1
- package/dist/worker/console/operation-wait.js +1 -1
- package/dist/worker/console/operator-actions.js +104 -3
- package/dist/worker/console/operator-surface-health.js +1 -1
- package/dist/worker/console/operator-user-error.js +128 -0
- package/dist/worker/console/routes.js +927 -8
- package/dist/worker/console/security.js +39 -4
- package/dist/worker/console/server.js +532 -187
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CpWvwARc.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-DctXgO-g.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-D4mIkoQl.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-ClG0--KO.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-CwdLk78T.js} +1 -1
- package/dist/worker/console/static/assets/channel-C9VQZ-ru.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DKczcu-O.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-BV6F-vBI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtojVnvA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-BjDQRGnO.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-I0fV6ngU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-BM8C-mmC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-BEK-a15e.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-CQsg_fSR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-B-BcaUm3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D1Bu08fg.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BsCkw5MF.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BsCkw5MF.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-q8nwtxBR.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-CruhpURw.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-CJne6iYF.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-CbeLvBU5.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-DmCxH1Pe.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-DJ5OYhtC.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-B-UN-hln.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-CcEro8Ey.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BBBzDden.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-xKEyH-oo.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-C7xl63VC.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-nthQQ9nR.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-CWN5mE33.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-Wn86tQ6N.js} +1 -1
- package/dist/worker/console/static/assets/index-D-0NVyMk.css +1 -0
- package/dist/worker/console/static/assets/index-gwyOxQiS.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-_WQymGtW.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-COBeivBm.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-CwGOJl3Q.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-Ci0CJ5Vi.js} +1 -1
- package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CPjMq6vN.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-Z0wGoe0b.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-dISked0d.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-BJxgof7Y.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-CIblU8eW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-vwYiqh8E.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-_iOsVeFm.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Dk0MLxO_.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-Dx7HtLVW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-DRuWjlpL.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Bhy9DQAA.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-DAx9twnU.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dmdf8cGy.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-okMlvVrM.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-W1IcAFff.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DXqCKe37.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CeIUkwBh.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-BP76BKTN.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-BpVI0OpF.js} +1 -1
- package/dist/worker/console/static/index.html +7 -2
- package/dist/worker/console/static-src/app/console-types.js +29 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
- package/dist/worker/console/workspace-context.js +260 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/continuation/worker-continuation.js +278 -0
- package/dist/worker/observe/health.js +1 -1
- package/dist/worker/observe/node-transparency.js +572 -0
- package/dist/worker/observe/routes.js +51 -1
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +94 -10
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
- package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -100
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +175 -1
- package/dist/worker/observe/static/styles.css +1039 -128
- package/dist/worker/observe/static/views/dag-inspector.js +1218 -60
- package/dist/worker/observe/static/views/dag.js +2 -1
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/session-timeline.js +75 -10
- package/dist/worker/observe/static/views/task.js +1 -1
- package/dist/worker/outcomes/adapters.js +19 -0
- package/dist/worker/outcomes/types.js +1 -0
- package/dist/worker/pool/attempt-lease.js +97 -66
- package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
- package/dist/worker/pool/reconcile.js +46 -1
- package/dist/worker/pool/run-store.js +2 -0
- package/dist/worker/pool/state-projection.js +2 -0
- package/dist/worker/task-spec/workflow-routing.js +8 -3
- package/dist/workflows/dag/artifact-bindings.js +149 -0
- package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +11 -10
- package/dist/workflows/dag/backend-test-writer-completeness.js +7 -12
- package/dist/workflows/dag/context-receipt.js +305 -0
- package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
- package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
- package/dist/workflows/dag/context-transfer/renderers.js +93 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +1 -89
- package/dist/workflows/dag/init-hybrid.js +259 -47
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +112 -27
- package/dist/workflows/dag/path-safety.js +1 -0
- package/dist/workflows/dag/prompt.js +29 -4
- package/dist/workflows/dag/rerun-plan.js +272 -0
- package/dist/workflows/dag/rerun-run.js +390 -12
- package/dist/workflows/dag/skill-snapshot.js +1 -0
- package/dist/workflows/dag/types.js +203 -8
- package/docs/README.md +1 -0
- package/docs/architecture/runtime-boundaries.md +6 -6
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/agent-dag.schema.json +50 -0
- package/docs/templates/backend-test-dag.json +265 -237
- package/docs/templates/product-line/task.yaml +1 -1
- package/harness.json +3 -3
- package/package.json +5 -1
- package/skills/loop-agent/references/command-reference.md +6 -0
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
- /package/dist/{worker → infrastructure}/console/operation-store.js +0 -0
- /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
|
|
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
|
-
*
|
|
888
|
-
*
|
|
889
|
-
*
|
|
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
|
-
*
|
|
897
|
-
*
|
|
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
|
-
*
|
|
906
|
-
*
|
|
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
|
+
}
|