@martinloop/mcp 0.3.2 → 0.3.3
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/dist/prompts.d.ts +2 -1
- package/dist/resources.d.ts +2 -1
- package/dist/server-validation.js +3 -2
- package/dist/server.js +4 -3
- package/dist/tools/run-loop.d.ts +1 -1
- package/dist/tools/run-store.js +165 -5
- package/dist/tools/tool-support.d.ts +2 -1
- package/dist/tools/tool-support.js +1 -0
- package/dist/vendor/adapters/claude-cli.d.ts +1 -1
- package/dist/vendor/adapters/claude-cli.js +2 -1
- package/dist/vendor/adapters/cli-bridge.js +29 -5
- package/dist/vendor/adapters/openai-compatible.d.ts +1 -1
- package/dist/vendor/adapters/openai-compatible.js +1 -1
- package/dist/vendor/adapters/verifier-only.js +1 -1
- package/dist/vendor/core/context-integrity.js +1 -1
- package/dist/vendor/core/grounding.js +1 -1
- package/dist/vendor/core/index.d.ts +2 -2
- package/dist/vendor/core/index.js +25 -27
- package/dist/vendor/core/leash.js +1 -1
- package/dist/vendor/core/persistence/integrity.d.ts +1 -0
- package/dist/vendor/core/persistence/integrity.js +5 -1
- package/dist/vendor/core/persistence/runs-reader.d.ts +14 -0
- package/dist/vendor/core/persistence/runs-reader.js +61 -4
- package/dist/vendor/core/persistence/store.js +13 -0
- package/dist/vendor/core/rollback.d.ts +4 -0
- package/dist/vendor/core/rollback.js +15 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/prompts.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { GetPromptResult, Prompt } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import type { MartinEngine } from "./tools/tool-support.js";
|
|
2
3
|
export declare const MARTIN_PROMPTS: Prompt[];
|
|
3
4
|
export interface MartinGetPromptInput {
|
|
4
5
|
name: string;
|
|
5
6
|
arguments?: Record<string, string>;
|
|
6
7
|
runsDir?: string;
|
|
7
8
|
workingDirectory?: string;
|
|
8
|
-
engine?:
|
|
9
|
+
engine?: MartinEngine;
|
|
9
10
|
}
|
|
10
11
|
export declare function listMartinPrompts(): {
|
|
11
12
|
prompts: Prompt[];
|
package/dist/resources.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReadResourceResult, Resource, ResourceTemplate } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import type { MartinEngine } from "./tools/tool-support.js";
|
|
2
3
|
export declare const MARTIN_STATIC_RESOURCE_URIS: {
|
|
3
4
|
readonly serverHealth: "martin://server/health";
|
|
4
5
|
readonly recentRuns: "martin://runs/recent";
|
|
@@ -26,7 +27,7 @@ export interface MartinReadResourceInput {
|
|
|
26
27
|
uri: string;
|
|
27
28
|
runsDir?: string;
|
|
28
29
|
workingDirectory?: string;
|
|
29
|
-
engine?:
|
|
30
|
+
engine?: MartinEngine;
|
|
30
31
|
}
|
|
31
32
|
export declare function listMartinResources(): {
|
|
32
33
|
resources: Resource[];
|
|
@@ -2,6 +2,7 @@ import { existsSync, lstatSync, realpathSync } from "node:fs";
|
|
|
2
2
|
import { dirname, extname, isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { resolveRunsRoot } from "./vendor/core/index.js";
|
|
4
4
|
import { invalidArgumentsError, invalidPathError, invalidSelectorError } from "./tools/tool-errors.js";
|
|
5
|
+
import { MARTIN_ENGINE_VALUES } from "./tools/tool-support.js";
|
|
5
6
|
export { sanitizeToolErrorMessage } from "./tools/tool-errors.js";
|
|
6
7
|
export function validateToolInput(name, args) {
|
|
7
8
|
switch (name) {
|
|
@@ -156,7 +157,7 @@ function validateRunInput(args) {
|
|
|
156
157
|
"workspaceId",
|
|
157
158
|
"projectId"
|
|
158
159
|
]);
|
|
159
|
-
const engine = optionalEnum(record.engine, "engine",
|
|
160
|
+
const engine = optionalEnum(record.engine, "engine", MARTIN_ENGINE_VALUES);
|
|
160
161
|
return {
|
|
161
162
|
objective: requireString(record.objective, "objective"),
|
|
162
163
|
...(record.workingDirectory !== undefined
|
|
@@ -246,7 +247,7 @@ function validateDoctorInput(args) {
|
|
|
246
247
|
...(record.runsDir !== undefined
|
|
247
248
|
? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
|
|
248
249
|
: {}),
|
|
249
|
-
...optionalEnumAsObject(record.engine, "engine",
|
|
250
|
+
...optionalEnumAsObject(record.engine, "engine", MARTIN_ENGINE_VALUES)
|
|
250
251
|
};
|
|
251
252
|
}
|
|
252
253
|
function validatePreflightInput(args) {
|
package/dist/server.js
CHANGED
|
@@ -42,6 +42,7 @@ import { martinRunDossierTool } from "./tools/run-dossier.js";
|
|
|
42
42
|
import { createRunControlReceipt } from "./tools/run-controls.js";
|
|
43
43
|
import { martinTriageRunsTool } from "./tools/triage-runs.js";
|
|
44
44
|
import { runLoopTool } from "./tools/run-loop.js";
|
|
45
|
+
import { MARTIN_ENGINE_VALUES } from "./tools/tool-support.js";
|
|
45
46
|
import { createToolErrorResult, createToolSuccessResult } from "./tools/tool-response.js";
|
|
46
47
|
import { MartinToolError, toToolFailure } from "./tools/tool-errors.js";
|
|
47
48
|
import { normalizeLoopBudget } from "./tools/workflow-governance.js";
|
|
@@ -739,7 +740,7 @@ export function createMartinMcpServer(serverInfo) {
|
|
|
739
740
|
},
|
|
740
741
|
engine: {
|
|
741
742
|
type: "string",
|
|
742
|
-
enum: [
|
|
743
|
+
enum: [...MARTIN_ENGINE_VALUES],
|
|
743
744
|
description: "Which agent CLI to use. Defaults to claude."
|
|
744
745
|
},
|
|
745
746
|
model: {
|
|
@@ -871,7 +872,7 @@ export function createMartinMcpServer(serverInfo) {
|
|
|
871
872
|
},
|
|
872
873
|
engine: {
|
|
873
874
|
type: "string",
|
|
874
|
-
enum: [
|
|
875
|
+
enum: [...MARTIN_ENGINE_VALUES],
|
|
875
876
|
description: "Optional engine to highlight in diagnostics."
|
|
876
877
|
}
|
|
877
878
|
}
|
|
@@ -934,7 +935,7 @@ export function createMartinMcpServer(serverInfo) {
|
|
|
934
935
|
},
|
|
935
936
|
engine: {
|
|
936
937
|
type: "string",
|
|
937
|
-
enum: [
|
|
938
|
+
enum: [...MARTIN_ENGINE_VALUES],
|
|
938
939
|
description: "Which agent CLI would be used. Defaults to claude."
|
|
939
940
|
},
|
|
940
941
|
model: {
|
package/dist/tools/run-loop.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { buildArtifactSummary, buildVerificationSummary, buildLoopPreview, type
|
|
|
5
5
|
export interface RunLoopInput {
|
|
6
6
|
objective: string;
|
|
7
7
|
workingDirectory?: string;
|
|
8
|
-
engine?:
|
|
8
|
+
engine?: MartinEngine;
|
|
9
9
|
model?: string;
|
|
10
10
|
maxUsd?: number;
|
|
11
11
|
maxIterations?: number;
|
package/dist/tools/run-store.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { readFile, readdir, stat } from "node:fs/promises";
|
|
1
|
+
import { open, readFile, readdir, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, verifyReceiptIntegrityFromFiles } from "../vendor/core/index.js";
|
|
4
4
|
import { resolveSafeLoopRecordPath, resolveSafeRunsJsonPath, resolveSafeRunsPath, resolveSafeRunsRootPath } from "../server-validation.js";
|
|
5
5
|
import { attemptNotFoundError, invalidSelectorError, noLoopRecordsError, storeUnreadableError } from "./tool-errors.js";
|
|
6
|
+
const RUN_INDEX_FILENAME = "run-index.ndjson";
|
|
7
|
+
const RUN_INDEX_READ_MAX_BYTES = 2 * 1024 * 1024;
|
|
6
8
|
async function attachReceiptIntegrity(detail) {
|
|
7
9
|
const ledgerPath = detail.canonicalRunDirectory
|
|
8
10
|
? await resolveReceiptEvidencePath(detail.canonicalRunDirectory)
|
|
@@ -17,10 +19,15 @@ async function attachReceiptIntegrity(detail) {
|
|
|
17
19
|
state: "unsigned",
|
|
18
20
|
reason: "Receipt integrity verification could not be completed."
|
|
19
21
|
}))
|
|
20
|
-
:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
: detail.canonicalLoopRecordPath && detail.canonicalRunDirectory
|
|
23
|
+
? ({
|
|
24
|
+
state: "unsigned",
|
|
25
|
+
reason: "Receipt integrity material is incomplete for this canonical run."
|
|
26
|
+
})
|
|
27
|
+
: ({
|
|
28
|
+
state: "unsigned",
|
|
29
|
+
reason: "Receipt integrity is only available for canonical run directories."
|
|
30
|
+
});
|
|
24
31
|
const receiptScope = resolveReceiptScope(detail.loop, detail.runsRoot);
|
|
25
32
|
return {
|
|
26
33
|
...detail,
|
|
@@ -107,6 +114,20 @@ export async function loadLoopRecordForStatus(input) {
|
|
|
107
114
|
}
|
|
108
115
|
export async function listLoopRecords(input) {
|
|
109
116
|
const runsRoot = resolveSafeRunsRootPath(input.runsDir, resolveRunsRoot(process.env));
|
|
117
|
+
const indexed = await listLoopsFromRunIndex(runsRoot, input);
|
|
118
|
+
if (indexed.loops.length > 0) {
|
|
119
|
+
const loops = indexed.loops;
|
|
120
|
+
const warnings = [...indexed.warnings];
|
|
121
|
+
if (loops.length === 0) {
|
|
122
|
+
warnings.push("No loop records matched the current filters.");
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
source: runsRoot,
|
|
126
|
+
runsRoot,
|
|
127
|
+
loops,
|
|
128
|
+
warnings
|
|
129
|
+
};
|
|
130
|
+
}
|
|
110
131
|
const inspected = await readAllLoopRecordsSafely(runsRoot);
|
|
111
132
|
const warnings = [...inspected.warnings];
|
|
112
133
|
const updatedAfterTimestamp = input.updatedAfter !== undefined ? new Date(input.updatedAfter).getTime() : undefined;
|
|
@@ -248,6 +269,15 @@ export async function loadDetailedLoopRecord(input) {
|
|
|
248
269
|
warnings: [...detail.warnings, ...inspected.warnings]
|
|
249
270
|
});
|
|
250
271
|
}
|
|
272
|
+
const indexedLatest = await loadLatestLoopFromRunIndex(runsRoot);
|
|
273
|
+
if (indexedLatest) {
|
|
274
|
+
return await attachReceiptIntegrity(await buildDetailedLoopSourceFromDiscoveredLoop({
|
|
275
|
+
source: runsRoot,
|
|
276
|
+
sourceKind: "latest",
|
|
277
|
+
runsRoot,
|
|
278
|
+
loop: indexedLatest.loop
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
251
281
|
const inspected = await readAllLoopRecordsSafely(runsRoot);
|
|
252
282
|
const loop = inspected.loops[0];
|
|
253
283
|
if (!loop) {
|
|
@@ -264,6 +294,129 @@ export async function loadDetailedLoopRecord(input) {
|
|
|
264
294
|
warnings: [...detail.warnings, ...inspected.warnings]
|
|
265
295
|
});
|
|
266
296
|
}
|
|
297
|
+
async function listLoopsFromRunIndex(runsRoot, input) {
|
|
298
|
+
const indexed = await readRunIndexEntries(runsRoot);
|
|
299
|
+
if (indexed.entries.length === 0) {
|
|
300
|
+
return { loops: [], warnings: [] };
|
|
301
|
+
}
|
|
302
|
+
const warnings = [];
|
|
303
|
+
const updatedAfterTimestamp = input.updatedAfter !== undefined ? new Date(input.updatedAfter).getTime() : undefined;
|
|
304
|
+
if (input.updatedAfter !== undefined &&
|
|
305
|
+
(!Number.isFinite(updatedAfterTimestamp) || Number.isNaN(updatedAfterTimestamp))) {
|
|
306
|
+
throw invalidSelectorError("Invalid updatedAfter.", "Provide updatedAfter as an ISO-8601 timestamp.");
|
|
307
|
+
}
|
|
308
|
+
const deduped = dedupeRunIndexEntries(indexed.entries);
|
|
309
|
+
const loops = [];
|
|
310
|
+
const limit = input.limit ?? 20;
|
|
311
|
+
const maxLookups = Math.max(limit * 5, 100);
|
|
312
|
+
for (const entry of deduped) {
|
|
313
|
+
if (loops.length >= limit || loops.length >= maxLookups) {
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
if (input.status && entry.status !== input.status) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (input.lifecycleState && entry.lifecycleState !== input.lifecycleState) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (updatedAfterTimestamp !== undefined) {
|
|
323
|
+
const timestamp = Date.parse(entry.updatedAt);
|
|
324
|
+
if (!Number.isFinite(timestamp) || timestamp <= updatedAfterTimestamp) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const loop = await loadLoopFromIndexEntry(runsRoot, entry.loopId).catch(() => null);
|
|
329
|
+
if (!loop) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (input.adapterId && !loop.attempts.some((attempt) => attempt.adapterId === input.adapterId)) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (input.model && !loop.attempts.some((attempt) => attempt.model === input.model)) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
loops.push(loop);
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
loops: loops
|
|
342
|
+
.sort((left, right) => timestampForLoop(right) - timestampForLoop(left))
|
|
343
|
+
.slice(0, limit),
|
|
344
|
+
warnings
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
async function loadLatestLoopFromRunIndex(runsRoot) {
|
|
348
|
+
const indexed = await readRunIndexEntries(runsRoot);
|
|
349
|
+
if (indexed.entries.length === 0) {
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
for (const entry of dedupeRunIndexEntries(indexed.entries)) {
|
|
353
|
+
const loop = await loadLoopFromIndexEntry(runsRoot, entry.loopId).catch(() => null);
|
|
354
|
+
if (loop) {
|
|
355
|
+
return { loop };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
async function loadLoopFromIndexEntry(runsRoot, loopId) {
|
|
361
|
+
const canonicalLoopRecordPath = resolvePotentialLoopRecordPath(loopId, runsRoot);
|
|
362
|
+
const canonicalStats = await safeStat(canonicalLoopRecordPath);
|
|
363
|
+
if (!canonicalStats?.isFile()) {
|
|
364
|
+
throw noLoopRecordsError();
|
|
365
|
+
}
|
|
366
|
+
return await readCanonicalLoopRecord(canonicalLoopRecordPath);
|
|
367
|
+
}
|
|
368
|
+
async function readRunIndexEntries(runsRoot) {
|
|
369
|
+
const indexPath = path.join(runsRoot, RUN_INDEX_FILENAME);
|
|
370
|
+
const stats = await safeStat(indexPath);
|
|
371
|
+
if (!stats?.isFile()) {
|
|
372
|
+
return { entries: [], truncated: false };
|
|
373
|
+
}
|
|
374
|
+
const size = Number(stats.size);
|
|
375
|
+
const readBytes = Math.min(size, RUN_INDEX_READ_MAX_BYTES);
|
|
376
|
+
const start = Math.max(0, size - readBytes);
|
|
377
|
+
const handle = await open(indexPath, "r");
|
|
378
|
+
try {
|
|
379
|
+
const buffer = Buffer.alloc(readBytes);
|
|
380
|
+
if (readBytes > 0) {
|
|
381
|
+
await handle.read(buffer, 0, readBytes, start);
|
|
382
|
+
}
|
|
383
|
+
const lines = buffer
|
|
384
|
+
.toString("utf8")
|
|
385
|
+
.split(/\r?\n/u)
|
|
386
|
+
.map((line) => line.trim())
|
|
387
|
+
.filter(Boolean);
|
|
388
|
+
const entries = [];
|
|
389
|
+
for (const line of lines) {
|
|
390
|
+
try {
|
|
391
|
+
const parsed = JSON.parse(line);
|
|
392
|
+
if (isRunIndexEntry(parsed)) {
|
|
393
|
+
entries.push(parsed);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
// Ignore partial tail lines.
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
entries.sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
|
|
401
|
+
return {
|
|
402
|
+
entries,
|
|
403
|
+
truncated: size > RUN_INDEX_READ_MAX_BYTES
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
await handle.close();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function dedupeRunIndexEntries(entries) {
|
|
411
|
+
const byLoopId = new Map();
|
|
412
|
+
for (const entry of entries) {
|
|
413
|
+
const existing = byLoopId.get(entry.loopId);
|
|
414
|
+
if (!existing || Date.parse(entry.updatedAt) >= Date.parse(existing.updatedAt)) {
|
|
415
|
+
byLoopId.set(entry.loopId, entry);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return [...byLoopId.values()].sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
|
|
419
|
+
}
|
|
267
420
|
export async function loadAttemptFromLoop(input) {
|
|
268
421
|
const detail = await loadDetailedLoopRecord(input);
|
|
269
422
|
const attempt = input.attemptIndex !== undefined
|
|
@@ -510,3 +663,10 @@ function isRecord(value) {
|
|
|
510
663
|
function isFiniteNumber(value) {
|
|
511
664
|
return typeof value === "number" && Number.isFinite(value);
|
|
512
665
|
}
|
|
666
|
+
function isRunIndexEntry(value) {
|
|
667
|
+
return (isRecord(value) &&
|
|
668
|
+
typeof value["loopId"] === "string" &&
|
|
669
|
+
value["loopId"].trim().length > 0 &&
|
|
670
|
+
typeof value["updatedAt"] === "string" &&
|
|
671
|
+
value["updatedAt"].trim().length > 0);
|
|
672
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LoopArtifact, LoopBudget, LoopCost, LoopEvent, LoopTask, ReceiptIntegritySummary, ReceiptScope } from "../vendor/contracts/index.js";
|
|
2
2
|
import { type LedgerEvent, type LoopAttemptRecord, type LoopRunRecord } from "../vendor/core/index.js";
|
|
3
|
-
export
|
|
3
|
+
export declare const MARTIN_ENGINE_VALUES: readonly ["claude", "codex", "gemini"];
|
|
4
|
+
export type MartinEngine = (typeof MARTIN_ENGINE_VALUES)[number];
|
|
4
5
|
export interface InspectableLoopAttempt extends LoopAttemptRecord {
|
|
5
6
|
attemptId?: string;
|
|
6
7
|
summary?: string;
|
|
@@ -3,6 +3,7 @@ import { readdir, stat } from "node:fs/promises";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { evaluateCostGovernor, resolveRunsRoot } from "../vendor/core/index.js";
|
|
5
5
|
import { readAllLoopRecordsSafely } from "./run-store.js";
|
|
6
|
+
export const MARTIN_ENGINE_VALUES = ["claude", "codex", "gemini"];
|
|
6
7
|
const CLI_CACHE_TTL_MS = 60_000;
|
|
7
8
|
const RUN_STORE_CACHE_TTL_MS = 5_000;
|
|
8
9
|
const cliAvailabilityCache = new Map();
|
|
@@ -33,7 +33,7 @@ export interface AgentCliAdapterOptions {
|
|
|
33
33
|
workingDirectory?: string;
|
|
34
34
|
/** Timeout for the agent subprocess in ms. Defaults to 300_000 (5 min). */
|
|
35
35
|
timeoutMs?: number;
|
|
36
|
-
/** Timeout per verification command in ms. Defaults to
|
|
36
|
+
/** Timeout per verification command in ms. Defaults to 120_000 (2 min). */
|
|
37
37
|
verifyTimeoutMs?: number;
|
|
38
38
|
/** Human-readable label shown in loop records. */
|
|
39
39
|
label?: string;
|
|
@@ -349,7 +349,7 @@ function inferStructuralClassHint(agentOutput, verificationSummary, exitCode, ob
|
|
|
349
349
|
export function createAgentCliAdapter(options) {
|
|
350
350
|
const workingDirectory = options.workingDirectory ?? process.cwd();
|
|
351
351
|
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
352
|
-
const verifyTimeoutMs = options.verifyTimeoutMs ??
|
|
352
|
+
const verifyTimeoutMs = options.verifyTimeoutMs ?? 120_000;
|
|
353
353
|
const adapterId = `agent-cli:${options.adapterIdSuffix ?? options.command}`;
|
|
354
354
|
const supportsJsonOutput = options.supportsJsonOutput === true;
|
|
355
355
|
const supportsUsageSettlement = supportsJsonOutput || options.command === "codex" || options.command === "gemini";
|
|
@@ -976,6 +976,7 @@ function redactSecretsForPrompt(input) {
|
|
|
976
976
|
.replace(/\bAIza[0-9A-Za-z_-]{30,}\b/gu, "[REDACTED_SECRET]")
|
|
977
977
|
.replace(/-----BEGIN(?:\s+[A-Z0-9]+)*\s+PRIVATE KEY-----[\s\S]*?-----END(?:\s+[A-Z0-9]+)*\s+PRIVATE KEY-----/gu, "[REDACTED_SECRET]")
|
|
978
978
|
.replace(/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/gu, "[REDACTED_SECRET]")
|
|
979
|
+
.replace(/\b(?:api[_-]?key|secret|password|token)\s*[:=]\s*["']?[A-Za-z0-9_\-/+=]{8,}["']?/giu, "[REDACTED_SECRET]")
|
|
979
980
|
.replace(/\B\.env(?!\.example\b)(?:\.[A-Za-z0-9._-]+)?\b/giu, "[REDACTED_PATH]");
|
|
980
981
|
}
|
|
981
982
|
function extractStructuredErrors(stderr, stdout) {
|
|
@@ -9,6 +9,7 @@ export async function runSubprocess(command, args, options) {
|
|
|
9
9
|
let outputCapped = false;
|
|
10
10
|
let terminationReason;
|
|
11
11
|
let settled = false;
|
|
12
|
+
let exited = false;
|
|
12
13
|
let outputBytes = 0;
|
|
13
14
|
const stdoutChunks = [];
|
|
14
15
|
const stderrChunks = [];
|
|
@@ -35,12 +36,12 @@ export async function runSubprocess(command, args, options) {
|
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
38
|
const trackOutput = (chunks, chunk) => {
|
|
39
|
+
if (outputCapped || timedOut || terminationReason) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
38
42
|
chunks.push(chunk);
|
|
39
43
|
outputBytes += chunk.byteLength;
|
|
40
|
-
if (options.maxOutputBytes !== undefined &&
|
|
41
|
-
!outputCapped &&
|
|
42
|
-
!timedOut &&
|
|
43
|
-
outputBytes > options.maxOutputBytes) {
|
|
44
|
+
if (options.maxOutputBytes !== undefined && outputBytes > options.maxOutputBytes) {
|
|
44
45
|
outputCapped = true;
|
|
45
46
|
proc.kill("SIGTERM");
|
|
46
47
|
}
|
|
@@ -53,10 +54,23 @@ export async function runSubprocess(command, args, options) {
|
|
|
53
54
|
proc.kill("SIGTERM");
|
|
54
55
|
};
|
|
55
56
|
proc.stdout?.on("data", (chunk) => {
|
|
57
|
+
if (outputCapped || timedOut || terminationReason) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
56
60
|
trackOutput(stdoutChunks, chunk);
|
|
57
|
-
options.onStdoutChunk
|
|
61
|
+
if (options.onStdoutChunk) {
|
|
62
|
+
try {
|
|
63
|
+
options.onStdoutChunk(chunk, terminateEarly);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
terminateEarly(`stdout inspector error: ${error instanceof Error ? error.message : String(error)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
58
69
|
});
|
|
59
70
|
proc.stderr?.on("data", (chunk) => {
|
|
71
|
+
if (outputCapped || timedOut || terminationReason) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
60
74
|
trackOutput(stderrChunks, chunk);
|
|
61
75
|
});
|
|
62
76
|
proc.stdin?.on("error", (error) => {
|
|
@@ -68,9 +82,15 @@ export async function runSubprocess(command, args, options) {
|
|
|
68
82
|
stderrChunks.push(Buffer.from(`${error.message}\n`, "utf8"));
|
|
69
83
|
});
|
|
70
84
|
const timer = setTimeout(() => {
|
|
85
|
+
if (settled || exited || proc.exitCode !== null) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
71
88
|
timedOut = true;
|
|
72
89
|
proc.kill("SIGTERM");
|
|
73
90
|
}, options.timeoutMs);
|
|
91
|
+
proc.on("exit", () => {
|
|
92
|
+
exited = true;
|
|
93
|
+
});
|
|
74
94
|
proc.on("error", (error) => {
|
|
75
95
|
clearTimeout(timer);
|
|
76
96
|
resolveOnce({ exitCode: 1, stdout: "", stderr: error.message, launched: false });
|
|
@@ -200,6 +220,10 @@ export function resolveGitRepositoryRoot(workingDirectory) {
|
|
|
200
220
|
if (cached !== undefined) {
|
|
201
221
|
return cached ?? undefined;
|
|
202
222
|
}
|
|
223
|
+
if (!existsSync(resolvedWorkingDirectory)) {
|
|
224
|
+
gitRepositoryRootCache.set(resolvedWorkingDirectory, null);
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
203
227
|
const visited = [];
|
|
204
228
|
let current = resolvedWorkingDirectory;
|
|
205
229
|
while (true) {
|
|
@@ -43,7 +43,7 @@ export interface OpenAiCompatibleAdapterOptions {
|
|
|
43
43
|
systemPrompt?: string;
|
|
44
44
|
/** Request timeout in milliseconds. Default: 300_000 (5 min). */
|
|
45
45
|
timeoutMs?: number;
|
|
46
|
-
/** Verifier timeout in milliseconds. Default:
|
|
46
|
+
/** Verifier timeout in milliseconds. Default: 120_000. */
|
|
47
47
|
verifyTimeoutMs?: number;
|
|
48
48
|
/** Working directory for git artifact collection and verification. */
|
|
49
49
|
workingDirectory?: string;
|
|
@@ -120,7 +120,7 @@ function buildPrompt(request) {
|
|
|
120
120
|
export function createOpenAiCompatibleAdapter(options) {
|
|
121
121
|
const workingDirectory = options.workingDirectory ?? process.cwd();
|
|
122
122
|
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
123
|
-
const verifyTimeoutMs = options.verifyTimeoutMs ??
|
|
123
|
+
const verifyTimeoutMs = options.verifyTimeoutMs ?? 120_000;
|
|
124
124
|
const systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
125
125
|
const fetchFn = options.fetchImpl ?? globalThis.fetch;
|
|
126
126
|
const runtimeConfig = resolveOpenAiCompatibleRuntimeConfig();
|
|
@@ -2,7 +2,7 @@ import { readGitChangedFiles, runVerification } from "./cli-bridge.js";
|
|
|
2
2
|
import { createAdapterCapabilities, normalizeUsage } from "./runtime-support.js";
|
|
3
3
|
export function createVerifierOnlyAdapter(options = {}) {
|
|
4
4
|
const workingDirectory = options.workingDirectory ?? process.cwd();
|
|
5
|
-
const verifyTimeoutMs = options.verifyTimeoutMs ??
|
|
5
|
+
const verifyTimeoutMs = options.verifyTimeoutMs ?? 120_000;
|
|
6
6
|
return {
|
|
7
7
|
adapterId: "direct:verifier:verify-only",
|
|
8
8
|
kind: "direct-provider",
|
|
@@ -21,7 +21,7 @@ const POISON_PATTERNS = [
|
|
|
21
21
|
*/
|
|
22
22
|
const IDENTITY_REDEFINITION_PATTERNS = [
|
|
23
23
|
/\byou(?:'re|\s+are)\s+now\s+(?:a|an|the)\b(?!\s+(?:martin\s+loop|ai\s+coding\s+agent))/i,
|
|
24
|
-
/\byou(?:'re|\s+are)\s+no\s+longer\s+(
|
|
24
|
+
/\byou(?:'re|\s+are)\s+no\s+longer\s+(?!(?:martin\s+loop|an?\s+ai)\b)/i,
|
|
25
25
|
/\bforget\s+(?:that\s+)?you(?:'re|\s+are)\s+martin\s+loop\b/i,
|
|
26
26
|
/\b(?:pretend|imagine)\s+(?:that\s+)?you(?:'re|\s+are)\b/i,
|
|
27
27
|
/\bact\s+as\s+(?:if\s+you(?:'re|\s+are)\s+)?(?:a|an)\s+(?:different|new|unrestricted|jailbroken)\b/i,
|
|
@@ -13,7 +13,7 @@ const IGNORED_DIRS = new Set([
|
|
|
13
13
|
const MAX_FILE_BYTES = 64_000;
|
|
14
14
|
const MAX_FILES = 500;
|
|
15
15
|
export function resolveGroundingRoot(env = process.env) {
|
|
16
|
-
return env["MARTIN_GROUNDING_DIR"]?.trim()
|
|
16
|
+
return env["MARTIN_GROUNDING_DIR"]?.trim() ||
|
|
17
17
|
join(homedir(), ".martin", "grounding");
|
|
18
18
|
}
|
|
19
19
|
export async function loadOrBuildRepoGroundingIndex(repoRoot) {
|
|
@@ -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
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 };
|
|
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";
|
|
@@ -1,13 +1,12 @@
|
|
|
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
|
-
export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, restoreRollbackBoundary };
|
|
9
|
+
export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, restoreRollbackBoundary };
|
|
11
10
|
// ─── Context Integrity Pre-gate ──────────────────────────────────────────────
|
|
12
11
|
export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
13
12
|
// ─── Prompt packet compiler ──────────────────────────────────────────────────
|
|
@@ -278,7 +277,7 @@ export async function runMartin(input) {
|
|
|
278
277
|
// re-enters subsequent prompts, matching the documented "tool output / test output" scope.
|
|
279
278
|
const priorVerifierOutput = loop.events
|
|
280
279
|
.filter((event) => event.type === "verification.completed")
|
|
281
|
-
.flatMap((event) => event.payload
|
|
280
|
+
.flatMap((event) => event.payload?.steps ?? [])
|
|
282
281
|
.map((step) => step.detail)
|
|
283
282
|
.filter((detail) => Boolean(detail))
|
|
284
283
|
.join("\n---\n");
|
|
@@ -468,7 +467,7 @@ export async function runMartin(input) {
|
|
|
468
467
|
estimatedUsd: roundUsd((loop.cost.estimatedUsd ?? loop.cost.actualUsd) + result.usage.estimatedUsd)
|
|
469
468
|
}
|
|
470
469
|
: {}),
|
|
471
|
-
provenance: getUsageProvenance(result.usage),
|
|
470
|
+
provenance: mergeCostProvenance(loop.cost.provenance, getUsageProvenance(result.usage)),
|
|
472
471
|
...(result.usage.providerSettlement
|
|
473
472
|
? { providerSettlement: result.usage.providerSettlement }
|
|
474
473
|
: {})
|
|
@@ -552,7 +551,7 @@ export async function runMartin(input) {
|
|
|
552
551
|
actualUsd: loop.cost.actualUsd,
|
|
553
552
|
remainingBudgetUsd: costState.remainingBudgetUsd,
|
|
554
553
|
pressure: costState.pressure,
|
|
555
|
-
provenance:
|
|
554
|
+
provenance: loop.cost.provenance
|
|
556
555
|
}
|
|
557
556
|
}, { now: now(), idFactory });
|
|
558
557
|
if (input.store) {
|
|
@@ -610,7 +609,7 @@ export async function runMartin(input) {
|
|
|
610
609
|
}));
|
|
611
610
|
}
|
|
612
611
|
const changedFiles = tracksWorkspaceMutations
|
|
613
|
-
? resolveChangedFiles(result, request.context.repoRoot)
|
|
612
|
+
? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary)
|
|
614
613
|
: [];
|
|
615
614
|
// Evidence is only reliable when the adapter explicitly reported files OR git actually
|
|
616
615
|
// returned a non-empty list. A repoRoot alone is insufficient — git may fail (e.g. not
|
|
@@ -1136,29 +1135,28 @@ function getUsageProvenance(usage) {
|
|
|
1136
1135
|
}
|
|
1137
1136
|
return "actual";
|
|
1138
1137
|
}
|
|
1139
|
-
|
|
1138
|
+
const COST_PROVENANCE_RANK = {
|
|
1139
|
+
unavailable: 0,
|
|
1140
|
+
estimated: 1,
|
|
1141
|
+
actual: 2
|
|
1142
|
+
};
|
|
1143
|
+
/**
|
|
1144
|
+
* Aggregates cost provenance across attempts. The cumulative loop provenance
|
|
1145
|
+
* can only be as trustworthy as its weakest attempt: if any attempt's cost was
|
|
1146
|
+
* estimated or unavailable, the cumulative total must reflect that, even if a
|
|
1147
|
+
* later attempt reports an authoritative actual cost.
|
|
1148
|
+
*/
|
|
1149
|
+
function mergeCostProvenance(previous, current) {
|
|
1150
|
+
if (previous === undefined) {
|
|
1151
|
+
return current;
|
|
1152
|
+
}
|
|
1153
|
+
return COST_PROVENANCE_RANK[current] < COST_PROVENANCE_RANK[previous] ? current : previous;
|
|
1154
|
+
}
|
|
1155
|
+
function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
|
|
1140
1156
|
if (result.execution?.changedFiles !== undefined) {
|
|
1141
1157
|
return result.execution.changedFiles;
|
|
1142
1158
|
}
|
|
1143
|
-
|
|
1144
|
-
return [];
|
|
1145
|
-
}
|
|
1146
|
-
try {
|
|
1147
|
-
const diff = spawnSync("git", ["diff", "--name-only", "HEAD", "--", "."], {
|
|
1148
|
-
cwd: repoRoot,
|
|
1149
|
-
encoding: "utf8"
|
|
1150
|
-
});
|
|
1151
|
-
if (diff.status !== 0 || typeof diff.stdout !== "string") {
|
|
1152
|
-
return [];
|
|
1153
|
-
}
|
|
1154
|
-
return diff.stdout
|
|
1155
|
-
.split(/\r?\n/u)
|
|
1156
|
-
.map((entry) => entry.trim())
|
|
1157
|
-
.filter(Boolean);
|
|
1158
|
-
}
|
|
1159
|
-
catch {
|
|
1160
|
-
return [];
|
|
1161
|
-
}
|
|
1159
|
+
return listAttemptChangedFilesSinceBoundary({ repoRoot, boundary: rollbackBoundary });
|
|
1162
1160
|
}
|
|
1163
1161
|
function buildPatchDiff(result, changedFiles) {
|
|
1164
1162
|
// Use structured diff stats to build a minimal diff header if no raw diff is available
|
|
@@ -39,7 +39,7 @@ const BLOCKED_PATTERNS = [
|
|
|
39
39
|
*/
|
|
40
40
|
function commandContainsDestructiveRemoval(command) {
|
|
41
41
|
const normalized = command.replace(/\$\{?IFS\}?/giu, " ").toLowerCase();
|
|
42
|
-
const rmInvocation = /(?:^|[\s;&|`(])(
|
|
42
|
+
const rmInvocation = /(?:^|[\s;&|`(])(?:[^\s;&|`(]+\/)?rm\s+([^\n;|`]+)/giu;
|
|
43
43
|
let match;
|
|
44
44
|
while ((match = rmInvocation.exec(normalized)) !== null) {
|
|
45
45
|
const args = match[1] ?? "";
|
|
@@ -21,6 +21,7 @@ export interface StoredReceiptIntegrityMaterial {
|
|
|
21
21
|
chain: ReceiptIntegrityChainEntry[];
|
|
22
22
|
signatureHmacSha256: string;
|
|
23
23
|
}
|
|
24
|
+
export declare function resolveReceiptIntegrityRoot(env?: NodeJS.ProcessEnv): string;
|
|
24
25
|
export declare function writeReceiptIntegrityMaterial(input: {
|
|
25
26
|
runId: string;
|
|
26
27
|
runsRoot: string;
|
|
@@ -5,6 +5,10 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
const RECEIPT_INTEGRITY_SCHEMA_VERSION = "martin.receipt-integrity.v1";
|
|
6
6
|
const RECEIPT_INTEGRITY_KEY_DIR_MODE = 0o700;
|
|
7
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
|
+
}
|
|
8
12
|
export async function writeReceiptIntegrityMaterial(input) {
|
|
9
13
|
const signedAt = input.signedAt ?? new Date().toISOString();
|
|
10
14
|
const keyMaterial = await ensureReceiptIntegrityKey(input.runsRoot, input.runId);
|
|
@@ -232,7 +236,7 @@ async function readReceiptIntegrityKey(runsRoot, runId) {
|
|
|
232
236
|
}
|
|
233
237
|
function resolveReceiptIntegrityKeyPath(runsRoot, runId) {
|
|
234
238
|
const rootHash = sha256(runsRoot).slice(0, 16);
|
|
235
|
-
return join(
|
|
239
|
+
return join(resolveReceiptIntegrityRoot(), rootHash, `${runId}.key`);
|
|
236
240
|
}
|
|
237
241
|
function serializeStoredJson(value) {
|
|
238
242
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
@@ -39,6 +39,19 @@ export interface LoopRunRecord {
|
|
|
39
39
|
objective: string;
|
|
40
40
|
};
|
|
41
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
|
+
}
|
|
42
55
|
export declare function readLoopRecordsFromFile(file: string): Promise<LoopRunRecord[]>;
|
|
43
56
|
export declare function readLatestLoopRecordFromFile(file: string): Promise<LoopRunRecord | null>;
|
|
44
57
|
/**
|
|
@@ -50,3 +63,4 @@ export declare function readAllLoopRecords(runsDir?: string): Promise<LoopRunRec
|
|
|
50
63
|
* Returns the most recently updated loop record, or null if none exist.
|
|
51
64
|
*/
|
|
52
65
|
export declare function readLatestLoopRecord(runsDir?: string): Promise<LoopRunRecord | null>;
|
|
66
|
+
export declare function buildLoopRecordsRollup(records: LoopRunRecord[]): LoopRecordsRollup;
|
|
@@ -45,13 +45,16 @@ export async function readAllLoopRecords(runsDir) {
|
|
|
45
45
|
catch {
|
|
46
46
|
return [];
|
|
47
47
|
}
|
|
48
|
-
const
|
|
48
|
+
const recordsByLoopId = new Map();
|
|
49
49
|
const jsonlFiles = entries
|
|
50
50
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
51
51
|
.map((entry) => entry.name);
|
|
52
52
|
for (const file of jsonlFiles) {
|
|
53
53
|
try {
|
|
54
|
-
|
|
54
|
+
const fromFile = await readLoopRecordsFromFile(join(dir, file));
|
|
55
|
+
for (const record of fromFile) {
|
|
56
|
+
ingestRecord(recordsByLoopId, record, "legacy_jsonl");
|
|
57
|
+
}
|
|
55
58
|
}
|
|
56
59
|
catch {
|
|
57
60
|
// skip malformed files or lines
|
|
@@ -60,13 +63,16 @@ export async function readAllLoopRecords(runsDir) {
|
|
|
60
63
|
const runDirectories = entries.filter((entry) => entry.isDirectory());
|
|
61
64
|
for (const entry of runDirectories) {
|
|
62
65
|
try {
|
|
63
|
-
|
|
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
|
+
}
|
|
64
70
|
}
|
|
65
71
|
catch {
|
|
66
72
|
// skip missing or malformed canonical records
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
|
-
return
|
|
75
|
+
return [...recordsByLoopId.values()].map((entry) => entry.record);
|
|
70
76
|
}
|
|
71
77
|
/**
|
|
72
78
|
* Returns the most recently updated loop record, or null if none exist.
|
|
@@ -81,3 +87,54 @@ export async function readLatestLoopRecord(runsDir) {
|
|
|
81
87
|
return a > b ? r : latest;
|
|
82
88
|
}, records[0]);
|
|
83
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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { PatchDecision, RollbackBoundaryArtifact, RollbackOutcomeArtifact } from "../contracts/index.js";
|
|
2
|
+
export declare function listAttemptChangedFilesSinceBoundary(input: {
|
|
3
|
+
repoRoot?: string;
|
|
4
|
+
boundary?: RollbackBoundaryArtifact;
|
|
5
|
+
}): string[];
|
|
2
6
|
export declare function captureRollbackBoundary(input: {
|
|
3
7
|
repoRoot?: string;
|
|
4
8
|
capturedAt: string;
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, relative, resolve } from "node:path";
|
|
4
|
+
export function listAttemptChangedFilesSinceBoundary(input) {
|
|
5
|
+
if (!input.repoRoot) {
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
const repoState = readRepoState(input.repoRoot);
|
|
9
|
+
if (!input.boundary) {
|
|
10
|
+
return uniqueSorted([...repoState.trackedDirtyFiles, ...repoState.untrackedFiles]);
|
|
11
|
+
}
|
|
12
|
+
const baselineTracked = new Set(input.boundary.trackedDirtyFiles);
|
|
13
|
+
const baselineUntracked = new Set(input.boundary.untrackedFiles);
|
|
14
|
+
return uniqueSorted([
|
|
15
|
+
...repoState.trackedDirtyFiles.filter((filePath) => !baselineTracked.has(filePath)),
|
|
16
|
+
...repoState.untrackedFiles.filter((filePath) => !baselineUntracked.has(filePath))
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
4
19
|
export async function captureRollbackBoundary(input) {
|
|
5
20
|
if (!input.repoRoot) {
|
|
6
21
|
return undefined;
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/Keesan12/martin-loop",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.3.
|
|
10
|
+
"version": "0.3.3",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "@martinloop/mcp",
|
|
15
|
-
"version": "0.3.
|
|
15
|
+
"version": "0.3.3",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|