@martinloop/mcp 0.3.7 → 0.3.9

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.
Files changed (88) hide show
  1. package/README.md +196 -137
  2. package/dist/resources.js +148 -148
  3. package/dist/server.js +0 -0
  4. package/dist/tools/tool-support.js +11 -1
  5. package/dist/vendor/adapters/claude-cli.d.ts +2 -2
  6. package/dist/vendor/adapters/claude-cli.js +127 -51
  7. package/dist/vendor/adapters/codex-launcher.js +10 -1
  8. package/dist/vendor/adapters/openai-compatible.js +6 -6
  9. package/package.json +86 -76
  10. package/server.json +21 -21
  11. package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +0 -146
  12. package/dist/vendor/adapters/adapters/src/claude-cli.js +0 -1100
  13. package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +0 -103
  14. package/dist/vendor/adapters/adapters/src/cli-bridge.js +0 -569
  15. package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +0 -78
  16. package/dist/vendor/adapters/adapters/src/codex-launcher.js +0 -663
  17. package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +0 -10
  18. package/dist/vendor/adapters/adapters/src/direct-provider.js +0 -40
  19. package/dist/vendor/adapters/adapters/src/index.d.ts +0 -8
  20. package/dist/vendor/adapters/adapters/src/index.js +0 -8
  21. package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +0 -62
  22. package/dist/vendor/adapters/adapters/src/openai-compatible.js +0 -306
  23. package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +0 -17
  24. package/dist/vendor/adapters/adapters/src/runtime-support.js +0 -59
  25. package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +0 -8
  26. package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +0 -40
  27. package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +0 -8
  28. package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +0 -12
  29. package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +0 -9
  30. package/dist/vendor/adapters/adapters/src/verifier-only.js +0 -63
  31. package/dist/vendor/adapters/contracts/src/context-graph.d.ts +0 -48
  32. package/dist/vendor/adapters/contracts/src/context-graph.js +0 -26
  33. package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +0 -78
  34. package/dist/vendor/adapters/contracts/src/execution-policy.js +0 -43
  35. package/dist/vendor/adapters/contracts/src/governance.d.ts +0 -24
  36. package/dist/vendor/adapters/contracts/src/governance.js +0 -11
  37. package/dist/vendor/adapters/contracts/src/identity.d.ts +0 -29
  38. package/dist/vendor/adapters/contracts/src/identity.js +0 -30
  39. package/dist/vendor/adapters/contracts/src/index.d.ts +0 -412
  40. package/dist/vendor/adapters/contracts/src/index.js +0 -225
  41. package/dist/vendor/adapters/contracts/src/operator.d.ts +0 -19
  42. package/dist/vendor/adapters/contracts/src/operator.js +0 -11
  43. package/dist/vendor/adapters/contracts/src/trajectory.d.ts +0 -26
  44. package/dist/vendor/adapters/contracts/src/trajectory.js +0 -27
  45. package/dist/vendor/adapters/core/src/compiler.d.ts +0 -52
  46. package/dist/vendor/adapters/core/src/compiler.js +0 -52
  47. package/dist/vendor/adapters/core/src/context-graph.d.ts +0 -3
  48. package/dist/vendor/adapters/core/src/context-graph.js +0 -371
  49. package/dist/vendor/adapters/core/src/context-integrity.d.ts +0 -26
  50. package/dist/vendor/adapters/core/src/context-integrity.js +0 -80
  51. package/dist/vendor/adapters/core/src/grounding.d.ts +0 -38
  52. package/dist/vendor/adapters/core/src/grounding.js +0 -273
  53. package/dist/vendor/adapters/core/src/identity.d.ts +0 -20
  54. package/dist/vendor/adapters/core/src/identity.js +0 -86
  55. package/dist/vendor/adapters/core/src/index.d.ts +0 -186
  56. package/dist/vendor/adapters/core/src/index.js +0 -1495
  57. package/dist/vendor/adapters/core/src/leash.d.ts +0 -48
  58. package/dist/vendor/adapters/core/src/leash.js +0 -497
  59. package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +0 -18
  60. package/dist/vendor/adapters/core/src/persistence/compiler.js +0 -34
  61. package/dist/vendor/adapters/core/src/persistence/index.d.ts +0 -10
  62. package/dist/vendor/adapters/core/src/persistence/index.js +0 -5
  63. package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +0 -39
  64. package/dist/vendor/adapters/core/src/persistence/integrity.js +0 -252
  65. package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +0 -23
  66. package/dist/vendor/adapters/core/src/persistence/ledger.js +0 -9
  67. package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +0 -63
  68. package/dist/vendor/adapters/core/src/persistence/memory-store.js +0 -115
  69. package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +0 -66
  70. package/dist/vendor/adapters/core/src/persistence/runs-reader.js +0 -140
  71. package/dist/vendor/adapters/core/src/persistence/store.d.ts +0 -89
  72. package/dist/vendor/adapters/core/src/persistence/store.js +0 -125
  73. package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +0 -85
  74. package/dist/vendor/adapters/core/src/persistence/trace-store.js +0 -125
  75. package/dist/vendor/adapters/core/src/policy-compiler.d.ts +0 -2
  76. package/dist/vendor/adapters/core/src/policy-compiler.js +0 -195
  77. package/dist/vendor/adapters/core/src/policy.d.ts +0 -156
  78. package/dist/vendor/adapters/core/src/policy.js +0 -704
  79. package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +0 -53
  80. package/dist/vendor/adapters/core/src/repo-analyzer.js +0 -263
  81. package/dist/vendor/adapters/core/src/rollback.d.ts +0 -15
  82. package/dist/vendor/adapters/core/src/rollback.js +0 -253
  83. package/dist/vendor/adapters/core/src/routing.d.ts +0 -82
  84. package/dist/vendor/adapters/core/src/routing.js +0 -336
  85. package/dist/vendor/adapters/core/src/test-integrity.d.ts +0 -64
  86. package/dist/vendor/adapters/core/src/test-integrity.js +0 -197
  87. package/dist/vendor/adapters/core/src/trajectory.d.ts +0 -18
  88. package/dist/vendor/adapters/core/src/trajectory.js +0 -295
@@ -1,1495 +0,0 @@
1
- import { appendLoopEvent, createLoopRecord } from "../../../contracts/index.js";
2
- import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe } from "./policy.js";
3
- import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
4
- import { buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations } from "./grounding.js";
5
- import { captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, restoreRollbackBoundary } from "./rollback.js";
6
- import { compilePromptPacket } from "./compiler.js";
7
- import { makeLedgerEvent, resolveRunsRoot, runDir } from "./persistence/index.js";
8
- import { runContextIntegrityPrecheck } from "./context-integrity.js";
9
- import { decideCircuitBreak } from "./trajectory.js";
10
- 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
- export { buildContextGraphSnapshot, queryContextGraph } from "./context-graph.js";
12
- export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, verifyIdentityToken } from "./identity.js";
13
- export { compileExecutionPolicy } from "./policy-compiler.js";
14
- export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
15
- // ─── Routing economics ─────────────────────────────────────────────────────
16
- export { classifyRoute, evaluatePreworkBurnPolicy, resolveModelForTier, selectBestEngine } from "./routing.js";
17
- // ─── Test Integrity Validation ──────────────────────────────────────────────
18
- export { validateTestIntegrity, snapshotTestFiles, identifyNewTests, findTestFiles, isTestFile, extractImports, hasTrivialAssertionsOnly, importsPreExistingCode } from "./test-integrity.js";
19
- // ─── Repo Style Analyzer ────────────────────────────────────────────────────
20
- export { analyzeRepoStyle, buildPromptDirectives } from "./repo-analyzer.js";
21
- // ─── Context Integrity Pre-gate ──────────────────────────────────────────────
22
- export { runContextIntegrityPrecheck } from "./context-integrity.js";
23
- // ─── Prompt packet compiler ──────────────────────────────────────────────────
24
- export { compilePromptPacket } from "./compiler.js";
25
- // ─── Persistence (RunStore, LedgerEvent, FileRunStore) ──────────────────────
26
- export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
27
- export { compileAndPersistContext } from "./persistence/index.js";
28
- // ─── MartinLoop Memory Store ────────────────────────────────────────────────
29
- export { appendMemory, readMemoryEntries, getPreference, buildMemorySummary, recordPreference, recordConsent } from "./persistence/memory-store.js";
30
- // ─── Trace Intelligence Store ───────────────────────────────────────────────
31
- export { appendTraceEntry, readTraceEntries, aggregateTraces, getHistoricalDirectSuccessRate } from "./persistence/trace-store.js";
32
- /**
33
- * Admission gate — must pass before any attempt is executed.
34
- * Evaluates budget headroom, oscillation, and repetitive failure patterns.
35
- * PolicyPhase remains explicit: attempts are only admitted in ADMIT phase.
36
- */
37
- export function evaluateAttemptPolicy(input) {
38
- const { request, projectedUsd } = input;
39
- const trajectoryAttempts = request.previousAttempts.map((attempt) => ({
40
- index: attempt.index,
41
- summary: attempt.summary,
42
- failureClass: attempt.failureClass
43
- }));
44
- const circuitBreakDecision = decideCircuitBreak({
45
- objective: request.context.objective,
46
- verificationPlan: request.context.verificationPlan,
47
- attempts: trajectoryAttempts,
48
- remainingIterations: request.context.remainingIterations
49
- });
50
- const trajectoryAssessment = circuitBreakDecision.assessment;
51
- if (circuitBreakDecision.shouldStop) {
52
- return {
53
- allowed: false,
54
- reason: circuitBreakDecision.reason,
55
- recommendedIntervention: circuitBreakDecision.recommendedIntervention,
56
- trajectoryAssessment,
57
- circuitBreakDecision
58
- };
59
- }
60
- // Budget gate: reject if projected cost exceeds remaining
61
- if (projectedUsd > request.context.remainingBudgetUsd) {
62
- return {
63
- allowed: false,
64
- reason: `Projected cost $${projectedUsd} exceeds remaining budget $${request.context.remainingBudgetUsd}.`,
65
- recommendedIntervention: "stop_loop",
66
- trajectoryAssessment,
67
- circuitBreakDecision
68
- };
69
- }
70
- // Iteration gate
71
- if (request.context.remainingIterations <= 0) {
72
- return {
73
- allowed: false,
74
- reason: "No remaining iterations in the budget.",
75
- recommendedIntervention: "stop_loop",
76
- trajectoryAssessment,
77
- circuitBreakDecision
78
- };
79
- }
80
- // Oscillation detection: A/B/A pattern in failure classes
81
- const failures = request.previousAttempts
82
- .map((a) => a.failureClass)
83
- .filter((fc) => Boolean(fc));
84
- if (failures.length >= 3) {
85
- const last3 = failures.slice(-3);
86
- const isOscillating = last3[0] !== last3[1] && last3[0] === last3[2];
87
- if (isOscillating) {
88
- return {
89
- allowed: false,
90
- reason: "Oscillating failure pattern detected. Escalating to human.",
91
- recommendedIntervention: "escalate_human",
92
- trajectoryAssessment,
93
- circuitBreakDecision
94
- };
95
- }
96
- }
97
- // Materially repetitive detection: same summary content pattern 3x
98
- if (request.previousAttempts.length >= 3) {
99
- const lastThree = request.previousAttempts.slice(-3);
100
- const summaries = lastThree
101
- .map((a) => a.summary?.toLowerCase() ?? "")
102
- .filter((s) => s.length > 10);
103
- if (summaries.length === 3) {
104
- // Compute rough similarity: shared significant tokens
105
- const tokenize = (s) => new Set(s.match(/[a-z]{4,}/g) ?? []);
106
- const tokens0 = tokenize(summaries[0] ?? "");
107
- const tokens2 = tokenize(summaries[2] ?? "");
108
- const shared = [...tokens0].filter((t) => tokens2.has(t));
109
- const similarity = shared.length / Math.max(tokens0.size, 1);
110
- if (similarity > 0.5) {
111
- return {
112
- allowed: false,
113
- reason: "Materially repetitive attempts detected. Escalating to human.",
114
- recommendedIntervention: "escalate_human",
115
- trajectoryAssessment,
116
- circuitBreakDecision
117
- };
118
- }
119
- }
120
- }
121
- return {
122
- allowed: true,
123
- reason: "Attempt admitted.",
124
- trajectoryAssessment,
125
- circuitBreakDecision
126
- };
127
- }
128
- export function distillContext(loop, options = {}) {
129
- const maxRecentAttempts = options.maxRecentAttempts ?? 3;
130
- const recentAttempts = loop.attempts.slice(-maxRecentAttempts);
131
- return {
132
- focus: `${loop.task.objective} Follow the verification plan and stay inside the configured budget.`,
133
- recentAttempts,
134
- constraints: {
135
- remainingBudgetUsd: roundUsd(loop.budget.maxUsd - loop.cost.actualUsd),
136
- remainingIterations: Math.max(loop.budget.maxIterations - loop.attempts.length, 0),
137
- remainingTokens: Math.max(loop.budget.maxTokens - loop.cost.tokensIn - loop.cost.tokensOut, 0)
138
- }
139
- };
140
- }
141
- export async function runMartin(input) {
142
- const now = input.now ?? (() => new Date().toISOString());
143
- const idFactory = input.idFactory;
144
- let loop = createLoopRecord({
145
- workspaceId: input.workspaceId,
146
- projectId: input.projectId,
147
- task: input.task,
148
- budget: input.budget,
149
- ...(input.receiptScope ? { receiptScope: input.receiptScope } : {}),
150
- ...(input.teamId ? { teamId: input.teamId } : {}),
151
- ...(input.metadata ? { metadata: input.metadata } : {})
152
- }, { now: now(), idFactory });
153
- loop = appendLoopEvent(loop, {
154
- type: "run.started",
155
- lifecycleState: "running",
156
- payload: {
157
- adapterId: input.adapter.adapterId,
158
- providerId: input.adapter.metadata.providerId,
159
- model: input.adapter.metadata.model,
160
- transport: getAdapterTransport(input.adapter)
161
- }
162
- }, { now: now(), idFactory });
163
- if (input.store) {
164
- await input.store.initRun({
165
- runId: loop.loopId,
166
- workspaceId: input.workspaceId,
167
- projectId: input.projectId,
168
- task: input.task,
169
- budget: input.budget,
170
- createdAt: loop.createdAt,
171
- ...(input.metadata ? { metadata: input.metadata } : {})
172
- });
173
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
174
- kind: "contract.created",
175
- runId: loop.loopId,
176
- payload: { workspaceId: input.workspaceId, projectId: input.projectId }
177
- }));
178
- await persistLoopRecordIfSupported(input.store, loop);
179
- }
180
- const DEFAULT_FALLBACK_MODELS = [
181
- "claude-haiku-4-5",
182
- "claude-sonnet-4-6",
183
- "claude-opus-4-6"
184
- ];
185
- const adapterChain = [input.adapter, ...(input.fallbackAdapters ?? [])];
186
- let currentAdapterIndex = 0;
187
- let currentAdapter = adapterChain[currentAdapterIndex] ?? input.adapter;
188
- let useCompressedContext = false;
189
- const isVerifyOnly = input.task.mutationMode === "verify_only";
190
- const executionProfile = resolveExecutionProfile({
191
- executionProfile: input.task.executionProfile,
192
- allowedNetworkDomains: input.task.allowedNetworkDomains
193
- });
194
- // Safety leash: block destructive verifier commands before any attempt
195
- const leashDecision = evaluateVerificationLeash({
196
- verificationPlan: input.task.verificationPlan,
197
- verificationStack: input.task.verificationStack,
198
- executionProfile: input.task.executionProfile,
199
- allowedNetworkDomains: input.task.allowedNetworkDomains
200
- });
201
- if (!leashDecision.allowed) {
202
- const reason = `${leashDecision.reason ?? "Safety leash blocked verifier commands."} Blocked: ${leashDecision.blockedCommands.join(", ")}`;
203
- const leashExitDecision = {
204
- shouldExit: true,
205
- lifecycleState: "human_escalation",
206
- status: "exited",
207
- reason,
208
- ...classifySafetyLeashExit(leashDecision, "verifier")
209
- };
210
- if (input.store) {
211
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
212
- kind: "safety.violations_found",
213
- runId: loop.loopId,
214
- payload: {
215
- surface: leashDecision.surface,
216
- blocked: true,
217
- profile: leashDecision.profile ?? executionProfile.name,
218
- violations: serializeSafetyViolations(leashDecision)
219
- }
220
- }));
221
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
222
- kind: "run.exited",
223
- runId: loop.loopId,
224
- payload: createRunExitPayload(leashExitDecision)
225
- }));
226
- }
227
- const finalizedLoop = finalizeLoop(loop, leashExitDecision, now(), idFactory);
228
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
229
- return {
230
- loop: finalizedLoop,
231
- decision: leashExitDecision
232
- };
233
- }
234
- const secretDecision = evaluateSecretLeash({
235
- values: [
236
- input.task.title,
237
- input.task.objective,
238
- ...(input.task.acceptanceCriteria ?? [])
239
- ]
240
- });
241
- if (!secretDecision.allowed) {
242
- const secretExitDecision = {
243
- shouldExit: true,
244
- lifecycleState: "human_escalation",
245
- status: "exited",
246
- reason: secretDecision.reason ?? "Safety leash blocked secret-like values in the runtime context.",
247
- ...classifySafetyLeashExit(secretDecision, "secret")
248
- };
249
- if (input.store) {
250
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
251
- kind: "safety.violations_found",
252
- runId: loop.loopId,
253
- payload: {
254
- surface: "secret",
255
- blocked: true,
256
- violations: secretDecision.violations.map((violation) => violation.match ?? violation.message)
257
- }
258
- }));
259
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
260
- kind: "run.exited",
261
- runId: loop.loopId,
262
- payload: createRunExitPayload(secretExitDecision)
263
- }));
264
- }
265
- const finalizedLoop = finalizeLoop(loop, secretExitDecision, now(), idFactory);
266
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
267
- return {
268
- loop: finalizedLoop,
269
- decision: secretExitDecision
270
- };
271
- }
272
- // Explicit PolicyPhase state machine — starts at GATHER, advances per attempt
273
- let currentPhase = "GATHER";
274
- let phaseRetryCount = 0;
275
- // Routing economics: track first meaningful workspace delta
276
- const runStartMs = Date.now();
277
- let firstDeltaDetected = false;
278
- let firstDeltaTimestampMs;
279
- let firstDeltaFilePath;
280
- let firstDeltaChangeType;
281
- let preDeltaCostUsd = 0;
282
- while (loop.attempts.length < loop.budget.maxIterations) {
283
- const distilled = distillContext(loop, {
284
- maxRecentAttempts: useCompressedContext ? 1 : (input.maxRecentAttempts ?? 3)
285
- });
286
- useCompressedContext = false;
287
- const attemptStartedAt = now();
288
- const attemptId = makeId("att", idFactory);
289
- const executingAdapter = currentAdapter;
290
- const budgetPreflight = evaluateBudgetPreflight({
291
- promptCharCount: distilled.focus.length + loop.task.objective.length * 3,
292
- attemptCount: loop.attempts.length,
293
- remainingBudgetUsd: distilled.constraints.remainingBudgetUsd,
294
- perAttemptCapUsd: loop.budget.maxUsd * 0.25
295
- });
296
- if (!budgetPreflight.allowed) {
297
- const preflightExitDecision = {
298
- shouldExit: true,
299
- lifecycleState: "budget_exit",
300
- status: "exited",
301
- reason: budgetPreflight.reason
302
- };
303
- if (input.store) {
304
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
305
- kind: "attempt.rejected",
306
- runId: loop.loopId,
307
- attemptIndex: loop.attempts.length + 1,
308
- payload: { reason: budgetPreflight.reason, source: "budget_preflight" }
309
- }));
310
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
311
- kind: "run.exited",
312
- runId: loop.loopId,
313
- payload: createRunExitPayload(preflightExitDecision)
314
- }));
315
- }
316
- const finalizedLoop = finalizeLoop(loop, preflightExitDecision, now(), idFactory);
317
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
318
- return {
319
- loop: finalizedLoop,
320
- decision: preflightExitDecision
321
- };
322
- }
323
- // GATHER → ADMIT: run admission control before executing
324
- currentPhase = "ADMIT";
325
- // T05: Context Integrity Pre-gate — blocks authority inversion / injection before reasoning
326
- //
327
- // Untrusted "tool output" re-entering the loop is verifier command output from prior
328
- // attempts (e.g. test runners echoing attacker-controlled strings). Pull that text from
329
- // already-persisted verification.completed events so the gate scans what actually
330
- // re-enters subsequent prompts, matching the documented "tool output / test output" scope.
331
- const priorVerifierOutput = loop.events
332
- .filter((event) => event.type === "verification.completed")
333
- .flatMap((event) => event.payload?.steps ?? [])
334
- .map((step) => step.detail)
335
- .filter((detail) => Boolean(detail))
336
- .join("\n---\n");
337
- const contextPrecheck = await runContextIntegrityPrecheck(loop.loopId, loop.attempts.length + 1, runDir(resolveActiveRunsRoot(input.store), loop.loopId), {
338
- userPrompt: distilled.focus,
339
- toolOutput: priorVerifierOutput || undefined,
340
- history: loop.attempts.map(a => a.summary).join("\n")
341
- });
342
- if (contextPrecheck.verdict === "context_poisoning_block") {
343
- currentPhase = "ABORT";
344
- const poisoningExitDecision = {
345
- shouldExit: true,
346
- lifecycleState: "human_escalation",
347
- status: "exited",
348
- reason: "Context Integrity Pre-gate: context poisoning attempt detected.",
349
- failureClass: "safety_leash_blocked",
350
- safetySurface: "context_integrity",
351
- reasonCode: "context_poisoning_blocked"
352
- };
353
- if (input.store) {
354
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
355
- kind: "safety.violations_found",
356
- runId: loop.loopId,
357
- payload: {
358
- verdict: contextPrecheck.verdict,
359
- signals: contextPrecheck.detectedSignals,
360
- source: "context_integrity_pregate"
361
- }
362
- }));
363
- }
364
- const finalizedLoop = finalizeLoop(loop, poisoningExitDecision, now(), idFactory);
365
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
366
- return {
367
- loop: finalizedLoop,
368
- decision: poisoningExitDecision
369
- };
370
- }
371
- const admissionDecision = evaluateAttemptPolicy({
372
- request: {
373
- loopId: loop.loopId,
374
- attemptId,
375
- context: {
376
- taskTitle: loop.task.title,
377
- objective: loop.task.objective,
378
- verificationPlan: loop.task.verificationPlan,
379
- ...(loop.task.verificationStack ? { verificationStack: loop.task.verificationStack } : {}),
380
- ...(loop.task.repoRoot ? { repoRoot: loop.task.repoRoot } : {}),
381
- ...(loop.task.allowedPaths ? { allowedPaths: loop.task.allowedPaths } : {}),
382
- ...(loop.task.deniedPaths ? { deniedPaths: loop.task.deniedPaths } : {}),
383
- ...(loop.task.acceptanceCriteria ? { acceptanceCriteria: loop.task.acceptanceCriteria } : {}),
384
- ...(loop.task.executionProfile ? { executionProfile: loop.task.executionProfile } : {}),
385
- ...(loop.task.allowedNetworkDomains
386
- ? { allowedNetworkDomains: loop.task.allowedNetworkDomains }
387
- : {}),
388
- ...(loop.task.approvalPolicy ? { approvalPolicy: loop.task.approvalPolicy } : {}),
389
- focus: distilled.focus,
390
- remainingBudgetUsd: distilled.constraints.remainingBudgetUsd,
391
- remainingIterations: distilled.constraints.remainingIterations,
392
- remainingTokens: distilled.constraints.remainingTokens
393
- },
394
- previousAttempts: loop.attempts
395
- },
396
- projectedUsd: budgetPreflight.estimate.estimatedAttemptCostUsd
397
- });
398
- if (!admissionDecision.allowed) {
399
- const exitReason = admissionDecision.reason;
400
- const exitDecision = {
401
- shouldExit: true,
402
- lifecycleState: admissionDecision.recommendedIntervention === "escalate_human"
403
- ? "human_escalation"
404
- : "budget_exit",
405
- status: "exited",
406
- reason: exitReason
407
- };
408
- if (input.store) {
409
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
410
- kind: "attempt.rejected",
411
- runId: loop.loopId,
412
- attemptIndex: loop.attempts.length + 1,
413
- payload: { reason: admissionDecision.reason }
414
- }));
415
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
416
- kind: "run.exited",
417
- runId: loop.loopId,
418
- payload: createRunExitPayload(exitDecision)
419
- }));
420
- }
421
- const finalizedLoop = finalizeLoop(loop, exitDecision, now(), idFactory);
422
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
423
- return {
424
- loop: finalizedLoop,
425
- decision: exitDecision
426
- };
427
- }
428
- if (input.store) {
429
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
430
- kind: "attempt.admitted",
431
- runId: loop.loopId,
432
- attemptIndex: loop.attempts.length + 1,
433
- payload: {
434
- attemptId,
435
- adapterId: executingAdapter.adapterId,
436
- providerId: executingAdapter.metadata.providerId,
437
- model: executingAdapter.metadata.model,
438
- transport: getAdapterTransport(executingAdapter)
439
- }
440
- }));
441
- }
442
- // ADMIT → PATCH
443
- currentPhase = "PATCH";
444
- loop = appendLoopEvent(loop, {
445
- type: "attempt.started",
446
- lifecycleState: "running",
447
- payload: {
448
- attemptId,
449
- adapterId: executingAdapter.adapterId,
450
- model: executingAdapter.metadata.model,
451
- policyPhase: currentPhase
452
- }
453
- }, { now: attemptStartedAt, idFactory });
454
- const request = {
455
- loopId: loop.loopId,
456
- attemptId,
457
- context: {
458
- taskTitle: loop.task.title,
459
- objective: loop.task.objective,
460
- verificationPlan: loop.task.verificationPlan,
461
- ...(loop.task.verificationStack ? { verificationStack: loop.task.verificationStack } : {}),
462
- ...(loop.task.mutationMode ? { mutationMode: loop.task.mutationMode } : {}),
463
- ...(loop.task.repoRoot ? { repoRoot: loop.task.repoRoot } : {}),
464
- ...(loop.task.allowedPaths ? { allowedPaths: loop.task.allowedPaths } : {}),
465
- ...(loop.task.deniedPaths ? { deniedPaths: loop.task.deniedPaths } : {}),
466
- ...(loop.task.acceptanceCriteria ? { acceptanceCriteria: loop.task.acceptanceCriteria } : {}),
467
- ...(loop.task.executionProfile ? { executionProfile: loop.task.executionProfile } : {}),
468
- ...(loop.task.allowedNetworkDomains
469
- ? { allowedNetworkDomains: loop.task.allowedNetworkDomains }
470
- : {}),
471
- ...(loop.task.approvalPolicy ? { approvalPolicy: loop.task.approvalPolicy } : {}),
472
- focus: distilled.focus,
473
- remainingBudgetUsd: distilled.constraints.remainingBudgetUsd,
474
- remainingIterations: distilled.constraints.remainingIterations,
475
- remainingTokens: distilled.constraints.remainingTokens
476
- },
477
- previousAttempts: loop.attempts
478
- };
479
- const tracksWorkspaceMutations = request.context.repoRoot !== undefined &&
480
- executingAdapter.metadata.capabilities?.workspaceMutations !== false;
481
- const rollbackBoundary = tracksWorkspaceMutations
482
- ? await captureRollbackBoundary({
483
- repoRoot: request.context.repoRoot,
484
- capturedAt: attemptStartedAt
485
- })
486
- : undefined;
487
- const result = await executingAdapter.execute(request);
488
- const attemptCompletedAt = now();
489
- const compiledContext = compilePromptPacket(request);
490
- const verification = normalizeVerificationOutcome(result);
491
- // PATCH → VERIFY
492
- currentPhase = "VERIFY";
493
- let failure = result.status === "failed"
494
- ? classifyFailure({ attempts: loop.attempts, result })
495
- : undefined;
496
- const currentAttemptIndex = loop.attempts.length + 1;
497
- const attempt = {
498
- attemptId,
499
- index: currentAttemptIndex,
500
- adapterId: executingAdapter.adapterId,
501
- model: executingAdapter.metadata.model,
502
- startedAt: attemptStartedAt,
503
- completedAt: attemptCompletedAt,
504
- summary: result.summary,
505
- ...(failure?.failureClass ? { failureClass: failure.failureClass } : {}),
506
- ...(failure?.recommendedIntervention
507
- ? { intervention: failure.recommendedIntervention }
508
- : {}),
509
- ...(failure?.diagnosticHint ? { diagnosticHint: failure.diagnosticHint } : {})
510
- };
511
- loop = {
512
- ...loop,
513
- attempts: [...loop.attempts, attempt],
514
- cost: {
515
- actualUsd: roundUsd(loop.cost.actualUsd + getUsageUsd(result.usage)),
516
- avoidedUsd: loop.cost.avoidedUsd,
517
- tokensIn: loop.cost.tokensIn + result.usage.tokensIn,
518
- tokensOut: loop.cost.tokensOut + result.usage.tokensOut,
519
- ...(result.usage.estimatedUsd !== undefined
520
- ? {
521
- estimatedUsd: roundUsd((loop.cost.estimatedUsd ?? loop.cost.actualUsd) + result.usage.estimatedUsd)
522
- }
523
- : {}),
524
- provenance: mergeCostProvenance(loop.cost.provenance, getUsageProvenance(result.usage)),
525
- ...(result.usage.providerSettlement
526
- ? { providerSettlement: result.usage.providerSettlement }
527
- : {})
528
- },
529
- updatedAt: attemptCompletedAt
530
- };
531
- loop = appendLoopEvent(loop, {
532
- type: "attempt.completed",
533
- lifecycleState: "running",
534
- payload: { attemptId, status: result.status, summary: result.summary }
535
- }, { now: attemptCompletedAt, idFactory });
536
- const previousVerifierScore = getLastVerifierScore(loop);
537
- if (failure) {
538
- if (failure.recommendedIntervention === "compress_context") {
539
- useCompressedContext = true;
540
- }
541
- let adapterSwitched = false;
542
- if (failure.recommendedIntervention === "switch_adapter") {
543
- const nextAdapter = adapterChain[currentAdapterIndex + 1];
544
- if (nextAdapter) {
545
- currentAdapterIndex += 1;
546
- currentAdapter = nextAdapter;
547
- adapterSwitched = true;
548
- }
549
- }
550
- if (failure.recommendedIntervention === "change_model" && currentAdapter.withModel) {
551
- const fallbackModels = input.fallbackModels ?? DEFAULT_FALLBACK_MODELS;
552
- const nextModel = fallbackModels[loop.attempts.length % fallbackModels.length];
553
- if (nextModel) {
554
- currentAdapter = currentAdapter.withModel(nextModel);
555
- }
556
- }
557
- loop = appendLoopEvent(loop, {
558
- type: "failure.classified",
559
- lifecycleState: "running",
560
- payload: {
561
- attemptId,
562
- failureClass: failure.failureClass,
563
- rationale: failure.rationale
564
- }
565
- }, { now: attemptCompletedAt, idFactory });
566
- loop = appendLoopEvent(loop, {
567
- type: "intervention.selected",
568
- lifecycleState: "running",
569
- payload: { attemptId, intervention: failure.recommendedIntervention }
570
- }, { now: attemptCompletedAt, idFactory });
571
- if (adapterSwitched) {
572
- loop = appendLoopEvent(loop, {
573
- type: "intervention.selected",
574
- lifecycleState: "running",
575
- payload: {
576
- attemptId,
577
- intervention: "switch_adapter",
578
- nextAdapterId: currentAdapter.adapterId,
579
- transport: getAdapterTransport(currentAdapter)
580
- }
581
- }, { now: attemptCompletedAt, idFactory });
582
- }
583
- }
584
- loop = appendLoopEvent(loop, {
585
- type: "verification.completed",
586
- lifecycleState: verification.passed ? "completed" : "verifying",
587
- payload: {
588
- attemptId,
589
- attemptIndex: currentAttemptIndex,
590
- passed: verification.passed,
591
- summary: verification.summary,
592
- ...(verification.steps?.length ? { steps: verification.steps } : {}),
593
- ...(verification.warnings?.length ? { warnings: verification.warnings } : {})
594
- }
595
- }, { now: attemptCompletedAt, idFactory });
596
- const costState = evaluateCostGovernor({
597
- budget: loop.budget,
598
- cost: loop.cost,
599
- attemptsUsed: loop.attempts.length
600
- });
601
- loop = appendLoopEvent(loop, {
602
- type: "budget.updated",
603
- lifecycleState: costState.shouldStop ? "budget_exit" : "running",
604
- payload: {
605
- actualUsd: loop.cost.actualUsd,
606
- remainingBudgetUsd: costState.remainingBudgetUsd,
607
- pressure: costState.pressure,
608
- provenance: loop.cost.provenance
609
- }
610
- }, { now: now(), idFactory });
611
- if (input.store) {
612
- const settlement = createBudgetSettlement({
613
- runId: loop.loopId,
614
- attemptIndex: currentAttemptIndex,
615
- usage: result.usage,
616
- estimate: budgetPreflight.estimate,
617
- settledAt: attemptCompletedAt
618
- });
619
- await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
620
- compiledContext,
621
- verification,
622
- ...(rollbackBoundary ? { rollbackBoundary } : {})
623
- });
624
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
625
- kind: "patch.generated",
626
- runId: loop.loopId,
627
- attemptIndex: currentAttemptIndex,
628
- payload: { status: result.status, summary: result.summary }
629
- }));
630
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
631
- kind: "verification.completed",
632
- runId: loop.loopId,
633
- attemptIndex: currentAttemptIndex,
634
- payload: {
635
- attemptId,
636
- passed: verification.passed,
637
- summary: verification.summary,
638
- ...(verification.steps?.length ? { steps: verification.steps } : {}),
639
- ...(verification.warnings?.length ? { warnings: verification.warnings } : {})
640
- }
641
- }));
642
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
643
- kind: "budget.settled",
644
- runId: loop.loopId,
645
- attemptIndex: currentAttemptIndex,
646
- payload: {
647
- actualUsd: settlement.totalActualUsd,
648
- estimatedUsd: result.usage.estimatedUsd,
649
- tokensIn: result.usage.tokensIn,
650
- tokensOut: result.usage.tokensOut,
651
- cachedInputTokens: result.usage.cachedInputTokens,
652
- reasoningTokensOut: result.usage.reasoningTokensOut,
653
- provenance: getUsageProvenance(result.usage),
654
- transport: getAdapterTransport(executingAdapter),
655
- providerId: executingAdapter.metadata.providerId,
656
- model: executingAdapter.metadata.model,
657
- providerSettlement: result.usage.providerSettlement,
658
- patchCost: settlement.patchCost,
659
- verificationCost: settlement.verificationCost,
660
- varianceUsd: settlement.varianceUsd,
661
- preflightEstimateUsd: settlement.preflightEstimateUsd
662
- }
663
- }));
664
- }
665
- const changedFiles = tracksWorkspaceMutations
666
- ? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary)
667
- : [];
668
- // Routing economics: detect first meaningful workspace delta
669
- if (!firstDeltaDetected && changedFiles.length > 0) {
670
- const meaningfulFile = changedFiles.find((f) => !isMartinLoopMetadata(f));
671
- if (meaningfulFile) {
672
- firstDeltaDetected = true;
673
- firstDeltaTimestampMs = Date.now();
674
- firstDeltaFilePath = meaningfulFile;
675
- firstDeltaChangeType = "modify";
676
- preDeltaCostUsd = roundUsd(loop.cost.actualUsd - getUsageUsd(result.usage));
677
- }
678
- }
679
- if (!firstDeltaDetected) {
680
- preDeltaCostUsd = loop.cost.actualUsd;
681
- }
682
- // Evidence is only reliable when the adapter explicitly reported files OR git actually
683
- // returned a non-empty list. A repoRoot alone is insufficient — git may fail (e.g. not
684
- // a git repo) and silently return [], which would falsely trigger no_code_change.
685
- const changedFileEvidenceAvailable = result.execution?.changedFiles !== undefined || changedFiles.length > 0;
686
- const isVerifierOnlyAdapter = executingAdapter.adapterId === "direct:verifier:verify-only";
687
- const patchTruthCountsEdits = !isVerifyOnly && !isVerifierOnlyAdapter && changedFileEvidenceAvailable;
688
- if (isVerifyOnly && changedFiles.length > 0) {
689
- const patchDecision = evaluatePatchDecision({
690
- verificationPassed: verification.passed,
691
- previousVerifierScore,
692
- verifierScore: verification.passed ? 1 : 0,
693
- scopeViolationCount: changedFiles.length,
694
- changedFileCount: changedFiles.length,
695
- diffNovelty: 1,
696
- diffStats: result.execution?.diffStats,
697
- costUsd: getUsageUsd(result.usage),
698
- summary: result.summary
699
- });
700
- const verifyOnlyExitDecision = {
701
- shouldExit: true,
702
- lifecycleState: "human_escalation",
703
- status: "exited",
704
- reason: "Verify-only mode forbids file changes.",
705
- failureClass: "safety_leash_blocked",
706
- safetySurface: "filesystem",
707
- reasonCode: "verify_only_write_attempt"
708
- };
709
- const rollbackOutcome = await restoreRollbackBoundary({
710
- repoRoot: request.context.repoRoot,
711
- boundary: rollbackBoundary,
712
- restoredAt: attemptCompletedAt,
713
- decision: patchDecision.decision
714
- });
715
- if (input.store) {
716
- const verifyOnlyViolation = {
717
- kind: "path_not_allowed",
718
- message: `Verify-only mode forbids changed files: ${changedFiles.join(", ")}`,
719
- file: changedFiles[0]
720
- };
721
- await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
722
- compiledContext,
723
- leash: createLeashArtifact({
724
- surface: "filesystem",
725
- reason: verifyOnlyExitDecision.reason,
726
- violations: [verifyOnlyViolation]
727
- }, currentAttemptIndex),
728
- patchScore: patchDecision.score,
729
- patchDecision: toPatchDecisionArtifact(patchDecision),
730
- ...(rollbackBoundary ? { rollbackBoundary } : {}),
731
- ...(rollbackOutcome ? { rollbackOutcome } : {})
732
- });
733
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
734
- kind: "safety.violations_found",
735
- runId: loop.loopId,
736
- attemptIndex: currentAttemptIndex,
737
- payload: {
738
- surface: "filesystem",
739
- blocked: true,
740
- attemptIndex: currentAttemptIndex,
741
- violations: [
742
- {
743
- kind: "path_not_allowed",
744
- message: verifyOnlyExitDecision.reason,
745
- files: changedFiles
746
- }
747
- ]
748
- }
749
- }));
750
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
751
- kind: "attempt.discarded",
752
- runId: loop.loopId,
753
- attemptIndex: currentAttemptIndex,
754
- payload: {
755
- decision: patchDecision.decision,
756
- reason: patchDecision.summary,
757
- reasonCodes: patchDecision.reasonCodes,
758
- score: patchDecision.score.score
759
- }
760
- }));
761
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
762
- kind: "run.exited",
763
- runId: loop.loopId,
764
- payload: createRunExitPayload(verifyOnlyExitDecision)
765
- }));
766
- }
767
- const finalizedLoop = finalizeLoop(loop, verifyOnlyExitDecision, now(), idFactory);
768
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
769
- return {
770
- loop: finalizedLoop,
771
- decision: verifyOnlyExitDecision
772
- };
773
- }
774
- const filesystemDecision = evaluateFilesystemLeash({
775
- repoRoot: request.context.repoRoot,
776
- changedFiles,
777
- allowedPaths: request.context.allowedPaths,
778
- deniedPaths: request.context.deniedPaths
779
- });
780
- if (!filesystemDecision.allowed) {
781
- const patchDecision = evaluatePatchDecision({
782
- verificationPassed: result.verification.passed,
783
- previousVerifierScore,
784
- verifierScore: result.verification.passed ? 1 : 0,
785
- scopeViolationCount: filesystemDecision.violations.length,
786
- changedFileCount: changedFiles.length,
787
- diffNovelty: changedFiles.length > 0 ? 1 : 0,
788
- diffStats: result.execution?.diffStats,
789
- costUsd: getUsageUsd(result.usage),
790
- summary: result.summary
791
- });
792
- const filesystemExitDecision = {
793
- shouldExit: true,
794
- lifecycleState: "human_escalation",
795
- status: "exited",
796
- reason: filesystemDecision.reason ?? "Safety leash blocked filesystem changes.",
797
- ...classifySafetyLeashExit(filesystemDecision, "filesystem")
798
- };
799
- const rollbackOutcome = await restoreRollbackBoundary({
800
- repoRoot: request.context.repoRoot,
801
- boundary: rollbackBoundary,
802
- restoredAt: attemptCompletedAt,
803
- decision: patchDecision.decision
804
- });
805
- if (input.store) {
806
- await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
807
- compiledContext,
808
- leash: createLeashArtifact(filesystemDecision, currentAttemptIndex),
809
- patchScore: patchDecision.score,
810
- patchDecision: toPatchDecisionArtifact(patchDecision),
811
- ...(rollbackBoundary ? { rollbackBoundary } : {}),
812
- ...(rollbackOutcome ? { rollbackOutcome } : {})
813
- });
814
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
815
- kind: "safety.violations_found",
816
- runId: loop.loopId,
817
- attemptIndex: currentAttemptIndex,
818
- payload: {
819
- surface: "filesystem",
820
- blocked: true,
821
- attemptIndex: currentAttemptIndex,
822
- violations: filesystemDecision.violations
823
- }
824
- }));
825
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
826
- kind: "attempt.discarded",
827
- runId: loop.loopId,
828
- attemptIndex: currentAttemptIndex,
829
- payload: {
830
- decision: patchDecision.decision,
831
- reason: patchDecision.summary,
832
- reasonCodes: patchDecision.reasonCodes,
833
- score: patchDecision.score.score
834
- }
835
- }));
836
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
837
- kind: "run.exited",
838
- runId: loop.loopId,
839
- payload: createRunExitPayload(filesystemExitDecision)
840
- }));
841
- }
842
- const finalizedLoop = finalizeLoop(loop, filesystemExitDecision, now(), idFactory);
843
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
844
- return {
845
- loop: finalizedLoop,
846
- decision: filesystemExitDecision
847
- };
848
- }
849
- const changeApprovalDecision = evaluateChangeApprovalLeash({
850
- changedFiles,
851
- executionProfile: request.context.executionProfile,
852
- approvalPolicy: request.context.approvalPolicy
853
- });
854
- if (!changeApprovalDecision.allowed) {
855
- const patchDecision = evaluatePatchDecision({
856
- verificationPassed: result.verification.passed,
857
- previousVerifierScore,
858
- verifierScore: result.verification.passed ? 1 : 0,
859
- safetyViolationCount: changeApprovalDecision.violations.length,
860
- changedFileCount: changedFiles.length,
861
- diffNovelty: changedFiles.length > 0 ? 1 : 0,
862
- diffStats: result.execution?.diffStats,
863
- costUsd: getUsageUsd(result.usage),
864
- humanApprovalRequired: true,
865
- summary: result.summary
866
- });
867
- const approvalExitDecision = {
868
- shouldExit: true,
869
- lifecycleState: "human_escalation",
870
- status: "exited",
871
- reason: changeApprovalDecision.reason ??
872
- "Safety leash blocked dependency or migration changes that require approval.",
873
- ...classifySafetyLeashExit(changeApprovalDecision, "dependency")
874
- };
875
- const rollbackOutcome = await restoreRollbackBoundary({
876
- repoRoot: request.context.repoRoot,
877
- boundary: rollbackBoundary,
878
- restoredAt: attemptCompletedAt,
879
- decision: patchDecision.decision
880
- });
881
- if (input.store) {
882
- await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
883
- compiledContext,
884
- leash: createLeashArtifact(changeApprovalDecision, currentAttemptIndex),
885
- patchScore: patchDecision.score,
886
- patchDecision: toPatchDecisionArtifact(patchDecision),
887
- ...(rollbackBoundary ? { rollbackBoundary } : {}),
888
- ...(rollbackOutcome ? { rollbackOutcome } : {})
889
- });
890
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
891
- kind: "safety.violations_found",
892
- runId: loop.loopId,
893
- attemptIndex: currentAttemptIndex,
894
- payload: {
895
- surface: "dependency",
896
- blocked: true,
897
- profile: changeApprovalDecision.profile ?? executionProfile.name,
898
- attemptIndex: currentAttemptIndex,
899
- violations: changeApprovalDecision.violations
900
- }
901
- }));
902
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
903
- kind: "attempt.discarded",
904
- runId: loop.loopId,
905
- attemptIndex: currentAttemptIndex,
906
- payload: {
907
- decision: patchDecision.decision,
908
- reason: patchDecision.summary,
909
- reasonCodes: patchDecision.reasonCodes,
910
- score: patchDecision.score.score
911
- }
912
- }));
913
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
914
- kind: "run.exited",
915
- runId: loop.loopId,
916
- payload: createRunExitPayload(approvalExitDecision)
917
- }));
918
- }
919
- const finalizedLoop = finalizeLoop(loop, approvalExitDecision, now(), idFactory);
920
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
921
- return {
922
- loop: finalizedLoop,
923
- decision: approvalExitDecision
924
- };
925
- }
926
- // VERIFY: Run grounding scan on patch diff if available
927
- // Uses the task's repoRoot to build/load the grounding index, then scans any diff
928
- let groundingScanResult;
929
- const patchDiff = buildPatchDiff(result, changedFiles);
930
- if (patchDiff && input.task.repoRoot) {
931
- try {
932
- const groundingIndex = await loadOrBuildRepoGroundingIndex(input.task.repoRoot);
933
- groundingScanResult = scanPatchForGroundingViolations(patchDiff, groundingIndex, {
934
- allowedPaths: input.task.allowedPaths
935
- });
936
- if (input.store && groundingScanResult.violations.length > 0) {
937
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
938
- kind: "grounding.violations_found",
939
- runId: loop.loopId,
940
- attemptIndex: currentAttemptIndex,
941
- payload: {
942
- violationCount: groundingScanResult.violations.length,
943
- resolvedFiles: groundingScanResult.resolvedFiles,
944
- contentOnly: groundingScanResult.contentOnly,
945
- violations: groundingScanResult.violations.slice(0, 10)
946
- }
947
- }));
948
- }
949
- }
950
- catch {
951
- // Grounding scan is best-effort — never fail the loop because of a scan error
952
- }
953
- }
954
- let patchDecision;
955
- if (result.status === "completed") {
956
- patchDecision = evaluatePatchDecision({
957
- verificationPassed: verification.passed,
958
- previousVerifierScore,
959
- verifierScore: verification.passed ? 1 : 0,
960
- groundingViolationCount: groundingScanResult?.violations.length ?? 0,
961
- changedFileCount: patchTruthCountsEdits ? changedFiles.length : undefined,
962
- diffNovelty: patchTruthCountsEdits ? (changedFiles.length > 0 ? 1 : 0) : undefined,
963
- diffStats: result.execution?.diffStats,
964
- costUsd: getUsageUsd(result.usage),
965
- summary: result.summary
966
- });
967
- }
968
- let rollbackOutcome;
969
- if (patchDecision && patchDecision.decision !== "KEEP") {
970
- rollbackOutcome = await restoreRollbackBoundary({
971
- repoRoot: request.context.repoRoot,
972
- boundary: rollbackBoundary,
973
- restoredAt: attemptCompletedAt,
974
- decision: patchDecision.decision
975
- });
976
- }
977
- else if (result.status === "failed") {
978
- rollbackOutcome = await restoreRollbackBoundary({
979
- repoRoot: request.context.repoRoot,
980
- boundary: rollbackBoundary,
981
- restoredAt: attemptCompletedAt,
982
- decision: "DISCARD"
983
- });
984
- }
985
- if (input.store) {
986
- await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
987
- compiledContext,
988
- ...(patchDiff ? { diff: patchDiff } : {}),
989
- ...(groundingScanResult ? { groundingScan: groundingScanResult } : {}),
990
- ...(patchDecision ? { patchScore: patchDecision.score } : {}),
991
- ...(patchDecision ? { patchDecision: toPatchDecisionArtifact(patchDecision) } : {}),
992
- ...(rollbackBoundary ? { rollbackBoundary } : {}),
993
- ...(rollbackOutcome ? { rollbackOutcome } : {})
994
- });
995
- }
996
- if (input.store) {
997
- if (patchDecision) {
998
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
999
- kind: patchDecision.decision === "KEEP" ? "attempt.kept" : "attempt.discarded",
1000
- runId: loop.loopId,
1001
- attemptIndex: currentAttemptIndex,
1002
- payload: {
1003
- decision: patchDecision.decision,
1004
- reason: patchDecision.summary,
1005
- reasonCodes: patchDecision.reasonCodes,
1006
- score: patchDecision.score.score
1007
- }
1008
- }));
1009
- }
1010
- else {
1011
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
1012
- kind: verification.passed ? "attempt.kept" : "attempt.discarded",
1013
- runId: loop.loopId,
1014
- attemptIndex: currentAttemptIndex,
1015
- payload: { reason: verification.summary }
1016
- }));
1017
- }
1018
- }
1019
- if (patchDecision && patchDecision.decision !== "KEEP" && !failure) {
1020
- failure = classifyPatchDecisionFailure(patchDecision, result);
1021
- loop = applyPatchFailureToLoop(loop, {
1022
- attemptId,
1023
- summary: patchDecision.summary,
1024
- failure
1025
- });
1026
- if (failure.recommendedIntervention === "compress_context") {
1027
- useCompressedContext = true;
1028
- }
1029
- loop = appendLoopEvent(loop, {
1030
- type: "failure.classified",
1031
- lifecycleState: "running",
1032
- payload: {
1033
- attemptId,
1034
- failureClass: failure.failureClass,
1035
- rationale: failure.rationale
1036
- }
1037
- }, { now: attemptCompletedAt, idFactory });
1038
- loop = appendLoopEvent(loop, {
1039
- type: "intervention.selected",
1040
- lifecycleState: "running",
1041
- payload: { attemptId, intervention: failure.recommendedIntervention }
1042
- }, { now: attemptCompletedAt, idFactory });
1043
- }
1044
- if (patchDecision?.decision === "ESCALATE" || patchDecision?.decision === "HANDOFF") {
1045
- const patchExitDecision = {
1046
- shouldExit: true,
1047
- lifecycleState: "human_escalation",
1048
- status: "exited",
1049
- reason: patchDecision.summary
1050
- };
1051
- if (input.store) {
1052
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
1053
- kind: "run.exited",
1054
- runId: loop.loopId,
1055
- payload: createRunExitPayload(patchExitDecision)
1056
- }));
1057
- }
1058
- const finalizedLoop = finalizeLoop(loop, patchExitDecision, now(), idFactory);
1059
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
1060
- return {
1061
- loop: finalizedLoop,
1062
- decision: patchExitDecision
1063
- };
1064
- }
1065
- const effectiveResult = patchDecision && patchDecision.decision !== "KEEP"
1066
- ? {
1067
- ...result,
1068
- status: "failed",
1069
- summary: patchDecision.summary,
1070
- verification: {
1071
- ...result.verification,
1072
- passed: false,
1073
- summary: patchDecision.summary
1074
- },
1075
- failure: {
1076
- message: patchDecision.summary,
1077
- ...(failure?.failureClass ? { classHint: failure.failureClass } : {})
1078
- }
1079
- }
1080
- : result;
1081
- const decision = inferExit({
1082
- loop,
1083
- lastResult: effectiveResult,
1084
- lastFailure: failure,
1085
- costState,
1086
- canSwitchAdapter: failure?.recommendedIntervention === "switch_adapter" &&
1087
- adapterChain[currentAdapterIndex] !== undefined &&
1088
- currentAdapter.adapterId !== executingAdapter.adapterId
1089
- });
1090
- // Advance phase based on result
1091
- currentPhase = nextPolicyPhase(currentPhase, effectiveResult, costState, phaseRetryCount);
1092
- if (failure)
1093
- phaseRetryCount++;
1094
- else
1095
- phaseRetryCount = 0;
1096
- if (decision.shouldExit) {
1097
- if (input.store) {
1098
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
1099
- kind: "run.exited",
1100
- runId: loop.loopId,
1101
- payload: createRunExitPayload(decision)
1102
- }));
1103
- }
1104
- const routingInput = { runStartMs, firstDeltaDetected, firstDeltaTimestampMs, firstDeltaFilePath, firstDeltaChangeType, preDeltaCostUsd };
1105
- const finalizedLoop = finalizeLoop(loop, decision, now(), idFactory, routingInput);
1106
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
1107
- return {
1108
- loop: finalizedLoop,
1109
- decision
1110
- };
1111
- }
1112
- }
1113
- const decision = {
1114
- shouldExit: true,
1115
- lifecycleState: "budget_exit",
1116
- status: "exited",
1117
- reason: "Martin exited because the loop exhausted its configured iteration budget."
1118
- };
1119
- if (input.store) {
1120
- await input.store.appendLedger(loop.loopId, makeLedgerEvent({
1121
- kind: "run.exited",
1122
- runId: loop.loopId,
1123
- payload: createRunExitPayload(decision)
1124
- }));
1125
- }
1126
- const routingInput = { runStartMs, firstDeltaDetected, firstDeltaTimestampMs, firstDeltaFilePath, firstDeltaChangeType, preDeltaCostUsd };
1127
- const finalizedLoop = finalizeLoop(loop, decision, now(), idFactory, routingInput);
1128
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
1129
- return {
1130
- loop: finalizedLoop,
1131
- decision
1132
- };
1133
- }
1134
- function createRunExitPayload(decision) {
1135
- return {
1136
- lifecycleState: decision.lifecycleState,
1137
- status: decision.status,
1138
- reason: decision.reason,
1139
- ...(decision.failureClass ? { failureClass: decision.failureClass } : {}),
1140
- ...(decision.safetySurface ? { safetySurface: decision.safetySurface } : {}),
1141
- ...(decision.reasonCode ? { reasonCode: decision.reasonCode } : {})
1142
- };
1143
- }
1144
- function classifySafetyLeashExit(decision, safetySurface = decision.surface) {
1145
- return {
1146
- failureClass: "safety_leash_blocked",
1147
- safetySurface,
1148
- reasonCode: safetyLeashReasonCode(decision, safetySurface)
1149
- };
1150
- }
1151
- function safetyLeashReasonCode(decision, safetySurface) {
1152
- const kind = decision.violations[0]?.kind;
1153
- switch (kind) {
1154
- case "command_blocked":
1155
- return safetySurface === "verifier" ? "destructive_verifier_command" : "command_blocked";
1156
- case "network_blocked":
1157
- return safetySurface === "verifier" ? "verifier_network_blocked" : "network_access_blocked";
1158
- case "secret_value":
1159
- return "secret_context_value";
1160
- case "path_denied":
1161
- case "protected_path":
1162
- return "protected_surface_write";
1163
- case "path_not_allowed":
1164
- return "surface_write_not_allowed";
1165
- case "path_outside_repo":
1166
- return "outside_repo_write";
1167
- case "dependency_approval_required":
1168
- return "dependency_approval_required";
1169
- case "migration_approval_required":
1170
- return "migration_approval_required";
1171
- case "config_change_approval_required":
1172
- return "config_change_approval_required";
1173
- default:
1174
- return `${safetySurface}_safety_block`;
1175
- }
1176
- }
1177
- function finalizeLoop(loop, decision, timestamp, idFactory, routingEconomicsInput) {
1178
- const finalized = appendLoopEvent(loop, {
1179
- type: "run.completed",
1180
- lifecycleState: decision.lifecycleState,
1181
- payload: createRunExitPayload(decision)
1182
- }, { now: timestamp, idFactory });
1183
- const routingEconomics = routingEconomicsInput
1184
- ? buildRoutingEconomics(finalized, routingEconomicsInput, decision)
1185
- : undefined;
1186
- return {
1187
- ...finalized,
1188
- status: decision.status,
1189
- lifecycleState: decision.lifecycleState,
1190
- updatedAt: timestamp,
1191
- ...(routingEconomics ? { routingEconomics } : {})
1192
- };
1193
- }
1194
- function buildRoutingEconomics(loop, input, decision) {
1195
- const totalCost = loop.cost.actualUsd;
1196
- const preworkCost = roundUsd(Math.max(input.preDeltaCostUsd, 0));
1197
- const executionCost = roundUsd(Math.max(totalCost - preworkCost, 0));
1198
- const preworkBurnPct = totalCost > 0 ? Math.round((preworkCost / totalCost) * 100) : 0;
1199
- const timeToFirstDeltaMs = input.firstDeltaTimestampMs
1200
- ? input.firstDeltaTimestampMs - input.runStartMs
1201
- : undefined;
1202
- const accepted = decision.lifecycleState === "completed";
1203
- // Route recommendation based on prework burn
1204
- let routeRecommendation;
1205
- let routeRecommendationReason;
1206
- if (preworkBurnPct > 50) {
1207
- routeRecommendation = "direct_worker";
1208
- routeRecommendationReason = `${String(preworkBurnPct)}% of spend was pre-work coordination. Use direct execution for similar tasks.`;
1209
- }
1210
- else if (preworkBurnPct > 30) {
1211
- routeRecommendation = "direct_worker";
1212
- routeRecommendationReason = `${String(preworkBurnPct)}% pre-work burn is high. Consider direct execution.`;
1213
- }
1214
- else {
1215
- routeRecommendation = "same_route";
1216
- routeRecommendationReason = `Pre-work burn at ${String(preworkBurnPct)}% is within acceptable range.`;
1217
- }
1218
- return {
1219
- preworkCostUsd: preworkCost,
1220
- executionCostUsd: executionCost,
1221
- verificationCostUsd: 0,
1222
- retryCostUsd: 0,
1223
- totalCostUsd: totalCost,
1224
- preworkBurnPct,
1225
- timeToFirstDeltaMs,
1226
- firstDelta: {
1227
- detected: input.firstDeltaDetected,
1228
- ...(input.firstDeltaTimestampMs ? { timestampMs: input.firstDeltaTimestampMs } : {}),
1229
- ...(input.firstDeltaFilePath ? { filePath: input.firstDeltaFilePath } : {}),
1230
- ...(input.firstDeltaChangeType ? { changeType: input.firstDeltaChangeType } : {}),
1231
- ...(timeToFirstDeltaMs !== undefined ? { timeToFirstDeltaMs } : {})
1232
- },
1233
- ...(accepted ? { costPerAcceptedChange: totalCost } : {}),
1234
- routeRecommendation,
1235
- routeRecommendationReason
1236
- };
1237
- }
1238
- const MARTINLOOP_METADATA_PATTERNS = [
1239
- /^PROGRESS\.md$/u,
1240
- /^\.martin\//u,
1241
- /\.lock$/u,
1242
- /^node_modules\//u,
1243
- /^\.git\//u,
1244
- /^\.cache\//u,
1245
- ];
1246
- function isMartinLoopMetadata(filePath) {
1247
- return MARTINLOOP_METADATA_PATTERNS.some((p) => p.test(filePath));
1248
- }
1249
- async function persistLoopRecordIfSupported(store, loop) {
1250
- await store?.writeLoopRecord?.(loop.loopId, loop);
1251
- }
1252
- function getAdapterTransport(adapter) {
1253
- return adapter.metadata.transport ?? (adapter.kind === "agent-cli" ? "cli" : "http");
1254
- }
1255
- function getUsageUsd(usage) {
1256
- return roundUsd(usage.actualUsd);
1257
- }
1258
- function getUsageProvenance(usage) {
1259
- if (usage.provenance) {
1260
- return usage.provenance;
1261
- }
1262
- if (usage.estimatedUsd !== undefined) {
1263
- return "estimated";
1264
- }
1265
- return "actual";
1266
- }
1267
- const COST_PROVENANCE_RANK = {
1268
- unavailable: 0,
1269
- estimated: 1,
1270
- actual: 2
1271
- };
1272
- /**
1273
- * Aggregates cost provenance across attempts. The cumulative loop provenance
1274
- * can only be as trustworthy as its weakest attempt: if any attempt's cost was
1275
- * estimated or unavailable, the cumulative total must reflect that, even if a
1276
- * later attempt reports an authoritative actual cost.
1277
- */
1278
- function mergeCostProvenance(previous, current) {
1279
- if (previous === undefined) {
1280
- return current;
1281
- }
1282
- return COST_PROVENANCE_RANK[current] < COST_PROVENANCE_RANK[previous] ? current : previous;
1283
- }
1284
- function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
1285
- if (result.execution?.changedFiles !== undefined) {
1286
- return result.execution.changedFiles;
1287
- }
1288
- return listAttemptChangedFilesSinceBoundary({ repoRoot, boundary: rollbackBoundary });
1289
- }
1290
- function buildPatchDiff(result, changedFiles) {
1291
- // Use structured diff stats to build a minimal diff header if no raw diff is available
1292
- if (result.execution?.changedFiles?.length) {
1293
- // Build a synthetic diff header from changed file list
1294
- return result.execution.changedFiles
1295
- .map((file) => `--- a/${file}\n+++ b/${file}\n@@ -0,0 +1 @@\n+`)
1296
- .join("\n");
1297
- }
1298
- if (changedFiles.length > 0) {
1299
- return changedFiles
1300
- .map((file) => `--- a/${file}\n+++ b/${file}\n@@ -0,0 +1 @@\n+`)
1301
- .join("\n");
1302
- }
1303
- return undefined;
1304
- }
1305
- function createBudgetSettlement(input) {
1306
- const totalActualUsd = getUsageUsd(input.usage);
1307
- return {
1308
- runId: input.runId,
1309
- attemptIndex: input.attemptIndex,
1310
- patchCost: {
1311
- usd: totalActualUsd,
1312
- tokensIn: input.usage.tokensIn,
1313
- tokensOut: input.usage.tokensOut,
1314
- provenance: getUsageProvenance(input.usage)
1315
- },
1316
- verificationCost: {
1317
- usd: 0,
1318
- provenance: "unavailable"
1319
- },
1320
- ...(input.usage.providerSettlement
1321
- ? { providerSettlement: input.usage.providerSettlement }
1322
- : {}),
1323
- totalActualUsd,
1324
- preflightEstimateUsd: input.estimate.estimatedAttemptCostUsd,
1325
- varianceUsd: roundUsd(totalActualUsd - input.estimate.estimatedAttemptCostUsd),
1326
- settledAt: input.settledAt
1327
- };
1328
- }
1329
- function roundUsd(value) {
1330
- return Math.round(value * 100) / 100;
1331
- }
1332
- function makeId(prefix, idFactory) {
1333
- if (idFactory)
1334
- return idFactory(prefix);
1335
- return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
1336
- }
1337
- function serializeSafetyViolations(decision) {
1338
- if (decision.surface === "command") {
1339
- return decision.blockedCommands;
1340
- }
1341
- return decision.violations;
1342
- }
1343
- function createLeashArtifact(decision, attemptIndex) {
1344
- return {
1345
- attemptIndex,
1346
- surface: decision.surface,
1347
- blocked: true,
1348
- ...(decision.profile ? { profile: decision.profile } : {}),
1349
- ...(decision.reason ? { reason: decision.reason } : {}),
1350
- violations: decision.violations
1351
- };
1352
- }
1353
- function getLastVerifierScore(loop) {
1354
- for (let index = loop.events.length - 1; index >= 0; index -= 1) {
1355
- const event = loop.events[index];
1356
- if (event?.type !== "verification.completed") {
1357
- continue;
1358
- }
1359
- return event.payload["passed"] === true ? 1 : 0;
1360
- }
1361
- return 0;
1362
- }
1363
- function truncateVerificationDetail(text, maxLength) {
1364
- return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
1365
- }
1366
- function normalizeVerificationOutcome(result) {
1367
- const warnings = [...(result.verification.warnings ?? [])];
1368
- const contradiction = detectVerificationContradiction(result);
1369
- if (contradiction && !warnings.includes(contradiction)) {
1370
- warnings.push(contradiction);
1371
- }
1372
- return {
1373
- passed: result.verification.passed,
1374
- summary: result.verification.summary,
1375
- ...(result.verification.steps?.length ? { steps: result.verification.steps } : {}),
1376
- ...(warnings.length ? { warnings } : {})
1377
- };
1378
- }
1379
- function detectVerificationContradiction(result) {
1380
- if (!result.verification.passed) {
1381
- return undefined;
1382
- }
1383
- const sources = [result.summary, result.failure?.message]
1384
- .filter((value) => typeof value === "string" && value.trim().length > 0);
1385
- const contradictionPatterns = [
1386
- /createprocessasuserw failed:\s*\d+/iu,
1387
- /\bverifier not run\b/iu,
1388
- /\bfailed before verifier\b/iu,
1389
- /\bnever launched\b/iu,
1390
- /\bfailed to launch\b/iu,
1391
- /\bcould not launch\b/iu
1392
- ];
1393
- for (const source of sources) {
1394
- const match = contradictionPatterns.find((pattern) => pattern.test(source));
1395
- if (!match) {
1396
- continue;
1397
- }
1398
- const excerpt = truncateVerificationDetail(source.trim().replace(/\s+/gu, " "), 220);
1399
- return `Adapter output reported a tool-launch problem before MartinLoop ran its own verifier: ${excerpt}`;
1400
- }
1401
- return undefined;
1402
- }
1403
- function resolveActiveRunsRoot(store) {
1404
- const configuredRunsRoot = store?.runsRoot?.trim();
1405
- return configuredRunsRoot && configuredRunsRoot.length > 0 ? configuredRunsRoot : resolveRunsRoot();
1406
- }
1407
- function toPatchDecisionArtifact(decision) {
1408
- return {
1409
- decision: decision.decision,
1410
- summary: decision.summary,
1411
- reasonCodes: decision.reasonCodes
1412
- };
1413
- }
1414
- const SANDBOX_WRITE_BLOCKED_PATTERNS = [
1415
- /createprocessasuserw failed:\s*\d+/iu,
1416
- /windows sandbox: runner error/iu,
1417
- /writing is blocked by read-only sandbox/iu,
1418
- /read-only filesystem sandbox/iu,
1419
- /approval is disabled/iu
1420
- ];
1421
- function detectSandboxWriteBlocked(result) {
1422
- const sources = [result.summary, result.failure?.message]
1423
- .filter((value) => typeof value === "string" && value.trim().length > 0);
1424
- return sources.some((source) => SANDBOX_WRITE_BLOCKED_PATTERNS.some((pattern) => pattern.test(source)));
1425
- }
1426
- function classifyPatchDecisionFailure(decision, result) {
1427
- if (decision.reasonCodes.includes("grounding_failure")) {
1428
- return {
1429
- failureClass: "repo_grounding_failure",
1430
- rationale: "Patch truth discarded the attempt because grounding evidence contradicted the patch.",
1431
- retryable: true,
1432
- recommendedIntervention: "run_verifier"
1433
- };
1434
- }
1435
- if (decision.reasonCodes.includes("scope_violation")) {
1436
- return {
1437
- failureClass: "scope_creep",
1438
- rationale: "Patch truth discarded the attempt because it changed files outside the task scope.",
1439
- retryable: true,
1440
- recommendedIntervention: "tighten_task"
1441
- };
1442
- }
1443
- if (decision.reasonCodes.includes("verifier_regressed") ||
1444
- decision.reasonCodes.includes("large_diff_no_improvement")) {
1445
- return {
1446
- failureClass: "test_regression",
1447
- rationale: "Patch truth discarded the attempt because the verifier regressed or stopped improving.",
1448
- retryable: true,
1449
- recommendedIntervention: "run_verifier"
1450
- };
1451
- }
1452
- if (decision.reasonCodes.includes("human_approval_required")) {
1453
- return {
1454
- failureClass: "scope_creep",
1455
- rationale: "Patch truth escalated the attempt because it requires explicit human approval.",
1456
- retryable: false,
1457
- recommendedIntervention: "escalate_human"
1458
- };
1459
- }
1460
- if (decision.reasonCodes.includes("safety_violation")) {
1461
- return {
1462
- failureClass: "scope_creep",
1463
- rationale: "Patch truth escalated the attempt because safety evidence blocked it.",
1464
- retryable: false,
1465
- recommendedIntervention: "escalate_human"
1466
- };
1467
- }
1468
- if (result && detectSandboxWriteBlocked(result)) {
1469
- return {
1470
- failureClass: "sandbox_write_blocked",
1471
- rationale: "The adapter reported a completed patch but the workspace-write sandbox blocked the underlying file write, so no trustworthy code change landed on disk.",
1472
- retryable: true,
1473
- recommendedIntervention: "switch_adapter"
1474
- };
1475
- }
1476
- return {
1477
- failureClass: "no_progress",
1478
- rationale: "Patch truth discarded the attempt because it did not produce a trustworthy code change.",
1479
- retryable: true,
1480
- recommendedIntervention: "compress_context"
1481
- };
1482
- }
1483
- function applyPatchFailureToLoop(loop, input) {
1484
- return {
1485
- ...loop,
1486
- attempts: loop.attempts.map((attempt) => attempt.attemptId === input.attemptId
1487
- ? {
1488
- ...attempt,
1489
- summary: input.summary,
1490
- failureClass: input.failure.failureClass,
1491
- intervention: input.failure.recommendedIntervention
1492
- }
1493
- : attempt)
1494
- };
1495
- }