@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.
- package/README.md +112 -107
- package/dist/discovery-metadata.d.ts +6 -6
- package/dist/discovery-metadata.js +6 -0
- package/dist/discovery-support.js +2 -2
- package/dist/resources.d.ts +1 -0
- package/dist/resources.js +210 -148
- package/dist/server-validation.js +2 -0
- package/dist/server.js +87 -1
- package/dist/tools/eval.js +29 -5
- package/dist/tools/execution-policy.d.ts +11 -0
- package/dist/tools/execution-policy.js +47 -0
- package/dist/tools/run-dossier.js +2 -2
- package/dist/tools/run-loop.d.ts +1 -0
- package/dist/tools/run-loop.js +15 -2
- package/dist/tools/tool-support.d.ts +3 -1
- package/dist/tools/tool-support.js +127 -19
- package/dist/tools/triage-runs.js +31 -6
- package/dist/vendor/adapters/claude-cli.js +100 -21
- package/dist/vendor/adapters/cli-bridge.d.ts +16 -0
- package/dist/vendor/adapters/cli-bridge.js +92 -2
- package/dist/vendor/adapters/codex-launcher.d.ts +2 -0
- package/dist/vendor/adapters/codex-launcher.js +124 -10
- package/dist/vendor/adapters/openai-compatible.js +6 -6
- package/dist/vendor/contracts/adapter-capabilities.d.ts +17 -0
- package/dist/vendor/contracts/adapter-capabilities.js +35 -0
- package/dist/vendor/contracts/context-graph.d.ts +48 -0
- package/dist/vendor/contracts/context-graph.js +26 -0
- package/dist/vendor/contracts/execution-policy.d.ts +78 -0
- package/dist/vendor/contracts/execution-policy.js +43 -0
- package/dist/vendor/contracts/governance.d.ts +6 -3
- package/dist/vendor/contracts/identity.d.ts +29 -0
- package/dist/vendor/contracts/identity.js +30 -0
- package/dist/vendor/contracts/index.d.ts +40 -2
- package/dist/vendor/contracts/index.js +5 -0
- package/dist/vendor/contracts/observation.d.ts +21 -0
- package/dist/vendor/contracts/observation.js +27 -0
- package/dist/vendor/contracts/trajectory.d.ts +26 -0
- package/dist/vendor/contracts/trajectory.js +27 -0
- package/dist/vendor/contracts/verification.d.ts +28 -0
- package/dist/vendor/contracts/verification.js +7 -0
- package/dist/vendor/core/context-graph.d.ts +3 -0
- package/dist/vendor/core/context-graph.js +371 -0
- package/dist/vendor/core/identity.d.ts +20 -0
- package/dist/vendor/core/identity.js +86 -0
- package/dist/vendor/core/index.d.ts +10 -1
- package/dist/vendor/core/index.js +131 -10
- package/dist/vendor/core/persistence/integrity.js +2 -2
- package/dist/vendor/core/policy-compiler.d.ts +2 -0
- package/dist/vendor/core/policy-compiler.js +195 -0
- package/dist/vendor/core/policy.d.ts +15 -0
- package/dist/vendor/core/policy.js +84 -4
- package/dist/vendor/core/rollback.js +32 -12
- package/dist/vendor/core/routing.d.ts +40 -0
- package/dist/vendor/core/routing.js +185 -0
- package/dist/vendor/core/trajectory.d.ts +18 -0
- package/dist/vendor/core/trajectory.js +295 -0
- package/dist/workflow-state.d.ts +16 -0
- package/dist/workflow-state.js +1 -1
- package/package.json +76 -75
- 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
|
|
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
|
|
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: "
|
|
52
|
+
state: "material_missing",
|
|
53
53
|
reason: "Receipt integrity material is missing for this run.",
|
|
54
|
-
warnings: ["
|
|
54
|
+
warnings: ["Receipt integrity material is incomplete; trust claims are unavailable."]
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
let material;
|
|
@@ -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;
|