@martinloop/mcp 0.2.7 → 0.3.1

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 (64) hide show
  1. package/README.md +49 -104
  2. package/dist/package-version.d.ts +1 -1
  3. package/dist/package-version.js +1 -1
  4. package/dist/prompts.d.ts +1 -1
  5. package/dist/resources.d.ts +1 -1
  6. package/dist/resources.js +2 -2
  7. package/dist/server-validation.d.ts +1 -0
  8. package/dist/server-validation.js +8 -0
  9. package/dist/server.js +87 -9
  10. package/dist/tools/doctor.d.ts +39 -1
  11. package/dist/tools/doctor.js +68 -9
  12. package/dist/tools/eval.js +3 -2
  13. package/dist/tools/get-run.d.ts +3 -0
  14. package/dist/tools/get-run.js +3 -1
  15. package/dist/tools/get-verification-results.d.ts +3 -0
  16. package/dist/tools/get-verification-results.js +3 -1
  17. package/dist/tools/plan.js +4 -2
  18. package/dist/tools/pr-tools.js +2 -1
  19. package/dist/tools/preflight.d.ts +41 -1
  20. package/dist/tools/preflight.js +74 -19
  21. package/dist/tools/run-dossier.d.ts +3 -0
  22. package/dist/tools/run-dossier.js +5 -2
  23. package/dist/tools/run-loop.d.ts +7 -2
  24. package/dist/tools/run-loop.js +67 -35
  25. package/dist/tools/run-store.js +67 -15
  26. package/dist/tools/tool-errors.js +1 -1
  27. package/dist/tools/tool-support.d.ts +8 -3
  28. package/dist/tools/tool-support.js +61 -18
  29. package/dist/tools/workflow-governance.d.ts +19 -3
  30. package/dist/tools/workflow-governance.js +107 -55
  31. package/dist/vendor/adapters/claude-cli.d.ts +45 -3
  32. package/dist/vendor/adapters/claude-cli.js +465 -45
  33. package/dist/vendor/adapters/cli-bridge.d.ts +46 -0
  34. package/dist/vendor/adapters/cli-bridge.js +147 -38
  35. package/dist/vendor/adapters/codex-launcher.d.ts +76 -0
  36. package/dist/vendor/adapters/codex-launcher.js +538 -0
  37. package/dist/vendor/adapters/index.d.ts +3 -2
  38. package/dist/vendor/adapters/index.js +3 -2
  39. package/dist/vendor/adapters/openai-compatible.d.ts +19 -4
  40. package/dist/vendor/adapters/openai-compatible.js +50 -19
  41. package/dist/vendor/adapters/runtime-support.d.ts +3 -0
  42. package/dist/vendor/adapters/runtime-support.js +9 -1
  43. package/dist/vendor/adapters/stub-direct-provider.js +3 -0
  44. package/dist/vendor/adapters/verifier-only.d.ts +2 -0
  45. package/dist/vendor/adapters/verifier-only.js +11 -4
  46. package/dist/vendor/contracts/index.d.ts +39 -0
  47. package/dist/vendor/contracts/index.js +2 -0
  48. package/dist/vendor/core/context-integrity.js +28 -3
  49. package/dist/vendor/core/grounding.d.ts +1 -0
  50. package/dist/vendor/core/grounding.js +6 -2
  51. package/dist/vendor/core/index.d.ts +24 -3
  52. package/dist/vendor/core/index.js +113 -21
  53. package/dist/vendor/core/leash.js +85 -8
  54. package/dist/vendor/core/persistence/index.d.ts +2 -0
  55. package/dist/vendor/core/persistence/index.js +1 -0
  56. package/dist/vendor/core/persistence/integrity.d.ts +38 -0
  57. package/dist/vendor/core/persistence/integrity.js +248 -0
  58. package/dist/vendor/core/persistence/store.d.ts +7 -0
  59. package/dist/vendor/core/persistence/store.js +25 -1
  60. package/dist/vendor/core/policy.d.ts +9 -0
  61. package/dist/workflow-state.d.ts +9 -0
  62. package/dist/workflow-state.js +46 -3
  63. package/package.json +2 -2
  64. package/server.json +2 -2
@@ -92,7 +92,7 @@ export function toToolFailure(error) {
92
92
  code: "engine_unavailable",
93
93
  category: "environment",
94
94
  message,
95
- suggestion: "Install the requested CLI or set MARTIN_LIVE=false for stub execution.",
95
+ suggestion: "Install the requested CLI or set MARTIN_LIVE=false for a no-spend proof run.",
96
96
  retryable: false
97
97
  };
98
98
  }
@@ -1,6 +1,6 @@
1
- import type { LoopArtifact, LoopBudget, LoopCost, LoopEvent, LoopTask } from "../vendor/contracts/index.js";
1
+ import type { LoopArtifact, LoopBudget, LoopCost, LoopEvent, LoopTask, ReceiptIntegritySummary, ReceiptScope } from "../vendor/contracts/index.js";
2
2
  import { type LedgerEvent, type LoopAttemptRecord, type LoopRunRecord } from "../vendor/core/index.js";
3
- export type MartinEngine = "claude" | "codex";
3
+ export type MartinEngine = "claude" | "codex" | "gemini";
4
4
  export interface InspectableLoopAttempt extends LoopAttemptRecord {
5
5
  attemptId?: string;
6
6
  summary?: string;
@@ -11,6 +11,8 @@ export interface InspectableLoopRecord extends Omit<LoopRunRecord, "attempts" |
11
11
  artifacts?: LoopArtifact[];
12
12
  events?: LoopEvent[];
13
13
  metadata?: Record<string, string>;
14
+ receiptIntegrity?: ReceiptIntegritySummary;
15
+ receiptScope?: ReceiptScope;
14
16
  }
15
17
  export interface LoopPreview {
16
18
  loopId: string;
@@ -87,10 +89,11 @@ export interface CliAvailability {
87
89
  locator: string;
88
90
  detail: string;
89
91
  resolvedPath?: string;
92
+ candidatePaths?: string[];
90
93
  }
91
94
  export interface ExecutionMode {
92
95
  liveMode: boolean;
93
- mode: "live" | "stub";
96
+ mode: "live" | "proof";
94
97
  detail: string;
95
98
  }
96
99
  export interface RunStoreInspection extends LoopCollectionSummary {
@@ -108,11 +111,13 @@ export interface CanonicalRunPaths {
108
111
  export declare function resolveExecutionMode(): ExecutionMode;
109
112
  export declare function detectCliAvailability(command: string): CliAvailability;
110
113
  export declare function getEngineAvailability(engine: MartinEngine): CliAvailability;
114
+ export declare function createSkippedCliAvailability(command: string, detail?: string): CliAvailability;
111
115
  export declare function formatUsd(value: number): string;
112
116
  export declare function buildLoopPreview(loop: InspectableLoopRecord): LoopPreview;
113
117
  export declare function buildAttemptSummary(attempt: InspectableLoopAttempt, artifacts?: AttemptArtifactFiles): AttemptSummary;
114
118
  export declare function buildArtifactSummary(loop: InspectableLoopRecord): ArtifactSummary;
115
119
  export declare function buildVerificationSummary(loop: InspectableLoopRecord, ledgerEvents?: LedgerEvent[]): VerificationSummary;
120
+ export declare function resolveReceiptIntegrity(loop: InspectableLoopRecord): ReceiptIntegritySummary;
116
121
  export declare function buildEventSummaries(loop: InspectableLoopRecord, limit?: number): EventSummary[];
117
122
  export declare function buildLoopCollectionSummary(loops: Array<LoopRunRecord | InspectableLoopRecord>): LoopCollectionSummary;
118
123
  export declare function inspectRunsRoot(runsRoot?: string): Promise<RunStoreInspection>;
@@ -1,4 +1,4 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { accessSync, constants } from "node:fs";
2
2
  import { readdir, stat } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { evaluateCostGovernor, resolveRunsRoot } from "../vendor/core/index.js";
@@ -11,10 +11,10 @@ export function resolveExecutionMode() {
11
11
  const liveMode = process.env.MARTIN_LIVE !== "false";
12
12
  return {
13
13
  liveMode,
14
- mode: liveMode ? "live" : "stub",
14
+ mode: liveMode ? "live" : "proof",
15
15
  detail: liveMode
16
16
  ? "Live CLI execution is enabled."
17
- : "Stub mode is active because MARTIN_LIVE=false."
17
+ : "Proof mode is active because MARTIN_LIVE=false."
18
18
  };
19
19
  }
20
20
  export function detectCliAvailability(command) {
@@ -23,18 +23,9 @@ export function detectCliAvailability(command) {
23
23
  if (cached && cached.expiresAt > Date.now()) {
24
24
  return cached.value;
25
25
  }
26
- const locator = process.platform === "win32" ? "where.exe" : "which";
27
- const result = spawnSync(locator, [command], {
28
- encoding: "utf8",
29
- stdio: ["ignore", "pipe", "pipe"]
30
- });
31
- const resolvedPath = result.status === 0
32
- ? (result.stdout ?? "")
33
- .split(/\r?\n/u)
34
- .map((line) => line.trim())
35
- .find(Boolean)
36
- : undefined;
37
- const value = result.status === 0
26
+ const locator = process.platform === "win32" ? "path-scan(win32)" : "path-scan(posix)";
27
+ const resolvedPath = findCommandOnPath(command);
28
+ const value = resolvedPath
38
29
  ? {
39
30
  command,
40
31
  available: true,
@@ -54,9 +45,54 @@ export function detectCliAvailability(command) {
54
45
  });
55
46
  return value;
56
47
  }
48
+ function findCommandOnPath(command) {
49
+ const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path");
50
+ const rawPath = pathKey ? process.env[pathKey] : undefined;
51
+ if (!rawPath) {
52
+ return undefined;
53
+ }
54
+ const pathEntries = rawPath
55
+ .split(process.platform === "win32" ? ";" : ":")
56
+ .map((entry) => entry.trim())
57
+ .filter(Boolean);
58
+ const hasExtension = /\.[A-Za-z0-9]+$/u.test(command);
59
+ const candidateNames = process.platform === "win32" && !hasExtension
60
+ ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
61
+ .split(";")
62
+ .map((extension) => extension.trim())
63
+ .filter(Boolean)
64
+ .map((extension) => `${command}${extension.toLowerCase()}`)
65
+ : [command];
66
+ for (const directory of pathEntries) {
67
+ for (const candidateName of candidateNames) {
68
+ const candidatePath = join(directory, candidateName);
69
+ if (isExecutablePath(candidatePath)) {
70
+ return candidatePath;
71
+ }
72
+ }
73
+ }
74
+ return undefined;
75
+ }
76
+ function isExecutablePath(candidatePath) {
77
+ try {
78
+ accessSync(candidatePath, process.platform === "win32" ? constants.F_OK : constants.X_OK);
79
+ return true;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
57
85
  export function getEngineAvailability(engine) {
58
86
  return detectCliAvailability(engine);
59
87
  }
88
+ export function createSkippedCliAvailability(command, detail = "Proof mode skipped live CLI availability detection.") {
89
+ return {
90
+ command,
91
+ available: false,
92
+ locator: "skipped",
93
+ detail
94
+ };
95
+ }
60
96
  export function formatUsd(value) {
61
97
  return `$${value.toFixed(2)}`;
62
98
  }
@@ -126,7 +162,11 @@ export function buildVerificationSummary(loop, ledgerEvents = []) {
126
162
  const verificationEvents = (loop.events ?? []).filter((event) => event.type === "verification.completed");
127
163
  const verificationLedgerEvents = ledgerEvents.filter((event) => event.kind === "verification.completed");
128
164
  const warnings = [];
165
+ const integrity = resolveReceiptIntegrity(loop);
129
166
  const ledgerWarnings = getLedgerWarnings(ledgerEvents);
167
+ if (integrity.state !== "verified") {
168
+ warnings.push(`Receipt integrity is ${integrity.state}; persisted verifier evidence is not trustworthy yet.`);
169
+ }
130
170
  warnings.push(...ledgerWarnings);
131
171
  if (verificationEvents.length === 0) {
132
172
  warnings.push(verificationLedgerEvents.length > 0
@@ -163,6 +203,12 @@ export function buildVerificationSummary(loop, ledgerEvents = []) {
163
203
  warnings
164
204
  };
165
205
  }
206
+ export function resolveReceiptIntegrity(loop) {
207
+ return (loop.receiptIntegrity ?? {
208
+ state: "unsigned",
209
+ reason: "Receipt integrity metadata was not available on the loop record."
210
+ });
211
+ }
166
212
  export function buildEventSummaries(loop, limit = 5) {
167
213
  return (loop.events ?? [])
168
214
  .slice(-limit)
@@ -287,9 +333,6 @@ export function buildSuggestedResourceUris(loopId) {
287
333
  `martin://runs/${loopId}/verification`,
288
334
  "martin://guides/mcp-usage",
289
335
  "martin://guides/agent-start",
290
- "martin://guides/command-map",
291
- "martin://guides/ide-onboarding",
292
- "martin://guides/operating-rules",
293
336
  "martin://guides/publish-readiness"
294
337
  ];
295
338
  }
@@ -1,3 +1,4 @@
1
+ import { type LoopBudget } from "../vendor/contracts/index.js";
1
2
  import { type RunStoreInspection } from "./tool-support.js";
2
3
  export type MartinPolicyPack = "solo-founder" | "startup-team" | "enterprise-strict" | "oss-maintainer" | "security-sensitive";
3
4
  export interface RepoGitState {
@@ -23,6 +24,7 @@ export interface RepoSignals {
23
24
  packageScripts: Record<string, string>;
24
25
  git: RepoGitState;
25
26
  sensitivePaths: string[];
27
+ hostAvailabilityChecked: boolean;
26
28
  availableHosts: Record<"claude" | "codex" | "cursor" | "gemini", {
27
29
  available: boolean;
28
30
  detail: string;
@@ -110,11 +112,25 @@ interface ContractOverrides {
110
112
  maxFilesChanged?: number;
111
113
  maxCommands?: number;
112
114
  }
113
- export declare function inspectRepoSignals(workingDirectory: string): RepoSignals;
115
+ interface LoopBudgetOverrides {
116
+ maxUsd?: number;
117
+ softLimitUsd?: number;
118
+ maxIterations?: number;
119
+ maxTokens?: number;
120
+ }
121
+ export declare function inspectRepoSignals(workingDirectory: string, options?: {
122
+ includeHostAvailability?: boolean;
123
+ }): RepoSignals;
114
124
  export declare function buildReadinessReport(signals: RepoSignals, runStore: RunStoreInspection): MartinReadinessReport;
115
125
  export declare function buildPolicyPackDefinition(policyPack: MartinPolicyPack | undefined, signals: RepoSignals): MartinPolicyPackDefinition;
116
- export declare function buildPlanProposal(workingDirectory: string, overrides: ContractOverrides): MartinPlanProposal;
117
- export declare function buildRunContract(workingDirectory: string, overrides: ContractOverrides): MartinRunContract;
126
+ export declare function buildPlanProposal(workingDirectory: string, overrides: ContractOverrides, options?: {
127
+ signals?: RepoSignals;
128
+ }): MartinPlanProposal;
129
+ export declare function buildRunContract(workingDirectory: string, overrides: ContractOverrides, options?: {
130
+ signals?: RepoSignals;
131
+ plan?: MartinPlanProposal;
132
+ }): MartinRunContract;
133
+ export declare function normalizeLoopBudget(overrides?: LoopBudgetOverrides): LoopBudget;
118
134
  export declare function assessRunRisk(input: {
119
135
  objective: string;
120
136
  context?: string;
@@ -2,13 +2,17 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { DEFAULT_BUDGET } from "../vendor/contracts/index.js";
5
- import { detectCliAvailability } from "./tool-support.js";
5
+ import { createSkippedCliAvailability, detectCliAvailability } from "./tool-support.js";
6
6
  const HOST_COMMANDS = {
7
7
  claude: "claude",
8
8
  codex: "codex",
9
9
  cursor: "cursor",
10
10
  gemini: "gemini"
11
11
  };
12
+ const REPO_SIGNALS_CACHE_TTL_MS = 5_000;
13
+ const repoSignalsCache = new Map();
14
+ const GIT_STATE_CACHE_TTL_MS = 60_000;
15
+ const repoGitStateCache = new Map();
12
16
  const POLICY_PACKS = {
13
17
  "solo-founder": {
14
18
  name: "solo-founder",
@@ -110,13 +114,19 @@ const POLICY_PACKS = {
110
114
  requireApprovalAtOrAbove: "medium"
111
115
  }
112
116
  };
113
- export function inspectRepoSignals(workingDirectory) {
117
+ export function inspectRepoSignals(workingDirectory, options = {}) {
118
+ const includeHostAvailability = options.includeHostAvailability ?? true;
119
+ const cacheKey = `${workingDirectory}::hosts=${includeHostAvailability ? "live" : "skipped"}`;
120
+ const cached = repoSignalsCache.get(cacheKey);
121
+ if (cached && cached.expiresAt > Date.now()) {
122
+ return cached.value;
123
+ }
114
124
  const packageScripts = readPackageScripts(workingDirectory);
115
125
  const packageManager = detectPackageManager(workingDirectory);
116
126
  const frameworks = detectFrameworks(workingDirectory, packageScripts);
117
127
  const languages = detectLanguages(workingDirectory, frameworks);
118
128
  const verifiers = detectVerifierCommands(packageScripts, packageManager);
119
- return {
129
+ const signals = {
120
130
  workingDirectory,
121
131
  packageManager,
122
132
  languages,
@@ -125,13 +135,27 @@ export function inspectRepoSignals(workingDirectory) {
125
135
  packageScripts,
126
136
  git: detectGitState(workingDirectory),
127
137
  sensitivePaths: detectSensitivePaths(workingDirectory),
138
+ hostAvailabilityChecked: includeHostAvailability,
128
139
  availableHosts: {
129
- claude: detectCliAvailability(HOST_COMMANDS.claude),
130
- codex: detectCliAvailability(HOST_COMMANDS.codex),
131
- cursor: detectCliAvailability(HOST_COMMANDS.cursor),
132
- gemini: detectCliAvailability(HOST_COMMANDS.gemini)
140
+ claude: includeHostAvailability
141
+ ? detectCliAvailability(HOST_COMMANDS.claude)
142
+ : createSkippedCliAvailability(HOST_COMMANDS.claude),
143
+ codex: includeHostAvailability
144
+ ? detectCliAvailability(HOST_COMMANDS.codex)
145
+ : createSkippedCliAvailability(HOST_COMMANDS.codex),
146
+ cursor: includeHostAvailability
147
+ ? detectCliAvailability(HOST_COMMANDS.cursor)
148
+ : createSkippedCliAvailability(HOST_COMMANDS.cursor),
149
+ gemini: includeHostAvailability
150
+ ? detectCliAvailability(HOST_COMMANDS.gemini)
151
+ : createSkippedCliAvailability(HOST_COMMANDS.gemini)
133
152
  }
134
153
  };
154
+ repoSignalsCache.set(cacheKey, {
155
+ expiresAt: Date.now() + REPO_SIGNALS_CACHE_TTL_MS,
156
+ value: signals
157
+ });
158
+ return signals;
135
159
  }
136
160
  export function buildReadinessReport(signals, runStore) {
137
161
  const missingSafeguards = [];
@@ -155,7 +179,9 @@ export function buildReadinessReport(signals, runStore) {
155
179
  if (signals.frameworks.length === 0) {
156
180
  score -= 8;
157
181
  }
158
- if (!signals.availableHosts.claude.available && !signals.availableHosts.codex.available) {
182
+ if (signals.hostAvailabilityChecked &&
183
+ !signals.availableHosts.claude.available &&
184
+ !signals.availableHosts.codex.available) {
159
185
  score -= 18;
160
186
  }
161
187
  score = Math.max(0, Math.min(100, score));
@@ -189,8 +215,8 @@ export function buildPolicyPackDefinition(policyPack, signals) {
189
215
  : fallbackVerifierPlan(signals.packageManager)
190
216
  };
191
217
  }
192
- export function buildPlanProposal(workingDirectory, overrides) {
193
- const signals = inspectRepoSignals(workingDirectory);
218
+ export function buildPlanProposal(workingDirectory, overrides, options = {}) {
219
+ const signals = options.signals ?? inspectRepoSignals(workingDirectory);
194
220
  const policy = buildPolicyPackDefinition(overrides.policyPack, signals);
195
221
  const scope = inferScopeFromObjective(overrides.objective, policy, overrides);
196
222
  const estimatedBudget = buildBudget(overrides, signals);
@@ -223,8 +249,8 @@ export function buildPlanProposal(workingDirectory, overrides) {
223
249
  ]
224
250
  };
225
251
  }
226
- export function buildRunContract(workingDirectory, overrides) {
227
- const plan = buildPlanProposal(workingDirectory, overrides);
252
+ export function buildRunContract(workingDirectory, overrides, options = {}) {
253
+ const plan = options.plan ?? buildPlanProposal(workingDirectory, overrides, options);
228
254
  return {
229
255
  objective: overrides.objective,
230
256
  ...(overrides.context ? { context: overrides.context } : {}),
@@ -238,6 +264,16 @@ export function buildRunContract(workingDirectory, overrides) {
238
264
  shouldRequireApproval(plan.policyPack.requireApprovalAtOrAbove, plan.risk.level)
239
265
  };
240
266
  }
267
+ export function normalizeLoopBudget(overrides = {}) {
268
+ const maxUsd = overrides.maxUsd ?? DEFAULT_BUDGET.maxUsd;
269
+ const softLimitUsd = Math.min(overrides.softLimitUsd ?? DEFAULT_BUDGET.softLimitUsd, maxUsd);
270
+ return {
271
+ maxUsd,
272
+ softLimitUsd,
273
+ maxIterations: overrides.maxIterations ?? DEFAULT_BUDGET.maxIterations,
274
+ maxTokens: overrides.maxTokens ?? DEFAULT_BUDGET.maxTokens
275
+ };
276
+ }
241
277
  export function assessRunRisk(input) {
242
278
  const reasons = [];
243
279
  let score = 12;
@@ -406,17 +442,10 @@ function detectVerifierCommands(scripts, packageManager) {
406
442
  return { test, lint, build, defaultPlan };
407
443
  }
408
444
  function detectGitState(workingDirectory) {
409
- const availability = spawnSync("git", ["--version"], {
410
- cwd: workingDirectory,
411
- encoding: "utf8",
412
- stdio: ["ignore", "pipe", "pipe"]
413
- });
414
- if (availability.status !== 0) {
415
- return {
416
- available: false,
417
- isRepo: false,
418
- clean: false
419
- };
445
+ const cacheKey = workingDirectory;
446
+ const cached = repoGitStateCache.get(cacheKey);
447
+ if (cached && cached.expiresAt > Date.now()) {
448
+ return cached.value;
420
449
  }
421
450
  const isRepo = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
422
451
  cwd: workingDirectory,
@@ -424,18 +453,29 @@ function detectGitState(workingDirectory) {
424
453
  stdio: ["ignore", "pipe", "pipe"]
425
454
  });
426
455
  if (isRepo.status !== 0 || !/true/u.test(isRepo.stdout ?? "")) {
427
- return {
428
- available: true,
429
- isRepo: false,
430
- clean: false
431
- };
456
+ const availability = spawnSync("git", ["--version"], {
457
+ cwd: workingDirectory,
458
+ encoding: "utf8",
459
+ stdio: ["ignore", "pipe", "pipe"]
460
+ });
461
+ const value = availability.status !== 0
462
+ ? {
463
+ available: false,
464
+ isRepo: false,
465
+ clean: false
466
+ }
467
+ : {
468
+ available: true,
469
+ isRepo: false,
470
+ clean: false
471
+ };
472
+ repoGitStateCache.set(cacheKey, {
473
+ expiresAt: Date.now() + GIT_STATE_CACHE_TTL_MS,
474
+ value
475
+ });
476
+ return value;
432
477
  }
433
- const branch = spawnSync("git", ["branch", "--show-current"], {
434
- cwd: workingDirectory,
435
- encoding: "utf8",
436
- stdio: ["ignore", "pipe", "pipe"]
437
- }).stdout.trim();
438
- const status = spawnSync("git", ["status", "--porcelain", "--branch"], {
478
+ const status = spawnSync("git", ["status", "--porcelain=v2", "--branch", "--untracked-files=normal", "--ignored=no", "--", "."], {
439
479
  cwd: workingDirectory,
440
480
  encoding: "utf8",
441
481
  stdio: ["ignore", "pipe", "pipe"]
@@ -444,20 +484,42 @@ function detectGitState(workingDirectory) {
444
484
  .split(/\r?\n/u)
445
485
  .map((line) => line.trim())
446
486
  .filter(Boolean);
447
- const dirty = statusLines.some((line) => !line.startsWith("##"));
448
- const header = statusLines.find((line) => line.startsWith("##"));
449
- const upstream = header?.match(/\.\.\.([^\s[]+)/u)?.[1];
450
- const ahead = parseCount(header, /ahead (\d+)/u);
451
- const behind = parseCount(header, /behind (\d+)/u);
452
- return {
487
+ const dirty = statusLines.some((line) => !line.startsWith("#"));
488
+ const branch = statusLines
489
+ .find((line) => line.startsWith("# branch.head "))
490
+ ?.replace("# branch.head ", "")
491
+ .trim();
492
+ const upstream = statusLines
493
+ .find((line) => line.startsWith("# branch.upstream "))
494
+ ?.replace("# branch.upstream ", "")
495
+ .trim();
496
+ const aheadBehind = statusLines
497
+ .find((line) => line.startsWith("# branch.ab "))
498
+ ?.replace("# branch.ab ", "")
499
+ .trim()
500
+ .split(/\s+/u);
501
+ const aheadToken = aheadBehind?.find((token) => token.startsWith("+"));
502
+ const behindToken = aheadBehind?.find((token) => token.startsWith("-"));
503
+ const ahead = aheadToken && aheadToken.length > 1
504
+ ? Number.parseInt(aheadToken.slice(1), 10)
505
+ : undefined;
506
+ const behind = behindToken && behindToken.length > 1
507
+ ? Number.parseInt(behindToken.slice(1), 10)
508
+ : undefined;
509
+ const value = {
453
510
  available: true,
454
511
  isRepo: true,
455
512
  clean: !dirty,
456
- ...(branch ? { branch } : {}),
513
+ ...(branch && branch !== "(detached)" ? { branch } : {}),
457
514
  ...(upstream ? { upstream } : {}),
458
- ...(ahead !== undefined ? { ahead } : {}),
459
- ...(behind !== undefined ? { behind } : {})
515
+ ...(Number.isFinite(ahead) ? { ahead } : {}),
516
+ ...(Number.isFinite(behind) ? { behind } : {})
460
517
  };
518
+ repoGitStateCache.set(cacheKey, {
519
+ expiresAt: Date.now() + GIT_STATE_CACHE_TTL_MS,
520
+ value
521
+ });
522
+ return value;
461
523
  }
462
524
  function detectSensitivePaths(workingDirectory) {
463
525
  const candidates = [
@@ -520,11 +582,9 @@ function inferScopeFromObjective(objective, policy, overrides) {
520
582
  }
521
583
  function buildBudget(overrides, signals) {
522
584
  const defaultCommands = signals.verifiers.defaultPlan.length > 0 ? 12 : 8;
585
+ const normalizedBudget = normalizeLoopBudget(overrides);
523
586
  return {
524
- maxUsd: overrides.maxUsd ?? DEFAULT_BUDGET.maxUsd,
525
- softLimitUsd: Math.min(overrides.maxUsd ?? DEFAULT_BUDGET.maxUsd, DEFAULT_BUDGET.softLimitUsd),
526
- maxIterations: overrides.maxIterations ?? DEFAULT_BUDGET.maxIterations,
527
- maxTokens: overrides.maxTokens ?? DEFAULT_BUDGET.maxTokens,
587
+ ...normalizedBudget,
528
588
  maxMinutes: overrides.maxMinutes ?? 20,
529
589
  maxFilesChanged: overrides.maxFilesChanged ?? 8,
530
590
  maxCommands: overrides.maxCommands ?? defaultCommands
@@ -571,11 +631,3 @@ function shouldRequireApproval(threshold, level) {
571
631
  const ordering = ["low", "medium", "high"];
572
632
  return ordering.indexOf(level) >= ordering.indexOf(threshold);
573
633
  }
574
- function parseCount(value, pattern) {
575
- const match = value?.match(pattern)?.[1];
576
- if (!match) {
577
- return undefined;
578
- }
579
- const parsed = Number.parseInt(match, 10);
580
- return Number.isFinite(parsed) ? parsed : undefined;
581
- }
@@ -45,6 +45,15 @@ export interface AgentCliAdapterOptions {
45
45
  * Defaults to true for Claude.
46
46
  */
47
47
  supportsJsonOutput?: boolean;
48
+ /**
49
+ * Set when `argsBuilder` requests `--output-format stream-json` (newline-
50
+ * delimited JSON events) rather than single-blob `json`. Enables (a)
51
+ * incremental result parsing that scans for the final `result` event, and
52
+ * (b) a live cumulative-cost circuit breaker that terminates the subprocess
53
+ * the moment projected spend crosses the remaining per-attempt budget,
54
+ * rather than only learning about an overspend after the process exits.
55
+ */
56
+ streamingUsageCap?: boolean;
48
57
  /** Test-only override for subprocess spawning. */
49
58
  spawnImpl?: SpawnLike;
50
59
  }
@@ -60,6 +69,8 @@ export interface ClaudeCliAdapterOptions {
60
69
  spawnImpl?: SpawnLike;
61
70
  }
62
71
  export interface CodexCliAdapterOptions {
72
+ /** Override the executable or absolute command path used to launch Codex. */
73
+ command?: string;
63
74
  workingDirectory?: string;
64
75
  timeoutMs?: number;
65
76
  verifyTimeoutMs?: number;
@@ -80,12 +91,33 @@ export interface CodexCliAdapterOptions {
80
91
  extraArgs?: string[];
81
92
  spawnImpl?: SpawnLike;
82
93
  }
94
+ export interface GeminiCliAdapterOptions {
95
+ workingDirectory?: string;
96
+ timeoutMs?: number;
97
+ verifyTimeoutMs?: number;
98
+ label?: string;
99
+ /** Override the model passed via --model flag. Defaults to the Gemini `flash` alias. */
100
+ model?: string;
101
+ /** Approval mode for headless Gemini runs. Defaults to yolo for autonomous execution. */
102
+ approvalMode?: "default" | "auto_edit" | "yolo" | "plan";
103
+ /** Enable Gemini sandbox mode when the host is configured for it. Disabled by default. */
104
+ sandbox?: boolean;
105
+ /** Extra args appended after core args. */
106
+ extraArgs?: string[];
107
+ spawnImpl?: SpawnLike;
108
+ }
83
109
  export declare function createAgentCliAdapter(options: AgentCliAdapterOptions): MartinAdapter;
84
110
  /**
85
- * Spawns `claude --output-format json --print "<prompt>" --dangerously-skip-permissions [extraArgs]`.
111
+ * Spawns `claude --output-format stream-json --verbose --print "<prompt>" [extraArgs]`.
86
112
  *
87
- * The --output-format json flag causes Claude CLI to return structured JSON
88
- * including real token usage counts, enabling accurate cost tracking.
113
+ * `stream-json` emits one JSON event per line including per-turn usage on
114
+ * each `assistant` message and a final `result` event carrying the same
115
+ * `result`/`usage`/`total_cost_usd` fields as single-blob `json` output — so
116
+ * MartinLoop can both (a) recover real token usage/cost as before, and
117
+ * (b) watch cumulative spend live and self-terminate the subprocess the
118
+ * moment it crosses the remaining per-attempt budget (see
119
+ * `streamingUsageCap` / `createStreamingUsageInspector`), instead of only
120
+ * discovering an overspend after the whole process has already exited.
89
121
  *
90
122
  * Requires the Claude Code CLI to be installed and authenticated:
91
123
  * https://docs.anthropic.com/claude-code
@@ -102,3 +134,13 @@ export declare function createClaudeCliAdapter(options?: ClaudeCliAdapterOptions
102
134
  * npm install -g @openai/codex
103
135
  */
104
136
  export declare function createCodexCliAdapter(options?: CodexCliAdapterOptions): MartinAdapter;
137
+ /**
138
+ * Spawns `gemini --model <model> --prompt "" --approval-mode <mode> --output-format json [...]`.
139
+ *
140
+ * The prompt is delivered via stdin while forcing headless mode with `--prompt ""`,
141
+ * which keeps large MartinLoop prompts off the command line on Windows.
142
+ *
143
+ * Requires the Gemini CLI to be installed and authenticated:
144
+ * npm install -g @google/gemini-cli
145
+ */
146
+ export declare function createGeminiCliAdapter(options?: GeminiCliAdapterOptions): MartinAdapter;