@martinloop/mcp 0.5.1 → 0.5.5
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 +52 -14
- package/dist/server-validation.js +2 -0
- package/dist/server.js +30 -3
- package/dist/tools/run-loop.d.ts +1 -0
- package/dist/tools/run-loop.js +25 -4
- package/dist/tools/tool-response.d.ts +9 -1
- package/dist/tools/tool-response.js +7 -5
- package/dist/tools/tool-support.js +62 -271
- package/dist/vendor/adapters/claude-cli.d.ts +9 -0
- package/dist/vendor/adapters/claude-cli.js +19 -2
- package/dist/vendor/adapters/codex-capabilities.d.ts +69 -0
- package/dist/vendor/adapters/codex-capabilities.js +265 -0
- package/dist/vendor/adapters/codex-cli.d.ts +23 -0
- package/dist/vendor/adapters/codex-cli.js +72 -0
- package/dist/vendor/adapters/codex-host.d.ts +107 -0
- package/dist/vendor/adapters/codex-host.js +595 -0
- package/dist/vendor/adapters/codex-launcher.d.ts +7 -135
- package/dist/vendor/adapters/codex-launcher.js +17 -766
- package/dist/vendor/adapters/index.d.ts +4 -2
- package/dist/vendor/adapters/index.js +4 -2
- package/dist/vendor/contracts/agent-execution.d.ts +9 -0
- package/dist/vendor/contracts/agent-execution.js +14 -0
- package/dist/vendor/contracts/index.d.ts +9 -0
- package/dist/vendor/contracts/index.js +1 -0
- package/dist/vendor/core/grounding.js +42 -7
- package/dist/vendor/core/index.d.ts +5 -2
- package/dist/vendor/core/index.js +12 -3
- package/dist/vendor/presentation/governed-plan.d.ts +33 -0
- package/dist/vendor/presentation/governed-plan.js +146 -0
- package/dist/vendor/presentation/index.d.ts +7 -0
- package/dist/vendor/presentation/index.js +7 -0
- package/dist/vendor/presentation/motion.d.ts +12 -0
- package/dist/vendor/presentation/motion.js +123 -0
- package/dist/vendor/presentation/status.d.ts +8 -0
- package/dist/vendor/presentation/status.js +11 -0
- package/dist/vendor/presentation/table.d.ts +11 -0
- package/dist/vendor/presentation/table.js +47 -0
- package/dist/vendor/presentation/text.d.ts +6 -0
- package/dist/vendor/presentation/text.js +30 -0
- package/dist/vendor/presentation/theme.d.ts +31 -0
- package/dist/vendor/presentation/theme.js +82 -0
- package/dist/vendor/presentation/verified-handoff.d.ts +15 -0
- package/dist/vendor/presentation/verified-handoff.js +151 -0
- package/package.json +3 -2
- package/server.json +2 -2
|
@@ -21,9 +21,8 @@ export function resolveExecutionMode() {
|
|
|
21
21
|
export function detectCliAvailability(command) {
|
|
22
22
|
const cacheKey = `${process.platform}:${command}`;
|
|
23
23
|
const cached = cliAvailabilityCache.get(cacheKey);
|
|
24
|
-
if (cached && cached.expiresAt > Date.now())
|
|
24
|
+
if (cached && cached.expiresAt > Date.now())
|
|
25
25
|
return cached.value;
|
|
26
|
-
}
|
|
27
26
|
const locator = process.platform === "win32" ? "path-scan(win32)" : "path-scan(posix)";
|
|
28
27
|
const resolvedPath = findCommandOnPath(command);
|
|
29
28
|
const value = resolvedPath
|
|
@@ -34,7 +33,7 @@ export function detectCliAvailability(command) {
|
|
|
34
33
|
detail: isOnPathDirectly(command, resolvedPath)
|
|
35
34
|
? `${command} is available on PATH.`
|
|
36
35
|
: `${command} found at ${resolvedPath} (not on PATH, auto-discovered).`,
|
|
37
|
-
|
|
36
|
+
resolvedPath
|
|
38
37
|
}
|
|
39
38
|
: {
|
|
40
39
|
command,
|
|
@@ -42,10 +41,7 @@ export function detectCliAvailability(command) {
|
|
|
42
41
|
locator,
|
|
43
42
|
detail: `${command} is not installed. ${suggestInstallCommand(command)}`
|
|
44
43
|
};
|
|
45
|
-
cliAvailabilityCache.set(cacheKey, {
|
|
46
|
-
expiresAt: Date.now() + CLI_CACHE_TTL_MS,
|
|
47
|
-
value
|
|
48
|
-
});
|
|
44
|
+
cliAvailabilityCache.set(cacheKey, { expiresAt: Date.now() + CLI_CACHE_TTL_MS, value });
|
|
49
45
|
return value;
|
|
50
46
|
}
|
|
51
47
|
function findCommandOnPath(command) {
|
|
@@ -53,80 +49,47 @@ function findCommandOnPath(command) {
|
|
|
53
49
|
const rawPath = pathKey ? process.env[pathKey] : undefined;
|
|
54
50
|
const hasExtension = /\.[A-Za-z0-9]+$/u.test(command);
|
|
55
51
|
const candidateNames = process.platform === "win32" && !hasExtension
|
|
56
|
-
? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
|
|
57
|
-
.split(";")
|
|
58
|
-
.map((extension) => extension.trim())
|
|
59
|
-
.filter(Boolean)
|
|
60
|
-
.map((extension) => `${command}${extension.toLowerCase()}`)
|
|
52
|
+
? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((extension) => extension.trim()).filter(Boolean).map((extension) => `${command}${extension.toLowerCase()}`)
|
|
61
53
|
: [command];
|
|
62
|
-
// 1. Search PATH entries
|
|
63
54
|
if (rawPath) {
|
|
64
|
-
const pathEntries = rawPath
|
|
65
|
-
.split(process.platform === "win32" ? ";" : ":")
|
|
66
|
-
.map((entry) => entry.trim())
|
|
67
|
-
.filter(Boolean);
|
|
55
|
+
const pathEntries = rawPath.split(process.platform === "win32" ? ";" : ":").map((entry) => entry.trim()).filter(Boolean);
|
|
68
56
|
for (const directory of pathEntries) {
|
|
69
57
|
for (const candidateName of candidateNames) {
|
|
70
58
|
const candidatePath = join(directory, candidateName);
|
|
71
|
-
if (isExecutablePath(candidatePath))
|
|
59
|
+
if (isExecutablePath(candidatePath))
|
|
72
60
|
return candidatePath;
|
|
73
|
-
}
|
|
74
61
|
}
|
|
75
62
|
}
|
|
76
63
|
}
|
|
77
|
-
// 2. Search common install locations when PATH didn't find it.
|
|
78
|
-
// This catches CLIs installed via npm/brew/pip that aren't on the
|
|
79
|
-
// current shell's PATH (common in IDEs, CI, and agent environments).
|
|
80
64
|
for (const directory of discoverCommonInstallDirectories(command)) {
|
|
81
65
|
for (const candidateName of candidateNames) {
|
|
82
66
|
const candidatePath = join(directory, candidateName);
|
|
83
|
-
if (isExecutablePath(candidatePath))
|
|
67
|
+
if (isExecutablePath(candidatePath))
|
|
84
68
|
return candidatePath;
|
|
85
|
-
}
|
|
86
69
|
}
|
|
87
70
|
}
|
|
88
71
|
return undefined;
|
|
89
72
|
}
|
|
90
|
-
/**
|
|
91
|
-
* Returns directories where CLI tools are commonly installed but may not
|
|
92
|
-
* be on PATH. Checked only as a fallback after PATH search fails.
|
|
93
|
-
*/
|
|
94
73
|
function discoverCommonInstallDirectories(command) {
|
|
95
74
|
const dirs = [];
|
|
96
75
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
97
76
|
if (process.platform === "win32") {
|
|
98
|
-
// npm global bin (Roaming)
|
|
99
77
|
const appData = process.env.APPDATA;
|
|
100
|
-
if (appData)
|
|
78
|
+
if (appData)
|
|
101
79
|
dirs.push(join(appData, "npm"));
|
|
102
|
-
}
|
|
103
|
-
// Codex desktop install
|
|
104
80
|
const localAppData = process.env.LOCALAPPDATA;
|
|
105
|
-
if (localAppData)
|
|
81
|
+
if (localAppData)
|
|
106
82
|
dirs.push(join(localAppData, "OpenAI", "Codex", "bin"));
|
|
107
|
-
|
|
108
|
-
// Scoop
|
|
109
|
-
if (home) {
|
|
83
|
+
if (home)
|
|
110
84
|
dirs.push(join(home, "scoop", "shims"));
|
|
111
|
-
}
|
|
112
85
|
}
|
|
113
86
|
else {
|
|
114
|
-
// Common Unix install paths
|
|
115
87
|
dirs.push("/usr/local/bin", "/opt/homebrew/bin");
|
|
116
|
-
if (home)
|
|
117
|
-
dirs.push(join(home, ".local", "bin"),
|
|
118
|
-
join(home, ".npm-global", "bin"), // npm prefix
|
|
119
|
-
join(home, ".bun", "bin"), // bun
|
|
120
|
-
join(home, ".cargo", "bin"), // cargo
|
|
121
|
-
join(home, "go", "bin") // go install
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
// nvm managed node
|
|
88
|
+
if (home)
|
|
89
|
+
dirs.push(join(home, ".local", "bin"), join(home, ".npm-global", "bin"), join(home, ".bun", "bin"), join(home, ".cargo", "bin"), join(home, "go", "bin"));
|
|
125
90
|
const nvmDir = process.env.NVM_DIR;
|
|
126
|
-
if (nvmDir)
|
|
127
|
-
|
|
128
|
-
dirs.push(nvmBin);
|
|
129
|
-
}
|
|
91
|
+
if (nvmDir)
|
|
92
|
+
dirs.push(join(nvmDir, "current", "bin"));
|
|
130
93
|
}
|
|
131
94
|
return dirs.filter(Boolean);
|
|
132
95
|
}
|
|
@@ -139,27 +102,15 @@ function isExecutablePath(candidatePath) {
|
|
|
139
102
|
return false;
|
|
140
103
|
}
|
|
141
104
|
}
|
|
142
|
-
|
|
143
|
-
* Returns true if the resolved path is inside a directory that's already
|
|
144
|
-
* on PATH (i.e., it was found via normal PATH lookup, not auto-discovery).
|
|
145
|
-
*/
|
|
146
|
-
function isOnPathDirectly(command, resolvedPath) {
|
|
105
|
+
function isOnPathDirectly(_command, resolvedPath) {
|
|
147
106
|
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path");
|
|
148
107
|
const rawPath = pathKey ? process.env[pathKey] : undefined;
|
|
149
108
|
if (!rawPath)
|
|
150
109
|
return false;
|
|
151
|
-
const pathEntries = rawPath
|
|
152
|
-
|
|
153
|
-
.map((entry) => entry.trim().replace(/[\\/]+$/u, ""))
|
|
154
|
-
.filter(Boolean);
|
|
155
|
-
const resolvedDir = resolvedPath
|
|
156
|
-
.replace(/[\\/][^\\/]+$/u, "")
|
|
157
|
-
.replace(/[\\/]+$/u, "");
|
|
110
|
+
const pathEntries = rawPath.split(process.platform === "win32" ? ";" : ":").map((entry) => entry.trim().replace(/[\\/]+$/u, "")).filter(Boolean);
|
|
111
|
+
const resolvedDir = resolvedPath.replace(/[\\/][^\\/]+$/u, "").replace(/[\\/]+$/u, "");
|
|
158
112
|
return pathEntries.some((entry) => entry.toLowerCase() === resolvedDir.toLowerCase());
|
|
159
113
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Returns a platform-specific one-liner install command for a known CLI.
|
|
162
|
-
*/
|
|
163
114
|
function suggestInstallCommand(command) {
|
|
164
115
|
const npmInstalls = {
|
|
165
116
|
claude: "npm install -g @anthropic-ai/claude-code",
|
|
@@ -167,21 +118,13 @@ function suggestInstallCommand(command) {
|
|
|
167
118
|
gemini: "npm install -g @google/gemini-cli"
|
|
168
119
|
};
|
|
169
120
|
const install = npmInstalls[command];
|
|
170
|
-
|
|
171
|
-
return `Install with: ${install}`;
|
|
172
|
-
}
|
|
173
|
-
return `Install ${command} and ensure it is on PATH.`;
|
|
121
|
+
return install ? `Install with: ${install}` : `Install ${command} and ensure it is on PATH.`;
|
|
174
122
|
}
|
|
175
123
|
export function getEngineAvailability(engine) {
|
|
176
124
|
return detectCliAvailability(engine);
|
|
177
125
|
}
|
|
178
126
|
export function createSkippedCliAvailability(command, detail = "Verification-only mode skipped live CLI availability detection; governed VERIFIED is unavailable.") {
|
|
179
|
-
return {
|
|
180
|
-
command,
|
|
181
|
-
available: false,
|
|
182
|
-
locator: "skipped",
|
|
183
|
-
detail
|
|
184
|
-
};
|
|
127
|
+
return { command, available: false, locator: "skipped", detail };
|
|
185
128
|
}
|
|
186
129
|
export function formatUsd(value) {
|
|
187
130
|
return `$${value.toFixed(2)}`;
|
|
@@ -227,10 +170,7 @@ function buildRoutingEconomicsSummary(economics) {
|
|
|
227
170
|
totalCostUsd: economics.totalCostUsd,
|
|
228
171
|
...(economics.timeToFirstDeltaMs !== undefined ? { timeToFirstDeltaMs: economics.timeToFirstDeltaMs } : {}),
|
|
229
172
|
...(economics.costPerAcceptedChange !== undefined ? { costPerAcceptedChange: economics.costPerAcceptedChange } : {}),
|
|
230
|
-
...(economics.routeRecommendation ? {
|
|
231
|
-
routeRecommendation: economics.routeRecommendation,
|
|
232
|
-
routeRecommendationReason: economics.routeRecommendationReason
|
|
233
|
-
} : {})
|
|
173
|
+
...(economics.routeRecommendation ? { routeRecommendation: economics.routeRecommendation, routeRecommendationReason: economics.routeRecommendationReason } : {})
|
|
234
174
|
};
|
|
235
175
|
}
|
|
236
176
|
export function buildAttemptSummary(attempt, artifacts) {
|
|
@@ -256,12 +196,7 @@ export function buildArtifactSummary(loop) {
|
|
|
256
196
|
return {
|
|
257
197
|
totalCount: artifacts.length,
|
|
258
198
|
kinds,
|
|
259
|
-
highlights: artifacts.slice(0, 5).map((artifact) => ({
|
|
260
|
-
artifactId: artifact.artifactId,
|
|
261
|
-
kind: artifact.kind,
|
|
262
|
-
label: artifact.label,
|
|
263
|
-
uri: artifact.uri
|
|
264
|
-
}))
|
|
199
|
+
highlights: artifacts.slice(0, 5).map((artifact) => ({ artifactId: artifact.artifactId, kind: artifact.kind, label: artifact.label, uri: artifact.uri }))
|
|
265
200
|
};
|
|
266
201
|
}
|
|
267
202
|
export function buildVerificationSummary(loop, ledgerEvents = []) {
|
|
@@ -270,88 +205,43 @@ export function buildVerificationSummary(loop, ledgerEvents = []) {
|
|
|
270
205
|
const warnings = [];
|
|
271
206
|
const integrity = resolveReceiptIntegrity(loop);
|
|
272
207
|
const ledgerWarnings = getLedgerWarnings(ledgerEvents);
|
|
273
|
-
if (integrity.state !== "verified")
|
|
208
|
+
if (integrity.state !== "verified")
|
|
274
209
|
warnings.push(`Receipt integrity is ${integrity.state}; persisted verifier evidence is not trustworthy yet.`);
|
|
275
|
-
}
|
|
276
210
|
warnings.push(...ledgerWarnings);
|
|
277
|
-
if (verificationEvents.length === 0)
|
|
278
|
-
warnings.push(verificationLedgerEvents.length > 0
|
|
279
|
-
|
|
280
|
-
: "No verification.completed events were found in the loop record.");
|
|
281
|
-
}
|
|
282
|
-
if (verificationLedgerEvents.length === 0 && ledgerWarnings.length === 0) {
|
|
211
|
+
if (verificationEvents.length === 0)
|
|
212
|
+
warnings.push(verificationLedgerEvents.length > 0 ? "No verification.completed events were found in the loop record; using ledger evidence." : "No verification.completed events were found in the loop record.");
|
|
213
|
+
if (verificationLedgerEvents.length === 0 && ledgerWarnings.length === 0)
|
|
283
214
|
warnings.push("No verification.completed ledger events were found for this run.");
|
|
284
|
-
}
|
|
285
215
|
const selectedEvidence = selectLatestVerificationEvidence(loop, verificationEvents, verificationLedgerEvents);
|
|
286
216
|
warnings.push(...selectedEvidence.warnings);
|
|
287
217
|
const latestEvidence = selectedEvidence.evidence;
|
|
288
|
-
if (!latestEvidence)
|
|
289
|
-
return {
|
|
290
|
-
|
|
291
|
-
eventCount: verificationEvents.length,
|
|
292
|
-
ledgerEventCount: verificationLedgerEvents.length,
|
|
293
|
-
warnings
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
const contradicted = latestEvidence.passed === true &&
|
|
297
|
-
warnings.some((warning) => warning.toLowerCase().includes("conflicts for the latest attempt"));
|
|
218
|
+
if (!latestEvidence)
|
|
219
|
+
return { status: "not_run", eventCount: verificationEvents.length, ledgerEventCount: verificationLedgerEvents.length, warnings };
|
|
220
|
+
const contradicted = latestEvidence.passed === true && warnings.some((warning) => warning.toLowerCase().includes("conflicts for the latest attempt"));
|
|
298
221
|
return {
|
|
299
|
-
status: contradicted
|
|
300
|
-
? "contradicted"
|
|
301
|
-
: latestEvidence.passed === true
|
|
302
|
-
? "passed"
|
|
303
|
-
: latestEvidence.passed === false
|
|
304
|
-
? "failed"
|
|
305
|
-
: "not_run",
|
|
222
|
+
status: contradicted ? "contradicted" : latestEvidence.passed === true ? "passed" : latestEvidence.passed === false ? "failed" : "not_run",
|
|
306
223
|
eventCount: verificationEvents.length,
|
|
307
224
|
ledgerEventCount: verificationLedgerEvents.length,
|
|
308
225
|
...(latestEvidence.attemptIndex !== undefined ? { latestAttemptIndex: latestEvidence.attemptIndex } : {}),
|
|
309
226
|
...(latestEvidence.timestamp ? { completedAt: latestEvidence.timestamp } : {}),
|
|
310
|
-
...(typeof latestEvidence.summary === "string" && latestEvidence.summary.trim().length > 0
|
|
311
|
-
? { summary: latestEvidence.summary.trim() }
|
|
312
|
-
: {}),
|
|
227
|
+
...(typeof latestEvidence.summary === "string" && latestEvidence.summary.trim().length > 0 ? { summary: latestEvidence.summary.trim() } : {}),
|
|
313
228
|
warnings
|
|
314
229
|
};
|
|
315
230
|
}
|
|
316
231
|
export function resolveReceiptIntegrity(loop) {
|
|
317
|
-
return
|
|
318
|
-
state: "selector_noncanonical",
|
|
319
|
-
reason: "Receipt integrity metadata was not available on the loop record."
|
|
320
|
-
});
|
|
232
|
+
return loop.receiptIntegrity ?? { state: "selector_noncanonical", reason: "Receipt integrity metadata was not available on the loop record." };
|
|
321
233
|
}
|
|
322
234
|
export function buildEventSummaries(loop, limit = 5) {
|
|
323
|
-
return (loop.events ?? [])
|
|
324
|
-
.slice(-limit)
|
|
325
|
-
.reverse()
|
|
326
|
-
.map((event) => ({
|
|
327
|
-
type: event.type,
|
|
328
|
-
...(event.timestamp ? { timestamp: event.timestamp } : {}),
|
|
329
|
-
...(event.lifecycleState ? { lifecycleState: event.lifecycleState } : {}),
|
|
330
|
-
payload: event.payload ?? {}
|
|
331
|
-
}));
|
|
235
|
+
return (loop.events ?? []).slice(-limit).reverse().map((event) => ({ type: event.type, ...(event.timestamp ? { timestamp: event.timestamp } : {}), ...(event.lifecycleState ? { lifecycleState: event.lifecycleState } : {}), payload: event.payload ?? {} }));
|
|
332
236
|
}
|
|
333
237
|
export function buildLoopCollectionSummary(loops) {
|
|
334
|
-
const previews = loops
|
|
335
|
-
|
|
336
|
-
.sort((left, right) => {
|
|
337
|
-
const leftTime = toPreviewTimestamp(left);
|
|
338
|
-
const rightTime = toPreviewTimestamp(right);
|
|
339
|
-
return rightTime - leftTime;
|
|
340
|
-
});
|
|
341
|
-
const statusBreakdown = countBy(previews, "status");
|
|
342
|
-
const lifecycleBreakdown = countBy(previews, "lifecycleState");
|
|
343
|
-
return {
|
|
344
|
-
...(previews[0] ? { latestRun: previews[0] } : {}),
|
|
345
|
-
recentRuns: previews.slice(0, 5),
|
|
346
|
-
statusBreakdown,
|
|
347
|
-
lifecycleBreakdown
|
|
348
|
-
};
|
|
238
|
+
const previews = loops.map((loop) => buildLoopPreview(loop)).sort((left, right) => toPreviewTimestamp(right) - toPreviewTimestamp(left));
|
|
239
|
+
return { ...(previews[0] ? { latestRun: previews[0] } : {}), recentRuns: previews.slice(0, 5), statusBreakdown: countBy(previews, "status"), lifecycleBreakdown: countBy(previews, "lifecycleState") };
|
|
349
240
|
}
|
|
350
241
|
export async function inspectRunsRoot(runsRoot = resolveRunsRoot(process.env)) {
|
|
351
242
|
const cached = runStoreInspectionCache.get(runsRoot);
|
|
352
|
-
if (cached && cached.expiresAt > Date.now())
|
|
243
|
+
if (cached && cached.expiresAt > Date.now())
|
|
353
244
|
return cached.value;
|
|
354
|
-
}
|
|
355
245
|
let exists = false;
|
|
356
246
|
try {
|
|
357
247
|
exists = (await stat(runsRoot)).isDirectory();
|
|
@@ -371,20 +261,12 @@ export async function inspectRunsRoot(runsRoot = resolveRunsRoot(process.env)) {
|
|
|
371
261
|
lifecycleBreakdown: summary.lifecycleBreakdown,
|
|
372
262
|
warnings: inspected.warnings
|
|
373
263
|
};
|
|
374
|
-
runStoreInspectionCache.set(runsRoot, {
|
|
375
|
-
expiresAt: Date.now() + RUN_STORE_CACHE_TTL_MS,
|
|
376
|
-
value
|
|
377
|
-
});
|
|
264
|
+
runStoreInspectionCache.set(runsRoot, { expiresAt: Date.now() + RUN_STORE_CACHE_TTL_MS, value });
|
|
378
265
|
return value;
|
|
379
266
|
}
|
|
380
267
|
export function buildRunRecordPaths(runsRoot, loopId) {
|
|
381
268
|
const runDirectory = join(runsRoot, loopId);
|
|
382
|
-
return {
|
|
383
|
-
runsRoot,
|
|
384
|
-
runDirectory,
|
|
385
|
-
loopRecordPath: join(runDirectory, "loop-record.json"),
|
|
386
|
-
ledgerPath: join(runDirectory, "ledger.jsonl")
|
|
387
|
-
};
|
|
269
|
+
return { runsRoot, runDirectory, loopRecordPath: join(runDirectory, "loop-record.json"), ledgerPath: join(runDirectory, "ledger.jsonl") };
|
|
388
270
|
}
|
|
389
271
|
export function buildAttemptArtifactDirectory(runsRoot, loopId, attemptIndex) {
|
|
390
272
|
return join(runsRoot, loopId, "artifacts", `attempt-${String(attemptIndex).padStart(3, "0")}`);
|
|
@@ -393,18 +275,10 @@ export async function buildAttemptArtifactsReference(runsRoot, loopId, attemptIn
|
|
|
393
275
|
const directory = buildAttemptArtifactDirectory(runsRoot, loopId, attemptIndex);
|
|
394
276
|
try {
|
|
395
277
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
396
|
-
return {
|
|
397
|
-
directory,
|
|
398
|
-
available: true,
|
|
399
|
-
files: entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort()
|
|
400
|
-
};
|
|
278
|
+
return { directory, available: true, files: entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort() };
|
|
401
279
|
}
|
|
402
280
|
catch {
|
|
403
|
-
return {
|
|
404
|
-
directory,
|
|
405
|
-
available: false,
|
|
406
|
-
files: []
|
|
407
|
-
};
|
|
281
|
+
return { directory, available: false, files: [] };
|
|
408
282
|
}
|
|
409
283
|
}
|
|
410
284
|
export function buildCostSnapshot(cost) {
|
|
@@ -417,62 +291,16 @@ export function buildCostSnapshot(cost) {
|
|
|
417
291
|
};
|
|
418
292
|
}
|
|
419
293
|
export function buildBudgetSnapshot(budget) {
|
|
420
|
-
return {
|
|
421
|
-
maxUsd: budget.maxUsd,
|
|
422
|
-
softLimitUsd: budget.softLimitUsd,
|
|
423
|
-
maxIterations: budget.maxIterations,
|
|
424
|
-
maxTokens: budget.maxTokens
|
|
425
|
-
};
|
|
294
|
+
return { maxUsd: budget.maxUsd, softLimitUsd: budget.softLimitUsd, maxIterations: budget.maxIterations, maxTokens: budget.maxTokens };
|
|
426
295
|
}
|
|
427
296
|
export function buildSuggestedResourceUris(loopId) {
|
|
428
|
-
return [
|
|
429
|
-
"martin://server/health",
|
|
430
|
-
"martin://runs/recent",
|
|
431
|
-
"martin://runs/triage",
|
|
432
|
-
"martin://runs/latest",
|
|
433
|
-
"martin://runs/latest/summary",
|
|
434
|
-
"martin://runs/latest/proof-card",
|
|
435
|
-
"martin://runs/latest/budget-status",
|
|
436
|
-
"martin://runs/latest/verifier-evidence",
|
|
437
|
-
"martin://runs/latest/rollback-evidence",
|
|
438
|
-
"martin://policies/current",
|
|
439
|
-
"martin://repo/risk-map",
|
|
440
|
-
"martin://verifiers/results",
|
|
441
|
-
"martin://agent/next-step",
|
|
442
|
-
`martin://runs/${loopId}`,
|
|
443
|
-
`martin://runs/${loopId}/dossier`,
|
|
444
|
-
`martin://runs/${loopId}/verification`,
|
|
445
|
-
"martin://guides/mcp-usage",
|
|
446
|
-
"martin://guides/agent-start",
|
|
447
|
-
"martin://guides/publish-readiness"
|
|
448
|
-
];
|
|
297
|
+
return ["martin://server/health", "martin://runs/recent", "martin://runs/triage", "martin://runs/latest", "martin://runs/latest/summary", "martin://runs/latest/proof-card", "martin://runs/latest/budget-status", "martin://runs/latest/verifier-evidence", "martin://runs/latest/rollback-evidence", "martin://policies/current", "martin://repo/risk-map", "martin://verifiers/results", "martin://agent/next-step", `martin://runs/${loopId}`, `martin://runs/${loopId}/dossier`, `martin://runs/${loopId}/verification`, "martin://guides/mcp-usage", "martin://guides/agent-start", "martin://guides/publish-readiness"];
|
|
449
298
|
}
|
|
450
299
|
export function buildSuggestedPromptNames() {
|
|
451
|
-
return [
|
|
452
|
-
"martin_start",
|
|
453
|
-
"martin_preflight",
|
|
454
|
-
"martin_triage",
|
|
455
|
-
"martin_resume",
|
|
456
|
-
"martin_prove",
|
|
457
|
-
"martin_release_check",
|
|
458
|
-
"martin_governed_coding_kickoff",
|
|
459
|
-
"martin_debug_failed_run",
|
|
460
|
-
"martin_publish_readiness_review",
|
|
461
|
-
"martin_triage_run_store",
|
|
462
|
-
"safe_bug_fix",
|
|
463
|
-
"write_tests_first",
|
|
464
|
-
"small_refactor",
|
|
465
|
-
"security_review",
|
|
466
|
-
"pr_review",
|
|
467
|
-
"release_check"
|
|
468
|
-
];
|
|
300
|
+
return ["martin_start", "martin_preflight", "martin_triage", "martin_resume", "martin_prove", "martin_release_check", "martin_governed_coding_kickoff", "martin_debug_failed_run", "martin_publish_readiness_review", "martin_triage_run_store", "safe_bug_fix", "write_tests_first", "small_refactor", "security_review", "pr_review", "release_check"];
|
|
469
301
|
}
|
|
470
302
|
function countBy(values, key) {
|
|
471
|
-
return values.reduce((accumulator, value) => {
|
|
472
|
-
const bucket = String(value[key]);
|
|
473
|
-
accumulator[bucket] = (accumulator[bucket] ?? 0) + 1;
|
|
474
|
-
return accumulator;
|
|
475
|
-
}, {});
|
|
303
|
+
return values.reduce((accumulator, value) => { const bucket = String(value[key]); accumulator[bucket] = (accumulator[bucket] ?? 0) + 1; return accumulator; }, {});
|
|
476
304
|
}
|
|
477
305
|
function toPreviewTimestamp(loop) {
|
|
478
306
|
const value = loop.updatedAt ?? loop.createdAt;
|
|
@@ -480,59 +308,33 @@ function toPreviewTimestamp(loop) {
|
|
|
480
308
|
}
|
|
481
309
|
function selectLatestVerificationEvidence(loop, verificationEvents, verificationLedgerEvents) {
|
|
482
310
|
const warnings = [];
|
|
483
|
-
const futureEvidenceCount = [
|
|
484
|
-
|
|
485
|
-
...verificationLedgerEvents.map((event) => event.timestamp)
|
|
486
|
-
].filter(isFutureVerificationTimestamp).length;
|
|
487
|
-
if (futureEvidenceCount > 0) {
|
|
311
|
+
const futureEvidenceCount = [...verificationEvents.map((event) => event.timestamp), ...verificationLedgerEvents.map((event) => event.timestamp)].filter(isFutureVerificationTimestamp).length;
|
|
312
|
+
if (futureEvidenceCount > 0)
|
|
488
313
|
warnings.push(`Ignored ${futureEvidenceCount} future-dated verification evidence item(s) that cannot be trusted yet.`);
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
...verificationEvents.map((event) => normalizeLoopVerificationEvidence(loop, event)),
|
|
492
|
-
...verificationLedgerEvents.map((event) => normalizeLedgerVerificationEvidence(loop, event))
|
|
493
|
-
].filter((candidate) => candidate !== undefined);
|
|
494
|
-
if (evidence.length === 0) {
|
|
314
|
+
const evidence = [...verificationEvents.map((event) => normalizeLoopVerificationEvidence(loop, event)), ...verificationLedgerEvents.map((event) => normalizeLedgerVerificationEvidence(loop, event))].filter((candidate) => candidate !== undefined);
|
|
315
|
+
if (evidence.length === 0)
|
|
495
316
|
return { warnings };
|
|
496
|
-
}
|
|
497
317
|
evidence.sort((left, right) => new Date(right.timestamp).getTime() - new Date(left.timestamp).getTime());
|
|
498
318
|
const latest = evidence[0];
|
|
499
|
-
if (!latest)
|
|
319
|
+
if (!latest)
|
|
500
320
|
return { warnings };
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
? candidate.attemptId === latest.attemptId
|
|
504
|
-
: latest.attemptIndex !== undefined
|
|
505
|
-
? candidate.attemptIndex === latest.attemptIndex
|
|
506
|
-
: false);
|
|
507
|
-
const distinctStatuses = new Set(latestAttemptEvidence
|
|
508
|
-
.map((candidate) => candidate.passed)
|
|
509
|
-
.filter((candidate) => typeof candidate === "boolean"));
|
|
321
|
+
const latestAttemptEvidence = evidence.filter((candidate) => latest.attemptId ? candidate.attemptId === latest.attemptId : latest.attemptIndex !== undefined ? candidate.attemptIndex === latest.attemptIndex : false);
|
|
322
|
+
const distinctStatuses = new Set(latestAttemptEvidence.map((candidate) => candidate.passed).filter((candidate) => typeof candidate === "boolean"));
|
|
510
323
|
if (distinctStatuses.size > 1) {
|
|
511
324
|
warnings.push("Verification evidence conflicts for the latest attempt; marking verification as contradicted.");
|
|
512
325
|
return { warnings };
|
|
513
326
|
}
|
|
514
|
-
return {
|
|
515
|
-
evidence: latest,
|
|
516
|
-
warnings
|
|
517
|
-
};
|
|
327
|
+
return { evidence: latest, warnings };
|
|
518
328
|
}
|
|
519
329
|
function normalizeLoopVerificationEvidence(loop, event) {
|
|
520
|
-
if (!isTrustedVerificationTimestamp(event.timestamp))
|
|
330
|
+
if (!isTrustedVerificationTimestamp(event.timestamp))
|
|
521
331
|
return undefined;
|
|
522
|
-
}
|
|
523
332
|
const payload = isRecord(event.payload) ? event.payload : undefined;
|
|
524
333
|
const attemptId = typeof payload?.["attemptId"] === "string" ? payload["attemptId"] : undefined;
|
|
525
|
-
const attemptIndex = typeof payload?.["attemptIndex"] === "number" && Number.isInteger(payload["attemptIndex"])
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const matchedAttempt = attemptId
|
|
529
|
-
? loop.attempts.find((attempt) => attempt.attemptId === attemptId)
|
|
530
|
-
: attemptIndex !== undefined
|
|
531
|
-
? loop.attempts.find((attempt) => attempt.index === attemptIndex)
|
|
532
|
-
: undefined;
|
|
533
|
-
if (!matchedAttempt) {
|
|
334
|
+
const attemptIndex = typeof payload?.["attemptIndex"] === "number" && Number.isInteger(payload["attemptIndex"]) ? payload["attemptIndex"] : undefined;
|
|
335
|
+
const matchedAttempt = attemptId ? loop.attempts.find((attempt) => attempt.attemptId === attemptId) : attemptIndex !== undefined ? loop.attempts.find((attempt) => attempt.index === attemptIndex) : undefined;
|
|
336
|
+
if (!matchedAttempt)
|
|
534
337
|
return undefined;
|
|
535
|
-
}
|
|
536
338
|
return {
|
|
537
339
|
timestamp: event.timestamp,
|
|
538
340
|
...(matchedAttempt.attemptId ? { attemptId: matchedAttempt.attemptId } : {}),
|
|
@@ -542,20 +344,17 @@ function normalizeLoopVerificationEvidence(loop, event) {
|
|
|
542
344
|
};
|
|
543
345
|
}
|
|
544
346
|
function normalizeLedgerVerificationEvidence(loop, event) {
|
|
545
|
-
if (!isTrustedVerificationTimestamp(event.timestamp))
|
|
347
|
+
if (!isTrustedVerificationTimestamp(event.timestamp))
|
|
546
348
|
return undefined;
|
|
547
|
-
|
|
548
|
-
if (event.attemptIndex === undefined || !Number.isInteger(event.attemptIndex)) {
|
|
349
|
+
if (event.attemptIndex === undefined || !Number.isInteger(event.attemptIndex))
|
|
549
350
|
return undefined;
|
|
550
|
-
}
|
|
551
351
|
const payload = isRecord(event.payload) ? event.payload : undefined;
|
|
552
352
|
const matchedAttempt = loop.attempts.find((attempt) => attempt.index === event.attemptIndex);
|
|
553
|
-
if (!matchedAttempt)
|
|
353
|
+
if (!matchedAttempt)
|
|
554
354
|
return undefined;
|
|
555
|
-
}
|
|
556
355
|
return {
|
|
557
356
|
timestamp: event.timestamp,
|
|
558
|
-
...(matchedAttempt
|
|
357
|
+
...(matchedAttempt.attemptId ? { attemptId: matchedAttempt.attemptId } : {}),
|
|
559
358
|
attemptIndex: event.attemptIndex,
|
|
560
359
|
...(typeof payload?.["passed"] === "boolean" ? { passed: payload["passed"] } : {}),
|
|
561
360
|
...(typeof payload?.["summary"] === "string" ? { summary: payload["summary"] } : {})
|
|
@@ -563,23 +362,15 @@ function normalizeLedgerVerificationEvidence(loop, event) {
|
|
|
563
362
|
}
|
|
564
363
|
function isTrustedVerificationTimestamp(value) {
|
|
565
364
|
const timestamp = new Date(value).getTime();
|
|
566
|
-
|
|
567
|
-
return false;
|
|
568
|
-
}
|
|
569
|
-
return timestamp <= Date.now() + 5 * 60_000;
|
|
365
|
+
return Number.isFinite(timestamp) && timestamp <= Date.now() + 5 * 60_000;
|
|
570
366
|
}
|
|
571
367
|
function isFutureVerificationTimestamp(value) {
|
|
572
368
|
const timestamp = new Date(value).getTime();
|
|
573
|
-
|
|
574
|
-
return false;
|
|
575
|
-
}
|
|
576
|
-
return timestamp > Date.now() + 5 * 60_000;
|
|
369
|
+
return Number.isFinite(timestamp) && timestamp > Date.now() + 5 * 60_000;
|
|
577
370
|
}
|
|
578
371
|
function getLedgerWarnings(ledgerEvents) {
|
|
579
372
|
const diagnostics = ledgerEvents;
|
|
580
|
-
return Array.isArray(diagnostics.warnings)
|
|
581
|
-
? diagnostics.warnings.filter((warning) => typeof warning === "string")
|
|
582
|
-
: [];
|
|
373
|
+
return Array.isArray(diagnostics.warnings) ? diagnostics.warnings.filter((warning) => typeof warning === "string") : [];
|
|
583
374
|
}
|
|
584
375
|
function isRecord(value) {
|
|
585
376
|
return typeof value === "object" && value !== null;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* MCP tools and integration tests use the same factories.
|
|
13
13
|
*/
|
|
14
14
|
import type { MartinAdapter, MartinAdapterRequest } from "../core/index.js";
|
|
15
|
+
import { type AgentExecutionIntent } from "../core/index.js";
|
|
15
16
|
import { type SpawnLike } from "./cli-bridge.js";
|
|
16
17
|
/**
|
|
17
18
|
* Given a prompt string, returns the full argv array to pass to spawn().
|
|
@@ -33,6 +34,8 @@ export interface AgentCliAdapterOptions {
|
|
|
33
34
|
workingDirectory?: string;
|
|
34
35
|
/** Timeout for the agent subprocess in ms. Defaults to 300_000 (5 min). */
|
|
35
36
|
timeoutMs?: number;
|
|
37
|
+
agentExecutionIntent?: AgentExecutionIntent;
|
|
38
|
+
providerExecutionTimeoutMs?: number;
|
|
36
39
|
/** Timeout per verification command in ms. Defaults to 120_000 (2 min). */
|
|
37
40
|
verifyTimeoutMs?: number;
|
|
38
41
|
/** Human-readable label shown in loop records. */
|
|
@@ -60,6 +63,8 @@ export interface AgentCliAdapterOptions {
|
|
|
60
63
|
export interface ClaudeCliAdapterOptions {
|
|
61
64
|
workingDirectory?: string;
|
|
62
65
|
timeoutMs?: number;
|
|
66
|
+
agentExecutionIntent?: AgentExecutionIntent;
|
|
67
|
+
providerExecutionTimeoutMs?: number;
|
|
63
68
|
verifyTimeoutMs?: number;
|
|
64
69
|
label?: string;
|
|
65
70
|
/** Override the model passed via --model flag. */
|
|
@@ -73,6 +78,8 @@ export interface CodexCliAdapterOptions {
|
|
|
73
78
|
command?: string;
|
|
74
79
|
workingDirectory?: string;
|
|
75
80
|
timeoutMs?: number;
|
|
81
|
+
agentExecutionIntent?: AgentExecutionIntent;
|
|
82
|
+
providerExecutionTimeoutMs?: number;
|
|
76
83
|
verifyTimeoutMs?: number;
|
|
77
84
|
label?: string;
|
|
78
85
|
/** Override the model passed via --model flag. */
|
|
@@ -94,6 +101,8 @@ export interface CodexCliAdapterOptions {
|
|
|
94
101
|
export interface GeminiCliAdapterOptions {
|
|
95
102
|
workingDirectory?: string;
|
|
96
103
|
timeoutMs?: number;
|
|
104
|
+
agentExecutionIntent?: AgentExecutionIntent;
|
|
105
|
+
providerExecutionTimeoutMs?: number;
|
|
97
106
|
verifyTimeoutMs?: number;
|
|
98
107
|
label?: string;
|
|
99
108
|
/** Explicit model override passed via --model. Omitted to preserve Gemini Auto. */
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* MCP tools and integration tests use the same factories.
|
|
13
13
|
*/
|
|
14
|
+
import { DEFAULT_AGENT_EXECUTION_INTENT, normalizeProviderExecutionTimeoutMs } from "../core/index.js";
|
|
14
15
|
import { readGitChangedFiles, readGitExecutionArtifacts, resolveGitRepositoryRoot, runSubprocess, runVerification } from "./cli-bridge.js";
|
|
15
16
|
import { buildCodexExecArgs } from "./codex-launcher.js";
|
|
16
17
|
import { createAdapterCapabilities, normalizeStructuredErrors, normalizeUsage } from "./runtime-support.js";
|
|
@@ -518,7 +519,8 @@ function inferStructuralClassHint(agentOutput, verificationSummary, exitCode, ob
|
|
|
518
519
|
// ---------------------------------------------------------------------------
|
|
519
520
|
export function createAgentCliAdapter(options) {
|
|
520
521
|
const workingDirectory = options.workingDirectory ?? process.cwd();
|
|
521
|
-
const
|
|
522
|
+
const agentExecutionIntent = options.agentExecutionIntent ?? DEFAULT_AGENT_EXECUTION_INTENT;
|
|
523
|
+
const timeoutMs = normalizeProviderExecutionTimeoutMs(options.providerExecutionTimeoutMs ?? options.timeoutMs);
|
|
522
524
|
const verifyTimeoutMs = options.verifyTimeoutMs ?? 120_000;
|
|
523
525
|
const adapterId = `agent-cli:${options.adapterIdSuffix ?? options.command}`;
|
|
524
526
|
const supportsJsonOutput = options.supportsJsonOutput === true;
|
|
@@ -531,6 +533,8 @@ export function createAgentCliAdapter(options) {
|
|
|
531
533
|
providerId: options.command,
|
|
532
534
|
...(options.model ? { model: options.model } : {}),
|
|
533
535
|
transport: "cli",
|
|
536
|
+
agentExecutionIntent,
|
|
537
|
+
providerExecutionTimeoutMs: timeoutMs,
|
|
534
538
|
capabilities: createAdapterCapabilities({
|
|
535
539
|
preflight: true,
|
|
536
540
|
usageSettlement: supportsUsageSettlement,
|
|
@@ -571,6 +575,7 @@ export function createAgentCliAdapter(options) {
|
|
|
571
575
|
}
|
|
572
576
|
const args = options.argsBuilder(prompt, request);
|
|
573
577
|
const stdinData = options.stdinBuilder?.(prompt);
|
|
578
|
+
const executionTimeoutMs = normalizeProviderExecutionTimeoutMs(request.context.providerExecutionTimeoutMs ?? timeoutMs);
|
|
574
579
|
// Live cumulative-cost circuit breaker: a single attempt should never be
|
|
575
580
|
// allowed to spend more than the loop has left. `--output-format json`
|
|
576
581
|
// only reports usage once the process exits, so for `stream-json` we
|
|
@@ -582,7 +587,7 @@ export function createAgentCliAdapter(options) {
|
|
|
582
587
|
: undefined;
|
|
583
588
|
const agentResult = await runSubprocess(options.command, args, {
|
|
584
589
|
cwd: workingDirectory,
|
|
585
|
-
timeoutMs,
|
|
590
|
+
timeoutMs: executionTimeoutMs,
|
|
586
591
|
spawnImpl: options.spawnImpl,
|
|
587
592
|
...(stdinData === undefined ? {} : { stdinData }),
|
|
588
593
|
...(streamingUsage ? { onStdoutChunk: streamingUsage.onChunk } : {}),
|
|
@@ -901,6 +906,8 @@ export function createClaudeCliAdapter(options = {}) {
|
|
|
901
906
|
label: options.label ?? "Claude CLI adapter",
|
|
902
907
|
workingDirectory: options.workingDirectory,
|
|
903
908
|
timeoutMs: options.timeoutMs,
|
|
909
|
+
agentExecutionIntent: options.agentExecutionIntent,
|
|
910
|
+
providerExecutionTimeoutMs: options.providerExecutionTimeoutMs,
|
|
904
911
|
verifyTimeoutMs: options.verifyTimeoutMs,
|
|
905
912
|
supportsJsonOutput: true,
|
|
906
913
|
streamingUsageCap: true,
|
|
@@ -952,6 +959,8 @@ export function createCodexCliAdapter(options = {}) {
|
|
|
952
959
|
label: options.label ?? "Codex CLI adapter",
|
|
953
960
|
workingDirectory,
|
|
954
961
|
timeoutMs: options.timeoutMs,
|
|
962
|
+
agentExecutionIntent: options.agentExecutionIntent,
|
|
963
|
+
providerExecutionTimeoutMs: options.providerExecutionTimeoutMs,
|
|
955
964
|
verifyTimeoutMs: options.verifyTimeoutMs,
|
|
956
965
|
supportsJsonOutput: false,
|
|
957
966
|
spawnImpl: options.spawnImpl,
|
|
@@ -979,7 +988,13 @@ export function createCodexCliAdapter(options = {}) {
|
|
|
979
988
|
*/
|
|
980
989
|
export function createGeminiCliAdapter(options = {}) {
|
|
981
990
|
const approvalMode = options.approvalMode ?? "yolo";
|
|
991
|
+
if (approvalMode !== "yolo") {
|
|
992
|
+
throw new Error("governed-autonomous Gemini execution requires yolo approval mode; interactive downgrade rejected.");
|
|
993
|
+
}
|
|
982
994
|
const extraArgs = options.extraArgs ?? [];
|
|
995
|
+
if (extraArgs.some((arg) => arg === "--approval-mode" || arg.startsWith("--approval-mode="))) {
|
|
996
|
+
throw new Error("Gemini approval mode cannot be overridden through extraArgs.");
|
|
997
|
+
}
|
|
983
998
|
return createAgentCliAdapter({
|
|
984
999
|
command: "gemini",
|
|
985
1000
|
adapterIdSuffix: "gemini",
|
|
@@ -987,6 +1002,8 @@ export function createGeminiCliAdapter(options = {}) {
|
|
|
987
1002
|
label: options.label ?? "Gemini CLI adapter",
|
|
988
1003
|
workingDirectory: options.workingDirectory,
|
|
989
1004
|
timeoutMs: options.timeoutMs,
|
|
1005
|
+
agentExecutionIntent: options.agentExecutionIntent,
|
|
1006
|
+
providerExecutionTimeoutMs: options.providerExecutionTimeoutMs,
|
|
990
1007
|
verifyTimeoutMs: options.verifyTimeoutMs,
|
|
991
1008
|
supportsJsonOutput: false,
|
|
992
1009
|
spawnImpl: options.spawnImpl,
|