@martinloop/mcp 0.3.5 → 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 +1 -1
- package/dist/discovery-metadata.js +2 -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 +2 -0
- package/dist/resources.js +202 -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 +161 -5
- package/dist/tools/doctor.js +6 -0
- package/dist/tools/tool-support.d.ts +1 -1
- package/dist/tools/tool-support.js +1 -1
- package/dist/vendor/adapters/claude-cli.d.ts +2 -2
- package/dist/vendor/adapters/claude-cli.js +22 -7
- package/dist/vendor/adapters/cli-bridge.d.ts +7 -0
- package/dist/vendor/adapters/cli-bridge.js +94 -8
- package/dist/vendor/adapters/codex-launcher.js +18 -7
- package/dist/vendor/adapters/openai-compatible.js +90 -57
- package/dist/vendor/contracts/index.d.ts +1 -1
- package/dist/vendor/contracts/index.js +1 -0
- package/dist/vendor/core/index.d.ts +10 -2
- package/dist/vendor/core/index.js +31 -3
- 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/trace-store.d.ts +85 -0
- package/dist/vendor/core/persistence/trace-store.js +125 -0
- package/dist/vendor/core/repo-analyzer.d.ts +53 -0
- package/dist/vendor/core/repo-analyzer.js +263 -0
- package/dist/vendor/core/routing.d.ts +42 -0
- package/dist/vendor/core/routing.js +156 -5
- 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 +1 -1
- package/dist/workflow-state.js +12 -4
- package/package.json +76 -76
- package/server.json +21 -21
|
@@ -72,12 +72,12 @@ function estimateCost(model, inputChars, outputChars) {
|
|
|
72
72
|
// ---------------------------------------------------------------------------
|
|
73
73
|
// Prompt builder
|
|
74
74
|
// ---------------------------------------------------------------------------
|
|
75
|
-
const DEFAULT_SYSTEM_PROMPT = `You are an expert software engineer executing a governed coding task.
|
|
76
|
-
Follow these rules exactly:
|
|
77
|
-
- Read the task description carefully and implement only what is asked.
|
|
78
|
-
- Do not add features, refactors, or improvements beyond the stated task.
|
|
79
|
-
- If the task asks you to write or modify code, output the complete file content with changes applied.
|
|
80
|
-
- Be precise, minimal, and test-backed in all changes.
|
|
75
|
+
const DEFAULT_SYSTEM_PROMPT = `You are an expert software engineer executing a governed coding task.
|
|
76
|
+
Follow these rules exactly:
|
|
77
|
+
- Read the task description carefully and implement only what is asked.
|
|
78
|
+
- Do not add features, refactors, or improvements beyond the stated task.
|
|
79
|
+
- If the task asks you to write or modify code, output the complete file content with changes applied.
|
|
80
|
+
- Be precise, minimal, and test-backed in all changes.
|
|
81
81
|
- State what you changed and why at the end of your response.`;
|
|
82
82
|
export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
|
|
83
83
|
export const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
|
|
@@ -166,70 +166,103 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
166
166
|
failure: { message: "budget_preflight_exceeded", classHint: "budget_pressure" }
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
|
-
// Call the OpenAI-compatible endpoint
|
|
169
|
+
// Call the OpenAI-compatible endpoint with exponential-backoff retry on
|
|
170
|
+
// transient failures (429 rate-limit, 503/5xx server errors, network errors).
|
|
171
|
+
// Auth errors (401/403) and bad-request errors (400) are not retried — they
|
|
172
|
+
// indicate a permanent configuration problem.
|
|
173
|
+
const MAX_RETRIES = 3;
|
|
174
|
+
const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
|
|
170
175
|
const endpoint = `${baseUrl}/v1/chat/completions`;
|
|
171
176
|
let responseText = "";
|
|
172
177
|
let tokensIn = estimated.tokensIn;
|
|
173
178
|
let tokensOut = 0;
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
179
|
+
const headers = { "Content-Type": "application/json" };
|
|
180
|
+
if (apiKey)
|
|
181
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
182
|
+
if (baseUrl.includes("openrouter")) {
|
|
183
|
+
headers["HTTP-Referer"] = "https://martinloop.com";
|
|
184
|
+
headers["X-Title"] = "MartinLoop";
|
|
185
|
+
}
|
|
186
|
+
const requestBody = JSON.stringify({
|
|
187
|
+
model,
|
|
188
|
+
messages: [
|
|
189
|
+
{ role: "system", content: systemPrompt },
|
|
190
|
+
{ role: "user", content: prompt }
|
|
191
|
+
],
|
|
192
|
+
temperature: 0.2,
|
|
193
|
+
max_tokens: 8192
|
|
194
|
+
});
|
|
195
|
+
let lastError = "";
|
|
196
|
+
let succeeded = false;
|
|
197
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
198
|
+
const controller = new AbortController();
|
|
199
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
200
|
+
try {
|
|
201
|
+
const res = await fetchFn(endpoint, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers,
|
|
204
|
+
body: requestBody,
|
|
205
|
+
signal: controller.signal
|
|
206
|
+
});
|
|
207
|
+
const body = (await res.json());
|
|
208
|
+
if (!res.ok || body.error) {
|
|
209
|
+
const errMsg = body.error?.message ?? `HTTP ${res.status}`;
|
|
210
|
+
if (RETRYABLE_STATUS.has(res.status) && attempt < MAX_RETRIES - 1) {
|
|
211
|
+
lastError = errMsg;
|
|
212
|
+
// Exponential backoff: 1s, 2s, 4s
|
|
213
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
status: "failed",
|
|
218
|
+
summary: `${model} API error: ${errMsg}`,
|
|
219
|
+
usage: normalizeUsage({ actualUsd: 0, tokensIn: 0, tokensOut: 0, provenance: "unavailable" }),
|
|
220
|
+
verification: { passed: false, summary: "API call failed before verifier." },
|
|
221
|
+
failure: { message: errMsg, classHint: "infrastructure_error" }
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
responseText = body.choices?.[0]?.message?.content ?? "";
|
|
225
|
+
if (body.usage) {
|
|
226
|
+
tokensIn = body.usage.prompt_tokens ?? tokensIn;
|
|
227
|
+
tokensOut = body.usage.completion_tokens ?? 0;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
tokensOut = Math.ceil(responseText.length / CHARS_PER_TOKEN);
|
|
231
|
+
}
|
|
232
|
+
succeeded = true;
|
|
233
|
+
break;
|
|
209
234
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
235
|
+
catch (error) {
|
|
236
|
+
const isAbort = error instanceof Error && error.name === "AbortError";
|
|
237
|
+
if (isAbort || attempt === MAX_RETRIES - 1) {
|
|
238
|
+
const message = isAbort
|
|
239
|
+
? `${model} request timed out after ${timeoutMs}ms`
|
|
240
|
+
: String(error);
|
|
241
|
+
return {
|
|
242
|
+
status: "failed",
|
|
243
|
+
summary: message,
|
|
244
|
+
usage: normalizeUsage({ actualUsd: 0, tokensIn: 0, tokensOut: 0, provenance: "unavailable" }),
|
|
245
|
+
verification: { passed: false, summary: isAbort ? "Request timed out." : "Network error." },
|
|
246
|
+
failure: { message, classHint: "infrastructure_error" }
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// Transient network error — retry with backoff
|
|
250
|
+
lastError = String(error);
|
|
251
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
|
|
214
252
|
}
|
|
215
|
-
|
|
216
|
-
|
|
253
|
+
finally {
|
|
254
|
+
clearTimeout(timer);
|
|
217
255
|
}
|
|
218
256
|
}
|
|
219
|
-
|
|
220
|
-
const isAbort = error instanceof Error && error.name === "AbortError";
|
|
221
|
-
const message = isAbort ? `${model} request timed out after ${timeoutMs}ms` : String(error);
|
|
257
|
+
if (!succeeded) {
|
|
222
258
|
return {
|
|
223
259
|
status: "failed",
|
|
224
|
-
summary:
|
|
260
|
+
summary: `${model} API error after ${MAX_RETRIES} attempts: ${lastError}`,
|
|
225
261
|
usage: normalizeUsage({ actualUsd: 0, tokensIn: 0, tokensOut: 0, provenance: "unavailable" }),
|
|
226
|
-
verification: { passed: false, summary:
|
|
227
|
-
failure: { message, classHint: "infrastructure_error" }
|
|
262
|
+
verification: { passed: false, summary: "API call failed after retries." },
|
|
263
|
+
failure: { message: lastError, classHint: "infrastructure_error" }
|
|
228
264
|
};
|
|
229
265
|
}
|
|
230
|
-
finally {
|
|
231
|
-
clearTimeout(timer);
|
|
232
|
-
}
|
|
233
266
|
if (!responseText.trim()) {
|
|
234
267
|
return {
|
|
235
268
|
status: "failed",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type LoopStatus = "queued" | "running" | "verifying" | "completed" | "failed" | "exited";
|
|
2
2
|
export type LoopLifecycleState = "created" | "running" | "verifying" | "completed" | "budget_exit" | "diminishing_returns" | "stuck_exit" | "human_escalation";
|
|
3
|
-
export declare const FAILURE_CLASSES: readonly ["logic_error", "hallucination", "syntax_error", "type_error", "test_regression", "scope_creep", "no_progress", "repo_grounding_failure", "verification_failure", "environment_mismatch", "budget_pressure", "safety_leash_blocked"];
|
|
3
|
+
export declare const FAILURE_CLASSES: readonly ["logic_error", "hallucination", "syntax_error", "type_error", "test_regression", "scope_creep", "no_progress", "repo_grounding_failure", "verification_failure", "environment_mismatch", "budget_pressure", "safety_leash_blocked", "sandbox_write_blocked"];
|
|
4
4
|
export type FailureClass = (typeof FAILURE_CLASSES)[number];
|
|
5
5
|
export type InterventionType = "compress_context" | "change_model" | "tighten_task" | "switch_adapter" | "run_verifier" | "escalate_human" | "stop_loop";
|
|
6
6
|
export type LoopEventType = "run.started" | "attempt.started" | "attempt.completed" | "failure.classified" | "intervention.selected" | "verification.completed" | "budget.updated" | "run.completed";
|
|
@@ -14,8 +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 } from "./routing.js";
|
|
18
|
-
export type { RouteDecision, RouteClassificationInput } from "./routing.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";
|
|
19
23
|
export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
20
24
|
export type { ContextIntegrityPrecheck, ContextIntegrityVerdict } from "./context-integrity.js";
|
|
21
25
|
export { compilePromptPacket } from "./compiler.js";
|
|
@@ -24,6 +28,10 @@ export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoop
|
|
|
24
28
|
export type { AttemptArtifacts, LedgerEvent, LedgerEventKind, LoopAttemptRecord, LoopRunRecord, ReceiptIntegrityChainEntry, RunContract, RunStore, StoredReceiptIntegrityMaterial } from "./persistence/index.js";
|
|
25
29
|
export { compileAndPersistContext } from "./persistence/index.js";
|
|
26
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";
|
|
27
35
|
export interface MartinAdapterRequest {
|
|
28
36
|
loopId: string;
|
|
29
37
|
attemptId: string;
|
|
@@ -13,7 +13,11 @@ export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, ver
|
|
|
13
13
|
export { compileExecutionPolicy } from "./policy-compiler.js";
|
|
14
14
|
export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
|
|
15
15
|
// ─── Routing economics ─────────────────────────────────────────────────────
|
|
16
|
-
export { classifyRoute, evaluatePreworkBurnPolicy } from "./routing.js";
|
|
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";
|
|
17
21
|
// ─── Context Integrity Pre-gate ──────────────────────────────────────────────
|
|
18
22
|
export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
19
23
|
// ─── Prompt packet compiler ──────────────────────────────────────────────────
|
|
@@ -21,6 +25,10 @@ export { compilePromptPacket } from "./compiler.js";
|
|
|
21
25
|
// ─── Persistence (RunStore, LedgerEvent, FileRunStore) ──────────────────────
|
|
22
26
|
export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
|
|
23
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";
|
|
24
32
|
/**
|
|
25
33
|
* Admission gate — must pass before any attempt is executed.
|
|
26
34
|
* Evaluates budget headroom, oscillation, and repetitive failure patterns.
|
|
@@ -1009,7 +1017,7 @@ export async function runMartin(input) {
|
|
|
1009
1017
|
}
|
|
1010
1018
|
}
|
|
1011
1019
|
if (patchDecision && patchDecision.decision !== "KEEP" && !failure) {
|
|
1012
|
-
failure = classifyPatchDecisionFailure(patchDecision);
|
|
1020
|
+
failure = classifyPatchDecisionFailure(patchDecision, result);
|
|
1013
1021
|
loop = applyPatchFailureToLoop(loop, {
|
|
1014
1022
|
attemptId,
|
|
1015
1023
|
summary: patchDecision.summary,
|
|
@@ -1403,7 +1411,19 @@ function toPatchDecisionArtifact(decision) {
|
|
|
1403
1411
|
reasonCodes: decision.reasonCodes
|
|
1404
1412
|
};
|
|
1405
1413
|
}
|
|
1406
|
-
|
|
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) {
|
|
1407
1427
|
if (decision.reasonCodes.includes("grounding_failure")) {
|
|
1408
1428
|
return {
|
|
1409
1429
|
failureClass: "repo_grounding_failure",
|
|
@@ -1445,6 +1465,14 @@ function classifyPatchDecisionFailure(decision) {
|
|
|
1445
1465
|
recommendedIntervention: "escalate_human"
|
|
1446
1466
|
};
|
|
1447
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
|
+
}
|
|
1448
1476
|
return {
|
|
1449
1477
|
failureClass: "no_progress",
|
|
1450
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
|
+
}
|
|
@@ -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>;
|