@martinloop/mcp 0.2.0 → 0.2.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 (96) hide show
  1. package/README.md +118 -182
  2. package/dist/discovery-metadata.d.ts +21 -0
  3. package/dist/discovery-metadata.js +152 -0
  4. package/dist/discovery-support.d.ts +62 -0
  5. package/dist/discovery-support.js +224 -0
  6. package/dist/package-version.d.ts +1 -0
  7. package/dist/package-version.js +3 -0
  8. package/dist/prompts.d.ts +13 -3
  9. package/dist/prompts.js +537 -74
  10. package/dist/resources.d.ts +35 -5
  11. package/dist/resources.js +788 -71
  12. package/dist/server-validation.d.ts +2 -3
  13. package/dist/server-validation.js +375 -119
  14. package/dist/server.d.ts +76 -7
  15. package/dist/server.js +1478 -394
  16. package/dist/tools/doctor.d.ts +2 -0
  17. package/dist/tools/doctor.js +18 -6
  18. package/dist/tools/eval.d.ts +24 -0
  19. package/dist/tools/eval.js +65 -0
  20. package/dist/tools/get-attempt.d.ts +13 -6
  21. package/dist/tools/get-attempt.js +14 -5
  22. package/dist/tools/get-run.d.ts +19 -12
  23. package/dist/tools/get-run.js +20 -11
  24. package/dist/tools/get-status.d.ts +19 -0
  25. package/dist/tools/get-status.js +30 -2
  26. package/dist/tools/get-verification-results.d.ts +10 -7
  27. package/dist/tools/get-verification-results.js +11 -6
  28. package/dist/tools/inspect-loop.d.ts +9 -0
  29. package/dist/tools/inspect-loop.js +11 -2
  30. package/dist/tools/list-runs.d.ts +25 -5
  31. package/dist/tools/list-runs.js +21 -4
  32. package/dist/tools/logs.d.ts +25 -0
  33. package/dist/tools/logs.js +49 -0
  34. package/dist/tools/plan.d.ts +20 -0
  35. package/dist/tools/plan.js +10 -0
  36. package/dist/tools/pr-tools.d.ts +31 -0
  37. package/dist/tools/pr-tools.js +111 -0
  38. package/dist/tools/preflight.d.ts +10 -0
  39. package/dist/tools/preflight.js +18 -4
  40. package/dist/tools/run-controls.d.ts +36 -0
  41. package/dist/tools/run-controls.js +88 -0
  42. package/dist/tools/run-dossier.d.ts +51 -4
  43. package/dist/tools/run-dossier.js +100 -5
  44. package/dist/tools/run-loop.d.ts +19 -0
  45. package/dist/tools/run-loop.js +61 -4
  46. package/dist/tools/run-store.d.ts +57 -3
  47. package/dist/tools/run-store.js +404 -53
  48. package/dist/tools/tool-errors.d.ts +37 -0
  49. package/dist/tools/tool-errors.js +170 -0
  50. package/dist/tools/tool-response.d.ts +16 -0
  51. package/dist/tools/tool-response.js +34 -0
  52. package/dist/tools/tool-support.d.ts +92 -2
  53. package/dist/tools/tool-support.js +385 -63
  54. package/dist/tools/triage-runs.d.ts +33 -0
  55. package/dist/tools/triage-runs.js +138 -0
  56. package/dist/tools/workflow-governance.d.ts +133 -0
  57. package/dist/tools/workflow-governance.js +581 -0
  58. package/dist/vendor/adapters/claude-cli.js +0 -1
  59. package/dist/vendor/adapters/cli-bridge.d.ts +5 -0
  60. package/dist/vendor/adapters/cli-bridge.js +16 -9
  61. package/dist/vendor/adapters/direct-provider.js +0 -1
  62. package/dist/vendor/adapters/index.d.ts +2 -1
  63. package/dist/vendor/adapters/index.js +2 -1
  64. package/dist/vendor/adapters/openai-compatible.d.ts +47 -0
  65. package/dist/vendor/adapters/openai-compatible.js +242 -0
  66. package/dist/vendor/adapters/runtime-support.js +0 -1
  67. package/dist/vendor/adapters/stub-agent-cli.js +0 -1
  68. package/dist/vendor/adapters/stub-direct-provider.js +0 -1
  69. package/dist/vendor/adapters/verifier-only.js +0 -1
  70. package/dist/vendor/contracts/governance.js +0 -1
  71. package/dist/vendor/contracts/index.d.ts +2 -0
  72. package/dist/vendor/contracts/index.js +1 -1
  73. package/dist/vendor/contracts/operator.d.ts +19 -0
  74. package/dist/vendor/contracts/operator.js +11 -0
  75. package/dist/vendor/core/compiler.js +0 -1
  76. package/dist/vendor/core/context-integrity.js +0 -1
  77. package/dist/vendor/core/grounding.js +0 -1
  78. package/dist/vendor/core/index.js +1 -2
  79. package/dist/vendor/core/leash.js +19 -12
  80. package/dist/vendor/core/persistence/compiler.js +0 -1
  81. package/dist/vendor/core/persistence/index.js +0 -1
  82. package/dist/vendor/core/persistence/ledger.js +0 -1
  83. package/dist/vendor/core/persistence/runs-reader.js +0 -1
  84. package/dist/vendor/core/persistence/store.js +0 -1
  85. package/dist/vendor/core/policy.js +0 -1
  86. package/dist/vendor/core/red-blue/red-phase.d.ts +64 -0
  87. package/dist/vendor/core/red-blue/red-phase.js +135 -0
  88. package/dist/vendor/core/red-blue/risk-tiers.d.ts +22 -0
  89. package/dist/vendor/core/red-blue/risk-tiers.js +32 -0
  90. package/dist/vendor/core/rollback.js +2 -3
  91. package/dist/workflow-state.d.ts +25 -0
  92. package/dist/workflow-state.js +102 -0
  93. package/package.json +12 -7
  94. package/server.json +2 -2
  95. package/dist/tools/cockpit-support.d.ts +0 -69
  96. package/dist/tools/cockpit-support.js +0 -108
@@ -1,12 +1,44 @@
1
- import { type LoopRunRecord } from "../vendor/core/index.js";
1
+ import { type LedgerEvent } from "../vendor/core/index.js";
2
+ import type { InspectableLoopRecord } from "./tool-support.js";
2
3
  export interface InspectLoopSource {
3
4
  source: string;
4
- loops: LoopRunRecord[];
5
+ loops: InspectableLoopRecord[];
6
+ warnings: string[];
5
7
  }
6
8
  export interface StatusLoopSource {
7
9
  source: string;
8
- loop: LoopRunRecord;
10
+ loop: InspectableLoopRecord;
9
11
  }
12
+ export interface DetailedLoopSource {
13
+ source: string;
14
+ sourceKind: "file" | "loop_id" | "latest" | "runs_root";
15
+ runsRoot: string;
16
+ loop: InspectableLoopRecord;
17
+ warnings: string[];
18
+ canonicalRunDirectory?: string;
19
+ canonicalLoopRecordPath?: string;
20
+ ledgerPath?: string;
21
+ }
22
+ export interface LoopListInput {
23
+ runsDir?: string;
24
+ limit?: number;
25
+ status?: string;
26
+ lifecycleState?: string;
27
+ adapterId?: string;
28
+ model?: string;
29
+ updatedAfter?: string;
30
+ }
31
+ export interface LoopListResult {
32
+ source: string;
33
+ runsRoot: string;
34
+ loops: InspectableLoopRecord[];
35
+ warnings: string[];
36
+ }
37
+ export type LedgerEventsWithDiagnostics = LedgerEvent[] & {
38
+ warnings?: string[];
39
+ unreadable?: boolean;
40
+ ledgerPath?: string;
41
+ };
10
42
  export declare function loadLoopRecordsForInspect(input: {
11
43
  file?: string;
12
44
  runsDir?: string;
@@ -18,3 +50,25 @@ export declare function loadLoopRecordForStatus(input: {
18
50
  runsDir?: string;
19
51
  latest?: boolean;
20
52
  }): Promise<StatusLoopSource>;
53
+ export declare function listLoopRecords(input: LoopListInput): Promise<LoopListResult>;
54
+ export declare function loadDetailedLoopRecord(input: {
55
+ file?: string;
56
+ loopId?: string;
57
+ runsDir?: string;
58
+ latest?: boolean;
59
+ }): Promise<DetailedLoopSource>;
60
+ export declare function loadAttemptFromLoop(input: {
61
+ file?: string;
62
+ loopId?: string;
63
+ runsDir?: string;
64
+ attemptIndex?: number;
65
+ }): Promise<{
66
+ detail: DetailedLoopSource;
67
+ attempt: InspectableLoopRecord["attempts"][number];
68
+ }>;
69
+ export declare function readLedgerEvents(detail: DetailedLoopSource): Promise<LedgerEventsWithDiagnostics>;
70
+ export declare function readAttemptArtifactFiles(detail: DetailedLoopSource, attemptIndex: number): Promise<string[]>;
71
+ export declare function readAllLoopRecordsSafely(runsRoot: string): Promise<{
72
+ loops: InspectableLoopRecord[];
73
+ warnings: string[];
74
+ }>;
@@ -1,39 +1,43 @@
1
- import { stat } from "node:fs/promises";
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot } from "../vendor/core/index.js";
4
- import { resolveSafeLoopRecordPath, resolveSafeRunsPath, resolveSafeRunsRootPath } from "../server-validation.js";
3
+ import { readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot } from "../vendor/core/index.js";
4
+ import { resolveSafeLoopRecordPath, resolveSafeRunsJsonPath, resolveSafeRunsPath, resolveSafeRunsRootPath } from "../server-validation.js";
5
+ import { attemptNotFoundError, invalidSelectorError, noLoopRecordsError, storeUnreadableError } from "./tool-errors.js";
5
6
  export async function loadLoopRecordsForInspect(input) {
6
7
  const runsRoot = resolveSafeRunsRootPath(input.runsDir, resolveRunsRoot(process.env));
7
8
  if (!input.file) {
9
+ const inspected = await readAllLoopRecordsSafely(runsRoot);
8
10
  return {
9
11
  source: runsRoot,
10
- loops: await readAllLoopRecords(runsRoot)
12
+ loops: inspected.loops,
13
+ warnings: inspected.warnings
11
14
  };
12
15
  }
13
16
  const targetPath = resolveSafeRunsPath(input.file, runsRoot);
14
- const targetStats = await stat(targetPath);
15
- if (targetStats.isDirectory()) {
16
- const canonicalLoopRecordPath = path.join(targetPath, "loop-record.json");
17
- try {
18
- const canonicalLoopRecordStats = await stat(canonicalLoopRecordPath);
19
- if (canonicalLoopRecordStats.isFile()) {
17
+ const targetStats = await safeStat(targetPath);
18
+ if (targetStats?.isDirectory()) {
19
+ const canonicalLoopRecordPath = resolveInspectableLoopRecordPath(targetPath, runsRoot);
20
+ if (canonicalLoopRecordPath) {
21
+ const canonicalLoopRecordStats = await safeStat(canonicalLoopRecordPath);
22
+ if (canonicalLoopRecordStats?.isFile()) {
20
23
  return {
21
24
  source: canonicalLoopRecordPath,
22
- loops: await readLoopRecordsFromFile(canonicalLoopRecordPath)
25
+ loops: await readRecordsFromFile(canonicalLoopRecordPath),
26
+ warnings: []
23
27
  };
24
28
  }
25
29
  }
26
- catch {
27
- // fall through to treating the directory as a full runs root
28
- }
30
+ const inspected = await readAllLoopRecordsSafely(targetPath);
29
31
  return {
30
32
  source: targetPath,
31
- loops: await readAllLoopRecords(targetPath)
33
+ loops: inspected.loops,
34
+ warnings: inspected.warnings
32
35
  };
33
36
  }
34
37
  return {
35
38
  source: targetPath,
36
- loops: await readLoopRecordsFromFile(targetPath)
39
+ loops: await readRecordsFromFile(targetPath),
40
+ warnings: []
37
41
  };
38
42
  }
39
43
  export async function loadLoopRecordForStatus(input) {
@@ -43,67 +47,414 @@ export async function loadLoopRecordForStatus(input) {
43
47
  loop: JSON.parse(input.loopJson)
44
48
  };
45
49
  }
50
+ const detail = await loadDetailedLoopRecord(input);
51
+ return {
52
+ source: detail.source,
53
+ loop: detail.loop
54
+ };
55
+ }
56
+ export async function listLoopRecords(input) {
57
+ const runsRoot = resolveSafeRunsRootPath(input.runsDir, resolveRunsRoot(process.env));
58
+ const inspected = await readAllLoopRecordsSafely(runsRoot);
59
+ const warnings = [...inspected.warnings];
60
+ const updatedAfterTimestamp = input.updatedAfter !== undefined ? new Date(input.updatedAfter).getTime() : undefined;
61
+ if (input.updatedAfter !== undefined &&
62
+ (!Number.isFinite(updatedAfterTimestamp) || Number.isNaN(updatedAfterTimestamp))) {
63
+ throw invalidSelectorError("Invalid updatedAfter.", "Provide updatedAfter as an ISO-8601 timestamp.");
64
+ }
65
+ const loops = inspected.loops
66
+ .filter((loop) => {
67
+ if (input.status && loop.status !== input.status) {
68
+ return false;
69
+ }
70
+ if (input.lifecycleState && loop.lifecycleState !== input.lifecycleState) {
71
+ return false;
72
+ }
73
+ if (input.adapterId && !loop.attempts.some((attempt) => attempt.adapterId === input.adapterId)) {
74
+ return false;
75
+ }
76
+ if (input.model && !loop.attempts.some((attempt) => attempt.model === input.model)) {
77
+ return false;
78
+ }
79
+ if (updatedAfterTimestamp !== undefined) {
80
+ const loopTimestamp = new Date(loop.updatedAt ?? loop.createdAt ?? 0).getTime();
81
+ if (!Number.isFinite(loopTimestamp) || loopTimestamp <= updatedAfterTimestamp) {
82
+ return false;
83
+ }
84
+ }
85
+ return true;
86
+ })
87
+ .sort((left, right) => timestampForLoop(right) - timestampForLoop(left))
88
+ .slice(0, input.limit ?? 20);
89
+ if (loops.length === 0) {
90
+ warnings.push("No loop records matched the current filters.");
91
+ }
92
+ return {
93
+ source: runsRoot,
94
+ runsRoot,
95
+ loops,
96
+ warnings
97
+ };
98
+ }
99
+ export async function loadDetailedLoopRecord(input) {
46
100
  const runsRoot = resolveSafeRunsRootPath(input.runsDir, resolveRunsRoot(process.env));
101
+ const selectors = [
102
+ input.file !== undefined ? "file" : null,
103
+ input.loopId !== undefined ? "loopId" : null,
104
+ input.latest ? "latest" : null
105
+ ].filter((value) => value !== null);
106
+ if (selectors.length !== 1) {
107
+ throw invalidSelectorError("Provide exactly one of file, loopId, or latest.", "Choose exactly one run selector per call.");
108
+ }
47
109
  if (input.file) {
48
110
  const targetPath = resolveSafeRunsPath(input.file, runsRoot);
49
- const targetStats = await stat(targetPath);
111
+ const targetStats = await safeStat(targetPath);
112
+ if (!targetStats) {
113
+ throw noLoopRecordsError();
114
+ }
50
115
  if (targetStats.isDirectory()) {
51
- const canonicalLoopRecordPath = path.join(targetPath, "loop-record.json");
52
- try {
53
- const canonicalLoopRecordStats = await stat(canonicalLoopRecordPath);
54
- if (canonicalLoopRecordStats.isFile()) {
55
- const loop = await readLatestLoopRecordFromFile(canonicalLoopRecordPath);
56
- if (!loop) {
57
- throw new Error("No loop records found.");
58
- }
59
- return {
116
+ const canonicalLoopRecordPath = resolveInspectableLoopRecordPath(targetPath, runsRoot);
117
+ if (canonicalLoopRecordPath) {
118
+ const canonicalStats = await safeStat(canonicalLoopRecordPath);
119
+ if (canonicalStats?.isFile()) {
120
+ const loop = await readCanonicalLoopRecord(canonicalLoopRecordPath);
121
+ return buildDetailedLoopSource({
60
122
  source: canonicalLoopRecordPath,
61
- loop
62
- };
123
+ sourceKind: "file",
124
+ runsRoot,
125
+ loop,
126
+ canonicalLoopRecordPath,
127
+ canonicalRunDirectory: path.dirname(canonicalLoopRecordPath)
128
+ });
63
129
  }
64
130
  }
65
- catch {
66
- // fall through to treating the directory as a full runs root
67
- }
68
- const loop = await readLatestLoopRecord(targetPath);
131
+ const inspected = await readAllLoopRecordsSafely(targetPath);
132
+ const loop = inspected.loops[0];
69
133
  if (!loop) {
70
- throw new Error("No loop records found.");
134
+ throw noLoopRecordsError();
71
135
  }
72
- return {
136
+ const detail = await buildDetailedLoopSourceFromDiscoveredLoop({
73
137
  source: targetPath,
138
+ sourceKind: "runs_root",
139
+ runsRoot,
74
140
  loop
141
+ });
142
+ return {
143
+ ...detail,
144
+ warnings: [...detail.warnings, ...inspected.warnings]
75
145
  };
76
146
  }
77
- const loop = await readLatestLoopRecordFromFile(targetPath);
78
- if (!loop) {
79
- throw new Error("No loop records found.");
147
+ const latest = await readLatestLoopRecordFromFile(targetPath);
148
+ if (!latest) {
149
+ throw noLoopRecordsError();
80
150
  }
81
- return {
151
+ if (path.basename(targetPath) === "loop-record.json") {
152
+ const loop = await readCanonicalLoopRecord(targetPath);
153
+ return buildDetailedLoopSource({
154
+ source: targetPath,
155
+ sourceKind: "file",
156
+ runsRoot,
157
+ loop,
158
+ canonicalLoopRecordPath: targetPath,
159
+ canonicalRunDirectory: path.dirname(targetPath)
160
+ });
161
+ }
162
+ return await buildDetailedLoopSourceFromDiscoveredLoop({
82
163
  source: targetPath,
83
- loop
84
- };
164
+ sourceKind: "file",
165
+ runsRoot,
166
+ loop: latest
167
+ });
85
168
  }
86
169
  if (input.loopId) {
87
- const targetPath = resolveSafeLoopRecordPath(input.loopId, runsRoot);
88
- const loop = await readLatestLoopRecordFromFile(targetPath);
170
+ const canonicalLoopRecordPath = resolvePotentialLoopRecordPath(input.loopId, runsRoot);
171
+ const canonicalStats = await safeStat(canonicalLoopRecordPath);
172
+ if (canonicalStats?.isFile()) {
173
+ const loop = await readCanonicalLoopRecord(canonicalLoopRecordPath);
174
+ return buildDetailedLoopSource({
175
+ source: canonicalLoopRecordPath,
176
+ sourceKind: "loop_id",
177
+ runsRoot,
178
+ loop,
179
+ canonicalLoopRecordPath,
180
+ canonicalRunDirectory: path.dirname(canonicalLoopRecordPath)
181
+ });
182
+ }
183
+ const inspected = await readAllLoopRecordsSafely(runsRoot);
184
+ const loop = inspected.loops.find((candidate) => candidate.loopId === input.loopId);
89
185
  if (!loop) {
90
- throw new Error("No loop records found.");
186
+ throw noLoopRecordsError();
91
187
  }
92
- return {
93
- source: targetPath,
188
+ const detail = await buildDetailedLoopSourceFromDiscoveredLoop({
189
+ source: runsRoot,
190
+ sourceKind: "loop_id",
191
+ runsRoot,
94
192
  loop
193
+ });
194
+ return {
195
+ ...detail,
196
+ warnings: [...detail.warnings, ...inspected.warnings]
95
197
  };
96
198
  }
97
- if (input.latest) {
98
- const loop = await readLatestLoopRecord(runsRoot);
99
- if (!loop) {
100
- throw new Error("No loop records found.");
199
+ const inspected = await readAllLoopRecordsSafely(runsRoot);
200
+ const loop = inspected.loops[0];
201
+ if (!loop) {
202
+ throw noLoopRecordsError();
203
+ }
204
+ const detail = await buildDetailedLoopSourceFromDiscoveredLoop({
205
+ source: runsRoot,
206
+ sourceKind: "latest",
207
+ runsRoot,
208
+ loop
209
+ });
210
+ return {
211
+ ...detail,
212
+ warnings: [...detail.warnings, ...inspected.warnings]
213
+ };
214
+ }
215
+ export async function loadAttemptFromLoop(input) {
216
+ const detail = await loadDetailedLoopRecord(input);
217
+ const attempt = input.attemptIndex !== undefined
218
+ ? detail.loop.attempts.find((candidate) => candidate.index === input.attemptIndex)
219
+ : detail.loop.attempts.at(-1);
220
+ if (!attempt) {
221
+ throw attemptNotFoundError(input.attemptIndex ?? detail.loop.attempts.length);
222
+ }
223
+ return {
224
+ detail,
225
+ attempt
226
+ };
227
+ }
228
+ export async function readLedgerEvents(detail) {
229
+ const requestedLedgerPath = detail.ledgerPath ??
230
+ (detail.canonicalRunDirectory ? path.join(detail.canonicalRunDirectory, "ledger.jsonl") : undefined);
231
+ if (!requestedLedgerPath) {
232
+ return withLedgerDiagnostics([]);
233
+ }
234
+ try {
235
+ const ledgerPath = resolveInspectableLedgerPath(requestedLedgerPath, detail.runsRoot);
236
+ const ledgerStats = await safeStat(ledgerPath);
237
+ if (!ledgerStats?.isFile()) {
238
+ return withLedgerDiagnostics([], { ledgerPath });
101
239
  }
240
+ const text = await readFile(ledgerPath, "utf8");
241
+ const events = text
242
+ .split(/\r?\n/u)
243
+ .map((line) => line.trim())
244
+ .filter(Boolean)
245
+ .map((line) => JSON.parse(line));
246
+ return withLedgerDiagnostics(events, { ledgerPath });
247
+ }
248
+ catch {
249
+ return withLedgerDiagnostics([], {
250
+ unreadable: true,
251
+ warnings: [
252
+ `Verification ledger for '${detail.loop.loopId}' is unreadable; ledger verification evidence is unavailable.`
253
+ ]
254
+ });
255
+ }
256
+ }
257
+ export async function readAttemptArtifactFiles(detail, attemptIndex) {
258
+ if (!detail.canonicalRunDirectory) {
259
+ return [];
260
+ }
261
+ const directory = path.join(detail.canonicalRunDirectory, "artifacts", `attempt-${String(attemptIndex).padStart(3, "0")}`);
262
+ try {
263
+ const entries = await readdir(directory, { withFileTypes: true });
264
+ return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
265
+ }
266
+ catch {
267
+ return [];
268
+ }
269
+ }
270
+ export async function readAllLoopRecordsSafely(runsRoot) {
271
+ let entries;
272
+ try {
273
+ entries = await readdir(runsRoot, { withFileTypes: true });
274
+ }
275
+ catch {
102
276
  return {
103
- source: runsRoot,
104
- loop
277
+ loops: [],
278
+ warnings: [
279
+ "Configured Martin runs root is missing or unreadable; no loop records could be inspected."
280
+ ]
105
281
  };
106
282
  }
107
- throw new Error("Provide exactly one of loopJson, file, loopId, or latest.");
283
+ const loops = [];
284
+ const warnings = [];
285
+ for (const entry of entries) {
286
+ if (entry.isDirectory()) {
287
+ const canonicalLoopRecordPath = resolveInspectableLoopRecordPath(path.join(runsRoot, entry.name), runsRoot);
288
+ if (!canonicalLoopRecordPath) {
289
+ continue;
290
+ }
291
+ const canonicalStats = await safeStat(canonicalLoopRecordPath);
292
+ if (!canonicalStats?.isFile()) {
293
+ continue;
294
+ }
295
+ try {
296
+ loops.push(await readCanonicalLoopRecord(canonicalLoopRecordPath));
297
+ }
298
+ catch {
299
+ warnings.push(`Skipped unreadable loop record for '${entry.name}'.`);
300
+ }
301
+ continue;
302
+ }
303
+ if (!entry.isFile()) {
304
+ continue;
305
+ }
306
+ const extension = path.extname(entry.name).toLowerCase();
307
+ if (extension !== ".json" && extension !== ".jsonl") {
308
+ continue;
309
+ }
310
+ const filePath = path.join(runsRoot, entry.name);
311
+ try {
312
+ loops.push(...(await readRecordsFromFile(resolveSafeRunsJsonPath(entry.name, runsRoot))));
313
+ }
314
+ catch {
315
+ warnings.push(`Skipped unreadable run file '${entry.name}'.`);
316
+ }
317
+ }
318
+ loops.sort((left, right) => timestampForLoop(right) - timestampForLoop(left));
319
+ return {
320
+ loops,
321
+ warnings
322
+ };
323
+ }
324
+ async function buildDetailedLoopSourceFromDiscoveredLoop(input) {
325
+ let canonicalLoopRecordPath;
326
+ try {
327
+ canonicalLoopRecordPath = resolveSafeLoopRecordPath(input.loop.loopId, input.runsRoot);
328
+ }
329
+ catch {
330
+ canonicalLoopRecordPath = undefined;
331
+ }
332
+ const canonicalStats = canonicalLoopRecordPath ? await safeStat(canonicalLoopRecordPath) : undefined;
333
+ if (canonicalLoopRecordPath && canonicalStats?.isFile()) {
334
+ const canonicalLoop = await readCanonicalLoopRecord(canonicalLoopRecordPath);
335
+ return buildDetailedLoopSource({
336
+ source: input.source,
337
+ sourceKind: input.sourceKind,
338
+ runsRoot: input.runsRoot,
339
+ loop: canonicalLoop,
340
+ canonicalLoopRecordPath,
341
+ canonicalRunDirectory: path.dirname(canonicalLoopRecordPath)
342
+ });
343
+ }
344
+ return buildDetailedLoopSource({
345
+ source: input.source,
346
+ sourceKind: input.sourceKind,
347
+ runsRoot: input.runsRoot,
348
+ loop: input.loop,
349
+ warnings: [
350
+ `Canonical run directory for ${input.loop.loopId} is not available; dossier data may be partial.`
351
+ ]
352
+ });
353
+ }
354
+ function buildDetailedLoopSource(input) {
355
+ return {
356
+ source: input.source,
357
+ sourceKind: input.sourceKind,
358
+ runsRoot: input.runsRoot,
359
+ loop: input.loop,
360
+ warnings: input.warnings ?? [],
361
+ ...(input.canonicalRunDirectory ? { canonicalRunDirectory: input.canonicalRunDirectory } : {}),
362
+ ...(input.canonicalLoopRecordPath ? { canonicalLoopRecordPath: input.canonicalLoopRecordPath } : {}),
363
+ ...(input.canonicalRunDirectory
364
+ ? { ledgerPath: path.join(input.canonicalRunDirectory, "ledger.jsonl") }
365
+ : {})
366
+ };
367
+ }
368
+ async function readCanonicalLoopRecord(file) {
369
+ try {
370
+ const text = await readFile(file, "utf8");
371
+ return validateInspectableLoopRecord(JSON.parse(text));
372
+ }
373
+ catch {
374
+ throw storeUnreadableError();
375
+ }
376
+ }
377
+ async function readRecordsFromFile(file) {
378
+ try {
379
+ return (await readLoopRecordsFromFile(file)).map((loop) => validateInspectableLoopRecord(loop));
380
+ }
381
+ catch {
382
+ throw storeUnreadableError();
383
+ }
384
+ }
385
+ async function safeStat(targetPath) {
386
+ try {
387
+ return await stat(targetPath);
388
+ }
389
+ catch {
390
+ return null;
391
+ }
392
+ }
393
+ function resolveInspectableLoopRecordPath(runDirectory, runsRoot) {
394
+ try {
395
+ const relativeLoopRecordPath = path.relative(runsRoot, path.join(runDirectory, "loop-record.json"));
396
+ return resolveSafeRunsJsonPath(relativeLoopRecordPath, runsRoot);
397
+ }
398
+ catch {
399
+ return undefined;
400
+ }
401
+ }
402
+ function resolvePotentialLoopRecordPath(loopId, runsRoot) {
403
+ try {
404
+ return resolveSafeLoopRecordPath(loopId, runsRoot);
405
+ }
406
+ catch (error) {
407
+ if (!/^[A-Za-z0-9._-]+$/u.test(loopId)) {
408
+ throw error;
409
+ }
410
+ return path.join(runsRoot, loopId, "loop-record.json");
411
+ }
412
+ }
413
+ function resolveInspectableLedgerPath(ledgerPath, runsRoot) {
414
+ const relativeLedgerPath = path.relative(runsRoot, ledgerPath);
415
+ return resolveSafeRunsJsonPath(relativeLedgerPath, runsRoot);
416
+ }
417
+ function timestampForLoop(loop) {
418
+ const timestamp = new Date(loop.updatedAt ?? loop.createdAt ?? 0).getTime();
419
+ return Number.isFinite(timestamp) ? timestamp : 0;
420
+ }
421
+ function withLedgerDiagnostics(events, diagnostics = {}) {
422
+ return Object.assign(events, diagnostics);
423
+ }
424
+ function validateInspectableLoopRecord(value) {
425
+ if (!isRecord(value)) {
426
+ throw storeUnreadableError();
427
+ }
428
+ const budget = value["budget"];
429
+ const cost = value["cost"];
430
+ const attempts = value["attempts"];
431
+ if (typeof value["loopId"] !== "string" ||
432
+ !/^[A-Za-z0-9._-]+$/u.test(value["loopId"]) ||
433
+ typeof value["status"] !== "string" ||
434
+ typeof value["lifecycleState"] !== "string" ||
435
+ !isRecord(budget) ||
436
+ !isFiniteNumber(budget["maxUsd"]) ||
437
+ !isFiniteNumber(budget["softLimitUsd"]) ||
438
+ !Number.isInteger(budget["maxIterations"]) ||
439
+ !Number.isInteger(budget["maxTokens"]) ||
440
+ !isRecord(cost) ||
441
+ !isFiniteNumber(cost["actualUsd"]) ||
442
+ !isFiniteNumber(cost["tokensIn"]) ||
443
+ !isFiniteNumber(cost["tokensOut"]) ||
444
+ (cost["avoidedUsd"] !== undefined && !isFiniteNumber(cost["avoidedUsd"])) ||
445
+ !Array.isArray(attempts) ||
446
+ !attempts.every(isRecord) ||
447
+ (value["events"] !== undefined &&
448
+ (!Array.isArray(value["events"]) || !value["events"].every(isRecord))) ||
449
+ (value["artifacts"] !== undefined &&
450
+ (!Array.isArray(value["artifacts"]) || !value["artifacts"].every(isRecord)))) {
451
+ throw storeUnreadableError();
452
+ }
453
+ return value;
454
+ }
455
+ function isRecord(value) {
456
+ return typeof value === "object" && value !== null;
457
+ }
458
+ function isFiniteNumber(value) {
459
+ return typeof value === "number" && Number.isFinite(value);
108
460
  }
109
- //# sourceMappingURL=run-store.js.map
@@ -0,0 +1,37 @@
1
+ import type { MartinErrorCategory } from "../vendor/contracts/index.js";
2
+ export type ToolFailureCode = "attempt_not_found" | "engine_unavailable" | "invalid_arguments" | "invalid_json" | "invalid_path" | "invalid_selector" | "no_loop_records" | "policy_blocked" | "store_unreadable" | "tool_execution_failed" | "unknown_tool" | "unsupported_operation";
3
+ export type ToolFailureCategory = MartinErrorCategory;
4
+ export interface ToolFailure {
5
+ code: ToolFailureCode;
6
+ category: ToolFailureCategory;
7
+ message: string;
8
+ suggestion?: string;
9
+ retryable: boolean;
10
+ details?: Record<string, unknown>;
11
+ }
12
+ export interface MartinToolErrorOptions {
13
+ category?: ToolFailureCategory;
14
+ suggestion?: string;
15
+ retryable?: boolean;
16
+ exposeMessage?: boolean;
17
+ details?: Record<string, unknown>;
18
+ }
19
+ export declare class MartinToolError extends Error {
20
+ readonly code: ToolFailureCode;
21
+ readonly category: ToolFailureCategory;
22
+ readonly suggestion?: string;
23
+ readonly retryable: boolean;
24
+ readonly exposeMessage: boolean;
25
+ readonly details?: Record<string, unknown>;
26
+ constructor(code: ToolFailureCode, message: string, options?: MartinToolErrorOptions);
27
+ }
28
+ export declare function sanitizePotentiallySensitiveMessage(message: string): string;
29
+ export declare function sanitizeToolErrorMessage(error: unknown): string;
30
+ export declare function toToolFailure(error: unknown): ToolFailure;
31
+ export declare function invalidArgumentsError(message: string, suggestion?: string): MartinToolError;
32
+ export declare function invalidPathError(message: string, suggestion?: string): MartinToolError;
33
+ export declare function invalidSelectorError(message: string, suggestion?: string): MartinToolError;
34
+ export declare function noLoopRecordsError(message?: string): MartinToolError;
35
+ export declare function attemptNotFoundError(attemptIndex: number): MartinToolError;
36
+ export declare function storeUnreadableError(message?: string): MartinToolError;
37
+ export declare function unsupportedOperationError(message: string, suggestion?: string): MartinToolError;