@martinloop/mcp 0.3.8 → 0.5.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/README.md +90 -106
- package/dist/discovery-metadata.d.ts +1 -1
- package/dist/discovery-metadata.js +1 -1
- package/dist/resources.d.ts +1 -2
- package/dist/resources.js +190 -238
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +16 -11
- package/dist/server.d.ts +23 -0
- package/dist/server.js +152 -7
- package/dist/tools/run-dossier.d.ts +3 -1
- package/dist/tools/run-dossier.js +40 -2
- package/dist/tools/run-loop.d.ts +1 -4
- package/dist/tools/run-loop.js +6 -15
- package/dist/tools/tool-support.d.ts +2 -1
- package/dist/tools/tool-support.js +1 -12
- package/dist/vendor/adapters/claude-cli.js +38 -35
- package/dist/vendor/adapters/cli-bridge.d.ts +9 -1
- package/dist/vendor/adapters/cli-bridge.js +54 -13
- package/dist/vendor/adapters/codex-launcher.d.ts +61 -1
- package/dist/vendor/adapters/codex-launcher.js +118 -19
- package/dist/vendor/adapters/index.d.ts +1 -3
- package/dist/vendor/adapters/index.js +1 -3
- package/dist/vendor/adapters/openai-compatible.js +57 -18
- package/dist/vendor/contracts/adapter-capabilities.d.ts +1 -1
- package/dist/vendor/contracts/context-handoff.d.ts +100 -0
- package/dist/vendor/contracts/context-handoff.js +11 -0
- package/dist/vendor/contracts/context-manifest.d.ts +195 -0
- package/dist/vendor/contracts/context-manifest.js +17 -0
- package/dist/vendor/contracts/context-shadow.d.ts +75 -0
- package/dist/vendor/contracts/context-shadow.js +8 -0
- package/dist/vendor/contracts/exits.d.ts +126 -0
- package/dist/vendor/contracts/exits.js +32 -0
- package/dist/vendor/contracts/index.d.ts +39 -2
- package/dist/vendor/contracts/index.js +22 -0
- package/dist/vendor/contracts/mission.d.ts +102 -0
- package/dist/vendor/contracts/mission.js +64 -0
- package/dist/vendor/contracts/operator.d.ts +3 -1
- package/dist/vendor/contracts/operator.js +2 -1
- package/dist/vendor/contracts/verified-handoff.d.ts +100 -0
- package/dist/vendor/contracts/verified-handoff.js +35 -0
- package/dist/vendor/core/context-chain-gate.d.ts +89 -0
- package/dist/vendor/core/context-chain-gate.js +173 -0
- package/dist/vendor/core/context-compiler.d.ts +53 -0
- package/dist/vendor/core/context-compiler.js +261 -0
- package/dist/vendor/core/context-handoff.d.ts +42 -0
- package/dist/vendor/core/context-handoff.js +167 -0
- package/dist/vendor/core/context-shadow.d.ts +39 -0
- package/dist/vendor/core/context-shadow.js +200 -0
- package/dist/vendor/core/exit-signal.d.ts +88 -0
- package/dist/vendor/core/exit-signal.js +339 -0
- package/dist/vendor/core/exits.d.ts +41 -0
- package/dist/vendor/core/exits.js +258 -0
- package/dist/vendor/core/index.d.ts +42 -1
- package/dist/vendor/core/index.js +1086 -662
- package/dist/vendor/core/mission/aggregate.d.ts +27 -0
- package/dist/vendor/core/mission/aggregate.js +42 -0
- package/dist/vendor/core/mission/index.d.ts +2 -0
- package/dist/vendor/core/mission/index.js +1 -0
- package/dist/vendor/core/persistence/compiler.d.ts +8 -0
- package/dist/vendor/core/persistence/compiler.js +156 -0
- package/dist/vendor/core/persistence/context-handoff-store.d.ts +68 -0
- package/dist/vendor/core/persistence/context-handoff-store.js +112 -0
- package/dist/vendor/core/persistence/index.d.ts +4 -0
- package/dist/vendor/core/persistence/index.js +2 -0
- package/dist/vendor/core/persistence/ledger.d.ts +1 -1
- package/dist/vendor/core/persistence/mission-store.d.ts +56 -0
- package/dist/vendor/core/persistence/mission-store.js +281 -0
- package/dist/vendor/core/policy.d.ts +2 -0
- package/dist/vendor/core/policy.js +5 -2
- package/dist/vendor/core/rollback.js +15 -1
- package/dist/vendor/core/savings.d.ts +13 -0
- package/dist/vendor/core/savings.js +22 -0
- package/dist/vendor/core/termination-store.d.ts +15 -0
- package/dist/vendor/core/termination-store.js +44 -0
- package/dist/vendor/core/verified-handoff.d.ts +67 -0
- package/dist/vendor/core/verified-handoff.js +210 -0
- package/package.json +76 -83
- package/server.json +21 -21
- package/LICENSE +0 -216
- package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +0 -146
- package/dist/vendor/adapters/adapters/src/claude-cli.js +0 -1100
- package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +0 -103
- package/dist/vendor/adapters/adapters/src/cli-bridge.js +0 -569
- package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +0 -78
- package/dist/vendor/adapters/adapters/src/codex-launcher.js +0 -663
- package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +0 -10
- package/dist/vendor/adapters/adapters/src/direct-provider.js +0 -40
- package/dist/vendor/adapters/adapters/src/index.d.ts +0 -8
- package/dist/vendor/adapters/adapters/src/index.js +0 -8
- package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +0 -62
- package/dist/vendor/adapters/adapters/src/openai-compatible.js +0 -306
- package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +0 -17
- package/dist/vendor/adapters/adapters/src/runtime-support.js +0 -59
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +0 -8
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +0 -40
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +0 -8
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +0 -12
- package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +0 -9
- package/dist/vendor/adapters/adapters/src/verifier-only.js +0 -63
- package/dist/vendor/adapters/contracts/src/context-graph.d.ts +0 -48
- package/dist/vendor/adapters/contracts/src/context-graph.js +0 -26
- package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +0 -78
- package/dist/vendor/adapters/contracts/src/execution-policy.js +0 -43
- package/dist/vendor/adapters/contracts/src/governance.d.ts +0 -24
- package/dist/vendor/adapters/contracts/src/governance.js +0 -11
- package/dist/vendor/adapters/contracts/src/identity.d.ts +0 -29
- package/dist/vendor/adapters/contracts/src/identity.js +0 -30
- package/dist/vendor/adapters/contracts/src/index.d.ts +0 -412
- package/dist/vendor/adapters/contracts/src/index.js +0 -225
- package/dist/vendor/adapters/contracts/src/operator.d.ts +0 -19
- package/dist/vendor/adapters/contracts/src/operator.js +0 -11
- package/dist/vendor/adapters/contracts/src/trajectory.d.ts +0 -26
- package/dist/vendor/adapters/contracts/src/trajectory.js +0 -27
- package/dist/vendor/adapters/core/src/compiler.d.ts +0 -52
- package/dist/vendor/adapters/core/src/compiler.js +0 -52
- package/dist/vendor/adapters/core/src/context-graph.d.ts +0 -3
- package/dist/vendor/adapters/core/src/context-graph.js +0 -371
- package/dist/vendor/adapters/core/src/context-integrity.d.ts +0 -26
- package/dist/vendor/adapters/core/src/context-integrity.js +0 -80
- package/dist/vendor/adapters/core/src/grounding.d.ts +0 -38
- package/dist/vendor/adapters/core/src/grounding.js +0 -273
- package/dist/vendor/adapters/core/src/identity.d.ts +0 -20
- package/dist/vendor/adapters/core/src/identity.js +0 -86
- package/dist/vendor/adapters/core/src/index.d.ts +0 -186
- package/dist/vendor/adapters/core/src/index.js +0 -1495
- package/dist/vendor/adapters/core/src/leash.d.ts +0 -48
- package/dist/vendor/adapters/core/src/leash.js +0 -497
- package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +0 -18
- package/dist/vendor/adapters/core/src/persistence/compiler.js +0 -34
- package/dist/vendor/adapters/core/src/persistence/index.d.ts +0 -10
- package/dist/vendor/adapters/core/src/persistence/index.js +0 -5
- package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +0 -39
- package/dist/vendor/adapters/core/src/persistence/integrity.js +0 -252
- package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +0 -23
- package/dist/vendor/adapters/core/src/persistence/ledger.js +0 -9
- package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +0 -63
- package/dist/vendor/adapters/core/src/persistence/memory-store.js +0 -115
- package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +0 -66
- package/dist/vendor/adapters/core/src/persistence/runs-reader.js +0 -140
- package/dist/vendor/adapters/core/src/persistence/store.d.ts +0 -89
- package/dist/vendor/adapters/core/src/persistence/store.js +0 -125
- package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +0 -85
- package/dist/vendor/adapters/core/src/persistence/trace-store.js +0 -125
- package/dist/vendor/adapters/core/src/policy-compiler.d.ts +0 -2
- package/dist/vendor/adapters/core/src/policy-compiler.js +0 -195
- package/dist/vendor/adapters/core/src/policy.d.ts +0 -156
- package/dist/vendor/adapters/core/src/policy.js +0 -704
- package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +0 -53
- package/dist/vendor/adapters/core/src/repo-analyzer.js +0 -263
- package/dist/vendor/adapters/core/src/rollback.d.ts +0 -15
- package/dist/vendor/adapters/core/src/rollback.js +0 -253
- package/dist/vendor/adapters/core/src/routing.d.ts +0 -82
- package/dist/vendor/adapters/core/src/routing.js +0 -336
- package/dist/vendor/adapters/core/src/test-integrity.d.ts +0 -64
- package/dist/vendor/adapters/core/src/test-integrity.js +0 -197
- package/dist/vendor/adapters/core/src/trajectory.d.ts +0 -18
- package/dist/vendor/adapters/core/src/trajectory.js +0 -295
- package/dist/vendor/adapters/stub-agent-cli.d.ts +0 -8
- package/dist/vendor/adapters/stub-agent-cli.js +0 -40
- package/dist/vendor/adapters/verifier-only.d.ts +0 -9
- package/dist/vendor/adapters/verifier-only.js +0 -63
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
1
3
|
import { appendLoopEvent, createLoopRecord } from "../contracts/index.js";
|
|
2
4
|
import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe } from "./policy.js";
|
|
3
5
|
import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
|
|
@@ -5,13 +7,20 @@ import { buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGround
|
|
|
5
7
|
import { captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, restoreRollbackBoundary } from "./rollback.js";
|
|
6
8
|
import { compilePromptPacket } from "./compiler.js";
|
|
7
9
|
import { makeLedgerEvent, resolveRunsRoot, runDir } from "./persistence/index.js";
|
|
10
|
+
import { createDefaultExitPolicy, evaluateExitPolicy, hashProgressState, toLegacyExitDecision } from "./exits.js";
|
|
11
|
+
import { createFileExitSignalSource, startExitSignalMonitor } from "./exit-signal.js";
|
|
12
|
+
import { persistTerminationEnvelope } from "./termination-store.js";
|
|
13
|
+
import { calculateLoopAvoidedUsd } from "./savings.js";
|
|
8
14
|
import { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
9
15
|
import { decideCircuitBreak } from "./trajectory.js";
|
|
16
|
+
import { verifyContextHandoff, decideContextCircuitBreak } from "./context-handoff.js";
|
|
17
|
+
import { verifierActuallyPassed, } from "./verified-handoff.js";
|
|
10
18
|
export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, restoreRollbackBoundary };
|
|
11
19
|
export { buildContextGraphSnapshot, queryContextGraph } from "./context-graph.js";
|
|
12
20
|
export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, verifyIdentityToken } from "./identity.js";
|
|
13
21
|
export { compileExecutionPolicy } from "./policy-compiler.js";
|
|
14
22
|
export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
|
|
23
|
+
export { calculateAvoidedUsd, calculateLoopAvoidedUsd } from "./savings.js";
|
|
15
24
|
// ─── Routing economics ─────────────────────────────────────────────────────
|
|
16
25
|
export { classifyRoute, evaluatePreworkBurnPolicy, resolveModelForTier, selectBestEngine } from "./routing.js";
|
|
17
26
|
// ─── Test Integrity Validation ──────────────────────────────────────────────
|
|
@@ -22,9 +31,19 @@ export { analyzeRepoStyle, buildPromptDirectives } from "./repo-analyzer.js";
|
|
|
22
31
|
export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
23
32
|
// ─── Prompt packet compiler ──────────────────────────────────────────────────
|
|
24
33
|
export { compilePromptPacket } from "./compiler.js";
|
|
34
|
+
// ─── Verified Handoff builder ────────────────────────────────────────────────
|
|
35
|
+
export { buildVerifiedHandoff, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
|
|
25
36
|
// ─── Persistence (RunStore, LedgerEvent, FileRunStore) ──────────────────────
|
|
26
37
|
export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
|
|
27
38
|
export { compileAndPersistContext } from "./persistence/index.js";
|
|
39
|
+
// ─── Context Shadow — A-CTX-0 ────────────────────────────────────────────────
|
|
40
|
+
export { compileContextShadow, estimateContextTokens } from "./context-shadow.js";
|
|
41
|
+
// ─── Context Compiler — A-CTX-1 ──────────────────────────────────────────────
|
|
42
|
+
export { compileContext, HEURISTIC_ADAPTER, MAX_RENDER_PASSES } from "./context-compiler.js";
|
|
43
|
+
// ─── Context Handoff — A-CTX-2 ───────────────────────────────────────────────
|
|
44
|
+
export { verifyContextHandoff, decideContextCircuitBreak } from "./context-handoff.js";
|
|
45
|
+
// ─── Context Chain Gate — C1 ─────────────────────────────────────────────────
|
|
46
|
+
export { evaluateChainGate, renderGatePrComment } from "./context-chain-gate.js";
|
|
28
47
|
// ─── MartinLoop Memory Store ────────────────────────────────────────────────
|
|
29
48
|
export { appendMemory, readMemoryEntries, getPreference, buildMemorySummary, recordPreference, recordConsent } from "./persistence/memory-store.js";
|
|
30
49
|
// ─── Trace Intelligence Store ───────────────────────────────────────────────
|
|
@@ -177,6 +196,75 @@ export async function runMartin(input) {
|
|
|
177
196
|
}));
|
|
178
197
|
await persistLoopRecordIfSupported(input.store, loop);
|
|
179
198
|
}
|
|
199
|
+
// ── A-CTX-2 context handoff gate ─────────────────────────────────────────
|
|
200
|
+
// The gate runs before ANY adapter call. A blocked handoff hard-stops the run
|
|
201
|
+
// and emits ledger evidence — the adapter is never invoked.
|
|
202
|
+
if (input.contextHandoff !== undefined) {
|
|
203
|
+
const handoffVerification = verifyContextHandoff({
|
|
204
|
+
handoff: input.contextHandoff,
|
|
205
|
+
producerReceiptVerified: input.producerReceiptVerified ?? false,
|
|
206
|
+
availableArtifacts: input.availableArtifacts ?? new Map()
|
|
207
|
+
});
|
|
208
|
+
if (input.store) {
|
|
209
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
210
|
+
kind: "context.handoff.received",
|
|
211
|
+
runId: loop.loopId,
|
|
212
|
+
payload: {
|
|
213
|
+
handoffId: input.contextHandoff.handoffId,
|
|
214
|
+
chainId: input.contextHandoff.chainId,
|
|
215
|
+
producerRunId: input.contextHandoff.producerRunId,
|
|
216
|
+
upstreamIntegrity: input.contextHandoff.upstreamIntegrity
|
|
217
|
+
}
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
const handoffGate = decideContextCircuitBreak(handoffVerification);
|
|
221
|
+
if (handoffGate.shouldStop) {
|
|
222
|
+
if (input.store) {
|
|
223
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
224
|
+
kind: "context.handoff.blocked",
|
|
225
|
+
runId: loop.loopId,
|
|
226
|
+
payload: {
|
|
227
|
+
reasonCode: handoffGate.reasonCode,
|
|
228
|
+
message: handoffGate.message,
|
|
229
|
+
integrity: handoffVerification.integrity,
|
|
230
|
+
reasons: handoffVerification.reasons
|
|
231
|
+
}
|
|
232
|
+
}));
|
|
233
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
234
|
+
kind: "run.exited",
|
|
235
|
+
runId: loop.loopId,
|
|
236
|
+
payload: {
|
|
237
|
+
status: "exited",
|
|
238
|
+
lifecycleState: "human_escalation",
|
|
239
|
+
reason: handoffGate.message ?? "Context handoff verification failed.",
|
|
240
|
+
reasonCode: handoffGate.reasonCode ?? "handoff_verification_failed"
|
|
241
|
+
}
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
const handoffExitDecision = {
|
|
245
|
+
shouldExit: true,
|
|
246
|
+
lifecycleState: "human_escalation",
|
|
247
|
+
status: "exited",
|
|
248
|
+
reason: handoffGate.message ?? "Context handoff verification failed.",
|
|
249
|
+
reasonCode: handoffGate.reasonCode ?? "handoff_verification_failed"
|
|
250
|
+
};
|
|
251
|
+
const finalizedLoop = finalizeLoop(loop, handoffExitDecision, now(), idFactory);
|
|
252
|
+
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
253
|
+
return { loop: finalizedLoop, decision: handoffExitDecision };
|
|
254
|
+
}
|
|
255
|
+
if (input.store) {
|
|
256
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
257
|
+
kind: "context.handoff.verified",
|
|
258
|
+
runId: loop.loopId,
|
|
259
|
+
payload: {
|
|
260
|
+
handoffId: input.contextHandoff.handoffId,
|
|
261
|
+
chainId: input.contextHandoff.chainId,
|
|
262
|
+
producerRunId: input.contextHandoff.producerRunId,
|
|
263
|
+
integrity: handoffVerification.integrity
|
|
264
|
+
}
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
180
268
|
const DEFAULT_FALLBACK_MODELS = [
|
|
181
269
|
"claude-haiku-4-5",
|
|
182
270
|
"claude-sonnet-4-6",
|
|
@@ -278,104 +366,381 @@ export async function runMartin(input) {
|
|
|
278
366
|
let firstDeltaFilePath;
|
|
279
367
|
let firstDeltaChangeType;
|
|
280
368
|
let preDeltaCostUsd = 0;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
369
|
+
// ── Eight-exit runtime control state ─────────────────────────────────────
|
|
370
|
+
const nowMs = input.nowMs ?? (() => Date.now());
|
|
371
|
+
const exitPolicy = createDefaultExitPolicy(input.budget, input.exitPolicy);
|
|
372
|
+
let activeAttemptController;
|
|
373
|
+
let observedHumanInterrupt;
|
|
374
|
+
let observedExternalEvent;
|
|
375
|
+
let terminationPromise;
|
|
376
|
+
const recentStateHashes = [];
|
|
377
|
+
let consecutiveErrors = 0;
|
|
378
|
+
// governedTurnsUsed starts from the already-completed attempt count so
|
|
379
|
+
// resumed runs count correctly; incremented once per governed attempt.
|
|
380
|
+
let governedTurnsUsed = loop.attempts.length;
|
|
381
|
+
const runsRoot = input.store?.runsRoot ?? resolveRunsRoot();
|
|
382
|
+
const exitSignalSource = input.exitSignalSource ?? createFileExitSignalSource(runsRoot);
|
|
383
|
+
// Diagnostic writes fire from sync callbacks — track them so the finally
|
|
384
|
+
// block can await completion before the run tears down.
|
|
385
|
+
const pendingControlWrites = [];
|
|
386
|
+
// Non-null when the signal monitor itself fails — rethrown at the next
|
|
387
|
+
// evaluateCheckpoint so the run surfaces the error rather than silently
|
|
388
|
+
// losing cancellation.
|
|
389
|
+
let signalMonitorError;
|
|
390
|
+
function observeSignals(signals) {
|
|
391
|
+
for (const s of signals) {
|
|
392
|
+
if (s.kind === "human_interrupt")
|
|
393
|
+
observedHumanInterrupt = s;
|
|
394
|
+
else if (s.kind === "external_event" && s.externalEvent !== undefined)
|
|
395
|
+
observedExternalEvent = s.externalEvent;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function queueControlDiagnostic(code, details) {
|
|
399
|
+
if (!input.store)
|
|
400
|
+
return;
|
|
401
|
+
pendingControlWrites.push(input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
402
|
+
kind: "run.diagnostic",
|
|
403
|
+
runId: loop.loopId,
|
|
404
|
+
payload: { code, details }
|
|
405
|
+
})));
|
|
406
|
+
}
|
|
407
|
+
const runController = new AbortController();
|
|
408
|
+
const wallTimer = exitPolicy.wallClock.maxElapsedMs > 0
|
|
409
|
+
? setTimeout(() => {
|
|
410
|
+
activeAttemptController?.abort(new Error("MartinLoop wall-clock exit reached"));
|
|
411
|
+
}, exitPolicy.wallClock.maxElapsedMs)
|
|
412
|
+
: undefined;
|
|
413
|
+
wallTimer?.unref?.();
|
|
414
|
+
const disposeSignalMonitor = startExitSignalMonitor({
|
|
415
|
+
source: exitSignalSource,
|
|
416
|
+
runId: loop.loopId,
|
|
417
|
+
controller: runController,
|
|
418
|
+
pollIntervalMs: input.exitSignalPollIntervalMs,
|
|
419
|
+
onSignal: (signals) => {
|
|
420
|
+
observeSignals(signals);
|
|
421
|
+
activeAttemptController?.abort(signals);
|
|
422
|
+
},
|
|
423
|
+
onDiagnostic: (diagnostics) => {
|
|
424
|
+
queueControlDiagnostic("exit_signal_invalid", diagnostics.map(({ kind, error }) => ({ kind, error })));
|
|
425
|
+
},
|
|
426
|
+
onError: (err) => {
|
|
427
|
+
signalMonitorError = err instanceof Error ? err : new Error(String(err));
|
|
428
|
+
activeAttemptController?.abort(err);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
// Snapshot of the current run state for eight-exit evaluation
|
|
432
|
+
function currentExitSnapshot(phase, result) {
|
|
433
|
+
return {
|
|
434
|
+
phase,
|
|
435
|
+
evaluatedAt: now(),
|
|
436
|
+
runStartedAtMs: runStartMs,
|
|
437
|
+
nowMs: nowMs(),
|
|
438
|
+
turnsUsed: governedTurnsUsed,
|
|
439
|
+
actualUsd: loop.cost.actualUsd,
|
|
440
|
+
tokensUsed: loop.cost.tokensIn + loop.cost.tokensOut,
|
|
441
|
+
recentStateHashes: [...recentStateHashes],
|
|
442
|
+
consecutiveErrors,
|
|
443
|
+
...(result !== undefined
|
|
444
|
+
? {
|
|
445
|
+
result: {
|
|
446
|
+
status: result.status,
|
|
447
|
+
verificationPassed: result.verificationPassed,
|
|
448
|
+
verifierScore: result.verificationPassed ? 1 : 0
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
: {}),
|
|
452
|
+
...(observedHumanInterrupt !== undefined ? { humanInterrupt: observedHumanInterrupt } : {}),
|
|
453
|
+
...(observedExternalEvent !== undefined ? { externalEvent: observedExternalEvent } : {})
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
// Idempotent termination: only the first caller writes the envelope (A1 fix)
|
|
457
|
+
function finishFromEvaluation(evaluation) {
|
|
458
|
+
if (terminationPromise !== undefined)
|
|
459
|
+
return terminationPromise;
|
|
460
|
+
const envelope = {
|
|
461
|
+
schemaVersion: "termination/1",
|
|
462
|
+
class: "operational_exit",
|
|
463
|
+
exit: evaluation
|
|
464
|
+
};
|
|
465
|
+
terminationPromise = (async () => {
|
|
302
466
|
if (input.store) {
|
|
467
|
+
// persistTerminationEnvelope is atomic and returns the winning envelope
|
|
468
|
+
// (may differ from ours if another path won the race — that's fine).
|
|
469
|
+
await persistTerminationEnvelope(runDir(runsRoot, loop.loopId), envelope);
|
|
303
470
|
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
304
|
-
kind: "
|
|
471
|
+
kind: "run.terminated",
|
|
305
472
|
runId: loop.loopId,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
473
|
+
payload: {
|
|
474
|
+
selectedExit: evaluation.primary,
|
|
475
|
+
matchedExits: evaluation.matched,
|
|
476
|
+
policyVersion: evaluation.policyVersion,
|
|
477
|
+
evaluationVersion: evaluation.schemaVersion,
|
|
478
|
+
terminationPhase: evaluation.phase
|
|
479
|
+
}
|
|
313
480
|
}));
|
|
314
481
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
482
|
+
return envelope;
|
|
483
|
+
})();
|
|
484
|
+
return terminationPromise;
|
|
485
|
+
}
|
|
486
|
+
async function evaluateCheckpoint(phase, result) {
|
|
487
|
+
// Surface signal-monitor failures immediately rather than silently losing cancellation
|
|
488
|
+
if (signalMonitorError !== undefined)
|
|
489
|
+
throw signalMonitorError;
|
|
490
|
+
const evaluation = evaluateExitPolicy(exitPolicy, currentExitSnapshot(phase, result));
|
|
491
|
+
return evaluation.shouldExit ? finishFromEvaluation(evaluation) : undefined;
|
|
492
|
+
}
|
|
493
|
+
async function terminateRun(envelope) {
|
|
494
|
+
if (envelope.class !== "operational_exit") {
|
|
495
|
+
throw new Error("terminateRun: unexpected non-operational envelope");
|
|
321
496
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
497
|
+
const legacyDecision = toLegacyExitDecision(envelope.exit, observedExternalEvent?.disposition);
|
|
498
|
+
const exitDecision = {
|
|
499
|
+
shouldExit: legacyDecision.shouldExit,
|
|
500
|
+
lifecycleState: legacyDecision.lifecycleState,
|
|
501
|
+
status: legacyDecision.status,
|
|
502
|
+
reason: legacyDecision.reason,
|
|
503
|
+
...(legacyDecision.reasonCode ? { reasonCode: legacyDecision.reasonCode } : {}),
|
|
504
|
+
...(legacyDecision.failureClass
|
|
505
|
+
? { failureClass: legacyDecision.failureClass }
|
|
506
|
+
: {})
|
|
507
|
+
};
|
|
508
|
+
if (input.store) {
|
|
509
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
510
|
+
kind: "run.exited",
|
|
511
|
+
runId: loop.loopId,
|
|
512
|
+
payload: createRunExitPayload(exitDecision)
|
|
513
|
+
}));
|
|
514
|
+
}
|
|
515
|
+
const routingInput = {
|
|
516
|
+
runStartMs,
|
|
517
|
+
firstDeltaDetected,
|
|
518
|
+
firstDeltaTimestampMs,
|
|
519
|
+
firstDeltaFilePath,
|
|
520
|
+
firstDeltaChangeType,
|
|
521
|
+
preDeltaCostUsd
|
|
522
|
+
};
|
|
523
|
+
// Drain all pending diagnostic ledger writes BEFORE signing the receipt.
|
|
524
|
+
// persistLoopRecordIfSupported reads ledger.jsonl to compute the integrity hash;
|
|
525
|
+
// any write that lands after that read produces a false ledger_hash_mismatch on verify.
|
|
526
|
+
if (pendingControlWrites.length > 0) {
|
|
527
|
+
await Promise.all(pendingControlWrites.splice(0));
|
|
528
|
+
}
|
|
529
|
+
const finalizedLoop = finalizeLoop(loop, exitDecision, now(), idFactory, routingInput);
|
|
530
|
+
const finalizedWithEnvelope = { ...finalizedLoop, terminationEnvelope: envelope };
|
|
531
|
+
await persistLoopRecordIfSupported(input.store, finalizedWithEnvelope);
|
|
532
|
+
return { loop: finalizedWithEnvelope, decision: exitDecision };
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
// Poll once synchronously before the first attempt so any signal written
|
|
536
|
+
// before the run started is observed immediately (not after the first poll interval).
|
|
537
|
+
const initial = await exitSignalSource.poll(loop.loopId);
|
|
538
|
+
observeSignals(initial.signals);
|
|
539
|
+
if (initial.diagnostics.length > 0) {
|
|
540
|
+
queueControlDiagnostic("exit_signal_invalid", initial.diagnostics);
|
|
541
|
+
}
|
|
542
|
+
// Pre-run check — catches signals written before the first attempt starts
|
|
543
|
+
const preRunTerminal = await evaluateCheckpoint("pre_run");
|
|
544
|
+
if (preRunTerminal !== undefined)
|
|
545
|
+
return await terminateRun(preRunTerminal);
|
|
546
|
+
while (loop.attempts.length < loop.budget.maxIterations) {
|
|
547
|
+
const distilled = distillContext(loop, {
|
|
548
|
+
maxRecentAttempts: useCompressedContext ? 1 : (input.maxRecentAttempts ?? 3)
|
|
549
|
+
});
|
|
550
|
+
useCompressedContext = false;
|
|
551
|
+
const attemptStartedAt = now();
|
|
552
|
+
const attemptId = makeId("att", idFactory);
|
|
553
|
+
const executingAdapter = currentAdapter;
|
|
554
|
+
const budgetPreflight = evaluateBudgetPreflight({
|
|
555
|
+
promptCharCount: distilled.focus.length + loop.task.objective.length * 3,
|
|
556
|
+
attemptCount: loop.attempts.length,
|
|
557
|
+
remainingBudgetUsd: distilled.constraints.remainingBudgetUsd,
|
|
558
|
+
perAttemptCapUsd: loop.budget.maxUsd * 0.25
|
|
559
|
+
});
|
|
560
|
+
if (!budgetPreflight.allowed) {
|
|
561
|
+
const preflightExitDecision = {
|
|
562
|
+
shouldExit: true,
|
|
563
|
+
lifecycleState: "budget_exit",
|
|
564
|
+
status: "exited",
|
|
565
|
+
reason: budgetPreflight.reason
|
|
566
|
+
};
|
|
567
|
+
if (input.store) {
|
|
568
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
569
|
+
kind: "attempt.rejected",
|
|
570
|
+
runId: loop.loopId,
|
|
571
|
+
attemptIndex: loop.attempts.length + 1,
|
|
572
|
+
payload: { reason: budgetPreflight.reason, source: "budget_preflight" }
|
|
573
|
+
}));
|
|
574
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
575
|
+
kind: "run.exited",
|
|
576
|
+
runId: loop.loopId,
|
|
577
|
+
payload: createRunExitPayload(preflightExitDecision)
|
|
578
|
+
}));
|
|
579
|
+
}
|
|
580
|
+
if (pendingControlWrites.length > 0) {
|
|
581
|
+
await Promise.all(pendingControlWrites.splice(0));
|
|
582
|
+
}
|
|
583
|
+
const finalizedLoop = finalizeLoop(loop, preflightExitDecision, now(), idFactory);
|
|
584
|
+
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
585
|
+
return {
|
|
586
|
+
loop: finalizedLoop,
|
|
587
|
+
decision: preflightExitDecision
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
// GATHER → ADMIT: run admission control before executing
|
|
591
|
+
currentPhase = "ADMIT";
|
|
592
|
+
// T05: Context Integrity Pre-gate — blocks authority inversion / injection before reasoning
|
|
593
|
+
//
|
|
594
|
+
// Untrusted "tool output" re-entering the loop is verifier command output from prior
|
|
595
|
+
// attempts (e.g. test runners echoing attacker-controlled strings). Pull that text from
|
|
596
|
+
// already-persisted verification.completed events so the gate scans what actually
|
|
597
|
+
// re-enters subsequent prompts, matching the documented "tool output / test output" scope.
|
|
598
|
+
const priorVerifierOutput = loop.events
|
|
599
|
+
.filter((event) => event.type === "verification.completed")
|
|
600
|
+
.flatMap((event) => event.payload?.steps ?? [])
|
|
601
|
+
.map((step) => step.detail)
|
|
602
|
+
.filter((detail) => Boolean(detail))
|
|
603
|
+
.join("\n---\n");
|
|
604
|
+
const contextPrecheck = await runContextIntegrityPrecheck(loop.loopId, loop.attempts.length + 1, runDir(resolveActiveRunsRoot(input.store), loop.loopId), {
|
|
605
|
+
userPrompt: distilled.focus,
|
|
606
|
+
toolOutput: priorVerifierOutput || undefined,
|
|
607
|
+
history: loop.attempts.map(a => a.summary).join("\n")
|
|
608
|
+
});
|
|
609
|
+
if (contextPrecheck.verdict === "context_poisoning_block") {
|
|
610
|
+
currentPhase = "ABORT";
|
|
611
|
+
const poisoningExitDecision = {
|
|
612
|
+
shouldExit: true,
|
|
613
|
+
lifecycleState: "human_escalation",
|
|
614
|
+
status: "exited",
|
|
615
|
+
reason: "Context Integrity Pre-gate: context poisoning attempt detected.",
|
|
616
|
+
failureClass: "safety_leash_blocked",
|
|
617
|
+
safetySurface: "context_integrity",
|
|
618
|
+
reasonCode: "context_poisoning_blocked"
|
|
619
|
+
};
|
|
620
|
+
if (input.store) {
|
|
621
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
622
|
+
kind: "safety.violations_found",
|
|
623
|
+
runId: loop.loopId,
|
|
624
|
+
payload: {
|
|
625
|
+
verdict: contextPrecheck.verdict,
|
|
626
|
+
signals: contextPrecheck.detectedSignals,
|
|
627
|
+
source: "context_integrity_pregate"
|
|
628
|
+
}
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
if (pendingControlWrites.length > 0) {
|
|
632
|
+
await Promise.all(pendingControlWrites.splice(0));
|
|
633
|
+
}
|
|
634
|
+
const finalizedLoop = finalizeLoop(loop, poisoningExitDecision, now(), idFactory);
|
|
635
|
+
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
636
|
+
return {
|
|
637
|
+
loop: finalizedLoop,
|
|
638
|
+
decision: poisoningExitDecision
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
const admissionDecision = evaluateAttemptPolicy({
|
|
642
|
+
request: {
|
|
643
|
+
loopId: loop.loopId,
|
|
644
|
+
workspaceId: loop.workspaceId,
|
|
645
|
+
attemptId,
|
|
646
|
+
context: {
|
|
647
|
+
taskTitle: loop.task.title,
|
|
648
|
+
objective: loop.task.objective,
|
|
649
|
+
verificationPlan: loop.task.verificationPlan,
|
|
650
|
+
...(loop.task.verificationStack ? { verificationStack: loop.task.verificationStack } : {}),
|
|
651
|
+
...(loop.task.definitionOfDonePreSatisfied !== undefined
|
|
652
|
+
? { definitionOfDonePreSatisfied: loop.task.definitionOfDonePreSatisfied }
|
|
653
|
+
: {}),
|
|
654
|
+
...(loop.task.repoRoot ? { repoRoot: loop.task.repoRoot } : {}),
|
|
655
|
+
...(loop.task.allowedPaths ? { allowedPaths: loop.task.allowedPaths } : {}),
|
|
656
|
+
...(loop.task.deniedPaths ? { deniedPaths: loop.task.deniedPaths } : {}),
|
|
657
|
+
...(loop.task.acceptanceCriteria ? { acceptanceCriteria: loop.task.acceptanceCriteria } : {}),
|
|
658
|
+
...(loop.task.executionProfile ? { executionProfile: loop.task.executionProfile } : {}),
|
|
659
|
+
...(loop.task.allowedNetworkDomains
|
|
660
|
+
? { allowedNetworkDomains: loop.task.allowedNetworkDomains }
|
|
661
|
+
: {}),
|
|
662
|
+
...(loop.task.approvalPolicy ? { approvalPolicy: loop.task.approvalPolicy } : {}),
|
|
663
|
+
focus: distilled.focus,
|
|
664
|
+
remainingBudgetUsd: distilled.constraints.remainingBudgetUsd,
|
|
665
|
+
remainingIterations: distilled.constraints.remainingIterations,
|
|
666
|
+
remainingTokens: distilled.constraints.remainingTokens
|
|
667
|
+
},
|
|
668
|
+
previousAttempts: loop.attempts
|
|
669
|
+
},
|
|
670
|
+
projectedUsd: budgetPreflight.estimate.estimatedAttemptCostUsd
|
|
671
|
+
});
|
|
672
|
+
if (!admissionDecision.allowed) {
|
|
673
|
+
const exitReason = admissionDecision.reason;
|
|
674
|
+
const exitDecision = {
|
|
675
|
+
shouldExit: true,
|
|
676
|
+
lifecycleState: admissionDecision.recommendedIntervention === "escalate_human"
|
|
677
|
+
? "human_escalation"
|
|
678
|
+
: "budget_exit",
|
|
679
|
+
status: "exited",
|
|
680
|
+
reason: exitReason
|
|
681
|
+
};
|
|
682
|
+
if (input.store) {
|
|
683
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
684
|
+
kind: "attempt.rejected",
|
|
685
|
+
runId: loop.loopId,
|
|
686
|
+
attemptIndex: loop.attempts.length + 1,
|
|
687
|
+
payload: { reason: admissionDecision.reason }
|
|
688
|
+
}));
|
|
689
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
690
|
+
kind: "run.exited",
|
|
691
|
+
runId: loop.loopId,
|
|
692
|
+
payload: createRunExitPayload(exitDecision)
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
695
|
+
if (pendingControlWrites.length > 0) {
|
|
696
|
+
await Promise.all(pendingControlWrites.splice(0));
|
|
697
|
+
}
|
|
698
|
+
const finalizedLoop = finalizeLoop(loop, exitDecision, now(), idFactory);
|
|
699
|
+
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
700
|
+
return {
|
|
701
|
+
loop: finalizedLoop,
|
|
702
|
+
decision: exitDecision
|
|
703
|
+
};
|
|
704
|
+
}
|
|
352
705
|
if (input.store) {
|
|
353
706
|
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
354
|
-
kind: "
|
|
707
|
+
kind: "attempt.admitted",
|
|
355
708
|
runId: loop.loopId,
|
|
709
|
+
attemptIndex: loop.attempts.length + 1,
|
|
356
710
|
payload: {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
711
|
+
attemptId,
|
|
712
|
+
adapterId: executingAdapter.adapterId,
|
|
713
|
+
providerId: executingAdapter.metadata.providerId,
|
|
714
|
+
model: executingAdapter.metadata.model,
|
|
715
|
+
transport: getAdapterTransport(executingAdapter)
|
|
360
716
|
}
|
|
361
717
|
}));
|
|
362
718
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
719
|
+
// ADMIT → PATCH
|
|
720
|
+
currentPhase = "PATCH";
|
|
721
|
+
loop = appendLoopEvent(loop, {
|
|
722
|
+
type: "attempt.started",
|
|
723
|
+
lifecycleState: "running",
|
|
724
|
+
payload: {
|
|
725
|
+
attemptId,
|
|
726
|
+
adapterId: executingAdapter.adapterId,
|
|
727
|
+
model: executingAdapter.metadata.model,
|
|
728
|
+
policyPhase: currentPhase
|
|
729
|
+
}
|
|
730
|
+
}, { now: attemptStartedAt, idFactory });
|
|
731
|
+
const request = {
|
|
372
732
|
loopId: loop.loopId,
|
|
733
|
+
workspaceId: loop.workspaceId,
|
|
373
734
|
attemptId,
|
|
374
735
|
context: {
|
|
375
736
|
taskTitle: loop.task.title,
|
|
376
737
|
objective: loop.task.objective,
|
|
377
738
|
verificationPlan: loop.task.verificationPlan,
|
|
378
739
|
...(loop.task.verificationStack ? { verificationStack: loop.task.verificationStack } : {}),
|
|
740
|
+
...(loop.task.mutationMode ? { mutationMode: loop.task.mutationMode } : {}),
|
|
741
|
+
...(loop.task.definitionOfDonePreSatisfied !== undefined
|
|
742
|
+
? { definitionOfDonePreSatisfied: loop.task.definitionOfDonePreSatisfied }
|
|
743
|
+
: {}),
|
|
379
744
|
...(loop.task.repoRoot ? { repoRoot: loop.task.repoRoot } : {}),
|
|
380
745
|
...(loop.task.allowedPaths ? { allowedPaths: loop.task.allowedPaths } : {}),
|
|
381
746
|
...(loop.task.deniedPaths ? { deniedPaths: loop.task.deniedPaths } : {}),
|
|
@@ -391,658 +756,659 @@ export async function runMartin(input) {
|
|
|
391
756
|
remainingTokens: distilled.constraints.remainingTokens
|
|
392
757
|
},
|
|
393
758
|
previousAttempts: loop.attempts
|
|
394
|
-
},
|
|
395
|
-
projectedUsd: budgetPreflight.estimate.estimatedAttemptCostUsd
|
|
396
|
-
});
|
|
397
|
-
if (!admissionDecision.allowed) {
|
|
398
|
-
const exitReason = admissionDecision.reason;
|
|
399
|
-
const exitDecision = {
|
|
400
|
-
shouldExit: true,
|
|
401
|
-
lifecycleState: admissionDecision.recommendedIntervention === "escalate_human"
|
|
402
|
-
? "human_escalation"
|
|
403
|
-
: "budget_exit",
|
|
404
|
-
status: "exited",
|
|
405
|
-
reason: exitReason
|
|
406
759
|
};
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
760
|
+
const tracksWorkspaceMutations = request.context.repoRoot !== undefined &&
|
|
761
|
+
executingAdapter.metadata.capabilities?.workspaceMutations !== false;
|
|
762
|
+
// Pre-attempt: check signals that arrived during guard evaluation
|
|
763
|
+
const preAttemptTerminal = await evaluateCheckpoint("pre_attempt");
|
|
764
|
+
if (preAttemptTerminal !== undefined)
|
|
765
|
+
return await terminateRun(preAttemptTerminal);
|
|
766
|
+
activeAttemptController = new AbortController();
|
|
767
|
+
governedTurnsUsed += 1;
|
|
768
|
+
const rollbackBoundary = tracksWorkspaceMutations
|
|
769
|
+
? await captureRollbackBoundary({
|
|
770
|
+
repoRoot: request.context.repoRoot,
|
|
771
|
+
capturedAt: attemptStartedAt
|
|
772
|
+
})
|
|
773
|
+
: undefined;
|
|
774
|
+
let result;
|
|
775
|
+
try {
|
|
776
|
+
result = await executingAdapter.execute({
|
|
777
|
+
...request,
|
|
778
|
+
signal: activeAttemptController.signal
|
|
779
|
+
});
|
|
419
780
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
currentPhase = "PATCH";
|
|
443
|
-
loop = appendLoopEvent(loop, {
|
|
444
|
-
type: "attempt.started",
|
|
445
|
-
lifecycleState: "running",
|
|
446
|
-
payload: {
|
|
781
|
+
catch (execError) {
|
|
782
|
+
// Convert an abort (wall-clock or signal-driven) into the correct exit
|
|
783
|
+
// rather than letting it propagate as an unhandled exception.
|
|
784
|
+
const abortTerminal = await evaluateCheckpoint("post_attempt");
|
|
785
|
+
if (abortTerminal !== undefined)
|
|
786
|
+
return await terminateRun(abortTerminal);
|
|
787
|
+
throw execError;
|
|
788
|
+
}
|
|
789
|
+
finally {
|
|
790
|
+
activeAttemptController = undefined;
|
|
791
|
+
}
|
|
792
|
+
const attemptCompletedAt = now();
|
|
793
|
+
const compiledContext = compilePromptPacket(request);
|
|
794
|
+
const expectedVerifierBinding = resolveExpectedVerifierBinding(request);
|
|
795
|
+
const verification = normalizeVerificationOutcome(result, expectedVerifierBinding);
|
|
796
|
+
// PATCH → VERIFY
|
|
797
|
+
currentPhase = "VERIFY";
|
|
798
|
+
let failure = result.status === "failed"
|
|
799
|
+
? classifyFailure({ attempts: loop.attempts, result })
|
|
800
|
+
: undefined;
|
|
801
|
+
const currentAttemptIndex = loop.attempts.length + 1;
|
|
802
|
+
const attempt = {
|
|
447
803
|
attemptId,
|
|
804
|
+
index: currentAttemptIndex,
|
|
448
805
|
adapterId: executingAdapter.adapterId,
|
|
449
806
|
model: executingAdapter.metadata.model,
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
context: {
|
|
457
|
-
taskTitle: loop.task.title,
|
|
458
|
-
objective: loop.task.objective,
|
|
459
|
-
verificationPlan: loop.task.verificationPlan,
|
|
460
|
-
...(loop.task.verificationStack ? { verificationStack: loop.task.verificationStack } : {}),
|
|
461
|
-
...(loop.task.mutationMode ? { mutationMode: loop.task.mutationMode } : {}),
|
|
462
|
-
...(loop.task.repoRoot ? { repoRoot: loop.task.repoRoot } : {}),
|
|
463
|
-
...(loop.task.allowedPaths ? { allowedPaths: loop.task.allowedPaths } : {}),
|
|
464
|
-
...(loop.task.deniedPaths ? { deniedPaths: loop.task.deniedPaths } : {}),
|
|
465
|
-
...(loop.task.acceptanceCriteria ? { acceptanceCriteria: loop.task.acceptanceCriteria } : {}),
|
|
466
|
-
...(loop.task.executionProfile ? { executionProfile: loop.task.executionProfile } : {}),
|
|
467
|
-
...(loop.task.allowedNetworkDomains
|
|
468
|
-
? { allowedNetworkDomains: loop.task.allowedNetworkDomains }
|
|
807
|
+
startedAt: attemptStartedAt,
|
|
808
|
+
completedAt: attemptCompletedAt,
|
|
809
|
+
summary: result.summary,
|
|
810
|
+
...(failure?.failureClass ? { failureClass: failure.failureClass } : {}),
|
|
811
|
+
...(failure?.recommendedIntervention
|
|
812
|
+
? { intervention: failure.recommendedIntervention }
|
|
469
813
|
: {}),
|
|
470
|
-
...(
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
?
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
completedAt: attemptCompletedAt,
|
|
503
|
-
summary: result.summary,
|
|
504
|
-
...(failure?.failureClass ? { failureClass: failure.failureClass } : {}),
|
|
505
|
-
...(failure?.recommendedIntervention
|
|
506
|
-
? { intervention: failure.recommendedIntervention }
|
|
507
|
-
: {}),
|
|
508
|
-
...(failure?.diagnosticHint ? { diagnosticHint: failure.diagnosticHint } : {})
|
|
509
|
-
};
|
|
510
|
-
loop = {
|
|
511
|
-
...loop,
|
|
512
|
-
attempts: [...loop.attempts, attempt],
|
|
513
|
-
cost: {
|
|
514
|
-
actualUsd: roundUsd(loop.cost.actualUsd + getUsageUsd(result.usage)),
|
|
515
|
-
avoidedUsd: loop.cost.avoidedUsd,
|
|
516
|
-
tokensIn: loop.cost.tokensIn + result.usage.tokensIn,
|
|
517
|
-
tokensOut: loop.cost.tokensOut + result.usage.tokensOut,
|
|
518
|
-
...(result.usage.estimatedUsd !== undefined
|
|
519
|
-
? {
|
|
520
|
-
estimatedUsd: roundUsd((loop.cost.estimatedUsd ?? loop.cost.actualUsd) + result.usage.estimatedUsd)
|
|
521
|
-
}
|
|
522
|
-
: {}),
|
|
523
|
-
provenance: mergeCostProvenance(loop.cost.provenance, getUsageProvenance(result.usage)),
|
|
524
|
-
...(result.usage.providerSettlement
|
|
525
|
-
? { providerSettlement: result.usage.providerSettlement }
|
|
526
|
-
: {})
|
|
527
|
-
},
|
|
528
|
-
updatedAt: attemptCompletedAt
|
|
529
|
-
};
|
|
530
|
-
loop = appendLoopEvent(loop, {
|
|
531
|
-
type: "attempt.completed",
|
|
532
|
-
lifecycleState: "running",
|
|
533
|
-
payload: { attemptId, status: result.status, summary: result.summary }
|
|
534
|
-
}, { now: attemptCompletedAt, idFactory });
|
|
535
|
-
const previousVerifierScore = getLastVerifierScore(loop);
|
|
536
|
-
if (failure) {
|
|
537
|
-
if (failure.recommendedIntervention === "compress_context") {
|
|
538
|
-
useCompressedContext = true;
|
|
539
|
-
}
|
|
540
|
-
let adapterSwitched = false;
|
|
541
|
-
if (failure.recommendedIntervention === "switch_adapter") {
|
|
542
|
-
const nextAdapter = adapterChain[currentAdapterIndex + 1];
|
|
543
|
-
if (nextAdapter) {
|
|
544
|
-
currentAdapterIndex += 1;
|
|
545
|
-
currentAdapter = nextAdapter;
|
|
546
|
-
adapterSwitched = true;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
if (failure.recommendedIntervention === "change_model" && currentAdapter.withModel) {
|
|
550
|
-
const fallbackModels = input.fallbackModels ?? DEFAULT_FALLBACK_MODELS;
|
|
551
|
-
const nextModel = fallbackModels[loop.attempts.length % fallbackModels.length];
|
|
552
|
-
if (nextModel) {
|
|
553
|
-
currentAdapter = currentAdapter.withModel(nextModel);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
loop = appendLoopEvent(loop, {
|
|
557
|
-
type: "failure.classified",
|
|
558
|
-
lifecycleState: "running",
|
|
559
|
-
payload: {
|
|
560
|
-
attemptId,
|
|
561
|
-
failureClass: failure.failureClass,
|
|
562
|
-
rationale: failure.rationale
|
|
563
|
-
}
|
|
564
|
-
}, { now: attemptCompletedAt, idFactory });
|
|
814
|
+
...(failure?.diagnosticHint ? { diagnosticHint: failure.diagnosticHint } : {})
|
|
815
|
+
};
|
|
816
|
+
loop = {
|
|
817
|
+
...loop,
|
|
818
|
+
attempts: [...loop.attempts, attempt],
|
|
819
|
+
cost: {
|
|
820
|
+
actualUsd: roundUsd(loop.cost.actualUsd + getUsageUsd(result.usage)),
|
|
821
|
+
avoidedUsd: loop.cost.avoidedUsd,
|
|
822
|
+
tokensIn: loop.cost.tokensIn + result.usage.tokensIn,
|
|
823
|
+
tokensOut: loop.cost.tokensOut + result.usage.tokensOut,
|
|
824
|
+
...(result.usage.estimatedUsd !== undefined
|
|
825
|
+
? {
|
|
826
|
+
estimatedUsd: roundUsd((loop.cost.estimatedUsd ?? loop.cost.actualUsd) + result.usage.estimatedUsd)
|
|
827
|
+
}
|
|
828
|
+
: {}),
|
|
829
|
+
provenance: mergeCostProvenance(loop.cost.provenance, getUsageProvenance(result.usage)),
|
|
830
|
+
...(result.usage.providerSettlement
|
|
831
|
+
? { providerSettlement: result.usage.providerSettlement }
|
|
832
|
+
: {})
|
|
833
|
+
},
|
|
834
|
+
updatedAt: attemptCompletedAt
|
|
835
|
+
};
|
|
836
|
+
// ── Eight-exit state tracking ─────────────────────────────────────────
|
|
837
|
+
consecutiveErrors = result.status === "failed" ? consecutiveErrors + 1 : 0;
|
|
838
|
+
const stateHash = hashProgressState({
|
|
839
|
+
summary: result.summary,
|
|
840
|
+
verificationPassed: verification.passed,
|
|
841
|
+
status: result.status
|
|
842
|
+
});
|
|
843
|
+
if (recentStateHashes.length >= exitPolicy.progress.windowSize)
|
|
844
|
+
recentStateHashes.shift();
|
|
845
|
+
recentStateHashes.push(stateHash);
|
|
565
846
|
loop = appendLoopEvent(loop, {
|
|
566
|
-
type: "
|
|
847
|
+
type: "attempt.completed",
|
|
567
848
|
lifecycleState: "running",
|
|
568
|
-
payload: { attemptId,
|
|
849
|
+
payload: { attemptId, status: result.status, summary: result.summary }
|
|
569
850
|
}, { now: attemptCompletedAt, idFactory });
|
|
570
|
-
|
|
851
|
+
// post_attempt check is deferred until after verification.completed and all
|
|
852
|
+
// per-attempt ledger writes — see the check just before before_retry below.
|
|
853
|
+
const previousVerifierScore = getLastVerifierScore(loop);
|
|
854
|
+
if (failure) {
|
|
855
|
+
if (failure.recommendedIntervention === "compress_context") {
|
|
856
|
+
useCompressedContext = true;
|
|
857
|
+
}
|
|
858
|
+
let adapterSwitched = false;
|
|
859
|
+
if (failure.recommendedIntervention === "switch_adapter") {
|
|
860
|
+
const nextAdapter = adapterChain[currentAdapterIndex + 1];
|
|
861
|
+
if (nextAdapter) {
|
|
862
|
+
currentAdapterIndex += 1;
|
|
863
|
+
currentAdapter = nextAdapter;
|
|
864
|
+
adapterSwitched = true;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (failure.recommendedIntervention === "change_model" && currentAdapter.withModel) {
|
|
868
|
+
const fallbackModels = input.fallbackModels ?? DEFAULT_FALLBACK_MODELS;
|
|
869
|
+
const nextModel = fallbackModels[loop.attempts.length % fallbackModels.length];
|
|
870
|
+
if (nextModel) {
|
|
871
|
+
currentAdapter = currentAdapter.withModel(nextModel);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
571
874
|
loop = appendLoopEvent(loop, {
|
|
572
|
-
type: "
|
|
875
|
+
type: "failure.classified",
|
|
573
876
|
lifecycleState: "running",
|
|
574
877
|
payload: {
|
|
575
878
|
attemptId,
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
transport: getAdapterTransport(currentAdapter)
|
|
879
|
+
failureClass: failure.failureClass,
|
|
880
|
+
rationale: failure.rationale
|
|
579
881
|
}
|
|
580
882
|
}, { now: attemptCompletedAt, idFactory });
|
|
883
|
+
loop = appendLoopEvent(loop, {
|
|
884
|
+
type: "intervention.selected",
|
|
885
|
+
lifecycleState: "running",
|
|
886
|
+
payload: { attemptId, intervention: failure.recommendedIntervention }
|
|
887
|
+
}, { now: attemptCompletedAt, idFactory });
|
|
888
|
+
if (adapterSwitched) {
|
|
889
|
+
loop = appendLoopEvent(loop, {
|
|
890
|
+
type: "intervention.selected",
|
|
891
|
+
lifecycleState: "running",
|
|
892
|
+
payload: {
|
|
893
|
+
attemptId,
|
|
894
|
+
intervention: "switch_adapter",
|
|
895
|
+
nextAdapterId: currentAdapter.adapterId,
|
|
896
|
+
transport: getAdapterTransport(currentAdapter)
|
|
897
|
+
}
|
|
898
|
+
}, { now: attemptCompletedAt, idFactory });
|
|
899
|
+
}
|
|
581
900
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
payload: {
|
|
587
|
-
attemptId,
|
|
588
|
-
attemptIndex: currentAttemptIndex,
|
|
589
|
-
passed: verification.passed,
|
|
590
|
-
summary: verification.summary,
|
|
591
|
-
...(verification.steps?.length ? { steps: verification.steps } : {}),
|
|
592
|
-
...(verification.warnings?.length ? { warnings: verification.warnings } : {})
|
|
593
|
-
}
|
|
594
|
-
}, { now: attemptCompletedAt, idFactory });
|
|
595
|
-
const costState = evaluateCostGovernor({
|
|
596
|
-
budget: loop.budget,
|
|
597
|
-
cost: loop.cost,
|
|
598
|
-
attemptsUsed: loop.attempts.length
|
|
599
|
-
});
|
|
600
|
-
loop = appendLoopEvent(loop, {
|
|
601
|
-
type: "budget.updated",
|
|
602
|
-
lifecycleState: costState.shouldStop ? "budget_exit" : "running",
|
|
603
|
-
payload: {
|
|
604
|
-
actualUsd: loop.cost.actualUsd,
|
|
605
|
-
remainingBudgetUsd: costState.remainingBudgetUsd,
|
|
606
|
-
pressure: costState.pressure,
|
|
607
|
-
provenance: loop.cost.provenance
|
|
608
|
-
}
|
|
609
|
-
}, { now: now(), idFactory });
|
|
610
|
-
if (input.store) {
|
|
611
|
-
const settlement = createBudgetSettlement({
|
|
612
|
-
runId: loop.loopId,
|
|
613
|
-
attemptIndex: currentAttemptIndex,
|
|
614
|
-
usage: result.usage,
|
|
615
|
-
estimate: budgetPreflight.estimate,
|
|
616
|
-
settledAt: attemptCompletedAt
|
|
901
|
+
const costState = evaluateCostGovernor({
|
|
902
|
+
budget: loop.budget,
|
|
903
|
+
cost: loop.cost,
|
|
904
|
+
attemptsUsed: loop.attempts.length
|
|
617
905
|
});
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
...(rollbackBoundary ? { rollbackBoundary } : {})
|
|
622
|
-
});
|
|
623
|
-
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
624
|
-
kind: "patch.generated",
|
|
625
|
-
runId: loop.loopId,
|
|
626
|
-
attemptIndex: currentAttemptIndex,
|
|
627
|
-
payload: { status: result.status, summary: result.summary }
|
|
628
|
-
}));
|
|
629
|
-
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
630
|
-
kind: "verification.completed",
|
|
631
|
-
runId: loop.loopId,
|
|
632
|
-
attemptIndex: currentAttemptIndex,
|
|
633
|
-
payload: {
|
|
634
|
-
attemptId,
|
|
635
|
-
passed: verification.passed,
|
|
636
|
-
summary: verification.summary,
|
|
637
|
-
...(verification.steps?.length ? { steps: verification.steps } : {}),
|
|
638
|
-
...(verification.warnings?.length ? { warnings: verification.warnings } : {})
|
|
639
|
-
}
|
|
640
|
-
}));
|
|
641
|
-
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
642
|
-
kind: "budget.settled",
|
|
643
|
-
runId: loop.loopId,
|
|
644
|
-
attemptIndex: currentAttemptIndex,
|
|
906
|
+
loop = appendLoopEvent(loop, {
|
|
907
|
+
type: "budget.updated",
|
|
908
|
+
lifecycleState: costState.shouldStop ? "budget_exit" : "running",
|
|
645
909
|
payload: {
|
|
646
|
-
actualUsd:
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
cachedInputTokens: result.usage.cachedInputTokens,
|
|
651
|
-
reasoningTokensOut: result.usage.reasoningTokensOut,
|
|
652
|
-
provenance: getUsageProvenance(result.usage),
|
|
653
|
-
transport: getAdapterTransport(executingAdapter),
|
|
654
|
-
providerId: executingAdapter.metadata.providerId,
|
|
655
|
-
model: executingAdapter.metadata.model,
|
|
656
|
-
providerSettlement: result.usage.providerSettlement,
|
|
657
|
-
patchCost: settlement.patchCost,
|
|
658
|
-
verificationCost: settlement.verificationCost,
|
|
659
|
-
varianceUsd: settlement.varianceUsd,
|
|
660
|
-
preflightEstimateUsd: settlement.preflightEstimateUsd
|
|
910
|
+
actualUsd: loop.cost.actualUsd,
|
|
911
|
+
remainingBudgetUsd: costState.remainingBudgetUsd,
|
|
912
|
+
pressure: costState.pressure,
|
|
913
|
+
provenance: loop.cost.provenance
|
|
661
914
|
}
|
|
662
|
-
}));
|
|
663
|
-
}
|
|
664
|
-
const changedFiles = tracksWorkspaceMutations
|
|
665
|
-
? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary)
|
|
666
|
-
: [];
|
|
667
|
-
// Routing economics: detect first meaningful workspace delta
|
|
668
|
-
if (!firstDeltaDetected && changedFiles.length > 0) {
|
|
669
|
-
const meaningfulFile = changedFiles.find((f) => !isMartinLoopMetadata(f));
|
|
670
|
-
if (meaningfulFile) {
|
|
671
|
-
firstDeltaDetected = true;
|
|
672
|
-
firstDeltaTimestampMs = Date.now();
|
|
673
|
-
firstDeltaFilePath = meaningfulFile;
|
|
674
|
-
firstDeltaChangeType = "modify";
|
|
675
|
-
preDeltaCostUsd = roundUsd(loop.cost.actualUsd - getUsageUsd(result.usage));
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
if (!firstDeltaDetected) {
|
|
679
|
-
preDeltaCostUsd = loop.cost.actualUsd;
|
|
680
|
-
}
|
|
681
|
-
// Evidence is only reliable when the adapter explicitly reported files OR git actually
|
|
682
|
-
// returned a non-empty list. A repoRoot alone is insufficient — git may fail (e.g. not
|
|
683
|
-
// a git repo) and silently return [], which would falsely trigger no_code_change.
|
|
684
|
-
const changedFileEvidenceAvailable = result.execution?.changedFiles !== undefined || changedFiles.length > 0;
|
|
685
|
-
const isVerifierOnlyAdapter = executingAdapter.adapterId === "direct:verifier:verify-only";
|
|
686
|
-
const patchTruthCountsEdits = !isVerifierOnlyAdapter && changedFileEvidenceAvailable;
|
|
687
|
-
const filesystemDecision = evaluateFilesystemLeash({
|
|
688
|
-
repoRoot: request.context.repoRoot,
|
|
689
|
-
changedFiles,
|
|
690
|
-
allowedPaths: request.context.allowedPaths,
|
|
691
|
-
deniedPaths: request.context.deniedPaths
|
|
692
|
-
});
|
|
693
|
-
if (!filesystemDecision.allowed) {
|
|
694
|
-
const patchDecision = evaluatePatchDecision({
|
|
695
|
-
verificationPassed: result.verification.passed,
|
|
696
|
-
previousVerifierScore,
|
|
697
|
-
verifierScore: result.verification.passed ? 1 : 0,
|
|
698
|
-
scopeViolationCount: filesystemDecision.violations.length,
|
|
699
|
-
changedFileCount: changedFiles.length,
|
|
700
|
-
diffNovelty: changedFiles.length > 0 ? 1 : 0,
|
|
701
|
-
diffStats: result.execution?.diffStats,
|
|
702
|
-
costUsd: getUsageUsd(result.usage),
|
|
703
|
-
summary: result.summary
|
|
704
|
-
});
|
|
705
|
-
const filesystemExitDecision = {
|
|
706
|
-
shouldExit: true,
|
|
707
|
-
lifecycleState: "human_escalation",
|
|
708
|
-
status: "exited",
|
|
709
|
-
reason: filesystemDecision.reason ?? "Safety leash blocked filesystem changes.",
|
|
710
|
-
...classifySafetyLeashExit(filesystemDecision, "filesystem")
|
|
711
|
-
};
|
|
712
|
-
const rollbackOutcome = await restoreRollbackBoundary({
|
|
713
|
-
repoRoot: request.context.repoRoot,
|
|
714
|
-
boundary: rollbackBoundary,
|
|
715
|
-
restoredAt: attemptCompletedAt,
|
|
716
|
-
decision: patchDecision.decision
|
|
717
|
-
});
|
|
915
|
+
}, { now: now(), idFactory });
|
|
718
916
|
if (input.store) {
|
|
917
|
+
const settlement = createBudgetSettlement({
|
|
918
|
+
runId: loop.loopId,
|
|
919
|
+
attemptIndex: currentAttemptIndex,
|
|
920
|
+
usage: result.usage,
|
|
921
|
+
estimate: budgetPreflight.estimate,
|
|
922
|
+
settledAt: attemptCompletedAt
|
|
923
|
+
});
|
|
719
924
|
await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
|
|
720
925
|
compiledContext,
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
patchDecision: toPatchDecisionArtifact(patchDecision),
|
|
724
|
-
...(rollbackBoundary ? { rollbackBoundary } : {}),
|
|
725
|
-
...(rollbackOutcome ? { rollbackOutcome } : {})
|
|
926
|
+
verification,
|
|
927
|
+
...(rollbackBoundary ? { rollbackBoundary } : {})
|
|
726
928
|
});
|
|
727
929
|
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
728
|
-
kind: "
|
|
930
|
+
kind: "patch.generated",
|
|
729
931
|
runId: loop.loopId,
|
|
730
932
|
attemptIndex: currentAttemptIndex,
|
|
731
|
-
payload: {
|
|
732
|
-
surface: "filesystem",
|
|
733
|
-
blocked: true,
|
|
734
|
-
attemptIndex: currentAttemptIndex,
|
|
735
|
-
violations: filesystemDecision.violations
|
|
736
|
-
}
|
|
933
|
+
payload: { status: result.status, summary: result.summary }
|
|
737
934
|
}));
|
|
738
935
|
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
739
|
-
kind: "
|
|
936
|
+
kind: "budget.settled",
|
|
740
937
|
runId: loop.loopId,
|
|
741
938
|
attemptIndex: currentAttemptIndex,
|
|
742
939
|
payload: {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
940
|
+
actualUsd: settlement.totalActualUsd,
|
|
941
|
+
estimatedUsd: result.usage.estimatedUsd,
|
|
942
|
+
tokensIn: result.usage.tokensIn,
|
|
943
|
+
tokensOut: result.usage.tokensOut,
|
|
944
|
+
cachedInputTokens: result.usage.cachedInputTokens,
|
|
945
|
+
reasoningTokensOut: result.usage.reasoningTokensOut,
|
|
946
|
+
provenance: getUsageProvenance(result.usage),
|
|
947
|
+
transport: getAdapterTransport(executingAdapter),
|
|
948
|
+
providerId: executingAdapter.metadata.providerId,
|
|
949
|
+
model: executingAdapter.metadata.model,
|
|
950
|
+
providerSettlement: result.usage.providerSettlement,
|
|
951
|
+
patchCost: settlement.patchCost,
|
|
952
|
+
verificationCost: settlement.verificationCost,
|
|
953
|
+
varianceUsd: settlement.varianceUsd,
|
|
954
|
+
preflightEstimateUsd: settlement.preflightEstimateUsd
|
|
747
955
|
}
|
|
748
956
|
}));
|
|
749
|
-
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
750
|
-
kind: "run.exited",
|
|
751
|
-
runId: loop.loopId,
|
|
752
|
-
payload: createRunExitPayload(filesystemExitDecision)
|
|
753
|
-
}));
|
|
754
957
|
}
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
humanApprovalRequired: true,
|
|
778
|
-
summary: result.summary
|
|
779
|
-
});
|
|
780
|
-
const approvalExitDecision = {
|
|
781
|
-
shouldExit: true,
|
|
782
|
-
lifecycleState: "human_escalation",
|
|
783
|
-
status: "exited",
|
|
784
|
-
reason: changeApprovalDecision.reason ??
|
|
785
|
-
"Safety leash blocked dependency or migration changes that require approval.",
|
|
786
|
-
...classifySafetyLeashExit(changeApprovalDecision, "dependency")
|
|
787
|
-
};
|
|
788
|
-
const rollbackOutcome = await restoreRollbackBoundary({
|
|
958
|
+
const changedFiles = tracksWorkspaceMutations
|
|
959
|
+
? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary)
|
|
960
|
+
: [];
|
|
961
|
+
// Routing economics: detect first meaningful workspace delta
|
|
962
|
+
if (!firstDeltaDetected && changedFiles.length > 0) {
|
|
963
|
+
const meaningfulFile = changedFiles.find((f) => !isMartinLoopMetadata(f));
|
|
964
|
+
if (meaningfulFile) {
|
|
965
|
+
firstDeltaDetected = true;
|
|
966
|
+
firstDeltaTimestampMs = Date.now();
|
|
967
|
+
firstDeltaFilePath = meaningfulFile;
|
|
968
|
+
firstDeltaChangeType = "modify";
|
|
969
|
+
preDeltaCostUsd = roundUsd(loop.cost.actualUsd - getUsageUsd(result.usage));
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (!firstDeltaDetected) {
|
|
973
|
+
preDeltaCostUsd = loop.cost.actualUsd;
|
|
974
|
+
}
|
|
975
|
+
// Evidence is only reliable when the adapter explicitly reported files OR git actually
|
|
976
|
+
// returned a non-empty list. A repoRoot alone is insufficient — git may fail (e.g. not
|
|
977
|
+
// a git repo) and silently return [], which would falsely trigger no_code_change.
|
|
978
|
+
const changedFileEvidenceAvailable = result.execution?.changedFiles !== undefined || changedFiles.length > 0;
|
|
979
|
+
const filesystemDecision = evaluateFilesystemLeash({
|
|
789
980
|
repoRoot: request.context.repoRoot,
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
981
|
+
changedFiles,
|
|
982
|
+
allowedPaths: request.context.allowedPaths,
|
|
983
|
+
deniedPaths: request.context.deniedPaths
|
|
984
|
+
});
|
|
985
|
+
if (!filesystemDecision.allowed) {
|
|
986
|
+
const patchDecision = evaluatePatchDecision({
|
|
987
|
+
verificationPassed: result.verification.passed,
|
|
988
|
+
previousVerifierScore,
|
|
989
|
+
verifierScore: result.verification.passed ? 1 : 0,
|
|
990
|
+
scopeViolationCount: filesystemDecision.violations.length,
|
|
991
|
+
changedFileCount: changedFiles.length,
|
|
992
|
+
diffNovelty: changedFiles.length > 0 ? 1 : 0,
|
|
993
|
+
diffStats: result.execution?.diffStats,
|
|
994
|
+
costUsd: getUsageUsd(result.usage),
|
|
995
|
+
summary: result.summary
|
|
996
|
+
});
|
|
997
|
+
const filesystemExitDecision = {
|
|
998
|
+
shouldExit: true,
|
|
999
|
+
lifecycleState: "human_escalation",
|
|
1000
|
+
status: "exited",
|
|
1001
|
+
reason: filesystemDecision.reason ?? "Safety leash blocked filesystem changes.",
|
|
1002
|
+
...classifySafetyLeashExit(filesystemDecision, "filesystem")
|
|
1003
|
+
};
|
|
1004
|
+
const rollbackOutcome = await restoreRollbackBoundary({
|
|
1005
|
+
repoRoot: request.context.repoRoot,
|
|
1006
|
+
boundary: rollbackBoundary,
|
|
1007
|
+
restoredAt: attemptCompletedAt,
|
|
1008
|
+
decision: patchDecision.decision
|
|
1009
|
+
});
|
|
1010
|
+
if (input.store) {
|
|
1011
|
+
await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
|
|
1012
|
+
compiledContext,
|
|
1013
|
+
leash: createLeashArtifact(filesystemDecision, currentAttemptIndex),
|
|
1014
|
+
patchScore: patchDecision.score,
|
|
1015
|
+
patchDecision: toPatchDecisionArtifact(patchDecision),
|
|
1016
|
+
...(rollbackBoundary ? { rollbackBoundary } : {}),
|
|
1017
|
+
...(rollbackOutcome ? { rollbackOutcome } : {})
|
|
1018
|
+
});
|
|
1019
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1020
|
+
kind: "safety.violations_found",
|
|
1021
|
+
runId: loop.loopId,
|
|
1022
|
+
attemptIndex: currentAttemptIndex,
|
|
1023
|
+
payload: {
|
|
1024
|
+
surface: "filesystem",
|
|
1025
|
+
blocked: true,
|
|
1026
|
+
attemptIndex: currentAttemptIndex,
|
|
1027
|
+
violations: filesystemDecision.violations
|
|
1028
|
+
}
|
|
1029
|
+
}));
|
|
1030
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1031
|
+
kind: "attempt.discarded",
|
|
1032
|
+
runId: loop.loopId,
|
|
1033
|
+
attemptIndex: currentAttemptIndex,
|
|
1034
|
+
payload: {
|
|
1035
|
+
decision: patchDecision.decision,
|
|
1036
|
+
reason: patchDecision.summary,
|
|
1037
|
+
reasonCodes: patchDecision.reasonCodes,
|
|
1038
|
+
score: patchDecision.score.score
|
|
1039
|
+
}
|
|
1040
|
+
}));
|
|
1041
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1042
|
+
kind: "run.exited",
|
|
1043
|
+
runId: loop.loopId,
|
|
1044
|
+
payload: createRunExitPayload(filesystemExitDecision)
|
|
1045
|
+
}));
|
|
1046
|
+
}
|
|
1047
|
+
if (pendingControlWrites.length > 0) {
|
|
1048
|
+
await Promise.all(pendingControlWrites.splice(0));
|
|
1049
|
+
}
|
|
1050
|
+
const finalizedLoop = finalizeLoop(loop, filesystemExitDecision, now(), idFactory);
|
|
1051
|
+
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
1052
|
+
return {
|
|
1053
|
+
loop: finalizedLoop,
|
|
1054
|
+
decision: filesystemExitDecision
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
const changeApprovalDecision = evaluateChangeApprovalLeash({
|
|
1058
|
+
changedFiles,
|
|
1059
|
+
executionProfile: request.context.executionProfile,
|
|
1060
|
+
approvalPolicy: request.context.approvalPolicy
|
|
793
1061
|
});
|
|
1062
|
+
if (!changeApprovalDecision.allowed) {
|
|
1063
|
+
const patchDecision = evaluatePatchDecision({
|
|
1064
|
+
verificationPassed: result.verification.passed,
|
|
1065
|
+
previousVerifierScore,
|
|
1066
|
+
verifierScore: result.verification.passed ? 1 : 0,
|
|
1067
|
+
safetyViolationCount: changeApprovalDecision.violations.length,
|
|
1068
|
+
changedFileCount: changedFiles.length,
|
|
1069
|
+
diffNovelty: changedFiles.length > 0 ? 1 : 0,
|
|
1070
|
+
diffStats: result.execution?.diffStats,
|
|
1071
|
+
costUsd: getUsageUsd(result.usage),
|
|
1072
|
+
humanApprovalRequired: true,
|
|
1073
|
+
summary: result.summary
|
|
1074
|
+
});
|
|
1075
|
+
const approvalExitDecision = {
|
|
1076
|
+
shouldExit: true,
|
|
1077
|
+
lifecycleState: "human_escalation",
|
|
1078
|
+
status: "exited",
|
|
1079
|
+
reason: changeApprovalDecision.reason ??
|
|
1080
|
+
"Safety leash blocked dependency or migration changes that require approval.",
|
|
1081
|
+
...classifySafetyLeashExit(changeApprovalDecision, "dependency")
|
|
1082
|
+
};
|
|
1083
|
+
const rollbackOutcome = await restoreRollbackBoundary({
|
|
1084
|
+
repoRoot: request.context.repoRoot,
|
|
1085
|
+
boundary: rollbackBoundary,
|
|
1086
|
+
restoredAt: attemptCompletedAt,
|
|
1087
|
+
decision: patchDecision.decision
|
|
1088
|
+
});
|
|
1089
|
+
if (input.store) {
|
|
1090
|
+
await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
|
|
1091
|
+
compiledContext,
|
|
1092
|
+
leash: createLeashArtifact(changeApprovalDecision, currentAttemptIndex),
|
|
1093
|
+
patchScore: patchDecision.score,
|
|
1094
|
+
patchDecision: toPatchDecisionArtifact(patchDecision),
|
|
1095
|
+
...(rollbackBoundary ? { rollbackBoundary } : {}),
|
|
1096
|
+
...(rollbackOutcome ? { rollbackOutcome } : {})
|
|
1097
|
+
});
|
|
1098
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1099
|
+
kind: "safety.violations_found",
|
|
1100
|
+
runId: loop.loopId,
|
|
1101
|
+
attemptIndex: currentAttemptIndex,
|
|
1102
|
+
payload: {
|
|
1103
|
+
surface: "dependency",
|
|
1104
|
+
blocked: true,
|
|
1105
|
+
profile: changeApprovalDecision.profile ?? executionProfile.name,
|
|
1106
|
+
attemptIndex: currentAttemptIndex,
|
|
1107
|
+
violations: changeApprovalDecision.violations
|
|
1108
|
+
}
|
|
1109
|
+
}));
|
|
1110
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1111
|
+
kind: "attempt.discarded",
|
|
1112
|
+
runId: loop.loopId,
|
|
1113
|
+
attemptIndex: currentAttemptIndex,
|
|
1114
|
+
payload: {
|
|
1115
|
+
decision: patchDecision.decision,
|
|
1116
|
+
reason: patchDecision.summary,
|
|
1117
|
+
reasonCodes: patchDecision.reasonCodes,
|
|
1118
|
+
score: patchDecision.score.score
|
|
1119
|
+
}
|
|
1120
|
+
}));
|
|
1121
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1122
|
+
kind: "run.exited",
|
|
1123
|
+
runId: loop.loopId,
|
|
1124
|
+
payload: createRunExitPayload(approvalExitDecision)
|
|
1125
|
+
}));
|
|
1126
|
+
}
|
|
1127
|
+
if (pendingControlWrites.length > 0) {
|
|
1128
|
+
await Promise.all(pendingControlWrites.splice(0));
|
|
1129
|
+
}
|
|
1130
|
+
const finalizedLoop = finalizeLoop(loop, approvalExitDecision, now(), idFactory);
|
|
1131
|
+
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
1132
|
+
return {
|
|
1133
|
+
loop: finalizedLoop,
|
|
1134
|
+
decision: approvalExitDecision
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
// VERIFY: Run grounding scan on patch diff if available
|
|
1138
|
+
// Uses the task's repoRoot to build/load the grounding index, then scans any diff
|
|
1139
|
+
let groundingScanResult;
|
|
1140
|
+
const patchDiff = buildPatchDiff(result, changedFiles);
|
|
1141
|
+
if (patchDiff && input.task.repoRoot) {
|
|
1142
|
+
try {
|
|
1143
|
+
const groundingIndex = await loadOrBuildRepoGroundingIndex(input.task.repoRoot);
|
|
1144
|
+
groundingScanResult = reconcileExistingGroundingFiles(scanPatchForGroundingViolations(patchDiff, groundingIndex, {
|
|
1145
|
+
allowedPaths: input.task.allowedPaths
|
|
1146
|
+
}), input.task.repoRoot, rollbackBoundary);
|
|
1147
|
+
if (input.store && groundingScanResult.violations.length > 0) {
|
|
1148
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1149
|
+
kind: "grounding.violations_found",
|
|
1150
|
+
runId: loop.loopId,
|
|
1151
|
+
attemptIndex: currentAttemptIndex,
|
|
1152
|
+
payload: {
|
|
1153
|
+
violationCount: groundingScanResult.violations.length,
|
|
1154
|
+
resolvedFiles: groundingScanResult.resolvedFiles,
|
|
1155
|
+
contentOnly: groundingScanResult.contentOnly,
|
|
1156
|
+
violations: groundingScanResult.violations.slice(0, 10)
|
|
1157
|
+
}
|
|
1158
|
+
}));
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
catch {
|
|
1162
|
+
// Grounding scan is best-effort — never fail the loop because of a scan error
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
const shouldEvaluatePatchTruth = result.status === "completed" && tracksWorkspaceMutations;
|
|
1166
|
+
let patchDecision;
|
|
1167
|
+
if (shouldEvaluatePatchTruth) {
|
|
1168
|
+
patchDecision = evaluatePatchDecision({
|
|
1169
|
+
verificationPassed: verification.passed,
|
|
1170
|
+
previousVerifierScore,
|
|
1171
|
+
verifierScore: verification.passed ? 1 : 0,
|
|
1172
|
+
groundingViolationCount: groundingScanResult?.violations.length ?? 0,
|
|
1173
|
+
changedFileCount: changedFileEvidenceAvailable ? changedFiles.length : undefined,
|
|
1174
|
+
mutationRequired: request.context.mutationMode === "edit",
|
|
1175
|
+
definitionOfDonePreSatisfied: request.context.definitionOfDonePreSatisfied,
|
|
1176
|
+
diffNovelty: changedFileEvidenceAvailable ? (changedFiles.length > 0 ? 1 : 0) : undefined,
|
|
1177
|
+
diffStats: result.execution?.diffStats,
|
|
1178
|
+
costUsd: getUsageUsd(result.usage),
|
|
1179
|
+
summary: result.summary
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
let rollbackOutcome;
|
|
1183
|
+
if (patchDecision && patchDecision.decision !== "KEEP") {
|
|
1184
|
+
rollbackOutcome = await restoreRollbackBoundary({
|
|
1185
|
+
repoRoot: request.context.repoRoot,
|
|
1186
|
+
boundary: rollbackBoundary,
|
|
1187
|
+
restoredAt: attemptCompletedAt,
|
|
1188
|
+
decision: patchDecision.decision
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
else if (result.status === "failed") {
|
|
1192
|
+
rollbackOutcome = await restoreRollbackBoundary({
|
|
1193
|
+
repoRoot: request.context.repoRoot,
|
|
1194
|
+
boundary: rollbackBoundary,
|
|
1195
|
+
restoredAt: attemptCompletedAt,
|
|
1196
|
+
decision: "DISCARD"
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
794
1199
|
if (input.store) {
|
|
795
1200
|
await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
|
|
796
1201
|
compiledContext,
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
patchDecision:
|
|
1202
|
+
...(patchDiff ? { diff: patchDiff } : {}),
|
|
1203
|
+
...(groundingScanResult ? { groundingScan: groundingScanResult } : {}),
|
|
1204
|
+
...(patchDecision ? { patchScore: patchDecision.score } : {}),
|
|
1205
|
+
...(patchDecision ? { patchDecision: toPatchDecisionArtifact(patchDecision) } : {}),
|
|
800
1206
|
...(rollbackBoundary ? { rollbackBoundary } : {}),
|
|
801
1207
|
...(rollbackOutcome ? { rollbackOutcome } : {})
|
|
802
1208
|
});
|
|
803
|
-
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
804
|
-
kind: "safety.violations_found",
|
|
805
|
-
runId: loop.loopId,
|
|
806
|
-
attemptIndex: currentAttemptIndex,
|
|
807
|
-
payload: {
|
|
808
|
-
surface: "dependency",
|
|
809
|
-
blocked: true,
|
|
810
|
-
profile: changeApprovalDecision.profile ?? executionProfile.name,
|
|
811
|
-
attemptIndex: currentAttemptIndex,
|
|
812
|
-
violations: changeApprovalDecision.violations
|
|
813
|
-
}
|
|
814
|
-
}));
|
|
815
|
-
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
816
|
-
kind: "attempt.discarded",
|
|
817
|
-
runId: loop.loopId,
|
|
818
|
-
attemptIndex: currentAttemptIndex,
|
|
819
|
-
payload: {
|
|
820
|
-
decision: patchDecision.decision,
|
|
821
|
-
reason: patchDecision.summary,
|
|
822
|
-
reasonCodes: patchDecision.reasonCodes,
|
|
823
|
-
score: patchDecision.score.score
|
|
824
|
-
}
|
|
825
|
-
}));
|
|
826
|
-
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
827
|
-
kind: "run.exited",
|
|
828
|
-
runId: loop.loopId,
|
|
829
|
-
payload: createRunExitPayload(approvalExitDecision)
|
|
830
|
-
}));
|
|
831
1209
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
return {
|
|
835
|
-
loop: finalizedLoop,
|
|
836
|
-
decision: approvalExitDecision
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
// VERIFY: Run grounding scan on patch diff if available
|
|
840
|
-
// Uses the task's repoRoot to build/load the grounding index, then scans any diff
|
|
841
|
-
let groundingScanResult;
|
|
842
|
-
const patchDiff = buildPatchDiff(result, changedFiles);
|
|
843
|
-
if (patchDiff && input.task.repoRoot) {
|
|
844
|
-
try {
|
|
845
|
-
const groundingIndex = await loadOrBuildRepoGroundingIndex(input.task.repoRoot);
|
|
846
|
-
groundingScanResult = scanPatchForGroundingViolations(patchDiff, groundingIndex, {
|
|
847
|
-
allowedPaths: input.task.allowedPaths
|
|
848
|
-
});
|
|
849
|
-
if (input.store && groundingScanResult.violations.length > 0) {
|
|
1210
|
+
if (input.store) {
|
|
1211
|
+
if (patchDecision) {
|
|
850
1212
|
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
851
|
-
kind: "
|
|
1213
|
+
kind: patchDecision.decision === "KEEP" ? "attempt.kept" : "attempt.discarded",
|
|
852
1214
|
runId: loop.loopId,
|
|
853
1215
|
attemptIndex: currentAttemptIndex,
|
|
854
1216
|
payload: {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1217
|
+
decision: patchDecision.decision,
|
|
1218
|
+
reason: patchDecision.summary,
|
|
1219
|
+
reasonCodes: patchDecision.reasonCodes,
|
|
1220
|
+
score: patchDecision.score.score
|
|
859
1221
|
}
|
|
860
1222
|
}));
|
|
861
1223
|
}
|
|
1224
|
+
else {
|
|
1225
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1226
|
+
kind: verification.passed ? "attempt.kept" : "attempt.discarded",
|
|
1227
|
+
runId: loop.loopId,
|
|
1228
|
+
attemptIndex: currentAttemptIndex,
|
|
1229
|
+
payload: { reason: verification.summary }
|
|
1230
|
+
}));
|
|
1231
|
+
}
|
|
862
1232
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
diffStats: result.execution?.diffStats,
|
|
877
|
-
costUsd: getUsageUsd(result.usage),
|
|
878
|
-
summary: result.summary
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
let rollbackOutcome;
|
|
882
|
-
if (patchDecision && patchDecision.decision !== "KEEP") {
|
|
883
|
-
rollbackOutcome = await restoreRollbackBoundary({
|
|
884
|
-
repoRoot: request.context.repoRoot,
|
|
885
|
-
boundary: rollbackBoundary,
|
|
886
|
-
restoredAt: attemptCompletedAt,
|
|
887
|
-
decision: patchDecision.decision
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
else if (result.status === "failed") {
|
|
891
|
-
rollbackOutcome = await restoreRollbackBoundary({
|
|
892
|
-
repoRoot: request.context.repoRoot,
|
|
893
|
-
boundary: rollbackBoundary,
|
|
894
|
-
restoredAt: attemptCompletedAt,
|
|
895
|
-
decision: "DISCARD"
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
if (input.store) {
|
|
899
|
-
await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
|
|
900
|
-
compiledContext,
|
|
901
|
-
...(patchDiff ? { diff: patchDiff } : {}),
|
|
902
|
-
...(groundingScanResult ? { groundingScan: groundingScanResult } : {}),
|
|
903
|
-
...(patchDecision ? { patchScore: patchDecision.score } : {}),
|
|
904
|
-
...(patchDecision ? { patchDecision: toPatchDecisionArtifact(patchDecision) } : {}),
|
|
905
|
-
...(rollbackBoundary ? { rollbackBoundary } : {}),
|
|
906
|
-
...(rollbackOutcome ? { rollbackOutcome } : {})
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
if (input.store) {
|
|
910
|
-
if (patchDecision) {
|
|
911
|
-
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
912
|
-
kind: patchDecision.decision === "KEEP" ? "attempt.kept" : "attempt.discarded",
|
|
913
|
-
runId: loop.loopId,
|
|
914
|
-
attemptIndex: currentAttemptIndex,
|
|
1233
|
+
if (patchDecision && patchDecision.decision !== "KEEP" && !failure) {
|
|
1234
|
+
failure = classifyPatchDecisionFailure(patchDecision, result);
|
|
1235
|
+
loop = applyPatchFailureToLoop(loop, {
|
|
1236
|
+
attemptId,
|
|
1237
|
+
summary: patchDecision.summary,
|
|
1238
|
+
failure
|
|
1239
|
+
});
|
|
1240
|
+
if (failure.recommendedIntervention === "compress_context") {
|
|
1241
|
+
useCompressedContext = true;
|
|
1242
|
+
}
|
|
1243
|
+
loop = appendLoopEvent(loop, {
|
|
1244
|
+
type: "failure.classified",
|
|
1245
|
+
lifecycleState: "running",
|
|
915
1246
|
payload: {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
score: patchDecision.score.score
|
|
1247
|
+
attemptId,
|
|
1248
|
+
failureClass: failure.failureClass,
|
|
1249
|
+
rationale: failure.rationale
|
|
920
1250
|
}
|
|
921
|
-
})
|
|
1251
|
+
}, { now: attemptCompletedAt, idFactory });
|
|
1252
|
+
loop = appendLoopEvent(loop, {
|
|
1253
|
+
type: "intervention.selected",
|
|
1254
|
+
lifecycleState: "running",
|
|
1255
|
+
payload: { attemptId, intervention: failure.recommendedIntervention }
|
|
1256
|
+
}, { now: attemptCompletedAt, idFactory });
|
|
922
1257
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
}
|
|
1258
|
+
if (patchDecision?.decision === "ESCALATE" || patchDecision?.decision === "HANDOFF") {
|
|
1259
|
+
const patchExitDecision = {
|
|
1260
|
+
shouldExit: true,
|
|
1261
|
+
lifecycleState: "human_escalation",
|
|
1262
|
+
status: "exited",
|
|
1263
|
+
reason: patchDecision.summary
|
|
1264
|
+
};
|
|
1265
|
+
if (input.store) {
|
|
1266
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1267
|
+
kind: "run.exited",
|
|
1268
|
+
runId: loop.loopId,
|
|
1269
|
+
payload: createRunExitPayload(patchExitDecision)
|
|
1270
|
+
}));
|
|
1271
|
+
}
|
|
1272
|
+
if (pendingControlWrites.length > 0) {
|
|
1273
|
+
await Promise.all(pendingControlWrites.splice(0));
|
|
1274
|
+
}
|
|
1275
|
+
const finalizedLoop = finalizeLoop(loop, patchExitDecision, now(), idFactory);
|
|
1276
|
+
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
1277
|
+
return {
|
|
1278
|
+
loop: finalizedLoop,
|
|
1279
|
+
decision: patchExitDecision
|
|
1280
|
+
};
|
|
930
1281
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1282
|
+
const effectiveResult = patchDecision && patchDecision.decision !== "KEEP"
|
|
1283
|
+
? {
|
|
1284
|
+
...result,
|
|
1285
|
+
status: "failed",
|
|
1286
|
+
summary: patchDecision.summary,
|
|
1287
|
+
verification: {
|
|
1288
|
+
...result.verification,
|
|
1289
|
+
passed: false,
|
|
1290
|
+
summary: patchDecision.summary
|
|
1291
|
+
},
|
|
1292
|
+
failure: {
|
|
1293
|
+
message: patchDecision.summary,
|
|
1294
|
+
...(failure?.failureClass ? { classHint: failure.failureClass } : {})
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
: result;
|
|
1298
|
+
const decision = inferExit({
|
|
1299
|
+
loop,
|
|
1300
|
+
lastResult: effectiveResult,
|
|
1301
|
+
lastFailure: failure,
|
|
1302
|
+
costState,
|
|
1303
|
+
canSwitchAdapter: failure?.recommendedIntervention === "switch_adapter" &&
|
|
1304
|
+
adapterChain[currentAdapterIndex] !== undefined &&
|
|
1305
|
+
currentAdapter.adapterId !== executingAdapter.adapterId
|
|
938
1306
|
});
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1307
|
+
// Advance phase based on result
|
|
1308
|
+
currentPhase = nextPolicyPhase(currentPhase, effectiveResult, costState, phaseRetryCount);
|
|
1309
|
+
if (failure)
|
|
1310
|
+
phaseRetryCount++;
|
|
1311
|
+
else
|
|
1312
|
+
phaseRetryCount = 0;
|
|
1313
|
+
// Post-attempt: persisted verification and exit decisions must use the same
|
|
1314
|
+
// authoritative outcome. effectiveResult reflects any grounding/leash override,
|
|
1315
|
+
// so verification.completed is written here — never from the raw adapter result.
|
|
1316
|
+
const effectiveVerification = normalizeVerificationOutcome(effectiveResult, expectedVerifierBinding);
|
|
942
1317
|
loop = appendLoopEvent(loop, {
|
|
943
|
-
type: "
|
|
944
|
-
lifecycleState: "
|
|
1318
|
+
type: "verification.completed",
|
|
1319
|
+
lifecycleState: effectiveVerification.passed ? "completed" : "verifying",
|
|
945
1320
|
payload: {
|
|
946
1321
|
attemptId,
|
|
947
|
-
|
|
948
|
-
|
|
1322
|
+
attemptIndex: currentAttemptIndex,
|
|
1323
|
+
passed: effectiveVerification.passed,
|
|
1324
|
+
summary: effectiveVerification.summary,
|
|
1325
|
+
...(effectiveVerification.steps?.length ? { steps: effectiveVerification.steps } : {}),
|
|
1326
|
+
...(effectiveVerification.warnings?.length ? { warnings: effectiveVerification.warnings } : {}),
|
|
1327
|
+
...(effectiveVerification.binding ? { binding: effectiveVerification.binding } : {}),
|
|
1328
|
+
changedFiles,
|
|
949
1329
|
}
|
|
950
1330
|
}, { now: attemptCompletedAt, idFactory });
|
|
951
|
-
loop = appendLoopEvent(loop, {
|
|
952
|
-
type: "intervention.selected",
|
|
953
|
-
lifecycleState: "running",
|
|
954
|
-
payload: { attemptId, intervention: failure.recommendedIntervention }
|
|
955
|
-
}, { now: attemptCompletedAt, idFactory });
|
|
956
|
-
}
|
|
957
|
-
if (patchDecision?.decision === "ESCALATE" || patchDecision?.decision === "HANDOFF") {
|
|
958
|
-
const patchExitDecision = {
|
|
959
|
-
shouldExit: true,
|
|
960
|
-
lifecycleState: "human_escalation",
|
|
961
|
-
status: "exited",
|
|
962
|
-
reason: patchDecision.summary
|
|
963
|
-
};
|
|
964
1331
|
if (input.store) {
|
|
965
1332
|
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
966
|
-
kind: "
|
|
1333
|
+
kind: "verification.completed",
|
|
967
1334
|
runId: loop.loopId,
|
|
968
|
-
|
|
1335
|
+
attemptIndex: currentAttemptIndex,
|
|
1336
|
+
payload: {
|
|
1337
|
+
attemptId,
|
|
1338
|
+
passed: effectiveVerification.passed,
|
|
1339
|
+
summary: effectiveVerification.summary,
|
|
1340
|
+
...(effectiveVerification.steps?.length ? { steps: effectiveVerification.steps } : {}),
|
|
1341
|
+
...(effectiveVerification.warnings?.length ? { warnings: effectiveVerification.warnings } : {}),
|
|
1342
|
+
...(effectiveVerification.binding ? { binding: effectiveVerification.binding } : {}),
|
|
1343
|
+
changedFiles,
|
|
1344
|
+
}
|
|
969
1345
|
}));
|
|
970
1346
|
}
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
failure: {
|
|
989
|
-
message: patchDecision.summary,
|
|
990
|
-
...(failure?.failureClass ? { classHint: failure.failureClass } : {})
|
|
1347
|
+
const postAttemptTerminal = await evaluateCheckpoint("post_attempt", {
|
|
1348
|
+
status: effectiveResult.status === "completed" ? "completed" : "failed",
|
|
1349
|
+
verificationPassed: effectiveVerification.passed
|
|
1350
|
+
});
|
|
1351
|
+
if (postAttemptTerminal !== undefined)
|
|
1352
|
+
return await terminateRun(postAttemptTerminal);
|
|
1353
|
+
// Before-retry: check signals before scheduling the next attempt
|
|
1354
|
+
const preRetryTerminal = await evaluateCheckpoint("before_retry");
|
|
1355
|
+
if (preRetryTerminal !== undefined)
|
|
1356
|
+
return await terminateRun(preRetryTerminal);
|
|
1357
|
+
if (decision.shouldExit) {
|
|
1358
|
+
if (input.store) {
|
|
1359
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1360
|
+
kind: "run.exited",
|
|
1361
|
+
runId: loop.loopId,
|
|
1362
|
+
payload: createRunExitPayload(decision)
|
|
1363
|
+
}));
|
|
991
1364
|
}
|
|
1365
|
+
// Drain pending diagnostic ledger writes before receipt signing.
|
|
1366
|
+
if (pendingControlWrites.length > 0) {
|
|
1367
|
+
await Promise.all(pendingControlWrites.splice(0));
|
|
1368
|
+
}
|
|
1369
|
+
const routingInput = { runStartMs, firstDeltaDetected, firstDeltaTimestampMs, firstDeltaFilePath, firstDeltaChangeType, preDeltaCostUsd };
|
|
1370
|
+
const finalizedLoop = finalizeLoop(loop, decision, now(), idFactory, routingInput);
|
|
1371
|
+
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
1372
|
+
return {
|
|
1373
|
+
loop: finalizedLoop,
|
|
1374
|
+
decision
|
|
1375
|
+
};
|
|
992
1376
|
}
|
|
993
|
-
: result;
|
|
994
|
-
const decision = inferExit({
|
|
995
|
-
loop,
|
|
996
|
-
lastResult: effectiveResult,
|
|
997
|
-
lastFailure: failure,
|
|
998
|
-
costState,
|
|
999
|
-
canSwitchAdapter: failure?.recommendedIntervention === "switch_adapter" &&
|
|
1000
|
-
adapterChain[currentAdapterIndex] !== undefined &&
|
|
1001
|
-
currentAdapter.adapterId !== executingAdapter.adapterId
|
|
1002
|
-
});
|
|
1003
|
-
// Advance phase based on result
|
|
1004
|
-
currentPhase = nextPolicyPhase(currentPhase, effectiveResult, costState, phaseRetryCount);
|
|
1005
|
-
if (failure)
|
|
1006
|
-
phaseRetryCount++;
|
|
1007
|
-
else
|
|
1008
|
-
phaseRetryCount = 0;
|
|
1009
|
-
if (decision.shouldExit) {
|
|
1010
|
-
if (input.store) {
|
|
1011
|
-
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1012
|
-
kind: "run.exited",
|
|
1013
|
-
runId: loop.loopId,
|
|
1014
|
-
payload: createRunExitPayload(decision)
|
|
1015
|
-
}));
|
|
1016
|
-
}
|
|
1017
|
-
const routingInput = { runStartMs, firstDeltaDetected, firstDeltaTimestampMs, firstDeltaFilePath, firstDeltaChangeType, preDeltaCostUsd };
|
|
1018
|
-
const finalizedLoop = finalizeLoop(loop, decision, now(), idFactory, routingInput);
|
|
1019
|
-
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
1020
|
-
return {
|
|
1021
|
-
loop: finalizedLoop,
|
|
1022
|
-
decision
|
|
1023
|
-
};
|
|
1024
1377
|
}
|
|
1378
|
+
const decision = {
|
|
1379
|
+
shouldExit: true,
|
|
1380
|
+
lifecycleState: "budget_exit",
|
|
1381
|
+
status: "exited",
|
|
1382
|
+
reason: "Martin exited because the loop exhausted its configured iteration budget."
|
|
1383
|
+
};
|
|
1384
|
+
if (input.store) {
|
|
1385
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1386
|
+
kind: "run.exited",
|
|
1387
|
+
runId: loop.loopId,
|
|
1388
|
+
payload: createRunExitPayload(decision)
|
|
1389
|
+
}));
|
|
1390
|
+
}
|
|
1391
|
+
// Drain pending diagnostic ledger writes before receipt signing.
|
|
1392
|
+
if (pendingControlWrites.length > 0) {
|
|
1393
|
+
await Promise.all(pendingControlWrites.splice(0));
|
|
1394
|
+
}
|
|
1395
|
+
const routingInput = { runStartMs, firstDeltaDetected, firstDeltaTimestampMs, firstDeltaFilePath, firstDeltaChangeType, preDeltaCostUsd };
|
|
1396
|
+
const finalizedLoop = finalizeLoop(loop, decision, now(), idFactory, routingInput);
|
|
1397
|
+
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
1398
|
+
return {
|
|
1399
|
+
loop: finalizedLoop,
|
|
1400
|
+
decision
|
|
1401
|
+
};
|
|
1025
1402
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
await
|
|
1034
|
-
kind: "run.exited",
|
|
1035
|
-
runId: loop.loopId,
|
|
1036
|
-
payload: createRunExitPayload(decision)
|
|
1037
|
-
}));
|
|
1403
|
+
finally {
|
|
1404
|
+
if (wallTimer !== undefined)
|
|
1405
|
+
clearTimeout(wallTimer);
|
|
1406
|
+
disposeSignalMonitor();
|
|
1407
|
+
runController.abort();
|
|
1408
|
+
activeAttemptController?.abort();
|
|
1409
|
+
// Flush any diagnostic ledger writes queued from signal callbacks
|
|
1410
|
+
await Promise.all(pendingControlWrites);
|
|
1038
1411
|
}
|
|
1039
|
-
const routingInput = { runStartMs, firstDeltaDetected, firstDeltaTimestampMs, firstDeltaFilePath, firstDeltaChangeType, preDeltaCostUsd };
|
|
1040
|
-
const finalizedLoop = finalizeLoop(loop, decision, now(), idFactory, routingInput);
|
|
1041
|
-
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
1042
|
-
return {
|
|
1043
|
-
loop: finalizedLoop,
|
|
1044
|
-
decision
|
|
1045
|
-
};
|
|
1046
1412
|
}
|
|
1047
1413
|
function createRunExitPayload(decision) {
|
|
1048
1414
|
return {
|
|
@@ -1096,11 +1462,13 @@ function finalizeLoop(loop, decision, timestamp, idFactory, routingEconomicsInpu
|
|
|
1096
1462
|
const routingEconomics = routingEconomicsInput
|
|
1097
1463
|
? buildRoutingEconomics(finalized, routingEconomicsInput, decision)
|
|
1098
1464
|
: undefined;
|
|
1465
|
+
const avoidedUsd = calculateLoopAvoidedUsd({ loop: finalized, decision });
|
|
1099
1466
|
return {
|
|
1100
1467
|
...finalized,
|
|
1101
1468
|
status: decision.status,
|
|
1102
1469
|
lifecycleState: decision.lifecycleState,
|
|
1103
1470
|
updatedAt: timestamp,
|
|
1471
|
+
cost: { ...finalized.cost, avoidedUsd },
|
|
1104
1472
|
...(routingEconomics ? { routingEconomics } : {})
|
|
1105
1473
|
};
|
|
1106
1474
|
}
|
|
@@ -1195,6 +1563,9 @@ function mergeCostProvenance(previous, current) {
|
|
|
1195
1563
|
return COST_PROVENANCE_RANK[current] < COST_PROVENANCE_RANK[previous] ? current : previous;
|
|
1196
1564
|
}
|
|
1197
1565
|
function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
|
|
1566
|
+
if (repoRoot && rollbackBoundary?.headRef) {
|
|
1567
|
+
return listAttemptChangedFilesSinceBoundary({ repoRoot, boundary: rollbackBoundary });
|
|
1568
|
+
}
|
|
1198
1569
|
if (result.execution?.changedFiles !== undefined) {
|
|
1199
1570
|
return result.execution.changedFiles;
|
|
1200
1571
|
}
|
|
@@ -1215,6 +1586,41 @@ function buildPatchDiff(result, changedFiles) {
|
|
|
1215
1586
|
}
|
|
1216
1587
|
return undefined;
|
|
1217
1588
|
}
|
|
1589
|
+
function reconcileExistingGroundingFiles(scan, repoRoot, boundary) {
|
|
1590
|
+
const resolvedFiles = new Set(scan.resolvedFiles);
|
|
1591
|
+
const violations = scan.violations.filter((violation) => {
|
|
1592
|
+
if (violation.kind !== "file_not_found") {
|
|
1593
|
+
return true;
|
|
1594
|
+
}
|
|
1595
|
+
const absolutePath = resolve(repoRoot, violation.reference);
|
|
1596
|
+
const relativePath = relative(repoRoot, absolutePath);
|
|
1597
|
+
const isInsideRepo = relativePath.length > 0 && !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
1598
|
+
if (!isInsideRepo || !isTrackedRepoFileAtBoundary(repoRoot, violation.reference, boundary)) {
|
|
1599
|
+
return true;
|
|
1600
|
+
}
|
|
1601
|
+
resolvedFiles.add(violation.reference);
|
|
1602
|
+
return false;
|
|
1603
|
+
});
|
|
1604
|
+
return {
|
|
1605
|
+
...scan,
|
|
1606
|
+
violations,
|
|
1607
|
+
resolvedFiles: [...resolvedFiles]
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
function isTrackedRepoFileAtBoundary(repoRoot, filePath, boundary) {
|
|
1611
|
+
if (!boundary) {
|
|
1612
|
+
return false;
|
|
1613
|
+
}
|
|
1614
|
+
const normalizedPath = filePath.replace(/\\/gu, "/");
|
|
1615
|
+
if (boundary.snapshots.some((snapshot) => snapshot.existed && snapshot.path === normalizedPath)) {
|
|
1616
|
+
return true;
|
|
1617
|
+
}
|
|
1618
|
+
if (!boundary.headRef) {
|
|
1619
|
+
return false;
|
|
1620
|
+
}
|
|
1621
|
+
const result = spawnSync("git", ["-C", repoRoot, "cat-file", "-e", `${boundary.headRef}:${normalizedPath}`], { encoding: "utf8", stdio: "ignore", windowsHide: true });
|
|
1622
|
+
return result.status === 0;
|
|
1623
|
+
}
|
|
1218
1624
|
function createBudgetSettlement(input) {
|
|
1219
1625
|
const totalActualUsd = getUsageUsd(input.usage);
|
|
1220
1626
|
return {
|
|
@@ -1276,17 +1682,33 @@ function getLastVerifierScore(loop) {
|
|
|
1276
1682
|
function truncateVerificationDetail(text, maxLength) {
|
|
1277
1683
|
return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
|
|
1278
1684
|
}
|
|
1279
|
-
function
|
|
1685
|
+
function resolveExpectedVerifierBinding(request) {
|
|
1686
|
+
const commands = request.context.verificationStack?.length
|
|
1687
|
+
? request.context.verificationStack.map((step) => step.command)
|
|
1688
|
+
: request.context.verificationPlan;
|
|
1689
|
+
return {
|
|
1690
|
+
runId: request.loopId,
|
|
1691
|
+
workspaceId: request.workspaceId,
|
|
1692
|
+
cwd: request.context.repoRoot ?? process.cwd(),
|
|
1693
|
+
commands,
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
function normalizeVerificationOutcome(result, expectedBinding) {
|
|
1280
1697
|
const warnings = [...(result.verification.warnings ?? [])];
|
|
1281
1698
|
const contradiction = detectVerificationContradiction(result);
|
|
1282
1699
|
if (contradiction && !warnings.includes(contradiction)) {
|
|
1283
1700
|
warnings.push(contradiction);
|
|
1284
1701
|
}
|
|
1702
|
+
const executionPassed = verifierActuallyPassed(result.verification, expectedBinding);
|
|
1703
|
+
if (result.verification.passed && !executionPassed && !contradiction) {
|
|
1704
|
+
warnings.push("Verification evidence conflicts with the configured run, workspace, cwd, command, or subprocess result.");
|
|
1705
|
+
}
|
|
1285
1706
|
return {
|
|
1286
|
-
passed:
|
|
1707
|
+
passed: executionPassed && !contradiction,
|
|
1287
1708
|
summary: result.verification.summary,
|
|
1288
1709
|
...(result.verification.steps?.length ? { steps: result.verification.steps } : {}),
|
|
1289
|
-
...(warnings.length ? { warnings } : {})
|
|
1710
|
+
...(warnings.length ? { warnings } : {}),
|
|
1711
|
+
...(result.verification.binding ? { binding: result.verification.binding } : {})
|
|
1290
1712
|
};
|
|
1291
1713
|
}
|
|
1292
1714
|
function detectVerificationContradiction(result) {
|
|
@@ -1406,5 +1828,7 @@ function applyPatchFailureToLoop(loop, input) {
|
|
|
1406
1828
|
: attempt)
|
|
1407
1829
|
};
|
|
1408
1830
|
}
|
|
1831
|
+
export { createDefaultExitPolicy, evaluateExitPolicy, hashProgressState, toLegacyExitDecision, validateExitPolicy } from "./exits.js";
|
|
1832
|
+
export { SignalDiagnosticError, createFileExitSignalSource, exitSignalPath, readAllExitSignals, readExitSignal, startExitSignalMonitor, writeExitSignal } from "./exit-signal.js";
|
|
1409
1833
|
// ─── R4 Delivery ─────────────────────────────────────────────────────────────
|
|
1410
1834
|
export { cacheMessage, fetchSelectedMessage, getCliInstalledVersion, getMcpInstalledVersion, isCooldownExpired, isDismissed, isNewerVersion, loadDeliveryRecord, parseMessageSelectionResponse, recordDismissed, recordShown, resolveDefaultLedgerPath, saveDeliveryRecord, } from "./delivery/index.js";
|