@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.
- package/README.md +49 -104
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/prompts.d.ts +1 -1
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +2 -2
- package/dist/server-validation.d.ts +1 -0
- package/dist/server-validation.js +8 -0
- package/dist/server.js +87 -9
- package/dist/tools/doctor.d.ts +39 -1
- package/dist/tools/doctor.js +68 -9
- package/dist/tools/eval.js +3 -2
- package/dist/tools/get-run.d.ts +3 -0
- package/dist/tools/get-run.js +3 -1
- package/dist/tools/get-verification-results.d.ts +3 -0
- package/dist/tools/get-verification-results.js +3 -1
- package/dist/tools/plan.js +4 -2
- package/dist/tools/pr-tools.js +2 -1
- package/dist/tools/preflight.d.ts +41 -1
- package/dist/tools/preflight.js +74 -19
- package/dist/tools/run-dossier.d.ts +3 -0
- package/dist/tools/run-dossier.js +5 -2
- package/dist/tools/run-loop.d.ts +7 -2
- package/dist/tools/run-loop.js +67 -35
- package/dist/tools/run-store.js +67 -15
- package/dist/tools/tool-errors.js +1 -1
- package/dist/tools/tool-support.d.ts +8 -3
- package/dist/tools/tool-support.js +61 -18
- package/dist/tools/workflow-governance.d.ts +19 -3
- package/dist/tools/workflow-governance.js +107 -55
- package/dist/vendor/adapters/claude-cli.d.ts +45 -3
- package/dist/vendor/adapters/claude-cli.js +465 -45
- package/dist/vendor/adapters/cli-bridge.d.ts +46 -0
- package/dist/vendor/adapters/cli-bridge.js +147 -38
- package/dist/vendor/adapters/codex-launcher.d.ts +76 -0
- package/dist/vendor/adapters/codex-launcher.js +538 -0
- package/dist/vendor/adapters/index.d.ts +3 -2
- package/dist/vendor/adapters/index.js +3 -2
- package/dist/vendor/adapters/openai-compatible.d.ts +19 -4
- package/dist/vendor/adapters/openai-compatible.js +50 -19
- package/dist/vendor/adapters/runtime-support.d.ts +3 -0
- package/dist/vendor/adapters/runtime-support.js +9 -1
- package/dist/vendor/adapters/stub-direct-provider.js +3 -0
- package/dist/vendor/adapters/verifier-only.d.ts +2 -0
- package/dist/vendor/adapters/verifier-only.js +11 -4
- package/dist/vendor/contracts/index.d.ts +39 -0
- package/dist/vendor/contracts/index.js +2 -0
- package/dist/vendor/core/context-integrity.js +28 -3
- package/dist/vendor/core/grounding.d.ts +1 -0
- package/dist/vendor/core/grounding.js +6 -2
- package/dist/vendor/core/index.d.ts +24 -3
- package/dist/vendor/core/index.js +113 -21
- package/dist/vendor/core/leash.js +85 -8
- package/dist/vendor/core/persistence/index.d.ts +2 -0
- package/dist/vendor/core/persistence/index.js +1 -0
- package/dist/vendor/core/persistence/integrity.d.ts +38 -0
- package/dist/vendor/core/persistence/integrity.js +248 -0
- package/dist/vendor/core/persistence/store.d.ts +7 -0
- package/dist/vendor/core/persistence/store.js +25 -1
- package/dist/vendor/core/policy.d.ts +9 -0
- package/dist/workflow-state.d.ts +9 -0
- package/dist/workflow-state.js +46 -3
- package/package.json +2 -2
- 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
|
|
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" | "
|
|
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 {
|
|
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" : "
|
|
14
|
+
mode: liveMode ? "live" : "proof",
|
|
15
15
|
detail: liveMode
|
|
16
16
|
? "Live CLI execution is enabled."
|
|
17
|
-
: "
|
|
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" ? "
|
|
27
|
-
const
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
117
|
-
|
|
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
|
-
|
|
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:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 (
|
|
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
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
|
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
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|
459
|
-
...(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
|
-
|
|
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>"
|
|
111
|
+
* Spawns `claude --output-format stream-json --verbose --print "<prompt>" [extraArgs]`.
|
|
86
112
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
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;
|