@stigmer/runner 3.0.9-dev.20260615153829 → 3.1.0
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/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/approval-contract/types.d.ts +174 -0
- package/dist/__test-utils__/approval-contract/types.js +24 -0
- package/dist/__test-utils__/approval-contract/types.js.map +1 -0
- package/dist/activities/call-agent-status.d.ts +19 -1
- package/dist/activities/call-agent-status.js +64 -5
- package/dist/activities/call-agent-status.js.map +1 -1
- package/dist/activities/call-llm.js +19 -53
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +24 -0
- package/dist/activities/classify-tool-approvals.js +69 -17
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -0
- package/dist/activities/discover-mcp-server.js +11 -1
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +140 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +130 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.d.ts +18 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +123 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-cursor/approval-policy.d.ts +17 -61
- package/dist/activities/execute-cursor/approval-policy.js +8 -120
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +219 -9
- package/dist/activities/execute-cursor/approval-state.js +233 -17
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +127 -0
- package/dist/activities/execute-cursor/capture-flow.js +234 -0
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -0
- package/dist/activities/execute-cursor/cas-observations.d.ts +96 -0
- package/dist/activities/execute-cursor/cas-observations.js +184 -0
- package/dist/activities/execute-cursor/cas-observations.js.map +1 -0
- package/dist/activities/execute-cursor/command-provenance.d.ts +62 -0
- package/dist/activities/execute-cursor/command-provenance.js +128 -0
- package/dist/activities/execute-cursor/command-provenance.js.map +1 -0
- package/dist/activities/execute-cursor/exact-apply.d.ts +110 -0
- package/dist/activities/execute-cursor/exact-apply.js +204 -0
- package/dist/activities/execute-cursor/exact-apply.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +53 -24
- package/dist/activities/execute-cursor/hook-script.js +310 -47
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +8 -1
- package/dist/activities/execute-cursor/index.js +500 -64
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +258 -11
- package/dist/activities/execute-cursor/message-translator.js +836 -90
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +20 -23
- package/dist/activities/execute-cursor/prompt-builder.js +74 -15
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/workspace-setup.d.ts +17 -0
- package/dist/activities/execute-cursor/workspace-setup.js +212 -33
- package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.d.ts +19 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js +143 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +88 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +81 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -0
- package/dist/activities/execute-deep-agent/approval-file-change.d.ts +47 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js +68 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -0
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +8 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +7 -7
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +42 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js +47 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +79 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js +112 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -0
- package/dist/activities/execute-deep-agent/hitl.d.ts +16 -3
- package/dist/activities/execute-deep-agent/hitl.js +11 -3
- package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.d.ts +2 -1
- package/dist/activities/execute-deep-agent/index.js +393 -33
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +7 -1
- package/dist/activities/execute-deep-agent/inline-publisher.js +23 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +53 -2
- package/dist/activities/execute-deep-agent/setup.js +149 -92
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +36 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +56 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +1 -0
- package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +34 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.js +26 -25
- package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.d.ts +17 -5
- package/dist/activities/execute-deep-agent/status-builder.js +44 -3
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +2 -19
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.js +3 -15
- package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js +23 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +30 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js +29 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +32 -2
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +9 -5
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +2 -0
- package/dist/client/stigmer-client.js +2 -0
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/middleware/approval-gate.d.ts +85 -4
- package/dist/middleware/approval-gate.js +165 -38
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/middleware/types.d.ts +2 -5
- package/dist/shared/activity-input.d.ts +43 -0
- package/dist/shared/activity-input.js +17 -0
- package/dist/shared/activity-input.js.map +1 -0
- package/dist/shared/approval-canonicalize.d.ts +19 -0
- package/dist/shared/approval-canonicalize.js +119 -0
- package/dist/shared/approval-canonicalize.js.map +1 -0
- package/dist/shared/approval-fingerprint.d.ts +106 -0
- package/dist/shared/approval-fingerprint.js +113 -0
- package/dist/shared/approval-fingerprint.js.map +1 -0
- package/dist/shared/approval-policy.d.ts +182 -12
- package/dist/shared/approval-policy.js +213 -27
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/args-preview.d.ts +52 -0
- package/dist/shared/args-preview.js +93 -0
- package/dist/shared/args-preview.js.map +1 -0
- package/dist/shared/artifact-storage.d.ts +19 -1
- package/dist/shared/artifact-storage.js +48 -11
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/file-change.d.ts +44 -0
- package/dist/shared/file-change.js +57 -0
- package/dist/shared/file-change.js.map +1 -0
- package/dist/shared/file-tools.d.ts +107 -0
- package/dist/shared/file-tools.js +168 -0
- package/dist/shared/file-tools.js.map +1 -0
- package/dist/shared/filereview/capture.d.ts +202 -0
- package/dist/shared/filereview/capture.js +498 -0
- package/dist/shared/filereview/capture.js.map +1 -0
- package/dist/shared/filereview/cas-substrate.d.ts +190 -0
- package/dist/shared/filereview/cas-substrate.js +284 -0
- package/dist/shared/filereview/cas-substrate.js.map +1 -0
- package/dist/shared/filereview/digest.d.ts +40 -0
- package/dist/shared/filereview/digest.js +66 -0
- package/dist/shared/filereview/digest.js.map +1 -0
- package/dist/shared/filereview/events.d.ts +170 -0
- package/dist/shared/filereview/events.js +298 -0
- package/dist/shared/filereview/events.js.map +1 -0
- package/dist/shared/filereview/git-substrate.d.ts +175 -0
- package/dist/shared/filereview/git-substrate.js +439 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -0
- package/dist/shared/filereview/index.d.ts +11 -0
- package/dist/shared/filereview/index.js +12 -0
- package/dist/shared/filereview/index.js.map +1 -0
- package/dist/shared/filereview/secret-paths.d.ts +63 -0
- package/dist/shared/filereview/secret-paths.js +105 -0
- package/dist/shared/filereview/secret-paths.js.map +1 -0
- package/dist/shared/fingerprint-secret.d.ts +26 -0
- package/dist/shared/fingerprint-secret.js +47 -0
- package/dist/shared/fingerprint-secret.js.map +1 -0
- package/dist/shared/model-client.d.ts +51 -0
- package/dist/shared/model-client.js +77 -0
- package/dist/shared/model-client.js.map +1 -0
- package/dist/shared/plan-artifact.js +0 -2
- package/dist/shared/plan-artifact.js.map +1 -1
- package/dist/shared/status-offload.d.ts +83 -9
- package/dist/shared/status-offload.js +399 -79
- package/dist/shared/status-offload.js.map +1 -1
- package/dist/shared/status.js +14 -1
- package/dist/shared/status.js.map +1 -1
- package/dist/shared/tool-kind.d.ts +19 -0
- package/dist/shared/tool-kind.js +13 -0
- package/dist/shared/tool-kind.js.map +1 -1
- package/dist/shared/tool-row.d.ts +88 -0
- package/dist/shared/tool-row.js +127 -0
- package/dist/shared/tool-row.js.map +1 -0
- package/dist/shared/workspace/platform-dir.d.ts +25 -0
- package/dist/shared/workspace/platform-dir.js +38 -2
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.js +56 -7
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.d.ts +50 -0
- package/dist/workflows/connect-mcp-server.js +136 -15
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +2 -2
- package/src/__test-utils__/approval-contract/contract.ts +224 -0
- package/src/__test-utils__/approval-contract/types.ts +179 -0
- package/src/__test-utils__/fake-artifact-storage.ts +72 -0
- package/src/__tests__/approval-gateway-contract.test.ts +29 -0
- package/src/__tests__/claimcheck-codec.test.ts +16 -53
- package/src/__tests__/golden-e2e.test.ts +2 -0
- package/src/__tests__/runner-token-coordinator.test.ts +3 -3
- package/src/activities/__tests__/call-agent-status.test.ts +135 -0
- package/src/activities/__tests__/call-llm.test.ts +1 -1
- package/src/activities/__tests__/classify-tool-approvals.test.ts +208 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +30 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +2 -1
- package/src/activities/call-agent-status.ts +74 -4
- package/src/activities/call-llm.ts +18 -63
- package/src/activities/classify-tool-approvals.ts +101 -19
- package/src/activities/discover-mcp-server.ts +29 -1
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +216 -0
- package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +148 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +41 -9
- package/src/activities/execute-cursor/__tests__/approval-state.test.ts +292 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +68 -1
- package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +1005 -0
- package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +187 -0
- package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +97 -0
- package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +240 -0
- package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +203 -0
- package/src/activities/execute-cursor/__tests__/exact-apply.test.ts +375 -0
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +1294 -24
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +446 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +384 -110
- package/src/activities/execute-cursor/__tests__/message-translator.test.ts +171 -25
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +189 -0
- package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +44 -23
- package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +190 -10
- package/src/activities/execute-cursor/approval-policy.ts +28 -159
- package/src/activities/execute-cursor/approval-state.ts +366 -18
- package/src/activities/execute-cursor/capture-flow.ts +323 -0
- package/src/activities/execute-cursor/cas-observations.ts +204 -0
- package/src/activities/execute-cursor/command-provenance.ts +168 -0
- package/src/activities/execute-cursor/exact-apply.ts +253 -0
- package/src/activities/execute-cursor/hook-script.ts +317 -51
- package/src/activities/execute-cursor/index.ts +575 -67
- package/src/activities/execute-cursor/message-translator.ts +963 -89
- package/src/activities/execute-cursor/prompt-builder.ts +80 -14
- package/src/activities/execute-cursor/workspace-setup.ts +257 -42
- package/src/activities/execute-deep-agent/__test-utils__/gateway-substrate.ts +180 -0
- package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +134 -0
- package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +11 -24
- package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +64 -0
- package/src/activities/execute-deep-agent/__tests__/cas-capture-observer.test.ts +163 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +8 -5
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +342 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +284 -0
- package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +14 -36
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
- package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +91 -10
- package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
- package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
- package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
- package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
- package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
- package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
- package/src/activities/execute-deep-agent/hitl.ts +23 -7
- package/src/activities/execute-deep-agent/index.ts +463 -40
- package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
- package/src/activities/execute-deep-agent/setup.ts +223 -125
- package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
- package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
- package/src/activities/execute-deep-agent/status-builder.ts +63 -8
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
- package/src/activities/execute-deep-agent/streaming.ts +3 -13
- package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
- package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
- package/src/activities/execute-deep-agent/v3-status-builder.ts +36 -2
- package/src/claimcheck/payload-codec.ts +8 -8
- package/src/client/stigmer-client.ts +9 -1
- package/src/middleware/__tests__/approval-gate.test.ts +488 -4
- package/src/middleware/approval-gate.ts +247 -38
- package/src/middleware/types.ts +5 -5
- package/src/shared/__tests__/activity-input.test.ts +78 -0
- package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
- package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
- package/src/shared/__tests__/approval-policy.test.ts +274 -40
- package/src/shared/__tests__/args-preview.test.ts +78 -0
- package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
- package/src/shared/__tests__/artifact-storage.test.ts +123 -11
- package/src/shared/__tests__/file-change.test.ts +85 -0
- package/src/shared/__tests__/file-tools.test.ts +90 -0
- package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
- package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
- package/src/shared/__tests__/model-client.test.ts +162 -0
- package/src/shared/__tests__/plan-artifact.test.ts +11 -26
- package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
- package/src/shared/__tests__/status-offload.test.ts +573 -16
- package/src/shared/__tests__/status.test.ts +4 -5
- package/src/shared/__tests__/tool-kind.test.ts +24 -1
- package/src/shared/__tests__/tool-row.test.ts +221 -0
- package/src/shared/activity-input.ts +57 -0
- package/src/shared/approval-canonicalize.ts +159 -0
- package/src/shared/approval-fingerprint.ts +148 -0
- package/src/shared/approval-policy.ts +303 -27
- package/src/shared/args-preview.ts +98 -0
- package/src/shared/artifact-storage.ts +62 -11
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
- package/src/shared/file-change.ts +64 -0
- package/src/shared/file-tools.ts +169 -0
- package/src/shared/filereview/__tests__/capture.test.ts +856 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
- package/src/shared/filereview/__tests__/digest.test.ts +100 -0
- package/src/shared/filereview/__tests__/events.test.ts +245 -0
- package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
- package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
- package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
- package/src/shared/filereview/capture.ts +727 -0
- package/src/shared/filereview/cas-substrate.ts +401 -0
- package/src/shared/filereview/digest.ts +83 -0
- package/src/shared/filereview/events.ts +449 -0
- package/src/shared/filereview/git-substrate.ts +555 -0
- package/src/shared/filereview/index.ts +60 -0
- package/src/shared/filereview/secret-paths.ts +121 -0
- package/src/shared/fingerprint-secret.ts +53 -0
- package/src/shared/model-client.ts +122 -0
- package/src/shared/plan-artifact.ts +0 -2
- package/src/shared/status-offload.ts +433 -77
- package/src/shared/status.ts +13 -0
- package/src/shared/tool-kind.ts +33 -0
- package/src/shared/tool-row.ts +135 -0
- package/src/shared/workspace/platform-dir.ts +41 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
- package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
- package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
- package/src/workflows/call-agent-orchestrator.ts +53 -6
- package/src/workflows/connect-mcp-server.ts +179 -24
- package/src/workflows/types.ts +8 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness-agnostic capture-mode turn orchestration for the apply-then-review
|
|
3
|
+
* HITL subsystem. This is the glue between the git snapshot/restore substrate
|
|
4
|
+
* ({@link ./git-substrate.js}) and the file-review ledger ({@link ./events.js}):
|
|
5
|
+
* it turns "the agent edited files freely this turn" into the append-only
|
|
6
|
+
* `file_review` events the server folds into `file_change_sets` (the single
|
|
7
|
+
* review surface), and on resume reconciles the tree to the user's decisions.
|
|
8
|
+
*
|
|
9
|
+
* It is parameterized by `harnessId` (stamped onto the BASELINE payload, which is
|
|
10
|
+
* the projection's source of truth for `turn_id`/`harness_id`) and `excludePaths`
|
|
11
|
+
* (the runner-owned files a harness writes into the workspace), so the Cursor and
|
|
12
|
+
* deep-agent harnesses author IDENTICAL ledger entries through this one seam. Each
|
|
13
|
+
* harness keeps only its presentation concerns (e.g. hiding flowed transcript
|
|
14
|
+
* rows) in its own adapter.
|
|
15
|
+
*
|
|
16
|
+
* Identity: the change set id is `${executionId}:${turnSeq}` (one turn = one
|
|
17
|
+
* change set), minted by the caller; each file's stable id is
|
|
18
|
+
* `${changeSetId}:${pathAfter||pathBefore}` so the resume can map a per-file
|
|
19
|
+
* decision back to its path without depending on any other identity.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { create } from "@bufbuild/protobuf";
|
|
23
|
+
import {
|
|
24
|
+
FileCaptureClass,
|
|
25
|
+
FileChangeKind,
|
|
26
|
+
FileChangeType,
|
|
27
|
+
FileDecisionAction,
|
|
28
|
+
FileDecisionScope,
|
|
29
|
+
FileReviewBlockReason,
|
|
30
|
+
FileReviewFailureKind,
|
|
31
|
+
SnapshotKind,
|
|
32
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
33
|
+
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
34
|
+
import {
|
|
35
|
+
CasManifestRefSchema,
|
|
36
|
+
GitTreeRefSchema,
|
|
37
|
+
SnapshotRefSchema,
|
|
38
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
39
|
+
import type {
|
|
40
|
+
CapturedFileChange,
|
|
41
|
+
FileChangeSet,
|
|
42
|
+
SnapshotRef,
|
|
43
|
+
TurnCommandProvenance,
|
|
44
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
45
|
+
import { utcTimestamp } from "../status.js";
|
|
46
|
+
import {
|
|
47
|
+
appendFileReviewEvents,
|
|
48
|
+
buildBaselineCapturedEvent,
|
|
49
|
+
buildCandidateCapturedEvent,
|
|
50
|
+
buildCapturedFileChange,
|
|
51
|
+
buildFailedEvent,
|
|
52
|
+
buildReconciledEvent,
|
|
53
|
+
contentSha256,
|
|
54
|
+
type CapturedChangeInput,
|
|
55
|
+
type ChangeSetContext,
|
|
56
|
+
} from "./events.js";
|
|
57
|
+
import {
|
|
58
|
+
applyApprovedPaths,
|
|
59
|
+
baselineRef,
|
|
60
|
+
captureChangeSet,
|
|
61
|
+
captureRef,
|
|
62
|
+
dropCaptureRefs,
|
|
63
|
+
recomputeChangeSet,
|
|
64
|
+
restoreToBaseline,
|
|
65
|
+
snapshotApproved,
|
|
66
|
+
snapshotBaseline,
|
|
67
|
+
type GitSubstrateChange as GitCapturedChange,
|
|
68
|
+
} from "./git-substrate.js";
|
|
69
|
+
import type { ArtifactStorage } from "../artifact-storage.js";
|
|
70
|
+
import {
|
|
71
|
+
applyCasApproved,
|
|
72
|
+
loadCasManifest,
|
|
73
|
+
restoreCasToBaseline,
|
|
74
|
+
snapshotCasChangeSet,
|
|
75
|
+
type BlobReader,
|
|
76
|
+
type CasCapturedFile,
|
|
77
|
+
type CasPathCapture,
|
|
78
|
+
type CasSnapshotRef,
|
|
79
|
+
} from "./cas-substrate.js";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Whether a turn runs in apply-then-review CAPTURE mode (file edits flow and are
|
|
83
|
+
* reviewed post-hoc) rather than the classic pre-write DENY-GATE. Shared by BOTH
|
|
84
|
+
* harnesses (Cursor and deep-agent) so the capture-vs-deny-gate decision is made
|
|
85
|
+
* one way everywhere — the single point where a workspace's substrate availability
|
|
86
|
+
* decides the HITL model.
|
|
87
|
+
*
|
|
88
|
+
* Capture needs a primary workspace AND a substrate to capture into:
|
|
89
|
+
* - a git work tree captures tracked edits from the git diff (no storage needed);
|
|
90
|
+
* - a NON-git workspace captures every write via the path-scoped CAS substrate,
|
|
91
|
+
* which needs artifact storage to persist blobs.
|
|
92
|
+
*
|
|
93
|
+
* So a non-git workspace with NO artifact storage — or no workspace at all — has
|
|
94
|
+
* no capture substrate and falls back to the deny-gate: the agent still runs, but
|
|
95
|
+
* file writes gate pre-execution instead of flowing. In the Cursor harness that
|
|
96
|
+
* fallback is also the ONLY branch that arms the resume-time exact-apply guarantee
|
|
97
|
+
* (see execute-cursor/exact-apply.ts); the deep-agent harness re-applies an
|
|
98
|
+
* approved write via LangGraph checkpoint replay and needs no exact-apply. Keeping
|
|
99
|
+
* this the single, named, truth-table-tested decision pins exactly when the
|
|
100
|
+
* no-storage deny-gate engages in either harness.
|
|
101
|
+
*/
|
|
102
|
+
export function deriveCaptureMode(
|
|
103
|
+
primaryWorkspaceDir: string | undefined,
|
|
104
|
+
gitWorkspace: boolean,
|
|
105
|
+
hasArtifactStorage: boolean,
|
|
106
|
+
): boolean {
|
|
107
|
+
return !!primaryWorkspaceDir && (gitWorkspace || hasArtifactStorage);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Turn start: pin the pre-turn working tree behind the baseline ref and author
|
|
112
|
+
* the BASELINE_CAPTURED event. The projection reads `turn_id`/`harness_id` ONLY
|
|
113
|
+
* from this payload, so `turnId == changeSetId` (one turn = one change set) and
|
|
114
|
+
* `harnessId` identifies the producer. Returns the baseline tree sha for the
|
|
115
|
+
* turn-end diff. Appends onto `status.fileReviewEventStream`; the event rides the
|
|
116
|
+
* next persist.
|
|
117
|
+
*
|
|
118
|
+
* `gitWorkspace=false` (a non-git workspace, DD-21 D2) has no whole-tree baseline
|
|
119
|
+
* to pin — a file's pre-edit bytes are captured per-path by the CAS observer at
|
|
120
|
+
* mutation time, and the reconcile sources them from the durable CAS manifest,
|
|
121
|
+
* not this snapshot. The BASELINE event is still authored (it is the projection's
|
|
122
|
+
* sole source for `turn_id`/`harness_id`), carrying a `CAS_MANIFEST` snapshot with
|
|
123
|
+
* no manifest yet (the manifest is authored at candidate time). Returns "".
|
|
124
|
+
*/
|
|
125
|
+
export async function captureBaselineToLedger(opts: {
|
|
126
|
+
readonly status: AgentExecutionStatus;
|
|
127
|
+
readonly gitRoot: string;
|
|
128
|
+
readonly executionId: string;
|
|
129
|
+
readonly changeSetId: string;
|
|
130
|
+
readonly harnessId: string;
|
|
131
|
+
readonly excludePaths?: readonly string[];
|
|
132
|
+
/** True (default) for a git work tree; false for a CAS-only non-git workspace. */
|
|
133
|
+
readonly gitWorkspace?: boolean;
|
|
134
|
+
}): Promise<string> {
|
|
135
|
+
const { status, gitRoot, executionId, changeSetId, harnessId, excludePaths } = opts;
|
|
136
|
+
const gitWorkspace = opts.gitWorkspace ?? true;
|
|
137
|
+
|
|
138
|
+
if (!gitWorkspace) {
|
|
139
|
+
const event = buildBaselineCapturedEvent(
|
|
140
|
+
changeSetContext(changeSetId, harnessId),
|
|
141
|
+
casManifestSnapshotRef(),
|
|
142
|
+
);
|
|
143
|
+
appendFileReviewEvents(status, executionId, [event]);
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const baselineTree = await snapshotBaseline(gitRoot, executionId, excludePaths);
|
|
148
|
+
const event = buildBaselineCapturedEvent(
|
|
149
|
+
changeSetContext(changeSetId, harnessId),
|
|
150
|
+
gitTreeSnapshotRef(baselineTree, baselineRef(executionId)),
|
|
151
|
+
);
|
|
152
|
+
appendFileReviewEvents(status, executionId, [event]);
|
|
153
|
+
return baselineTree;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Turn end: capture the change set, LEAVE the working tree in its applied
|
|
158
|
+
* ("after") state (the user reviews the real change; nothing is committed and the
|
|
159
|
+
* next turn is blocked until they decide), and author the CANDIDATE_CAPTURED
|
|
160
|
+
* event. The pinned baseline/after refs are the authoritative source for the
|
|
161
|
+
* resume-time reconcile ({@link applyCaptureDecisions}).
|
|
162
|
+
*
|
|
163
|
+
* Presentation (hiding the harness's flowed transcript rows so `file_change_sets`
|
|
164
|
+
* is the single review surface) is the harness adapter's concern, not this seam's.
|
|
165
|
+
* Returns the captured changes. No-op authoring when the turn changed nothing.
|
|
166
|
+
*/
|
|
167
|
+
export async function captureCandidateToLedger(opts: {
|
|
168
|
+
readonly status: AgentExecutionStatus;
|
|
169
|
+
readonly gitRoot: string;
|
|
170
|
+
readonly executionId: string;
|
|
171
|
+
readonly changeSetId: string;
|
|
172
|
+
readonly baselineTree: string;
|
|
173
|
+
readonly harnessId: string;
|
|
174
|
+
readonly excludePaths?: readonly string[];
|
|
175
|
+
/**
|
|
176
|
+
* Ignored / non-git paths captured this turn (before/after bytes the harness
|
|
177
|
+
* recorded at mutation time). When present, they are stored as content-
|
|
178
|
+
* addressed CAS blobs and composed with the git-tracked changes into ONE
|
|
179
|
+
* hybrid change set (design doc 11). Omit for a git-only turn — existing
|
|
180
|
+
* callers are unaffected.
|
|
181
|
+
*/
|
|
182
|
+
readonly casCaptures?: readonly CasPathCapture[];
|
|
183
|
+
/** The CAS blob store; required when {@link casCaptures} is non-empty. */
|
|
184
|
+
readonly storage?: ArtifactStorage;
|
|
185
|
+
/**
|
|
186
|
+
* Gitignored paths whose bytes are deliberately NOT captured — the DD-E secret
|
|
187
|
+
* gate refused them. Each is authored as a content-less `DIFF_UNREVIEWABLE`
|
|
188
|
+
* change entry (`diff_complete=false`, no before/after) so the change set is
|
|
189
|
+
* PARTIAL_BLOCKED (approval blocked) and the path is honestly surfaced, while
|
|
190
|
+
* its content never enters the ledger or storage. Never overlaps `casCaptures`.
|
|
191
|
+
*/
|
|
192
|
+
readonly unreviewablePaths?: readonly string[];
|
|
193
|
+
/**
|
|
194
|
+
* Capture class stamped on the content-less DIFF_UNREVIEWABLE entries — the CAS
|
|
195
|
+
* class of THIS turn's substrate: `GIT_IGNORED_CAPTURED` for a git work tree's
|
|
196
|
+
* ignored paths, `NON_GIT_CAS` for a non-git workspace. Content-addressed
|
|
197
|
+
* `casCaptures` carry their own per-item class; this only labels the blocked
|
|
198
|
+
* (content-less) ones so a non-git blocked secret is not mislabeled "gitignored".
|
|
199
|
+
* Defaults to `GIT_IGNORED_CAPTURED` (git callers are unaffected).
|
|
200
|
+
*/
|
|
201
|
+
readonly unreviewableCaptureClass?: FileCaptureClass;
|
|
202
|
+
/**
|
|
203
|
+
* True (default) for a git work tree — git-tracked edits are diffed from the
|
|
204
|
+
* pinned baseline/after trees and composed with any CAS captures. False for a
|
|
205
|
+
* CAS-only non-git workspace (DD-21 D2): there is no git tree to diff, so the
|
|
206
|
+
* change set is sourced ENTIRELY from `casCaptures` (+ `unreviewablePaths`) and
|
|
207
|
+
* the snapshot is `CAS_MANIFEST`, not `GIT_TREE_REF`/`HYBRID`.
|
|
208
|
+
*/
|
|
209
|
+
readonly gitWorkspace?: boolean;
|
|
210
|
+
/**
|
|
211
|
+
* The harness's approved-command turn facts (DD-28): present only when the
|
|
212
|
+
* turn's sole mutation source was consented shell commands. Carried verbatim
|
|
213
|
+
* on the CANDIDATE event for the backend to verify and — on success — author
|
|
214
|
+
* the policy auto-keep decision. Omitted → the set reviews manually.
|
|
215
|
+
*/
|
|
216
|
+
readonly commandProvenance?: TurnCommandProvenance;
|
|
217
|
+
}): Promise<readonly GitCapturedChange[]> {
|
|
218
|
+
const {
|
|
219
|
+
status, gitRoot, executionId, changeSetId, baselineTree, harnessId, excludePaths,
|
|
220
|
+
casCaptures, storage, unreviewablePaths, commandProvenance,
|
|
221
|
+
} = opts;
|
|
222
|
+
const gitWorkspace = opts.gitWorkspace ?? true;
|
|
223
|
+
const unreviewableCaptureClass =
|
|
224
|
+
opts.unreviewableCaptureClass ?? FileCaptureClass.GIT_IGNORED_CAPTURED;
|
|
225
|
+
|
|
226
|
+
// A non-git workspace has no tree to diff; git-tracked changes are empty and
|
|
227
|
+
// the whole change set comes from the CAS captures below.
|
|
228
|
+
const { afterTree, changes: gitChanges } = gitWorkspace
|
|
229
|
+
? await captureChangeSet(gitRoot, executionId, baselineTree, excludePaths)
|
|
230
|
+
: { afterTree: "", changes: [] as readonly GitCapturedChange[] };
|
|
231
|
+
|
|
232
|
+
// Capture the ignored / non-git deltas into CAS (content-addressed, deduped,
|
|
233
|
+
// offloaded), if the harness supplied any this turn.
|
|
234
|
+
let casFiles: readonly CasCapturedFile[] = [];
|
|
235
|
+
let casRef: CasSnapshotRef | undefined;
|
|
236
|
+
if (casCaptures && casCaptures.length > 0) {
|
|
237
|
+
if (!storage) {
|
|
238
|
+
throw new Error("captureCandidateToLedger: casCaptures requires an ArtifactStorage");
|
|
239
|
+
}
|
|
240
|
+
const snap = await snapshotCasChangeSet({ storage, executionId, changeSetId, captures: casCaptures });
|
|
241
|
+
casFiles = snap.manifest.files;
|
|
242
|
+
if (casFiles.length > 0) casRef = snap.ref;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const unreviewable = unreviewablePaths ?? [];
|
|
246
|
+
|
|
247
|
+
// A turn that changed nothing (no tracked/ignored change and no blocked path)
|
|
248
|
+
// authors no event.
|
|
249
|
+
if (gitChanges.length === 0 && casFiles.length === 0 && unreviewable.length === 0) {
|
|
250
|
+
return gitChanges;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// One combined change set: git-tracked (inline bodies) + CAS (blob refs) +
|
|
254
|
+
// secret-blocked (content-less, DIFF_UNREVIEWABLE). The aggregate digest folds
|
|
255
|
+
// all (buildCandidateCapturedEvent sorts by file digest), so the reviewed diff
|
|
256
|
+
// and its identity span every substrate. deriveDiffCompleteness then rolls the
|
|
257
|
+
// per-file signals up three ways: any content-less (non-binary) entry forces
|
|
258
|
+
// PARTIAL_BLOCKED; a set blocked only by binaries is BINARY_SUMMARY_ONLY
|
|
259
|
+
// (keepable in one acknowledged action); else COMPLETE.
|
|
260
|
+
const captured = [
|
|
261
|
+
...gitChanges.map((c) => buildCapturedFileChange(toCapturedChangeInput(changeSetId, c))),
|
|
262
|
+
...casFiles.map((f) => buildCapturedFileChange(casToCapturedChangeInput(changeSetId, f))),
|
|
263
|
+
...unreviewable.map((p) => buildCapturedFileChange(unreviewableChangeInput(changeSetId, p, unreviewableCaptureClass))),
|
|
264
|
+
];
|
|
265
|
+
// Snapshot kind follows the substrate(s) that captured this turn: CAS-only for
|
|
266
|
+
// a non-git workspace, HYBRID when a git turn also touched ignored/non-git
|
|
267
|
+
// paths, else a plain git tree.
|
|
268
|
+
const snapshot = !gitWorkspace
|
|
269
|
+
? casManifestSnapshotRef(casRef)
|
|
270
|
+
: casRef
|
|
271
|
+
? hybridSnapshotRef(afterTree, captureRef(executionId), casRef)
|
|
272
|
+
: gitTreeSnapshotRef(afterTree, captureRef(executionId));
|
|
273
|
+
const event = buildCandidateCapturedEvent(
|
|
274
|
+
changeSetContext(changeSetId, harnessId),
|
|
275
|
+
snapshot,
|
|
276
|
+
captured,
|
|
277
|
+
commandProvenance,
|
|
278
|
+
);
|
|
279
|
+
appendFileReviewEvents(status, executionId, [event]);
|
|
280
|
+
return gitChanges;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Outcome of reconciling a DECIDED change set's decisions on resume. */
|
|
284
|
+
export interface CaptureResumeResult {
|
|
285
|
+
/** False when this resume is not a capture turn (no capture ref present). */
|
|
286
|
+
readonly isCaptureTurn: boolean;
|
|
287
|
+
/** Repo-relative paths applied (approved) to the working tree. */
|
|
288
|
+
readonly approvedPaths: readonly string[];
|
|
289
|
+
/** Repo-relative paths discarded (rejected / undecided-then-skipped). */
|
|
290
|
+
readonly rejectedPaths: readonly string[];
|
|
291
|
+
/** True when at least one file was rejected. */
|
|
292
|
+
readonly hadReject: boolean;
|
|
293
|
+
/**
|
|
294
|
+
* True when an approved file's on-disk bytes no longer hash to the digest the
|
|
295
|
+
* user approved: a FileReviewFailure(HASH_MISMATCH) was authored and NOTHING
|
|
296
|
+
* was applied (what-you-approve-is-what-applies is a hard gate).
|
|
297
|
+
*/
|
|
298
|
+
readonly failed: boolean;
|
|
299
|
+
/** Human-readable detail when {@link failed} (the diverged paths). */
|
|
300
|
+
readonly failureDetail?: string;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Resume: reconcile the working tree to the change set's DECIDED decisions
|
|
305
|
+
* (projected server-side onto `FileChangeSet.decisions`), sourced entirely from
|
|
306
|
+
* the pinned baseline/after refs — approved files are ensured at their "after"
|
|
307
|
+
* bytes (uncommitted; normally already on disk from the review window, re-asserted
|
|
308
|
+
* idempotently), rejected/undecided files are snapped back to baseline. Then
|
|
309
|
+
* author RECONCILED with the post-reconcile approved snapshot and release the
|
|
310
|
+
* refs. Because both sides are ref-sourced, the result is correct regardless of
|
|
311
|
+
* the tree's current contents (idempotent under a Temporal retry or a tree reset).
|
|
312
|
+
*
|
|
313
|
+
* Scope precedence is runner-owned, most-specific-wins: the CHANGE_SET decision
|
|
314
|
+
* (if any) seeds every file, then per-file FILE decisions override it.
|
|
315
|
+
*
|
|
316
|
+
* Enforcement ("what you approve is what applies"): before keeping an approved
|
|
317
|
+
* file, the recomputed bytes must still hash to the reviewed digest. Any mismatch
|
|
318
|
+
* authors a FileReviewFailure(HASH_MISMATCH) and applies NOTHING.
|
|
319
|
+
*
|
|
320
|
+
* Mutates `status.fileReviewEventStream` in place (authors RECONCILED / FAILED).
|
|
321
|
+
*/
|
|
322
|
+
export async function applyCaptureDecisions(opts: {
|
|
323
|
+
readonly status: AgentExecutionStatus;
|
|
324
|
+
readonly gitRoot: string;
|
|
325
|
+
readonly executionId: string;
|
|
326
|
+
readonly changeSet: FileChangeSet;
|
|
327
|
+
readonly harnessId: string;
|
|
328
|
+
readonly excludePaths?: readonly string[];
|
|
329
|
+
/**
|
|
330
|
+
* The CAS blob reader, used to reconcile ignored / non-git (CAS) files from the
|
|
331
|
+
* durable manifest — approved after-blobs re-written (hash-verified), rejected
|
|
332
|
+
* files snapped back to their before-blobs. Whether the CAS branch runs at all
|
|
333
|
+
* is decided by the change set's CANDIDATE snapshot (a git-only turn carries no
|
|
334
|
+
* CAS ref), NOT by probing storage. Omit for a harness with no artifact storage;
|
|
335
|
+
* a CAS/HYBRID change set then cannot be reconciled.
|
|
336
|
+
*
|
|
337
|
+
* `storage` is accepted so callers can pass the store they derived `readBlob`
|
|
338
|
+
* from (via {@link casBlobReader}); the reconcile itself reads only through
|
|
339
|
+
* `readBlob` and never probes `storage`.
|
|
340
|
+
*/
|
|
341
|
+
readonly storage?: ArtifactStorage;
|
|
342
|
+
readonly readBlob?: BlobReader;
|
|
343
|
+
/**
|
|
344
|
+
* True (default) for a git work tree; false for a CAS-only non-git workspace
|
|
345
|
+
* (DD-21 D2). When false, git refs are never consulted — the reconcile is driven
|
|
346
|
+
* entirely by the durable CAS manifest, and RECONCILED carries a `CAS_MANIFEST`
|
|
347
|
+
* snapshot instead of a re-pinned git tree.
|
|
348
|
+
*/
|
|
349
|
+
readonly gitWorkspace?: boolean;
|
|
350
|
+
}): Promise<CaptureResumeResult> {
|
|
351
|
+
const { status, gitRoot, executionId, changeSet, harnessId, excludePaths, readBlob } = opts;
|
|
352
|
+
const gitWorkspace = opts.gitWorkspace ?? true;
|
|
353
|
+
|
|
354
|
+
// The two substrates are resolved up front and reconciled independently below:
|
|
355
|
+
// git-tracked files from the pinned baseline/after refs, ignored/non-git files
|
|
356
|
+
// from the durable CAS manifest. A resume is a capture turn iff at least one is
|
|
357
|
+
// present; with neither this is an ordinary (non-capture) resume.
|
|
358
|
+
//
|
|
359
|
+
// Whether this turn captured any CAS (ignored / non-git) files is a fact the
|
|
360
|
+
// ledger already records: the change set's CANDIDATE snapshot carries a CAS
|
|
361
|
+
// manifest ref for a CAS/HYBRID turn and none for a git-only turn. We branch on
|
|
362
|
+
// that ledger fact — NOT on probing artifact storage. A git-only turn writes no
|
|
363
|
+
// manifest, and asking storage "does the manifest exist?" is both redundant and
|
|
364
|
+
// unsafe: a presigned/proxy backend answers true for any key, which turned a
|
|
365
|
+
// git-only reconcile into a doomed manifest download and a 404 crash. Single
|
|
366
|
+
// source of truth: derive the substrate from the ledger, never from storage.
|
|
367
|
+
const recomputed = gitWorkspace ? await recomputeChangeSet(gitRoot, executionId) : undefined;
|
|
368
|
+
const casRef = candidateCasRef(changeSet);
|
|
369
|
+
if (casRef && !readBlob) {
|
|
370
|
+
// The ledger says CAS files were captured, but this caller cannot read blobs
|
|
371
|
+
// back. Skipping would leave those files applied with their decisions
|
|
372
|
+
// unenforced — a silent partial reconcile — so refuse loudly instead.
|
|
373
|
+
throw new Error(
|
|
374
|
+
`applyCaptureDecisions: change set '${changeSet.id}' captured CAS files ` +
|
|
375
|
+
`(manifest '${casRef.artifactUri}') but no blob reader was provided`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const manifest = casRef && readBlob ? await loadCasManifest({ readBlob, ref: casRef }) : undefined;
|
|
379
|
+
if (!recomputed && !manifest) {
|
|
380
|
+
return {
|
|
381
|
+
isCaptureTurn: false,
|
|
382
|
+
approvedPaths: [],
|
|
383
|
+
rejectedPaths: [],
|
|
384
|
+
hadReject: false,
|
|
385
|
+
failed: false,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const actionByChangeId = resolveDecisions(changeSet);
|
|
390
|
+
|
|
391
|
+
const approved: GitCapturedChange[] = [];
|
|
392
|
+
const rejected: GitCapturedChange[] = [];
|
|
393
|
+
const approvedPaths: string[] = [];
|
|
394
|
+
const rejectedPaths: string[] = [];
|
|
395
|
+
const mismatches: string[] = [];
|
|
396
|
+
let hadReject = false;
|
|
397
|
+
|
|
398
|
+
for (const change of recomputed?.changes ?? []) {
|
|
399
|
+
const protoChange = changeSet.changes.find(
|
|
400
|
+
(c) => c.id === `${changeSet.id}:${change.path}`,
|
|
401
|
+
);
|
|
402
|
+
const action = protoChange
|
|
403
|
+
? actionByChangeId.get(protoChange.id) ?? FileDecisionAction.UNSPECIFIED
|
|
404
|
+
: FileDecisionAction.UNSPECIFIED;
|
|
405
|
+
|
|
406
|
+
if (action === FileDecisionAction.APPROVE) {
|
|
407
|
+
// Enforcement gate: the bytes we are about to keep must still match what
|
|
408
|
+
// the reviewer approved. A divergence is never silently applied.
|
|
409
|
+
if (protoChange && !digestMatches(change, protoChange)) {
|
|
410
|
+
mismatches.push(change.path);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
approved.push(change);
|
|
414
|
+
approvedPaths.push(change.path);
|
|
415
|
+
} else {
|
|
416
|
+
if (action === FileDecisionAction.REJECT) hadReject = true;
|
|
417
|
+
rejected.push(change);
|
|
418
|
+
rejectedPaths.push(change.path);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (mismatches.length > 0) {
|
|
423
|
+
const detail = `on-disk content diverged from the approved digest for: ${mismatches.join(", ")}`;
|
|
424
|
+
appendFileReviewEvents(status, executionId, [
|
|
425
|
+
buildFailedEvent(
|
|
426
|
+
changeSetContext(changeSet.id, harnessId),
|
|
427
|
+
FileReviewFailureKind.HASH_MISMATCH,
|
|
428
|
+
detail,
|
|
429
|
+
),
|
|
430
|
+
]);
|
|
431
|
+
// Refuse to apply anything: a partial apply under a verification failure is
|
|
432
|
+
// worse than none. The refs are left in place for diagnosis.
|
|
433
|
+
return {
|
|
434
|
+
isCaptureTurn: true,
|
|
435
|
+
approvedPaths: [],
|
|
436
|
+
rejectedPaths: [],
|
|
437
|
+
hadReject,
|
|
438
|
+
failed: true,
|
|
439
|
+
failureDetail: detail,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Reconcile git-tracked files from the authoritative refs: approved files keep
|
|
444
|
+
// (re-assert) their "after" bytes; rejected/undecided files snap back to baseline.
|
|
445
|
+
if (recomputed) {
|
|
446
|
+
await applyApprovedPaths(gitRoot, recomputed.afterTree, approved);
|
|
447
|
+
await restoreToBaseline(gitRoot, recomputed.baselineTree, rejected);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Reconcile CAS-captured (ignored / non-git) files from the durable manifest,
|
|
451
|
+
// sourced entirely from artifact storage (the CAS analogue of the git refs).
|
|
452
|
+
// Same decision map, keyed by the per-file change id, so the resolution rule is
|
|
453
|
+
// identical across substrates. For a non-git workspace this is the ONLY reconcile.
|
|
454
|
+
if (manifest) {
|
|
455
|
+
const casApproved: CasCapturedFile[] = [];
|
|
456
|
+
const casRejected: CasCapturedFile[] = [];
|
|
457
|
+
for (const file of manifest.files) {
|
|
458
|
+
const path = file.pathAfter || file.pathBefore;
|
|
459
|
+
const action = actionByChangeId.get(`${changeSet.id}:${path}`) ?? FileDecisionAction.UNSPECIFIED;
|
|
460
|
+
if (action === FileDecisionAction.APPROVE) {
|
|
461
|
+
casApproved.push(file);
|
|
462
|
+
approvedPaths.push(path);
|
|
463
|
+
} else {
|
|
464
|
+
if (action === FileDecisionAction.REJECT) hadReject = true;
|
|
465
|
+
casRejected.push(file);
|
|
466
|
+
rejectedPaths.push(path);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
await applyCasApproved({ readBlob: readBlob!, workspaceRoot: gitRoot, files: casApproved });
|
|
470
|
+
await restoreCasToBaseline({ readBlob: readBlob!, workspaceRoot: gitRoot, files: casRejected });
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Author RECONCILED carrying the exact post-reconcile snapshot. A git turn
|
|
474
|
+
// re-pins the approved working tree; a CAS-only turn references the durable
|
|
475
|
+
// manifest (the reconciled state is fully derivable from manifest + decisions,
|
|
476
|
+
// and there is no tree to pin).
|
|
477
|
+
let approvedSnapshot: SnapshotRef;
|
|
478
|
+
if (recomputed) {
|
|
479
|
+
const snap = await snapshotApproved(gitRoot, executionId, excludePaths);
|
|
480
|
+
approvedSnapshot = gitTreeSnapshotRef(snap.treeOid, snap.ref);
|
|
481
|
+
} else {
|
|
482
|
+
approvedSnapshot = casManifestSnapshotRef(candidateCasRef(changeSet));
|
|
483
|
+
}
|
|
484
|
+
appendFileReviewEvents(status, executionId, [
|
|
485
|
+
buildReconciledEvent(changeSetContext(changeSet.id, harnessId), approvedSnapshot),
|
|
486
|
+
]);
|
|
487
|
+
|
|
488
|
+
if (recomputed) await dropCaptureRefs(gitRoot, executionId);
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
isCaptureTurn: true,
|
|
492
|
+
approvedPaths,
|
|
493
|
+
rejectedPaths,
|
|
494
|
+
hadReject,
|
|
495
|
+
failed: false,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
// Internals
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Resolve each file's effective decision (most-specific-wins, runner-owned):
|
|
505
|
+
* seed every change with the CHANGE_SET decision (if present), then override per
|
|
506
|
+
* file with FILE decisions. Returns change-id -> action.
|
|
507
|
+
*/
|
|
508
|
+
function resolveDecisions(
|
|
509
|
+
changeSet: FileChangeSet,
|
|
510
|
+
): Map<string, FileDecisionAction> {
|
|
511
|
+
const byChangeId = new Map<string, FileDecisionAction>();
|
|
512
|
+
const changeSetDecision = changeSet.decisions.find(
|
|
513
|
+
(d) => d.scope === FileDecisionScope.CHANGE_SET,
|
|
514
|
+
);
|
|
515
|
+
if (changeSetDecision) {
|
|
516
|
+
for (const change of changeSet.changes) {
|
|
517
|
+
byChangeId.set(change.id, changeSetDecision.action);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
for (const decision of changeSet.decisions) {
|
|
521
|
+
if (decision.scope === FileDecisionScope.FILE && decision.fileChangeId) {
|
|
522
|
+
byChangeId.set(decision.fileChangeId, decision.action);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return byChangeId;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Verify the recomputed content still hashes to the digests the reviewer
|
|
530
|
+
* approved. The "after" side is checked for CREATE/MODIFY (the bytes we keep);
|
|
531
|
+
* the "before" side for MODIFY/DELETE (the baseline we restore from / remove).
|
|
532
|
+
*
|
|
533
|
+
* The sha is taken via the shared {@link contentSha256} over the recomputed
|
|
534
|
+
* {@link CapturedContent} — the SAME function that produced the persisted
|
|
535
|
+
* `before_sha256`/`after_sha256` at capture — so it is byte-true for binary and
|
|
536
|
+
* text alike and matches by construction when the underlying bytes are unchanged.
|
|
537
|
+
* (Re-hashing an inline body string here would be lossy for binaries and would
|
|
538
|
+
* spuriously fail an approved binary, since a binary side carries no body.)
|
|
539
|
+
*/
|
|
540
|
+
function digestMatches(
|
|
541
|
+
gitChange: GitCapturedChange,
|
|
542
|
+
protoChange: CapturedFileChange,
|
|
543
|
+
): boolean {
|
|
544
|
+
if (gitChange.changeType !== FileChangeType.DELETE) {
|
|
545
|
+
if (!gitChange.after || contentSha256(gitChange.after) !== protoChange.afterSha256) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (gitChange.changeType !== FileChangeType.CREATE) {
|
|
550
|
+
if (!gitChange.before || contentSha256(gitChange.before) !== protoChange.beforeSha256) {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** The shared change-set context every event for this turn carries. */
|
|
558
|
+
function changeSetContext(changeSetId: string, harnessId: string): ChangeSetContext {
|
|
559
|
+
return {
|
|
560
|
+
changeSetId,
|
|
561
|
+
turnId: changeSetId,
|
|
562
|
+
harnessId,
|
|
563
|
+
timestamp: utcTimestamp(),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Build a git-tree {@link SnapshotRef} for the ledger from a pinned ref. */
|
|
568
|
+
function gitTreeSnapshotRef(treeOid: string, ref: string): SnapshotRef {
|
|
569
|
+
return create(SnapshotRefSchema, {
|
|
570
|
+
kind: SnapshotKind.GIT_TREE_REF,
|
|
571
|
+
git: create(GitTreeRefSchema, { treeOid, ref }),
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Build a HYBRID {@link SnapshotRef} composing the git after-tree with the CAS
|
|
577
|
+
* manifest — the shape for a turn that touched both git-tracked and ignored /
|
|
578
|
+
* non-git paths (design doc 06 D3).
|
|
579
|
+
*/
|
|
580
|
+
function hybridSnapshotRef(treeOid: string, ref: string, cas: CasSnapshotRef): SnapshotRef {
|
|
581
|
+
return create(SnapshotRefSchema, {
|
|
582
|
+
kind: SnapshotKind.HYBRID,
|
|
583
|
+
git: create(GitTreeRefSchema, { treeOid, ref }),
|
|
584
|
+
cas: create(CasManifestRefSchema, {
|
|
585
|
+
manifestDigest: cas.manifestDigest,
|
|
586
|
+
artifactUri: cas.artifactUri,
|
|
587
|
+
}),
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Build a CAS-only {@link SnapshotRef} — the shape for a non-git workspace, whose
|
|
593
|
+
* every captured path lives in the content-addressed manifest and no git tree
|
|
594
|
+
* exists (DD-21 D2). `cas` is absent for the BASELINE placeholder (the manifest is
|
|
595
|
+
* authored at candidate time); present for CANDIDATE/RECONCILED once it exists.
|
|
596
|
+
*/
|
|
597
|
+
function casManifestSnapshotRef(cas?: CasSnapshotRef): SnapshotRef {
|
|
598
|
+
return create(SnapshotRefSchema, {
|
|
599
|
+
kind: SnapshotKind.CAS_MANIFEST,
|
|
600
|
+
cas: cas
|
|
601
|
+
? create(CasManifestRefSchema, {
|
|
602
|
+
manifestDigest: cas.manifestDigest,
|
|
603
|
+
artifactUri: cas.artifactUri,
|
|
604
|
+
})
|
|
605
|
+
: undefined,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* The CAS manifest reference a change set was captured against, read from its
|
|
611
|
+
* CANDIDATE snapshot — the durable record a CAS-only RECONCILED event points back
|
|
612
|
+
* at (the reconciled state is manifest + decisions; there is no tree to re-pin).
|
|
613
|
+
* Undefined when the candidate carried no CAS manifest.
|
|
614
|
+
*/
|
|
615
|
+
function candidateCasRef(changeSet: FileChangeSet): CasSnapshotRef | undefined {
|
|
616
|
+
const cas = changeSet.candidateSnapshot?.cas;
|
|
617
|
+
return cas ? { manifestDigest: cas.manifestDigest, artifactUri: cas.artifactUri } : undefined;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Map a CAS-captured file to the harness-agnostic producer input. The before/
|
|
622
|
+
* after bodies are carried as blob REFS (already offloaded to artifact storage
|
|
623
|
+
* by the CAS substrate), never re-inlined — so the CANDIDATE event stays small
|
|
624
|
+
* and the bytes are stored exactly once. Enforcement digests come from the blob
|
|
625
|
+
* content addresses, identical in form to the git path.
|
|
626
|
+
*/
|
|
627
|
+
function casToCapturedChangeInput(
|
|
628
|
+
changeSetId: string,
|
|
629
|
+
file: CasCapturedFile,
|
|
630
|
+
): CapturedChangeInput {
|
|
631
|
+
return {
|
|
632
|
+
id: `${changeSetId}:${file.pathAfter || file.pathBefore}`,
|
|
633
|
+
pathBefore: file.pathBefore,
|
|
634
|
+
pathAfter: file.pathAfter,
|
|
635
|
+
kind: file.kind,
|
|
636
|
+
captureClass: file.captureClass,
|
|
637
|
+
before: file.before
|
|
638
|
+
? {
|
|
639
|
+
kind: "ref",
|
|
640
|
+
sha256: file.before.sha256,
|
|
641
|
+
storageKey: file.before.storageKey,
|
|
642
|
+
sizeBytes: file.before.sizeBytes,
|
|
643
|
+
isBinary: file.before.isBinary,
|
|
644
|
+
}
|
|
645
|
+
: undefined,
|
|
646
|
+
after: file.after
|
|
647
|
+
? {
|
|
648
|
+
kind: "ref",
|
|
649
|
+
sha256: file.after.sha256,
|
|
650
|
+
storageKey: file.after.storageKey,
|
|
651
|
+
sizeBytes: file.after.sizeBytes,
|
|
652
|
+
isBinary: file.after.isBinary,
|
|
653
|
+
}
|
|
654
|
+
: undefined,
|
|
655
|
+
diffComplete: file.diffComplete,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Map a secret-blocked gitignored path to a content-less producer input (design
|
|
661
|
+
* doc 12, DD-E). The bytes are deliberately never captured, so both sides are
|
|
662
|
+
* absent (empty enforcement digests) and `diffComplete=false`. Being non-binary
|
|
663
|
+
* incomplete with no keepable bytes, it forces the change set to PARTIAL_BLOCKED
|
|
664
|
+
* (never BINARY_SUMMARY_ONLY) — approval is blocked and the path is surfaced
|
|
665
|
+
* honestly, while its CONTENT never enters the ledger or storage. `blockedReason` records
|
|
666
|
+
* the honest cause (SECRET_WITHHELD) so the review UI can say *why* rather than
|
|
667
|
+
* showing a cause-agnostic "unavailable" (doc 15). Kind is MODIFY: the write was
|
|
668
|
+
* blocked before it ran, so create-vs-modify is unknown and irrelevant (nothing
|
|
669
|
+
* is ever applied or reconciled for this entry). `captureClass` is the turn's CAS
|
|
670
|
+
* substrate class (GIT_IGNORED_CAPTURED | NON_GIT_CAS) so the blocked path is
|
|
671
|
+
* labeled with its true provenance.
|
|
672
|
+
*/
|
|
673
|
+
function unreviewableChangeInput(
|
|
674
|
+
changeSetId: string,
|
|
675
|
+
path: string,
|
|
676
|
+
captureClass: FileCaptureClass,
|
|
677
|
+
): CapturedChangeInput {
|
|
678
|
+
return {
|
|
679
|
+
id: `${changeSetId}:${path}`,
|
|
680
|
+
pathBefore: path,
|
|
681
|
+
pathAfter: path,
|
|
682
|
+
kind: FileChangeKind.MODIFY,
|
|
683
|
+
captureClass,
|
|
684
|
+
diffComplete: false,
|
|
685
|
+
blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** Map the git capture kind to the file-review {@link FileChangeKind}. */
|
|
690
|
+
function toFileChangeKind(changeType: FileChangeType): FileChangeKind {
|
|
691
|
+
switch (changeType) {
|
|
692
|
+
case FileChangeType.CREATE:
|
|
693
|
+
return FileChangeKind.ADD;
|
|
694
|
+
case FileChangeType.DELETE:
|
|
695
|
+
return FileChangeKind.DELETE;
|
|
696
|
+
default:
|
|
697
|
+
return FileChangeKind.MODIFY;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Map a git-derived capture (`--no-renames`, so ADD/MODIFY/DELETE) to the
|
|
703
|
+
* harness-agnostic producer input. The before/after content is the
|
|
704
|
+
* {@link CapturedContent} the substrate derived from the exact blob bytes; a
|
|
705
|
+
* binary side (carried as a content address, no body) marks the file incomplete
|
|
706
|
+
* so the change set cannot be approved as a complete text diff.
|
|
707
|
+
*/
|
|
708
|
+
function toCapturedChangeInput(
|
|
709
|
+
changeSetId: string,
|
|
710
|
+
change: GitCapturedChange,
|
|
711
|
+
): CapturedChangeInput {
|
|
712
|
+
const isCreate = change.changeType === FileChangeType.CREATE;
|
|
713
|
+
const isDelete = change.changeType === FileChangeType.DELETE;
|
|
714
|
+
const pathBefore = isCreate ? "" : change.path;
|
|
715
|
+
const pathAfter = isDelete ? "" : change.path;
|
|
716
|
+
const binary = change.before?.kind === "binary" || change.after?.kind === "binary";
|
|
717
|
+
return {
|
|
718
|
+
id: `${changeSetId}:${pathAfter || pathBefore}`,
|
|
719
|
+
pathBefore,
|
|
720
|
+
pathAfter,
|
|
721
|
+
kind: toFileChangeKind(change.changeType),
|
|
722
|
+
captureClass: FileCaptureClass.GIT_TRACKED,
|
|
723
|
+
before: change.before,
|
|
724
|
+
after: change.after,
|
|
725
|
+
diffComplete: !binary,
|
|
726
|
+
};
|
|
727
|
+
}
|