@stigmer/runner 3.0.9-dev.20260616060535 → 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 +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
|
@@ -117,17 +117,30 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
|
|
|
117
117
|
* tool-call ids, which mean nothing to the model. A resumed agent re-issues the
|
|
118
118
|
* approved tool with a fresh id; the approval gate (see approval-state.ts grants)
|
|
119
119
|
* is what actually lets the re-attempt through.
|
|
120
|
+
*
|
|
121
|
+
* `appliedToolCallIds` are approved whole-file writes the RUNNER has already
|
|
122
|
+
* applied to the workspace itself (exact-apply — the "what you approve is what
|
|
123
|
+
* gets applied" guarantee). Those are described as ALREADY DONE so the model
|
|
124
|
+
* does not redo them; only the remaining approved actions (hunk edits, shell,
|
|
125
|
+
* MCP) are the ones it must still carry out. No grant is issued for an applied
|
|
126
|
+
* write, so if the model edits that file again it is correctly re-gated.
|
|
120
127
|
*/
|
|
121
128
|
export function buildReinvocationPrompt(
|
|
122
129
|
pendingApprovals: PendingApproval[],
|
|
123
130
|
approvalDecisions: Map<string, ApprovalAction>,
|
|
131
|
+
appliedToolCallIds: ReadonlySet<string> = new Set(),
|
|
124
132
|
): string {
|
|
125
133
|
const approved: string[] = [];
|
|
134
|
+
const alreadyApplied: string[] = [];
|
|
126
135
|
const skipped: string[] = [];
|
|
127
136
|
|
|
128
137
|
for (const pa of pendingApprovals) {
|
|
129
138
|
const action = approvalDecisions.get(pa.toolCallId);
|
|
130
|
-
|
|
139
|
+
const isApproved =
|
|
140
|
+
action === ApprovalAction.APPROVE || action === ApprovalAction.APPROVE_ALL;
|
|
141
|
+
if (isApproved && appliedToolCallIds.has(pa.toolCallId)) {
|
|
142
|
+
alreadyApplied.push(describeApproval(pa));
|
|
143
|
+
} else if (isApproved) {
|
|
131
144
|
approved.push(describeApproval(pa));
|
|
132
145
|
} else if (action === ApprovalAction.SKIP) {
|
|
133
146
|
skipped.push(describeApproval(pa));
|
|
@@ -135,6 +148,15 @@ export function buildReinvocationPrompt(
|
|
|
135
148
|
}
|
|
136
149
|
|
|
137
150
|
const parts: string[] = [];
|
|
151
|
+
if (alreadyApplied.length) {
|
|
152
|
+
parts.push(
|
|
153
|
+
"The user reviewed and APPROVED the following change(s), and the platform " +
|
|
154
|
+
"has ALREADY applied them to the workspace exactly as shown. Do NOT redo " +
|
|
155
|
+
"or rewrite them — treat them as done and continue with the rest of the " +
|
|
156
|
+
"task:\n" +
|
|
157
|
+
alreadyApplied.map((a) => `- ${a}`).join("\n"),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
138
160
|
if (approved.length) {
|
|
139
161
|
parts.push(
|
|
140
162
|
"The user reviewed the following action(s) you proposed and APPROVED them. " +
|
|
@@ -387,22 +409,66 @@ export function formatResponseRules(): string {
|
|
|
387
409
|
* rather than asking for permission in prose. It is the only lever Cursor's
|
|
388
410
|
* hook model leaves available.
|
|
389
411
|
*/
|
|
412
|
+
const TOOL_APPROVAL_PROTOCOL_INTRO =
|
|
413
|
+
"You run inside a platform that automatically gates sensitive actions for human approval.";
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* The platform's tool-approval rules, authored once and rendered in two places:
|
|
417
|
+
* the system prompt (see {@link formatToolApprovalProtocol}) and an always-applied
|
|
418
|
+
* Cursor project rule (see {@link buildToolApprovalRuleFile}). Keeping a single
|
|
419
|
+
* source means the two surfaces can never drift.
|
|
420
|
+
*
|
|
421
|
+
* The fourth rule is the load-bearing fix for the Cursor harness: because the
|
|
422
|
+
* deny-based gate surfaces an approval pause to the model as a tool *failure*
|
|
423
|
+
* (often Cursor's own generic "blocked by a hook" text — confirmed in Phase 0,
|
|
424
|
+
* and unavoidable since the SDK exposes no non-leaky approval primitive), a
|
|
425
|
+
* well-behaved model otherwise concludes the environment is broken and tells the
|
|
426
|
+
* user to "enable hooks in your Cursor settings", contradicting the approval
|
|
427
|
+
* card. This rule reframes that signal as the gate working as designed.
|
|
428
|
+
*/
|
|
429
|
+
const TOOL_APPROVAL_PROTOCOL_RULES: readonly string[] = [
|
|
430
|
+
"Carry out every action by calling the appropriate tool directly. Never describe an action you intend to take and then stop, and never ask the user for permission in prose.",
|
|
431
|
+
"When an action needs approval, the platform pauses it, asks the user, and resumes you automatically after they decide. You do not request approval yourself — invoking the tool is how you request it.",
|
|
432
|
+
"Even if a tool or MCP server instructs you to confirm with the user before acting (for example before sending, deleting, or purchasing), do NOT ask in prose. Invoke the tool and let the platform's approval step handle it.",
|
|
433
|
+
"A tool result that says the action was blocked, denied, requires approval, or was \"blocked by a hook\" is the platform's approval gate doing its job — it is NOT an error and NOT a Cursor misconfiguration. Never tell the user to change Cursor settings, enable hooks, or fix their configuration; the gate is intentional and the platform will resume you automatically once the user decides.",
|
|
434
|
+
"If an action is declined, do not retry it or attempt a workaround for it; continue with the rest of the task.",
|
|
435
|
+
];
|
|
436
|
+
|
|
390
437
|
export function formatToolApprovalProtocol(): string {
|
|
391
438
|
return [
|
|
392
439
|
"<tool_approval_protocol>",
|
|
393
|
-
|
|
394
|
-
"
|
|
395
|
-
|
|
396
|
-
" describe an action you intend to take and then stop, and never ask the",
|
|
397
|
-
" user for permission in prose.",
|
|
398
|
-
"- When an action needs approval, the platform pauses it, asks the user, and",
|
|
399
|
-
" resumes you automatically after they decide. You do not request approval",
|
|
400
|
-
" yourself — invoking the tool is how you request it.",
|
|
401
|
-
"- Even if a tool or MCP server instructs you to confirm with the user before",
|
|
402
|
-
" acting (for example before sending, deleting, or purchasing), do NOT ask",
|
|
403
|
-
" in prose. Invoke the tool and let the platform's approval step handle it.",
|
|
404
|
-
"- If an action is declined, do not retry it or attempt a workaround for it;",
|
|
405
|
-
" continue with the rest of the task.",
|
|
440
|
+
TOOL_APPROVAL_PROTOCOL_INTRO,
|
|
441
|
+
"Follow these rules without exception:",
|
|
442
|
+
...TOOL_APPROVAL_PROTOCOL_RULES.map((rule) => `- ${rule}`),
|
|
406
443
|
"</tool_approval_protocol>",
|
|
407
444
|
].join("\n");
|
|
408
445
|
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Render the tool-approval protocol as an always-applied Cursor project rule
|
|
449
|
+
* (`.cursor/rules/*.mdc`).
|
|
450
|
+
*
|
|
451
|
+
* A `.cursor/rules` entry with `alwaysApply: true` is injected into the model's
|
|
452
|
+
* context on EVERY turn and takes precedence over a connected MCP server's
|
|
453
|
+
* `instructions`. The system-prompt copy ({@link formatToolApprovalProtocol})
|
|
454
|
+
* only reliably reaches the FIRST user message; the rule file is the durable,
|
|
455
|
+
* higher-precedence surface that keeps the protocol in force across resumed
|
|
456
|
+
* turns and against server-authored "ask the user first" guidance. The runner
|
|
457
|
+
* installs it into the workspace for the duration of a turn and restores the
|
|
458
|
+
* workspace afterward (see workspace-setup.ts).
|
|
459
|
+
*/
|
|
460
|
+
export function buildToolApprovalRuleFile(): string {
|
|
461
|
+
return [
|
|
462
|
+
"---",
|
|
463
|
+
"description: Stigmer platform tool-approval protocol — how human-in-the-loop approvals work and why a denied/blocked tool is not an error",
|
|
464
|
+
"alwaysApply: true",
|
|
465
|
+
"---",
|
|
466
|
+
"",
|
|
467
|
+
"# Tool approval protocol",
|
|
468
|
+
"",
|
|
469
|
+
TOOL_APPROVAL_PROTOCOL_INTRO,
|
|
470
|
+
"",
|
|
471
|
+
...TOOL_APPROVAL_PROTOCOL_RULES.map((rule) => `- ${rule}`),
|
|
472
|
+
"",
|
|
473
|
+
].join("\n");
|
|
474
|
+
}
|
|
@@ -31,14 +31,34 @@
|
|
|
31
31
|
* PID is gone, and the relocated artifacts are not in the repo.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import { writeFile, readFile, mkdir, chmod, rm } from "node:fs/promises";
|
|
35
|
-
import { join } from "node:path";
|
|
34
|
+
import { writeFile, readFile, mkdir, chmod, rm, rmdir, readdir } from "node:fs/promises";
|
|
35
|
+
import { basename, dirname, join } from "node:path";
|
|
36
36
|
import { generateHookScript } from "./hook-script.js";
|
|
37
|
-
import {
|
|
37
|
+
import { buildToolApprovalRuleFile } from "./prompt-builder.js";
|
|
38
|
+
import {
|
|
39
|
+
writeApprovalStateFile,
|
|
40
|
+
resetDenialLedger,
|
|
41
|
+
activePointerPath,
|
|
42
|
+
writeActiveTurnPointer,
|
|
43
|
+
removeActiveTurnPointer,
|
|
44
|
+
type ApprovalStateFile,
|
|
45
|
+
} from "./approval-state.js";
|
|
46
|
+
import { resetCasObservations } from "./cas-observations.js";
|
|
47
|
+
import { ensureHitlGateDir } from "../../shared/workspace/platform-dir.js";
|
|
38
48
|
|
|
39
49
|
const CURSOR_DIR = ".cursor";
|
|
40
50
|
const HOOKS_CONFIG_FILE = "hooks.json";
|
|
41
51
|
const HOOK_SCRIPT_FILE = "stigmer-approval.sh";
|
|
52
|
+
const RULES_DIR = "rules";
|
|
53
|
+
// Pre-#173 the gate wrote its script/state/ledger INTO the repo here, as
|
|
54
|
+
// `stigmer-*` files. Newer runs relocate them to the session HITL dir, but the
|
|
55
|
+
// old files (and their hooks.json entry) linger in already-touched repos — see
|
|
56
|
+
// removeLegacyWorkspaceHookArtifacts / isStigmerHookEntry.
|
|
57
|
+
const LEGACY_HOOKS_DIR = "hooks";
|
|
58
|
+
const RUNNER_OWNED_HOOK_FILE_PREFIX = "stigmer-";
|
|
59
|
+
// Unmistakably runner-owned filename so installing it never collides with a
|
|
60
|
+
// user's own project rules and restoring it never touches one.
|
|
61
|
+
const TOOL_APPROVAL_RULE_FILE = "stigmer-tool-approval.mdc";
|
|
42
62
|
|
|
43
63
|
/**
|
|
44
64
|
* Cursor hook events the gate registers, both pointing at the same script (which
|
|
@@ -71,6 +91,24 @@ export interface HitlGateHandle {
|
|
|
71
91
|
* before this turn (in which case teardown deletes the file).
|
|
72
92
|
*/
|
|
73
93
|
restoreTo: string | null;
|
|
94
|
+
/**
|
|
95
|
+
* The `.cursor/rules/stigmer-tool-approval.mdc` this turn manages: its
|
|
96
|
+
* absolute path and the bytes to restore (null → teardown deletes the file,
|
|
97
|
+
* the normal case since the filename is runner-owned).
|
|
98
|
+
*/
|
|
99
|
+
rule: WorkspaceFileSnapshot;
|
|
100
|
+
/**
|
|
101
|
+
* Workspace-scoped gate directory holding the stable hook script and the
|
|
102
|
+
* active-turn pointer. Teardown drops the pointer (see removeHitlGate) so the
|
|
103
|
+
* gate is inert between turns.
|
|
104
|
+
*/
|
|
105
|
+
gateDir: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Absolute path + restore target for a single workspace file the gate manages. */
|
|
109
|
+
interface WorkspaceFileSnapshot {
|
|
110
|
+
path: string;
|
|
111
|
+
restoreTo: string | null;
|
|
74
112
|
}
|
|
75
113
|
|
|
76
114
|
/**
|
|
@@ -88,14 +126,41 @@ export async function installHitlGate(params: {
|
|
|
88
126
|
}): Promise<HitlGateHandle> {
|
|
89
127
|
const { workspaceRoot, hitlDir, approvalState, runnerPid } = params;
|
|
90
128
|
|
|
91
|
-
|
|
129
|
+
// Heal any pre-#173 in-workspace gate leftovers before installing. Without
|
|
130
|
+
// this, a stale `.cursor/hooks/stigmer-approval.sh` (from a runner build that
|
|
131
|
+
// wrote the gate into the repo) keeps running alongside the current
|
|
132
|
+
// out-of-workspace gate and — reading an old, taskless approval-state with no
|
|
133
|
+
// grant for the current action — vetoes EVERY gated tool, including ones the
|
|
134
|
+
// user just approved ("blocked despite approval"). Its denials also land in a
|
|
135
|
+
// ledger this runner never reads, so the denial never surfaces as a pause and
|
|
136
|
+
// the run silently completes with the file never written. The merge below
|
|
137
|
+
// strips the stale hooks.json entry (see isStigmerHookEntry); this removes the
|
|
138
|
+
// orphaned script/state/ledger files so nothing can re-run them.
|
|
139
|
+
await removeLegacyWorkspaceHookArtifacts(workspaceRoot);
|
|
140
|
+
|
|
141
|
+
const { scriptPath: approvalScriptPath, gateDir } = await writeHitlArtifacts(
|
|
142
|
+
workspaceRoot,
|
|
143
|
+
hitlDir,
|
|
144
|
+
approvalState,
|
|
145
|
+
runnerPid,
|
|
146
|
+
);
|
|
92
147
|
// One script, two events: preToolUse gates built-ins; beforeMCPExecution is
|
|
93
148
|
// the only event Cursor enforces for MCP tools. The script branches internally
|
|
94
149
|
// on hook_event_name so MCP is gated in exactly one place.
|
|
95
|
-
|
|
150
|
+
const hookHandle = await installWorkspaceHook(workspaceRoot, [
|
|
96
151
|
{ event: PRE_TOOL_USE_EVENT, scriptPath: approvalScriptPath },
|
|
97
152
|
{ event: BEFORE_MCP_EVENT, scriptPath: approvalScriptPath },
|
|
98
153
|
]);
|
|
154
|
+
|
|
155
|
+
// Install the always-applied tool-approval rule. The deny-based gate surfaces
|
|
156
|
+
// an approval pause to the model as a tool failure (Cursor's generic "blocked
|
|
157
|
+
// by a hook" text), and the SDK exposes no non-leaky approval primitive — so
|
|
158
|
+
// this rule, which takes precedence over MCP-server instructions and persists
|
|
159
|
+
// across resumed turns, is the strongest available lever to stop the model
|
|
160
|
+
// from misreading the gate as a broken environment.
|
|
161
|
+
const rule = await installWorkspaceRule(workspaceRoot);
|
|
162
|
+
|
|
163
|
+
return { ...hookHandle, rule, gateDir };
|
|
99
164
|
}
|
|
100
165
|
|
|
101
166
|
/**
|
|
@@ -105,48 +170,149 @@ export async function installHitlGate(params: {
|
|
|
105
170
|
* and a leftover hooks.json is inert anyway (see the module doc).
|
|
106
171
|
*/
|
|
107
172
|
export async function removeHitlGate(handle: HitlGateHandle): Promise<void> {
|
|
173
|
+
await restoreWorkspaceFile(handle.hooksJsonPath, handle.restoreTo);
|
|
174
|
+
// Drop the active-turn pointer so the gate is INERT between turns: a hook that
|
|
175
|
+
// fires while no turn is active (a cached hooks.json, the user's own IDE on the
|
|
176
|
+
// same repo) reads no pointer and allows immediately. The stable hook script
|
|
177
|
+
// itself is intentionally left in place — it is harmless without a pointer and
|
|
178
|
+
// is reused (and refreshed) by the next turn.
|
|
179
|
+
await removeActiveTurnPointer(handle.gateDir);
|
|
180
|
+
if (handle.rule) {
|
|
181
|
+
await restoreWorkspaceFile(handle.rule.path, handle.rule.restoreTo);
|
|
182
|
+
// When we deleted our own rule file (restoreTo === null) and the `.cursor/
|
|
183
|
+
// rules` dir is now empty, remove it too so a repo that had no rules before
|
|
184
|
+
// is left exactly as we found it. rmdir only removes an empty dir, so a
|
|
185
|
+
// workspace with the user's own rules is untouched.
|
|
186
|
+
if (handle.rule.restoreTo === null) {
|
|
187
|
+
try {
|
|
188
|
+
await rmdir(dirname(handle.rule.path));
|
|
189
|
+
} catch {
|
|
190
|
+
// Non-empty (user has other rules) or already gone — nothing to clean.
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Restore a single gate-managed workspace file to its pre-turn state: delete it
|
|
198
|
+
* when nothing existed before (restoreTo === null), else write the snapshot
|
|
199
|
+
* bytes back. Best-effort and never throws — a teardown failure must not fail
|
|
200
|
+
* the execution, and a leftover artifact is inert (see the module doc).
|
|
201
|
+
*/
|
|
202
|
+
async function restoreWorkspaceFile(path: string, restoreTo: string | null): Promise<void> {
|
|
108
203
|
try {
|
|
109
|
-
if (
|
|
110
|
-
await rm(
|
|
204
|
+
if (restoreTo === null) {
|
|
205
|
+
await rm(path, { force: true });
|
|
111
206
|
} else {
|
|
112
|
-
await writeFile(
|
|
207
|
+
await writeFile(path, restoreTo, "utf-8");
|
|
113
208
|
}
|
|
114
209
|
} catch (err) {
|
|
115
210
|
console.warn(
|
|
116
|
-
`removeHitlGate: failed to restore ${
|
|
211
|
+
`removeHitlGate: failed to restore ${path} (non-fatal): ` +
|
|
117
212
|
`${err instanceof Error ? err.message : err}`,
|
|
118
213
|
);
|
|
119
214
|
}
|
|
120
215
|
}
|
|
121
216
|
|
|
122
217
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
218
|
+
* Delete pre-#173 in-workspace gate leftovers: the runner-owned `stigmer-*`
|
|
219
|
+
* files an older runner build wrote to `.cursor/hooks/` (the hook script, its
|
|
220
|
+
* approval-state, its denial ledger). These are the orphan that shadows the
|
|
221
|
+
* current gate (see installHitlGate). Best-effort and never throws — a cleanup
|
|
222
|
+
* failure must not fail the run.
|
|
223
|
+
*
|
|
224
|
+
* Only `stigmer-`-prefixed files are removed (the unmistakably runner-owned
|
|
225
|
+
* namespace, like the rule filename), so a user's own scripts in `.cursor/hooks`
|
|
226
|
+
* are untouched; the directory itself is removed only if it ends up empty.
|
|
227
|
+
*/
|
|
228
|
+
async function removeLegacyWorkspaceHookArtifacts(workspaceRoot: string): Promise<void> {
|
|
229
|
+
const hooksDir = join(workspaceRoot, CURSOR_DIR, LEGACY_HOOKS_DIR);
|
|
230
|
+
let entries: string[];
|
|
231
|
+
try {
|
|
232
|
+
entries = await readdir(hooksDir);
|
|
233
|
+
} catch {
|
|
234
|
+
return; // no legacy hooks dir — nothing to heal
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let removedAny = false;
|
|
238
|
+
for (const name of entries) {
|
|
239
|
+
if (!name.startsWith(RUNNER_OWNED_HOOK_FILE_PREFIX)) continue;
|
|
240
|
+
try {
|
|
241
|
+
await rm(join(hooksDir, name), { force: true });
|
|
242
|
+
removedAny = true;
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.warn(
|
|
245
|
+
`removeLegacyWorkspaceHookArtifacts: failed to remove ${name} (non-fatal): ` +
|
|
246
|
+
`${err instanceof Error ? err.message : err}`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (removedAny) {
|
|
252
|
+
// rmdir only succeeds on an empty dir, so a user who keeps their own hooks
|
|
253
|
+
// here is left untouched; an empty (entirely-ours) dir is cleaned away.
|
|
254
|
+
try {
|
|
255
|
+
await rmdir(hooksDir);
|
|
256
|
+
} catch {
|
|
257
|
+
// Non-empty (user owns other hooks) or already gone — nothing to clean.
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Write this turn's gate artifacts and return the STABLE hook-script path plus
|
|
264
|
+
* the workspace gate directory.
|
|
265
|
+
*
|
|
266
|
+
* Two surfaces, deliberately split by scope:
|
|
267
|
+
*
|
|
268
|
+
* - Per-SESSION (in {@link hitlDir}): the approval-state file (hook input) and a
|
|
269
|
+
* freshly-reset denial ledger (hook output the runner reads). Reset every turn
|
|
270
|
+
* so the runner only ever reads denials from the current run, even across HITL
|
|
271
|
+
* reinvocations and Temporal activity retries.
|
|
272
|
+
*
|
|
273
|
+
* - Per-WORKSPACE (in the gate dir): the STABLE hook script and the active-turn
|
|
274
|
+
* pointer. The script path is stable across executions because the Cursor SDK
|
|
275
|
+
* caches `.cursor/hooks.json` (the script path) for the runner process — a
|
|
276
|
+
* per-session script would be cached at the first execution and reused for all
|
|
277
|
+
* later ones, sending their denials to the first session's ledger (the
|
|
278
|
+
* empty-ledger → COMPLETED regression). The script bakes NO per-session paths;
|
|
279
|
+
* it reads the pointer, which we repoint (atomically) every turn to THIS turn's
|
|
280
|
+
* state file, ledger, and runner PID. See generateHookScript / writeActiveTurnPointer.
|
|
127
281
|
*/
|
|
128
282
|
async function writeHitlArtifacts(
|
|
283
|
+
workspaceRoot: string,
|
|
129
284
|
hitlDir: string,
|
|
130
285
|
approvalState: ApprovalStateFile,
|
|
131
286
|
runnerPid: number,
|
|
132
|
-
): Promise<string> {
|
|
287
|
+
): Promise<{ scriptPath: string; gateDir: string }> {
|
|
133
288
|
await mkdir(hitlDir, { recursive: true });
|
|
134
289
|
|
|
135
290
|
const stateFilePath = await writeApprovalStateFile(hitlDir, approvalState);
|
|
136
|
-
// Reset the denial ledger for this turn so the runner only reads denials
|
|
137
|
-
// produced by the current run, even across HITL reinvocations on the durable
|
|
138
|
-
// HITL directory and Temporal activity retries.
|
|
139
291
|
const ledgerFilePath = await resetDenialLedger(hitlDir);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
292
|
+
// Reset the cas-observations sidecar too, so the turn boundary only ever reads
|
|
293
|
+
// gitignored observations the hook staged this run (deterministic across HITL
|
|
294
|
+
// reinvocations and Temporal retries, exactly like the denial ledger).
|
|
295
|
+
await resetCasObservations(hitlDir);
|
|
296
|
+
|
|
297
|
+
const gateDir = await ensureHitlGateDir(workspaceRoot);
|
|
298
|
+
const pointerPath = activePointerPath(gateDir);
|
|
299
|
+
const scriptPath = join(gateDir, HOOK_SCRIPT_FILE);
|
|
300
|
+
// Bake the workspace root so capture mode's `git check-ignore` runs against the
|
|
301
|
+
// right repo regardless of the hook's cwd. The root is stable per workspace
|
|
302
|
+
// (the gate dir is keyed by it), so the script stays stable across executions.
|
|
303
|
+
await writeFile(scriptPath, generateHookScript(pointerPath, workspaceRoot), "utf-8");
|
|
304
|
+
await chmod(scriptPath, 0o755);
|
|
305
|
+
|
|
306
|
+
// Point the stable hook at THIS turn's per-session artifacts (atomic, last
|
|
307
|
+
// write wins) — the single indirection that makes a process-cached hook resolve
|
|
308
|
+
// the current execution instead of the first one's.
|
|
309
|
+
await writeActiveTurnPointer(gateDir, {
|
|
310
|
+
stateFile: stateFilePath,
|
|
311
|
+
ledgerFile: ledgerFilePath,
|
|
312
|
+
runnerPid,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return { scriptPath, gateDir };
|
|
150
316
|
}
|
|
151
317
|
|
|
152
318
|
/**
|
|
@@ -157,7 +323,7 @@ async function writeHitlArtifacts(
|
|
|
157
323
|
async function installWorkspaceHook(
|
|
158
324
|
workspaceRoot: string,
|
|
159
325
|
registrations: HookRegistration[],
|
|
160
|
-
): Promise<HitlGateHandle
|
|
326
|
+
): Promise<Omit<HitlGateHandle, "rule" | "gateDir">> {
|
|
161
327
|
const cursorDir = join(workspaceRoot, CURSOR_DIR);
|
|
162
328
|
const hooksJsonPath = join(cursorDir, HOOKS_CONFIG_FILE);
|
|
163
329
|
|
|
@@ -176,6 +342,31 @@ async function installWorkspaceHook(
|
|
|
176
342
|
return { hooksJsonPath, restoreTo };
|
|
177
343
|
}
|
|
178
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Snapshot and install the always-applied tool-approval rule into
|
|
347
|
+
* `.cursor/rules/stigmer-tool-approval.mdc`, returning the path + restore
|
|
348
|
+
* target. The filename is runner-owned, so restoreTo is normally null (teardown
|
|
349
|
+
* deletes it); if a same-named file somehow pre-exists (e.g. a crash-leftover
|
|
350
|
+
* from a prior turn), its bytes are captured and restored so we never clobber a
|
|
351
|
+
* file we did not create.
|
|
352
|
+
*/
|
|
353
|
+
async function installWorkspaceRule(workspaceRoot: string): Promise<WorkspaceFileSnapshot> {
|
|
354
|
+
const rulesDir = join(workspaceRoot, CURSOR_DIR, RULES_DIR);
|
|
355
|
+
const rulePath = join(rulesDir, TOOL_APPROVAL_RULE_FILE);
|
|
356
|
+
|
|
357
|
+
let restoreTo: string | null = null;
|
|
358
|
+
try {
|
|
359
|
+
restoreTo = await readFile(rulePath, "utf-8");
|
|
360
|
+
} catch {
|
|
361
|
+
restoreTo = null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
await mkdir(rulesDir, { recursive: true });
|
|
365
|
+
await writeFile(rulePath, buildToolApprovalRuleFile(), "utf-8");
|
|
366
|
+
|
|
367
|
+
return { path: rulePath, restoreTo };
|
|
368
|
+
}
|
|
369
|
+
|
|
179
370
|
/**
|
|
180
371
|
* A hook entry the gate installs. Absolute `command` so the hook is found
|
|
181
372
|
* regardless of which workspace root a multi-root IDE resolves against.
|
|
@@ -185,19 +376,28 @@ function buildHookEntry(scriptPath: string): Record<string, unknown> {
|
|
|
185
376
|
}
|
|
186
377
|
|
|
187
378
|
/**
|
|
188
|
-
* Identify a hook entry the gate itself wrote
|
|
189
|
-
* turn
|
|
190
|
-
*
|
|
191
|
-
*
|
|
379
|
+
* Identify a hook entry the gate itself wrote — in this turn, a prior
|
|
380
|
+
* crash-leftover turn, or a pre-#173 runner build — so a re-install never
|
|
381
|
+
* duplicates it and a restore strips it. Matches two runner-owned shapes, never
|
|
382
|
+
* a user's own hook:
|
|
383
|
+
*
|
|
384
|
+
* 1. Current: any script under the runner-owned `~/.stigmer/` tree (the session
|
|
385
|
+
* HITL dir or the workspace gate dir, `~/.stigmer/.../*.sh`) — location-based,
|
|
386
|
+
* so it covers every gate script regardless of name.
|
|
387
|
+
* 2. Legacy: a `stigmer-*.sh` script (historically the in-workspace
|
|
388
|
+
* `.cursor/hooks/stigmer-approval.sh`) — recognized by its unmistakably
|
|
389
|
+
* runner-owned basename. Without this, the old in-workspace hook is treated
|
|
390
|
+
* as a user hook and preserved by the merge, so it keeps running alongside
|
|
391
|
+
* (and vetoing) the current gate. The `stigmer-` prefix is the same
|
|
392
|
+
* runner-owned namespace convention as the rule filename.
|
|
192
393
|
*/
|
|
193
394
|
function isStigmerHookEntry(entry: unknown): boolean {
|
|
194
395
|
if (!entry || typeof entry !== "object") return false;
|
|
195
396
|
const command = (entry as { command?: unknown }).command;
|
|
196
|
-
return
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
);
|
|
397
|
+
if (typeof command !== "string" || command.length === 0) return false;
|
|
398
|
+
if (command.includes("/.stigmer/") && command.endsWith(".sh")) return true;
|
|
399
|
+
const file = basename(command);
|
|
400
|
+
return file.startsWith(RUNNER_OWNED_HOOK_FILE_PREFIX) && file.endsWith(".sh");
|
|
201
401
|
}
|
|
202
402
|
|
|
203
403
|
const STANDALONE_CONFIG = (registrations: HookRegistration[]): string =>
|
|
@@ -274,11 +474,26 @@ export function buildMergedConfig(
|
|
|
274
474
|
|
|
275
475
|
const merged = JSON.stringify({ ...root, version, hooks: mergedHooks }, null, 2);
|
|
276
476
|
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
477
|
+
// No stale Stigmer entry → restore the user's exact original bytes.
|
|
478
|
+
if (!strippedStale) {
|
|
479
|
+
return { merged, restoreTo: originalRaw };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// We stripped a stale Stigmer entry, so never restore the original bytes (that
|
|
483
|
+
// would re-plant our leftover). If stripping leaves NO user hooks at all and
|
|
484
|
+
// the file carried nothing but `version`/`hooks`, the entire hooks.json was our
|
|
485
|
+
// own leftover (the pre-#173 design wrote the whole file) — delete it on
|
|
486
|
+
// teardown so a polluted repo is left pristine. Otherwise restore the cleaned,
|
|
487
|
+
// Stigmer-free form, preserving the user's other hooks and root fields.
|
|
488
|
+
const onlyVersionAndHooks = Object.keys(root).every(
|
|
489
|
+
(k) => k === "version" || k === "hooks",
|
|
490
|
+
);
|
|
491
|
+
const noUserHooksRemain = Object.values(cleaned).every(
|
|
492
|
+
(v) => Array.isArray(v) && v.length === 0,
|
|
493
|
+
);
|
|
494
|
+
const restoreTo = onlyVersionAndHooks && noUserHooksRemain
|
|
495
|
+
? null
|
|
496
|
+
: JSON.stringify({ ...root, version, hooks: cleaned }, null, 2);
|
|
282
497
|
|
|
283
498
|
return { merged, restoreTo };
|
|
284
499
|
}
|