@martinloop/mcp 0.5.3 → 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.
Files changed (45) hide show
  1. package/README.md +6 -6
  2. package/dist/server-validation.js +2 -0
  3. package/dist/server.js +30 -3
  4. package/dist/tools/run-loop.d.ts +1 -0
  5. package/dist/tools/run-loop.js +25 -4
  6. package/dist/tools/tool-response.d.ts +9 -1
  7. package/dist/tools/tool-response.js +7 -5
  8. package/dist/tools/tool-support.js +62 -271
  9. package/dist/vendor/adapters/claude-cli.d.ts +9 -0
  10. package/dist/vendor/adapters/claude-cli.js +19 -2
  11. package/dist/vendor/adapters/codex-capabilities.d.ts +12 -3
  12. package/dist/vendor/adapters/codex-capabilities.js +46 -18
  13. package/dist/vendor/adapters/codex-cli.d.ts +2 -1
  14. package/dist/vendor/adapters/codex-cli.js +12 -2
  15. package/dist/vendor/adapters/codex-host.d.ts +4 -1
  16. package/dist/vendor/adapters/codex-host.js +34 -16
  17. package/dist/vendor/adapters/codex-launcher.d.ts +1 -1
  18. package/dist/vendor/adapters/codex-launcher.js +1 -1
  19. package/dist/vendor/adapters/index.d.ts +2 -1
  20. package/dist/vendor/adapters/index.js +1 -0
  21. package/dist/vendor/contracts/agent-execution.d.ts +9 -0
  22. package/dist/vendor/contracts/agent-execution.js +14 -0
  23. package/dist/vendor/contracts/index.d.ts +9 -0
  24. package/dist/vendor/contracts/index.js +1 -0
  25. package/dist/vendor/core/grounding.js +42 -7
  26. package/dist/vendor/core/index.d.ts +5 -2
  27. package/dist/vendor/core/index.js +12 -3
  28. package/dist/vendor/presentation/governed-plan.d.ts +33 -0
  29. package/dist/vendor/presentation/governed-plan.js +146 -0
  30. package/dist/vendor/presentation/index.d.ts +7 -0
  31. package/dist/vendor/presentation/index.js +7 -0
  32. package/dist/vendor/presentation/motion.d.ts +12 -0
  33. package/dist/vendor/presentation/motion.js +123 -0
  34. package/dist/vendor/presentation/status.d.ts +8 -0
  35. package/dist/vendor/presentation/status.js +11 -0
  36. package/dist/vendor/presentation/table.d.ts +11 -0
  37. package/dist/vendor/presentation/table.js +47 -0
  38. package/dist/vendor/presentation/text.d.ts +6 -0
  39. package/dist/vendor/presentation/text.js +30 -0
  40. package/dist/vendor/presentation/theme.d.ts +31 -0
  41. package/dist/vendor/presentation/theme.js +82 -0
  42. package/dist/vendor/presentation/verified-handoff.d.ts +15 -0
  43. package/dist/vendor/presentation/verified-handoff.js +151 -0
  44. package/package.json +3 -2
  45. 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
- ...(resolvedPath ? { resolvedPath } : {})
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"), // pip, pipx
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
- const nvmBin = join(nvmDir, "current", "bin");
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
- .split(process.platform === "win32" ? ";" : ":")
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
- if (install) {
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
- ? "No verification.completed events were found in the loop record; using ledger evidence."
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
- status: "not_run",
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 (loop.receiptIntegrity ?? {
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
- .map((loop) => buildLoopPreview(loop))
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
- ...verificationEvents.map((event) => event.timestamp),
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
- const evidence = [
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 latestAttemptEvidence = evidence.filter((candidate) => latest.attemptId
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
- ? payload["attemptIndex"]
527
- : undefined;
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?.attemptId ? { attemptId: matchedAttempt.attemptId } : {}),
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
- if (!Number.isFinite(timestamp)) {
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
- if (!Number.isFinite(timestamp)) {
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 timeoutMs = options.timeoutMs ?? 300_000;
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,