@martinloop/mcp 0.3.4 → 0.3.6
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 +10 -2
- package/dist/discovery-metadata.d.ts +6 -6
- package/dist/discovery-metadata.js +8 -0
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/prompts.js +19 -1
- package/dist/resources.d.ts +3 -0
- package/dist/resources.js +264 -148
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +17 -0
- package/dist/server.d.ts +23 -0
- package/dist/server.js +244 -2
- package/dist/tools/doctor.js +6 -0
- package/dist/tools/run-store.js +4 -4
- package/dist/tools/tool-support.d.ts +3 -1
- package/dist/tools/tool-support.js +17 -2
- package/dist/vendor/adapters/claude-cli.d.ts +2 -2
- package/dist/vendor/adapters/claude-cli.js +53 -9
- package/dist/vendor/adapters/cli-bridge.d.ts +17 -0
- package/dist/vendor/adapters/cli-bridge.js +124 -8
- package/dist/vendor/adapters/codex-launcher.d.ts +2 -0
- package/dist/vendor/adapters/codex-launcher.js +77 -9
- package/dist/vendor/adapters/openai-compatible.js +90 -57
- package/dist/vendor/contracts/execution-policy.d.ts +16 -0
- package/dist/vendor/contracts/execution-policy.js +9 -0
- package/dist/vendor/contracts/index.d.ts +29 -2
- package/dist/vendor/contracts/index.js +2 -0
- package/dist/vendor/core/index.d.ts +13 -3
- package/dist/vendor/core/index.js +123 -30
- package/dist/vendor/core/persistence/memory-store.d.ts +63 -0
- package/dist/vendor/core/persistence/memory-store.js +115 -0
- package/dist/vendor/core/persistence/store.js +13 -0
- package/dist/vendor/core/persistence/trace-store.d.ts +85 -0
- package/dist/vendor/core/persistence/trace-store.js +125 -0
- package/dist/vendor/core/policy.d.ts +13 -0
- package/dist/vendor/core/policy.js +13 -0
- package/dist/vendor/core/repo-analyzer.d.ts +53 -0
- package/dist/vendor/core/repo-analyzer.js +263 -0
- package/dist/vendor/core/rollback.d.ts +4 -0
- package/dist/vendor/core/rollback.js +15 -0
- package/dist/vendor/core/routing.d.ts +82 -0
- package/dist/vendor/core/routing.js +336 -0
- package/dist/vendor/core/test-integrity.d.ts +64 -0
- package/dist/vendor/core/test-integrity.js +197 -0
- package/dist/workflow-state.d.ts +17 -1
- package/dist/workflow-state.js +13 -5
- package/package.json +76 -76
- package/server.json +21 -21
|
@@ -2,10 +2,10 @@ import { type ApprovalPolicy, type CostProvenance, type ExecutionProfile, type F
|
|
|
2
2
|
import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, type ExitDecision } from "./policy.js";
|
|
3
3
|
import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
|
|
4
4
|
import { buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations } from "./grounding.js";
|
|
5
|
-
import { captureRollbackBoundary, restoreRollbackBoundary } from "./rollback.js";
|
|
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";
|
|
8
|
-
export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, restoreRollbackBoundary };
|
|
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
|
+
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";
|
|
@@ -14,6 +14,12 @@ export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, ver
|
|
|
14
14
|
export type { LocalIdentityAuthority } from "./identity.js";
|
|
15
15
|
export { compileExecutionPolicy } from "./policy-compiler.js";
|
|
16
16
|
export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
|
|
17
|
+
export { classifyRoute, evaluatePreworkBurnPolicy, resolveModelForTier, selectBestEngine } from "./routing.js";
|
|
18
|
+
export type { RouteDecision, RouteClassificationInput, AvailableEngine } from "./routing.js";
|
|
19
|
+
export { validateTestIntegrity, snapshotTestFiles, identifyNewTests, findTestFiles, isTestFile, extractImports, hasTrivialAssertionsOnly, importsPreExistingCode } from "./test-integrity.js";
|
|
20
|
+
export type { TestIntegrityReport, TestIntegrityFinding } from "./test-integrity.js";
|
|
21
|
+
export { analyzeRepoStyle, buildPromptDirectives } from "./repo-analyzer.js";
|
|
22
|
+
export type { RepoStyleProfile } from "./repo-analyzer.js";
|
|
17
23
|
export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
18
24
|
export type { ContextIntegrityPrecheck, ContextIntegrityVerdict } from "./context-integrity.js";
|
|
19
25
|
export { compilePromptPacket } from "./compiler.js";
|
|
@@ -22,6 +28,10 @@ export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoop
|
|
|
22
28
|
export type { AttemptArtifacts, LedgerEvent, LedgerEventKind, LoopAttemptRecord, LoopRunRecord, ReceiptIntegrityChainEntry, RunContract, RunStore, StoredReceiptIntegrityMaterial } from "./persistence/index.js";
|
|
23
29
|
export { compileAndPersistContext } from "./persistence/index.js";
|
|
24
30
|
export type { CompileResult } from "./persistence/index.js";
|
|
31
|
+
export { appendMemory, readMemoryEntries, getPreference, buildMemorySummary, recordPreference, recordConsent } from "./persistence/memory-store.js";
|
|
32
|
+
export type { MemoryEntry, MemoryKind, MemorySummary } from "./persistence/memory-store.js";
|
|
33
|
+
export { appendTraceEntry, readTraceEntries, aggregateTraces, getHistoricalDirectSuccessRate } from "./persistence/trace-store.js";
|
|
34
|
+
export type { TraceEntry, TraceAggregation } from "./persistence/trace-store.js";
|
|
25
35
|
export interface MartinAdapterRequest {
|
|
26
36
|
loopId: string;
|
|
27
37
|
attemptId: string;
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { appendLoopEvent, createLoopRecord } from "../contracts/index.js";
|
|
3
2
|
import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe } from "./policy.js";
|
|
4
3
|
import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
|
|
5
4
|
import { buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations } from "./grounding.js";
|
|
6
|
-
import { captureRollbackBoundary, restoreRollbackBoundary } from "./rollback.js";
|
|
5
|
+
import { captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, restoreRollbackBoundary } from "./rollback.js";
|
|
7
6
|
import { compilePromptPacket } from "./compiler.js";
|
|
8
7
|
import { makeLedgerEvent, resolveRunsRoot, runDir } from "./persistence/index.js";
|
|
9
8
|
import { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
10
9
|
import { decideCircuitBreak } from "./trajectory.js";
|
|
11
|
-
export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, restoreRollbackBoundary };
|
|
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 };
|
|
12
11
|
export { buildContextGraphSnapshot, queryContextGraph } from "./context-graph.js";
|
|
13
12
|
export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, verifyIdentityToken } from "./identity.js";
|
|
14
13
|
export { compileExecutionPolicy } from "./policy-compiler.js";
|
|
15
14
|
export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
|
|
15
|
+
// ─── Routing economics ─────────────────────────────────────────────────────
|
|
16
|
+
export { classifyRoute, evaluatePreworkBurnPolicy, resolveModelForTier, selectBestEngine } from "./routing.js";
|
|
17
|
+
// ─── Test Integrity Validation ──────────────────────────────────────────────
|
|
18
|
+
export { validateTestIntegrity, snapshotTestFiles, identifyNewTests, findTestFiles, isTestFile, extractImports, hasTrivialAssertionsOnly, importsPreExistingCode } from "./test-integrity.js";
|
|
19
|
+
// ─── Repo Style Analyzer ────────────────────────────────────────────────────
|
|
20
|
+
export { analyzeRepoStyle, buildPromptDirectives } from "./repo-analyzer.js";
|
|
16
21
|
// ─── Context Integrity Pre-gate ──────────────────────────────────────────────
|
|
17
22
|
export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
18
23
|
// ─── Prompt packet compiler ──────────────────────────────────────────────────
|
|
@@ -20,6 +25,10 @@ export { compilePromptPacket } from "./compiler.js";
|
|
|
20
25
|
// ─── Persistence (RunStore, LedgerEvent, FileRunStore) ──────────────────────
|
|
21
26
|
export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
|
|
22
27
|
export { compileAndPersistContext } from "./persistence/index.js";
|
|
28
|
+
// ─── MartinLoop Memory Store ────────────────────────────────────────────────
|
|
29
|
+
export { appendMemory, readMemoryEntries, getPreference, buildMemorySummary, recordPreference, recordConsent } from "./persistence/memory-store.js";
|
|
30
|
+
// ─── Trace Intelligence Store ───────────────────────────────────────────────
|
|
31
|
+
export { appendTraceEntry, readTraceEntries, aggregateTraces, getHistoricalDirectSuccessRate } from "./persistence/trace-store.js";
|
|
23
32
|
/**
|
|
24
33
|
* Admission gate — must pass before any attempt is executed.
|
|
25
34
|
* Evaluates budget headroom, oscillation, and repetitive failure patterns.
|
|
@@ -263,6 +272,13 @@ export async function runMartin(input) {
|
|
|
263
272
|
// Explicit PolicyPhase state machine — starts at GATHER, advances per attempt
|
|
264
273
|
let currentPhase = "GATHER";
|
|
265
274
|
let phaseRetryCount = 0;
|
|
275
|
+
// Routing economics: track first meaningful workspace delta
|
|
276
|
+
const runStartMs = Date.now();
|
|
277
|
+
let firstDeltaDetected = false;
|
|
278
|
+
let firstDeltaTimestampMs;
|
|
279
|
+
let firstDeltaFilePath;
|
|
280
|
+
let firstDeltaChangeType;
|
|
281
|
+
let preDeltaCostUsd = 0;
|
|
266
282
|
while (loop.attempts.length < loop.budget.maxIterations) {
|
|
267
283
|
const distilled = distillContext(loop, {
|
|
268
284
|
maxRecentAttempts: useCompressedContext ? 1 : (input.maxRecentAttempts ?? 3)
|
|
@@ -647,8 +663,22 @@ export async function runMartin(input) {
|
|
|
647
663
|
}));
|
|
648
664
|
}
|
|
649
665
|
const changedFiles = tracksWorkspaceMutations
|
|
650
|
-
? resolveChangedFiles(result, request.context.repoRoot)
|
|
666
|
+
? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary)
|
|
651
667
|
: [];
|
|
668
|
+
// Routing economics: detect first meaningful workspace delta
|
|
669
|
+
if (!firstDeltaDetected && changedFiles.length > 0) {
|
|
670
|
+
const meaningfulFile = changedFiles.find((f) => !isMartinLoopMetadata(f));
|
|
671
|
+
if (meaningfulFile) {
|
|
672
|
+
firstDeltaDetected = true;
|
|
673
|
+
firstDeltaTimestampMs = Date.now();
|
|
674
|
+
firstDeltaFilePath = meaningfulFile;
|
|
675
|
+
firstDeltaChangeType = "modify";
|
|
676
|
+
preDeltaCostUsd = roundUsd(loop.cost.actualUsd - getUsageUsd(result.usage));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (!firstDeltaDetected) {
|
|
680
|
+
preDeltaCostUsd = loop.cost.actualUsd;
|
|
681
|
+
}
|
|
652
682
|
// Evidence is only reliable when the adapter explicitly reported files OR git actually
|
|
653
683
|
// returned a non-empty list. A repoRoot alone is insufficient — git may fail (e.g. not
|
|
654
684
|
// a git repo) and silently return [], which would falsely trigger no_code_change.
|
|
@@ -987,7 +1017,7 @@ export async function runMartin(input) {
|
|
|
987
1017
|
}
|
|
988
1018
|
}
|
|
989
1019
|
if (patchDecision && patchDecision.decision !== "KEEP" && !failure) {
|
|
990
|
-
failure = classifyPatchDecisionFailure(patchDecision);
|
|
1020
|
+
failure = classifyPatchDecisionFailure(patchDecision, result);
|
|
991
1021
|
loop = applyPatchFailureToLoop(loop, {
|
|
992
1022
|
attemptId,
|
|
993
1023
|
summary: patchDecision.summary,
|
|
@@ -1071,7 +1101,8 @@ export async function runMartin(input) {
|
|
|
1071
1101
|
payload: createRunExitPayload(decision)
|
|
1072
1102
|
}));
|
|
1073
1103
|
}
|
|
1074
|
-
const
|
|
1104
|
+
const routingInput = { runStartMs, firstDeltaDetected, firstDeltaTimestampMs, firstDeltaFilePath, firstDeltaChangeType, preDeltaCostUsd };
|
|
1105
|
+
const finalizedLoop = finalizeLoop(loop, decision, now(), idFactory, routingInput);
|
|
1075
1106
|
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
1076
1107
|
return {
|
|
1077
1108
|
loop: finalizedLoop,
|
|
@@ -1092,7 +1123,8 @@ export async function runMartin(input) {
|
|
|
1092
1123
|
payload: createRunExitPayload(decision)
|
|
1093
1124
|
}));
|
|
1094
1125
|
}
|
|
1095
|
-
const
|
|
1126
|
+
const routingInput = { runStartMs, firstDeltaDetected, firstDeltaTimestampMs, firstDeltaFilePath, firstDeltaChangeType, preDeltaCostUsd };
|
|
1127
|
+
const finalizedLoop = finalizeLoop(loop, decision, now(), idFactory, routingInput);
|
|
1096
1128
|
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
1097
1129
|
return {
|
|
1098
1130
|
loop: finalizedLoop,
|
|
@@ -1142,19 +1174,78 @@ function safetyLeashReasonCode(decision, safetySurface) {
|
|
|
1142
1174
|
return `${safetySurface}_safety_block`;
|
|
1143
1175
|
}
|
|
1144
1176
|
}
|
|
1145
|
-
function finalizeLoop(loop, decision, timestamp, idFactory) {
|
|
1177
|
+
function finalizeLoop(loop, decision, timestamp, idFactory, routingEconomicsInput) {
|
|
1146
1178
|
const finalized = appendLoopEvent(loop, {
|
|
1147
1179
|
type: "run.completed",
|
|
1148
1180
|
lifecycleState: decision.lifecycleState,
|
|
1149
1181
|
payload: createRunExitPayload(decision)
|
|
1150
1182
|
}, { now: timestamp, idFactory });
|
|
1183
|
+
const routingEconomics = routingEconomicsInput
|
|
1184
|
+
? buildRoutingEconomics(finalized, routingEconomicsInput, decision)
|
|
1185
|
+
: undefined;
|
|
1151
1186
|
return {
|
|
1152
1187
|
...finalized,
|
|
1153
1188
|
status: decision.status,
|
|
1154
1189
|
lifecycleState: decision.lifecycleState,
|
|
1155
|
-
updatedAt: timestamp
|
|
1190
|
+
updatedAt: timestamp,
|
|
1191
|
+
...(routingEconomics ? { routingEconomics } : {})
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
function buildRoutingEconomics(loop, input, decision) {
|
|
1195
|
+
const totalCost = loop.cost.actualUsd;
|
|
1196
|
+
const preworkCost = roundUsd(Math.max(input.preDeltaCostUsd, 0));
|
|
1197
|
+
const executionCost = roundUsd(Math.max(totalCost - preworkCost, 0));
|
|
1198
|
+
const preworkBurnPct = totalCost > 0 ? Math.round((preworkCost / totalCost) * 100) : 0;
|
|
1199
|
+
const timeToFirstDeltaMs = input.firstDeltaTimestampMs
|
|
1200
|
+
? input.firstDeltaTimestampMs - input.runStartMs
|
|
1201
|
+
: undefined;
|
|
1202
|
+
const accepted = decision.lifecycleState === "completed";
|
|
1203
|
+
// Route recommendation based on prework burn
|
|
1204
|
+
let routeRecommendation;
|
|
1205
|
+
let routeRecommendationReason;
|
|
1206
|
+
if (preworkBurnPct > 50) {
|
|
1207
|
+
routeRecommendation = "direct_worker";
|
|
1208
|
+
routeRecommendationReason = `${String(preworkBurnPct)}% of spend was pre-work coordination. Use direct execution for similar tasks.`;
|
|
1209
|
+
}
|
|
1210
|
+
else if (preworkBurnPct > 30) {
|
|
1211
|
+
routeRecommendation = "direct_worker";
|
|
1212
|
+
routeRecommendationReason = `${String(preworkBurnPct)}% pre-work burn is high. Consider direct execution.`;
|
|
1213
|
+
}
|
|
1214
|
+
else {
|
|
1215
|
+
routeRecommendation = "same_route";
|
|
1216
|
+
routeRecommendationReason = `Pre-work burn at ${String(preworkBurnPct)}% is within acceptable range.`;
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
preworkCostUsd: preworkCost,
|
|
1220
|
+
executionCostUsd: executionCost,
|
|
1221
|
+
verificationCostUsd: 0,
|
|
1222
|
+
retryCostUsd: 0,
|
|
1223
|
+
totalCostUsd: totalCost,
|
|
1224
|
+
preworkBurnPct,
|
|
1225
|
+
timeToFirstDeltaMs,
|
|
1226
|
+
firstDelta: {
|
|
1227
|
+
detected: input.firstDeltaDetected,
|
|
1228
|
+
...(input.firstDeltaTimestampMs ? { timestampMs: input.firstDeltaTimestampMs } : {}),
|
|
1229
|
+
...(input.firstDeltaFilePath ? { filePath: input.firstDeltaFilePath } : {}),
|
|
1230
|
+
...(input.firstDeltaChangeType ? { changeType: input.firstDeltaChangeType } : {}),
|
|
1231
|
+
...(timeToFirstDeltaMs !== undefined ? { timeToFirstDeltaMs } : {})
|
|
1232
|
+
},
|
|
1233
|
+
...(accepted ? { costPerAcceptedChange: totalCost } : {}),
|
|
1234
|
+
routeRecommendation,
|
|
1235
|
+
routeRecommendationReason
|
|
1156
1236
|
};
|
|
1157
1237
|
}
|
|
1238
|
+
const MARTINLOOP_METADATA_PATTERNS = [
|
|
1239
|
+
/^PROGRESS\.md$/u,
|
|
1240
|
+
/^\.martin\//u,
|
|
1241
|
+
/\.lock$/u,
|
|
1242
|
+
/^node_modules\//u,
|
|
1243
|
+
/^\.git\//u,
|
|
1244
|
+
/^\.cache\//u,
|
|
1245
|
+
];
|
|
1246
|
+
function isMartinLoopMetadata(filePath) {
|
|
1247
|
+
return MARTINLOOP_METADATA_PATTERNS.some((p) => p.test(filePath));
|
|
1248
|
+
}
|
|
1158
1249
|
async function persistLoopRecordIfSupported(store, loop) {
|
|
1159
1250
|
await store?.writeLoopRecord?.(loop.loopId, loop);
|
|
1160
1251
|
}
|
|
@@ -1190,29 +1281,11 @@ function mergeCostProvenance(previous, current) {
|
|
|
1190
1281
|
}
|
|
1191
1282
|
return COST_PROVENANCE_RANK[current] < COST_PROVENANCE_RANK[previous] ? current : previous;
|
|
1192
1283
|
}
|
|
1193
|
-
function resolveChangedFiles(result, repoRoot) {
|
|
1284
|
+
function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
|
|
1194
1285
|
if (result.execution?.changedFiles !== undefined) {
|
|
1195
1286
|
return result.execution.changedFiles;
|
|
1196
1287
|
}
|
|
1197
|
-
|
|
1198
|
-
return [];
|
|
1199
|
-
}
|
|
1200
|
-
try {
|
|
1201
|
-
const diff = spawnSync("git", ["diff", "--name-only", "HEAD", "--", "."], {
|
|
1202
|
-
cwd: repoRoot,
|
|
1203
|
-
encoding: "utf8"
|
|
1204
|
-
});
|
|
1205
|
-
if (diff.status !== 0 || typeof diff.stdout !== "string") {
|
|
1206
|
-
return [];
|
|
1207
|
-
}
|
|
1208
|
-
return diff.stdout
|
|
1209
|
-
.split(/\r?\n/u)
|
|
1210
|
-
.map((entry) => entry.trim())
|
|
1211
|
-
.filter(Boolean);
|
|
1212
|
-
}
|
|
1213
|
-
catch {
|
|
1214
|
-
return [];
|
|
1215
|
-
}
|
|
1288
|
+
return listAttemptChangedFilesSinceBoundary({ repoRoot, boundary: rollbackBoundary });
|
|
1216
1289
|
}
|
|
1217
1290
|
function buildPatchDiff(result, changedFiles) {
|
|
1218
1291
|
// Use structured diff stats to build a minimal diff header if no raw diff is available
|
|
@@ -1338,7 +1411,19 @@ function toPatchDecisionArtifact(decision) {
|
|
|
1338
1411
|
reasonCodes: decision.reasonCodes
|
|
1339
1412
|
};
|
|
1340
1413
|
}
|
|
1341
|
-
|
|
1414
|
+
const SANDBOX_WRITE_BLOCKED_PATTERNS = [
|
|
1415
|
+
/createprocessasuserw failed:\s*\d+/iu,
|
|
1416
|
+
/windows sandbox: runner error/iu,
|
|
1417
|
+
/writing is blocked by read-only sandbox/iu,
|
|
1418
|
+
/read-only filesystem sandbox/iu,
|
|
1419
|
+
/approval is disabled/iu
|
|
1420
|
+
];
|
|
1421
|
+
function detectSandboxWriteBlocked(result) {
|
|
1422
|
+
const sources = [result.summary, result.failure?.message]
|
|
1423
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
1424
|
+
return sources.some((source) => SANDBOX_WRITE_BLOCKED_PATTERNS.some((pattern) => pattern.test(source)));
|
|
1425
|
+
}
|
|
1426
|
+
function classifyPatchDecisionFailure(decision, result) {
|
|
1342
1427
|
if (decision.reasonCodes.includes("grounding_failure")) {
|
|
1343
1428
|
return {
|
|
1344
1429
|
failureClass: "repo_grounding_failure",
|
|
@@ -1380,6 +1465,14 @@ function classifyPatchDecisionFailure(decision) {
|
|
|
1380
1465
|
recommendedIntervention: "escalate_human"
|
|
1381
1466
|
};
|
|
1382
1467
|
}
|
|
1468
|
+
if (result && detectSandboxWriteBlocked(result)) {
|
|
1469
|
+
return {
|
|
1470
|
+
failureClass: "sandbox_write_blocked",
|
|
1471
|
+
rationale: "The adapter reported a completed patch but the workspace-write sandbox blocked the underlying file write, so no trustworthy code change landed on disk.",
|
|
1472
|
+
retryable: true,
|
|
1473
|
+
recommendedIntervention: "switch_adapter"
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1383
1476
|
return {
|
|
1384
1477
|
failureClass: "no_progress",
|
|
1385
1478
|
rationale: "Patch truth discarded the attempt because it did not produce a trustworthy code change.",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MartinLoop Memory Store
|
|
3
|
+
*
|
|
4
|
+
* Append-only, never-overwriting persistent memory. Records user consent
|
|
5
|
+
* signals, preferences, behavioral patterns, and learning outcomes so
|
|
6
|
+
* MartinLoop gets smarter with every session.
|
|
7
|
+
*
|
|
8
|
+
* Design principles:
|
|
9
|
+
* - Append-only JSONL — entries are never deleted or overwritten
|
|
10
|
+
* - Structured by kind — each memory entry has a type and a key
|
|
11
|
+
* - Source-aware — distinguishes explicit user input from inferred patterns
|
|
12
|
+
* - Confidence-scored — inferred memories degrade gracefully
|
|
13
|
+
*
|
|
14
|
+
* Storage: <runsRoot>/_martin/memory.jsonl
|
|
15
|
+
*/
|
|
16
|
+
export type MemoryKind = "consent" | "preference" | "pattern" | "feedback" | "budget" | "model";
|
|
17
|
+
export interface MemoryEntry {
|
|
18
|
+
/** ISO-8601 timestamp */
|
|
19
|
+
timestamp: string;
|
|
20
|
+
/** What kind of memory this is */
|
|
21
|
+
kind: MemoryKind;
|
|
22
|
+
/** Dot-notated key, e.g. "model.direct.claude", "budget.auth.usd", "gate.bypassed" */
|
|
23
|
+
key: string;
|
|
24
|
+
/** The memory payload */
|
|
25
|
+
value: unknown;
|
|
26
|
+
/** Did the user say it explicitly, or did Martin observe it? */
|
|
27
|
+
source: "explicit" | "inferred";
|
|
28
|
+
/** Confidence score 0-1 — inferred memories should be < 0.8 */
|
|
29
|
+
confidence: number;
|
|
30
|
+
}
|
|
31
|
+
export interface MemorySummary {
|
|
32
|
+
totalEntries: number;
|
|
33
|
+
byKind: Partial<Record<MemoryKind, MemoryEntry[]>>;
|
|
34
|
+
recentPreferences: MemoryEntry[];
|
|
35
|
+
recentConsents: MemoryEntry[];
|
|
36
|
+
recentFeedback: MemoryEntry[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Append a memory entry. Never overwrites — always appends.
|
|
40
|
+
*/
|
|
41
|
+
export declare function appendMemory(runsRoot: string, entry: MemoryEntry): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Read all memory entries in order (oldest first).
|
|
44
|
+
*/
|
|
45
|
+
export declare function readMemoryEntries(runsRoot: string): Promise<MemoryEntry[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Get the most recent memory entry for a specific key.
|
|
48
|
+
* Returns undefined if no entry exists for that key.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getPreference(runsRoot: string, key: string): Promise<MemoryEntry | undefined>;
|
|
51
|
+
/**
|
|
52
|
+
* Build a summary of memory entries for MCP resource and MartinLoop context injection.
|
|
53
|
+
* Returns the top 20 most recent entries per kind.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildMemorySummary(entries: MemoryEntry[]): MemorySummary;
|
|
56
|
+
/**
|
|
57
|
+
* Record a preference from user input.
|
|
58
|
+
*/
|
|
59
|
+
export declare function recordPreference(runsRoot: string, key: string, value: unknown, source?: "explicit" | "inferred"): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Record a consent signal (user approved or denied a governance action).
|
|
62
|
+
*/
|
|
63
|
+
export declare function recordConsent(runsRoot: string, action: string, approved: boolean, context?: Record<string, unknown>): Promise<void>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MartinLoop Memory Store
|
|
3
|
+
*
|
|
4
|
+
* Append-only, never-overwriting persistent memory. Records user consent
|
|
5
|
+
* signals, preferences, behavioral patterns, and learning outcomes so
|
|
6
|
+
* MartinLoop gets smarter with every session.
|
|
7
|
+
*
|
|
8
|
+
* Design principles:
|
|
9
|
+
* - Append-only JSONL — entries are never deleted or overwritten
|
|
10
|
+
* - Structured by kind — each memory entry has a type and a key
|
|
11
|
+
* - Source-aware — distinguishes explicit user input from inferred patterns
|
|
12
|
+
* - Confidence-scored — inferred memories degrade gracefully
|
|
13
|
+
*
|
|
14
|
+
* Storage: <runsRoot>/_martin/memory.jsonl
|
|
15
|
+
*/
|
|
16
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
18
|
+
const MEMORY_DIRECTORY = "_martin";
|
|
19
|
+
const MEMORY_FILENAME = "memory.jsonl";
|
|
20
|
+
function resolveMemoryPath(runsRoot) {
|
|
21
|
+
return join(resolve(runsRoot), MEMORY_DIRECTORY, MEMORY_FILENAME);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Append a memory entry. Never overwrites — always appends.
|
|
25
|
+
*/
|
|
26
|
+
export async function appendMemory(runsRoot, entry) {
|
|
27
|
+
const memPath = resolveMemoryPath(runsRoot);
|
|
28
|
+
await mkdir(join(resolve(runsRoot), MEMORY_DIRECTORY), { recursive: true });
|
|
29
|
+
await appendFile(memPath, JSON.stringify(entry) + "\n", "utf8");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read all memory entries in order (oldest first).
|
|
33
|
+
*/
|
|
34
|
+
export async function readMemoryEntries(runsRoot) {
|
|
35
|
+
const memPath = resolveMemoryPath(runsRoot);
|
|
36
|
+
try {
|
|
37
|
+
const raw = await readFile(memPath, "utf8");
|
|
38
|
+
return raw
|
|
39
|
+
.split("\n")
|
|
40
|
+
.filter((line) => line.trim().length > 0)
|
|
41
|
+
.map((line) => {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(line);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.filter((e) => e !== null);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get the most recent memory entry for a specific key.
|
|
57
|
+
* Returns undefined if no entry exists for that key.
|
|
58
|
+
*/
|
|
59
|
+
export async function getPreference(runsRoot, key) {
|
|
60
|
+
const entries = await readMemoryEntries(runsRoot);
|
|
61
|
+
// Return the most recent entry for this key (last in file)
|
|
62
|
+
const matches = entries.filter((e) => e.key === key);
|
|
63
|
+
return matches[matches.length - 1];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build a summary of memory entries for MCP resource and MartinLoop context injection.
|
|
67
|
+
* Returns the top 20 most recent entries per kind.
|
|
68
|
+
*/
|
|
69
|
+
export function buildMemorySummary(entries) {
|
|
70
|
+
const byKind = {};
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (!byKind[entry.kind]) {
|
|
73
|
+
byKind[entry.kind] = [];
|
|
74
|
+
}
|
|
75
|
+
byKind[entry.kind].push(entry);
|
|
76
|
+
}
|
|
77
|
+
// Keep only the 20 most recent per kind
|
|
78
|
+
for (const kind of Object.keys(byKind)) {
|
|
79
|
+
byKind[kind] = byKind[kind].slice(-20);
|
|
80
|
+
}
|
|
81
|
+
const recent = entries.slice(-50);
|
|
82
|
+
return {
|
|
83
|
+
totalEntries: entries.length,
|
|
84
|
+
byKind,
|
|
85
|
+
recentPreferences: recent.filter((e) => e.kind === "preference"),
|
|
86
|
+
recentConsents: recent.filter((e) => e.kind === "consent"),
|
|
87
|
+
recentFeedback: recent.filter((e) => e.kind === "feedback")
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Record a preference from user input.
|
|
92
|
+
*/
|
|
93
|
+
export async function recordPreference(runsRoot, key, value, source = "explicit") {
|
|
94
|
+
await appendMemory(runsRoot, {
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
kind: "preference",
|
|
97
|
+
key,
|
|
98
|
+
value,
|
|
99
|
+
source,
|
|
100
|
+
confidence: source === "explicit" ? 1.0 : 0.6
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Record a consent signal (user approved or denied a governance action).
|
|
105
|
+
*/
|
|
106
|
+
export async function recordConsent(runsRoot, action, approved, context) {
|
|
107
|
+
await appendMemory(runsRoot, {
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
kind: "consent",
|
|
110
|
+
key: `consent.${action}`,
|
|
111
|
+
value: { approved, context },
|
|
112
|
+
source: "explicit",
|
|
113
|
+
confidence: 1.0
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { writeReceiptIntegrityMaterial } from "./integrity.js";
|
|
6
6
|
// ─── FileRunStore implementation ─────────────────────────────────────────────
|
|
7
|
+
const RUN_INDEX_FILENAME = "run-index.ndjson";
|
|
7
8
|
export function resolveRunsRoot(env = process.env) {
|
|
8
9
|
return env["MARTIN_RUNS_DIR"]?.trim() ??
|
|
9
10
|
join(homedir(), ".martin", "runs");
|
|
@@ -84,6 +85,7 @@ export function createFileRunStore(options = {}) {
|
|
|
84
85
|
const dir = runDir(runsRoot, runId);
|
|
85
86
|
await mkdir(dir, { recursive: true });
|
|
86
87
|
await writeJsonFile(join(dir, "loop-record.json"), loop);
|
|
88
|
+
await appendRunIndexRecord(runsRoot, loop);
|
|
87
89
|
const ledgerRaw = await readFile(join(dir, "ledger.jsonl"), "utf8").catch(() => "");
|
|
88
90
|
const ledgerEntries = ledgerRaw
|
|
89
91
|
.split(/\r?\n/u)
|
|
@@ -110,3 +112,14 @@ export function createFileRunStore(options = {}) {
|
|
|
110
112
|
async function writeJsonFile(path, value) {
|
|
111
113
|
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
112
114
|
}
|
|
115
|
+
async function appendRunIndexRecord(runsRoot, loop) {
|
|
116
|
+
const line = JSON.stringify({
|
|
117
|
+
loopId: loop.loopId,
|
|
118
|
+
workspaceId: loop.workspaceId,
|
|
119
|
+
projectId: loop.projectId,
|
|
120
|
+
status: loop.status,
|
|
121
|
+
lifecycleState: loop.lifecycleState,
|
|
122
|
+
updatedAt: loop.updatedAt
|
|
123
|
+
});
|
|
124
|
+
await appendFile(join(runsRoot, RUN_INDEX_FILENAME), `${line}\n`, "utf8");
|
|
125
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-Only Trace Store
|
|
3
|
+
*
|
|
4
|
+
* Aggregates run data over time to build institutional memory. Every governed
|
|
5
|
+
* run's routing decision, cost, failure class, and outcome is appended to a
|
|
6
|
+
* local JSONL trace file. Data is NEVER overwritten — only appended.
|
|
7
|
+
*
|
|
8
|
+
* The trace store feeds:
|
|
9
|
+
* - classifyRoute() with historicalDirectSuccessRate from real past runs
|
|
10
|
+
* - Cost prediction models with actual spend data
|
|
11
|
+
* - Failure pattern detection across runs
|
|
12
|
+
* - Team analytics and optimization recommendations
|
|
13
|
+
*
|
|
14
|
+
* Storage: `<runsRoot>/_martin/trace-log.jsonl` (one JSON object per line)
|
|
15
|
+
*/
|
|
16
|
+
export interface TraceEntry {
|
|
17
|
+
/** ISO-8601 timestamp */
|
|
18
|
+
timestamp: string;
|
|
19
|
+
/** Unique loop identifier */
|
|
20
|
+
loopId: string;
|
|
21
|
+
/** The objective that was governed */
|
|
22
|
+
objective: string;
|
|
23
|
+
/** Which engine executed the run */
|
|
24
|
+
engine: string;
|
|
25
|
+
/** Route selected by classifyRoute */
|
|
26
|
+
selectedRoute: "direct" | "manager" | "consensus";
|
|
27
|
+
/** Route classifier confidence (0-1) */
|
|
28
|
+
routeConfidence: number;
|
|
29
|
+
/** Budget allocated (USD) */
|
|
30
|
+
budgetUsd: number;
|
|
31
|
+
/** Actual spend (USD) */
|
|
32
|
+
actualCostUsd: number;
|
|
33
|
+
/** Pre-work burn percentage */
|
|
34
|
+
preWorkBurnPct: number;
|
|
35
|
+
/** Number of attempts */
|
|
36
|
+
attempts: number;
|
|
37
|
+
/** Final loop status */
|
|
38
|
+
status: string;
|
|
39
|
+
/** Final lifecycle state */
|
|
40
|
+
lifecycleState: string;
|
|
41
|
+
/** Failure class of last attempt (if failed) */
|
|
42
|
+
failureClass?: string;
|
|
43
|
+
/** Whether verification passed */
|
|
44
|
+
verificationPassed: boolean;
|
|
45
|
+
/** Number of files changed */
|
|
46
|
+
filesChanged: number;
|
|
47
|
+
/** Working directory (hashed for privacy) */
|
|
48
|
+
workspaceHash: string;
|
|
49
|
+
}
|
|
50
|
+
export interface TraceAggregation {
|
|
51
|
+
totalRuns: number;
|
|
52
|
+
directRuns: number;
|
|
53
|
+
managerRuns: number;
|
|
54
|
+
consensusRuns: number;
|
|
55
|
+
directSuccessRate: number;
|
|
56
|
+
managerSuccessRate: number;
|
|
57
|
+
averageCostUsd: number;
|
|
58
|
+
averagePreWorkBurnPct: number;
|
|
59
|
+
totalSpendUsd: number;
|
|
60
|
+
totalSavedUsd: number;
|
|
61
|
+
topFailureClasses: Array<{
|
|
62
|
+
failureClass: string;
|
|
63
|
+
count: number;
|
|
64
|
+
}>;
|
|
65
|
+
averageAttemptsPerRun: number;
|
|
66
|
+
lastUpdated: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Append a trace entry to the local trace store. Never overwrites.
|
|
70
|
+
*/
|
|
71
|
+
export declare function appendTraceEntry(runsRoot: string, entry: TraceEntry): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Read all trace entries from the local trace store.
|
|
74
|
+
*/
|
|
75
|
+
export declare function readTraceEntries(runsRoot: string): Promise<TraceEntry[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Aggregate trace entries into summary statistics.
|
|
78
|
+
* Used by classifyRoute() to get historicalDirectSuccessRate.
|
|
79
|
+
*/
|
|
80
|
+
export declare function aggregateTraces(entries: TraceEntry[]): TraceAggregation;
|
|
81
|
+
/**
|
|
82
|
+
* Get the historical direct success rate for use in route classification.
|
|
83
|
+
* Returns undefined if insufficient data (< 5 runs).
|
|
84
|
+
*/
|
|
85
|
+
export declare function getHistoricalDirectSuccessRate(runsRoot: string): Promise<number | undefined>;
|