@martinloop/mcp 0.3.6 → 0.3.8

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 (119) hide show
  1. package/LICENSE +216 -0
  2. package/README.md +105 -97
  3. package/dist/discovery-metadata.d.ts +1 -1
  4. package/dist/discovery-metadata.js +1 -1
  5. package/dist/package-version.d.ts +1 -1
  6. package/dist/package-version.js +1 -1
  7. package/dist/resources.d.ts +2 -1
  8. package/dist/resources.js +251 -197
  9. package/dist/server-validation.d.ts +1 -1
  10. package/dist/server-validation.js +11 -16
  11. package/dist/server.d.ts +0 -23
  12. package/dist/server.js +7 -152
  13. package/dist/tools/preflight.d.ts +2 -0
  14. package/dist/tools/preflight.js +42 -2
  15. package/dist/tools/run-loop.d.ts +2 -1
  16. package/dist/tools/run-loop.js +5 -1
  17. package/dist/tools/tool-support.js +12 -1
  18. package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +146 -0
  19. package/dist/vendor/adapters/adapters/src/claude-cli.js +1100 -0
  20. package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +103 -0
  21. package/dist/vendor/adapters/adapters/src/cli-bridge.js +569 -0
  22. package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +78 -0
  23. package/dist/vendor/adapters/adapters/src/codex-launcher.js +663 -0
  24. package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +10 -0
  25. package/dist/vendor/adapters/adapters/src/direct-provider.js +40 -0
  26. package/dist/vendor/adapters/adapters/src/index.d.ts +8 -0
  27. package/dist/vendor/adapters/adapters/src/index.js +8 -0
  28. package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +62 -0
  29. package/dist/vendor/adapters/adapters/src/openai-compatible.js +306 -0
  30. package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +17 -0
  31. package/dist/vendor/adapters/adapters/src/runtime-support.js +59 -0
  32. package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +8 -0
  33. package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +40 -0
  34. package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +8 -0
  35. package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +12 -0
  36. package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +9 -0
  37. package/dist/vendor/adapters/adapters/src/verifier-only.js +63 -0
  38. package/dist/vendor/adapters/claude-cli.js +130 -68
  39. package/dist/vendor/adapters/codex-launcher.js +10 -1
  40. package/dist/vendor/adapters/contracts/src/context-graph.d.ts +48 -0
  41. package/dist/vendor/adapters/contracts/src/context-graph.js +26 -0
  42. package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +78 -0
  43. package/dist/vendor/adapters/contracts/src/execution-policy.js +43 -0
  44. package/dist/vendor/adapters/contracts/src/governance.d.ts +24 -0
  45. package/dist/vendor/adapters/contracts/src/governance.js +11 -0
  46. package/dist/vendor/adapters/contracts/src/identity.d.ts +29 -0
  47. package/dist/vendor/adapters/contracts/src/identity.js +30 -0
  48. package/dist/vendor/adapters/contracts/src/index.d.ts +412 -0
  49. package/dist/vendor/adapters/contracts/src/index.js +225 -0
  50. package/dist/vendor/adapters/contracts/src/operator.d.ts +19 -0
  51. package/dist/vendor/adapters/contracts/src/operator.js +11 -0
  52. package/dist/vendor/adapters/contracts/src/trajectory.d.ts +26 -0
  53. package/dist/vendor/adapters/contracts/src/trajectory.js +27 -0
  54. package/dist/vendor/adapters/core/src/compiler.d.ts +52 -0
  55. package/dist/vendor/adapters/core/src/compiler.js +52 -0
  56. package/dist/vendor/adapters/core/src/context-graph.d.ts +3 -0
  57. package/dist/vendor/adapters/core/src/context-graph.js +371 -0
  58. package/dist/vendor/adapters/core/src/context-integrity.d.ts +26 -0
  59. package/dist/vendor/adapters/core/src/context-integrity.js +80 -0
  60. package/dist/vendor/adapters/core/src/grounding.d.ts +38 -0
  61. package/dist/vendor/adapters/core/src/grounding.js +273 -0
  62. package/dist/vendor/adapters/core/src/identity.d.ts +20 -0
  63. package/dist/vendor/adapters/core/src/identity.js +86 -0
  64. package/dist/vendor/adapters/core/src/index.d.ts +186 -0
  65. package/dist/vendor/adapters/core/src/index.js +1495 -0
  66. package/dist/vendor/adapters/core/src/leash.d.ts +48 -0
  67. package/dist/vendor/adapters/core/src/leash.js +497 -0
  68. package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +18 -0
  69. package/dist/vendor/adapters/core/src/persistence/compiler.js +34 -0
  70. package/dist/vendor/adapters/core/src/persistence/index.d.ts +10 -0
  71. package/dist/vendor/adapters/core/src/persistence/index.js +5 -0
  72. package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +39 -0
  73. package/dist/vendor/adapters/core/src/persistence/integrity.js +252 -0
  74. package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +23 -0
  75. package/dist/vendor/adapters/core/src/persistence/ledger.js +9 -0
  76. package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +63 -0
  77. package/dist/vendor/adapters/core/src/persistence/memory-store.js +115 -0
  78. package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +66 -0
  79. package/dist/vendor/adapters/core/src/persistence/runs-reader.js +140 -0
  80. package/dist/vendor/adapters/core/src/persistence/store.d.ts +89 -0
  81. package/dist/vendor/adapters/core/src/persistence/store.js +125 -0
  82. package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +85 -0
  83. package/dist/vendor/adapters/core/src/persistence/trace-store.js +125 -0
  84. package/dist/vendor/adapters/core/src/policy-compiler.d.ts +2 -0
  85. package/dist/vendor/adapters/core/src/policy-compiler.js +195 -0
  86. package/dist/vendor/adapters/core/src/policy.d.ts +156 -0
  87. package/dist/vendor/adapters/core/src/policy.js +704 -0
  88. package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +53 -0
  89. package/dist/vendor/adapters/core/src/repo-analyzer.js +263 -0
  90. package/dist/vendor/adapters/core/src/rollback.d.ts +15 -0
  91. package/dist/vendor/adapters/core/src/rollback.js +253 -0
  92. package/dist/vendor/adapters/core/src/routing.d.ts +82 -0
  93. package/dist/vendor/adapters/core/src/routing.js +336 -0
  94. package/dist/vendor/adapters/core/src/test-integrity.d.ts +64 -0
  95. package/dist/vendor/adapters/core/src/test-integrity.js +197 -0
  96. package/dist/vendor/adapters/core/src/trajectory.d.ts +18 -0
  97. package/dist/vendor/adapters/core/src/trajectory.js +295 -0
  98. package/dist/vendor/adapters/openai-compatible.js +6 -6
  99. package/dist/vendor/adapters/verifier-only.js +2 -2
  100. package/dist/vendor/contracts/delivery.d.ts +47 -0
  101. package/dist/vendor/contracts/delivery.js +18 -0
  102. package/dist/vendor/contracts/index.d.ts +3 -1
  103. package/dist/vendor/contracts/index.js +2 -0
  104. package/dist/vendor/core/compiler.js +4 -9
  105. package/dist/vendor/core/delivery/index.d.ts +6 -0
  106. package/dist/vendor/core/delivery/index.js +4 -0
  107. package/dist/vendor/core/delivery/message-client.d.ts +18 -0
  108. package/dist/vendor/core/delivery/message-client.js +38 -0
  109. package/dist/vendor/core/delivery/message-ledger.d.ts +35 -0
  110. package/dist/vendor/core/delivery/message-ledger.js +97 -0
  111. package/dist/vendor/core/delivery/message-schema.d.ts +16 -0
  112. package/dist/vendor/core/delivery/message-schema.js +111 -0
  113. package/dist/vendor/core/delivery/update-check.d.ts +19 -0
  114. package/dist/vendor/core/delivery/update-check.js +107 -0
  115. package/dist/vendor/core/index.d.ts +2 -0
  116. package/dist/vendor/core/index.js +3 -88
  117. package/dist/vendor/core/rollback.js +2 -3
  118. package/package.json +83 -76
  119. package/server.json +21 -21
@@ -0,0 +1,704 @@
1
+ /**
2
+ * Maps a PolicyPhase to the LoopLifecycleState used in events and persistence.
3
+ * FailureClass remains a thin output label — policy reads EvidenceVector, not the label.
4
+ */
5
+ export function policyPhaseToLifecycleState(phase) {
6
+ switch (phase) {
7
+ case "GATHER":
8
+ case "ADMIT":
9
+ case "PATCH":
10
+ return "running";
11
+ case "VERIFY":
12
+ return "verifying";
13
+ case "RECOVER":
14
+ return "running";
15
+ case "ESCALATE":
16
+ case "HANDOFF":
17
+ return "human_escalation";
18
+ case "ABORT":
19
+ return "stuck_exit";
20
+ default:
21
+ return "running";
22
+ }
23
+ }
24
+ /**
25
+ * Determines the next PolicyPhase given the current phase and last result.
26
+ * This is the explicit typed transition table — no implicit fallthrough.
27
+ */
28
+ export function nextPolicyPhase(current, result, costState, retryCount) {
29
+ // Successful verification → complete via HANDOFF
30
+ if (result.status === "completed" && result.verification.passed) {
31
+ return "HANDOFF";
32
+ }
33
+ // Hard budget limit → ABORT
34
+ if (costState.shouldStop) {
35
+ return "ABORT";
36
+ }
37
+ switch (current) {
38
+ case "GATHER":
39
+ return "ADMIT";
40
+ case "ADMIT":
41
+ return "PATCH";
42
+ case "PATCH":
43
+ return "VERIFY";
44
+ case "VERIFY":
45
+ return retryCount >= 2 ? "ESCALATE" : "RECOVER";
46
+ case "RECOVER":
47
+ return retryCount >= 3 ? "ESCALATE" : "PATCH";
48
+ case "ESCALATE":
49
+ return "ABORT";
50
+ case "ABORT":
51
+ case "HANDOFF":
52
+ return current; // terminal — no further transitions
53
+ default:
54
+ return "PATCH";
55
+ }
56
+ }
57
+ export function classifyFailure(input) {
58
+ if (input.result.failure?.classHint) {
59
+ return mapClassHintToAssessment(input.result.failure.classHint, input.attempts);
60
+ }
61
+ const message = [
62
+ input.result.summary,
63
+ input.result.verification.summary,
64
+ input.result.failure?.message
65
+ ]
66
+ .filter(Boolean)
67
+ .join(" ")
68
+ .toLowerCase();
69
+ const repeatedFailure = detectRepeatedFailure(input.attempts);
70
+ if (containsPositive(message, [
71
+ "enoent",
72
+ "command not found",
73
+ "missing from path",
74
+ "not in path",
75
+ "could not find"
76
+ ])) {
77
+ return {
78
+ failureClass: "environment_mismatch",
79
+ rationale: "The adapter could not access the required local runtime or CLI tooling.",
80
+ retryable: false,
81
+ recommendedIntervention: "switch_adapter",
82
+ diagnosticHint: diagnoseEnvironmentIssue(message)
83
+ };
84
+ }
85
+ if (containsPositive(message, ["syntax error", "parse error", "typescript error"])) {
86
+ return {
87
+ failureClass: "syntax_error",
88
+ rationale: "The attempt failed on a parser or compiler-style issue.",
89
+ retryable: true,
90
+ recommendedIntervention: "compress_context",
91
+ diagnosticHint: "Check for malformed edits — the verifier found a syntax or parse error in the changed files."
92
+ };
93
+ }
94
+ if (containsPositive(message, [
95
+ "cannot find module",
96
+ "module not found",
97
+ "cannot resolve module",
98
+ "cannot find name",
99
+ "does not exist on type",
100
+ "no such file or directory",
101
+ "unknown import",
102
+ "unknown symbol",
103
+ "undefined reference"
104
+ ])) {
105
+ return {
106
+ failureClass: "repo_grounding_failure",
107
+ rationale: "The attempt referenced modules, symbols, or files that are not grounded in the repo or approved docs.",
108
+ retryable: true,
109
+ recommendedIntervention: repeatedFailure === "repo_grounding_failure" ? "tighten_task" : "run_verifier",
110
+ diagnosticHint: diagnoseGroundingIssue(message)
111
+ };
112
+ }
113
+ if (containsPositive(message, [
114
+ "verification failed",
115
+ "test failed",
116
+ "regression",
117
+ "failing test",
118
+ "assertionerror"
119
+ ])) {
120
+ return {
121
+ failureClass: "verification_failure",
122
+ rationale: "The proposed fix did not satisfy the verification gate.",
123
+ retryable: true,
124
+ recommendedIntervention: repeatedFailure === "verification_failure" ? "tighten_task" : "run_verifier",
125
+ diagnosticHint: diagnoseVerificationIssue(message)
126
+ };
127
+ }
128
+ if (containsPositive(message, [
129
+ "scope creep",
130
+ "too broad",
131
+ "unrelated files",
132
+ "outside allowed paths",
133
+ "forbidden file"
134
+ ])) {
135
+ return {
136
+ failureClass: "scope_creep",
137
+ rationale: "The attempt drifted away from the original task boundary.",
138
+ retryable: true,
139
+ recommendedIntervention: "tighten_task"
140
+ };
141
+ }
142
+ if (!input.result.verification.passed) {
143
+ return {
144
+ failureClass: "verification_failure",
145
+ rationale: "Verification did not pass and no specific failure pattern was detected.",
146
+ retryable: true,
147
+ recommendedIntervention: repeatedFailure === "verification_failure" ? "tighten_task" : "run_verifier"
148
+ };
149
+ }
150
+ if (detectOscillation(input.attempts)) {
151
+ return {
152
+ failureClass: "logic_error",
153
+ rationale: "Oscillating failure pattern detected — switching strategy to break the cycle.",
154
+ retryable: true,
155
+ recommendedIntervention: "change_model"
156
+ };
157
+ }
158
+ return {
159
+ failureClass: repeatedFailure ?? "logic_error",
160
+ rationale: "The loop is still failing to produce a correct implementation.",
161
+ retryable: true,
162
+ recommendedIntervention: repeatedFailure === "logic_error" ? "change_model" : "compress_context"
163
+ };
164
+ }
165
+ export function evaluateCostGovernor(input) {
166
+ const remainingBudgetUsd = roundUsd(input.budget.maxUsd - input.cost.actualUsd);
167
+ const remainingIterations = Math.max(input.budget.maxIterations - input.attemptsUsed, 0);
168
+ const remainingTokens = Math.max(input.budget.maxTokens - input.cost.tokensIn - input.cost.tokensOut, 0);
169
+ if (input.cost.actualUsd >= input.budget.maxUsd ||
170
+ input.attemptsUsed >= input.budget.maxIterations ||
171
+ remainingTokens <= 0) {
172
+ return {
173
+ pressure: "hard_limit",
174
+ shouldStop: true,
175
+ remainingBudgetUsd,
176
+ remainingIterations,
177
+ remainingTokens,
178
+ recommendedIntervention: "stop_loop"
179
+ };
180
+ }
181
+ if (input.cost.actualUsd >= input.budget.softLimitUsd) {
182
+ return {
183
+ pressure: "soft_limit",
184
+ shouldStop: false,
185
+ remainingBudgetUsd,
186
+ remainingIterations,
187
+ remainingTokens,
188
+ recommendedIntervention: "compress_context"
189
+ };
190
+ }
191
+ return {
192
+ pressure: "healthy",
193
+ shouldStop: false,
194
+ remainingBudgetUsd,
195
+ remainingIterations,
196
+ remainingTokens
197
+ };
198
+ }
199
+ export function calculateCostPerOutcome(input) {
200
+ const costPerAttempt = input.attemptCount > 0
201
+ ? roundUsd(input.totalCostUsd / input.attemptCount)
202
+ : 0;
203
+ return {
204
+ costPerAcceptedChange: input.accepted ? input.totalCostUsd : undefined,
205
+ costPerAttempt,
206
+ acceptanceRate: input.accepted ? 1 : 0,
207
+ wastedCoordinationUsd: input.accepted
208
+ ? roundUsd(input.preworkCostUsd)
209
+ : roundUsd(input.totalCostUsd)
210
+ };
211
+ }
212
+ export function inferExit(input) {
213
+ if (input.lastResult.status === "completed" && input.lastResult.verification.passed) {
214
+ return {
215
+ shouldExit: true,
216
+ lifecycleState: "completed",
217
+ status: "completed",
218
+ reason: "Martin verified the fix and can hard-complete the loop."
219
+ };
220
+ }
221
+ if (input.costState.shouldStop) {
222
+ return {
223
+ shouldExit: true,
224
+ lifecycleState: "budget_exit",
225
+ status: "exited",
226
+ reason: "Martin exited because the budget governor hit a hard limit."
227
+ };
228
+ }
229
+ if (detectOscillation(input.loop.attempts)) {
230
+ return {
231
+ shouldExit: true,
232
+ lifecycleState: "diminishing_returns",
233
+ status: "exited",
234
+ reason: "Oscillating failure pattern detected — loop is cycling without measurable progress."
235
+ };
236
+ }
237
+ const lastTwo = input.loop.attempts.slice(-2);
238
+ const repeatedFailure = lastTwo.length === 2 &&
239
+ lastTwo.every((attempt) => attempt.failureClass &&
240
+ attempt.failureClass === lastTwo[0]?.failureClass &&
241
+ attempt.failureClass === input.lastFailure?.failureClass);
242
+ if (repeatedFailure && input.lastFailure) {
243
+ return {
244
+ shouldExit: true,
245
+ lifecycleState: "diminishing_returns",
246
+ status: "exited",
247
+ reason: `Martin exited because ${input.lastFailure.failureClass} repeated across consecutive attempts.`
248
+ };
249
+ }
250
+ if (input.lastFailure?.failureClass === "environment_mismatch" &&
251
+ !input.lastFailure.retryable &&
252
+ !input.canSwitchAdapter) {
253
+ return {
254
+ shouldExit: true,
255
+ lifecycleState: "stuck_exit",
256
+ status: "exited",
257
+ reason: "Martin exited because the runtime environment could not support the requested adapter."
258
+ };
259
+ }
260
+ return {
261
+ shouldExit: false,
262
+ lifecycleState: "running",
263
+ status: "running",
264
+ reason: "Martin should continue with another attempt."
265
+ };
266
+ }
267
+ function containsPositive(haystack, needles) {
268
+ return needles.some((needle) => {
269
+ const idx = haystack.indexOf(needle);
270
+ if (idx === -1)
271
+ return false;
272
+ const before = haystack.slice(Math.max(0, idx - 30), idx);
273
+ return !/\b(no|not|without|zero|pass|passes|passing|passed|clear|cleared|fix|fixed|resolve|resolved|0)\s*$/u.test(before);
274
+ });
275
+ }
276
+ function detectOscillation(attempts) {
277
+ const classes = attempts
278
+ .slice(-4)
279
+ .map((a) => a.failureClass)
280
+ .filter(Boolean);
281
+ if (classes.length < 3)
282
+ return false;
283
+ if (classes[0] !== classes[1] && classes[0] === classes[2])
284
+ return true;
285
+ if (classes.length >= 4 &&
286
+ classes[0] !== classes[1] &&
287
+ classes[0] === classes[2] &&
288
+ classes[1] === classes[3])
289
+ return true;
290
+ return false;
291
+ }
292
+ function mapClassHintToAssessment(classHint, attempts) {
293
+ const repeatedFailure = detectRepeatedFailure(attempts);
294
+ switch (classHint) {
295
+ case "syntax_error":
296
+ return {
297
+ failureClass: "syntax_error",
298
+ rationale: "Structural evidence: non-zero exit code with compiler-like error output.",
299
+ retryable: true,
300
+ recommendedIntervention: "compress_context"
301
+ };
302
+ case "repo_grounding_failure":
303
+ return {
304
+ failureClass: "repo_grounding_failure",
305
+ rationale: "Structural evidence: missing repo module, symbol, or file reference.",
306
+ retryable: true,
307
+ recommendedIntervention: repeatedFailure === "repo_grounding_failure" ? "tighten_task" : "run_verifier"
308
+ };
309
+ case "scope_creep":
310
+ return {
311
+ failureClass: "scope_creep",
312
+ rationale: "Structural evidence: agent output exceeded the task contract or touched forbidden files.",
313
+ retryable: true,
314
+ recommendedIntervention: "tighten_task"
315
+ };
316
+ case "hallucination":
317
+ return {
318
+ failureClass: "hallucination",
319
+ rationale: "Structural evidence: suspiciously short or trivial agent response.",
320
+ retryable: true,
321
+ recommendedIntervention: repeatedFailure === "hallucination" ? "change_model" : "run_verifier"
322
+ };
323
+ case "verification_failure":
324
+ case "test_regression":
325
+ return {
326
+ failureClass: classHint,
327
+ rationale: "Structural evidence: verification commands did not pass.",
328
+ retryable: true,
329
+ recommendedIntervention: repeatedFailure === classHint ? "tighten_task" : "run_verifier"
330
+ };
331
+ default:
332
+ return {
333
+ failureClass: classHint,
334
+ rationale: "Structural hint provided by adapter.",
335
+ retryable: true,
336
+ recommendedIntervention: repeatedFailure ? "change_model" : "compress_context"
337
+ };
338
+ }
339
+ }
340
+ function detectRepeatedFailure(attempts) {
341
+ const lastTwo = attempts.slice(-2);
342
+ if (lastTwo.length === 2 &&
343
+ lastTwo[0]?.failureClass &&
344
+ lastTwo[0].failureClass === lastTwo[1]?.failureClass) {
345
+ return lastTwo[0].failureClass;
346
+ }
347
+ return undefined;
348
+ }
349
+ function roundUsd(value) {
350
+ return Math.round(value * 100) / 100;
351
+ }
352
+ export function evaluateBudgetPreflight(input) {
353
+ const pricePerMToken = input.pricePerMTokenUsd ?? 3.0;
354
+ const rawPromptTokens = Math.ceil(input.promptCharCount / 4);
355
+ const estimatedPromptTokens = Math.ceil(rawPromptTokens * 1.2);
356
+ const estimatedToolOverheadTokens = 800 + input.attemptCount * 200;
357
+ const estimatedOutputTokensMax = 4_000;
358
+ const estimatedVerifierCostUsd = 0.01;
359
+ const estimatedAttemptCostUsd = roundUsd(((estimatedPromptTokens + estimatedToolOverheadTokens + estimatedOutputTokensMax) /
360
+ 1_000_000) *
361
+ pricePerMToken) + estimatedVerifierCostUsd;
362
+ const provenance = "estimated";
363
+ const estimate = {
364
+ estimatedPromptTokens,
365
+ estimatedToolOverheadTokens,
366
+ estimatedOutputTokensMax,
367
+ estimatedVerifierCostUsd,
368
+ estimatedAttemptCostUsd,
369
+ provenance
370
+ };
371
+ if (estimatedAttemptCostUsd > input.remainingBudgetUsd) {
372
+ return {
373
+ allowed: false,
374
+ reason: `Preflight: estimated attempt cost $${estimatedAttemptCostUsd} exceeds remaining budget $${input.remainingBudgetUsd}.`,
375
+ estimate
376
+ };
377
+ }
378
+ const perAttemptCap = input.perAttemptCapUsd ?? Math.max(input.remainingBudgetUsd * 0.2, 0.05);
379
+ if (estimatedAttemptCostUsd > perAttemptCap) {
380
+ return {
381
+ allowed: false,
382
+ reason: `Preflight: estimated attempt cost $${estimatedAttemptCostUsd} exceeds per-attempt cap $${roundUsd(perAttemptCap)}.`,
383
+ estimate
384
+ };
385
+ }
386
+ return {
387
+ allowed: true,
388
+ reason: "Preflight passed.",
389
+ estimate
390
+ };
391
+ }
392
+ export function computeEvidenceVector(input) {
393
+ const typeErrors = input.compilerOutput
394
+ ? (input.compilerOutput.match(/\berror TS\d+/g) ?? []).length
395
+ : 0;
396
+ const compileErrors = typeErrors +
397
+ (input.compilerOutput ? (input.compilerOutput.match(/\bSyntaxError\b/g) ?? []).length : 0);
398
+ const failingTests = input.testOutput
399
+ ? (input.testOutput.match(/\bFAIL\b/gi) ?? []).length +
400
+ (input.testOutput.match(/\bfailed\b/gi) ?? []).length +
401
+ (input.testOutput.match(/[✗×]/g) ?? []).length
402
+ : 0;
403
+ const verifierScore = input.verifierScore ?? 0;
404
+ let diffNovelty = 1;
405
+ if (input.diff && input.previousDiff) {
406
+ const currentTokens = tokenizeDiff(input.diff);
407
+ const previousTokens = tokenizeDiff(input.previousDiff);
408
+ if (previousTokens.size > 0) {
409
+ const overlap = [...currentTokens].filter((token) => previousTokens.has(token)).length;
410
+ const similarity = overlap / Math.max(currentTokens.size, previousTokens.size, 1);
411
+ diffNovelty = Math.max(0, 1 - similarity);
412
+ }
413
+ }
414
+ const forbiddenTouchedFileCount = input.forbiddenTouchedFiles?.length ?? 0;
415
+ const missingSymbolCount = input.missingSymbols?.length ?? 0;
416
+ const progressDelta = input.verifierScore !== undefined && input.previousVerifierScore !== undefined
417
+ ? Math.max(0, input.verifierScore - input.previousVerifierScore)
418
+ : 0;
419
+ const costPerProgressUnit = progressDelta > 0 && input.actualUsd !== undefined ? roundUsd(input.actualUsd / progressDelta) : 0;
420
+ const safetyRiskScore = Math.min(1, forbiddenTouchedFileCount * 0.3 + missingSymbolCount * 0.1);
421
+ return {
422
+ compileErrors,
423
+ typeErrors,
424
+ failingTests,
425
+ verifierScore,
426
+ diffNovelty,
427
+ forbiddenTouchedFileCount,
428
+ missingSymbolCount,
429
+ costPerProgressUnit,
430
+ retryCountForSurface: input.retryCountForSurface ?? 0,
431
+ safetyRiskScore
432
+ };
433
+ }
434
+ export function selectRecoveryRecipe(evidence) {
435
+ if (evidence.safetyRiskScore >= 0.7 || evidence.forbiddenTouchedFileCount > 2) {
436
+ return {
437
+ recipe: "abort_safety_violation",
438
+ rationale: "High safety risk score or multiple forbidden file touches. Abort required.",
439
+ intervention: "escalate_human"
440
+ };
441
+ }
442
+ if (evidence.missingSymbolCount > 0 && evidence.retryCountForSurface <= 1) {
443
+ return {
444
+ recipe: "force_repo_anatomy_slices",
445
+ rationale: "Missing symbols detected. Force repo grounding context into the next prompt.",
446
+ intervention: "run_verifier"
447
+ };
448
+ }
449
+ if (evidence.forbiddenTouchedFileCount > 0) {
450
+ return {
451
+ recipe: "tighten_allowlist_reduce_patch",
452
+ rationale: "Patch touched forbidden files. Tighten scope and reduce patch budget.",
453
+ intervention: "tighten_task"
454
+ };
455
+ }
456
+ if (evidence.compileErrors > 0 || evidence.typeErrors > 0) {
457
+ return {
458
+ recipe: "narrow_prompt_targeted_files",
459
+ rationale: "Compile or type errors detected. Narrow the next prompt to targeted files.",
460
+ intervention: "compress_context"
461
+ };
462
+ }
463
+ if (evidence.failingTests > 0 && evidence.retryCountForSurface <= 2) {
464
+ return {
465
+ recipe: "failing_tests_only",
466
+ rationale: "Test failures remain. Focus on the failing tests and touched files only.",
467
+ intervention: "run_verifier"
468
+ };
469
+ }
470
+ if (evidence.diffNovelty < 0.2) {
471
+ if (evidence.retryCountForSurface >= 2) {
472
+ return {
473
+ recipe: "strategy_swap",
474
+ rationale: "Very low diff novelty across repeated retries. Swap strategy.",
475
+ intervention: "change_model"
476
+ };
477
+ }
478
+ return {
479
+ recipe: "narrow_prompt_targeted_files",
480
+ rationale: "Low diff novelty suggests repetition. Compress context and narrow focus.",
481
+ intervention: "compress_context"
482
+ };
483
+ }
484
+ if (evidence.costPerProgressUnit > 5 || evidence.retryCountForSurface >= 3) {
485
+ return {
486
+ recipe: "downgrade_model",
487
+ rationale: "Cost efficiency is degrading or retries are exhausted. Downgrade the model.",
488
+ intervention: "change_model"
489
+ };
490
+ }
491
+ return {
492
+ recipe: "escalate_human",
493
+ rationale: "No specific recovery pattern matched. Escalate for human review.",
494
+ intervention: "escalate_human"
495
+ };
496
+ }
497
+ export function evaluatePatchDecision(input) {
498
+ const score = scorePatchDecision(input);
499
+ const reasonCodes = [...score.reasonCodes];
500
+ const decision = decidePatchOutcome(input, reasonCodes);
501
+ return {
502
+ decision,
503
+ summary: buildPatchDecisionSummary(decision, reasonCodes, input.summary),
504
+ reasonCodes,
505
+ score
506
+ };
507
+ }
508
+ export function scorePatchDecision(input) {
509
+ const verifierScore = input.verifierScore ?? (input.verificationPassed ? 1 : 0);
510
+ const previousVerifierScore = input.previousVerifierScore ?? 0;
511
+ const verifierDelta = roundScore(verifierScore - previousVerifierScore);
512
+ const groundingViolationCount = input.groundingViolationCount ?? 0;
513
+ const scopeViolationCount = input.scopeViolationCount ?? 0;
514
+ const safetyViolationCount = input.safetyViolationCount ?? 0;
515
+ const changedFileEvidenceAvailable = input.changedFileCount !== undefined;
516
+ const changedFileCount = input.changedFileCount ?? 0;
517
+ const noveltyScore = input.diffNovelty ?? (changedFileCount > 0 ? 1 : 0);
518
+ const diffRiskScore = computeDiffRiskScore(input.diffStats);
519
+ const costUsd = roundUsd(input.costUsd ?? 0);
520
+ const reasonCodes = [];
521
+ if (input.verificationPassed) {
522
+ reasonCodes.push("verifier_passed");
523
+ }
524
+ if (groundingViolationCount > 0) {
525
+ reasonCodes.push("grounding_failure");
526
+ }
527
+ if (scopeViolationCount > 0) {
528
+ reasonCodes.push("scope_violation");
529
+ }
530
+ if (changedFileEvidenceAvailable && changedFileCount === 0) {
531
+ reasonCodes.push("no_code_change");
532
+ }
533
+ if (input.humanApprovalRequired) {
534
+ reasonCodes.push("human_approval_required");
535
+ }
536
+ if (safetyViolationCount > 0) {
537
+ reasonCodes.push("safety_violation");
538
+ }
539
+ if (verifierDelta < 0) {
540
+ reasonCodes.push("verifier_regressed");
541
+ }
542
+ if (!input.verificationPassed && noveltyScore < 0.2 && verifierDelta <= 0) {
543
+ reasonCodes.push("low_novelty_no_progress");
544
+ }
545
+ if (!input.verificationPassed && diffRiskScore >= 0.7 && verifierDelta <= 0) {
546
+ reasonCodes.push("large_diff_no_improvement");
547
+ }
548
+ if (!input.verificationPassed &&
549
+ reasonCodes.length === 0) {
550
+ reasonCodes.push("verifier_not_improved");
551
+ }
552
+ let score = 0;
553
+ if (input.verificationPassed) {
554
+ score += 0.55;
555
+ }
556
+ score += Math.max(verifierDelta, 0) * 0.2;
557
+ score -= groundingViolationCount * 0.45;
558
+ score -= scopeViolationCount * 0.35;
559
+ score -= safetyViolationCount * 0.45;
560
+ if (input.humanApprovalRequired) {
561
+ score -= 0.25;
562
+ }
563
+ if (changedFileEvidenceAvailable && changedFileCount === 0) {
564
+ score -= 0.35;
565
+ }
566
+ if (!input.verificationPassed && noveltyScore < 0.2 && verifierDelta <= 0) {
567
+ score -= 0.2;
568
+ }
569
+ if (!input.verificationPassed && diffRiskScore >= 0.7 && verifierDelta <= 0) {
570
+ score -= 0.25;
571
+ }
572
+ score -= diffRiskScore * 0.1;
573
+ score -= Math.min(costUsd / 10, 0.1);
574
+ return {
575
+ score: roundScore(Math.max(-1, Math.min(1, score))),
576
+ verifierScore: roundScore(verifierScore),
577
+ verifierDelta,
578
+ groundingViolationCount,
579
+ scopeViolationCount,
580
+ safetyViolationCount,
581
+ changedFileCount,
582
+ diffRiskScore,
583
+ noveltyScore: roundScore(noveltyScore),
584
+ costUsd,
585
+ reasonCodes
586
+ };
587
+ }
588
+ function tokenizeDiff(diff) {
589
+ const tokens = new Set();
590
+ for (const line of diff.split("\n")) {
591
+ if ((!line.startsWith("+") && !line.startsWith("-")) || line.startsWith("+++") || line.startsWith("---")) {
592
+ continue;
593
+ }
594
+ for (const token of line.slice(1).match(/[A-Za-z_][A-Za-z0-9_]{2,}/g) ?? []) {
595
+ tokens.add(token);
596
+ }
597
+ }
598
+ return tokens;
599
+ }
600
+ function decidePatchOutcome(input, reasonCodes) {
601
+ if (input.humanApprovalRequired || reasonCodes.includes("safety_violation")) {
602
+ return "ESCALATE";
603
+ }
604
+ if (reasonCodes.includes("grounding_failure") ||
605
+ reasonCodes.includes("scope_violation") ||
606
+ reasonCodes.includes("no_code_change") ||
607
+ reasonCodes.includes("verifier_regressed") ||
608
+ reasonCodes.includes("large_diff_no_improvement") ||
609
+ reasonCodes.includes("low_novelty_no_progress") ||
610
+ reasonCodes.includes("verifier_not_improved")) {
611
+ return "DISCARD";
612
+ }
613
+ if (input.verificationPassed) {
614
+ return "KEEP";
615
+ }
616
+ return "DISCARD";
617
+ }
618
+ function buildPatchDecisionSummary(decision, reasonCodes, summary) {
619
+ const headline = {
620
+ KEEP: "Patch kept.",
621
+ DISCARD: "Patch discarded.",
622
+ ESCALATE: "Patch requires escalation.",
623
+ HANDOFF: "Patch requires handoff."
624
+ }[decision];
625
+ const reasons = reasonCodes.join(", ");
626
+ if (summary) {
627
+ return `${headline} Reasons: ${reasons || "none"}. Attempt summary: ${summary}`;
628
+ }
629
+ return `${headline} Reasons: ${reasons || "none"}.`;
630
+ }
631
+ function computeDiffRiskScore(input) {
632
+ if (!input) {
633
+ return 0;
634
+ }
635
+ const fileRisk = Math.min(input.filesChanged / 8, 1);
636
+ const lineRisk = Math.min((input.addedLines + input.deletedLines) / 200, 1);
637
+ return roundScore(Math.max(fileRisk, lineRisk));
638
+ }
639
+ function roundScore(value) {
640
+ return Math.round(value * 100) / 100;
641
+ }
642
+ // ---------------------------------------------------------------------------
643
+ // Diagnostic hint generators
644
+ //
645
+ // These extract actionable context from failure messages so the next attempt
646
+ // gets a concrete diagnosis, not just "verification failed."
647
+ // ---------------------------------------------------------------------------
648
+ const KNOWN_TOOLS = {
649
+ bun: "npm install -g bun",
650
+ pnpm: "npm install -g pnpm",
651
+ yarn: "npm install -g yarn",
652
+ eslint: "npm install eslint",
653
+ vitest: "npm install vitest",
654
+ jest: "npm install jest",
655
+ tsc: "npm install typescript",
656
+ prettier: "npm install prettier",
657
+ biome: "npm install @biomejs/biome"
658
+ };
659
+ function diagnoseEnvironmentIssue(message) {
660
+ // Try to extract the missing command name
661
+ const cmdMatch = message.match(/(?:command not found|enoent)[:\s]*['"]?(\w[\w.-]*)/i)
662
+ ?? message.match(/['"](\w[\w.-]*)['"].*(?:not found|enoent)/i);
663
+ const missingCmd = cmdMatch?.[1];
664
+ if (missingCmd && KNOWN_TOOLS[missingCmd.toLowerCase()]) {
665
+ return `The verifier needs "${missingCmd}" which is not installed. Fix: ${KNOWN_TOOLS[missingCmd.toLowerCase()]}`;
666
+ }
667
+ if (missingCmd) {
668
+ return `The verifier could not find "${missingCmd}". Ensure it is installed in the workspace before retrying.`;
669
+ }
670
+ return "A required tool or command was not found in the environment. Check that all dependencies referenced by the verify command are installed.";
671
+ }
672
+ function diagnoseGroundingIssue(message) {
673
+ // Extract the missing module/symbol name
674
+ const moduleMatch = message.match(/cannot find module ['"]([^'"]+)['"]/i)
675
+ ?? message.match(/module not found[:\s]*['"]?([^\s'"]+)/i);
676
+ const nameMatch = message.match(/cannot find name ['"]?(\w+)/i);
677
+ if (moduleMatch?.[1]) {
678
+ const mod = moduleMatch[1];
679
+ const isRelative = mod.startsWith(".") || mod.startsWith("/");
680
+ if (isRelative) {
681
+ return `Import "${mod}" points to a file that does not exist. Check the path and filename.`;
682
+ }
683
+ return `Module "${mod}" is not installed. Run npm install or check package.json dependencies.`;
684
+ }
685
+ if (nameMatch?.[1]) {
686
+ return `Symbol "${nameMatch[1]}" is not defined. Check that the import exists and the name is spelled correctly.`;
687
+ }
688
+ return "The code references modules or symbols that do not exist in the repository. Verify imports and file paths.";
689
+ }
690
+ function diagnoseVerificationIssue(message) {
691
+ // Count assertion failures for severity signal
692
+ const assertionCount = (message.match(/assertionerror/gi) ?? []).length;
693
+ if (assertionCount > 3) {
694
+ return `${String(assertionCount)} assertion failures detected — the change may have introduced a regression. Focus on the specific test expectations that broke.`;
695
+ }
696
+ // Check for common patterns
697
+ if (/timeout/i.test(message) && !/budget/i.test(message)) {
698
+ return "Tests timed out. The change may have introduced an infinite loop or unresolved async operation.";
699
+ }
700
+ if (/exit code (?!0)\d+/i.test(message) || /exited with code/i.test(message)) {
701
+ return "The verifier command exited non-zero. Read the output above to identify which specific check failed and why.";
702
+ }
703
+ return "Verification did not pass. Review the verifier output for the specific failing assertion or check.";
704
+ }