@martinloop/mcp 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -5
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.js +84 -0
- package/dist/resources.d.ts +7 -0
- package/dist/resources.js +89 -0
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +121 -1
- package/dist/server.d.ts +6 -4
- package/dist/server.js +271 -6
- package/dist/tools/cockpit-support.d.ts +69 -0
- package/dist/tools/cockpit-support.js +108 -0
- package/dist/tools/doctor.d.ts +35 -0
- package/dist/tools/doctor.js +56 -0
- package/dist/tools/get-attempt.d.ts +8 -0
- package/dist/tools/get-attempt.js +6 -0
- package/dist/tools/get-run.d.ts +17 -0
- package/dist/tools/get-run.js +14 -0
- package/dist/tools/get-verification-results.d.ts +11 -0
- package/dist/tools/get-verification-results.js +9 -0
- package/dist/tools/list-runs.d.ts +9 -0
- package/dist/tools/list-runs.js +7 -0
- package/dist/tools/preflight.d.ts +62 -0
- package/dist/tools/preflight.js +76 -0
- package/dist/tools/run-dossier.d.ts +8 -0
- package/dist/tools/run-dossier.js +6 -0
- package/dist/tools/tool-support.d.ts +37 -0
- package/dist/tools/tool-support.js +110 -0
- package/package.json +10 -5
- package/server.json +4 -4
package/dist/server.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Martin Loop MCP Server
|
|
4
4
|
*
|
|
5
|
-
* Exposes
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Exposes a governed local MCP cockpit over stdio:
|
|
6
|
+
* martin_doctor — inspect local readiness and run-store health
|
|
7
|
+
* martin_preflight — normalize a proposed run contract before execution
|
|
8
|
+
* martin_run — execute a full Martin loop on a coding task
|
|
9
|
+
* martin_inspect — summarise a saved loop record file
|
|
10
|
+
* martin_status — return cost and pressure state from a loop record
|
|
9
11
|
*
|
|
10
12
|
* Setup (Claude Code):
|
|
11
13
|
* macOS/Linux: claude mcp add --scope user martin-loop -- npx @martinloop/mcp
|
|
@@ -20,19 +22,117 @@
|
|
|
20
22
|
import { createRequire } from "node:module";
|
|
21
23
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
22
24
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
23
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
25
|
+
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
26
|
+
import { martinDoctorTool } from "./tools/doctor.js";
|
|
27
|
+
import { getAttemptTool } from "./tools/get-attempt.js";
|
|
28
|
+
import { getRunTool } from "./tools/get-run.js";
|
|
24
29
|
import { getStatusTool } from "./tools/get-status.js";
|
|
30
|
+
import { getVerificationResultsTool } from "./tools/get-verification-results.js";
|
|
25
31
|
import { inspectLoopTool } from "./tools/inspect-loop.js";
|
|
32
|
+
import { listRunsTool } from "./tools/list-runs.js";
|
|
33
|
+
import { martinPreflightTool } from "./tools/preflight.js";
|
|
34
|
+
import { getMartinPrompt, listMartinPrompts } from "./prompts.js";
|
|
35
|
+
import { listMartinResourceTemplates, listMartinResources, readMartinResource } from "./resources.js";
|
|
26
36
|
import { runLoopTool } from "./tools/run-loop.js";
|
|
37
|
+
import { runDossierTool } from "./tools/run-dossier.js";
|
|
27
38
|
import { sanitizeToolErrorMessage, validateToolInput } from "./server-validation.js";
|
|
28
39
|
const require = createRequire(import.meta.url);
|
|
29
40
|
const packageJson = require("../package.json");
|
|
30
|
-
const server = new Server({ name: "martin-loop", version: packageJson.version }, { capabilities: { tools: {} } });
|
|
41
|
+
const server = new Server({ name: "martin-loop", version: packageJson.version }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
31
42
|
// ---------------------------------------------------------------------------
|
|
32
43
|
// Tool manifest
|
|
33
44
|
// ---------------------------------------------------------------------------
|
|
34
45
|
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
35
46
|
tools: [
|
|
47
|
+
{
|
|
48
|
+
name: "martin_doctor",
|
|
49
|
+
description: "Inspect Martin MCP readiness without changing code. Reports workspace roots, run-store visibility, execution mode, and whether claude or codex is available on PATH.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
additionalProperties: false,
|
|
53
|
+
properties: {
|
|
54
|
+
workingDirectory: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Optional repo-root override resolved under the MCP workspace root (or current working directory). Must stay within that safe root."
|
|
57
|
+
},
|
|
58
|
+
runsDir: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Optional runs-root override resolved under the default Martin runs root. Defaults to MARTIN_RUNS_DIR or ~/.martin/runs."
|
|
61
|
+
},
|
|
62
|
+
engine: {
|
|
63
|
+
type: "string",
|
|
64
|
+
enum: ["claude", "codex"],
|
|
65
|
+
description: "Optional engine to emphasize in the readiness report."
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "martin_preflight",
|
|
72
|
+
description: "Validate and normalize a proposed martin_run contract before execution. Reports the effective budget, path scope, engine readiness, and expected run-store layout.",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
additionalProperties: false,
|
|
76
|
+
properties: {
|
|
77
|
+
objective: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "The coding task to complete. Be specific about what needs to change."
|
|
80
|
+
},
|
|
81
|
+
workingDirectory: {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: "Optional repo-root override resolved under the MCP workspace root (or current working directory). Must stay within that safe root."
|
|
84
|
+
},
|
|
85
|
+
engine: {
|
|
86
|
+
type: "string",
|
|
87
|
+
enum: ["claude", "codex"],
|
|
88
|
+
description: "Which agent CLI to use. Defaults to 'claude'."
|
|
89
|
+
},
|
|
90
|
+
model: {
|
|
91
|
+
type: "string",
|
|
92
|
+
description: "Model override passed to the CLI (e.g. 'claude-opus-4-6', 'o3')."
|
|
93
|
+
},
|
|
94
|
+
maxUsd: {
|
|
95
|
+
type: "number",
|
|
96
|
+
exclusiveMinimum: 0,
|
|
97
|
+
description: "Hard budget ceiling in USD. Defaults to 25."
|
|
98
|
+
},
|
|
99
|
+
maxIterations: {
|
|
100
|
+
type: "integer",
|
|
101
|
+
exclusiveMinimum: 0,
|
|
102
|
+
description: "Maximum number of loop attempts. Defaults to 8."
|
|
103
|
+
},
|
|
104
|
+
maxTokens: {
|
|
105
|
+
type: "integer",
|
|
106
|
+
exclusiveMinimum: 0,
|
|
107
|
+
description: "Maximum total tokens across all attempts. Defaults to 80000."
|
|
108
|
+
},
|
|
109
|
+
verificationPlan: {
|
|
110
|
+
type: "array",
|
|
111
|
+
items: { type: "string" },
|
|
112
|
+
description: "Shell commands that must all exit 0 for the task to be considered complete (e.g. ['pnpm test', 'pnpm build'])."
|
|
113
|
+
},
|
|
114
|
+
allowedPaths: {
|
|
115
|
+
type: "array",
|
|
116
|
+
items: { type: "string" },
|
|
117
|
+
description: "Repo-relative path globs Martin may modify, such as ['src/**', 'tests/**']. Absolute paths and '..' traversal are rejected."
|
|
118
|
+
},
|
|
119
|
+
deniedPaths: {
|
|
120
|
+
type: "array",
|
|
121
|
+
items: { type: "string" },
|
|
122
|
+
description: "Repo-relative path globs Martin must never modify, such as ['.env', 'docs/security/**']. Absolute paths and '..' traversal are rejected."
|
|
123
|
+
},
|
|
124
|
+
workspaceId: {
|
|
125
|
+
type: "string",
|
|
126
|
+
description: "Workspace identifier for telemetry. Defaults to 'ws_mcp'."
|
|
127
|
+
},
|
|
128
|
+
projectId: {
|
|
129
|
+
type: "string",
|
|
130
|
+
description: "Project identifier for telemetry. Defaults to 'proj_mcp'."
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
required: ["objective"]
|
|
134
|
+
}
|
|
135
|
+
},
|
|
36
136
|
{
|
|
37
137
|
name: "martin_run",
|
|
38
138
|
description: "Execute a full Martin Loop on a coding task. Martin spawns the selected agent CLI (claude or codex), runs the task, classifies failures, and retries within the specified budget. Returns the loop outcome including lifecycle state, attempt count, and spend.",
|
|
@@ -152,15 +252,155 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
|
152
252
|
{ required: ["latest"] }
|
|
153
253
|
]
|
|
154
254
|
}
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: "martin_list_runs",
|
|
258
|
+
description: "List recent Martin Loop runs from the local run store with budget, verifier, and lifecycle summaries. Read-only.",
|
|
259
|
+
inputSchema: {
|
|
260
|
+
type: "object",
|
|
261
|
+
additionalProperties: false,
|
|
262
|
+
properties: {
|
|
263
|
+
runsDir: {
|
|
264
|
+
type: "string",
|
|
265
|
+
description: "Optional runs-root override resolved under the default Martin runs root. Defaults to MARTIN_RUNS_DIR or ~/.martin/runs."
|
|
266
|
+
},
|
|
267
|
+
limit: {
|
|
268
|
+
type: "integer",
|
|
269
|
+
exclusiveMinimum: 0,
|
|
270
|
+
description: "Maximum number of runs to return. Defaults to 20."
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: "martin_get_run",
|
|
277
|
+
description: "Load a read-only run dossier by loopId or latest run selector, including task, budget, cost, and attempts.",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: "object",
|
|
280
|
+
additionalProperties: false,
|
|
281
|
+
properties: {
|
|
282
|
+
loopId: {
|
|
283
|
+
type: "string",
|
|
284
|
+
description: "Martin loop ID under the run store."
|
|
285
|
+
},
|
|
286
|
+
runsDir: {
|
|
287
|
+
type: "string",
|
|
288
|
+
description: "Optional runs-root override resolved under the default Martin runs root. Defaults to MARTIN_RUNS_DIR or ~/.martin/runs."
|
|
289
|
+
},
|
|
290
|
+
latest: {
|
|
291
|
+
const: true,
|
|
292
|
+
description: "When true, loads the most recently updated loop record in the runs directory."
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
oneOf: [{ required: ["loopId"] }, { required: ["latest"] }]
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: "martin_get_attempt",
|
|
300
|
+
description: "Load read-only attempt evidence for a single Martin Loop attempt by loopId and attempt index.",
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: "object",
|
|
303
|
+
additionalProperties: false,
|
|
304
|
+
properties: {
|
|
305
|
+
loopId: {
|
|
306
|
+
type: "string",
|
|
307
|
+
description: "Martin loop ID under the run store."
|
|
308
|
+
},
|
|
309
|
+
attemptIndex: {
|
|
310
|
+
type: "integer",
|
|
311
|
+
exclusiveMinimum: 0,
|
|
312
|
+
description: "1-based attempt index to inspect."
|
|
313
|
+
},
|
|
314
|
+
runsDir: {
|
|
315
|
+
type: "string",
|
|
316
|
+
description: "Optional runs-root override resolved under the default Martin runs root. Defaults to MARTIN_RUNS_DIR or ~/.martin/runs."
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
required: ["loopId", "attemptIndex"]
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "martin_get_verification_results",
|
|
324
|
+
description: "Extract verifier completion events for a run by loopId or latest selector. Read-only.",
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: "object",
|
|
327
|
+
additionalProperties: false,
|
|
328
|
+
properties: {
|
|
329
|
+
loopId: {
|
|
330
|
+
type: "string",
|
|
331
|
+
description: "Martin loop ID under the run store."
|
|
332
|
+
},
|
|
333
|
+
runsDir: {
|
|
334
|
+
type: "string",
|
|
335
|
+
description: "Optional runs-root override resolved under the default Martin runs root. Defaults to MARTIN_RUNS_DIR or ~/.martin/runs."
|
|
336
|
+
},
|
|
337
|
+
latest: {
|
|
338
|
+
const: true,
|
|
339
|
+
description: "When true, loads the most recently updated loop record in the runs directory."
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
oneOf: [{ required: ["loopId"] }, { required: ["latest"] }]
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "martin_run_dossier",
|
|
347
|
+
description: "Build a compact read-only dossier for review: summary, budget, attempts, and verification evidence.",
|
|
348
|
+
inputSchema: {
|
|
349
|
+
type: "object",
|
|
350
|
+
additionalProperties: false,
|
|
351
|
+
properties: {
|
|
352
|
+
loopId: {
|
|
353
|
+
type: "string",
|
|
354
|
+
description: "Martin loop ID under the run store."
|
|
355
|
+
},
|
|
356
|
+
runsDir: {
|
|
357
|
+
type: "string",
|
|
358
|
+
description: "Optional runs-root override resolved under the default Martin runs root. Defaults to MARTIN_RUNS_DIR or ~/.martin/runs."
|
|
359
|
+
},
|
|
360
|
+
latest: {
|
|
361
|
+
const: true,
|
|
362
|
+
description: "When true, loads the most recently updated loop record in the runs directory."
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
oneOf: [{ required: ["loopId"] }, { required: ["latest"] }]
|
|
366
|
+
}
|
|
155
367
|
}
|
|
156
368
|
]
|
|
157
369
|
}));
|
|
158
370
|
// ---------------------------------------------------------------------------
|
|
371
|
+
// Resources and prompts
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
server.setRequestHandler(ListResourcesRequestSchema, () => ({
|
|
374
|
+
resources: listMartinResources()
|
|
375
|
+
}));
|
|
376
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, () => ({
|
|
377
|
+
resourceTemplates: listMartinResourceTemplates()
|
|
378
|
+
}));
|
|
379
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
380
|
+
return readMartinResource(request.params.uri);
|
|
381
|
+
});
|
|
382
|
+
server.setRequestHandler(ListPromptsRequestSchema, () => ({
|
|
383
|
+
prompts: listMartinPrompts()
|
|
384
|
+
}));
|
|
385
|
+
server.setRequestHandler(GetPromptRequestSchema, (request) => {
|
|
386
|
+
return getMartinPrompt(request.params.name, request.params.arguments ?? {});
|
|
387
|
+
});
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
159
389
|
// Tool dispatch
|
|
160
390
|
// ---------------------------------------------------------------------------
|
|
161
391
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
162
392
|
const { name, arguments: args } = request.params;
|
|
163
393
|
try {
|
|
394
|
+
if (name === "martin_doctor") {
|
|
395
|
+
const input = validateToolInput("martin_doctor", args);
|
|
396
|
+
const output = await martinDoctorTool(input);
|
|
397
|
+
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
398
|
+
}
|
|
399
|
+
if (name === "martin_preflight") {
|
|
400
|
+
const input = validateToolInput("martin_preflight", args);
|
|
401
|
+
const output = await martinPreflightTool(input);
|
|
402
|
+
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
403
|
+
}
|
|
164
404
|
if (name === "martin_run") {
|
|
165
405
|
const input = validateToolInput("martin_run", args);
|
|
166
406
|
const output = await runLoopTool(input);
|
|
@@ -176,6 +416,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
176
416
|
const output = await getStatusTool(input);
|
|
177
417
|
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
178
418
|
}
|
|
419
|
+
if (name === "martin_list_runs") {
|
|
420
|
+
const input = validateToolInput("martin_list_runs", args);
|
|
421
|
+
const output = await listRunsTool(input);
|
|
422
|
+
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
423
|
+
}
|
|
424
|
+
if (name === "martin_get_run") {
|
|
425
|
+
const input = validateToolInput("martin_get_run", args);
|
|
426
|
+
const output = await getRunTool(input);
|
|
427
|
+
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
428
|
+
}
|
|
429
|
+
if (name === "martin_get_attempt") {
|
|
430
|
+
const input = validateToolInput("martin_get_attempt", args);
|
|
431
|
+
const output = await getAttemptTool(input);
|
|
432
|
+
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
433
|
+
}
|
|
434
|
+
if (name === "martin_get_verification_results") {
|
|
435
|
+
const input = validateToolInput("martin_get_verification_results", args);
|
|
436
|
+
const output = await getVerificationResultsTool(input);
|
|
437
|
+
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
438
|
+
}
|
|
439
|
+
if (name === "martin_run_dossier") {
|
|
440
|
+
const input = validateToolInput("martin_run_dossier", args);
|
|
441
|
+
const output = await runDossierTool(input);
|
|
442
|
+
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
443
|
+
}
|
|
179
444
|
return {
|
|
180
445
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
181
446
|
isError: true
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type LoopRunRecord } from "../vendor/core/index.js";
|
|
2
|
+
export interface RunSelectorInput {
|
|
3
|
+
loopId?: string;
|
|
4
|
+
runsDir?: string;
|
|
5
|
+
latest?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface RunSummary {
|
|
8
|
+
loopId: string;
|
|
9
|
+
title: string;
|
|
10
|
+
objective: string;
|
|
11
|
+
status: string;
|
|
12
|
+
lifecycleState: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
attempts: number;
|
|
16
|
+
costUsd: number;
|
|
17
|
+
avoidedUsd: number;
|
|
18
|
+
pressure: string;
|
|
19
|
+
shouldStop: boolean;
|
|
20
|
+
verificationCount: number;
|
|
21
|
+
}
|
|
22
|
+
export interface VerificationResultSummary {
|
|
23
|
+
eventId?: string;
|
|
24
|
+
timestamp?: string;
|
|
25
|
+
lifecycleState?: string;
|
|
26
|
+
passed?: boolean;
|
|
27
|
+
summary?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function summarizeRun(loop: LoopRunRecord): RunSummary;
|
|
30
|
+
export declare function listRunSummaries(input?: {
|
|
31
|
+
runsDir?: string;
|
|
32
|
+
limit?: number;
|
|
33
|
+
}): Promise<RunSummary[]>;
|
|
34
|
+
export declare function loadSelectedRun(input: RunSelectorInput): Promise<LoopRunRecord>;
|
|
35
|
+
export declare function extractVerificationResults(loop: LoopRunRecord): VerificationResultSummary[];
|
|
36
|
+
export declare function getAttempt(loop: LoopRunRecord, attemptIndex: number): import("../vendor/core/index.js").LoopAttemptRecord;
|
|
37
|
+
export declare function buildRunDossier(loop: LoopRunRecord): {
|
|
38
|
+
loopId: string;
|
|
39
|
+
generatedAt: string;
|
|
40
|
+
sections: ({
|
|
41
|
+
kind: string;
|
|
42
|
+
content: {
|
|
43
|
+
title: string;
|
|
44
|
+
objective: string;
|
|
45
|
+
};
|
|
46
|
+
} | {
|
|
47
|
+
kind: string;
|
|
48
|
+
content: {
|
|
49
|
+
budget: {
|
|
50
|
+
maxUsd: number;
|
|
51
|
+
softLimitUsd: number;
|
|
52
|
+
maxIterations: number;
|
|
53
|
+
maxTokens: number;
|
|
54
|
+
};
|
|
55
|
+
cost: {
|
|
56
|
+
actualUsd: number;
|
|
57
|
+
tokensIn: number;
|
|
58
|
+
tokensOut: number;
|
|
59
|
+
avoidedUsd?: number;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
} | {
|
|
63
|
+
kind: string;
|
|
64
|
+
content: import("../vendor/core/index.js").LoopAttemptRecord[];
|
|
65
|
+
} | {
|
|
66
|
+
kind: string;
|
|
67
|
+
content: VerificationResultSummary[];
|
|
68
|
+
})[];
|
|
69
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { evaluateCostGovernor } from "../vendor/core/index.js";
|
|
2
|
+
import { loadLoopRecordForStatus, loadLoopRecordsForInspect } from "./run-store.js";
|
|
3
|
+
export function summarizeRun(loop) {
|
|
4
|
+
const costState = evaluateCostGovernor({
|
|
5
|
+
budget: loop.budget,
|
|
6
|
+
cost: {
|
|
7
|
+
actualUsd: loop.cost.actualUsd,
|
|
8
|
+
avoidedUsd: loop.cost.avoidedUsd ?? 0,
|
|
9
|
+
tokensIn: loop.cost.tokensIn,
|
|
10
|
+
tokensOut: loop.cost.tokensOut
|
|
11
|
+
},
|
|
12
|
+
attemptsUsed: loop.attempts.length
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
loopId: loop.loopId,
|
|
16
|
+
title: loop.task.title,
|
|
17
|
+
objective: loop.task.objective,
|
|
18
|
+
status: loop.status,
|
|
19
|
+
lifecycleState: loop.lifecycleState,
|
|
20
|
+
createdAt: loop.createdAt,
|
|
21
|
+
updatedAt: loop.updatedAt,
|
|
22
|
+
attempts: loop.attempts.length,
|
|
23
|
+
costUsd: loop.cost.actualUsd,
|
|
24
|
+
avoidedUsd: loop.cost.avoidedUsd ?? 0,
|
|
25
|
+
pressure: costState.pressure,
|
|
26
|
+
shouldStop: costState.shouldStop,
|
|
27
|
+
verificationCount: extractVerificationResults(loop).length
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function listRunSummaries(input = {}) {
|
|
31
|
+
const inspection = await loadLoopRecordsForInspect({ runsDir: input.runsDir });
|
|
32
|
+
const summaries = inspection.loops.map((loop) => summarizeRun(loop));
|
|
33
|
+
summaries.sort((left, right) => {
|
|
34
|
+
const leftTime = Date.parse(left.updatedAt ?? left.createdAt);
|
|
35
|
+
const rightTime = Date.parse(right.updatedAt ?? right.createdAt);
|
|
36
|
+
return (Number.isFinite(rightTime) ? rightTime : 0) - (Number.isFinite(leftTime) ? leftTime : 0);
|
|
37
|
+
});
|
|
38
|
+
return summaries.slice(0, input.limit ?? 20);
|
|
39
|
+
}
|
|
40
|
+
export async function loadSelectedRun(input) {
|
|
41
|
+
const selectors = [input.loopId ? "loopId" : null, input.latest ? "latest" : null].filter(Boolean);
|
|
42
|
+
if (selectors.length !== 1) {
|
|
43
|
+
throw new Error("Provide exactly one of loopId or latest.");
|
|
44
|
+
}
|
|
45
|
+
const source = await loadLoopRecordForStatus({
|
|
46
|
+
...(input.loopId ? { loopId: input.loopId } : {}),
|
|
47
|
+
...(input.latest ? { latest: true } : {}),
|
|
48
|
+
...(input.runsDir ? { runsDir: input.runsDir } : {})
|
|
49
|
+
});
|
|
50
|
+
return source.loop;
|
|
51
|
+
}
|
|
52
|
+
export function extractVerificationResults(loop) {
|
|
53
|
+
const events = "events" in loop && Array.isArray(loop.events) ? loop.events : [];
|
|
54
|
+
return events
|
|
55
|
+
.filter((event) => event?.type === "verification.completed")
|
|
56
|
+
.map((event) => {
|
|
57
|
+
const payload = isRecord(event.payload) ? event.payload : {};
|
|
58
|
+
return {
|
|
59
|
+
...(typeof event.eventId === "string" ? { eventId: event.eventId } : {}),
|
|
60
|
+
...(typeof event.timestamp === "string" ? { timestamp: event.timestamp } : {}),
|
|
61
|
+
...(typeof event.lifecycleState === "string" ? { lifecycleState: event.lifecycleState } : {}),
|
|
62
|
+
...(typeof payload.passed === "boolean" ? { passed: payload.passed } : {}),
|
|
63
|
+
...(typeof payload.summary === "string" ? { summary: payload.summary } : {})
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function getAttempt(loop, attemptIndex) {
|
|
68
|
+
const attempt = loop.attempts.find((candidate) => candidate.index === attemptIndex);
|
|
69
|
+
if (!attempt) {
|
|
70
|
+
throw new Error("Attempt not found.");
|
|
71
|
+
}
|
|
72
|
+
return attempt;
|
|
73
|
+
}
|
|
74
|
+
export function buildRunDossier(loop) {
|
|
75
|
+
return {
|
|
76
|
+
loopId: loop.loopId,
|
|
77
|
+
generatedAt: new Date().toISOString(),
|
|
78
|
+
sections: [
|
|
79
|
+
{
|
|
80
|
+
kind: "summary",
|
|
81
|
+
content: summarizeRun(loop)
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
kind: "task",
|
|
85
|
+
content: loop.task
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
kind: "budget",
|
|
89
|
+
content: {
|
|
90
|
+
budget: loop.budget,
|
|
91
|
+
cost: loop.cost
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
kind: "attempts",
|
|
96
|
+
content: loop.attempts
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
kind: "verification",
|
|
100
|
+
content: extractVerificationResults(loop)
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function isRecord(value) {
|
|
106
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=cockpit-support.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type LoopPreview, type MartinEngine } from "./tool-support.js";
|
|
2
|
+
export interface MartinDoctorInput {
|
|
3
|
+
workingDirectory?: string;
|
|
4
|
+
runsDir?: string;
|
|
5
|
+
engine?: MartinEngine;
|
|
6
|
+
}
|
|
7
|
+
export interface MartinDoctorOutput {
|
|
8
|
+
status: "ok" | "degraded";
|
|
9
|
+
summary: string;
|
|
10
|
+
server: {
|
|
11
|
+
name: "martin-loop";
|
|
12
|
+
nodeVersion: string;
|
|
13
|
+
platform: NodeJS.Platform;
|
|
14
|
+
};
|
|
15
|
+
environment: {
|
|
16
|
+
workspaceRoot: string;
|
|
17
|
+
workingDirectory: string;
|
|
18
|
+
runsRoot: string;
|
|
19
|
+
mode: "live" | "stub";
|
|
20
|
+
liveMode: boolean;
|
|
21
|
+
};
|
|
22
|
+
engines: Record<MartinEngine, {
|
|
23
|
+
available: boolean;
|
|
24
|
+
detail: string;
|
|
25
|
+
resolvedPath?: string;
|
|
26
|
+
}>;
|
|
27
|
+
requestedEngine?: MartinEngine;
|
|
28
|
+
runStore: {
|
|
29
|
+
exists: boolean;
|
|
30
|
+
loopCount: number;
|
|
31
|
+
latestRun?: LoopPreview;
|
|
32
|
+
};
|
|
33
|
+
warnings: string[];
|
|
34
|
+
}
|
|
35
|
+
export declare function martinDoctorTool(input: MartinDoctorInput): Promise<MartinDoctorOutput>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { resolveRunsRoot } from "../vendor/core/index.js";
|
|
2
|
+
import { resolveSafeRepoRoot, resolveSafeRunsRootPath } from "../server-validation.js";
|
|
3
|
+
import { getEngineAvailability, inspectRunsRoot, resolveExecutionMode } from "./tool-support.js";
|
|
4
|
+
export async function martinDoctorTool(input) {
|
|
5
|
+
const workingDirectory = resolveSafeRepoRoot(input.workingDirectory);
|
|
6
|
+
const runsRoot = resolveSafeRunsRootPath(input.runsDir, resolveRunsRoot(process.env));
|
|
7
|
+
const executionMode = resolveExecutionMode();
|
|
8
|
+
const claude = getEngineAvailability("claude");
|
|
9
|
+
const codex = getEngineAvailability("codex");
|
|
10
|
+
const runStore = await inspectRunsRoot(runsRoot);
|
|
11
|
+
const warnings = [];
|
|
12
|
+
if (!runStore.exists) {
|
|
13
|
+
warnings.push("Configured Martin runs root does not exist yet.");
|
|
14
|
+
}
|
|
15
|
+
if (executionMode.liveMode && !claude.available && !codex.available) {
|
|
16
|
+
warnings.push("Neither claude nor codex is currently available on PATH for live runs.");
|
|
17
|
+
}
|
|
18
|
+
if (input.engine && executionMode.liveMode) {
|
|
19
|
+
const selected = input.engine === "claude" ? claude : codex;
|
|
20
|
+
if (!selected.available) {
|
|
21
|
+
warnings.push(`Requested engine '${input.engine}' is not available on PATH.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
warnings.push(...runStore.warnings);
|
|
25
|
+
const status = warnings.length === 0 ? "ok" : "degraded";
|
|
26
|
+
return {
|
|
27
|
+
status,
|
|
28
|
+
summary: status === "ok"
|
|
29
|
+
? `Doctor passed: ${runStore.loopCount} run(s) visible in ${runsRoot}.`
|
|
30
|
+
: `Doctor found ${warnings.length} issue(s); review warnings before live execution.`,
|
|
31
|
+
server: {
|
|
32
|
+
name: "martin-loop",
|
|
33
|
+
nodeVersion: process.version,
|
|
34
|
+
platform: process.platform
|
|
35
|
+
},
|
|
36
|
+
environment: {
|
|
37
|
+
workspaceRoot: resolveSafeRepoRoot(),
|
|
38
|
+
workingDirectory,
|
|
39
|
+
runsRoot,
|
|
40
|
+
mode: executionMode.mode,
|
|
41
|
+
liveMode: executionMode.liveMode
|
|
42
|
+
},
|
|
43
|
+
engines: {
|
|
44
|
+
claude,
|
|
45
|
+
codex
|
|
46
|
+
},
|
|
47
|
+
...(input.engine ? { requestedEngine: input.engine } : {}),
|
|
48
|
+
runStore: {
|
|
49
|
+
exists: runStore.exists,
|
|
50
|
+
loopCount: runStore.loopCount,
|
|
51
|
+
...(runStore.latestRun ? { latestRun: runStore.latestRun } : {})
|
|
52
|
+
},
|
|
53
|
+
warnings
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getAttempt } from "./cockpit-support.js";
|
|
2
|
+
export interface GetAttemptInput {
|
|
3
|
+
loopId: string;
|
|
4
|
+
attemptIndex: number;
|
|
5
|
+
runsDir?: string;
|
|
6
|
+
}
|
|
7
|
+
export type GetAttemptOutput = ReturnType<typeof getAttempt>;
|
|
8
|
+
export declare function getAttemptTool(input: GetAttemptInput): Promise<GetAttemptOutput>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { getAttempt, loadSelectedRun } from "./cockpit-support.js";
|
|
2
|
+
export async function getAttemptTool(input) {
|
|
3
|
+
const loop = await loadSelectedRun({ loopId: input.loopId, runsDir: input.runsDir });
|
|
4
|
+
return getAttempt(loop, input.attemptIndex);
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=get-attempt.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LoopRunRecord } from "../vendor/core/index.js";
|
|
2
|
+
import { summarizeRun } from "./cockpit-support.js";
|
|
3
|
+
export interface GetRunInput {
|
|
4
|
+
loopId?: string;
|
|
5
|
+
runsDir?: string;
|
|
6
|
+
latest?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface GetRunOutput {
|
|
9
|
+
summary: ReturnType<typeof summarizeRun>;
|
|
10
|
+
task: LoopRunRecord["task"];
|
|
11
|
+
budget: LoopRunRecord["budget"];
|
|
12
|
+
cost: LoopRunRecord["cost"];
|
|
13
|
+
attempts: LoopRunRecord["attempts"];
|
|
14
|
+
createdAt: string;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function getRunTool(input: GetRunInput): Promise<GetRunOutput>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { loadSelectedRun, summarizeRun } from "./cockpit-support.js";
|
|
2
|
+
export async function getRunTool(input) {
|
|
3
|
+
const loop = await loadSelectedRun(input);
|
|
4
|
+
return {
|
|
5
|
+
summary: summarizeRun(loop),
|
|
6
|
+
task: loop.task,
|
|
7
|
+
budget: loop.budget,
|
|
8
|
+
cost: loop.cost,
|
|
9
|
+
attempts: loop.attempts,
|
|
10
|
+
createdAt: loop.createdAt,
|
|
11
|
+
updatedAt: loop.updatedAt
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=get-run.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type VerificationResultSummary } from "./cockpit-support.js";
|
|
2
|
+
export interface GetVerificationResultsInput {
|
|
3
|
+
loopId?: string;
|
|
4
|
+
runsDir?: string;
|
|
5
|
+
latest?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface GetVerificationResultsOutput {
|
|
8
|
+
loopId: string;
|
|
9
|
+
results: VerificationResultSummary[];
|
|
10
|
+
}
|
|
11
|
+
export declare function getVerificationResultsTool(input: GetVerificationResultsInput): Promise<GetVerificationResultsOutput>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { extractVerificationResults, loadSelectedRun } from "./cockpit-support.js";
|
|
2
|
+
export async function getVerificationResultsTool(input) {
|
|
3
|
+
const loop = await loadSelectedRun(input);
|
|
4
|
+
return {
|
|
5
|
+
loopId: loop.loopId,
|
|
6
|
+
results: extractVerificationResults(loop)
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=get-verification-results.js.map
|