@stigmer/runner 3.0.9-dev.20260616060535 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +10 -0
- package/dist/activities/execute-deep-agent/hitl.js +5 -1
- 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 +370 -56
- 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 +11 -5
- package/dist/activities/execute-deep-agent/status-builder.js +6 -2
- 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 +6 -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 +2 -5
- 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 +12 -11
- 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 +15 -1
- package/src/activities/execute-deep-agent/index.ts +434 -64
- 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 +19 -7
- 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 +8 -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,555 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git snapshot/restore substrate for the apply-then-review HITL "capture mode".
|
|
3
|
+
*
|
|
4
|
+
* THE MODEL
|
|
5
|
+
* ---------
|
|
6
|
+
* Edits are applied to the working tree and reviewed AFTER the fact; rejecting a
|
|
7
|
+
* file snaps it back exactly. We use git as a NO-COMMIT snapshot/restore
|
|
8
|
+
* substrate at the turn boundary:
|
|
9
|
+
*
|
|
10
|
+
* 1. snapshotBaseline() at turn start — record the exact pre-turn working tree
|
|
11
|
+
* and pin it behind a ref so it survives a multi-day approval wait.
|
|
12
|
+
* 2. The agent edits files freely (the gate allows git-tracked write/edit/delete).
|
|
13
|
+
* 3. captureChangeSet() at turn end — diff the post-turn tree against the
|
|
14
|
+
* baseline into a per-file change set, and pin the post-turn ("after") tree
|
|
15
|
+
* behind a ref. The working tree is LEFT in its "after" state: the user
|
|
16
|
+
* reviews the real, applied change and the workspace browser shows it.
|
|
17
|
+
* 4. On resume, reconcile the working tree to the per-file decisions, sourced
|
|
18
|
+
* ENTIRELY from the two pinned refs (not the live tree, not the persisted
|
|
19
|
+
* bodies — the refs are the single source of truth):
|
|
20
|
+
* - approved -> applyApprovedPaths() ensures the "after" bytes.
|
|
21
|
+
* - rejected/undecided -> restoreToBaseline() snaps the file back to its
|
|
22
|
+
* exact pre-turn bytes.
|
|
23
|
+
* This is symmetric and idempotent: it converges to the correct end state
|
|
24
|
+
* regardless of the tree's current contents, so a Temporal retry or a tree
|
|
25
|
+
* reset is harmless.
|
|
26
|
+
* 5. dropCaptureRefs() releases the pinned objects.
|
|
27
|
+
*
|
|
28
|
+
* HARNESS-AGNOSTIC
|
|
29
|
+
* ----------------
|
|
30
|
+
* This is pure git plumbing keyed by `executionId`. It carries no Cursor- or
|
|
31
|
+
* deep-agent-specific knowledge: the runner-owned files a harness writes into the
|
|
32
|
+
* workspace (e.g. the Cursor gate's `.cursor/hooks.json`) are passed in as
|
|
33
|
+
* `excludePaths` so they never pollute the captured diff, rather than being
|
|
34
|
+
* hard-coded here. Both harnesses author IDENTICAL ledger entries through the
|
|
35
|
+
* shared producer (events.ts) on top of this substrate. A future CAS substrate
|
|
36
|
+
* (for ignored / non-git paths) sits beside this module.
|
|
37
|
+
*
|
|
38
|
+
* NOTHING IS PERMANENT UNTIL APPROVAL
|
|
39
|
+
* -----------------------------------
|
|
40
|
+
* Edits are visible during review, but nothing is committed and the next turn is
|
|
41
|
+
* blocked until the user decides; a reject snaps the file back byte-for-byte from
|
|
42
|
+
* the baseline ref. Shell/MCP and gitignored writes/deletes are NOT reversible by
|
|
43
|
+
* this substrate, so they stay on the approve-before-run gate (see each harness's
|
|
44
|
+
* gate: the Cursor hook and the deep-agent approval-gate middleware).
|
|
45
|
+
*
|
|
46
|
+
* WHY GIT, NO COMMITS
|
|
47
|
+
* -------------------
|
|
48
|
+
* Git is used ONLY as an exact, reversible capture/restore mechanism. We never
|
|
49
|
+
* move HEAD, never touch a branch, never push. Approved work stays uncommitted;
|
|
50
|
+
* a harness's own writeback path (if any) commits the reconciled tree afterwards.
|
|
51
|
+
*
|
|
52
|
+
* HARD INVARIANTS
|
|
53
|
+
* ---------------
|
|
54
|
+
* - NEVER `git clean -x`/`-a` or `git stash -a`: the git-excluded `.stigmer/`
|
|
55
|
+
* directory holds SDK resume state and MUST survive. `git clean -fd` (no `-x`)
|
|
56
|
+
* leaves ignored paths alone, so `.stigmer`/`lost+found` are safe.
|
|
57
|
+
* - Runner-owned files written into the workspace are EXCLUDED (via `excludePaths`)
|
|
58
|
+
* from both the baseline and the after tree, so they never pollute the captured
|
|
59
|
+
* diff regardless of gate-teardown ordering.
|
|
60
|
+
* - All staging uses a TEMP index (GIT_INDEX_FILE) so the repo's real index is
|
|
61
|
+
* never disturbed.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
import { execFile } from "node:child_process";
|
|
65
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
66
|
+
import { dirname, join } from "node:path";
|
|
67
|
+
import { promisify } from "node:util";
|
|
68
|
+
import { FileChangeType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
69
|
+
import { bytesLookBinary } from "../file-change.js";
|
|
70
|
+
import { sha256Bytes } from "./digest.js";
|
|
71
|
+
import type { CapturedContent } from "./events.js";
|
|
72
|
+
|
|
73
|
+
const execFileAsync = promisify(execFile);
|
|
74
|
+
|
|
75
|
+
/** Build `:(exclude)` pathspecs for the runner-owned files a harness passes in. */
|
|
76
|
+
function excludePathspecs(excludePaths: readonly string[]): string[] {
|
|
77
|
+
return excludePaths.map((p) => `:(exclude)${p}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Refs that pin the per-execution baseline and after trees against GC. Exported
|
|
82
|
+
* so the file-review producer can stamp them onto the ledger's SnapshotRef
|
|
83
|
+
* (GitTreeRef.ref) without re-deriving the format. Keyed by executionId only:
|
|
84
|
+
* the unified gate guarantees at most one un-reconciled change set per execution
|
|
85
|
+
* at a time, and the reconcile drops these refs before the next turn re-pins.
|
|
86
|
+
*/
|
|
87
|
+
export function baselineRef(executionId: string): string {
|
|
88
|
+
return `refs/stigmer/baseline/${executionId}`;
|
|
89
|
+
}
|
|
90
|
+
export function captureRef(executionId: string): string {
|
|
91
|
+
return `refs/stigmer/capture/${executionId}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A non-interactive identity for the snapshot commit objects. These commits are
|
|
96
|
+
* never on a branch, never pushed, and exist only to anchor a tree against
|
|
97
|
+
* garbage collection — but `git commit-tree` still requires an author/committer,
|
|
98
|
+
* and the repo may have no user.name/email configured. Supplied via env so we
|
|
99
|
+
* never mutate the repo's config.
|
|
100
|
+
*/
|
|
101
|
+
const SNAPSHOT_IDENTITY_ENV: Record<string, string> = {
|
|
102
|
+
GIT_AUTHOR_NAME: "stigmer-runner",
|
|
103
|
+
GIT_AUTHOR_EMAIL: "runner@stigmer.local",
|
|
104
|
+
GIT_COMMITTER_NAME: "stigmer-runner",
|
|
105
|
+
GIT_COMMITTER_EMAIL: "runner@stigmer.local",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A single file mutation captured from the git diff between the baseline and
|
|
110
|
+
* after trees. `path` is repo-relative (forward slashes); `changeType` drives
|
|
111
|
+
* restore/apply. `before`/`after` are the harness-agnostic {@link CapturedContent}
|
|
112
|
+
* for each side (absent per changeType: no `before` for a CREATE, no `after` for
|
|
113
|
+
* a DELETE) — text is carried inline, binary carries only its byte-true content
|
|
114
|
+
* address (its bytes reconcile byte-exact from the git ref, never the wire).
|
|
115
|
+
*/
|
|
116
|
+
export interface GitSubstrateChange {
|
|
117
|
+
/** Repo-relative path (the diff's path; the destination for a create/modify). */
|
|
118
|
+
readonly path: string;
|
|
119
|
+
readonly changeType: FileChangeType;
|
|
120
|
+
/** Pre-edit content (absent for a CREATE). */
|
|
121
|
+
readonly before?: CapturedContent;
|
|
122
|
+
/** Post-edit content (absent for a DELETE). */
|
|
123
|
+
readonly after?: CapturedContent;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Result of {@link captureChangeSet} / {@link recomputeChangeSet}. */
|
|
127
|
+
export interface CaptureResult {
|
|
128
|
+
/**
|
|
129
|
+
* The pre-turn tree sha (pinned behind the baseline ref). On resume this is
|
|
130
|
+
* the authoritative source for reverting rejected/undecided files exactly.
|
|
131
|
+
*/
|
|
132
|
+
readonly baselineTree: string;
|
|
133
|
+
/** The post-turn tree sha, also pinned behind the capture ref. */
|
|
134
|
+
readonly afterTree: string;
|
|
135
|
+
/** One entry per changed file (excluding the passed runner-owned paths). */
|
|
136
|
+
readonly changes: readonly GitSubstrateChange[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Run a git command in `gitRoot`. Returns stdout as a UTF-8 string. Binary
|
|
141
|
+
* output (file bodies) must use {@link gitBuffer} instead so bytes are exact.
|
|
142
|
+
*/
|
|
143
|
+
async function git(
|
|
144
|
+
gitRoot: string,
|
|
145
|
+
args: string[],
|
|
146
|
+
env?: Record<string, string>,
|
|
147
|
+
): Promise<string> {
|
|
148
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
149
|
+
cwd: gitRoot,
|
|
150
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
151
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
152
|
+
encoding: "utf-8",
|
|
153
|
+
});
|
|
154
|
+
return stdout;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Run a git command returning raw stdout BYTES (for exact file-body reads). */
|
|
158
|
+
async function gitBuffer(gitRoot: string, args: string[]): Promise<Buffer> {
|
|
159
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
160
|
+
cwd: gitRoot,
|
|
161
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
162
|
+
encoding: "buffer",
|
|
163
|
+
});
|
|
164
|
+
return stdout as unknown as Buffer;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Whether `dir` is inside a git work tree (the capability switch: a real git
|
|
169
|
+
* workspace gets capture mode, anything else keeps the deny-gate fallback).
|
|
170
|
+
*/
|
|
171
|
+
export async function isGitWorkTree(dir: string): Promise<boolean> {
|
|
172
|
+
try {
|
|
173
|
+
const out = await git(dir, ["rev-parse", "--is-inside-work-tree"]);
|
|
174
|
+
return out.trim() === "true";
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Whether a repo-relative path is capturable by this substrate — i.e. it would
|
|
182
|
+
* appear in the `git add -A` snapshot. A gitignored path is NOT capturable: the
|
|
183
|
+
* snapshot cannot see it, so it can be neither reviewed nor reverted, and a
|
|
184
|
+
* capture-mode gate must keep gating it. Mirrors the Cursor hook's
|
|
185
|
+
* `__stigmer_is_gitignored` check: `git check-ignore -q` exits 0 when the path IS
|
|
186
|
+
* ignored, non-zero otherwise; a non-git context / error is treated as
|
|
187
|
+
* not-ignored (capturable), matching the hook's allow-on-error behavior.
|
|
188
|
+
*/
|
|
189
|
+
export async function isPathCapturable(gitRoot: string, path: string): Promise<boolean> {
|
|
190
|
+
if (!path) return false;
|
|
191
|
+
try {
|
|
192
|
+
await git(gitRoot, ["check-ignore", "-q", "--", path]);
|
|
193
|
+
return false; // exit 0 -> ignored -> not capturable
|
|
194
|
+
} catch {
|
|
195
|
+
return true; // exit 1 (not ignored) or error -> capturable
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Whether the repo has a resolvable HEAD (false for a brand-new empty repo). */
|
|
200
|
+
async function headExists(gitRoot: string): Promise<boolean> {
|
|
201
|
+
try {
|
|
202
|
+
await git(gitRoot, ["rev-parse", "--verify", "--quiet", "HEAD"]);
|
|
203
|
+
return true;
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Absolute path of the repo's `.git` dir (for temp index placement). */
|
|
210
|
+
async function resolveGitDir(gitRoot: string): Promise<string> {
|
|
211
|
+
const out = (await git(gitRoot, ["rev-parse", "--absolute-git-dir"])).trim();
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Stage the entire working tree (tracked + untracked, honoring `.gitignore` /
|
|
217
|
+
* `.git/info/exclude`, EXCLUDING the passed runner-owned paths) into a TEMP index
|
|
218
|
+
* and write it out as a tree object. The real index is never touched.
|
|
219
|
+
*
|
|
220
|
+
* Using a temp index with `git add -A` records every present file as the tree's
|
|
221
|
+
* content; files absent from the working tree are simply absent from the tree,
|
|
222
|
+
* so the result is an exact snapshot of "what files exist right now and their
|
|
223
|
+
* bytes". `.stigmer` and other ignored paths are never staged.
|
|
224
|
+
*/
|
|
225
|
+
async function writeWorkingTree(
|
|
226
|
+
gitRoot: string,
|
|
227
|
+
gitDir: string,
|
|
228
|
+
label: string,
|
|
229
|
+
executionId: string,
|
|
230
|
+
excludePaths: readonly string[],
|
|
231
|
+
): Promise<string> {
|
|
232
|
+
const tmpIndex = join(gitDir, `stigmer-index-${label}-${executionId}`);
|
|
233
|
+
try {
|
|
234
|
+
await rm(tmpIndex, { force: true });
|
|
235
|
+
const env = { GIT_INDEX_FILE: tmpIndex };
|
|
236
|
+
await git(gitRoot, ["add", "-A", "--", ".", ...excludePathspecs(excludePaths)], env);
|
|
237
|
+
return (await git(gitRoot, ["write-tree"], env)).trim();
|
|
238
|
+
} finally {
|
|
239
|
+
await rm(tmpIndex, { force: true });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Pin `tree` behind `ref` as a parent-less-or-HEAD-parented commit (anti-GC). */
|
|
244
|
+
async function pinTree(
|
|
245
|
+
gitRoot: string,
|
|
246
|
+
ref: string,
|
|
247
|
+
tree: string,
|
|
248
|
+
message: string,
|
|
249
|
+
): Promise<void> {
|
|
250
|
+
const args = ["commit-tree", tree, "-m", message];
|
|
251
|
+
if (await headExists(gitRoot)) {
|
|
252
|
+
args.push("-p", "HEAD");
|
|
253
|
+
}
|
|
254
|
+
const commit = (await git(gitRoot, args, SNAPSHOT_IDENTITY_ENV)).trim();
|
|
255
|
+
await git(gitRoot, ["update-ref", ref, commit]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Snapshot the working tree at turn start and pin it behind the baseline ref.
|
|
260
|
+
* Returns the baseline tree sha (used by {@link captureChangeSet} and
|
|
261
|
+
* {@link restoreToBaseline} later in the SAME activity invocation). `excludePaths`
|
|
262
|
+
* are the runner-owned files the harness writes into the workspace (omit for none).
|
|
263
|
+
*/
|
|
264
|
+
export async function snapshotBaseline(
|
|
265
|
+
gitRoot: string,
|
|
266
|
+
executionId: string,
|
|
267
|
+
excludePaths: readonly string[] = [],
|
|
268
|
+
): Promise<string> {
|
|
269
|
+
const gitDir = await resolveGitDir(gitRoot);
|
|
270
|
+
const tree = await writeWorkingTree(gitRoot, gitDir, "baseline", executionId, excludePaths);
|
|
271
|
+
await pinTree(gitRoot, baselineRef(executionId), tree, `stigmer baseline ${executionId}`);
|
|
272
|
+
return tree;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Capture the change set the turn produced: write the post-turn working tree,
|
|
277
|
+
* pin it behind the capture ref (so it survives the approval wait), and diff it
|
|
278
|
+
* against `baselineTree` into a per-file {@link GitSubstrateChange} list.
|
|
279
|
+
*
|
|
280
|
+
* Renames are intentionally NOT detected (`--no-renames`): a rename surfaces as
|
|
281
|
+
* a delete + create, which restores and applies exactly and matches the per-file
|
|
282
|
+
* review model.
|
|
283
|
+
*/
|
|
284
|
+
export async function captureChangeSet(
|
|
285
|
+
gitRoot: string,
|
|
286
|
+
executionId: string,
|
|
287
|
+
baselineTree: string,
|
|
288
|
+
excludePaths: readonly string[] = [],
|
|
289
|
+
): Promise<CaptureResult> {
|
|
290
|
+
const gitDir = await resolveGitDir(gitRoot);
|
|
291
|
+
const afterTree = await writeWorkingTree(gitRoot, gitDir, "capture", executionId, excludePaths);
|
|
292
|
+
await pinTree(gitRoot, captureRef(executionId), afterTree, `stigmer capture ${executionId}`);
|
|
293
|
+
|
|
294
|
+
const raw = await git(gitRoot, [
|
|
295
|
+
"diff",
|
|
296
|
+
"--no-renames",
|
|
297
|
+
"--name-status",
|
|
298
|
+
"-z",
|
|
299
|
+
baselineTree,
|
|
300
|
+
afterTree,
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
const changes: GitSubstrateChange[] = [];
|
|
304
|
+
for (const { status, path } of parseNameStatusZ(raw)) {
|
|
305
|
+
const change = await buildCapturedChange(gitRoot, baselineTree, afterTree, status, path);
|
|
306
|
+
if (change) changes.push(change);
|
|
307
|
+
}
|
|
308
|
+
return { baselineTree, afterTree, changes };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Restore the given files to their `baselineTree` (pre-turn) bytes. On resume
|
|
313
|
+
* this reverts the REJECTED/undecided subset so a discarded change snaps back
|
|
314
|
+
* exactly; it is the generic "ensure baseline bytes" primitive. Surgical
|
|
315
|
+
* (bounded by the passed change set): an agent-created file is removed; an
|
|
316
|
+
* agent-modified or agent-deleted file is rewritten with its baseline bytes.
|
|
317
|
+
* `.stigmer` and other ignored paths are never touched (they are not in the
|
|
318
|
+
* change set). Idempotent — safe to re-run (e.g. on a Temporal retry).
|
|
319
|
+
*/
|
|
320
|
+
export async function restoreToBaseline(
|
|
321
|
+
gitRoot: string,
|
|
322
|
+
baselineTree: string,
|
|
323
|
+
changes: readonly GitSubstrateChange[],
|
|
324
|
+
): Promise<void> {
|
|
325
|
+
for (const change of changes) {
|
|
326
|
+
const abs = join(gitRoot, change.path);
|
|
327
|
+
if (change.changeType === FileChangeType.CREATE) {
|
|
328
|
+
await rm(abs, { force: true });
|
|
329
|
+
} else {
|
|
330
|
+
// MODIFY or DELETE: the baseline holds the file — restore its exact bytes.
|
|
331
|
+
await writeBlobToDisk(gitRoot, baselineTree, change.path, abs);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Ensure the APPROVED files hold their captured "after" bytes, as uncommitted
|
|
338
|
+
* working-tree changes (no commit). In capture mode the agent's edits are left
|
|
339
|
+
* applied through the review window, so on resume this is normally an idempotent
|
|
340
|
+
* re-assert of bytes already on disk; sourcing from the pinned "after" tree also
|
|
341
|
+
* makes it self-healing if the tree was reset (Temporal retry / sandbox
|
|
342
|
+
* recycle). An approved CREATE/MODIFY writes the after bytes; an approved DELETE
|
|
343
|
+
* removes the file. Rejected/undecided paths are not passed in (they go to
|
|
344
|
+
* {@link restoreToBaseline}).
|
|
345
|
+
*/
|
|
346
|
+
export async function applyApprovedPaths(
|
|
347
|
+
gitRoot: string,
|
|
348
|
+
afterTree: string,
|
|
349
|
+
approved: readonly GitSubstrateChange[],
|
|
350
|
+
): Promise<void> {
|
|
351
|
+
for (const change of approved) {
|
|
352
|
+
const abs = join(gitRoot, change.path);
|
|
353
|
+
if (change.changeType === FileChangeType.DELETE) {
|
|
354
|
+
await rm(abs, { force: true });
|
|
355
|
+
} else {
|
|
356
|
+
// CREATE or MODIFY: write the agent's approved bytes from the after tree.
|
|
357
|
+
await writeBlobToDisk(gitRoot, afterTree, change.path, abs);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Recompute the captured change set on a RESUME, from the pinned baseline and
|
|
364
|
+
* capture refs — the authoritative source that survived the approval wait,
|
|
365
|
+
* independent of any persisted fidelity. Returns `undefined` when this was not a
|
|
366
|
+
* capture turn (the capture ref is absent), so the caller can fall through to the
|
|
367
|
+
* non-capture path.
|
|
368
|
+
*/
|
|
369
|
+
export async function recomputeChangeSet(
|
|
370
|
+
gitRoot: string,
|
|
371
|
+
executionId: string,
|
|
372
|
+
): Promise<CaptureResult | undefined> {
|
|
373
|
+
const baselineTree = await resolveRefTree(gitRoot, baselineRef(executionId));
|
|
374
|
+
const afterTree = await resolveRefTree(gitRoot, captureRef(executionId));
|
|
375
|
+
if (!baselineTree || !afterTree) return undefined;
|
|
376
|
+
|
|
377
|
+
const raw = await git(gitRoot, [
|
|
378
|
+
"diff",
|
|
379
|
+
"--no-renames",
|
|
380
|
+
"--name-status",
|
|
381
|
+
"-z",
|
|
382
|
+
baselineTree,
|
|
383
|
+
afterTree,
|
|
384
|
+
]);
|
|
385
|
+
const changes: GitSubstrateChange[] = [];
|
|
386
|
+
for (const { status, path } of parseNameStatusZ(raw)) {
|
|
387
|
+
const change = await buildCapturedChange(gitRoot, baselineTree, afterTree, status, path);
|
|
388
|
+
if (change) changes.push(change);
|
|
389
|
+
}
|
|
390
|
+
return { baselineTree, afterTree, changes };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Resolve a ref to its tree sha, or `undefined` when the ref does not exist. */
|
|
394
|
+
async function resolveRefTree(gitRoot: string, ref: string): Promise<string | undefined> {
|
|
395
|
+
try {
|
|
396
|
+
return (await git(gitRoot, ["rev-parse", "--verify", "--quiet", `${ref}^{tree}`])).trim();
|
|
397
|
+
} catch {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** The ref pinning the post-reconcile (approved) tree for the RECONCILED event. */
|
|
403
|
+
function approvedRef(executionId: string): string {
|
|
404
|
+
return `refs/stigmer/approved/${executionId}`;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Snapshot the post-reconcile working tree (after approved bytes were applied and
|
|
409
|
+
* rejected files were reverted) and pin it behind the approved ref. Returns the
|
|
410
|
+
* approved tree sha for the RECONCILED event's `approved_snapshot`, plus the ref
|
|
411
|
+
* so the producer can stamp the SnapshotRef. Idempotent under a Temporal retry.
|
|
412
|
+
*/
|
|
413
|
+
export async function snapshotApproved(
|
|
414
|
+
gitRoot: string,
|
|
415
|
+
executionId: string,
|
|
416
|
+
excludePaths: readonly string[] = [],
|
|
417
|
+
): Promise<{ treeOid: string; ref: string }> {
|
|
418
|
+
const gitDir = await resolveGitDir(gitRoot);
|
|
419
|
+
const tree = await writeWorkingTree(gitRoot, gitDir, "approved", executionId, excludePaths);
|
|
420
|
+
const ref = approvedRef(executionId);
|
|
421
|
+
await pinTree(gitRoot, ref, tree, `stigmer approved ${executionId}`);
|
|
422
|
+
return { treeOid: tree, ref };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Release the per-execution baseline, capture, and approved refs (idempotent). */
|
|
426
|
+
export async function dropCaptureRefs(
|
|
427
|
+
gitRoot: string,
|
|
428
|
+
executionId: string,
|
|
429
|
+
): Promise<void> {
|
|
430
|
+
for (const ref of [baselineRef(executionId), captureRef(executionId), approvedRef(executionId)]) {
|
|
431
|
+
try {
|
|
432
|
+
await git(gitRoot, ["update-ref", "-d", ref]);
|
|
433
|
+
} catch {
|
|
434
|
+
// Already gone — nothing to release.
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
// Internals
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
interface NameStatusEntry {
|
|
444
|
+
status: string;
|
|
445
|
+
path: string;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Parse `git diff --name-status -z` output: a flat NUL-delimited stream of
|
|
450
|
+
* `STATUS\0PATH\0` pairs. NUL-delimiting avoids every quoting/escaping concern
|
|
451
|
+
* for paths with spaces, quotes, or unicode.
|
|
452
|
+
*/
|
|
453
|
+
function parseNameStatusZ(raw: string): NameStatusEntry[] {
|
|
454
|
+
const parts = raw.split("\u0000");
|
|
455
|
+
const entries: NameStatusEntry[] = [];
|
|
456
|
+
for (let i = 0; i + 1 < parts.length; i += 2) {
|
|
457
|
+
const status = parts[i];
|
|
458
|
+
const path = parts[i + 1];
|
|
459
|
+
if (!status || !path) continue;
|
|
460
|
+
entries.push({ status: status[0], path });
|
|
461
|
+
}
|
|
462
|
+
return entries;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Read a blob's exact BYTES from a tree, or `undefined` when absent. */
|
|
466
|
+
async function readBlobBytes(
|
|
467
|
+
gitRoot: string,
|
|
468
|
+
tree: string,
|
|
469
|
+
path: string,
|
|
470
|
+
): Promise<Buffer | undefined> {
|
|
471
|
+
try {
|
|
472
|
+
return await gitBuffer(gitRoot, ["cat-file", "-p", `${tree}:${path}`]);
|
|
473
|
+
} catch {
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Read one side of a change as {@link CapturedContent}, computing its identity
|
|
480
|
+
* from the RAW bytes (never a lossy UTF-8 decode): a binary blob carries only its
|
|
481
|
+
* byte-true content address (no body — it reconciles from the git ref), while a
|
|
482
|
+
* text blob is carried inline. Returns `undefined` only when the blob is absent
|
|
483
|
+
* from the tree (the side does not exist for this changeType).
|
|
484
|
+
*/
|
|
485
|
+
async function readSide(
|
|
486
|
+
gitRoot: string,
|
|
487
|
+
tree: string,
|
|
488
|
+
path: string,
|
|
489
|
+
): Promise<CapturedContent | undefined> {
|
|
490
|
+
const bytes = await readBlobBytes(gitRoot, tree, path);
|
|
491
|
+
if (bytes === undefined) return undefined;
|
|
492
|
+
// Identity is always computed over the RAW bytes at the source — never a lossy
|
|
493
|
+
// UTF-8 re-encode — so the enforcement digest is byte-true for binary and text
|
|
494
|
+
// alike. A binary blob carries no body (it reconciles from the git ref); a text
|
|
495
|
+
// blob carries its inline body for the review diff.
|
|
496
|
+
const sha256 = sha256Bytes(bytes);
|
|
497
|
+
if (bytesLookBinary(bytes)) {
|
|
498
|
+
return { kind: "binary", sha256 };
|
|
499
|
+
}
|
|
500
|
+
return { kind: "inline", text: bytes.toString("utf8"), sha256 };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Write a blob's exact bytes from a tree to `abs` (creating parent dirs). */
|
|
504
|
+
async function writeBlobToDisk(
|
|
505
|
+
gitRoot: string,
|
|
506
|
+
tree: string,
|
|
507
|
+
path: string,
|
|
508
|
+
abs: string,
|
|
509
|
+
): Promise<void> {
|
|
510
|
+
const buf = await gitBuffer(gitRoot, ["cat-file", "-p", `${tree}:${path}`]);
|
|
511
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
512
|
+
await writeFile(abs, buf);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** An empty inline side — the default when a MODIFY blob is unexpectedly absent. */
|
|
516
|
+
const EMPTY_INLINE: CapturedContent = { kind: "inline", text: "" };
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Build a {@link GitSubstrateChange} from a diff entry. WHOLE_FILE capture: the
|
|
520
|
+
* before/after content comes from the two trees, with identity computed over the
|
|
521
|
+
* exact bytes git holds (see {@link readSide}) — so the review shows the file's
|
|
522
|
+
* true net change and the enforcement digest is byte-true, binary or text alike.
|
|
523
|
+
* A binary side is carried as a content address only (no body); large text bodies
|
|
524
|
+
* are offloaded later at the persist chokepoint.
|
|
525
|
+
*/
|
|
526
|
+
async function buildCapturedChange(
|
|
527
|
+
gitRoot: string,
|
|
528
|
+
baselineTree: string,
|
|
529
|
+
afterTree: string,
|
|
530
|
+
status: string,
|
|
531
|
+
path: string,
|
|
532
|
+
): Promise<GitSubstrateChange | undefined> {
|
|
533
|
+
if (status === "A") {
|
|
534
|
+
const after = await readSide(gitRoot, afterTree, path);
|
|
535
|
+
if (after === undefined) return undefined;
|
|
536
|
+
return { path, changeType: FileChangeType.CREATE, after };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (status === "D") {
|
|
540
|
+
const before = await readSide(gitRoot, baselineTree, path);
|
|
541
|
+
if (before === undefined) return undefined;
|
|
542
|
+
return { path, changeType: FileChangeType.DELETE, before };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Modified (M) — and any other status (T type-change, etc.) treated as MODIFY.
|
|
546
|
+
const before = await readSide(gitRoot, baselineTree, path);
|
|
547
|
+
const after = await readSide(gitRoot, afterTree, path);
|
|
548
|
+
if (before === undefined && after === undefined) return undefined;
|
|
549
|
+
return {
|
|
550
|
+
path,
|
|
551
|
+
changeType: FileChangeType.MODIFY,
|
|
552
|
+
before: before ?? EMPTY_INLINE,
|
|
553
|
+
after: after ?? EMPTY_INLINE,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runner's file-review producer: harness-agnostic digest + event authoring
|
|
3
|
+
* for the apply-then-review HITL subsystem. Consumed by both the Cursor and
|
|
4
|
+
* deep-agent harnesses to author identical ledger entries.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
aggregateDigest,
|
|
9
|
+
fileChangeKindName,
|
|
10
|
+
fileDigest,
|
|
11
|
+
sha256Bytes,
|
|
12
|
+
sha256Hex,
|
|
13
|
+
type FileDigestInput,
|
|
14
|
+
} from "./digest.js";
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
appendFileReviewEvents,
|
|
18
|
+
buildBaselineCapturedEvent,
|
|
19
|
+
buildCandidateCapturedEvent,
|
|
20
|
+
buildCapturedFileChange,
|
|
21
|
+
buildFailedEvent,
|
|
22
|
+
buildReconciledEvent,
|
|
23
|
+
eventId,
|
|
24
|
+
hasCandidateCaptured,
|
|
25
|
+
type CapturedChangeInput,
|
|
26
|
+
type ChangeSetContext,
|
|
27
|
+
} from "./events.js";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
applyCaptureDecisions,
|
|
31
|
+
captureBaselineToLedger,
|
|
32
|
+
captureCandidateToLedger,
|
|
33
|
+
type CaptureResumeResult,
|
|
34
|
+
} from "./capture.js";
|
|
35
|
+
|
|
36
|
+
export { isGitWorkTree } from "./git-substrate.js";
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
applyCasApproved,
|
|
40
|
+
casBlobKey,
|
|
41
|
+
casBlobReader,
|
|
42
|
+
casManifestKey,
|
|
43
|
+
loadCasManifest,
|
|
44
|
+
restoreCasToBaseline,
|
|
45
|
+
snapshotCasChangeSet,
|
|
46
|
+
type BlobReader,
|
|
47
|
+
type CasBlobRef,
|
|
48
|
+
type CasCapturedFile,
|
|
49
|
+
type CasManifest,
|
|
50
|
+
type CasPathCapture,
|
|
51
|
+
type CasSnapshotRef,
|
|
52
|
+
} from "./cas-substrate.js";
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
isSecretLikePath,
|
|
56
|
+
partitionIgnoredPathsBySecret,
|
|
57
|
+
SECRET_BASENAME_PATTERNS,
|
|
58
|
+
SECRET_PATH_PATTERNS,
|
|
59
|
+
type CasSecretPartition,
|
|
60
|
+
} from "./secret-paths.js";
|