@martinloop/mcp 0.3.3 → 0.3.5

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 (60) hide show
  1. package/README.md +112 -107
  2. package/dist/discovery-metadata.d.ts +6 -6
  3. package/dist/discovery-metadata.js +6 -0
  4. package/dist/discovery-support.js +2 -2
  5. package/dist/resources.d.ts +1 -0
  6. package/dist/resources.js +210 -148
  7. package/dist/server-validation.js +2 -0
  8. package/dist/server.js +87 -1
  9. package/dist/tools/eval.js +29 -5
  10. package/dist/tools/execution-policy.d.ts +11 -0
  11. package/dist/tools/execution-policy.js +47 -0
  12. package/dist/tools/run-dossier.js +2 -2
  13. package/dist/tools/run-loop.d.ts +1 -0
  14. package/dist/tools/run-loop.js +15 -2
  15. package/dist/tools/tool-support.d.ts +3 -1
  16. package/dist/tools/tool-support.js +127 -19
  17. package/dist/tools/triage-runs.js +31 -6
  18. package/dist/vendor/adapters/claude-cli.js +100 -21
  19. package/dist/vendor/adapters/cli-bridge.d.ts +16 -0
  20. package/dist/vendor/adapters/cli-bridge.js +92 -2
  21. package/dist/vendor/adapters/codex-launcher.d.ts +2 -0
  22. package/dist/vendor/adapters/codex-launcher.js +124 -10
  23. package/dist/vendor/adapters/openai-compatible.js +6 -6
  24. package/dist/vendor/contracts/adapter-capabilities.d.ts +17 -0
  25. package/dist/vendor/contracts/adapter-capabilities.js +35 -0
  26. package/dist/vendor/contracts/context-graph.d.ts +48 -0
  27. package/dist/vendor/contracts/context-graph.js +26 -0
  28. package/dist/vendor/contracts/execution-policy.d.ts +78 -0
  29. package/dist/vendor/contracts/execution-policy.js +43 -0
  30. package/dist/vendor/contracts/governance.d.ts +6 -3
  31. package/dist/vendor/contracts/identity.d.ts +29 -0
  32. package/dist/vendor/contracts/identity.js +30 -0
  33. package/dist/vendor/contracts/index.d.ts +40 -2
  34. package/dist/vendor/contracts/index.js +5 -0
  35. package/dist/vendor/contracts/observation.d.ts +21 -0
  36. package/dist/vendor/contracts/observation.js +27 -0
  37. package/dist/vendor/contracts/trajectory.d.ts +26 -0
  38. package/dist/vendor/contracts/trajectory.js +27 -0
  39. package/dist/vendor/contracts/verification.d.ts +28 -0
  40. package/dist/vendor/contracts/verification.js +7 -0
  41. package/dist/vendor/core/context-graph.d.ts +3 -0
  42. package/dist/vendor/core/context-graph.js +371 -0
  43. package/dist/vendor/core/identity.d.ts +20 -0
  44. package/dist/vendor/core/identity.js +86 -0
  45. package/dist/vendor/core/index.d.ts +10 -1
  46. package/dist/vendor/core/index.js +131 -10
  47. package/dist/vendor/core/persistence/integrity.js +2 -2
  48. package/dist/vendor/core/policy-compiler.d.ts +2 -0
  49. package/dist/vendor/core/policy-compiler.js +195 -0
  50. package/dist/vendor/core/policy.d.ts +15 -0
  51. package/dist/vendor/core/policy.js +84 -4
  52. package/dist/vendor/core/rollback.js +32 -12
  53. package/dist/vendor/core/routing.d.ts +40 -0
  54. package/dist/vendor/core/routing.js +185 -0
  55. package/dist/vendor/core/trajectory.d.ts +18 -0
  56. package/dist/vendor/core/trajectory.js +295 -0
  57. package/dist/workflow-state.d.ts +16 -0
  58. package/dist/workflow-state.js +1 -1
  59. package/package.json +76 -75
  60. package/server.json +21 -21
@@ -0,0 +1,86 @@
1
+ import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
2
+ import { cloneIdentityAttestation, cloneIdentityClaims, cloneIdentityToken } from "../contracts/index.js";
3
+ export function createLocalIdentityAuthority(input = {}) {
4
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
5
+ return {
6
+ keyId: input.keyId ?? `martin-local-${Date.now().toString(36)}`,
7
+ algorithm: "ed25519",
8
+ privateKeyPem: privateKey.export({ format: "pem", type: "pkcs8" }).toString(),
9
+ publicKeyPem: publicKey.export({ format: "pem", type: "spki" }).toString()
10
+ };
11
+ }
12
+ export function issueIdentityToken(input) {
13
+ const claims = cloneIdentityClaims(input.claims);
14
+ const signer = createPrivateKey(input.authority.privateKeyPem);
15
+ const payload = serializeIdentityClaims(claims);
16
+ const signature = sign(null, payload, signer).toString("base64");
17
+ return cloneIdentityToken({
18
+ schemaVersion: "martin.identity.v1",
19
+ algorithm: input.authority.algorithm,
20
+ keyId: input.authority.keyId,
21
+ claims,
22
+ signature
23
+ });
24
+ }
25
+ export function verifyIdentityToken(input) {
26
+ const token = cloneIdentityToken(input.token);
27
+ const verifier = createPublicKey(input.publicKeyPem);
28
+ const payload = serializeIdentityClaims(token.claims);
29
+ const verifiedAt = input.now ?? new Date().toISOString();
30
+ if (token.schemaVersion !== "martin.identity.v1") {
31
+ return cloneIdentityAttestation({
32
+ valid: false,
33
+ verifiedAt,
34
+ reason: `Unsupported identity schema version '${token.schemaVersion}'.`,
35
+ token
36
+ });
37
+ }
38
+ const signatureValid = verify(null, payload, verifier, Buffer.from(token.signature, "base64"));
39
+ if (!signatureValid) {
40
+ return cloneIdentityAttestation({
41
+ valid: false,
42
+ verifiedAt,
43
+ reason: "Identity signature verification failed.",
44
+ token
45
+ });
46
+ }
47
+ const expiresAt = token.claims.expiresAt ? Date.parse(token.claims.expiresAt) : undefined;
48
+ if (expiresAt !== undefined && Number.isFinite(expiresAt) && expiresAt <= Date.parse(verifiedAt)) {
49
+ return cloneIdentityAttestation({
50
+ valid: false,
51
+ verifiedAt,
52
+ reason: "Identity token is expired.",
53
+ token
54
+ });
55
+ }
56
+ return cloneIdentityAttestation({
57
+ valid: true,
58
+ verifiedAt,
59
+ reason: `Identity token for role '${token.claims.role}' verified successfully.`,
60
+ token
61
+ });
62
+ }
63
+ export function hasIdentityScope(token, requiredScope) {
64
+ const required = requiredScope.trim().toLowerCase();
65
+ if (required.length === 0) {
66
+ return false;
67
+ }
68
+ return token.claims.scopes.some((scope) => scope.trim().toLowerCase() === required);
69
+ }
70
+ function serializeIdentityClaims(claims) {
71
+ return Buffer.from(JSON.stringify(claims, sortKeys), "utf8");
72
+ }
73
+ function sortKeys(_, value) {
74
+ if (Array.isArray(value)) {
75
+ return value;
76
+ }
77
+ if (value && typeof value === "object") {
78
+ return Object.keys(value)
79
+ .sort()
80
+ .reduce((sorted, key) => {
81
+ sorted[key] = value[key];
82
+ return sorted;
83
+ }, {});
84
+ }
85
+ return value;
86
+ }
@@ -4,11 +4,18 @@ import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLea
4
4
  import { buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations } from "./grounding.js";
5
5
  import { captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, restoreRollbackBoundary } from "./rollback.js";
6
6
  import { type RunStore } from "./persistence/index.js";
7
- export type { ApprovalPolicy, BudgetPreflightEstimate, BudgetSettlement, CostProvenance, EvidenceVector, ExecutionProfile, FailureClass, InterventionType, PatchDecision, PatchDecisionArtifact, PatchDecisionReasonCode, PatchScore, MutationMode, RollbackBoundaryArtifact, RollbackBoundaryStrategy, RollbackFileSnapshot, RollbackOutcomeArtifact, RollbackOutcomeStatus, PolicyPhase } from "../contracts/index.js";
7
+ export type { ApprovalPolicy, BudgetPreflightEstimate, BudgetSettlement, CostProvenance, EvidenceVector, ExecutionProfile, FailureClass, InterventionType, PatchDecision, PatchDecisionArtifact, PatchDecisionReasonCode, PatchScore, MutationMode, RollbackBoundaryArtifact, RollbackBoundaryStrategy, RollbackFileSnapshot, RollbackOutcomeArtifact, RollbackOutcomeStatus, PolicyPhase, CallStage, AgentRole, FirstDelta, RoutingEconomics } from "../contracts/index.js";
8
8
  export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, restoreRollbackBoundary };
9
9
  export type { BudgetPreflightDecision, BudgetPreflightInput, CostGovernorState, EvidenceVectorInput, EvaluatedPatchDecision, ExitDecision, FailureAssessment, PatchDecisionInput, RecoveryDecision, RecoveryRecipe } from "./policy.js";
10
10
  export type { ResolvedExecutionProfile, SafetyLeashDecision, SafetyViolation } from "./leash.js";
11
11
  export type { GroundingScanResult, GroundingViolation, GroundingViolationKind, RepoGroundingHit, RepoGroundingIndex } from "./grounding.js";
12
+ export { buildContextGraphSnapshot, queryContextGraph } from "./context-graph.js";
13
+ export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, verifyIdentityToken } from "./identity.js";
14
+ export type { LocalIdentityAuthority } from "./identity.js";
15
+ export { compileExecutionPolicy } from "./policy-compiler.js";
16
+ export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
17
+ export { classifyRoute, evaluatePreworkBurnPolicy } from "./routing.js";
18
+ export type { RouteDecision, RouteClassificationInput } from "./routing.js";
12
19
  export { runContextIntegrityPrecheck } from "./context-integrity.js";
13
20
  export type { ContextIntegrityPrecheck, ContextIntegrityVerdict } from "./context-integrity.js";
14
21
  export { compilePromptPacket } from "./compiler.js";
@@ -132,6 +139,8 @@ export interface AttemptPolicyDecision {
132
139
  allowed: boolean;
133
140
  reason: string;
134
141
  recommendedIntervention?: InterventionType;
142
+ trajectoryAssessment?: import("../contracts/index.js").TrajectoryAssessment;
143
+ circuitBreakDecision?: import("../contracts/index.js").CircuitBreakDecision;
135
144
  }
136
145
  /**
137
146
  * Admission gate — must pass before any attempt is executed.
@@ -6,7 +6,14 @@ import { captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, restoreR
6
6
  import { compilePromptPacket } from "./compiler.js";
7
7
  import { makeLedgerEvent, resolveRunsRoot, runDir } from "./persistence/index.js";
8
8
  import { runContextIntegrityPrecheck } from "./context-integrity.js";
9
+ import { decideCircuitBreak } from "./trajectory.js";
9
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 } from "./routing.js";
10
17
  // ─── Context Integrity Pre-gate ──────────────────────────────────────────────
11
18
  export { runContextIntegrityPrecheck } from "./context-integrity.js";
12
19
  // ─── Prompt packet compiler ──────────────────────────────────────────────────
@@ -21,12 +28,35 @@ export { compileAndPersistContext } from "./persistence/index.js";
21
28
  */
22
29
  export function evaluateAttemptPolicy(input) {
23
30
  const { request, projectedUsd } = input;
31
+ const trajectoryAttempts = request.previousAttempts.map((attempt) => ({
32
+ index: attempt.index,
33
+ summary: attempt.summary,
34
+ failureClass: attempt.failureClass
35
+ }));
36
+ const circuitBreakDecision = decideCircuitBreak({
37
+ objective: request.context.objective,
38
+ verificationPlan: request.context.verificationPlan,
39
+ attempts: trajectoryAttempts,
40
+ remainingIterations: request.context.remainingIterations
41
+ });
42
+ const trajectoryAssessment = circuitBreakDecision.assessment;
43
+ if (circuitBreakDecision.shouldStop) {
44
+ return {
45
+ allowed: false,
46
+ reason: circuitBreakDecision.reason,
47
+ recommendedIntervention: circuitBreakDecision.recommendedIntervention,
48
+ trajectoryAssessment,
49
+ circuitBreakDecision
50
+ };
51
+ }
24
52
  // Budget gate: reject if projected cost exceeds remaining
25
53
  if (projectedUsd > request.context.remainingBudgetUsd) {
26
54
  return {
27
55
  allowed: false,
28
56
  reason: `Projected cost $${projectedUsd} exceeds remaining budget $${request.context.remainingBudgetUsd}.`,
29
- recommendedIntervention: "stop_loop"
57
+ recommendedIntervention: "stop_loop",
58
+ trajectoryAssessment,
59
+ circuitBreakDecision
30
60
  };
31
61
  }
32
62
  // Iteration gate
@@ -34,7 +64,9 @@ export function evaluateAttemptPolicy(input) {
34
64
  return {
35
65
  allowed: false,
36
66
  reason: "No remaining iterations in the budget.",
37
- recommendedIntervention: "stop_loop"
67
+ recommendedIntervention: "stop_loop",
68
+ trajectoryAssessment,
69
+ circuitBreakDecision
38
70
  };
39
71
  }
40
72
  // Oscillation detection: A/B/A pattern in failure classes
@@ -48,7 +80,9 @@ export function evaluateAttemptPolicy(input) {
48
80
  return {
49
81
  allowed: false,
50
82
  reason: "Oscillating failure pattern detected. Escalating to human.",
51
- recommendedIntervention: "escalate_human"
83
+ recommendedIntervention: "escalate_human",
84
+ trajectoryAssessment,
85
+ circuitBreakDecision
52
86
  };
53
87
  }
54
88
  }
@@ -69,14 +103,18 @@ export function evaluateAttemptPolicy(input) {
69
103
  return {
70
104
  allowed: false,
71
105
  reason: "Materially repetitive attempts detected. Escalating to human.",
72
- recommendedIntervention: "escalate_human"
106
+ recommendedIntervention: "escalate_human",
107
+ trajectoryAssessment,
108
+ circuitBreakDecision
73
109
  };
74
110
  }
75
111
  }
76
112
  }
77
113
  return {
78
114
  allowed: true,
79
- reason: "Attempt admitted."
115
+ reason: "Attempt admitted.",
116
+ trajectoryAssessment,
117
+ circuitBreakDecision
80
118
  };
81
119
  }
82
120
  export function distillContext(loop, options = {}) {
@@ -226,6 +264,13 @@ export async function runMartin(input) {
226
264
  // Explicit PolicyPhase state machine — starts at GATHER, advances per attempt
227
265
  let currentPhase = "GATHER";
228
266
  let phaseRetryCount = 0;
267
+ // Routing economics: track first meaningful workspace delta
268
+ const runStartMs = Date.now();
269
+ let firstDeltaDetected = false;
270
+ let firstDeltaTimestampMs;
271
+ let firstDeltaFilePath;
272
+ let firstDeltaChangeType;
273
+ let preDeltaCostUsd = 0;
229
274
  while (loop.attempts.length < loop.budget.maxIterations) {
230
275
  const distilled = distillContext(loop, {
231
276
  maxRecentAttempts: useCompressedContext ? 1 : (input.maxRecentAttempts ?? 3)
@@ -452,7 +497,8 @@ export async function runMartin(input) {
452
497
  ...(failure?.failureClass ? { failureClass: failure.failureClass } : {}),
453
498
  ...(failure?.recommendedIntervention
454
499
  ? { intervention: failure.recommendedIntervention }
455
- : {})
500
+ : {}),
501
+ ...(failure?.diagnosticHint ? { diagnosticHint: failure.diagnosticHint } : {})
456
502
  };
457
503
  loop = {
458
504
  ...loop,
@@ -611,6 +657,20 @@ export async function runMartin(input) {
611
657
  const changedFiles = tracksWorkspaceMutations
612
658
  ? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary)
613
659
  : [];
660
+ // Routing economics: detect first meaningful workspace delta
661
+ if (!firstDeltaDetected && changedFiles.length > 0) {
662
+ const meaningfulFile = changedFiles.find((f) => !isMartinLoopMetadata(f));
663
+ if (meaningfulFile) {
664
+ firstDeltaDetected = true;
665
+ firstDeltaTimestampMs = Date.now();
666
+ firstDeltaFilePath = meaningfulFile;
667
+ firstDeltaChangeType = "modify";
668
+ preDeltaCostUsd = roundUsd(loop.cost.actualUsd - getUsageUsd(result.usage));
669
+ }
670
+ }
671
+ if (!firstDeltaDetected) {
672
+ preDeltaCostUsd = loop.cost.actualUsd;
673
+ }
614
674
  // Evidence is only reliable when the adapter explicitly reported files OR git actually
615
675
  // returned a non-empty list. A repoRoot alone is insufficient — git may fail (e.g. not
616
676
  // a git repo) and silently return [], which would falsely trigger no_code_change.
@@ -1033,7 +1093,8 @@ export async function runMartin(input) {
1033
1093
  payload: createRunExitPayload(decision)
1034
1094
  }));
1035
1095
  }
1036
- const finalizedLoop = finalizeLoop(loop, decision, now(), idFactory);
1096
+ const routingInput = { runStartMs, firstDeltaDetected, firstDeltaTimestampMs, firstDeltaFilePath, firstDeltaChangeType, preDeltaCostUsd };
1097
+ const finalizedLoop = finalizeLoop(loop, decision, now(), idFactory, routingInput);
1037
1098
  await persistLoopRecordIfSupported(input.store, finalizedLoop);
1038
1099
  return {
1039
1100
  loop: finalizedLoop,
@@ -1054,7 +1115,8 @@ export async function runMartin(input) {
1054
1115
  payload: createRunExitPayload(decision)
1055
1116
  }));
1056
1117
  }
1057
- const finalizedLoop = finalizeLoop(loop, decision, now(), idFactory);
1118
+ const routingInput = { runStartMs, firstDeltaDetected, firstDeltaTimestampMs, firstDeltaFilePath, firstDeltaChangeType, preDeltaCostUsd };
1119
+ const finalizedLoop = finalizeLoop(loop, decision, now(), idFactory, routingInput);
1058
1120
  await persistLoopRecordIfSupported(input.store, finalizedLoop);
1059
1121
  return {
1060
1122
  loop: finalizedLoop,
@@ -1104,19 +1166,78 @@ function safetyLeashReasonCode(decision, safetySurface) {
1104
1166
  return `${safetySurface}_safety_block`;
1105
1167
  }
1106
1168
  }
1107
- function finalizeLoop(loop, decision, timestamp, idFactory) {
1169
+ function finalizeLoop(loop, decision, timestamp, idFactory, routingEconomicsInput) {
1108
1170
  const finalized = appendLoopEvent(loop, {
1109
1171
  type: "run.completed",
1110
1172
  lifecycleState: decision.lifecycleState,
1111
1173
  payload: createRunExitPayload(decision)
1112
1174
  }, { now: timestamp, idFactory });
1175
+ const routingEconomics = routingEconomicsInput
1176
+ ? buildRoutingEconomics(finalized, routingEconomicsInput, decision)
1177
+ : undefined;
1113
1178
  return {
1114
1179
  ...finalized,
1115
1180
  status: decision.status,
1116
1181
  lifecycleState: decision.lifecycleState,
1117
- updatedAt: timestamp
1182
+ updatedAt: timestamp,
1183
+ ...(routingEconomics ? { routingEconomics } : {})
1118
1184
  };
1119
1185
  }
1186
+ function buildRoutingEconomics(loop, input, decision) {
1187
+ const totalCost = loop.cost.actualUsd;
1188
+ const preworkCost = roundUsd(Math.max(input.preDeltaCostUsd, 0));
1189
+ const executionCost = roundUsd(Math.max(totalCost - preworkCost, 0));
1190
+ const preworkBurnPct = totalCost > 0 ? Math.round((preworkCost / totalCost) * 100) : 0;
1191
+ const timeToFirstDeltaMs = input.firstDeltaTimestampMs
1192
+ ? input.firstDeltaTimestampMs - input.runStartMs
1193
+ : undefined;
1194
+ const accepted = decision.lifecycleState === "completed";
1195
+ // Route recommendation based on prework burn
1196
+ let routeRecommendation;
1197
+ let routeRecommendationReason;
1198
+ if (preworkBurnPct > 50) {
1199
+ routeRecommendation = "direct_worker";
1200
+ routeRecommendationReason = `${String(preworkBurnPct)}% of spend was pre-work coordination. Use direct execution for similar tasks.`;
1201
+ }
1202
+ else if (preworkBurnPct > 30) {
1203
+ routeRecommendation = "direct_worker";
1204
+ routeRecommendationReason = `${String(preworkBurnPct)}% pre-work burn is high. Consider direct execution.`;
1205
+ }
1206
+ else {
1207
+ routeRecommendation = "same_route";
1208
+ routeRecommendationReason = `Pre-work burn at ${String(preworkBurnPct)}% is within acceptable range.`;
1209
+ }
1210
+ return {
1211
+ preworkCostUsd: preworkCost,
1212
+ executionCostUsd: executionCost,
1213
+ verificationCostUsd: 0,
1214
+ retryCostUsd: 0,
1215
+ totalCostUsd: totalCost,
1216
+ preworkBurnPct,
1217
+ timeToFirstDeltaMs,
1218
+ firstDelta: {
1219
+ detected: input.firstDeltaDetected,
1220
+ ...(input.firstDeltaTimestampMs ? { timestampMs: input.firstDeltaTimestampMs } : {}),
1221
+ ...(input.firstDeltaFilePath ? { filePath: input.firstDeltaFilePath } : {}),
1222
+ ...(input.firstDeltaChangeType ? { changeType: input.firstDeltaChangeType } : {}),
1223
+ ...(timeToFirstDeltaMs !== undefined ? { timeToFirstDeltaMs } : {})
1224
+ },
1225
+ ...(accepted ? { costPerAcceptedChange: totalCost } : {}),
1226
+ routeRecommendation,
1227
+ routeRecommendationReason
1228
+ };
1229
+ }
1230
+ const MARTINLOOP_METADATA_PATTERNS = [
1231
+ /^PROGRESS\.md$/u,
1232
+ /^\.martin\//u,
1233
+ /\.lock$/u,
1234
+ /^node_modules\//u,
1235
+ /^\.git\//u,
1236
+ /^\.cache\//u,
1237
+ ];
1238
+ function isMartinLoopMetadata(filePath) {
1239
+ return MARTINLOOP_METADATA_PATTERNS.some((p) => p.test(filePath));
1240
+ }
1120
1241
  async function persistLoopRecordIfSupported(store, loop) {
1121
1242
  await store?.writeLoopRecord?.(loop.loopId, loop);
1122
1243
  }
@@ -49,9 +49,9 @@ export async function verifyReceiptIntegrityFromFiles(input) {
49
49
  ]);
50
50
  if (!rawMaterial || !rawLoopRecord || rawLedger === null || !key) {
51
51
  return {
52
- state: "unsigned",
52
+ state: "material_missing",
53
53
  reason: "Receipt integrity material is missing for this run.",
54
- warnings: ["Receipts are local-only and unsigned; trust claims are unavailable."]
54
+ warnings: ["Receipt integrity material is incomplete; trust claims are unavailable."]
55
55
  };
56
56
  }
57
57
  let material;
@@ -0,0 +1,2 @@
1
+ import { type ExecutionPolicy, type ExecutionPolicyCompileInput } from "../contracts/index.js";
2
+ export declare function compileExecutionPolicy(input: ExecutionPolicyCompileInput): ExecutionPolicy;
@@ -0,0 +1,195 @@
1
+ import { cloneExecutionPolicy } from "../contracts/index.js";
2
+ export function compileExecutionPolicy(input) {
3
+ const provenance = [];
4
+ const budget = {
5
+ maxUsd: resolveBudgetField("maxUsd", input, provenance),
6
+ softLimitUsd: resolveBudgetField("softLimitUsd", input, provenance),
7
+ maxIterations: resolveBudgetField("maxIterations", input, provenance),
8
+ maxTokens: resolveBudgetField("maxTokens", input, provenance)
9
+ };
10
+ if (budget.softLimitUsd >= budget.maxUsd) {
11
+ budget.softLimitUsd = Math.round(budget.maxUsd * 0.75 * 100) / 100;
12
+ provenance.push({
13
+ field: "budget.softLimitUsd",
14
+ source: "config",
15
+ value: String(budget.softLimitUsd)
16
+ });
17
+ }
18
+ const governance = {
19
+ policyProfile: resolveEnumField({
20
+ field: "governance.policyProfile",
21
+ defaultsTo: input.defaults.policyProfile,
22
+ configValue: input.config?.policyProfile,
23
+ requestValue: input.request.policyProfile,
24
+ allowed: ["strict", "balanced", "overnight", "debug"],
25
+ provenance
26
+ }),
27
+ telemetryDestination: resolveEnumField({
28
+ field: "governance.telemetryDestination",
29
+ defaultsTo: input.defaults.telemetryDestination,
30
+ configValue: input.config?.governance?.telemetryDestination,
31
+ requestValue: input.request.telemetryDestination,
32
+ allowed: ["local-only", "control-plane"],
33
+ provenance
34
+ }),
35
+ destructiveActionPolicy: resolveEnumField({
36
+ field: "governance.destructiveActionPolicy",
37
+ defaultsTo: input.defaults.destructiveActionPolicy,
38
+ configValue: input.config?.governance?.destructiveActionPolicy,
39
+ allowed: ["never", "approval", "allowed"],
40
+ provenance
41
+ })
42
+ };
43
+ const verificationPlan = resolveVerificationPlan(input, provenance);
44
+ const task = {
45
+ verificationPlan
46
+ };
47
+ if (input.request.mutationMode) {
48
+ task.mutationMode = input.request.mutationMode;
49
+ provenance.push({
50
+ field: "task.mutationMode",
51
+ source: "request",
52
+ value: input.request.mutationMode
53
+ });
54
+ }
55
+ if (input.request.repoRoot) {
56
+ task.repoRoot = input.request.repoRoot;
57
+ provenance.push({
58
+ field: "task.repoRoot",
59
+ source: "request",
60
+ value: input.request.repoRoot
61
+ });
62
+ }
63
+ if ((input.request.allowedPaths?.length ?? 0) > 0) {
64
+ const allowedPaths = input.request.allowedPaths ?? [];
65
+ task.allowedPaths = [...allowedPaths];
66
+ provenance.push({
67
+ field: "task.allowedPaths",
68
+ source: "request",
69
+ value: allowedPaths.join(", ")
70
+ });
71
+ }
72
+ if ((input.request.deniedPaths?.length ?? 0) > 0) {
73
+ const deniedPaths = input.request.deniedPaths ?? [];
74
+ task.deniedPaths = [...deniedPaths];
75
+ provenance.push({
76
+ field: "task.deniedPaths",
77
+ source: "request",
78
+ value: deniedPaths.join(", ")
79
+ });
80
+ }
81
+ if ((input.request.acceptanceCriteria?.length ?? 0) > 0) {
82
+ const acceptanceCriteria = input.request.acceptanceCriteria ?? [];
83
+ task.acceptanceCriteria = [...acceptanceCriteria];
84
+ provenance.push({
85
+ field: "task.acceptanceCriteria",
86
+ source: "request",
87
+ value: acceptanceCriteria.join(", ")
88
+ });
89
+ }
90
+ if (input.request.approvalPolicy) {
91
+ task.approvalPolicy = { ...input.request.approvalPolicy };
92
+ provenance.push({
93
+ field: "task.approvalPolicy",
94
+ source: "request",
95
+ value: JSON.stringify(input.request.approvalPolicy)
96
+ });
97
+ }
98
+ return cloneExecutionPolicy({
99
+ configPath: input.configPath,
100
+ budget,
101
+ governance,
102
+ task,
103
+ provenance
104
+ });
105
+ }
106
+ function resolveBudgetField(field, input, provenance) {
107
+ if (input.request.budgetOverrides?.[field]) {
108
+ const value = input.request.budget[field];
109
+ provenance.push({
110
+ field: `budget.${String(field)}`,
111
+ source: "request",
112
+ value: String(value)
113
+ });
114
+ return value;
115
+ }
116
+ const configValue = input.config?.budget?.[field];
117
+ if (typeof configValue === "number" && Number.isFinite(configValue)) {
118
+ provenance.push({
119
+ field: `budget.${String(field)}`,
120
+ source: "config",
121
+ value: String(configValue)
122
+ });
123
+ return configValue;
124
+ }
125
+ const value = input.defaults.budget[field];
126
+ provenance.push({
127
+ field: `budget.${String(field)}`,
128
+ source: "default",
129
+ value: String(value)
130
+ });
131
+ return value;
132
+ }
133
+ function resolveVerificationPlan(input, provenance) {
134
+ const requestVerificationPlan = normalizeStringList(input.request.verificationPlan);
135
+ if (requestVerificationPlan.length > 0) {
136
+ provenance.push({
137
+ field: "task.verificationPlan",
138
+ source: "request",
139
+ value: requestVerificationPlan.join(", ")
140
+ });
141
+ return requestVerificationPlan;
142
+ }
143
+ const configVerificationPlan = normalizeStringList(input.config?.governance?.verifierRules);
144
+ if (configVerificationPlan.length > 0) {
145
+ provenance.push({
146
+ field: "task.verificationPlan",
147
+ source: "config",
148
+ value: configVerificationPlan.join(", ")
149
+ });
150
+ return configVerificationPlan;
151
+ }
152
+ const defaultVerificationPlan = normalizeStringList(input.defaults.verifierRules);
153
+ provenance.push({
154
+ field: "task.verificationPlan",
155
+ source: "default",
156
+ value: defaultVerificationPlan.join(", ")
157
+ });
158
+ return defaultVerificationPlan;
159
+ }
160
+ function resolveEnumField(input) {
161
+ const normalizedRequest = normalizeEnumCandidate(input.requestValue, input.allowed);
162
+ if (normalizedRequest) {
163
+ input.provenance.push({
164
+ field: input.field,
165
+ source: "request",
166
+ value: normalizedRequest
167
+ });
168
+ return normalizedRequest;
169
+ }
170
+ const normalizedConfig = normalizeEnumCandidate(input.configValue, input.allowed);
171
+ if (normalizedConfig) {
172
+ input.provenance.push({
173
+ field: input.field,
174
+ source: "config",
175
+ value: normalizedConfig
176
+ });
177
+ return normalizedConfig;
178
+ }
179
+ input.provenance.push({
180
+ field: input.field,
181
+ source: "default",
182
+ value: input.defaultsTo
183
+ });
184
+ return input.defaultsTo;
185
+ }
186
+ function normalizeEnumCandidate(value, allowed) {
187
+ if (typeof value !== "string") {
188
+ return undefined;
189
+ }
190
+ const trimmed = value.trim();
191
+ return allowed.find((candidate) => candidate === trimmed);
192
+ }
193
+ function normalizeStringList(values) {
194
+ return (values ?? []).map((value) => value.trim()).filter(Boolean);
195
+ }
@@ -4,6 +4,8 @@ export interface FailureAssessment {
4
4
  rationale: string;
5
5
  retryable: boolean;
6
6
  recommendedIntervention: InterventionType;
7
+ /** Actionable diagnosis injected into the next attempt's prompt context. */
8
+ diagnosticHint?: string;
7
9
  }
8
10
  export interface CostGovernorState {
9
11
  pressure: "healthy" | "soft_limit" | "hard_limit";
@@ -72,6 +74,19 @@ export declare function evaluateCostGovernor(input: {
72
74
  cost: LoopCost;
73
75
  attemptsUsed: number;
74
76
  }): CostGovernorState;
77
+ export interface CostPerOutcome {
78
+ costPerAcceptedChange: number | undefined;
79
+ costPerAttempt: number;
80
+ acceptanceRate: number;
81
+ wastedCoordinationUsd: number;
82
+ }
83
+ export declare function calculateCostPerOutcome(input: {
84
+ totalCostUsd: number;
85
+ preworkCostUsd: number;
86
+ attemptCount: number;
87
+ accepted: boolean;
88
+ verificationPassed: boolean;
89
+ }): CostPerOutcome;
75
90
  export declare function inferExit(input: {
76
91
  loop: {
77
92
  budget: LoopBudget;