@martinloop/mcp 0.5.0 → 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/dist/resources.js +15 -3
- package/dist/server.js +40 -8
- 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.js +1 -1
- package/dist/tools/run-loop.d.ts +6 -1
- package/dist/tools/run-loop.js +22 -7
- package/dist/tools/tool-errors.js +1 -1
- package/dist/tools/tool-response.js +9 -2
- package/dist/tools/tool-support.d.ts +3 -2
- package/dist/tools/tool-support.js +5 -3
- package/dist/vendor/adapters/claude-cli.d.ts +2 -2
- package/dist/vendor/adapters/claude-cli.js +59 -34
- package/dist/vendor/adapters/index.d.ts +1 -1
- package/dist/vendor/adapters/index.js +1 -1
- package/dist/vendor/adapters/openai-compatible.d.ts +2 -3
- package/dist/vendor/adapters/openai-compatible.js +33 -7
- package/dist/vendor/adapters/verifier-only.d.ts +9 -0
- package/dist/vendor/adapters/verifier-only.js +68 -0
- package/dist/vendor/contracts/index.d.ts +16 -6
- package/dist/vendor/contracts/index.js +1 -1
- package/dist/vendor/contracts/verified-handoff.d.ts +6 -0
- package/dist/vendor/contracts/verified-handoff.js +5 -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 +4 -6
- package/dist/vendor/core/index.js +25 -19
- package/dist/vendor/core/persistence/runs-reader.d.ts +2 -0
- package/dist/vendor/core/policy.js +6 -6
- 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/routing.d.ts +0 -42
- package/dist/vendor/core/routing.js +4 -154
- package/dist/vendor/core/verified-handoff.d.ts +6 -1
- package/dist/vendor/core/verified-handoff.js +28 -0
- package/package.json +6 -2
- package/server.json +2 -2
- package/dist/vendor/adapters/stub-direct-provider.d.ts +0 -8
- package/dist/vendor/adapters/stub-direct-provider.js +0 -12
|
@@ -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
|
}
|
|
@@ -497,7 +529,7 @@ export function createAgentCliAdapter(options) {
|
|
|
497
529
|
label: options.label ?? `${options.command} CLI adapter`,
|
|
498
530
|
metadata: {
|
|
499
531
|
providerId: options.command,
|
|
500
|
-
model: options.model
|
|
532
|
+
...(options.model ? { model: options.model } : {}),
|
|
501
533
|
transport: "cli",
|
|
502
534
|
capabilities: createAdapterCapabilities({
|
|
503
535
|
preflight: true,
|
|
@@ -665,8 +697,15 @@ export function createAgentCliAdapter(options) {
|
|
|
665
697
|
parsed?.result ??
|
|
666
698
|
agentResult.stdout.trim();
|
|
667
699
|
const summary = truncate(agentText, 2000);
|
|
700
|
+
const observedClaudeModel = options.streamingUsageCap
|
|
701
|
+
? extractClaudeObservedModel(agentResult.stdout)
|
|
702
|
+
: undefined;
|
|
668
703
|
const usage = parsed?.usage
|
|
669
|
-
? extractUsage(parsed, options.model
|
|
704
|
+
? extractUsage(parsed, observedClaudeModel ?? options.model, observedClaudeModel
|
|
705
|
+
? "provider_reported"
|
|
706
|
+
: options.model
|
|
707
|
+
? "explicit_override"
|
|
708
|
+
: "agent_default")
|
|
670
709
|
: codexJsonlResult?.usage ??
|
|
671
710
|
geminiJsonResult?.usage ??
|
|
672
711
|
normalizeUsage({
|
|
@@ -678,7 +717,7 @@ export function createAgentCliAdapter(options) {
|
|
|
678
717
|
providerSettlement: options.command === "codex"
|
|
679
718
|
? {
|
|
680
719
|
providerId: "codex",
|
|
681
|
-
model: options.model
|
|
720
|
+
...(options.model ? { model: options.model } : {}),
|
|
682
721
|
transport: "cli",
|
|
683
722
|
source: "estimated_fallback",
|
|
684
723
|
inputTokens: estimatedUsage.tokensIn,
|
|
@@ -689,7 +728,7 @@ export function createAgentCliAdapter(options) {
|
|
|
689
728
|
: options.command === "gemini"
|
|
690
729
|
? {
|
|
691
730
|
providerId: "gemini",
|
|
692
|
-
model: options.model
|
|
731
|
+
...(options.model ? { model: options.model } : {}),
|
|
693
732
|
transport: "cli",
|
|
694
733
|
source: "estimated_fallback",
|
|
695
734
|
inputTokens: estimatedUsage.tokensIn,
|
|
@@ -830,18 +869,6 @@ export function createAgentCliAdapter(options) {
|
|
|
830
869
|
...(classHint ? { classHint } : {})
|
|
831
870
|
}
|
|
832
871
|
};
|
|
833
|
-
},
|
|
834
|
-
/**
|
|
835
|
-
* Return a new adapter instance with a different model.
|
|
836
|
-
* Used by run-martin.ts when a change_model intervention fires.
|
|
837
|
-
* Model escalation order: haiku → sonnet → opus (cheapest-first, escalate on repeated failure).
|
|
838
|
-
*/
|
|
839
|
-
withModel(newModel) {
|
|
840
|
-
return createAgentCliAdapter({
|
|
841
|
-
...options,
|
|
842
|
-
model: newModel,
|
|
843
|
-
adapterIdSuffix: `${options.adapterIdSuffix ?? options.command}:${newModel}`
|
|
844
|
-
});
|
|
845
872
|
}
|
|
846
873
|
};
|
|
847
874
|
return adapter;
|
|
@@ -870,7 +897,7 @@ export function createClaudeCliAdapter(options = {}) {
|
|
|
870
897
|
return createAgentCliAdapter({
|
|
871
898
|
command: "claude",
|
|
872
899
|
adapterIdSuffix: "claude",
|
|
873
|
-
model: options.model
|
|
900
|
+
model: options.model,
|
|
874
901
|
label: options.label ?? "Claude CLI adapter",
|
|
875
902
|
workingDirectory: options.workingDirectory,
|
|
876
903
|
timeoutMs: options.timeoutMs,
|
|
@@ -921,7 +948,7 @@ export function createCodexCliAdapter(options = {}) {
|
|
|
921
948
|
return createAgentCliAdapter({
|
|
922
949
|
command,
|
|
923
950
|
adapterIdSuffix: "codex",
|
|
924
|
-
model: options.model
|
|
951
|
+
model: options.model,
|
|
925
952
|
label: options.label ?? "Codex CLI adapter",
|
|
926
953
|
workingDirectory,
|
|
927
954
|
timeoutMs: options.timeoutMs,
|
|
@@ -942,7 +969,7 @@ export function createCodexCliAdapter(options = {}) {
|
|
|
942
969
|
// Pre-configured: Gemini CLI
|
|
943
970
|
// ---------------------------------------------------------------------------
|
|
944
971
|
/**
|
|
945
|
-
* Spawns `gemini --model <model> --prompt "" --approval-mode <mode> --output-format json [...]`.
|
|
972
|
+
* Spawns `gemini [--model <explicit-model>] --prompt "" --approval-mode <mode> --output-format json [...]`.
|
|
946
973
|
*
|
|
947
974
|
* The prompt is delivered via stdin while forcing headless mode with `--prompt ""`,
|
|
948
975
|
* which keeps large MartinLoop prompts off the command line on Windows.
|
|
@@ -951,13 +978,12 @@ export function createCodexCliAdapter(options = {}) {
|
|
|
951
978
|
* npm install -g @google/gemini-cli
|
|
952
979
|
*/
|
|
953
980
|
export function createGeminiCliAdapter(options = {}) {
|
|
954
|
-
const model = options.model ?? "flash";
|
|
955
981
|
const approvalMode = options.approvalMode ?? "yolo";
|
|
956
982
|
const extraArgs = options.extraArgs ?? [];
|
|
957
983
|
return createAgentCliAdapter({
|
|
958
984
|
command: "gemini",
|
|
959
985
|
adapterIdSuffix: "gemini",
|
|
960
|
-
model,
|
|
986
|
+
model: options.model,
|
|
961
987
|
label: options.label ?? "Gemini CLI adapter",
|
|
962
988
|
workingDirectory: options.workingDirectory,
|
|
963
989
|
timeoutMs: options.timeoutMs,
|
|
@@ -965,8 +991,7 @@ export function createGeminiCliAdapter(options = {}) {
|
|
|
965
991
|
supportsJsonOutput: false,
|
|
966
992
|
spawnImpl: options.spawnImpl,
|
|
967
993
|
argsBuilder: () => [
|
|
968
|
-
"--model",
|
|
969
|
-
model,
|
|
994
|
+
...(options.model ? ["--model", options.model] : []),
|
|
970
995
|
"--prompt",
|
|
971
996
|
"",
|
|
972
997
|
"--approval-mode",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createDirectProviderAdapter, type DirectProviderAdapterOptions } from "./direct-provider.js";
|
|
2
|
-
export { createStubDirectProviderAdapter, type StubDirectProviderAdapterOptions } from "./stub-direct-provider.js";
|
|
3
2
|
export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter, type AgentCliAdapterOptions, type ClaudeCliAdapterOptions, type CodexCliAdapterOptions, type GeminiCliAdapterOptions, type CliArgsBuilder } from "./claude-cli.js";
|
|
4
3
|
export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig, type OpenAiCompatibleAdapterOptions } from "./openai-compatible.js";
|
|
4
|
+
export { createVerifierOnlyAdapter, type VerifierOnlyAdapterOptions } from "./verifier-only.js";
|
|
5
5
|
export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability, type CliCommandAvailability, type CodexHostDiagnosis, type CodexHostPlatform, type CodexLaunchProbeResult, checkCodexSandboxPreflight, probeFilesystemWriteCapability, type CodexSandboxPreflightOk, type CodexSandboxPreflightOutcome, type CodexSandboxPreflightReadOnly } from "./codex-launcher.js";
|
|
6
6
|
export { createSpawnPlan, type SpawnLike, type SpawnPlan, type SubprocessResult, type VerificationOutcome } from "./cli-bridge.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createDirectProviderAdapter } from "./direct-provider.js";
|
|
2
|
-
export { createStubDirectProviderAdapter } from "./stub-direct-provider.js";
|
|
3
2
|
export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter } from "./claude-cli.js";
|
|
4
3
|
export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig } from "./openai-compatible.js";
|
|
4
|
+
export { createVerifierOnlyAdapter } from "./verifier-only.js";
|
|
5
5
|
export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability, checkCodexSandboxPreflight, probeFilesystemWriteCapability } from "./codex-launcher.js";
|
|
6
6
|
export { createSpawnPlan } from "./cli-bridge.js";
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Usage:
|
|
15
15
|
* # Defaults to OpenAI's hosted endpoint when MARTIN_OPENAI_BASE_URL is unset.
|
|
16
16
|
* MARTIN_OPENAI_API_KEY=sk-...
|
|
17
|
-
* MARTIN_OPENAI_MODEL
|
|
17
|
+
* MARTIN_OPENAI_MODEL=<provider-model-id>
|
|
18
18
|
* martin-loop run "fix the bug" --engine openai
|
|
19
19
|
*
|
|
20
20
|
* # Or route to a third-party / self-hosted OpenAI-compatible endpoint:
|
|
@@ -51,10 +51,9 @@ export interface OpenAiCompatibleAdapterOptions {
|
|
|
51
51
|
fetchImpl?: typeof fetch;
|
|
52
52
|
}
|
|
53
53
|
export declare const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
|
|
54
|
-
export declare const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
|
|
55
54
|
export declare function resolveOpenAiCompatibleRuntimeConfig(env?: NodeJS.ProcessEnv): {
|
|
56
55
|
baseUrl: string;
|
|
57
|
-
model
|
|
56
|
+
model?: string;
|
|
58
57
|
apiKey: string;
|
|
59
58
|
apiKeyConfigured: boolean;
|
|
60
59
|
authPosture: "api_key" | "anonymous_or_local";
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Usage:
|
|
15
15
|
* # Defaults to OpenAI's hosted endpoint when MARTIN_OPENAI_BASE_URL is unset.
|
|
16
16
|
* MARTIN_OPENAI_API_KEY=sk-...
|
|
17
|
-
* MARTIN_OPENAI_MODEL
|
|
17
|
+
* MARTIN_OPENAI_MODEL=<provider-model-id>
|
|
18
18
|
* martin-loop run "fix the bug" --engine openai
|
|
19
19
|
*
|
|
20
20
|
* # Or route to a third-party / self-hosted OpenAI-compatible endpoint:
|
|
@@ -77,13 +77,27 @@ function normalizeOpenAiCompatibleUsage(input) {
|
|
|
77
77
|
};
|
|
78
78
|
const actualUsd = (input.tokensIn / 1000) * pricing.inputPer1K +
|
|
79
79
|
(input.tokensOut / 1000) * pricing.outputPer1K;
|
|
80
|
-
const provenance = input.usageWasFullyProviderReported && hasKnownPricing ? "
|
|
80
|
+
const provenance = input.usageWasFullyProviderReported && hasKnownPricing ? "calculated" : "estimated";
|
|
81
81
|
return normalizeUsage({
|
|
82
82
|
actualUsd,
|
|
83
83
|
...(provenance === "estimated" ? { estimatedUsd: actualUsd } : {}),
|
|
84
84
|
tokensIn: input.tokensIn,
|
|
85
85
|
tokensOut: input.tokensOut,
|
|
86
|
-
provenance
|
|
86
|
+
provenance,
|
|
87
|
+
providerSettlement: {
|
|
88
|
+
providerId: "openai-compatible",
|
|
89
|
+
model: input.model,
|
|
90
|
+
transport: "http",
|
|
91
|
+
source: input.usageWasFullyProviderReported ? "openai_compatible_json" : "estimated_fallback",
|
|
92
|
+
inputTokens: input.tokensIn,
|
|
93
|
+
outputTokens: input.tokensOut,
|
|
94
|
+
billingMode: input.billingMode,
|
|
95
|
+
modelSource: input.modelSource,
|
|
96
|
+
pricingSource: hasKnownPricing ? "static_catalog" : "blended_fallback",
|
|
97
|
+
pricingVersion: "embedded-v1",
|
|
98
|
+
rawUsageAvailable: input.usageWasFullyProviderReported,
|
|
99
|
+
settledAt: new Date().toISOString()
|
|
100
|
+
}
|
|
87
101
|
});
|
|
88
102
|
}
|
|
89
103
|
// ---------------------------------------------------------------------------
|
|
@@ -97,12 +111,11 @@ Follow these rules exactly:
|
|
|
97
111
|
- Be precise, minimal, and test-backed in all changes.
|
|
98
112
|
- State what you changed and why at the end of your response.`;
|
|
99
113
|
export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
|
|
100
|
-
export const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
|
|
101
114
|
export function resolveOpenAiCompatibleRuntimeConfig(env = process.env) {
|
|
102
115
|
const apiKey = env["MARTIN_OPENAI_API_KEY"] ?? "";
|
|
103
116
|
return {
|
|
104
117
|
baseUrl: env["MARTIN_OPENAI_BASE_URL"] ?? DEFAULT_OPENAI_BASE_URL,
|
|
105
|
-
model: env["MARTIN_OPENAI_MODEL"]
|
|
118
|
+
...(env["MARTIN_OPENAI_MODEL"] ? { model: env["MARTIN_OPENAI_MODEL"] } : {}),
|
|
106
119
|
apiKey,
|
|
107
120
|
apiKeyConfigured: apiKey.length > 0,
|
|
108
121
|
authPosture: apiKey.length > 0 ? "api_key" : "anonymous_or_local"
|
|
@@ -143,7 +156,16 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
143
156
|
const runtimeConfig = resolveOpenAiCompatibleRuntimeConfig();
|
|
144
157
|
const baseUrl = (options.baseUrl ?? runtimeConfig.baseUrl).replace(/\/$/, "");
|
|
145
158
|
const model = options.model ?? runtimeConfig.model;
|
|
159
|
+
if (!model) {
|
|
160
|
+
throw new Error("MODEL_CONFIGURATION_REQUIRED: set --model or MARTIN_OPENAI_MODEL for direct OpenAI-compatible execution.");
|
|
161
|
+
}
|
|
146
162
|
const apiKey = options.apiKey ?? runtimeConfig.apiKey;
|
|
163
|
+
const modelSource = options.model ? "explicit_override" : "provider_configured";
|
|
164
|
+
const billingMode = /^(?:https?:\/\/)?(?:localhost|127\.0\.0\.1|\[::1\])/iu.test(baseUrl)
|
|
165
|
+
? "local_unmetered"
|
|
166
|
+
: apiKey
|
|
167
|
+
? "metered_api"
|
|
168
|
+
: "unknown";
|
|
147
169
|
return {
|
|
148
170
|
adapterId: `openai-compatible:${model}`,
|
|
149
171
|
kind: "direct-provider",
|
|
@@ -306,7 +328,9 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
306
328
|
model,
|
|
307
329
|
tokensIn,
|
|
308
330
|
tokensOut: 0,
|
|
309
|
-
usageWasFullyProviderReported
|
|
331
|
+
usageWasFullyProviderReported,
|
|
332
|
+
modelSource,
|
|
333
|
+
billingMode
|
|
310
334
|
}),
|
|
311
335
|
verification: { passed: false, summary: "Empty response — nothing to verify." },
|
|
312
336
|
failure: { message: "empty_response" }
|
|
@@ -332,7 +356,9 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
332
356
|
model,
|
|
333
357
|
tokensIn,
|
|
334
358
|
tokensOut,
|
|
335
|
-
usageWasFullyProviderReported
|
|
359
|
+
usageWasFullyProviderReported,
|
|
360
|
+
modelSource,
|
|
361
|
+
billingMode
|
|
336
362
|
}),
|
|
337
363
|
verification,
|
|
338
364
|
execution,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MartinAdapter } from "../core/index.js";
|
|
2
|
+
import { type SpawnLike } from "./cli-bridge.js";
|
|
3
|
+
export interface VerifierOnlyAdapterOptions {
|
|
4
|
+
workingDirectory?: string;
|
|
5
|
+
verifyTimeoutMs?: number;
|
|
6
|
+
label?: string;
|
|
7
|
+
spawnImpl?: SpawnLike;
|
|
8
|
+
}
|
|
9
|
+
export declare function createVerifierOnlyAdapter(options?: VerifierOnlyAdapterOptions): MartinAdapter;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readGitChangedFiles, runVerification } from "./cli-bridge.js";
|
|
2
|
+
import { createAdapterCapabilities, normalizeUsage } from "./runtime-support.js";
|
|
3
|
+
export function createVerifierOnlyAdapter(options = {}) {
|
|
4
|
+
const workingDirectory = options.workingDirectory ?? process.cwd();
|
|
5
|
+
const verifyTimeoutMs = options.verifyTimeoutMs ?? 120_000;
|
|
6
|
+
return {
|
|
7
|
+
adapterId: "direct:verifier:verify-only",
|
|
8
|
+
kind: "direct-provider",
|
|
9
|
+
label: options.label ?? "Verifier-only adapter",
|
|
10
|
+
metadata: {
|
|
11
|
+
providerId: "verifier",
|
|
12
|
+
model: "verify-only",
|
|
13
|
+
transport: "cli",
|
|
14
|
+
capabilities: createAdapterCapabilities({
|
|
15
|
+
usageSettlement: true,
|
|
16
|
+
diffArtifacts: true,
|
|
17
|
+
workspaceMutations: false
|
|
18
|
+
})
|
|
19
|
+
},
|
|
20
|
+
async execute(request) {
|
|
21
|
+
const shouldTrackVerifierWrites = request.context.verificationPlan.length > 0 ||
|
|
22
|
+
(request.context.verificationStack?.length ?? 0) > 0;
|
|
23
|
+
const baselineChangedFiles = shouldTrackVerifierWrites
|
|
24
|
+
? new Set(await readGitChangedFiles(workingDirectory, 5_000))
|
|
25
|
+
: new Set();
|
|
26
|
+
const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack, options.spawnImpl, {
|
|
27
|
+
runId: request.loopId,
|
|
28
|
+
workspaceId: request.workspaceId,
|
|
29
|
+
cwd: workingDirectory
|
|
30
|
+
});
|
|
31
|
+
const changedFiles = shouldTrackVerifierWrites
|
|
32
|
+
? (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
|
|
33
|
+
: [];
|
|
34
|
+
const execution = { changedFiles };
|
|
35
|
+
if (verification.passed) {
|
|
36
|
+
return {
|
|
37
|
+
status: "completed",
|
|
38
|
+
summary: changedFiles.length > 0
|
|
39
|
+
? `Verifier-only run completed but modified files: ${changedFiles.join(", ")}`
|
|
40
|
+
: "Verifier-only run completed without file edits.",
|
|
41
|
+
usage: normalizeUsage({
|
|
42
|
+
actualUsd: 0,
|
|
43
|
+
tokensIn: 0,
|
|
44
|
+
tokensOut: 0,
|
|
45
|
+
provenance: "actual"
|
|
46
|
+
}),
|
|
47
|
+
verification,
|
|
48
|
+
execution
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
status: "failed",
|
|
53
|
+
summary: "Verifier-only run failed.",
|
|
54
|
+
usage: normalizeUsage({
|
|
55
|
+
actualUsd: 0,
|
|
56
|
+
tokensIn: 0,
|
|
57
|
+
tokensOut: 0,
|
|
58
|
+
provenance: "actual"
|
|
59
|
+
}),
|
|
60
|
+
verification,
|
|
61
|
+
execution,
|
|
62
|
+
failure: {
|
|
63
|
+
message: verification.summary
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -66,17 +66,27 @@ export interface LoopCost {
|
|
|
66
66
|
estimatedUsd?: number;
|
|
67
67
|
provenance?: CostProvenance;
|
|
68
68
|
providerSettlement?: ProviderUsageSettlement;
|
|
69
|
+
savingsBaseline?: {
|
|
70
|
+
usd: number;
|
|
71
|
+
source: "measured_control" | "operator_supplied" | "fixture";
|
|
72
|
+
provenance: CostProvenance;
|
|
73
|
+
};
|
|
69
74
|
}
|
|
70
|
-
export type UsageSettlementSource = "claude_json" | "codex_jsonl" | "gemini_json" | "estimated_fallback" | "unavailable";
|
|
75
|
+
export type UsageSettlementSource = "claude_json" | "codex_jsonl" | "gemini_json" | "openai_compatible_json" | "estimated_fallback" | "unavailable";
|
|
71
76
|
export interface ProviderUsageSettlement {
|
|
72
77
|
providerId: string;
|
|
73
|
-
model
|
|
78
|
+
model?: string;
|
|
74
79
|
transport?: "cli" | "http" | "routed_http";
|
|
75
80
|
source: UsageSettlementSource;
|
|
76
81
|
inputTokens: number;
|
|
77
82
|
cachedInputTokens?: number;
|
|
78
83
|
outputTokens: number;
|
|
79
84
|
reasoningOutputTokens?: number;
|
|
85
|
+
cacheCreationInputTokens?: number;
|
|
86
|
+
billingMode?: "metered_api" | "subscription" | "local_unmetered" | "unknown";
|
|
87
|
+
modelSource?: "provider_reported" | "explicit_override" | "provider_configured" | "agent_default" | "unavailable";
|
|
88
|
+
pricingSource?: "provider_reported_total" | "static_catalog" | "blended_fallback" | "none";
|
|
89
|
+
pricingVersion?: string;
|
|
80
90
|
rawUsageAvailable: boolean;
|
|
81
91
|
settledAt: string;
|
|
82
92
|
}
|
|
@@ -141,7 +151,7 @@ export interface LoopAttempt {
|
|
|
141
151
|
attemptId: string;
|
|
142
152
|
index: number;
|
|
143
153
|
adapterId: string;
|
|
144
|
-
model
|
|
154
|
+
model?: string;
|
|
145
155
|
startedAt: string;
|
|
146
156
|
completedAt?: string;
|
|
147
157
|
summary?: string;
|
|
@@ -335,7 +345,7 @@ export interface MachineState {
|
|
|
335
345
|
* Cost provenance label — every budget metric must carry this.
|
|
336
346
|
* Never conflate actual with estimated or unavailable.
|
|
337
347
|
*/
|
|
338
|
-
export type CostProvenance = "actual" | "estimated" | "unavailable";
|
|
348
|
+
export type CostProvenance = "actual" | "calculated" | "estimated" | "unavailable";
|
|
339
349
|
/**
|
|
340
350
|
* Per-attempt budget preflight estimate produced before admission.
|
|
341
351
|
* Used to gate attempts before any tokens are spent.
|
|
@@ -443,8 +453,8 @@ export { CONTEXT_SHADOW_MANIFEST_VERSION, CONTEXT_C5_VERSION } from "./context-s
|
|
|
443
453
|
export type { ContextC5EnvelopeV1, ContextEvidence, ContextShadowDecisionV1, ContextShadowManifestV1, ContextShadowSegmentInput, ContextShadowSegmentKind } from "./context-shadow.js";
|
|
444
454
|
export { CONTEXT_MANIFEST_VERSION, CONTEXT_LEDGER_VERSION } from "./context-manifest.js";
|
|
445
455
|
export type { ContinuationCheckpoint, ContextBudget, ContextCandidateDecision, ContextFaultRequest, ContextFaultResult, ContextKind, ContextLedgerEntry, ContextManifest, ContextObject, ContextPolicy, ContextPriority, ContextSensitivity, ContextTrust, TaskItem, UsageEvidence } from "./context-manifest.js";
|
|
446
|
-
export { EVIDENCE_STATUSES, TEST_INTEGRITY_STATUSES, TEST_INTEGRITY_VERDICTS, VERIFIED_HANDOFF_OUTCOMES, } from "./verified-handoff.js";
|
|
447
|
-
export type { EvidenceStatus, TestIntegrityStatus, TestIntegrityVerdict, VerifiedHandoffCheckV1, VerifiedHandoffOutcome, VerifiedHandoffRecoveryV1, VerifiedHandoffRequirementV1, VerifiedHandoffScopeV1, VerifiedHandoffTestIntegrityV1, VerifiedHandoffV1, } from "./verified-handoff.js";
|
|
456
|
+
export { EVIDENCE_STATUSES, EXECUTION_MODES, TEST_INTEGRITY_STATUSES, TEST_INTEGRITY_VERDICTS, VERIFIED_HANDOFF_OUTCOMES, } from "./verified-handoff.js";
|
|
457
|
+
export type { EvidenceStatus, ExecutionMode, TestIntegrityStatus, TestIntegrityVerdict, VerifiedHandoffCheckV1, VerifiedHandoffOutcome, VerifiedHandoffRecoveryV1, VerifiedHandoffRequirementV1, VerifiedHandoffScopeV1, VerifiedHandoffTestIntegrityV1, VerifiedHandoffV1, } from "./verified-handoff.js";
|
|
448
458
|
export { HANDOFF_SCHEMA_VERSION } from "./context-handoff.js";
|
|
449
459
|
export type { ChainIntegrityState, ContextCircuitBreakResult, ContextExclusionDecision, ContextHandoffArtifact, ContextHandoffClaim, ContextHandoffReceipt, ContextHandoffVerification, HandoffClaimState } from "./context-handoff.js";
|
|
450
460
|
export { MISSION_SCHEMA_VERSION, MISSION_STATUSES, ALLOWED_MISSION_TRANSITIONS, createMissionRecord, isMissionTransitionAllowed } from './mission.js';
|
|
@@ -243,7 +243,7 @@ export { CONTEXT_SHADOW_MANIFEST_VERSION, CONTEXT_C5_VERSION } from "./context-s
|
|
|
243
243
|
// ─── Context Runtime — A-CTX-1 ───────────────────────────────────────────────
|
|
244
244
|
export { CONTEXT_MANIFEST_VERSION, CONTEXT_LEDGER_VERSION } from "./context-manifest.js";
|
|
245
245
|
// ─── Track A — Verified Handoff ───────────────────────────────────────────────
|
|
246
|
-
export { EVIDENCE_STATUSES, TEST_INTEGRITY_STATUSES, TEST_INTEGRITY_VERDICTS, VERIFIED_HANDOFF_OUTCOMES, } from "./verified-handoff.js";
|
|
246
|
+
export { EVIDENCE_STATUSES, EXECUTION_MODES, TEST_INTEGRITY_STATUSES, TEST_INTEGRITY_VERDICTS, VERIFIED_HANDOFF_OUTCOMES, } from "./verified-handoff.js";
|
|
247
247
|
// ─── Context Handoff — A-CTX-2 ───────────────────────────────────────────────
|
|
248
248
|
export { HANDOFF_SCHEMA_VERSION } from "./context-handoff.js";
|
|
249
249
|
export { MISSION_SCHEMA_VERSION, MISSION_STATUSES, ALLOWED_MISSION_TRANSITIONS, createMissionRecord, isMissionTransitionAllowed } from './mission.js';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CostProvenance, LoopLifecycleState, LoopStatus, ReceiptIntegritySummary } from "./index.js";
|
|
2
2
|
export declare const VERIFIED_HANDOFF_OUTCOMES: readonly ["VERIFIED", "STOPPED", "NEEDS_REVIEW"];
|
|
3
3
|
export type VerifiedHandoffOutcome = (typeof VERIFIED_HANDOFF_OUTCOMES)[number];
|
|
4
|
+
export declare const EXECUTION_MODES: readonly ["governed", "verification_only", "simulated"];
|
|
5
|
+
export type ExecutionMode = (typeof EXECUTION_MODES)[number];
|
|
4
6
|
export declare const EVIDENCE_STATUSES: readonly ["PASSED", "FAILED", "CONTRADICTED", "NOT_RUN", "NOT_EVALUATED"];
|
|
5
7
|
export type EvidenceStatus = (typeof EVIDENCE_STATUSES)[number];
|
|
6
8
|
/**
|
|
@@ -71,6 +73,10 @@ export interface VerifiedHandoffV1 {
|
|
|
71
73
|
verificationPlan: string[];
|
|
72
74
|
};
|
|
73
75
|
outcome: VerifiedHandoffOutcome;
|
|
76
|
+
/** Added compatibly in 1.0.0; missing legacy values must be treated as non-governed. */
|
|
77
|
+
executionMode?: ExecutionMode;
|
|
78
|
+
/** Only an explicit true value is eligible for a governed VERIFIED claim. */
|
|
79
|
+
governanceClaimEligible?: boolean;
|
|
74
80
|
sourceStatus: {
|
|
75
81
|
status: LoopStatus;
|
|
76
82
|
lifecycleState: LoopLifecycleState;
|
|
@@ -9,6 +9,7 @@ export interface RepoGroundingIndex {
|
|
|
9
9
|
createdAt: string;
|
|
10
10
|
fileCount: number;
|
|
11
11
|
files: RepoGroundingFile[];
|
|
12
|
+
repositoryFingerprint?: string;
|
|
12
13
|
}
|
|
13
14
|
export interface RepoGroundingHit {
|
|
14
15
|
path: string;
|
|
@@ -18,7 +19,7 @@ export interface RepoGroundingHit {
|
|
|
18
19
|
}
|
|
19
20
|
export declare function resolveGroundingRoot(env?: NodeJS.ProcessEnv): string;
|
|
20
21
|
export declare function loadOrBuildRepoGroundingIndex(repoRoot: string): Promise<RepoGroundingIndex>;
|
|
21
|
-
export declare function buildRepoGroundingIndex(repoRoot: string): Promise<RepoGroundingIndex>;
|
|
22
|
+
export declare function buildRepoGroundingIndex(repoRoot: string, repositoryFingerprint?: string): Promise<RepoGroundingIndex>;
|
|
22
23
|
export declare function queryRepoGroundingIndex(index: RepoGroundingIndex, query: string, limit?: number): RepoGroundingHit[];
|
|
23
24
|
export type GroundingViolationKind = "file_not_found" | "symbol_not_found" | "import_not_found" | "patch_outside_allowed_paths";
|
|
24
25
|
export interface GroundingViolation {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { extname, join, relative } from "node:path";
|
|
4
5
|
const TEXT_EXTENSIONS = new Set([
|
|
@@ -18,14 +19,16 @@ export function resolveGroundingRoot(env = process.env) {
|
|
|
18
19
|
}
|
|
19
20
|
export async function loadOrBuildRepoGroundingIndex(repoRoot) {
|
|
20
21
|
const cachePath = getGroundingCachePath(repoRoot);
|
|
22
|
+
const repositoryFingerprint = await computeRepositoryFingerprint(repoRoot);
|
|
21
23
|
try {
|
|
22
24
|
const cached = JSON.parse(await readFile(cachePath, "utf8"));
|
|
23
|
-
if (cached?.schemaVersion === "martin.grounding.v1"
|
|
25
|
+
if (cached?.schemaVersion === "martin.grounding.v1" &&
|
|
26
|
+
cached.repositoryFingerprint === repositoryFingerprint) {
|
|
24
27
|
return cached;
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
catch { }
|
|
28
|
-
const index = await buildRepoGroundingIndex(repoRoot);
|
|
31
|
+
const index = await buildRepoGroundingIndex(repoRoot, repositoryFingerprint);
|
|
29
32
|
try {
|
|
30
33
|
await mkdir(resolveGroundingRoot(), { recursive: true });
|
|
31
34
|
await writeFile(cachePath, JSON.stringify(index, null, 2), "utf8");
|
|
@@ -36,7 +39,7 @@ export async function loadOrBuildRepoGroundingIndex(repoRoot) {
|
|
|
36
39
|
}
|
|
37
40
|
return index;
|
|
38
41
|
}
|
|
39
|
-
export async function buildRepoGroundingIndex(repoRoot) {
|
|
42
|
+
export async function buildRepoGroundingIndex(repoRoot, repositoryFingerprint) {
|
|
40
43
|
const files = [];
|
|
41
44
|
const discovered = await walk(repoRoot, repoRoot, files, { count: 0 });
|
|
42
45
|
return {
|
|
@@ -44,9 +47,41 @@ export async function buildRepoGroundingIndex(repoRoot) {
|
|
|
44
47
|
repoRoot,
|
|
45
48
|
createdAt: new Date().toISOString(),
|
|
46
49
|
fileCount: discovered.count,
|
|
47
|
-
files
|
|
50
|
+
files,
|
|
51
|
+
repositoryFingerprint: repositoryFingerprint ?? await computeRepositoryFingerprint(repoRoot)
|
|
48
52
|
};
|
|
49
53
|
}
|
|
54
|
+
async function computeRepositoryFingerprint(repoRoot) {
|
|
55
|
+
const entries = [];
|
|
56
|
+
await collectFingerprintEntries(repoRoot, repoRoot, entries, { count: 0 });
|
|
57
|
+
entries.sort();
|
|
58
|
+
return createHash("sha256").update(entries.join("\n")).digest("hex");
|
|
59
|
+
}
|
|
60
|
+
async function collectFingerprintEntries(repoRoot, currentDir, entries, state) {
|
|
61
|
+
if (state.count >= MAX_FILES)
|
|
62
|
+
return;
|
|
63
|
+
const directoryEntries = await readdir(currentDir, { withFileTypes: true });
|
|
64
|
+
for (const entry of directoryEntries) {
|
|
65
|
+
if (state.count >= MAX_FILES)
|
|
66
|
+
break;
|
|
67
|
+
const absPath = join(currentDir, entry.name);
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
if (!IGNORED_DIRS.has(entry.name)) {
|
|
70
|
+
await collectFingerprintEntries(repoRoot, absPath, entries, state);
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
|
|
75
|
+
continue;
|
|
76
|
+
try {
|
|
77
|
+
const metadata = await stat(absPath);
|
|
78
|
+
const relPath = relative(repoRoot, absPath).replace(/\\/g, "/");
|
|
79
|
+
entries.push(`${relPath}:${metadata.size}:${metadata.mtimeMs}`);
|
|
80
|
+
state.count += 1;
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
50
85
|
export function queryRepoGroundingIndex(index, query, limit = 6) {
|
|
51
86
|
const terms = tokenize(query);
|
|
52
87
|
if (terms.length === 0)
|