@martinloop/mcp 0.3.5 → 0.3.7

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 (118) hide show
  1. package/README.md +137 -137
  2. package/dist/discovery-metadata.d.ts +1 -1
  3. package/dist/discovery-metadata.js +2 -0
  4. package/dist/prompts.js +19 -1
  5. package/dist/resources.d.ts +3 -0
  6. package/dist/resources.js +125 -17
  7. package/dist/server-validation.d.ts +1 -1
  8. package/dist/server-validation.js +12 -0
  9. package/dist/server.js +18 -7
  10. package/dist/tools/doctor.js +6 -0
  11. package/dist/tools/run-loop.d.ts +2 -1
  12. package/dist/tools/run-loop.js +5 -1
  13. package/dist/tools/tool-support.d.ts +1 -1
  14. package/dist/tools/tool-support.js +2 -1
  15. package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +146 -0
  16. package/dist/vendor/adapters/adapters/src/claude-cli.js +1100 -0
  17. package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +103 -0
  18. package/dist/vendor/adapters/adapters/src/cli-bridge.js +569 -0
  19. package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +78 -0
  20. package/dist/vendor/adapters/adapters/src/codex-launcher.js +663 -0
  21. package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +10 -0
  22. package/dist/vendor/adapters/adapters/src/direct-provider.js +40 -0
  23. package/dist/vendor/adapters/adapters/src/index.d.ts +8 -0
  24. package/dist/vendor/adapters/adapters/src/index.js +8 -0
  25. package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +62 -0
  26. package/dist/vendor/adapters/adapters/src/openai-compatible.js +306 -0
  27. package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +17 -0
  28. package/dist/vendor/adapters/adapters/src/runtime-support.js +59 -0
  29. package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +8 -0
  30. package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +40 -0
  31. package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +8 -0
  32. package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +12 -0
  33. package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +9 -0
  34. package/dist/vendor/adapters/adapters/src/verifier-only.js +63 -0
  35. package/dist/vendor/adapters/claude-cli.js +21 -20
  36. package/dist/vendor/adapters/cli-bridge.d.ts +7 -0
  37. package/dist/vendor/adapters/cli-bridge.js +94 -8
  38. package/dist/vendor/adapters/codex-launcher.js +18 -7
  39. package/dist/vendor/adapters/contracts/src/context-graph.d.ts +48 -0
  40. package/dist/vendor/adapters/contracts/src/context-graph.js +26 -0
  41. package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +78 -0
  42. package/dist/vendor/adapters/contracts/src/execution-policy.js +43 -0
  43. package/dist/vendor/adapters/contracts/src/governance.d.ts +24 -0
  44. package/dist/vendor/adapters/contracts/src/governance.js +11 -0
  45. package/dist/vendor/adapters/contracts/src/identity.d.ts +29 -0
  46. package/dist/vendor/adapters/contracts/src/identity.js +30 -0
  47. package/dist/vendor/adapters/contracts/src/index.d.ts +412 -0
  48. package/dist/vendor/adapters/contracts/src/index.js +225 -0
  49. package/dist/vendor/adapters/contracts/src/operator.d.ts +19 -0
  50. package/dist/vendor/adapters/contracts/src/operator.js +11 -0
  51. package/dist/vendor/adapters/contracts/src/trajectory.d.ts +26 -0
  52. package/dist/vendor/adapters/contracts/src/trajectory.js +27 -0
  53. package/dist/vendor/adapters/core/src/compiler.d.ts +52 -0
  54. package/dist/vendor/adapters/core/src/compiler.js +52 -0
  55. package/dist/vendor/adapters/core/src/context-graph.d.ts +3 -0
  56. package/dist/vendor/adapters/core/src/context-graph.js +371 -0
  57. package/dist/vendor/adapters/core/src/context-integrity.d.ts +26 -0
  58. package/dist/vendor/adapters/core/src/context-integrity.js +80 -0
  59. package/dist/vendor/adapters/core/src/grounding.d.ts +38 -0
  60. package/dist/vendor/adapters/core/src/grounding.js +273 -0
  61. package/dist/vendor/adapters/core/src/identity.d.ts +20 -0
  62. package/dist/vendor/adapters/core/src/identity.js +86 -0
  63. package/dist/vendor/adapters/core/src/index.d.ts +186 -0
  64. package/dist/vendor/adapters/core/src/index.js +1495 -0
  65. package/dist/vendor/adapters/core/src/leash.d.ts +48 -0
  66. package/dist/vendor/adapters/core/src/leash.js +497 -0
  67. package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +18 -0
  68. package/dist/vendor/adapters/core/src/persistence/compiler.js +34 -0
  69. package/dist/vendor/adapters/core/src/persistence/index.d.ts +10 -0
  70. package/dist/vendor/adapters/core/src/persistence/index.js +5 -0
  71. package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +39 -0
  72. package/dist/vendor/adapters/core/src/persistence/integrity.js +252 -0
  73. package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +23 -0
  74. package/dist/vendor/adapters/core/src/persistence/ledger.js +9 -0
  75. package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +63 -0
  76. package/dist/vendor/adapters/core/src/persistence/memory-store.js +115 -0
  77. package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +66 -0
  78. package/dist/vendor/adapters/core/src/persistence/runs-reader.js +140 -0
  79. package/dist/vendor/adapters/core/src/persistence/store.d.ts +89 -0
  80. package/dist/vendor/adapters/core/src/persistence/store.js +125 -0
  81. package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +85 -0
  82. package/dist/vendor/adapters/core/src/persistence/trace-store.js +125 -0
  83. package/dist/vendor/adapters/core/src/policy-compiler.d.ts +2 -0
  84. package/dist/vendor/adapters/core/src/policy-compiler.js +195 -0
  85. package/dist/vendor/adapters/core/src/policy.d.ts +156 -0
  86. package/dist/vendor/adapters/core/src/policy.js +704 -0
  87. package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +53 -0
  88. package/dist/vendor/adapters/core/src/repo-analyzer.js +263 -0
  89. package/dist/vendor/adapters/core/src/rollback.d.ts +15 -0
  90. package/dist/vendor/adapters/core/src/rollback.js +253 -0
  91. package/dist/vendor/adapters/core/src/routing.d.ts +82 -0
  92. package/dist/vendor/adapters/core/src/routing.js +336 -0
  93. package/dist/vendor/adapters/core/src/test-integrity.d.ts +64 -0
  94. package/dist/vendor/adapters/core/src/test-integrity.js +197 -0
  95. package/dist/vendor/adapters/core/src/trajectory.d.ts +18 -0
  96. package/dist/vendor/adapters/core/src/trajectory.js +295 -0
  97. package/dist/vendor/adapters/openai-compatible.js +84 -51
  98. package/dist/vendor/adapters/verifier-only.js +2 -2
  99. package/dist/vendor/contracts/index.d.ts +2 -2
  100. package/dist/vendor/contracts/index.js +1 -0
  101. package/dist/vendor/core/compiler.js +4 -9
  102. package/dist/vendor/core/index.d.ts +10 -2
  103. package/dist/vendor/core/index.js +32 -91
  104. package/dist/vendor/core/persistence/memory-store.d.ts +63 -0
  105. package/dist/vendor/core/persistence/memory-store.js +115 -0
  106. package/dist/vendor/core/persistence/trace-store.d.ts +85 -0
  107. package/dist/vendor/core/persistence/trace-store.js +125 -0
  108. package/dist/vendor/core/repo-analyzer.d.ts +53 -0
  109. package/dist/vendor/core/repo-analyzer.js +263 -0
  110. package/dist/vendor/core/rollback.js +2 -3
  111. package/dist/vendor/core/routing.d.ts +42 -0
  112. package/dist/vendor/core/routing.js +156 -5
  113. package/dist/vendor/core/test-integrity.d.ts +64 -0
  114. package/dist/vendor/core/test-integrity.js +197 -0
  115. package/dist/workflow-state.d.ts +1 -1
  116. package/dist/workflow-state.js +12 -4
  117. package/package.json +1 -1
  118. package/server.json +2 -2
@@ -0,0 +1,252 @@
1
+ import { createHash, createHmac, randomBytes } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ const RECEIPT_INTEGRITY_SCHEMA_VERSION = "martin.receipt-integrity.v1";
6
+ const RECEIPT_INTEGRITY_KEY_DIR_MODE = 0o700;
7
+ const RECEIPT_INTEGRITY_KEY_FILE_MODE = 0o600;
8
+ export function resolveReceiptIntegrityRoot(env = process.env) {
9
+ return env["MARTIN_INTEGRITY_KEY_DIR"]?.trim() ??
10
+ join(homedir(), ".martin", "receipt-integrity");
11
+ }
12
+ export async function writeReceiptIntegrityMaterial(input) {
13
+ const signedAt = input.signedAt ?? new Date().toISOString();
14
+ const keyMaterial = await ensureReceiptIntegrityKey(input.runsRoot, input.runId);
15
+ if (!keyMaterial) {
16
+ return undefined;
17
+ }
18
+ const [key, keyId] = keyMaterial;
19
+ const chain = buildReceiptIntegrityChain(input.ledgerEntries);
20
+ const loopRecordRaw = serializeStoredJson(input.loopRecord);
21
+ const ledgerRaw = serializeStoredJsonl(input.ledgerEntries);
22
+ const materialBase = {
23
+ schemaVersion: RECEIPT_INTEGRITY_SCHEMA_VERSION,
24
+ runId: input.runId,
25
+ keyId,
26
+ signedAt,
27
+ ...(input.scope ? { scope: input.scope } : {}),
28
+ loopRecordSha256: sha256(loopRecordRaw),
29
+ ledgerSha256: sha256(ledgerRaw),
30
+ ledgerHeadHash: chain.at(-1)?.entryHash ?? "root",
31
+ entryCount: chain.length,
32
+ chain
33
+ };
34
+ const material = {
35
+ ...materialBase,
36
+ signatureHmacSha256: createReceiptIntegritySignature(key, materialBase)
37
+ };
38
+ await mkdir(join(input.runsRoot, input.runId), { recursive: true });
39
+ await writeFile(resolveReceiptIntegrityPath(input.runsRoot, input.runId), serializeStoredJson(material), "utf8");
40
+ return material;
41
+ }
42
+ export async function verifyReceiptIntegrityFromFiles(input) {
43
+ const integrityPath = resolveReceiptIntegrityPath(input.runsRoot, input.runId);
44
+ const [rawMaterial, rawLoopRecord, rawLedger, key] = await Promise.all([
45
+ readFile(integrityPath, "utf8").catch(() => null),
46
+ readFile(input.loopRecordPath, "utf8").catch(() => null),
47
+ readFile(input.ledgerPath, "utf8").catch(() => null),
48
+ readReceiptIntegrityKey(input.runsRoot, input.runId).catch(() => null)
49
+ ]);
50
+ if (!rawMaterial || !rawLoopRecord || rawLedger === null || !key) {
51
+ return {
52
+ state: "material_missing",
53
+ reason: "Receipt integrity material is missing for this run.",
54
+ warnings: ["Receipt integrity material is incomplete; trust claims are unavailable."]
55
+ };
56
+ }
57
+ let material;
58
+ try {
59
+ material = JSON.parse(rawMaterial);
60
+ }
61
+ catch {
62
+ return {
63
+ state: "tamper_detected",
64
+ reason: "Receipt integrity metadata is unreadable."
65
+ };
66
+ }
67
+ const actualLoopRecordSha256 = sha256(rawLoopRecord);
68
+ if (actualLoopRecordSha256 !== material.loopRecordSha256) {
69
+ return {
70
+ state: "tamper_detected",
71
+ keyId: material.keyId,
72
+ signedAt: material.signedAt,
73
+ loopRecordSha256: material.loopRecordSha256,
74
+ ledgerSha256: material.ledgerSha256,
75
+ ledgerHeadHash: material.ledgerHeadHash,
76
+ entryCount: material.entryCount,
77
+ reason: "loop_record_hash_mismatch"
78
+ };
79
+ }
80
+ const actualLedgerSha256 = sha256(rawLedger);
81
+ if (actualLedgerSha256 !== material.ledgerSha256) {
82
+ return {
83
+ state: "tamper_detected",
84
+ keyId: material.keyId,
85
+ signedAt: material.signedAt,
86
+ loopRecordSha256: material.loopRecordSha256,
87
+ ledgerSha256: material.ledgerSha256,
88
+ ledgerHeadHash: material.ledgerHeadHash,
89
+ entryCount: material.entryCount,
90
+ reason: "ledger_hash_mismatch"
91
+ };
92
+ }
93
+ let parsedLedgerEntries;
94
+ try {
95
+ parsedLedgerEntries = rawLedger
96
+ .split(/\r?\n/u)
97
+ .map((line) => line.trim())
98
+ .filter(Boolean)
99
+ .map((line) => JSON.parse(line));
100
+ }
101
+ catch {
102
+ return {
103
+ state: "tamper_detected",
104
+ keyId: material.keyId,
105
+ signedAt: material.signedAt,
106
+ loopRecordSha256: material.loopRecordSha256,
107
+ ledgerSha256: material.ledgerSha256,
108
+ ledgerHeadHash: material.ledgerHeadHash,
109
+ entryCount: material.entryCount,
110
+ reason: "ledger_entry_parse_error"
111
+ };
112
+ }
113
+ const actualChain = buildReceiptIntegrityChain(parsedLedgerEntries);
114
+ if (actualChain.length !== material.chain.length) {
115
+ return {
116
+ state: "tamper_detected",
117
+ keyId: material.keyId,
118
+ signedAt: material.signedAt,
119
+ loopRecordSha256: material.loopRecordSha256,
120
+ ledgerSha256: material.ledgerSha256,
121
+ ledgerHeadHash: material.ledgerHeadHash,
122
+ entryCount: material.entryCount,
123
+ reason: "ledger_entry_count_mismatch"
124
+ };
125
+ }
126
+ for (let index = 0; index < actualChain.length; index += 1) {
127
+ const expected = material.chain[index];
128
+ const actual = actualChain[index];
129
+ if (!expected ||
130
+ !actual ||
131
+ expected.entryHash !== actual.entryHash ||
132
+ expected.prevHash !== actual.prevHash) {
133
+ return {
134
+ state: "tamper_detected",
135
+ keyId: material.keyId,
136
+ signedAt: material.signedAt,
137
+ loopRecordSha256: material.loopRecordSha256,
138
+ ledgerSha256: material.ledgerSha256,
139
+ ledgerHeadHash: material.ledgerHeadHash,
140
+ entryCount: material.entryCount,
141
+ reason: `ledger_chain_mismatch:${index}`
142
+ };
143
+ }
144
+ }
145
+ const signatureBase = {
146
+ schemaVersion: material.schemaVersion,
147
+ runId: material.runId,
148
+ keyId: material.keyId,
149
+ signedAt: material.signedAt,
150
+ ...(material.scope ? { scope: material.scope } : {}),
151
+ loopRecordSha256: material.loopRecordSha256,
152
+ ledgerSha256: material.ledgerSha256,
153
+ ledgerHeadHash: material.ledgerHeadHash,
154
+ entryCount: material.entryCount,
155
+ chain: material.chain
156
+ };
157
+ const expectedSignature = createReceiptIntegritySignature(key, signatureBase);
158
+ if (expectedSignature !== material.signatureHmacSha256) {
159
+ return {
160
+ state: "tamper_detected",
161
+ keyId: material.keyId,
162
+ signedAt: material.signedAt,
163
+ loopRecordSha256: material.loopRecordSha256,
164
+ ledgerSha256: material.ledgerSha256,
165
+ ledgerHeadHash: material.ledgerHeadHash,
166
+ entryCount: material.entryCount,
167
+ reason: "signature_mismatch"
168
+ };
169
+ }
170
+ return {
171
+ state: "verified",
172
+ keyId: material.keyId,
173
+ signedAt: material.signedAt,
174
+ loopRecordSha256: material.loopRecordSha256,
175
+ ledgerSha256: material.ledgerSha256,
176
+ ledgerHeadHash: material.ledgerHeadHash,
177
+ entryCount: material.entryCount
178
+ };
179
+ }
180
+ export function resolveReceiptIntegrityPath(runsRoot, runId) {
181
+ return join(runsRoot, runId, "receipt-integrity.json");
182
+ }
183
+ function buildReceiptIntegrityChain(entries) {
184
+ let previousHash = "root";
185
+ return entries.map((entry, index) => {
186
+ const normalizedEntry = JSON.stringify(entry);
187
+ const entryHash = sha256(`${previousHash}\n${normalizedEntry}`);
188
+ const candidate = entry;
189
+ const chainEntry = {
190
+ index,
191
+ prevHash: previousHash,
192
+ entryHash
193
+ };
194
+ if (typeof candidate.eventId === "string") {
195
+ chainEntry.eventId = candidate.eventId;
196
+ }
197
+ if (typeof candidate.type === "string") {
198
+ chainEntry.type = candidate.type;
199
+ }
200
+ if (typeof candidate.kind === "string") {
201
+ chainEntry.kind = candidate.kind;
202
+ }
203
+ if (typeof candidate.timestamp === "string") {
204
+ chainEntry.timestamp = candidate.timestamp;
205
+ }
206
+ previousHash = entryHash;
207
+ return chainEntry;
208
+ });
209
+ }
210
+ function createReceiptIntegritySignature(key, material) {
211
+ return createHmac("sha256", key).update(JSON.stringify(material)).digest("hex");
212
+ }
213
+ async function ensureReceiptIntegrityKey(runsRoot, runId) {
214
+ const keyPath = resolveReceiptIntegrityKeyPath(runsRoot, runId);
215
+ const existing = await readFile(keyPath, "utf8").catch(() => null);
216
+ if (existing) {
217
+ const trimmed = existing.trim();
218
+ return [trimmed, sha256(trimmed).slice(0, 16)];
219
+ }
220
+ const generated = randomBytes(32).toString("hex");
221
+ try {
222
+ await mkdir(dirname(keyPath), { recursive: true, mode: RECEIPT_INTEGRITY_KEY_DIR_MODE });
223
+ await writeFile(keyPath, `${generated}\n`, {
224
+ encoding: "utf8",
225
+ mode: RECEIPT_INTEGRITY_KEY_FILE_MODE
226
+ });
227
+ }
228
+ catch {
229
+ return undefined;
230
+ }
231
+ return [generated, sha256(generated).slice(0, 16)];
232
+ }
233
+ async function readReceiptIntegrityKey(runsRoot, runId) {
234
+ const raw = await readFile(resolveReceiptIntegrityKeyPath(runsRoot, runId), "utf8");
235
+ return raw.trim();
236
+ }
237
+ function resolveReceiptIntegrityKeyPath(runsRoot, runId) {
238
+ const rootHash = sha256(runsRoot).slice(0, 16);
239
+ return join(resolveReceiptIntegrityRoot(), rootHash, `${runId}.key`);
240
+ }
241
+ function serializeStoredJson(value) {
242
+ return `${JSON.stringify(value, null, 2)}\n`;
243
+ }
244
+ function serializeStoredJsonl(entries) {
245
+ if (entries.length === 0) {
246
+ return "";
247
+ }
248
+ return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
249
+ }
250
+ function sha256(value) {
251
+ return createHash("sha256").update(value).digest("hex");
252
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Ledger event types for the Phase 3 persistence stack.
3
+ * These are WRITE-side events that go into ledger.jsonl — separate from the
4
+ * read-model LoopEventType used by the control plane.
5
+ */
6
+ export type LedgerEventKind = "contract.created" | "attempt.admitted" | "attempt.rejected" | "prompt.compiled" | "patch.generated" | "verification.completed" | "grounding.violations_found" | "safety.violations_found" | "budget.settled" | "attempt.kept" | "attempt.discarded" | "run.exited";
7
+ export interface LedgerEvent {
8
+ kind: LedgerEventKind;
9
+ runId: string;
10
+ attemptIndex?: number;
11
+ timestamp: string;
12
+ payload: Record<string, unknown>;
13
+ }
14
+ export interface LedgerEventDraft {
15
+ kind: LedgerEventKind;
16
+ runId: string;
17
+ attemptIndex?: number;
18
+ payload: Record<string, unknown>;
19
+ timestamp?: string;
20
+ }
21
+ export declare function makeLedgerEvent(draft: LedgerEventDraft, options?: {
22
+ now?: string;
23
+ }): LedgerEvent;
@@ -0,0 +1,9 @@
1
+ export function makeLedgerEvent(draft, options = {}) {
2
+ return {
3
+ kind: draft.kind,
4
+ runId: draft.runId,
5
+ timestamp: draft.timestamp ?? options.now ?? new Date().toISOString(),
6
+ payload: draft.payload,
7
+ ...(draft.attemptIndex !== undefined ? { attemptIndex: draft.attemptIndex } : {})
8
+ };
9
+ }
@@ -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,66 @@
1
+ /**
2
+ * Reads completed loop records from ~/.martin/runs/ for analysis.
3
+ * Used by the Trust Calibration Engine and other offline analytics.
4
+ *
5
+ * Supports both storage layouts:
6
+ * - legacy root JSONL files: <runsRoot>/*.jsonl
7
+ * - canonical run trees: <runsRoot>/<loopId>/loop-record.json
8
+ */
9
+ export interface LoopAttemptRecord {
10
+ index: number;
11
+ model?: string;
12
+ adapterId?: string;
13
+ failureClass?: string;
14
+ intervention?: string;
15
+ startedAt?: string;
16
+ completedAt?: string;
17
+ }
18
+ export interface LoopRunRecord {
19
+ loopId: string;
20
+ status: string;
21
+ lifecycleState: string;
22
+ createdAt: string;
23
+ updatedAt: string;
24
+ budget: {
25
+ maxUsd: number;
26
+ softLimitUsd: number;
27
+ maxIterations: number;
28
+ maxTokens: number;
29
+ };
30
+ cost: {
31
+ actualUsd: number;
32
+ tokensIn: number;
33
+ tokensOut: number;
34
+ avoidedUsd?: number;
35
+ };
36
+ attempts: LoopAttemptRecord[];
37
+ task: {
38
+ title: string;
39
+ objective: string;
40
+ };
41
+ }
42
+ export interface LoopRecordsRollup {
43
+ generatedAt: string;
44
+ totalRuns: number;
45
+ statusBreakdown: Record<string, number>;
46
+ lifecycleBreakdown: Record<string, number>;
47
+ latestByLoopId: Record<string, {
48
+ status: string;
49
+ lifecycleState: string;
50
+ updatedAt: string;
51
+ costUsd: number;
52
+ attempts: number;
53
+ }>;
54
+ }
55
+ export declare function readLoopRecordsFromFile(file: string): Promise<LoopRunRecord[]>;
56
+ export declare function readLatestLoopRecordFromFile(file: string): Promise<LoopRunRecord | null>;
57
+ /**
58
+ * Reads all loop records from the given directory (default: ~/.martin/runs/).
59
+ * Returns an empty array if the directory doesn't exist or has no records.
60
+ */
61
+ export declare function readAllLoopRecords(runsDir?: string): Promise<LoopRunRecord[]>;
62
+ /**
63
+ * Returns the most recently updated loop record, or null if none exist.
64
+ */
65
+ export declare function readLatestLoopRecord(runsDir?: string): Promise<LoopRunRecord | null>;
66
+ export declare function buildLoopRecordsRollup(records: LoopRunRecord[]): LoopRecordsRollup;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Reads completed loop records from ~/.martin/runs/ for analysis.
3
+ * Used by the Trust Calibration Engine and other offline analytics.
4
+ *
5
+ * Supports both storage layouts:
6
+ * - legacy root JSONL files: <runsRoot>/*.jsonl
7
+ * - canonical run trees: <runsRoot>/<loopId>/loop-record.json
8
+ */
9
+ import { readFile, readdir } from "node:fs/promises";
10
+ import { homedir } from "node:os";
11
+ import { extname, join } from "node:path";
12
+ export async function readLoopRecordsFromFile(file) {
13
+ const text = await readFile(file, "utf8");
14
+ const extension = extname(file).toLowerCase();
15
+ if (extension === ".jsonl") {
16
+ return text
17
+ .split(/\r?\n/u)
18
+ .map((line) => line.trim())
19
+ .filter(Boolean)
20
+ .map((line) => JSON.parse(line));
21
+ }
22
+ const parsed = JSON.parse(text);
23
+ return Array.isArray(parsed) ? parsed : [parsed];
24
+ }
25
+ export async function readLatestLoopRecordFromFile(file) {
26
+ const records = await readLoopRecordsFromFile(file);
27
+ if (records.length === 0)
28
+ return null;
29
+ return records.reduce((latest, record) => {
30
+ const currentTimestamp = new Date(record.updatedAt ?? record.createdAt).getTime();
31
+ const latestTimestamp = new Date(latest.updatedAt ?? latest.createdAt).getTime();
32
+ return currentTimestamp > latestTimestamp ? record : latest;
33
+ }, records[0]);
34
+ }
35
+ /**
36
+ * Reads all loop records from the given directory (default: ~/.martin/runs/).
37
+ * Returns an empty array if the directory doesn't exist or has no records.
38
+ */
39
+ export async function readAllLoopRecords(runsDir) {
40
+ const dir = runsDir ?? join(homedir(), ".martin", "runs");
41
+ let entries;
42
+ try {
43
+ entries = await readdir(dir, { withFileTypes: true });
44
+ }
45
+ catch {
46
+ return [];
47
+ }
48
+ const recordsByLoopId = new Map();
49
+ const jsonlFiles = entries
50
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
51
+ .map((entry) => entry.name);
52
+ for (const file of jsonlFiles) {
53
+ try {
54
+ const fromFile = await readLoopRecordsFromFile(join(dir, file));
55
+ for (const record of fromFile) {
56
+ ingestRecord(recordsByLoopId, record, "legacy_jsonl");
57
+ }
58
+ }
59
+ catch {
60
+ // skip malformed files or lines
61
+ }
62
+ }
63
+ const runDirectories = entries.filter((entry) => entry.isDirectory());
64
+ for (const entry of runDirectories) {
65
+ try {
66
+ const canonical = await readLoopRecordsFromFile(join(dir, entry.name, "loop-record.json"));
67
+ for (const record of canonical) {
68
+ ingestRecord(recordsByLoopId, record, "canonical_tree");
69
+ }
70
+ }
71
+ catch {
72
+ // skip missing or malformed canonical records
73
+ }
74
+ }
75
+ return [...recordsByLoopId.values()].map((entry) => entry.record);
76
+ }
77
+ /**
78
+ * Returns the most recently updated loop record, or null if none exist.
79
+ */
80
+ export async function readLatestLoopRecord(runsDir) {
81
+ const records = await readAllLoopRecords(runsDir);
82
+ if (records.length === 0)
83
+ return null;
84
+ return records.reduce((latest, r) => {
85
+ const a = new Date(r.updatedAt ?? r.createdAt).getTime();
86
+ const b = new Date(latest.updatedAt ?? latest.createdAt).getTime();
87
+ return a > b ? r : latest;
88
+ }, records[0]);
89
+ }
90
+ export function buildLoopRecordsRollup(records) {
91
+ const statusBreakdown = {};
92
+ const lifecycleBreakdown = {};
93
+ const latestByLoopId = {};
94
+ for (const record of records) {
95
+ statusBreakdown[record.status] = (statusBreakdown[record.status] ?? 0) + 1;
96
+ lifecycleBreakdown[record.lifecycleState] = (lifecycleBreakdown[record.lifecycleState] ?? 0) + 1;
97
+ latestByLoopId[record.loopId] = {
98
+ status: record.status,
99
+ lifecycleState: record.lifecycleState,
100
+ updatedAt: record.updatedAt,
101
+ costUsd: record.cost.actualUsd,
102
+ attempts: record.attempts.length
103
+ };
104
+ }
105
+ return {
106
+ generatedAt: new Date().toISOString(),
107
+ totalRuns: records.length,
108
+ statusBreakdown,
109
+ lifecycleBreakdown,
110
+ latestByLoopId
111
+ };
112
+ }
113
+ function ingestRecord(recordsByLoopId, record, source) {
114
+ const existing = recordsByLoopId.get(record.loopId);
115
+ if (!existing) {
116
+ recordsByLoopId.set(record.loopId, { record, source });
117
+ return;
118
+ }
119
+ const candidateTimestamp = resolveRecordTimestamp(record);
120
+ const existingTimestamp = resolveRecordTimestamp(existing.record);
121
+ if (candidateTimestamp > existingTimestamp) {
122
+ recordsByLoopId.set(record.loopId, { record, source });
123
+ return;
124
+ }
125
+ if (candidateTimestamp === existingTimestamp &&
126
+ sourcePrecedence(source) > sourcePrecedence(existing.source)) {
127
+ recordsByLoopId.set(record.loopId, { record, source });
128
+ }
129
+ }
130
+ function resolveRecordTimestamp(record) {
131
+ const updated = Date.parse(record.updatedAt ?? "");
132
+ if (Number.isFinite(updated)) {
133
+ return updated;
134
+ }
135
+ const created = Date.parse(record.createdAt ?? "");
136
+ return Number.isFinite(created) ? created : 0;
137
+ }
138
+ function sourcePrecedence(source) {
139
+ return source === "canonical_tree" ? 2 : 1;
140
+ }