@martinloop/mcp 0.3.8 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -106
- package/dist/discovery-metadata.d.ts +1 -1
- package/dist/discovery-metadata.js +1 -1
- package/dist/resources.d.ts +1 -2
- package/dist/resources.js +190 -238
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +16 -11
- package/dist/server.d.ts +23 -0
- package/dist/server.js +152 -7
- package/dist/tools/run-dossier.d.ts +3 -1
- package/dist/tools/run-dossier.js +40 -2
- package/dist/tools/run-loop.d.ts +1 -4
- package/dist/tools/run-loop.js +6 -15
- package/dist/tools/tool-support.d.ts +2 -1
- package/dist/tools/tool-support.js +1 -12
- package/dist/vendor/adapters/claude-cli.js +38 -35
- package/dist/vendor/adapters/cli-bridge.d.ts +9 -1
- package/dist/vendor/adapters/cli-bridge.js +54 -13
- package/dist/vendor/adapters/codex-launcher.d.ts +61 -1
- package/dist/vendor/adapters/codex-launcher.js +118 -19
- package/dist/vendor/adapters/index.d.ts +1 -3
- package/dist/vendor/adapters/index.js +1 -3
- package/dist/vendor/adapters/openai-compatible.js +57 -18
- package/dist/vendor/contracts/adapter-capabilities.d.ts +1 -1
- package/dist/vendor/contracts/context-handoff.d.ts +100 -0
- package/dist/vendor/contracts/context-handoff.js +11 -0
- package/dist/vendor/contracts/context-manifest.d.ts +195 -0
- package/dist/vendor/contracts/context-manifest.js +17 -0
- package/dist/vendor/contracts/context-shadow.d.ts +75 -0
- package/dist/vendor/contracts/context-shadow.js +8 -0
- package/dist/vendor/contracts/exits.d.ts +126 -0
- package/dist/vendor/contracts/exits.js +32 -0
- package/dist/vendor/contracts/index.d.ts +39 -2
- package/dist/vendor/contracts/index.js +22 -0
- package/dist/vendor/contracts/mission.d.ts +102 -0
- package/dist/vendor/contracts/mission.js +64 -0
- package/dist/vendor/contracts/operator.d.ts +3 -1
- package/dist/vendor/contracts/operator.js +2 -1
- package/dist/vendor/contracts/verified-handoff.d.ts +100 -0
- package/dist/vendor/contracts/verified-handoff.js +35 -0
- package/dist/vendor/core/context-chain-gate.d.ts +89 -0
- package/dist/vendor/core/context-chain-gate.js +173 -0
- package/dist/vendor/core/context-compiler.d.ts +53 -0
- package/dist/vendor/core/context-compiler.js +261 -0
- package/dist/vendor/core/context-handoff.d.ts +42 -0
- package/dist/vendor/core/context-handoff.js +167 -0
- package/dist/vendor/core/context-shadow.d.ts +39 -0
- package/dist/vendor/core/context-shadow.js +200 -0
- package/dist/vendor/core/exit-signal.d.ts +88 -0
- package/dist/vendor/core/exit-signal.js +339 -0
- package/dist/vendor/core/exits.d.ts +41 -0
- package/dist/vendor/core/exits.js +258 -0
- package/dist/vendor/core/index.d.ts +42 -1
- package/dist/vendor/core/index.js +1086 -662
- package/dist/vendor/core/mission/aggregate.d.ts +27 -0
- package/dist/vendor/core/mission/aggregate.js +42 -0
- package/dist/vendor/core/mission/index.d.ts +2 -0
- package/dist/vendor/core/mission/index.js +1 -0
- package/dist/vendor/core/persistence/compiler.d.ts +8 -0
- package/dist/vendor/core/persistence/compiler.js +156 -0
- package/dist/vendor/core/persistence/context-handoff-store.d.ts +68 -0
- package/dist/vendor/core/persistence/context-handoff-store.js +112 -0
- package/dist/vendor/core/persistence/index.d.ts +4 -0
- package/dist/vendor/core/persistence/index.js +2 -0
- package/dist/vendor/core/persistence/ledger.d.ts +1 -1
- package/dist/vendor/core/persistence/mission-store.d.ts +56 -0
- package/dist/vendor/core/persistence/mission-store.js +281 -0
- package/dist/vendor/core/policy.d.ts +2 -0
- package/dist/vendor/core/policy.js +5 -2
- package/dist/vendor/core/rollback.js +15 -1
- package/dist/vendor/core/savings.d.ts +13 -0
- package/dist/vendor/core/savings.js +22 -0
- package/dist/vendor/core/termination-store.d.ts +15 -0
- package/dist/vendor/core/termination-store.js +44 -0
- package/dist/vendor/core/verified-handoff.d.ts +67 -0
- package/dist/vendor/core/verified-handoff.js +210 -0
- package/package.json +76 -83
- package/server.json +21 -21
- package/LICENSE +0 -216
- package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +0 -146
- package/dist/vendor/adapters/adapters/src/claude-cli.js +0 -1100
- package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +0 -103
- package/dist/vendor/adapters/adapters/src/cli-bridge.js +0 -569
- package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +0 -78
- package/dist/vendor/adapters/adapters/src/codex-launcher.js +0 -663
- package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +0 -10
- package/dist/vendor/adapters/adapters/src/direct-provider.js +0 -40
- package/dist/vendor/adapters/adapters/src/index.d.ts +0 -8
- package/dist/vendor/adapters/adapters/src/index.js +0 -8
- package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +0 -62
- package/dist/vendor/adapters/adapters/src/openai-compatible.js +0 -306
- package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +0 -17
- package/dist/vendor/adapters/adapters/src/runtime-support.js +0 -59
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +0 -8
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +0 -40
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +0 -8
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +0 -12
- package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +0 -9
- package/dist/vendor/adapters/adapters/src/verifier-only.js +0 -63
- package/dist/vendor/adapters/contracts/src/context-graph.d.ts +0 -48
- package/dist/vendor/adapters/contracts/src/context-graph.js +0 -26
- package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +0 -78
- package/dist/vendor/adapters/contracts/src/execution-policy.js +0 -43
- package/dist/vendor/adapters/contracts/src/governance.d.ts +0 -24
- package/dist/vendor/adapters/contracts/src/governance.js +0 -11
- package/dist/vendor/adapters/contracts/src/identity.d.ts +0 -29
- package/dist/vendor/adapters/contracts/src/identity.js +0 -30
- package/dist/vendor/adapters/contracts/src/index.d.ts +0 -412
- package/dist/vendor/adapters/contracts/src/index.js +0 -225
- package/dist/vendor/adapters/contracts/src/operator.d.ts +0 -19
- package/dist/vendor/adapters/contracts/src/operator.js +0 -11
- package/dist/vendor/adapters/contracts/src/trajectory.d.ts +0 -26
- package/dist/vendor/adapters/contracts/src/trajectory.js +0 -27
- package/dist/vendor/adapters/core/src/compiler.d.ts +0 -52
- package/dist/vendor/adapters/core/src/compiler.js +0 -52
- package/dist/vendor/adapters/core/src/context-graph.d.ts +0 -3
- package/dist/vendor/adapters/core/src/context-graph.js +0 -371
- package/dist/vendor/adapters/core/src/context-integrity.d.ts +0 -26
- package/dist/vendor/adapters/core/src/context-integrity.js +0 -80
- package/dist/vendor/adapters/core/src/grounding.d.ts +0 -38
- package/dist/vendor/adapters/core/src/grounding.js +0 -273
- package/dist/vendor/adapters/core/src/identity.d.ts +0 -20
- package/dist/vendor/adapters/core/src/identity.js +0 -86
- package/dist/vendor/adapters/core/src/index.d.ts +0 -186
- package/dist/vendor/adapters/core/src/index.js +0 -1495
- package/dist/vendor/adapters/core/src/leash.d.ts +0 -48
- package/dist/vendor/adapters/core/src/leash.js +0 -497
- package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +0 -18
- package/dist/vendor/adapters/core/src/persistence/compiler.js +0 -34
- package/dist/vendor/adapters/core/src/persistence/index.d.ts +0 -10
- package/dist/vendor/adapters/core/src/persistence/index.js +0 -5
- package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +0 -39
- package/dist/vendor/adapters/core/src/persistence/integrity.js +0 -252
- package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +0 -23
- package/dist/vendor/adapters/core/src/persistence/ledger.js +0 -9
- package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +0 -63
- package/dist/vendor/adapters/core/src/persistence/memory-store.js +0 -115
- package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +0 -66
- package/dist/vendor/adapters/core/src/persistence/runs-reader.js +0 -140
- package/dist/vendor/adapters/core/src/persistence/store.d.ts +0 -89
- package/dist/vendor/adapters/core/src/persistence/store.js +0 -125
- package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +0 -85
- package/dist/vendor/adapters/core/src/persistence/trace-store.js +0 -125
- package/dist/vendor/adapters/core/src/policy-compiler.d.ts +0 -2
- package/dist/vendor/adapters/core/src/policy-compiler.js +0 -195
- package/dist/vendor/adapters/core/src/policy.d.ts +0 -156
- package/dist/vendor/adapters/core/src/policy.js +0 -704
- package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +0 -53
- package/dist/vendor/adapters/core/src/repo-analyzer.js +0 -263
- package/dist/vendor/adapters/core/src/rollback.d.ts +0 -15
- package/dist/vendor/adapters/core/src/rollback.js +0 -253
- package/dist/vendor/adapters/core/src/routing.d.ts +0 -82
- package/dist/vendor/adapters/core/src/routing.js +0 -336
- package/dist/vendor/adapters/core/src/test-integrity.d.ts +0 -64
- package/dist/vendor/adapters/core/src/test-integrity.js +0 -197
- package/dist/vendor/adapters/core/src/trajectory.d.ts +0 -18
- package/dist/vendor/adapters/core/src/trajectory.js +0 -295
- package/dist/vendor/adapters/stub-agent-cli.d.ts +0 -8
- package/dist/vendor/adapters/stub-agent-cli.js +0 -40
- package/dist/vendor/adapters/verifier-only.d.ts +0 -9
- package/dist/vendor/adapters/verifier-only.js +0 -63
|
@@ -1,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
|
-
}
|