@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.
Files changed (39) hide show
  1. package/README.md +10 -2
  2. package/dist/discovery-metadata.d.ts +1 -1
  3. package/dist/discovery-metadata.js +2 -0
  4. package/dist/package-version.d.ts +1 -1
  5. package/dist/package-version.js +1 -1
  6. package/dist/prompts.js +19 -1
  7. package/dist/resources.d.ts +2 -0
  8. package/dist/resources.js +202 -148
  9. package/dist/server-validation.d.ts +1 -1
  10. package/dist/server-validation.js +17 -0
  11. package/dist/server.d.ts +23 -0
  12. package/dist/server.js +161 -5
  13. package/dist/tools/doctor.js +6 -0
  14. package/dist/tools/tool-support.d.ts +1 -1
  15. package/dist/tools/tool-support.js +1 -1
  16. package/dist/vendor/adapters/claude-cli.d.ts +2 -2
  17. package/dist/vendor/adapters/claude-cli.js +22 -7
  18. package/dist/vendor/adapters/cli-bridge.d.ts +7 -0
  19. package/dist/vendor/adapters/cli-bridge.js +94 -8
  20. package/dist/vendor/adapters/codex-launcher.js +18 -7
  21. package/dist/vendor/adapters/openai-compatible.js +90 -57
  22. package/dist/vendor/contracts/index.d.ts +1 -1
  23. package/dist/vendor/contracts/index.js +1 -0
  24. package/dist/vendor/core/index.d.ts +10 -2
  25. package/dist/vendor/core/index.js +31 -3
  26. package/dist/vendor/core/persistence/memory-store.d.ts +63 -0
  27. package/dist/vendor/core/persistence/memory-store.js +115 -0
  28. package/dist/vendor/core/persistence/trace-store.d.ts +85 -0
  29. package/dist/vendor/core/persistence/trace-store.js +125 -0
  30. package/dist/vendor/core/repo-analyzer.d.ts +53 -0
  31. package/dist/vendor/core/repo-analyzer.js +263 -0
  32. package/dist/vendor/core/routing.d.ts +42 -0
  33. package/dist/vendor/core/routing.js +156 -5
  34. package/dist/vendor/core/test-integrity.d.ts +64 -0
  35. package/dist/vendor/core/test-integrity.js +197 -0
  36. package/dist/workflow-state.d.ts +1 -1
  37. package/dist/workflow-state.js +12 -4
  38. package/package.json +76 -76
  39. 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 controller = new AbortController();
175
- const timer = setTimeout(() => controller.abort(), timeoutMs);
176
- try {
177
- const headers = { "Content-Type": "application/json" };
178
- if (apiKey)
179
- headers["Authorization"] = `Bearer ${apiKey}`;
180
- // OpenRouter requires a site URL header for attribution
181
- if (baseUrl.includes("openrouter")) {
182
- headers["HTTP-Referer"] = "https://martinloop.com";
183
- headers["X-Title"] = "MartinLoop";
184
- }
185
- const res = await fetchFn(endpoint, {
186
- method: "POST",
187
- headers,
188
- body: JSON.stringify({
189
- model,
190
- messages: [
191
- { role: "system", content: systemPrompt },
192
- { role: "user", content: prompt }
193
- ],
194
- temperature: 0.2,
195
- max_tokens: 8192
196
- }),
197
- signal: controller.signal
198
- });
199
- const body = (await res.json());
200
- if (!res.ok || body.error) {
201
- const errMsg = body.error?.message ?? `HTTP ${res.status}`;
202
- return {
203
- status: "failed",
204
- summary: `${model} API error: ${errMsg}`,
205
- usage: normalizeUsage({ actualUsd: 0, tokensIn: 0, tokensOut: 0, provenance: "unavailable" }),
206
- verification: { passed: false, summary: "API call failed before verifier." },
207
- failure: { message: errMsg, classHint: "infrastructure_error" }
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
- responseText = body.choices?.[0]?.message?.content ?? "";
211
- if (body.usage) {
212
- tokensIn = body.usage.prompt_tokens ?? tokensIn;
213
- tokensOut = body.usage.completion_tokens ?? 0;
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
- else {
216
- tokensOut = Math.ceil(responseText.length / CHARS_PER_TOKEN);
253
+ finally {
254
+ clearTimeout(timer);
217
255
  }
218
256
  }
219
- catch (error) {
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: message,
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: isAbort ? "Request timed out." : "Network error." },
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";
@@ -11,6 +11,7 @@ export const FAILURE_CLASSES = [
11
11
  "environment_mismatch",
12
12
  "budget_pressure",
13
13
  "safety_leash_blocked",
14
+ "sandbox_write_blocked",
14
15
  ];
15
16
  export { MARTIN_ERROR_CATEGORIES } from "./operator.js";
16
17
  export const DEFAULT_BUDGET = {
@@ -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
- function classifyPatchDecisionFailure(decision) {
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>;