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