@martinloop/mcp 0.3.9 → 0.5.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 +82 -141
- package/dist/discovery-metadata.d.ts +1 -1
- package/dist/discovery-metadata.js +1 -1
- package/dist/resources.d.ts +1 -2
- package/dist/resources.js +62 -98
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +16 -11
- package/dist/server.d.ts +23 -0
- package/dist/server.js +192 -15
- package/dist/tools/doctor.js +5 -4
- package/dist/tools/get-status.d.ts +2 -0
- package/dist/tools/get-status.js +1 -0
- package/dist/tools/preflight.d.ts +2 -0
- package/dist/tools/preflight.js +43 -3
- package/dist/tools/run-dossier.d.ts +3 -1
- package/dist/tools/run-dossier.js +40 -2
- package/dist/tools/run-loop.d.ts +5 -3
- package/dist/tools/run-loop.js +15 -9
- package/dist/tools/tool-errors.js +1 -1
- package/dist/tools/tool-response.js +9 -2
- package/dist/tools/tool-support.d.ts +4 -2
- package/dist/tools/tool-support.js +6 -15
- package/dist/vendor/adapters/claude-cli.d.ts +2 -2
- package/dist/vendor/adapters/claude-cli.js +97 -69
- package/dist/vendor/adapters/cli-bridge.d.ts +9 -1
- package/dist/vendor/adapters/cli-bridge.js +54 -13
- package/dist/vendor/adapters/codex-launcher.d.ts +61 -1
- package/dist/vendor/adapters/codex-launcher.js +118 -19
- package/dist/vendor/adapters/index.d.ts +2 -4
- package/dist/vendor/adapters/index.js +2 -4
- package/dist/vendor/adapters/openai-compatible.d.ts +2 -3
- package/dist/vendor/adapters/openai-compatible.js +80 -15
- package/dist/vendor/adapters/verifier-only.js +9 -4
- package/dist/vendor/contracts/adapter-capabilities.d.ts +1 -1
- package/dist/vendor/contracts/context-handoff.d.ts +100 -0
- package/dist/vendor/contracts/context-handoff.js +11 -0
- package/dist/vendor/contracts/context-manifest.d.ts +195 -0
- package/dist/vendor/contracts/context-manifest.js +17 -0
- package/dist/vendor/contracts/context-shadow.d.ts +75 -0
- package/dist/vendor/contracts/context-shadow.js +8 -0
- package/dist/vendor/contracts/delivery.d.ts +47 -0
- package/dist/vendor/contracts/delivery.js +18 -0
- package/dist/vendor/contracts/exits.d.ts +126 -0
- package/dist/vendor/contracts/exits.js +32 -0
- package/dist/vendor/contracts/index.d.ts +55 -6
- package/dist/vendor/contracts/index.js +24 -0
- package/dist/vendor/contracts/mission.d.ts +102 -0
- package/dist/vendor/contracts/mission.js +64 -0
- package/dist/vendor/contracts/operator.d.ts +3 -1
- package/dist/vendor/contracts/operator.js +2 -1
- package/dist/vendor/contracts/verified-handoff.d.ts +106 -0
- package/dist/vendor/contracts/verified-handoff.js +40 -0
- package/dist/vendor/core/context-chain-gate.d.ts +89 -0
- package/dist/vendor/core/context-chain-gate.js +173 -0
- package/dist/vendor/core/context-compiler.d.ts +53 -0
- package/dist/vendor/core/context-compiler.js +261 -0
- package/dist/vendor/core/context-handoff.d.ts +42 -0
- package/dist/vendor/core/context-handoff.js +167 -0
- package/dist/vendor/core/context-shadow.d.ts +39 -0
- package/dist/vendor/core/context-shadow.js +200 -0
- package/dist/vendor/core/delivery/index.d.ts +6 -0
- package/dist/vendor/core/delivery/index.js +4 -0
- package/dist/vendor/core/delivery/message-client.d.ts +18 -0
- package/dist/vendor/core/delivery/message-client.js +38 -0
- package/dist/vendor/core/delivery/message-ledger.d.ts +35 -0
- package/dist/vendor/core/delivery/message-ledger.js +97 -0
- package/dist/vendor/core/delivery/message-schema.d.ts +16 -0
- package/dist/vendor/core/delivery/message-schema.js +111 -0
- package/dist/vendor/core/delivery/update-check.d.ts +19 -0
- package/dist/vendor/core/delivery/update-check.js +107 -0
- package/dist/vendor/core/exit-signal.d.ts +88 -0
- package/dist/vendor/core/exit-signal.js +339 -0
- package/dist/vendor/core/exits.d.ts +41 -0
- package/dist/vendor/core/exits.js +258 -0
- package/dist/vendor/core/grounding.d.ts +2 -1
- package/dist/vendor/core/grounding.js +40 -5
- package/dist/vendor/core/index.d.ts +47 -6
- package/dist/vendor/core/index.js +1102 -670
- package/dist/vendor/core/mission/aggregate.d.ts +27 -0
- package/dist/vendor/core/mission/aggregate.js +42 -0
- package/dist/vendor/core/mission/index.d.ts +2 -0
- package/dist/vendor/core/mission/index.js +1 -0
- package/dist/vendor/core/persistence/compiler.d.ts +8 -0
- package/dist/vendor/core/persistence/compiler.js +156 -0
- package/dist/vendor/core/persistence/context-handoff-store.d.ts +68 -0
- package/dist/vendor/core/persistence/context-handoff-store.js +112 -0
- package/dist/vendor/core/persistence/index.d.ts +4 -0
- package/dist/vendor/core/persistence/index.js +2 -0
- package/dist/vendor/core/persistence/ledger.d.ts +1 -1
- package/dist/vendor/core/persistence/mission-store.d.ts +56 -0
- package/dist/vendor/core/persistence/mission-store.js +281 -0
- package/dist/vendor/core/persistence/runs-reader.d.ts +2 -0
- package/dist/vendor/core/policy.d.ts +2 -0
- package/dist/vendor/core/policy.js +11 -8
- package/dist/vendor/core/red-blue/red-phase.d.ts +2 -1
- package/dist/vendor/core/red-blue/red-phase.js +3 -3
- package/dist/vendor/core/red-blue/risk-tiers.d.ts +2 -4
- package/dist/vendor/core/red-blue/risk-tiers.js +1 -3
- package/dist/vendor/core/rollback.js +15 -1
- package/dist/vendor/core/routing.d.ts +0 -42
- package/dist/vendor/core/routing.js +4 -154
- package/dist/vendor/core/savings.d.ts +13 -0
- package/dist/vendor/core/savings.js +22 -0
- package/dist/vendor/core/termination-store.d.ts +15 -0
- package/dist/vendor/core/termination-store.js +44 -0
- package/dist/vendor/core/verified-handoff.d.ts +72 -0
- package/dist/vendor/core/verified-handoff.js +238 -0
- package/package.json +4 -10
- package/server.json +3 -3
- package/dist/vendor/adapters/stub-agent-cli.d.ts +0 -8
- package/dist/vendor/adapters/stub-agent-cli.js +0 -40
- package/dist/vendor/adapters/stub-direct-provider.d.ts +0 -8
- package/dist/vendor/adapters/stub-direct-provider.js +0 -12
package/dist/tools/run-loop.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter, probeCodexLaunch, resolveCliCommandAvailability
|
|
1
|
+
import { createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter, createVerifierOnlyAdapter, probeCodexLaunch, resolveCliCommandAvailability } from "../vendor/adapters/index.js";
|
|
2
2
|
import { createFileRunStore, evaluateCostGovernor, resolveRunsRoot, runMartin } from "../vendor/core/index.js";
|
|
3
3
|
import { normalizeSafePathPatterns, resolveSafeRepoRoot } from "../server-validation.js";
|
|
4
4
|
import { MartinToolError } from "./tool-errors.js";
|
|
@@ -6,16 +6,12 @@ import { buildArtifactSummary, buildVerificationSummary, buildLoopPreview, build
|
|
|
6
6
|
import { normalizeLoopBudget } from "./workflow-governance.js";
|
|
7
7
|
let proofModeVerifierSpawnImpl;
|
|
8
8
|
let runStoreOverrideForTests;
|
|
9
|
-
let runMartinImpl = runMartin;
|
|
10
9
|
export function __setProofModeVerifierSpawnImplForTests(spawnImpl) {
|
|
11
10
|
proofModeVerifierSpawnImpl = spawnImpl;
|
|
12
11
|
}
|
|
13
12
|
export function __setRunStoreOverrideForTests(store) {
|
|
14
13
|
runStoreOverrideForTests = store;
|
|
15
14
|
}
|
|
16
|
-
export function __setRunMartinImplForTests(impl) {
|
|
17
|
-
runMartinImpl = impl ?? runMartin;
|
|
18
|
-
}
|
|
19
15
|
export async function runLoopTool(input) {
|
|
20
16
|
const workingDirectory = resolveSafeRepoRoot(input.workingDirectory);
|
|
21
17
|
const engine = input.engine ?? "claude";
|
|
@@ -23,6 +19,9 @@ export async function runLoopTool(input) {
|
|
|
23
19
|
const allowedPaths = normalizeSafePathPatterns(input.allowedPaths, "allowedPaths");
|
|
24
20
|
const deniedPaths = normalizeSafePathPatterns(input.deniedPaths, "deniedPaths");
|
|
25
21
|
const executionMode = resolveExecutionMode();
|
|
22
|
+
const evidenceExecutionMode = executionMode.liveMode
|
|
23
|
+
? "governed"
|
|
24
|
+
: "verification_only";
|
|
26
25
|
const workspaceRoot = resolveSafeRepoRoot();
|
|
27
26
|
const runsRoot = resolveRunsRoot(process.env);
|
|
28
27
|
const receiptScope = {
|
|
@@ -38,7 +37,7 @@ export async function runLoopTool(input) {
|
|
|
38
37
|
if (!engineAvailability.available) {
|
|
39
38
|
throw new MartinToolError("engine_unavailable", `Engine '${engine}' is not available on PATH.`, {
|
|
40
39
|
category: "environment",
|
|
41
|
-
suggestion: "Install the requested CLI
|
|
40
|
+
suggestion: "Install the requested CLI. MARTIN_LIVE=false is verification-only and cannot emit governed VERIFIED.",
|
|
42
41
|
retryable: false
|
|
43
42
|
});
|
|
44
43
|
}
|
|
@@ -60,7 +59,7 @@ export async function runLoopTool(input) {
|
|
|
60
59
|
if (!engineAvailability.available) {
|
|
61
60
|
throw new MartinToolError("engine_unavailable", `Engine '${engine}' is not available on PATH.`, {
|
|
62
61
|
category: "environment",
|
|
63
|
-
suggestion: "Install the requested CLI
|
|
62
|
+
suggestion: "Install the requested CLI. MARTIN_LIVE=false is verification-only and cannot emit governed VERIFIED.",
|
|
64
63
|
retryable: false
|
|
65
64
|
});
|
|
66
65
|
}
|
|
@@ -69,7 +68,7 @@ export async function runLoopTool(input) {
|
|
|
69
68
|
const adapter = !executionMode.liveMode
|
|
70
69
|
? createVerifierOnlyAdapter({
|
|
71
70
|
workingDirectory,
|
|
72
|
-
label: "
|
|
71
|
+
label: "Verification-only adapter (MARTIN_LIVE=false)",
|
|
73
72
|
...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
|
|
74
73
|
...(proofModeVerifierSpawnImpl ? { spawnImpl: proofModeVerifierSpawnImpl } : {})
|
|
75
74
|
})
|
|
@@ -102,7 +101,7 @@ export async function runLoopTool(input) {
|
|
|
102
101
|
partialBudget.maxTokens = input.maxTokens;
|
|
103
102
|
}
|
|
104
103
|
const budget = normalizeLoopBudget(partialBudget);
|
|
105
|
-
const result = await
|
|
104
|
+
const result = await runMartin({
|
|
106
105
|
workspaceId: input.workspaceId ?? "ws_mcp",
|
|
107
106
|
projectId: input.projectId ?? "proj_mcp",
|
|
108
107
|
store: runStoreOverrideForTests ?? createFileRunStore({ runsRoot }),
|
|
@@ -119,6 +118,10 @@ export async function runLoopTool(input) {
|
|
|
119
118
|
...(deniedPaths ? { deniedPaths } : {})
|
|
120
119
|
},
|
|
121
120
|
budget,
|
|
121
|
+
metadata: {
|
|
122
|
+
executionMode: evidenceExecutionMode,
|
|
123
|
+
governanceClaimEligible: evidenceExecutionMode === "governed" ? "true" : "false"
|
|
124
|
+
},
|
|
122
125
|
adapter
|
|
123
126
|
});
|
|
124
127
|
const lastAttempt = result.loop.attempts.at(-1);
|
|
@@ -142,7 +145,10 @@ export async function runLoopTool(input) {
|
|
|
142
145
|
reason: result.decision.reason,
|
|
143
146
|
attempts: result.loop.attempts.length,
|
|
144
147
|
costUsd: result.loop.cost.actualUsd,
|
|
148
|
+
costProvenance: result.loop.cost.provenance ?? "unavailable",
|
|
145
149
|
verificationPassed,
|
|
150
|
+
executionMode: evidenceExecutionMode,
|
|
151
|
+
governanceClaimEligible: evidenceExecutionMode === "governed",
|
|
146
152
|
loopId: result.loop.loopId,
|
|
147
153
|
pressure: costState.pressure,
|
|
148
154
|
shouldStop: costState.shouldStop,
|
|
@@ -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
|
|
95
|
+
suggestion: "Install the requested CLI. MARTIN_LIVE=false is verification-only and cannot emit governed VERIFIED.",
|
|
96
96
|
retryable: false
|
|
97
97
|
};
|
|
98
98
|
}
|
|
@@ -4,13 +4,20 @@ function createJsonBlock(output) {
|
|
|
4
4
|
text: JSON.stringify(output, null, 2)
|
|
5
5
|
};
|
|
6
6
|
}
|
|
7
|
+
function resolveHumanArtifact(output, summary) {
|
|
8
|
+
const rendered = "rendered" in output ? output.rendered : undefined;
|
|
9
|
+
return typeof rendered === "string" && rendered.trim().length > 0 ? rendered : summary;
|
|
10
|
+
}
|
|
7
11
|
export function createToolSuccessResult(output, summary) {
|
|
8
12
|
return {
|
|
9
|
-
content: [
|
|
13
|
+
content: [
|
|
14
|
+
{ type: "text", text: resolveHumanArtifact(output, summary) },
|
|
15
|
+
createJsonBlock(output)
|
|
16
|
+
],
|
|
10
17
|
structuredContent: output,
|
|
11
18
|
_meta: {
|
|
12
19
|
"martinloop/summary": summary,
|
|
13
|
-
"martinloop/contentVersion": "2026-
|
|
20
|
+
"martinloop/contentVersion": "2026-08-18"
|
|
14
21
|
}
|
|
15
22
|
};
|
|
16
23
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LoopArtifact, LoopBudget, LoopCost, LoopEvent, LoopTask, ReceiptIntegritySummary, ReceiptScope } from "../vendor/contracts/index.js";
|
|
1
|
+
import type { CostProvenance, LoopArtifact, LoopBudget, LoopCost, LoopEvent, LoopTask, ReceiptIntegritySummary, ReceiptScope, TerminationEnvelopeV1 } from "../vendor/contracts/index.js";
|
|
2
2
|
import { type LedgerEvent, type LoopAttemptRecord, type LoopRunRecord } from "../vendor/core/index.js";
|
|
3
3
|
export declare const MARTIN_ENGINE_VALUES: readonly ["claude", "codex", "gemini", "openai"];
|
|
4
4
|
export type MartinEngine = (typeof MARTIN_ENGINE_VALUES)[number];
|
|
@@ -14,6 +14,7 @@ export interface InspectableLoopRecord extends Omit<LoopRunRecord, "attempts" |
|
|
|
14
14
|
metadata?: Record<string, string>;
|
|
15
15
|
receiptIntegrity?: ReceiptIntegritySummary;
|
|
16
16
|
receiptScope?: ReceiptScope;
|
|
17
|
+
terminationEnvelope?: TerminationEnvelopeV1;
|
|
17
18
|
routingEconomics?: import("../vendor/contracts/index.js").RoutingEconomics;
|
|
18
19
|
}
|
|
19
20
|
export interface LoopPreview {
|
|
@@ -26,6 +27,7 @@ export interface LoopPreview {
|
|
|
26
27
|
updatedAt?: string;
|
|
27
28
|
attempts: number;
|
|
28
29
|
costUsd: number;
|
|
30
|
+
costProvenance: CostProvenance;
|
|
29
31
|
avoidedUsd: number;
|
|
30
32
|
pressure: string;
|
|
31
33
|
shouldStop: boolean;
|
|
@@ -127,7 +129,7 @@ export declare function inspectRunsRoot(runsRoot?: string): Promise<RunStoreInsp
|
|
|
127
129
|
export declare function buildRunRecordPaths(runsRoot: string, loopId: string): CanonicalRunPaths;
|
|
128
130
|
export declare function buildAttemptArtifactDirectory(runsRoot: string, loopId: string, attemptIndex: number): string;
|
|
129
131
|
export declare function buildAttemptArtifactsReference(runsRoot: string, loopId: string, attemptIndex: number): Promise<AttemptArtifactFiles>;
|
|
130
|
-
export declare function buildCostSnapshot(cost: Pick<LoopCost, "actualUsd" | "tokensIn" | "tokensOut"> & {
|
|
132
|
+
export declare function buildCostSnapshot(cost: Pick<LoopCost, "actualUsd" | "tokensIn" | "tokensOut" | "provenance"> & {
|
|
131
133
|
avoidedUsd?: number;
|
|
132
134
|
}): LoopCost;
|
|
133
135
|
export declare function buildBudgetSnapshot(budget: LoopBudget): LoopBudget;
|
|
@@ -15,7 +15,7 @@ export function resolveExecutionMode() {
|
|
|
15
15
|
mode: liveMode ? "live" : "proof",
|
|
16
16
|
detail: liveMode
|
|
17
17
|
? "Live CLI execution is enabled."
|
|
18
|
-
: "
|
|
18
|
+
: "Verification-only mode is active because MARTIN_LIVE=false; governed VERIFIED is unavailable."
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
export function detectCliAvailability(command) {
|
|
@@ -105,11 +105,6 @@ function discoverCommonInstallDirectories(command) {
|
|
|
105
105
|
if (localAppData) {
|
|
106
106
|
dirs.push(join(localAppData, "OpenAI", "Codex", "bin"));
|
|
107
107
|
}
|
|
108
|
-
// Claude Code native installer places binary at %USERPROFILE%\.local\bin
|
|
109
|
-
const userProfile = process.env.USERPROFILE ?? process.env.HOMEPATH;
|
|
110
|
-
if (userProfile) {
|
|
111
|
-
dirs.push(join(userProfile, ".local", "bin"));
|
|
112
|
-
}
|
|
113
108
|
// Scoop
|
|
114
109
|
if (home) {
|
|
115
110
|
dirs.push(join(home, "scoop", "shims"));
|
|
@@ -166,13 +161,8 @@ function isOnPathDirectly(command, resolvedPath) {
|
|
|
166
161
|
* Returns a platform-specific one-liner install command for a known CLI.
|
|
167
162
|
*/
|
|
168
163
|
function suggestInstallCommand(command) {
|
|
169
|
-
if (command === "claude") {
|
|
170
|
-
const installCmd = process.platform === "win32"
|
|
171
|
-
? "irm https://claude.ai/install.ps1 | iex"
|
|
172
|
-
: "curl -fsSL https://claude.ai/install.sh | bash";
|
|
173
|
-
return `Install with: ${installCmd}`;
|
|
174
|
-
}
|
|
175
164
|
const npmInstalls = {
|
|
165
|
+
claude: "npm install -g @anthropic-ai/claude-code",
|
|
176
166
|
codex: "npm install -g @openai/codex",
|
|
177
167
|
gemini: "npm install -g @google/gemini-cli"
|
|
178
168
|
};
|
|
@@ -185,7 +175,7 @@ function suggestInstallCommand(command) {
|
|
|
185
175
|
export function getEngineAvailability(engine) {
|
|
186
176
|
return detectCliAvailability(engine);
|
|
187
177
|
}
|
|
188
|
-
export function createSkippedCliAvailability(command, detail = "
|
|
178
|
+
export function createSkippedCliAvailability(command, detail = "Verification-only mode skipped live CLI availability detection; governed VERIFIED is unavailable.") {
|
|
189
179
|
return {
|
|
190
180
|
command,
|
|
191
181
|
available: false,
|
|
@@ -218,6 +208,7 @@ export function buildLoopPreview(loop) {
|
|
|
218
208
|
...(loop.updatedAt ? { updatedAt: loop.updatedAt } : {}),
|
|
219
209
|
attempts: loop.attempts.length,
|
|
220
210
|
costUsd: loop.cost.actualUsd,
|
|
211
|
+
costProvenance: loop.cost.provenance ?? "unavailable",
|
|
221
212
|
avoidedUsd: loop.cost.avoidedUsd ?? 0,
|
|
222
213
|
pressure: costState.pressure,
|
|
223
214
|
shouldStop: costState.shouldStop,
|
|
@@ -421,7 +412,8 @@ export function buildCostSnapshot(cost) {
|
|
|
421
412
|
actualUsd: cost.actualUsd,
|
|
422
413
|
avoidedUsd: cost.avoidedUsd ?? 0,
|
|
423
414
|
tokensIn: cost.tokensIn,
|
|
424
|
-
tokensOut: cost.tokensOut
|
|
415
|
+
tokensOut: cost.tokensOut,
|
|
416
|
+
provenance: cost.provenance ?? "unavailable"
|
|
425
417
|
};
|
|
426
418
|
}
|
|
427
419
|
export function buildBudgetSnapshot(budget) {
|
|
@@ -439,7 +431,6 @@ export function buildSuggestedResourceUris(loopId) {
|
|
|
439
431
|
"martin://runs/triage",
|
|
440
432
|
"martin://runs/latest",
|
|
441
433
|
"martin://runs/latest/summary",
|
|
442
|
-
"martin://runs/latest/receipt",
|
|
443
434
|
"martin://runs/latest/proof-card",
|
|
444
435
|
"martin://runs/latest/budget-status",
|
|
445
436
|
"martin://runs/latest/verifier-evidence",
|
|
@@ -96,7 +96,7 @@ export interface GeminiCliAdapterOptions {
|
|
|
96
96
|
timeoutMs?: number;
|
|
97
97
|
verifyTimeoutMs?: number;
|
|
98
98
|
label?: string;
|
|
99
|
-
/**
|
|
99
|
+
/** Explicit model override passed via --model. Omitted to preserve Gemini Auto. */
|
|
100
100
|
model?: string;
|
|
101
101
|
/** Approval mode for headless Gemini runs. Defaults to yolo for autonomous execution. */
|
|
102
102
|
approvalMode?: "default" | "auto_edit" | "yolo" | "plan";
|
|
@@ -135,7 +135,7 @@ export declare function createClaudeCliAdapter(options?: ClaudeCliAdapterOptions
|
|
|
135
135
|
*/
|
|
136
136
|
export declare function createCodexCliAdapter(options?: CodexCliAdapterOptions): MartinAdapter;
|
|
137
137
|
/**
|
|
138
|
-
* Spawns `gemini --model <model> --prompt "" --approval-mode <mode> --output-format json [...]`.
|
|
138
|
+
* Spawns `gemini [--model <explicit-model>] --prompt "" --approval-mode <mode> --output-format json [...]`.
|
|
139
139
|
*
|
|
140
140
|
* The prompt is delivered via stdin while forcing headless mode with `--prompt ""`,
|
|
141
141
|
* which keeps large MartinLoop prompts off the command line on Windows.
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*
|
|
12
12
|
* MCP tools and integration tests use the same factories.
|
|
13
13
|
*/
|
|
14
|
-
import { readGitExecutionArtifacts, resolveGitRepositoryRoot, runSubprocess, runVerification } from "./cli-bridge.js";
|
|
15
|
-
import { buildCodexExecArgs
|
|
14
|
+
import { readGitChangedFiles, readGitExecutionArtifacts, resolveGitRepositoryRoot, runSubprocess, runVerification } from "./cli-bridge.js";
|
|
15
|
+
import { buildCodexExecArgs } from "./codex-launcher.js";
|
|
16
16
|
import { createAdapterCapabilities, normalizeStructuredErrors, normalizeUsage } from "./runtime-support.js";
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
// Cost estimation
|
|
@@ -68,7 +68,22 @@ function calculateUsageCost(usage, pricing) {
|
|
|
68
68
|
(usage.cacheCreationInputTokens / 1000) * (pricing.cacheCreationInputPer1K ?? pricing.inputPer1K) +
|
|
69
69
|
(usage.outputTokens / 1000) * pricing.outputPer1K);
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
const EMBEDDED_PRICING_VERSION = "embedded-v1";
|
|
72
|
+
function extractClaudeObservedModel(stdout) {
|
|
73
|
+
for (const line of stdout.split(/\r?\n/u)) {
|
|
74
|
+
try {
|
|
75
|
+
const event = JSON.parse(line);
|
|
76
|
+
if (event.type === "system" && event.subtype === "init" && typeof event.model === "string") {
|
|
77
|
+
return event.model;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Ignore non-JSON stream fragments.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
function extractUsage(parsed, modelLabel, modelSource) {
|
|
72
87
|
if (!parsed?.usage) {
|
|
73
88
|
return normalizeUsage({
|
|
74
89
|
actualUsd: 0,
|
|
@@ -102,15 +117,24 @@ function extractUsage(parsed, modelLabel) {
|
|
|
102
117
|
tokensIn,
|
|
103
118
|
tokensOut,
|
|
104
119
|
cachedInputTokens,
|
|
105
|
-
provenance: hasAuthoritativeCost ? "actual" : pricingResolution.pricing ? "
|
|
120
|
+
provenance: hasAuthoritativeCost ? "actual" : pricingResolution.pricing ? "calculated" : "unavailable",
|
|
106
121
|
providerSettlement: {
|
|
107
122
|
providerId: "claude",
|
|
108
|
-
model: modelLabel
|
|
123
|
+
...(modelLabel ? { model: modelLabel } : {}),
|
|
109
124
|
transport: "cli",
|
|
110
125
|
source: "claude_json",
|
|
111
126
|
inputTokens: promptTokens + cacheCreationInputTokens,
|
|
112
127
|
cachedInputTokens,
|
|
128
|
+
cacheCreationInputTokens,
|
|
113
129
|
outputTokens: tokensOut,
|
|
130
|
+
billingMode: "unknown",
|
|
131
|
+
modelSource,
|
|
132
|
+
pricingSource: hasAuthoritativeCost
|
|
133
|
+
? "provider_reported_total"
|
|
134
|
+
: pricingResolution.pricing
|
|
135
|
+
? "static_catalog"
|
|
136
|
+
: "none",
|
|
137
|
+
...(pricingResolution.pricing ? { pricingVersion: EMBEDDED_PRICING_VERSION } : {}),
|
|
114
138
|
rawUsageAvailable: true,
|
|
115
139
|
settledAt: new Date().toISOString()
|
|
116
140
|
}
|
|
@@ -152,7 +176,7 @@ function extractCodexJsonlResult(stdout, modelLabel) {
|
|
|
152
176
|
provenance: "unavailable",
|
|
153
177
|
providerSettlement: {
|
|
154
178
|
providerId: "codex",
|
|
155
|
-
model: modelLabel
|
|
179
|
+
...(modelLabel ? { model: modelLabel } : {}),
|
|
156
180
|
transport: "cli",
|
|
157
181
|
source: "unavailable",
|
|
158
182
|
inputTokens: 0,
|
|
@@ -169,8 +193,8 @@ function extractCodexJsonlResult(stdout, modelLabel) {
|
|
|
169
193
|
const reasoningOutputTokens = latestTurnCompleted.usage.reasoning_output_tokens ?? 0;
|
|
170
194
|
const tokensIn = promptTokens + cachedInputTokens;
|
|
171
195
|
const tokensOut = outputTokens + reasoningOutputTokens;
|
|
172
|
-
const
|
|
173
|
-
|
|
196
|
+
const exactPricing = modelLabel ? MODEL_PRICING[modelLabel] : undefined;
|
|
197
|
+
const pricing = exactPricing ?? MODEL_PRICING["codex"] ??
|
|
174
198
|
{ inputPer1K: BLENDED_INPUT_COST_PER_1K, outputPer1K: BLENDED_OUTPUT_COST_PER_1K };
|
|
175
199
|
const actualUsd = (promptTokens / 1000) * pricing.inputPer1K +
|
|
176
200
|
(cachedInputTokens / 1000) * (pricing.cachedInputPer1K ?? pricing.inputPer1K) +
|
|
@@ -183,16 +207,20 @@ function extractCodexJsonlResult(stdout, modelLabel) {
|
|
|
183
207
|
tokensOut,
|
|
184
208
|
cachedInputTokens,
|
|
185
209
|
reasoningTokensOut: reasoningOutputTokens,
|
|
186
|
-
provenance: "
|
|
210
|
+
provenance: exactPricing ? "calculated" : "estimated",
|
|
187
211
|
providerSettlement: {
|
|
188
212
|
providerId: "codex",
|
|
189
|
-
model: modelLabel
|
|
213
|
+
...(modelLabel ? { model: modelLabel } : {}),
|
|
190
214
|
transport: "cli",
|
|
191
215
|
source: "codex_jsonl",
|
|
192
216
|
inputTokens: promptTokens,
|
|
193
217
|
cachedInputTokens,
|
|
194
218
|
outputTokens,
|
|
195
219
|
reasoningOutputTokens,
|
|
220
|
+
billingMode: "unknown",
|
|
221
|
+
modelSource: modelLabel ? "explicit_override" : "agent_default",
|
|
222
|
+
pricingSource: exactPricing ? "static_catalog" : "blended_fallback",
|
|
223
|
+
pricingVersion: EMBEDDED_PRICING_VERSION,
|
|
196
224
|
rawUsageAvailable: true,
|
|
197
225
|
settledAt: new Date().toISOString()
|
|
198
226
|
}
|
|
@@ -228,7 +256,7 @@ function extractGeminiJsonResult(stdout, modelLabel) {
|
|
|
228
256
|
provenance: "unavailable",
|
|
229
257
|
providerSettlement: {
|
|
230
258
|
providerId: "gemini",
|
|
231
|
-
model: modelLabel
|
|
259
|
+
...(modelLabel ? { model: modelLabel } : {}),
|
|
232
260
|
transport: "cli",
|
|
233
261
|
source: "unavailable",
|
|
234
262
|
inputTokens: 0,
|
|
@@ -254,16 +282,20 @@ function extractGeminiJsonResult(stdout, modelLabel) {
|
|
|
254
282
|
tokensOut,
|
|
255
283
|
cachedInputTokens,
|
|
256
284
|
reasoningTokensOut: reasoningOutputTokens,
|
|
257
|
-
provenance: "
|
|
285
|
+
provenance: "estimated",
|
|
258
286
|
providerSettlement: {
|
|
259
287
|
providerId: "gemini",
|
|
260
|
-
model: modelLabel
|
|
288
|
+
...(modelLabel ? { model: modelLabel } : {}),
|
|
261
289
|
transport: "cli",
|
|
262
290
|
source: "gemini_json",
|
|
263
291
|
inputTokens: promptTokens,
|
|
264
292
|
cachedInputTokens,
|
|
265
293
|
outputTokens,
|
|
266
294
|
reasoningOutputTokens,
|
|
295
|
+
billingMode: "unknown",
|
|
296
|
+
modelSource: modelLabel ? "explicit_override" : "agent_default",
|
|
297
|
+
pricingSource: "blended_fallback",
|
|
298
|
+
pricingVersion: EMBEDDED_PRICING_VERSION,
|
|
267
299
|
rawUsageAvailable: true,
|
|
268
300
|
settledAt: new Date().toISOString()
|
|
269
301
|
}
|
|
@@ -381,14 +413,10 @@ function createStreamingUsageInspector(capUsd, modelLabel, promptTokenEstimate)
|
|
|
381
413
|
catch {
|
|
382
414
|
return;
|
|
383
415
|
}
|
|
384
|
-
|
|
385
|
-
// assistant-message usage events — skip to avoid double-counting.
|
|
416
|
+
extractUsageFromEvent(event, terminate);
|
|
386
417
|
if (event.type === "result") {
|
|
387
418
|
finalResult = event;
|
|
388
419
|
}
|
|
389
|
-
else {
|
|
390
|
-
extractUsageFromEvent(event, terminate);
|
|
391
|
-
}
|
|
392
420
|
};
|
|
393
421
|
return {
|
|
394
422
|
onChunk: (chunk, terminate) => {
|
|
@@ -501,7 +529,7 @@ export function createAgentCliAdapter(options) {
|
|
|
501
529
|
label: options.label ?? `${options.command} CLI adapter`,
|
|
502
530
|
metadata: {
|
|
503
531
|
providerId: options.command,
|
|
504
|
-
model: options.model
|
|
532
|
+
...(options.model ? { model: options.model } : {}),
|
|
505
533
|
transport: "cli",
|
|
506
534
|
capabilities: createAdapterCapabilities({
|
|
507
535
|
preflight: true,
|
|
@@ -514,6 +542,14 @@ export function createAgentCliAdapter(options) {
|
|
|
514
542
|
async execute(request) {
|
|
515
543
|
const prompt = buildPrompt(request);
|
|
516
544
|
const estimatedUsage = estimateUsage(prompt, options.model ?? options.command, options.command);
|
|
545
|
+
const repoRoot = request.context.repoRoot;
|
|
546
|
+
const gitRepoRoot = repoRoot ? resolveGitRepositoryRoot(repoRoot) : undefined;
|
|
547
|
+
// A governed run may begin in a deliberately dirty workspace. Capture that
|
|
548
|
+
// baseline so existing operator work is neither reported as this run's
|
|
549
|
+
// execution nor treated as scope creep.
|
|
550
|
+
const baselineChangedFiles = gitRepoRoot
|
|
551
|
+
? new Set(await readGitChangedFiles(gitRepoRoot, 5_000, options.spawnImpl))
|
|
552
|
+
: new Set();
|
|
517
553
|
// Preflight: bail if projected cost exceeds remaining budget
|
|
518
554
|
if (request.context.remainingBudgetUsd > 0) {
|
|
519
555
|
const projected = estimatePromptCost(prompt, options.model ?? "", options.command);
|
|
@@ -549,7 +585,8 @@ export function createAgentCliAdapter(options) {
|
|
|
549
585
|
timeoutMs,
|
|
550
586
|
spawnImpl: options.spawnImpl,
|
|
551
587
|
...(stdinData === undefined ? {} : { stdinData }),
|
|
552
|
-
...(streamingUsage ? { onStdoutChunk: streamingUsage.onChunk } : {})
|
|
588
|
+
...(streamingUsage ? { onStdoutChunk: streamingUsage.onChunk } : {}),
|
|
589
|
+
...(request.signal !== undefined ? { signal: request.signal } : {})
|
|
553
590
|
});
|
|
554
591
|
if (agentResult.terminationReason) {
|
|
555
592
|
const snapshot = streamingUsage?.snapshot();
|
|
@@ -660,8 +697,15 @@ export function createAgentCliAdapter(options) {
|
|
|
660
697
|
parsed?.result ??
|
|
661
698
|
agentResult.stdout.trim();
|
|
662
699
|
const summary = truncate(agentText, 2000);
|
|
700
|
+
const observedClaudeModel = options.streamingUsageCap
|
|
701
|
+
? extractClaudeObservedModel(agentResult.stdout)
|
|
702
|
+
: undefined;
|
|
663
703
|
const usage = parsed?.usage
|
|
664
|
-
? extractUsage(parsed, options.model
|
|
704
|
+
? extractUsage(parsed, observedClaudeModel ?? options.model, observedClaudeModel
|
|
705
|
+
? "provider_reported"
|
|
706
|
+
: options.model
|
|
707
|
+
? "explicit_override"
|
|
708
|
+
: "agent_default")
|
|
665
709
|
: codexJsonlResult?.usage ??
|
|
666
710
|
geminiJsonResult?.usage ??
|
|
667
711
|
normalizeUsage({
|
|
@@ -673,7 +717,7 @@ export function createAgentCliAdapter(options) {
|
|
|
673
717
|
providerSettlement: options.command === "codex"
|
|
674
718
|
? {
|
|
675
719
|
providerId: "codex",
|
|
676
|
-
model: options.model
|
|
720
|
+
...(options.model ? { model: options.model } : {}),
|
|
677
721
|
transport: "cli",
|
|
678
722
|
source: "estimated_fallback",
|
|
679
723
|
inputTokens: estimatedUsage.tokensIn,
|
|
@@ -684,7 +728,7 @@ export function createAgentCliAdapter(options) {
|
|
|
684
728
|
: options.command === "gemini"
|
|
685
729
|
? {
|
|
686
730
|
providerId: "gemini",
|
|
687
|
-
model: options.model
|
|
731
|
+
...(options.model ? { model: options.model } : {}),
|
|
688
732
|
transport: "cli",
|
|
689
733
|
source: "estimated_fallback",
|
|
690
734
|
inputTokens: estimatedUsage.tokensIn,
|
|
@@ -695,30 +739,36 @@ export function createAgentCliAdapter(options) {
|
|
|
695
739
|
: undefined
|
|
696
740
|
});
|
|
697
741
|
const verificationStack = request.context.verificationStack;
|
|
698
|
-
const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, verificationStack, options.spawnImpl
|
|
742
|
+
const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, verificationStack, options.spawnImpl, {
|
|
743
|
+
runId: request.loopId,
|
|
744
|
+
workspaceId: request.workspaceId,
|
|
745
|
+
cwd: workingDirectory,
|
|
746
|
+
});
|
|
699
747
|
// Check for zero-diff (agent ran but made no file changes)
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
748
|
+
const postRunChangedFiles = gitRepoRoot
|
|
749
|
+
? await readGitChangedFiles(gitRepoRoot, 5_000, options.spawnImpl)
|
|
750
|
+
: [];
|
|
751
|
+
const agentChangedFiles = postRunChangedFiles.filter((file) => !baselineChangedFiles.has(file));
|
|
752
|
+
const noDiff = gitRepoRoot !== undefined && agentChangedFiles.length === 0;
|
|
706
753
|
// Extract structured errors from stderr/stdout for better failure context
|
|
707
754
|
const structuredErrors = normalizeStructuredErrors(extractStructuredErrors(agentResult.stderr, agentResult.stdout));
|
|
708
|
-
const
|
|
755
|
+
const rawExecutionArtifacts = gitRepoRoot
|
|
709
756
|
? await readGitExecutionArtifacts(gitRepoRoot, 5000, options.spawnImpl)
|
|
710
757
|
: undefined;
|
|
758
|
+
const executionArtifacts = rawExecutionArtifacts
|
|
759
|
+
? {
|
|
760
|
+
...(agentChangedFiles.length > 0 ? { changedFiles: agentChangedFiles } : {}),
|
|
761
|
+
...(baselineChangedFiles.size === 0 && rawExecutionArtifacts.diffStats
|
|
762
|
+
? { diffStats: rawExecutionArtifacts.diffStats }
|
|
763
|
+
: {})
|
|
764
|
+
}
|
|
765
|
+
: undefined;
|
|
711
766
|
// Scope contract enforcement: check touched files against allowedPaths/deniedPaths
|
|
712
767
|
let scopeViolations = [];
|
|
713
768
|
const scopeCtx = request.context;
|
|
714
769
|
if (gitRepoRoot && (scopeCtx.allowedPaths?.length || scopeCtx.deniedPaths?.length)) {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
timeoutMs: 5000,
|
|
718
|
-
spawnImpl: options.spawnImpl
|
|
719
|
-
});
|
|
720
|
-
if (diffResult.exitCode === 0 && diffResult.stdout.trim()) {
|
|
721
|
-
const touchedFiles = diffResult.stdout.trim().split("\n").filter(Boolean);
|
|
770
|
+
if (agentChangedFiles.length > 0) {
|
|
771
|
+
const touchedFiles = agentChangedFiles;
|
|
722
772
|
const allowed = scopeCtx.allowedPaths ?? [];
|
|
723
773
|
const denied = scopeCtx.deniedPaths ?? [];
|
|
724
774
|
for (const file of touchedFiles) {
|
|
@@ -741,7 +791,7 @@ export function createAgentCliAdapter(options) {
|
|
|
741
791
|
status: "completed",
|
|
742
792
|
summary,
|
|
743
793
|
usage,
|
|
744
|
-
verification
|
|
794
|
+
verification,
|
|
745
795
|
...(executionArtifacts
|
|
746
796
|
? {
|
|
747
797
|
execution: {
|
|
@@ -786,7 +836,7 @@ export function createAgentCliAdapter(options) {
|
|
|
786
836
|
}
|
|
787
837
|
// Reset tracked files to HEAD so next attempt starts from clean state
|
|
788
838
|
try {
|
|
789
|
-
if (gitRepoRoot) {
|
|
839
|
+
if (gitRepoRoot && baselineChangedFiles.size === 0) {
|
|
790
840
|
await runSubprocess("git", ["restore", "--staged", "--worktree", "."], {
|
|
791
841
|
cwd: gitRepoRoot,
|
|
792
842
|
timeoutMs: 5000
|
|
@@ -803,7 +853,7 @@ export function createAgentCliAdapter(options) {
|
|
|
803
853
|
? `${summary}${errorBlock}${scopeViolations.length > 0 ? `\nScope violations: ${scopeViolations.join(", ")}` : ""}`
|
|
804
854
|
: summary,
|
|
805
855
|
usage,
|
|
806
|
-
verification
|
|
856
|
+
verification,
|
|
807
857
|
...(executionArtifacts
|
|
808
858
|
? {
|
|
809
859
|
execution: {
|
|
@@ -819,18 +869,6 @@ export function createAgentCliAdapter(options) {
|
|
|
819
869
|
...(classHint ? { classHint } : {})
|
|
820
870
|
}
|
|
821
871
|
};
|
|
822
|
-
},
|
|
823
|
-
/**
|
|
824
|
-
* Return a new adapter instance with a different model.
|
|
825
|
-
* Used by run-martin.ts when a change_model intervention fires.
|
|
826
|
-
* Model escalation order: haiku → sonnet → opus (cheapest-first, escalate on repeated failure).
|
|
827
|
-
*/
|
|
828
|
-
withModel(newModel) {
|
|
829
|
-
return createAgentCliAdapter({
|
|
830
|
-
...options,
|
|
831
|
-
model: newModel,
|
|
832
|
-
adapterIdSuffix: `${options.adapterIdSuffix ?? options.command}:${newModel}`
|
|
833
|
-
});
|
|
834
872
|
}
|
|
835
873
|
};
|
|
836
874
|
return adapter;
|
|
@@ -859,7 +897,7 @@ export function createClaudeCliAdapter(options = {}) {
|
|
|
859
897
|
return createAgentCliAdapter({
|
|
860
898
|
command: "claude",
|
|
861
899
|
adapterIdSuffix: "claude",
|
|
862
|
-
model: options.model
|
|
900
|
+
model: options.model,
|
|
863
901
|
label: options.label ?? "Claude CLI adapter",
|
|
864
902
|
workingDirectory: options.workingDirectory,
|
|
865
903
|
timeoutMs: options.timeoutMs,
|
|
@@ -906,11 +944,11 @@ export function createCodexCliAdapter(options = {}) {
|
|
|
906
944
|
const sandbox = options.sandbox ?? "workspace-write";
|
|
907
945
|
const workingDirectory = options.workingDirectory ?? process.cwd();
|
|
908
946
|
const command = options.command ?? "codex";
|
|
909
|
-
const launchModel = options.model
|
|
947
|
+
const launchModel = options.model;
|
|
910
948
|
return createAgentCliAdapter({
|
|
911
949
|
command,
|
|
912
950
|
adapterIdSuffix: "codex",
|
|
913
|
-
model: options.model
|
|
951
|
+
model: options.model,
|
|
914
952
|
label: options.label ?? "Codex CLI adapter",
|
|
915
953
|
workingDirectory,
|
|
916
954
|
timeoutMs: options.timeoutMs,
|
|
@@ -931,7 +969,7 @@ export function createCodexCliAdapter(options = {}) {
|
|
|
931
969
|
// Pre-configured: Gemini CLI
|
|
932
970
|
// ---------------------------------------------------------------------------
|
|
933
971
|
/**
|
|
934
|
-
* Spawns `gemini --model <model> --prompt "" --approval-mode <mode> --output-format json [...]`.
|
|
972
|
+
* Spawns `gemini [--model <explicit-model>] --prompt "" --approval-mode <mode> --output-format json [...]`.
|
|
935
973
|
*
|
|
936
974
|
* The prompt is delivered via stdin while forcing headless mode with `--prompt ""`,
|
|
937
975
|
* which keeps large MartinLoop prompts off the command line on Windows.
|
|
@@ -940,13 +978,12 @@ export function createCodexCliAdapter(options = {}) {
|
|
|
940
978
|
* npm install -g @google/gemini-cli
|
|
941
979
|
*/
|
|
942
980
|
export function createGeminiCliAdapter(options = {}) {
|
|
943
|
-
const model = options.model ?? "flash";
|
|
944
981
|
const approvalMode = options.approvalMode ?? "yolo";
|
|
945
982
|
const extraArgs = options.extraArgs ?? [];
|
|
946
983
|
return createAgentCliAdapter({
|
|
947
984
|
command: "gemini",
|
|
948
985
|
adapterIdSuffix: "gemini",
|
|
949
|
-
model,
|
|
986
|
+
model: options.model,
|
|
950
987
|
label: options.label ?? "Gemini CLI adapter",
|
|
951
988
|
workingDirectory: options.workingDirectory,
|
|
952
989
|
timeoutMs: options.timeoutMs,
|
|
@@ -954,8 +991,7 @@ export function createGeminiCliAdapter(options = {}) {
|
|
|
954
991
|
supportsJsonOutput: false,
|
|
955
992
|
spawnImpl: options.spawnImpl,
|
|
956
993
|
argsBuilder: () => [
|
|
957
|
-
"--model",
|
|
958
|
-
model,
|
|
994
|
+
...(options.model ? ["--model", options.model] : []),
|
|
959
995
|
"--prompt",
|
|
960
996
|
"",
|
|
961
997
|
"--approval-mode",
|
|
@@ -1152,11 +1188,3 @@ function extractStructuredErrors(stderr, stdout) {
|
|
|
1152
1188
|
}
|
|
1153
1189
|
return errors.slice(0, 10); // cap at 10 to avoid bloating prompts
|
|
1154
1190
|
}
|
|
1155
|
-
async function checkNoDiff(repoRoot, spawnImpl) {
|
|
1156
|
-
const result = await runSubprocess("git", ["diff", "--name-only", "HEAD"], {
|
|
1157
|
-
cwd: repoRoot,
|
|
1158
|
-
timeoutMs: 5000,
|
|
1159
|
-
spawnImpl
|
|
1160
|
-
});
|
|
1161
|
-
return result.exitCode === 0 && result.stdout.trim().length === 0;
|
|
1162
|
-
}
|