@martinloop/mcp 0.3.8 → 0.5.0
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 +90 -106
- 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 +190 -238
- 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 +152 -7
- package/dist/tools/run-dossier.d.ts +3 -1
- package/dist/tools/run-dossier.js +40 -2
- package/dist/tools/run-loop.d.ts +1 -4
- package/dist/tools/run-loop.js +6 -15
- package/dist/tools/tool-support.d.ts +2 -1
- package/dist/tools/tool-support.js +1 -12
- package/dist/vendor/adapters/claude-cli.js +38 -35
- 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 +1 -3
- package/dist/vendor/adapters/index.js +1 -3
- package/dist/vendor/adapters/openai-compatible.js +57 -18
- 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/exits.d.ts +126 -0
- package/dist/vendor/contracts/exits.js +32 -0
- package/dist/vendor/contracts/index.d.ts +39 -2
- package/dist/vendor/contracts/index.js +22 -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 +100 -0
- package/dist/vendor/contracts/verified-handoff.js +35 -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/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/index.d.ts +42 -1
- package/dist/vendor/core/index.js +1086 -662
- 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/policy.d.ts +2 -0
- package/dist/vendor/core/policy.js +5 -2
- package/dist/vendor/core/rollback.js +15 -1
- 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 +67 -0
- package/dist/vendor/core/verified-handoff.js +210 -0
- package/package.json +76 -83
- package/server.json +21 -21
- package/LICENSE +0 -216
- package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +0 -146
- package/dist/vendor/adapters/adapters/src/claude-cli.js +0 -1100
- package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +0 -103
- package/dist/vendor/adapters/adapters/src/cli-bridge.js +0 -569
- package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +0 -78
- package/dist/vendor/adapters/adapters/src/codex-launcher.js +0 -663
- package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +0 -10
- package/dist/vendor/adapters/adapters/src/direct-provider.js +0 -40
- package/dist/vendor/adapters/adapters/src/index.d.ts +0 -8
- package/dist/vendor/adapters/adapters/src/index.js +0 -8
- package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +0 -62
- package/dist/vendor/adapters/adapters/src/openai-compatible.js +0 -306
- package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +0 -17
- package/dist/vendor/adapters/adapters/src/runtime-support.js +0 -59
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +0 -8
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +0 -40
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +0 -8
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +0 -12
- package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +0 -9
- package/dist/vendor/adapters/adapters/src/verifier-only.js +0 -63
- package/dist/vendor/adapters/contracts/src/context-graph.d.ts +0 -48
- package/dist/vendor/adapters/contracts/src/context-graph.js +0 -26
- package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +0 -78
- package/dist/vendor/adapters/contracts/src/execution-policy.js +0 -43
- package/dist/vendor/adapters/contracts/src/governance.d.ts +0 -24
- package/dist/vendor/adapters/contracts/src/governance.js +0 -11
- package/dist/vendor/adapters/contracts/src/identity.d.ts +0 -29
- package/dist/vendor/adapters/contracts/src/identity.js +0 -30
- package/dist/vendor/adapters/contracts/src/index.d.ts +0 -412
- package/dist/vendor/adapters/contracts/src/index.js +0 -225
- package/dist/vendor/adapters/contracts/src/operator.d.ts +0 -19
- package/dist/vendor/adapters/contracts/src/operator.js +0 -11
- package/dist/vendor/adapters/contracts/src/trajectory.d.ts +0 -26
- package/dist/vendor/adapters/contracts/src/trajectory.js +0 -27
- package/dist/vendor/adapters/core/src/compiler.d.ts +0 -52
- package/dist/vendor/adapters/core/src/compiler.js +0 -52
- package/dist/vendor/adapters/core/src/context-graph.d.ts +0 -3
- package/dist/vendor/adapters/core/src/context-graph.js +0 -371
- package/dist/vendor/adapters/core/src/context-integrity.d.ts +0 -26
- package/dist/vendor/adapters/core/src/context-integrity.js +0 -80
- package/dist/vendor/adapters/core/src/grounding.d.ts +0 -38
- package/dist/vendor/adapters/core/src/grounding.js +0 -273
- package/dist/vendor/adapters/core/src/identity.d.ts +0 -20
- package/dist/vendor/adapters/core/src/identity.js +0 -86
- package/dist/vendor/adapters/core/src/index.d.ts +0 -186
- package/dist/vendor/adapters/core/src/index.js +0 -1495
- package/dist/vendor/adapters/core/src/leash.d.ts +0 -48
- package/dist/vendor/adapters/core/src/leash.js +0 -497
- package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +0 -18
- package/dist/vendor/adapters/core/src/persistence/compiler.js +0 -34
- package/dist/vendor/adapters/core/src/persistence/index.d.ts +0 -10
- package/dist/vendor/adapters/core/src/persistence/index.js +0 -5
- package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +0 -39
- package/dist/vendor/adapters/core/src/persistence/integrity.js +0 -252
- package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +0 -23
- package/dist/vendor/adapters/core/src/persistence/ledger.js +0 -9
- package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +0 -63
- package/dist/vendor/adapters/core/src/persistence/memory-store.js +0 -115
- package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +0 -66
- package/dist/vendor/adapters/core/src/persistence/runs-reader.js +0 -140
- package/dist/vendor/adapters/core/src/persistence/store.d.ts +0 -89
- package/dist/vendor/adapters/core/src/persistence/store.js +0 -125
- package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +0 -85
- package/dist/vendor/adapters/core/src/persistence/trace-store.js +0 -125
- package/dist/vendor/adapters/core/src/policy-compiler.d.ts +0 -2
- package/dist/vendor/adapters/core/src/policy-compiler.js +0 -195
- package/dist/vendor/adapters/core/src/policy.d.ts +0 -156
- package/dist/vendor/adapters/core/src/policy.js +0 -704
- package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +0 -53
- package/dist/vendor/adapters/core/src/repo-analyzer.js +0 -263
- package/dist/vendor/adapters/core/src/rollback.d.ts +0 -15
- package/dist/vendor/adapters/core/src/rollback.js +0 -253
- package/dist/vendor/adapters/core/src/routing.d.ts +0 -82
- package/dist/vendor/adapters/core/src/routing.js +0 -336
- package/dist/vendor/adapters/core/src/test-integrity.d.ts +0 -64
- package/dist/vendor/adapters/core/src/test-integrity.js +0 -197
- package/dist/vendor/adapters/core/src/trajectory.d.ts +0 -18
- package/dist/vendor/adapters/core/src/trajectory.js +0 -295
- 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/verifier-only.d.ts +0 -9
- package/dist/vendor/adapters/verifier-only.js +0 -63
|
@@ -1,1100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Real agent-CLI adapters.
|
|
3
|
-
*
|
|
4
|
-
* Exports a generic factory (`createAgentCliAdapter`) and two pre-configured
|
|
5
|
-
* factories (`createClaudeCliAdapter`, `createCodexCliAdapter`) that spawn
|
|
6
|
-
* the respective AI coding CLI as a child subprocess.
|
|
7
|
-
*
|
|
8
|
-
* Usage in CLI:
|
|
9
|
-
* createClaudeCliAdapter({ workingDirectory: process.cwd() })
|
|
10
|
-
* createCodexCliAdapter({ workingDirectory: process.cwd() })
|
|
11
|
-
*
|
|
12
|
-
* MCP tools and integration tests use the same factories.
|
|
13
|
-
*/
|
|
14
|
-
import { readGitExecutionArtifacts, resolveGitRepositoryRoot, runSubprocess, runVerification } from "./cli-bridge.js";
|
|
15
|
-
import { buildCodexExecArgs, DEFAULT_CODEX_CHATGPT_MODEL } from "./codex-launcher.js";
|
|
16
|
-
import { createAdapterCapabilities, normalizeStructuredErrors, normalizeUsage } from "./runtime-support.js";
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Cost estimation
|
|
19
|
-
//
|
|
20
|
-
// Token costs are estimated using a blended average across top models:
|
|
21
|
-
// Anthropic Sonnet, OpenAI GPT-4o Mini, Gemini Flash, Meta Llama 3.
|
|
22
|
-
// Override at runtime with --input-cost-per-1k / --output-cost-per-1k CLI
|
|
23
|
-
// flags or martin.config.yaml pricing section.
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
const BLENDED_INPUT_COST_PER_1K = 0.003; // $/1K input tokens
|
|
26
|
-
const BLENDED_OUTPUT_COST_PER_1K = 0.012; // $/1K output tokens
|
|
27
|
-
// Per-model overrides for common Claude models (fallback: blended average)
|
|
28
|
-
const MODEL_PRICING = {
|
|
29
|
-
"claude-opus-4-6": { inputPer1K: 0.015, outputPer1K: 0.075 },
|
|
30
|
-
"claude-sonnet-4-6": { inputPer1K: 0.003, outputPer1K: 0.015 },
|
|
31
|
-
"claude-haiku-4-5": { inputPer1K: 0.00025, outputPer1K: 0.00125 },
|
|
32
|
-
// Keep legacy names working
|
|
33
|
-
"claude-opus": { inputPer1K: 0.015, outputPer1K: 0.075 },
|
|
34
|
-
"claude-sonnet": { inputPer1K: 0.003, outputPer1K: 0.015 },
|
|
35
|
-
"claude-haiku": { inputPer1K: 0.00025, outputPer1K: 0.00125 },
|
|
36
|
-
// OpenAI coding models
|
|
37
|
-
"codex": { inputPer1K: 0.00125, cachedInputPer1K: 0.000125, outputPer1K: 0.01 },
|
|
38
|
-
"gpt-5-codex": { inputPer1K: 0.00125, cachedInputPer1K: 0.000125, outputPer1K: 0.01 },
|
|
39
|
-
"gpt-5.1-codex": { inputPer1K: 0.00125, cachedInputPer1K: 0.000125, outputPer1K: 0.01 },
|
|
40
|
-
"gpt-5.1-codex-max": { inputPer1K: 0.00125, cachedInputPer1K: 0.000125, outputPer1K: 0.01 },
|
|
41
|
-
"gpt-5.2-codex": { inputPer1K: 0.00175, cachedInputPer1K: 0.000175, outputPer1K: 0.014 },
|
|
42
|
-
"codex-mini-latest": { inputPer1K: 0.0015, cachedInputPer1K: 0.000375, outputPer1K: 0.006 }
|
|
43
|
-
};
|
|
44
|
-
function extractUsage(parsed, modelLabel) {
|
|
45
|
-
if (!parsed?.usage) {
|
|
46
|
-
return normalizeUsage({
|
|
47
|
-
actualUsd: 0,
|
|
48
|
-
tokensIn: 0,
|
|
49
|
-
tokensOut: 0,
|
|
50
|
-
provenance: "unavailable"
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
const promptTokens = (parsed.usage.inputTokens ?? parsed.usage.input_tokens ?? 0) +
|
|
54
|
-
(parsed.usage.cacheCreationInputTokens ?? parsed.usage.cache_creation_input_tokens ?? 0);
|
|
55
|
-
const cachedInputTokens = parsed.usage.cacheReadInputTokens ?? parsed.usage.cache_read_input_tokens ?? 0;
|
|
56
|
-
const tokensIn = promptTokens + cachedInputTokens;
|
|
57
|
-
const tokensOut = parsed.usage.outputTokens ?? parsed.usage.output_tokens ?? 0;
|
|
58
|
-
const pricing = (modelLabel ? MODEL_PRICING[modelLabel] : undefined) ??
|
|
59
|
-
{ inputPer1K: BLENDED_INPUT_COST_PER_1K, outputPer1K: BLENDED_OUTPUT_COST_PER_1K };
|
|
60
|
-
// Prefer Claude's own authoritative total_cost_usd (present on the final
|
|
61
|
-
// `result` event in json/stream-json output) over our pricing-table estimate,
|
|
62
|
-
// which can drift from real billed cost (cache discounts, surcharges, etc).
|
|
63
|
-
const hasAuthoritativeCost = typeof parsed.total_cost_usd === "number";
|
|
64
|
-
const actualUsd = hasAuthoritativeCost
|
|
65
|
-
? parsed.total_cost_usd
|
|
66
|
-
: (promptTokens / 1000) * pricing.inputPer1K +
|
|
67
|
-
(cachedInputTokens / 1000) * (pricing.cachedInputPer1K ?? pricing.inputPer1K) +
|
|
68
|
-
(tokensOut / 1000) * pricing.outputPer1K;
|
|
69
|
-
return normalizeUsage({
|
|
70
|
-
actualUsd: Number(actualUsd.toFixed(6)),
|
|
71
|
-
tokensIn,
|
|
72
|
-
tokensOut,
|
|
73
|
-
cachedInputTokens,
|
|
74
|
-
provenance: hasAuthoritativeCost ? "actual" : "estimated",
|
|
75
|
-
providerSettlement: {
|
|
76
|
-
providerId: "claude",
|
|
77
|
-
model: modelLabel ?? "claude",
|
|
78
|
-
transport: "cli",
|
|
79
|
-
source: "claude_json",
|
|
80
|
-
inputTokens: promptTokens,
|
|
81
|
-
cachedInputTokens,
|
|
82
|
-
outputTokens: tokensOut,
|
|
83
|
-
rawUsageAvailable: true,
|
|
84
|
-
settledAt: new Date().toISOString()
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
function extractCodexJsonlResult(stdout, modelLabel) {
|
|
89
|
-
const events = stdout
|
|
90
|
-
.split(/\r?\n/u)
|
|
91
|
-
.map((line) => line.trim())
|
|
92
|
-
.filter(Boolean)
|
|
93
|
-
.map((line) => {
|
|
94
|
-
try {
|
|
95
|
-
return JSON.parse(line);
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
|
-
})
|
|
101
|
-
.filter((event) => event !== undefined);
|
|
102
|
-
if (events.length === 0) {
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
const latestAgentMessage = [...events]
|
|
106
|
-
.reverse()
|
|
107
|
-
.find((event) => event.type === "item.completed" && event.item?.type === "agent_message");
|
|
108
|
-
const latestTurnCompleted = [...events]
|
|
109
|
-
.reverse()
|
|
110
|
-
.find((event) => event.type === "turn.completed" && event.usage !== undefined);
|
|
111
|
-
const summary = typeof latestAgentMessage?.item?.text === "string" && latestAgentMessage.item.text.trim().length > 0
|
|
112
|
-
? latestAgentMessage.item.text.trim()
|
|
113
|
-
: stdout.trim();
|
|
114
|
-
if (!latestTurnCompleted?.usage) {
|
|
115
|
-
return {
|
|
116
|
-
summary,
|
|
117
|
-
usage: normalizeUsage({
|
|
118
|
-
actualUsd: 0,
|
|
119
|
-
tokensIn: 0,
|
|
120
|
-
tokensOut: 0,
|
|
121
|
-
provenance: "unavailable",
|
|
122
|
-
providerSettlement: {
|
|
123
|
-
providerId: "codex",
|
|
124
|
-
model: modelLabel ?? "codex",
|
|
125
|
-
transport: "cli",
|
|
126
|
-
source: "unavailable",
|
|
127
|
-
inputTokens: 0,
|
|
128
|
-
outputTokens: 0,
|
|
129
|
-
rawUsageAvailable: false,
|
|
130
|
-
settledAt: new Date().toISOString()
|
|
131
|
-
}
|
|
132
|
-
})
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
const promptTokens = latestTurnCompleted.usage.input_tokens ?? 0;
|
|
136
|
-
const cachedInputTokens = latestTurnCompleted.usage.cached_input_tokens ?? 0;
|
|
137
|
-
const outputTokens = latestTurnCompleted.usage.output_tokens ?? 0;
|
|
138
|
-
const reasoningOutputTokens = latestTurnCompleted.usage.reasoning_output_tokens ?? 0;
|
|
139
|
-
const tokensIn = promptTokens + cachedInputTokens;
|
|
140
|
-
const tokensOut = outputTokens + reasoningOutputTokens;
|
|
141
|
-
const pricing = (modelLabel ? MODEL_PRICING[modelLabel] : undefined) ??
|
|
142
|
-
MODEL_PRICING["codex"] ??
|
|
143
|
-
{ inputPer1K: BLENDED_INPUT_COST_PER_1K, outputPer1K: BLENDED_OUTPUT_COST_PER_1K };
|
|
144
|
-
const actualUsd = (promptTokens / 1000) * pricing.inputPer1K +
|
|
145
|
-
(cachedInputTokens / 1000) * (pricing.cachedInputPer1K ?? pricing.inputPer1K) +
|
|
146
|
-
(tokensOut / 1000) * pricing.outputPer1K;
|
|
147
|
-
return {
|
|
148
|
-
summary,
|
|
149
|
-
usage: normalizeUsage({
|
|
150
|
-
actualUsd: Number(actualUsd.toFixed(6)),
|
|
151
|
-
tokensIn,
|
|
152
|
-
tokensOut,
|
|
153
|
-
cachedInputTokens,
|
|
154
|
-
reasoningTokensOut: reasoningOutputTokens,
|
|
155
|
-
provenance: "actual",
|
|
156
|
-
providerSettlement: {
|
|
157
|
-
providerId: "codex",
|
|
158
|
-
model: modelLabel ?? "codex",
|
|
159
|
-
transport: "cli",
|
|
160
|
-
source: "codex_jsonl",
|
|
161
|
-
inputTokens: promptTokens,
|
|
162
|
-
cachedInputTokens,
|
|
163
|
-
outputTokens,
|
|
164
|
-
reasoningOutputTokens,
|
|
165
|
-
rawUsageAvailable: true,
|
|
166
|
-
settledAt: new Date().toISOString()
|
|
167
|
-
}
|
|
168
|
-
})
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
function extractGeminiJsonResult(stdout, modelLabel) {
|
|
172
|
-
let parsed;
|
|
173
|
-
try {
|
|
174
|
-
parsed = JSON.parse(stdout);
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
return undefined;
|
|
178
|
-
}
|
|
179
|
-
const summary = typeof parsed.response === "string" && parsed.response.trim().length > 0
|
|
180
|
-
? parsed.response.trim()
|
|
181
|
-
: typeof parsed.error?.message === "string" && parsed.error.message.trim().length > 0
|
|
182
|
-
? parsed.error.message.trim()
|
|
183
|
-
: stdout.trim();
|
|
184
|
-
const promptTokens = parsed.stats?.inputTokens ?? 0;
|
|
185
|
-
const cachedInputTokens = parsed.stats?.cachedReadTokens ?? 0;
|
|
186
|
-
const outputTokens = parsed.stats?.outputTokens ?? 0;
|
|
187
|
-
const reasoningOutputTokens = parsed.stats?.thoughtTokens ?? 0;
|
|
188
|
-
const hasUsage = parsed.stats !== undefined &&
|
|
189
|
-
(promptTokens > 0 || cachedInputTokens > 0 || outputTokens > 0 || reasoningOutputTokens > 0);
|
|
190
|
-
if (!hasUsage) {
|
|
191
|
-
return {
|
|
192
|
-
summary,
|
|
193
|
-
usage: normalizeUsage({
|
|
194
|
-
actualUsd: 0,
|
|
195
|
-
tokensIn: 0,
|
|
196
|
-
tokensOut: 0,
|
|
197
|
-
provenance: "unavailable",
|
|
198
|
-
providerSettlement: {
|
|
199
|
-
providerId: "gemini",
|
|
200
|
-
model: modelLabel ?? "flash",
|
|
201
|
-
transport: "cli",
|
|
202
|
-
source: "unavailable",
|
|
203
|
-
inputTokens: 0,
|
|
204
|
-
outputTokens: 0,
|
|
205
|
-
rawUsageAvailable: false,
|
|
206
|
-
settledAt: new Date().toISOString()
|
|
207
|
-
}
|
|
208
|
-
})
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
const tokensIn = promptTokens + cachedInputTokens;
|
|
212
|
-
const tokensOut = outputTokens + reasoningOutputTokens;
|
|
213
|
-
const pricing = (modelLabel ? MODEL_PRICING[modelLabel] : undefined) ??
|
|
214
|
-
{ inputPer1K: BLENDED_INPUT_COST_PER_1K, outputPer1K: BLENDED_OUTPUT_COST_PER_1K };
|
|
215
|
-
const actualUsd = (promptTokens / 1000) * pricing.inputPer1K +
|
|
216
|
-
(cachedInputTokens / 1000) * (pricing.cachedInputPer1K ?? pricing.inputPer1K) +
|
|
217
|
-
(tokensOut / 1000) * pricing.outputPer1K;
|
|
218
|
-
return {
|
|
219
|
-
summary,
|
|
220
|
-
usage: normalizeUsage({
|
|
221
|
-
actualUsd: Number(actualUsd.toFixed(6)),
|
|
222
|
-
tokensIn,
|
|
223
|
-
tokensOut,
|
|
224
|
-
cachedInputTokens,
|
|
225
|
-
reasoningTokensOut: reasoningOutputTokens,
|
|
226
|
-
provenance: "actual",
|
|
227
|
-
providerSettlement: {
|
|
228
|
-
providerId: "gemini",
|
|
229
|
-
model: modelLabel ?? "flash",
|
|
230
|
-
transport: "cli",
|
|
231
|
-
source: "gemini_json",
|
|
232
|
-
inputTokens: promptTokens,
|
|
233
|
-
cachedInputTokens,
|
|
234
|
-
outputTokens,
|
|
235
|
-
reasoningOutputTokens,
|
|
236
|
-
rawUsageAvailable: true,
|
|
237
|
-
settledAt: new Date().toISOString()
|
|
238
|
-
}
|
|
239
|
-
})
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
function createStreamingUsageInspector(capUsd, modelLabel, promptTokenEstimate) {
|
|
243
|
-
const pricing = (modelLabel ? MODEL_PRICING[modelLabel] : undefined) ??
|
|
244
|
-
{ inputPer1K: BLENDED_INPUT_COST_PER_1K, outputPer1K: BLENDED_OUTPUT_COST_PER_1K };
|
|
245
|
-
// Safety margin: terminate at 80% of cap to bound one-turn overshoot.
|
|
246
|
-
// Without this, a single expensive turn can blow past the cap before the
|
|
247
|
-
// next check fires (proven live: $1.50 cap → $28.42 actual).
|
|
248
|
-
const largeContext = promptTokenEstimate > 10_000;
|
|
249
|
-
const effectiveCapRatio = largeContext ? 0.7 : 0.8;
|
|
250
|
-
const effectiveCapUsd = capUsd * effectiveCapRatio;
|
|
251
|
-
// Token-count ceiling fallback: if no usage events are ever parsed (e.g.
|
|
252
|
-
// Claude changes its stream-json event format), use raw byte volume as a
|
|
253
|
-
// last-resort circuit breaker. Derived from budget / blended cost per char.
|
|
254
|
-
const blendedCostPerChar = (pricing.inputPer1K / 1000 / 4) + (pricing.outputPer1K / 1000 / 4);
|
|
255
|
-
const bytesCeiling = blendedCostPerChar > 0
|
|
256
|
-
? Math.ceil((capUsd / blendedCostPerChar) * 2)
|
|
257
|
-
: 100_000_000;
|
|
258
|
-
// Time-based fallback: if we receive data but no usage events for this long,
|
|
259
|
-
// estimate spend from byte volume and enforce the cap. Prevents the inspector
|
|
260
|
-
// from going blind when Claude changes its stream-json event format.
|
|
261
|
-
const USAGE_BLIND_TIMEOUT_MS = 30_000;
|
|
262
|
-
let buffer = "";
|
|
263
|
-
let cumulativeUsd = 0;
|
|
264
|
-
let tokensIn = 0;
|
|
265
|
-
let tokensOut = 0;
|
|
266
|
-
let turns = 0;
|
|
267
|
-
let totalBytes = 0;
|
|
268
|
-
let usageEventSeen = false;
|
|
269
|
-
let firstChunkAt;
|
|
270
|
-
let finalResult;
|
|
271
|
-
const checkBudgetExceeded = (terminate) => {
|
|
272
|
-
if (capUsd > 0 && cumulativeUsd > effectiveCapUsd) {
|
|
273
|
-
terminate(`Streaming usage cap exceeded after ${String(turns)} turn(s): cumulative cost ~$${cumulativeUsd.toFixed(4)} ` +
|
|
274
|
-
`surpassed the per-attempt cap $${capUsd.toFixed(4)} (${String(Math.round(effectiveCapRatio * 100))}% threshold: $${effectiveCapUsd.toFixed(4)}). ` +
|
|
275
|
-
`Subprocess terminated to bound runaway overspend.`);
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
const extractUsageFromEvent = (event, terminate) => {
|
|
279
|
-
// Check for authoritative total_cost_usd on ANY event — if Claude reports
|
|
280
|
-
// cost exceeding cap, terminate immediately regardless of event type.
|
|
281
|
-
if (typeof event.total_cost_usd === "number" && event.total_cost_usd > 0) {
|
|
282
|
-
cumulativeUsd = event.total_cost_usd;
|
|
283
|
-
checkBudgetExceeded(terminate);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
// Extract usage from any event shape that carries it:
|
|
287
|
-
// - { type: "assistant", message: { usage: { ... } } } (original format)
|
|
288
|
-
// - { usage: { input_tokens, output_tokens, ... } } (top-level usage)
|
|
289
|
-
// - { message: { usage: { ... } } } (nested without type check)
|
|
290
|
-
const usage = (event.message && typeof event.message === "object" && "usage" in event.message
|
|
291
|
-
? event.message.usage
|
|
292
|
-
: undefined) ??
|
|
293
|
-
(event.usage && typeof event.usage === "object" ? event.usage : undefined);
|
|
294
|
-
if (!usage || typeof usage !== "object") {
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
const usageRecord = usage;
|
|
298
|
-
const turnTokensIn = (usageRecord.input_tokens ?? usageRecord.inputTokens ?? 0) +
|
|
299
|
-
(usageRecord.cache_read_input_tokens ?? usageRecord.cacheReadInputTokens ?? 0) +
|
|
300
|
-
(usageRecord.cache_creation_input_tokens ?? usageRecord.cacheCreationInputTokens ?? 0);
|
|
301
|
-
const turnTokensOut = usageRecord.output_tokens ?? usageRecord.outputTokens ?? 0;
|
|
302
|
-
if (turnTokensIn === 0 && turnTokensOut === 0) {
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
const turnUsd = (turnTokensIn / 1000) * pricing.inputPer1K + (turnTokensOut / 1000) * pricing.outputPer1K;
|
|
306
|
-
const remainingBudgetBeforeTurn = Math.max(capUsd - cumulativeUsd, 0);
|
|
307
|
-
if (capUsd > 0 && remainingBudgetBeforeTurn > 0 && turnUsd > remainingBudgetBeforeTurn * 0.5) {
|
|
308
|
-
cumulativeUsd += turnUsd;
|
|
309
|
-
tokensIn += turnTokensIn;
|
|
310
|
-
tokensOut += turnTokensOut;
|
|
311
|
-
turns += 1;
|
|
312
|
-
usageEventSeen = true;
|
|
313
|
-
terminate(`Single turn spend ~$${turnUsd.toFixed(4)} consumed more than 50% of the remaining per-attempt budget ` +
|
|
314
|
-
`($${remainingBudgetBeforeTurn.toFixed(4)} before the turn). Subprocess terminated to prevent a one-turn overshoot.`);
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
tokensIn += turnTokensIn;
|
|
318
|
-
tokensOut += turnTokensOut;
|
|
319
|
-
turns += 1;
|
|
320
|
-
usageEventSeen = true;
|
|
321
|
-
cumulativeUsd += turnUsd;
|
|
322
|
-
checkBudgetExceeded(terminate);
|
|
323
|
-
};
|
|
324
|
-
const ingestLine = (line, terminate) => {
|
|
325
|
-
const trimmed = line.trim();
|
|
326
|
-
if (!trimmed) {
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
let event;
|
|
330
|
-
try {
|
|
331
|
-
event = JSON.parse(trimmed);
|
|
332
|
-
}
|
|
333
|
-
catch {
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
extractUsageFromEvent(event, terminate);
|
|
337
|
-
if (event.type === "result") {
|
|
338
|
-
finalResult = event;
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
return {
|
|
342
|
-
onChunk: (chunk, terminate) => {
|
|
343
|
-
const chunkStr = chunk.toString("utf8");
|
|
344
|
-
totalBytes += chunk.byteLength;
|
|
345
|
-
buffer += chunkStr;
|
|
346
|
-
firstChunkAt ??= Date.now();
|
|
347
|
-
// Token-count ceiling fallback: if we've ingested a lot of bytes but
|
|
348
|
-
// never seen a single usage event, the event format may have changed
|
|
349
|
-
// and the inspector is silently blind. Terminate as a last resort.
|
|
350
|
-
if (!usageEventSeen && capUsd > 0 && totalBytes > bytesCeiling) {
|
|
351
|
-
terminate(`Streaming byte ceiling exceeded (${String(totalBytes)} bytes > ${String(bytesCeiling)} ceiling) ` +
|
|
352
|
-
`without any usage events parsed. The Claude stream-json event format may have changed. ` +
|
|
353
|
-
`Subprocess terminated as a fallback budget guard for cap $${capUsd.toFixed(4)}.`);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
// Time-based fallback: if we've been receiving data for 30+ seconds
|
|
357
|
-
// without a single usage event, estimate spend from byte volume and
|
|
358
|
-
// enforce the cap. This catches cases where Claude's event format
|
|
359
|
-
// changed but bytes are still flowing.
|
|
360
|
-
if (!usageEventSeen &&
|
|
361
|
-
capUsd > 0 &&
|
|
362
|
-
firstChunkAt !== undefined &&
|
|
363
|
-
Date.now() - firstChunkAt > USAGE_BLIND_TIMEOUT_MS &&
|
|
364
|
-
totalBytes > 10_000) {
|
|
365
|
-
const estimatedUsd = totalBytes * blendedCostPerChar;
|
|
366
|
-
if (estimatedUsd > effectiveCapUsd) {
|
|
367
|
-
terminate(`No usage events received after ${String(Math.round((Date.now() - firstChunkAt) / 1000))}s ` +
|
|
368
|
-
`(${String(totalBytes)} bytes). Estimated cost ~$${estimatedUsd.toFixed(4)} exceeds cap ` +
|
|
369
|
-
`$${capUsd.toFixed(4)}. Subprocess terminated to prevent unmetered spend.`);
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
let newlineIndex = buffer.indexOf("\n");
|
|
374
|
-
while (newlineIndex !== -1) {
|
|
375
|
-
const line = buffer.slice(0, newlineIndex);
|
|
376
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
377
|
-
ingestLine(line, terminate);
|
|
378
|
-
newlineIndex = buffer.indexOf("\n");
|
|
379
|
-
}
|
|
380
|
-
},
|
|
381
|
-
snapshot: () => ({ cumulativeUsd, tokensIn, tokensOut, turns, ...(finalResult ? { finalResult } : {}) })
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Parses Claude's `stream-json` output (one JSON object per line) and returns
|
|
386
|
-
* the final `result` event, which carries the same `result`/`usage`/
|
|
387
|
-
* `total_cost_usd` fields as the single-blob `json` format.
|
|
388
|
-
*/
|
|
389
|
-
function parseStreamJsonResult(stdout) {
|
|
390
|
-
let lastResult;
|
|
391
|
-
for (const rawLine of stdout.split(/\r?\n/u)) {
|
|
392
|
-
const line = rawLine.trim();
|
|
393
|
-
if (!line) {
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
try {
|
|
397
|
-
const event = JSON.parse(line);
|
|
398
|
-
if (event.type === "result") {
|
|
399
|
-
lastResult = event;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
catch {
|
|
403
|
-
// Ignore non-JSON / partial lines.
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
return lastResult;
|
|
407
|
-
}
|
|
408
|
-
// ---------------------------------------------------------------------------
|
|
409
|
-
// Structural failure hint detection
|
|
410
|
-
//
|
|
411
|
-
// Provides a classHint to failure-taxonomy based on structural evidence
|
|
412
|
-
// rather than keyword scanning (which suffers from false positives).
|
|
413
|
-
// ---------------------------------------------------------------------------
|
|
414
|
-
function inferStructuralClassHint(agentOutput, verificationSummary, exitCode, objective) {
|
|
415
|
-
// Exit code + stderr "Error:" pattern → syntax error
|
|
416
|
-
if (exitCode !== 0 && /\bError:/i.test(verificationSummary)) {
|
|
417
|
-
return "syntax_error";
|
|
418
|
-
}
|
|
419
|
-
// Agent output grossly longer than objective → scope creep signal
|
|
420
|
-
// (5× ratio heuristic: if the agent wrote 5× more than the objective length, flag it)
|
|
421
|
-
if (agentOutput.length > objective.length * 10 && agentOutput.length > 2000) {
|
|
422
|
-
return "scope_creep";
|
|
423
|
-
}
|
|
424
|
-
// Repeated identical short responses → stalled / hallucination
|
|
425
|
-
const trimmed = agentOutput.trim();
|
|
426
|
-
if (trimmed.length < 100 && trimmed.length > 0) {
|
|
427
|
-
// Very short response on a non-trivial task could be hallucination
|
|
428
|
-
return "hallucination";
|
|
429
|
-
}
|
|
430
|
-
return undefined;
|
|
431
|
-
}
|
|
432
|
-
// ---------------------------------------------------------------------------
|
|
433
|
-
// Generic factory
|
|
434
|
-
// ---------------------------------------------------------------------------
|
|
435
|
-
export function createAgentCliAdapter(options) {
|
|
436
|
-
const workingDirectory = options.workingDirectory ?? process.cwd();
|
|
437
|
-
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
438
|
-
const verifyTimeoutMs = options.verifyTimeoutMs ?? 120_000;
|
|
439
|
-
const adapterId = `agent-cli:${options.adapterIdSuffix ?? options.command}`;
|
|
440
|
-
const supportsJsonOutput = options.supportsJsonOutput === true;
|
|
441
|
-
const supportsUsageSettlement = supportsJsonOutput || options.command === "codex" || options.command === "gemini";
|
|
442
|
-
const adapter = {
|
|
443
|
-
adapterId,
|
|
444
|
-
kind: "agent-cli",
|
|
445
|
-
label: options.label ?? `${options.command} CLI adapter`,
|
|
446
|
-
metadata: {
|
|
447
|
-
providerId: options.command,
|
|
448
|
-
model: options.model ?? options.command,
|
|
449
|
-
transport: "cli",
|
|
450
|
-
capabilities: createAdapterCapabilities({
|
|
451
|
-
preflight: true,
|
|
452
|
-
usageSettlement: supportsUsageSettlement,
|
|
453
|
-
diffArtifacts: true,
|
|
454
|
-
structuredErrors: true,
|
|
455
|
-
cachingSignals: supportsUsageSettlement
|
|
456
|
-
})
|
|
457
|
-
},
|
|
458
|
-
async execute(request) {
|
|
459
|
-
const prompt = buildPrompt(request);
|
|
460
|
-
const estimatedUsage = estimateUsage(prompt, options.model ?? options.command);
|
|
461
|
-
// Preflight: bail if projected cost exceeds remaining budget
|
|
462
|
-
if (request.context.remainingBudgetUsd > 0) {
|
|
463
|
-
const projected = estimatePromptCost(prompt, options.model ?? "");
|
|
464
|
-
if (projected > request.context.remainingBudgetUsd * 0.95) {
|
|
465
|
-
return {
|
|
466
|
-
status: "failed",
|
|
467
|
-
summary: `Preflight: projected cost $${projected.toFixed(4)} exceeds remaining budget $${request.context.remainingBudgetUsd.toFixed(4)}.`,
|
|
468
|
-
usage: normalizeUsage({
|
|
469
|
-
actualUsd: projected,
|
|
470
|
-
estimatedUsd: projected,
|
|
471
|
-
tokensIn: estimatedUsage.tokensIn,
|
|
472
|
-
tokensOut: estimatedUsage.tokensOut,
|
|
473
|
-
provenance: "estimated"
|
|
474
|
-
}),
|
|
475
|
-
verification: { passed: false, summary: "Stopped before execution: budget preflight failed." },
|
|
476
|
-
failure: { message: "budget_preflight_exceeded", classHint: "budget_pressure" }
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
const args = options.argsBuilder(prompt, request);
|
|
481
|
-
const stdinData = options.stdinBuilder?.(prompt);
|
|
482
|
-
// Live cumulative-cost circuit breaker: a single attempt should never be
|
|
483
|
-
// allowed to spend more than the loop has left. `--output-format json`
|
|
484
|
-
// only reports usage once the process exits, so for `stream-json` we
|
|
485
|
-
// watch per-turn usage events as they arrive and kill the subprocess the
|
|
486
|
-
// instant projected spend crosses what remains — bounding the worst case
|
|
487
|
-
// to roughly one turn's overshoot rather than the entire runaway session.
|
|
488
|
-
const streamingUsage = options.streamingUsageCap && request.context.remainingBudgetUsd > 0
|
|
489
|
-
? createStreamingUsageInspector(request.context.remainingBudgetUsd, options.model ?? options.command, estimatedUsage.tokensIn)
|
|
490
|
-
: undefined;
|
|
491
|
-
const agentResult = await runSubprocess(options.command, args, {
|
|
492
|
-
cwd: workingDirectory,
|
|
493
|
-
timeoutMs,
|
|
494
|
-
spawnImpl: options.spawnImpl,
|
|
495
|
-
...(stdinData === undefined ? {} : { stdinData }),
|
|
496
|
-
...(streamingUsage ? { onStdoutChunk: streamingUsage.onChunk } : {})
|
|
497
|
-
});
|
|
498
|
-
if (agentResult.terminationReason) {
|
|
499
|
-
const snapshot = streamingUsage?.snapshot();
|
|
500
|
-
const cumulativeUsd = snapshot?.cumulativeUsd ?? 0;
|
|
501
|
-
return {
|
|
502
|
-
status: "failed",
|
|
503
|
-
summary: `${options.command} subprocess terminated mid-run by the budget circuit breaker. ${agentResult.terminationReason}`,
|
|
504
|
-
usage: normalizeUsage({
|
|
505
|
-
actualUsd: Number(cumulativeUsd.toFixed(6)),
|
|
506
|
-
estimatedUsd: Number(cumulativeUsd.toFixed(6)),
|
|
507
|
-
tokensIn: snapshot?.tokensIn ?? 0,
|
|
508
|
-
tokensOut: snapshot?.tokensOut ?? 0,
|
|
509
|
-
provenance: "estimated"
|
|
510
|
-
}),
|
|
511
|
-
verification: {
|
|
512
|
-
passed: false,
|
|
513
|
-
summary: "Subprocess terminated by the streaming budget circuit breaker before verification could run."
|
|
514
|
-
},
|
|
515
|
-
failure: {
|
|
516
|
-
message: agentResult.terminationReason,
|
|
517
|
-
classHint: "budget_pressure"
|
|
518
|
-
}
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
if (agentResult.timedOut) {
|
|
522
|
-
return {
|
|
523
|
-
status: "failed",
|
|
524
|
-
summary: `${options.command} subprocess timed out before completing.`,
|
|
525
|
-
usage: normalizeUsage({
|
|
526
|
-
actualUsd: estimatedUsage.actualUsd,
|
|
527
|
-
estimatedUsd: estimatedUsage.actualUsd,
|
|
528
|
-
tokensIn: estimatedUsage.tokensIn,
|
|
529
|
-
tokensOut: estimatedUsage.tokensOut,
|
|
530
|
-
provenance: "estimated"
|
|
531
|
-
}),
|
|
532
|
-
verification: { passed: false, summary: "Subprocess timed out." },
|
|
533
|
-
failure: {
|
|
534
|
-
message: `${options.command} did not respond within ${String(timeoutMs)}ms. stalled`
|
|
535
|
-
}
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
if (agentResult.exitCode !== 0 && agentResult.stdout.trim().length === 0) {
|
|
539
|
-
const failureMessage = formatPreVerifierSubprocessFailure(options.command, agentResult.stderr, agentResult.exitCode);
|
|
540
|
-
return {
|
|
541
|
-
status: "failed",
|
|
542
|
-
summary: `${options.command} subprocess exited before verifier execution.`,
|
|
543
|
-
usage: normalizeUsage({
|
|
544
|
-
actualUsd: 0,
|
|
545
|
-
tokensIn: 0,
|
|
546
|
-
tokensOut: 0,
|
|
547
|
-
provenance: "unavailable"
|
|
548
|
-
}),
|
|
549
|
-
verification: { passed: false, summary: `Verifier not run: ${failureMessage}` },
|
|
550
|
-
failure: {
|
|
551
|
-
message: failureMessage
|
|
552
|
-
}
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
// Parse JSON output if the CLI supports it. `stream-json` emits one JSON
|
|
556
|
-
// object per line — the final `result` event carries the same
|
|
557
|
-
// `result`/`usage`/`total_cost_usd` fields as single-blob `json` output.
|
|
558
|
-
let parsed;
|
|
559
|
-
if (supportsJsonOutput) {
|
|
560
|
-
try {
|
|
561
|
-
parsed = options.streamingUsageCap
|
|
562
|
-
? parseStreamJsonResult(agentResult.stdout)
|
|
563
|
-
: JSON.parse(agentResult.stdout);
|
|
564
|
-
}
|
|
565
|
-
catch {
|
|
566
|
-
// Fall through to plain-text handling
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
const codexJsonlResult = !supportsJsonOutput && options.command === "codex"
|
|
570
|
-
? extractCodexJsonlResult(agentResult.stdout, options.model)
|
|
571
|
-
: undefined;
|
|
572
|
-
const geminiJsonResult = !supportsJsonOutput && options.command === "gemini"
|
|
573
|
-
? extractGeminiJsonResult(agentResult.stdout, options.model)
|
|
574
|
-
: undefined;
|
|
575
|
-
const producedStructuredCompletion = parsed?.result !== undefined ||
|
|
576
|
-
codexJsonlResult !== undefined ||
|
|
577
|
-
geminiJsonResult !== undefined;
|
|
578
|
-
if (agentResult.exitCode !== 0 && !producedStructuredCompletion) {
|
|
579
|
-
const failureMessage = formatPreVerifierSubprocessFailure(options.command, agentResult.stderr || agentResult.stdout, agentResult.exitCode);
|
|
580
|
-
return {
|
|
581
|
-
status: "failed",
|
|
582
|
-
summary: `${options.command} subprocess exited before verifier execution.`,
|
|
583
|
-
usage: normalizeUsage({
|
|
584
|
-
actualUsd: 0,
|
|
585
|
-
tokensIn: 0,
|
|
586
|
-
tokensOut: 0,
|
|
587
|
-
provenance: "unavailable"
|
|
588
|
-
}),
|
|
589
|
-
verification: { passed: false, summary: `Verifier not run: ${failureMessage}` },
|
|
590
|
-
failure: {
|
|
591
|
-
message: failureMessage
|
|
592
|
-
}
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
const agentText = codexJsonlResult?.summary ??
|
|
596
|
-
geminiJsonResult?.summary ??
|
|
597
|
-
parsed?.result ??
|
|
598
|
-
agentResult.stdout.trim();
|
|
599
|
-
const summary = truncate(agentText, 2000);
|
|
600
|
-
const usage = parsed?.usage
|
|
601
|
-
? extractUsage(parsed, options.model)
|
|
602
|
-
: codexJsonlResult?.usage ??
|
|
603
|
-
geminiJsonResult?.usage ??
|
|
604
|
-
normalizeUsage({
|
|
605
|
-
actualUsd: estimatedUsage.actualUsd,
|
|
606
|
-
estimatedUsd: estimatedUsage.actualUsd,
|
|
607
|
-
tokensIn: estimatedUsage.tokensIn,
|
|
608
|
-
tokensOut: Math.max(estimatedUsage.tokensOut, Math.ceil(agentText.length / 4)),
|
|
609
|
-
provenance: "estimated",
|
|
610
|
-
providerSettlement: options.command === "codex"
|
|
611
|
-
? {
|
|
612
|
-
providerId: "codex",
|
|
613
|
-
model: options.model ?? "codex",
|
|
614
|
-
transport: "cli",
|
|
615
|
-
source: "estimated_fallback",
|
|
616
|
-
inputTokens: estimatedUsage.tokensIn,
|
|
617
|
-
outputTokens: Math.max(estimatedUsage.tokensOut, Math.ceil(agentText.length / 4)),
|
|
618
|
-
rawUsageAvailable: false,
|
|
619
|
-
settledAt: new Date().toISOString()
|
|
620
|
-
}
|
|
621
|
-
: options.command === "gemini"
|
|
622
|
-
? {
|
|
623
|
-
providerId: "gemini",
|
|
624
|
-
model: options.model ?? "flash",
|
|
625
|
-
transport: "cli",
|
|
626
|
-
source: "estimated_fallback",
|
|
627
|
-
inputTokens: estimatedUsage.tokensIn,
|
|
628
|
-
outputTokens: Math.max(estimatedUsage.tokensOut, Math.ceil(agentText.length / 4)),
|
|
629
|
-
rawUsageAvailable: false,
|
|
630
|
-
settledAt: new Date().toISOString()
|
|
631
|
-
}
|
|
632
|
-
: undefined
|
|
633
|
-
});
|
|
634
|
-
const verificationStack = request.context.verificationStack;
|
|
635
|
-
const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, verificationStack, options.spawnImpl);
|
|
636
|
-
// Check for zero-diff (agent ran but made no file changes)
|
|
637
|
-
const repoRoot = request.context.repoRoot;
|
|
638
|
-
const gitRepoRoot = repoRoot ? resolveGitRepositoryRoot(repoRoot) : undefined;
|
|
639
|
-
let noDiff = false;
|
|
640
|
-
if (gitRepoRoot) {
|
|
641
|
-
noDiff = await checkNoDiff(gitRepoRoot, options.spawnImpl);
|
|
642
|
-
}
|
|
643
|
-
// Extract structured errors from stderr/stdout for better failure context
|
|
644
|
-
const structuredErrors = normalizeStructuredErrors(extractStructuredErrors(agentResult.stderr, agentResult.stdout));
|
|
645
|
-
const executionArtifacts = gitRepoRoot
|
|
646
|
-
? await readGitExecutionArtifacts(gitRepoRoot, 5000, options.spawnImpl)
|
|
647
|
-
: undefined;
|
|
648
|
-
// Scope contract enforcement: check touched files against allowedPaths/deniedPaths
|
|
649
|
-
let scopeViolations = [];
|
|
650
|
-
const scopeCtx = request.context;
|
|
651
|
-
if (gitRepoRoot && (scopeCtx.allowedPaths?.length || scopeCtx.deniedPaths?.length)) {
|
|
652
|
-
const diffResult = await runSubprocess("git", ["diff", "--name-only", "HEAD"], {
|
|
653
|
-
cwd: gitRepoRoot,
|
|
654
|
-
timeoutMs: 5000,
|
|
655
|
-
spawnImpl: options.spawnImpl
|
|
656
|
-
});
|
|
657
|
-
if (diffResult.exitCode === 0 && diffResult.stdout.trim()) {
|
|
658
|
-
const touchedFiles = diffResult.stdout.trim().split("\n").filter(Boolean);
|
|
659
|
-
const allowed = scopeCtx.allowedPaths ?? [];
|
|
660
|
-
const denied = scopeCtx.deniedPaths ?? [];
|
|
661
|
-
for (const file of touchedFiles) {
|
|
662
|
-
// Check denied patterns (simple glob-like: prefix or exact)
|
|
663
|
-
if (denied.some((d) => file === d || file.startsWith(d.replace(/\*+$/, "")))) {
|
|
664
|
-
scopeViolations.push(file);
|
|
665
|
-
continue;
|
|
666
|
-
}
|
|
667
|
-
// If allowedPaths specified, file must match at least one
|
|
668
|
-
if (allowed.length > 0 && !allowed.some((a) => file === a || file.startsWith(a.replace(/\*+$/, "")))) {
|
|
669
|
-
scopeViolations.push(file);
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
// Derive structural classHint from evidence, not keyword scanning
|
|
675
|
-
const structuralHint = inferStructuralClassHint(agentText, verification.summary, agentResult.exitCode, request.context.objective);
|
|
676
|
-
if (verification.passed) {
|
|
677
|
-
return {
|
|
678
|
-
status: "completed",
|
|
679
|
-
summary,
|
|
680
|
-
usage,
|
|
681
|
-
verification: { passed: true, summary: verification.summary },
|
|
682
|
-
...(executionArtifacts
|
|
683
|
-
? {
|
|
684
|
-
execution: {
|
|
685
|
-
...executionArtifacts,
|
|
686
|
-
...(structuredErrors.length > 0 ? { structuredErrors } : {})
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
: structuredErrors.length > 0
|
|
690
|
-
? { execution: { structuredErrors } }
|
|
691
|
-
: {})
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
const classHint = scopeViolations.length > 0
|
|
695
|
-
? "scope_creep"
|
|
696
|
-
: noDiff
|
|
697
|
-
? "no_progress"
|
|
698
|
-
: (structuralHint ?? undefined);
|
|
699
|
-
const errorBlock = structuredErrors.length > 0
|
|
700
|
-
? `\nSTRUCTURED ERRORS:\n${structuredErrors.map(e => ` ${e.file}${e.line !== undefined ? `:${String(e.line)}` : ""} — ${e.code ? `${e.code}: ` : ""}${e.message}`).join("\n")}`
|
|
701
|
-
: "";
|
|
702
|
-
const scopeBlock = scopeViolations.length > 0
|
|
703
|
-
? `\n Scope violations: ${scopeViolations.join(", ")}`
|
|
704
|
-
: "";
|
|
705
|
-
// Write PROGRESS.md to help the next attempt re-anchor on the original objective
|
|
706
|
-
if (repoRoot) {
|
|
707
|
-
try {
|
|
708
|
-
const { writeFile, readFile, appendFile: appendFs } = await import("node:fs/promises");
|
|
709
|
-
const progressPath = `${repoRoot}/PROGRESS.md`;
|
|
710
|
-
const timestamp = new Date().toISOString();
|
|
711
|
-
const entry = `\n## Attempt ${String(request.previousAttempts.length + 1)} — ${timestamp}\n- Failure class: ${classHint ?? "verification_failure"}\n- Verification: ${verification.summary}${errorBlock}${scopeBlock}\n`;
|
|
712
|
-
let content;
|
|
713
|
-
try {
|
|
714
|
-
content = await readFile(progressPath, "utf8");
|
|
715
|
-
}
|
|
716
|
-
catch {
|
|
717
|
-
content = `# Martin Loop Progress\n\n**Original objective:** ${request.context.objective}\n`;
|
|
718
|
-
}
|
|
719
|
-
await writeFile(progressPath, content + entry, "utf8");
|
|
720
|
-
}
|
|
721
|
-
catch {
|
|
722
|
-
// Non-fatal
|
|
723
|
-
}
|
|
724
|
-
// Reset tracked files to HEAD so next attempt starts from clean state
|
|
725
|
-
try {
|
|
726
|
-
if (gitRepoRoot) {
|
|
727
|
-
await runSubprocess("git", ["restore", "--staged", "--worktree", "."], {
|
|
728
|
-
cwd: gitRepoRoot,
|
|
729
|
-
timeoutMs: 5000
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
catch {
|
|
734
|
-
// Non-fatal
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
return {
|
|
738
|
-
status: "failed",
|
|
739
|
-
summary: (structuredErrors.length > 0 || scopeViolations.length > 0)
|
|
740
|
-
? `${summary}${errorBlock}${scopeViolations.length > 0 ? `\nScope violations: ${scopeViolations.join(", ")}` : ""}`
|
|
741
|
-
: summary,
|
|
742
|
-
usage,
|
|
743
|
-
verification: { passed: false, summary: verification.summary },
|
|
744
|
-
...(executionArtifacts
|
|
745
|
-
? {
|
|
746
|
-
execution: {
|
|
747
|
-
...executionArtifacts,
|
|
748
|
-
...(structuredErrors.length > 0 ? { structuredErrors } : {})
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
: structuredErrors.length > 0
|
|
752
|
-
? { execution: { structuredErrors } }
|
|
753
|
-
: {}),
|
|
754
|
-
failure: {
|
|
755
|
-
message: verification.summary,
|
|
756
|
-
...(classHint ? { classHint } : {})
|
|
757
|
-
}
|
|
758
|
-
};
|
|
759
|
-
},
|
|
760
|
-
/**
|
|
761
|
-
* Return a new adapter instance with a different model.
|
|
762
|
-
* Used by run-martin.ts when a change_model intervention fires.
|
|
763
|
-
* Model escalation order: haiku → sonnet → opus (cheapest-first, escalate on repeated failure).
|
|
764
|
-
*/
|
|
765
|
-
withModel(newModel) {
|
|
766
|
-
return createAgentCliAdapter({
|
|
767
|
-
...options,
|
|
768
|
-
model: newModel,
|
|
769
|
-
adapterIdSuffix: `${options.adapterIdSuffix ?? options.command}:${newModel}`
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
};
|
|
773
|
-
return adapter;
|
|
774
|
-
}
|
|
775
|
-
// ---------------------------------------------------------------------------
|
|
776
|
-
// Pre-configured: Claude CLI
|
|
777
|
-
// ---------------------------------------------------------------------------
|
|
778
|
-
/**
|
|
779
|
-
* Spawns `claude --output-format stream-json --verbose --print "<prompt>" [extraArgs]`.
|
|
780
|
-
*
|
|
781
|
-
* `stream-json` emits one JSON event per line — including per-turn usage on
|
|
782
|
-
* each `assistant` message and a final `result` event carrying the same
|
|
783
|
-
* `result`/`usage`/`total_cost_usd` fields as single-blob `json` output — so
|
|
784
|
-
* MartinLoop can both (a) recover real token usage/cost as before, and
|
|
785
|
-
* (b) watch cumulative spend live and self-terminate the subprocess the
|
|
786
|
-
* moment it crosses the remaining per-attempt budget (see
|
|
787
|
-
* `streamingUsageCap` / `createStreamingUsageInspector`), instead of only
|
|
788
|
-
* discovering an overspend after the whole process has already exited.
|
|
789
|
-
*
|
|
790
|
-
* Requires the Claude Code CLI to be installed and authenticated:
|
|
791
|
-
* https://docs.anthropic.com/claude-code
|
|
792
|
-
*/
|
|
793
|
-
export function createClaudeCliAdapter(options = {}) {
|
|
794
|
-
const modelArgs = options.model ? ["--model", options.model] : [];
|
|
795
|
-
const extraArgs = options.extraArgs ?? [];
|
|
796
|
-
return createAgentCliAdapter({
|
|
797
|
-
command: "claude",
|
|
798
|
-
adapterIdSuffix: "claude",
|
|
799
|
-
model: options.model ?? "claude-sonnet-4-6",
|
|
800
|
-
label: options.label ?? "Claude CLI adapter",
|
|
801
|
-
workingDirectory: options.workingDirectory,
|
|
802
|
-
timeoutMs: options.timeoutMs,
|
|
803
|
-
verifyTimeoutMs: options.verifyTimeoutMs,
|
|
804
|
-
supportsJsonOutput: true,
|
|
805
|
-
streamingUsageCap: true,
|
|
806
|
-
spawnImpl: options.spawnImpl,
|
|
807
|
-
argsBuilder: (_prompt, request) => [
|
|
808
|
-
"--output-format",
|
|
809
|
-
"stream-json",
|
|
810
|
-
"--verbose",
|
|
811
|
-
"--print",
|
|
812
|
-
"--dangerously-skip-permissions",
|
|
813
|
-
// Prevent the child Claude process from loading the parent's MCP servers.
|
|
814
|
-
// Without this, a MartinLoop MCP server in the user's config gets spawned
|
|
815
|
-
// inside the governed subprocess, causing conflicts and "MCP server being
|
|
816
|
-
// overwritten" errors.
|
|
817
|
-
"--strict-mcp-config",
|
|
818
|
-
...(request.context.remainingTokens > 0 ? ["--max-tokens", String(request.context.remainingTokens)] : []),
|
|
819
|
-
...modelArgs,
|
|
820
|
-
...extraArgs
|
|
821
|
-
],
|
|
822
|
-
stdinBuilder: (prompt) => prompt
|
|
823
|
-
});
|
|
824
|
-
}
|
|
825
|
-
// ---------------------------------------------------------------------------
|
|
826
|
-
// Pre-configured: OpenAI Codex CLI
|
|
827
|
-
// ---------------------------------------------------------------------------
|
|
828
|
-
/**
|
|
829
|
-
* Spawns `codex exec --cd <workspace> --sandbox <mode> [--model <model>] [extraArgs] -`.
|
|
830
|
-
*
|
|
831
|
-
* The prompt is delivered via stdin so Windows shell quoting cannot truncate or
|
|
832
|
-
* reinterpret long MartinLoop prompts that contain paths, deny rules, or budget
|
|
833
|
-
* context.
|
|
834
|
-
*
|
|
835
|
-
* Requires the Codex CLI to be installed and authenticated:
|
|
836
|
-
* npm install -g @openai/codex
|
|
837
|
-
*/
|
|
838
|
-
export function createCodexCliAdapter(options = {}) {
|
|
839
|
-
const extraArgs = options.extraArgs ?? [];
|
|
840
|
-
const sandbox = options.sandbox ?? "workspace-write";
|
|
841
|
-
const workingDirectory = options.workingDirectory ?? process.cwd();
|
|
842
|
-
const command = options.command ?? "codex";
|
|
843
|
-
const launchModel = options.model ?? DEFAULT_CODEX_CHATGPT_MODEL;
|
|
844
|
-
return createAgentCliAdapter({
|
|
845
|
-
command,
|
|
846
|
-
adapterIdSuffix: "codex",
|
|
847
|
-
model: options.model ?? "codex",
|
|
848
|
-
label: options.label ?? "Codex CLI adapter",
|
|
849
|
-
workingDirectory,
|
|
850
|
-
timeoutMs: options.timeoutMs,
|
|
851
|
-
verifyTimeoutMs: options.verifyTimeoutMs,
|
|
852
|
-
supportsJsonOutput: false,
|
|
853
|
-
spawnImpl: options.spawnImpl,
|
|
854
|
-
argsBuilder: () => buildCodexExecArgs({
|
|
855
|
-
workingDirectory,
|
|
856
|
-
sandbox,
|
|
857
|
-
model: launchModel,
|
|
858
|
-
extraArgs,
|
|
859
|
-
mode: "prompt"
|
|
860
|
-
}),
|
|
861
|
-
stdinBuilder: (prompt) => prompt
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
// ---------------------------------------------------------------------------
|
|
865
|
-
// Pre-configured: Gemini CLI
|
|
866
|
-
// ---------------------------------------------------------------------------
|
|
867
|
-
/**
|
|
868
|
-
* Spawns `gemini --model <model> --prompt "" --approval-mode <mode> --output-format json [...]`.
|
|
869
|
-
*
|
|
870
|
-
* The prompt is delivered via stdin while forcing headless mode with `--prompt ""`,
|
|
871
|
-
* which keeps large MartinLoop prompts off the command line on Windows.
|
|
872
|
-
*
|
|
873
|
-
* Requires the Gemini CLI to be installed and authenticated:
|
|
874
|
-
* npm install -g @google/gemini-cli
|
|
875
|
-
*/
|
|
876
|
-
export function createGeminiCliAdapter(options = {}) {
|
|
877
|
-
const model = options.model ?? "flash";
|
|
878
|
-
const approvalMode = options.approvalMode ?? "yolo";
|
|
879
|
-
const extraArgs = options.extraArgs ?? [];
|
|
880
|
-
return createAgentCliAdapter({
|
|
881
|
-
command: "gemini",
|
|
882
|
-
adapterIdSuffix: "gemini",
|
|
883
|
-
model,
|
|
884
|
-
label: options.label ?? "Gemini CLI adapter",
|
|
885
|
-
workingDirectory: options.workingDirectory,
|
|
886
|
-
timeoutMs: options.timeoutMs,
|
|
887
|
-
verifyTimeoutMs: options.verifyTimeoutMs,
|
|
888
|
-
supportsJsonOutput: false,
|
|
889
|
-
spawnImpl: options.spawnImpl,
|
|
890
|
-
argsBuilder: () => [
|
|
891
|
-
"--model",
|
|
892
|
-
model,
|
|
893
|
-
"--prompt",
|
|
894
|
-
"",
|
|
895
|
-
"--approval-mode",
|
|
896
|
-
approvalMode,
|
|
897
|
-
...(options.sandbox ? ["--sandbox"] : []),
|
|
898
|
-
"--output-format",
|
|
899
|
-
"json",
|
|
900
|
-
...extraArgs
|
|
901
|
-
],
|
|
902
|
-
stdinBuilder: (prompt) => prompt
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
// ---------------------------------------------------------------------------
|
|
906
|
-
// Prompt builder
|
|
907
|
-
//
|
|
908
|
-
// Implements Qralph-style context isolation:
|
|
909
|
-
// - Each attempt gets a fresh, distilled prompt — NOT the full conversation history
|
|
910
|
-
// - Prior attempts are summarized (last 3 max, via distillContext in core)
|
|
911
|
-
// - Interventions translate into concrete prompt directives
|
|
912
|
-
// - Context budget info surfaces remaining runway to the agent
|
|
913
|
-
// ---------------------------------------------------------------------------
|
|
914
|
-
function buildPrompt(request) {
|
|
915
|
-
const lines = [];
|
|
916
|
-
const mutationMode = request.context.mutationMode ?? "edit";
|
|
917
|
-
lines.push("You are running in autonomous agentic mode.");
|
|
918
|
-
if (mutationMode === "verify_only") {
|
|
919
|
-
lines.push("DO NOT EDIT FILES. Run the verifier only and report whether it passes.");
|
|
920
|
-
lines.push("Do not ask for confirmation. Do not ask clarifying questions.");
|
|
921
|
-
}
|
|
922
|
-
else {
|
|
923
|
-
lines.push("MAKE ALL REQUIRED FILE EDITS NOW. Do not ask for confirmation. Do not ask clarifying questions.");
|
|
924
|
-
lines.push("Do not explain what you found without also making the changes. Edit the files and complete the task.");
|
|
925
|
-
}
|
|
926
|
-
lines.push("");
|
|
927
|
-
lines.push("If PROGRESS.md exists in your working directory, read it first for context from prior attempts.");
|
|
928
|
-
lines.push("If it does not exist, proceed with the objective below.");
|
|
929
|
-
lines.push("");
|
|
930
|
-
lines.push(mutationMode === "verify_only"
|
|
931
|
-
? "Complete the following verification-only task without making file changes."
|
|
932
|
-
: "Complete the following coding task. Make all necessary file changes.");
|
|
933
|
-
lines.push("When you are done, the verification commands listed below must pass.");
|
|
934
|
-
lines.push("");
|
|
935
|
-
lines.push("OBJECTIVE:");
|
|
936
|
-
lines.push(sanitizeForPrompt(request.context.objective));
|
|
937
|
-
lines.push("");
|
|
938
|
-
// Acceptance criteria (from task contract)
|
|
939
|
-
if (request.context.acceptanceCriteria?.length) {
|
|
940
|
-
lines.push("ACCEPTANCE CRITERIA (all must be satisfied):");
|
|
941
|
-
for (const criterion of request.context.acceptanceCriteria ?? []) {
|
|
942
|
-
lines.push(` - ${sanitizeForPrompt(criterion)}`);
|
|
943
|
-
}
|
|
944
|
-
lines.push("");
|
|
945
|
-
}
|
|
946
|
-
// Scope contract
|
|
947
|
-
const ctx = request.context;
|
|
948
|
-
if (ctx.allowedPaths?.length || ctx.deniedPaths?.length) {
|
|
949
|
-
lines.push("SCOPE CONTRACT (immutable — do not expand):");
|
|
950
|
-
if (ctx.allowedPaths?.length) {
|
|
951
|
-
lines.push(` Allowed paths: ${ctx.allowedPaths.join(", ")}`);
|
|
952
|
-
}
|
|
953
|
-
if (ctx.deniedPaths?.length) {
|
|
954
|
-
lines.push(` Forbidden paths: ${ctx.deniedPaths.join(", ")}`);
|
|
955
|
-
}
|
|
956
|
-
lines.push("");
|
|
957
|
-
}
|
|
958
|
-
if (request.context.verificationPlan.length > 0) {
|
|
959
|
-
lines.push("VERIFICATION (all commands must exit with code 0):");
|
|
960
|
-
for (const cmd of request.context.verificationPlan) {
|
|
961
|
-
lines.push(` ${cmd}`);
|
|
962
|
-
}
|
|
963
|
-
lines.push("");
|
|
964
|
-
}
|
|
965
|
-
const attemptNumber = request.previousAttempts.length + 1;
|
|
966
|
-
lines.push("CONSTRAINTS:");
|
|
967
|
-
lines.push(` Attempt ${String(attemptNumber)}`);
|
|
968
|
-
lines.push(` Remaining budget: $${String(request.context.remainingBudgetUsd)} USD`);
|
|
969
|
-
lines.push(` Remaining iterations: ${String(request.context.remainingIterations)}`);
|
|
970
|
-
lines.push(mutationMode === "verify_only"
|
|
971
|
-
? " Do not modify files; only run verification."
|
|
972
|
-
: " Do not expand scope beyond what is needed to pass verification.");
|
|
973
|
-
lines.push("");
|
|
974
|
-
if (request.previousAttempts.length > 0) {
|
|
975
|
-
lines.push("PRIOR FAILED ATTEMPTS (learn from these — do not repeat the same mistakes):");
|
|
976
|
-
for (const attempt of request.previousAttempts) {
|
|
977
|
-
const failurePart = attempt.failureClass ? ` [${attempt.failureClass}]` : "";
|
|
978
|
-
const interventionPart = attempt.intervention ? ` -> intervention: ${attempt.intervention}` : "";
|
|
979
|
-
const diagPart = attempt.diagnosticHint ? `\n DIAGNOSIS: ${sanitizeForPrompt(attempt.diagnosticHint)}` : "";
|
|
980
|
-
lines.push(` Attempt ${String(attempt.index)}${failurePart}: ${sanitizeForPrompt(attempt.summary ?? "")}${interventionPart}${diagPart}`);
|
|
981
|
-
}
|
|
982
|
-
lines.push("");
|
|
983
|
-
}
|
|
984
|
-
// Intervention directives
|
|
985
|
-
const lastIntervention = request.previousAttempts.at(-1)?.intervention;
|
|
986
|
-
if (lastIntervention === "tighten_task") {
|
|
987
|
-
lines.push("SCOPE LOCK (prior attempt expanded scope — do not repeat):");
|
|
988
|
-
lines.push(" Only touch files directly required to make the verification commands pass.");
|
|
989
|
-
lines.push(" Do NOT add features, refactor unrelated code, or modify files outside the objective.");
|
|
990
|
-
lines.push("");
|
|
991
|
-
}
|
|
992
|
-
if (lastIntervention === "compress_context") {
|
|
993
|
-
lines.push("BREVITY MODE (prior attempt was too large — be concise):");
|
|
994
|
-
lines.push(" Keep changes minimal. Only output what changed and why.");
|
|
995
|
-
lines.push("");
|
|
996
|
-
}
|
|
997
|
-
if (lastIntervention === "run_verifier") {
|
|
998
|
-
lines.push("VERIFICATION FOCUS (prior attempt failed verification):");
|
|
999
|
-
lines.push(" Before finalizing, mentally simulate running each verification command.");
|
|
1000
|
-
lines.push(" Only mark yourself done when confident all commands will pass.");
|
|
1001
|
-
lines.push("");
|
|
1002
|
-
}
|
|
1003
|
-
if (lastIntervention === "change_model") {
|
|
1004
|
-
lines.push("FRESH APPROACH (previous attempts did not converge):");
|
|
1005
|
-
lines.push(" Do not repeat prior reasoning. Start from first principles on the objective.");
|
|
1006
|
-
lines.push("");
|
|
1007
|
-
}
|
|
1008
|
-
lines.push(`FOCUS: ${sanitizeForPrompt(request.context.focus)}`);
|
|
1009
|
-
return lines.join("\n");
|
|
1010
|
-
}
|
|
1011
|
-
// ---------------------------------------------------------------------------
|
|
1012
|
-
// Utilities
|
|
1013
|
-
// ---------------------------------------------------------------------------
|
|
1014
|
-
function truncate(text, maxLength) {
|
|
1015
|
-
if (text.length <= maxLength) {
|
|
1016
|
-
return text;
|
|
1017
|
-
}
|
|
1018
|
-
return `...${text.slice(-(maxLength - 3))}`;
|
|
1019
|
-
}
|
|
1020
|
-
function formatPreVerifierSubprocessFailure(command, stderr, exitCode) {
|
|
1021
|
-
const detail = stderr.trim() || `Exit code ${String(exitCode)}`;
|
|
1022
|
-
const lowerDetail = detail.toLowerCase();
|
|
1023
|
-
const codexLaunchBlocked = command === "codex" &&
|
|
1024
|
-
/\b(full-auto|sandbox|approval|permission|trusted|safety|unexpected argument)\b/u.test(lowerDetail);
|
|
1025
|
-
if (codexLaunchBlocked) {
|
|
1026
|
-
return `Codex CLI failed before patch completion, likely due to its launch/sandbox configuration. MartinLoop invokes Codex through "codex exec --sandbox workspace-write"; verify Codex CLI auth and configuration if this persists. ${detail}. environment_mismatch`;
|
|
1027
|
-
}
|
|
1028
|
-
return `${detail}. environment_mismatch`;
|
|
1029
|
-
}
|
|
1030
|
-
const INJECTION_PATTERNS = [
|
|
1031
|
-
/\[INST\]/gi,
|
|
1032
|
-
/<\/?system>/gi,
|
|
1033
|
-
/^(IGNORE|DISREGARD|FORGET|NEW INSTRUCTION|OVERRIDE)\b.+$/gim,
|
|
1034
|
-
/<\/?s>/gi
|
|
1035
|
-
];
|
|
1036
|
-
function sanitizeForPrompt(input) {
|
|
1037
|
-
let out = input;
|
|
1038
|
-
for (const pattern of INJECTION_PATTERNS) {
|
|
1039
|
-
out = out.replace(pattern, "[FILTERED]");
|
|
1040
|
-
}
|
|
1041
|
-
return redactSecretsForPrompt(out);
|
|
1042
|
-
}
|
|
1043
|
-
function estimatePromptCost(promptText, model) {
|
|
1044
|
-
const inputTokens = Math.ceil(promptText.length / 3.5);
|
|
1045
|
-
const outputTokens = 2000;
|
|
1046
|
-
const pricing = MODEL_PRICING[model] ?? { inputPer1K: BLENDED_INPUT_COST_PER_1K, outputPer1K: BLENDED_OUTPUT_COST_PER_1K };
|
|
1047
|
-
return (inputTokens / 1000) * pricing.inputPer1K + (outputTokens / 1000) * pricing.outputPer1K;
|
|
1048
|
-
}
|
|
1049
|
-
function estimateUsage(promptText, model) {
|
|
1050
|
-
const inputTokens = Math.ceil(promptText.length / 3.5);
|
|
1051
|
-
const outputTokens = 2_000;
|
|
1052
|
-
return normalizeUsage({
|
|
1053
|
-
actualUsd: estimatePromptCost(promptText, model),
|
|
1054
|
-
estimatedUsd: estimatePromptCost(promptText, model),
|
|
1055
|
-
tokensIn: inputTokens,
|
|
1056
|
-
tokensOut: outputTokens,
|
|
1057
|
-
provenance: "estimated"
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
function redactSecretsForPrompt(input) {
|
|
1061
|
-
return input
|
|
1062
|
-
.replace(/\bOPENAI_API_KEY\s*=\s*[^\s"'`]+/giu, "OPENAI_API_KEY=[REDACTED_SECRET]")
|
|
1063
|
-
.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/gu, "[REDACTED_SECRET]")
|
|
1064
|
-
.replace(/\bghp_[A-Za-z0-9_]{16,}\b/gu, "[REDACTED_SECRET]")
|
|
1065
|
-
.replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/gu, "[REDACTED_SECRET]")
|
|
1066
|
-
.replace(/\b(?:gho|ghu|ghs|ghr)_[A-Za-z0-9_]{16,}\b/gu, "[REDACTED_SECRET]")
|
|
1067
|
-
.replace(/\bAKIA[0-9A-Z]{16}\b/gu, "[REDACTED_SECRET]")
|
|
1068
|
-
.replace(/\b(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[:=]\s*[^\s"'`]+/giu, "AWS_SECRET_ACCESS_KEY=[REDACTED_SECRET]")
|
|
1069
|
-
.replace(/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/giu, "[REDACTED_SECRET]")
|
|
1070
|
-
.replace(/\bAIza[0-9A-Za-z_-]{30,}\b/gu, "[REDACTED_SECRET]")
|
|
1071
|
-
.replace(/-----BEGIN(?:\s+[A-Z0-9]+)*\s+PRIVATE KEY-----[\s\S]*?-----END(?:\s+[A-Z0-9]+)*\s+PRIVATE KEY-----/gu, "[REDACTED_SECRET]")
|
|
1072
|
-
.replace(/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/gu, "[REDACTED_SECRET]")
|
|
1073
|
-
.replace(/\b(?:api[_-]?key|secret|password|token)\s*[:=]\s*["']?[A-Za-z0-9_\-/+=]{8,}["']?/giu, "[REDACTED_SECRET]")
|
|
1074
|
-
.replace(/\B\.env(?!\.example\b)(?:\.[A-Za-z0-9._-]+)?\b/giu, "[REDACTED_PATH]");
|
|
1075
|
-
}
|
|
1076
|
-
function extractStructuredErrors(stderr, stdout) {
|
|
1077
|
-
const errors = [];
|
|
1078
|
-
const combined = `${stderr}\n${stdout}`;
|
|
1079
|
-
// TypeScript: file.ts(42,5): error TS2322: message
|
|
1080
|
-
for (const m of combined.matchAll(/^(.+\.tsx?)\((\d+),(\d+)\): error (TS\d+): (.+)$/gm)) {
|
|
1081
|
-
errors.push({ file: m[1] ?? "", line: Number(m[2]), col: Number(m[3]), code: m[4], message: m[5] ?? "" });
|
|
1082
|
-
}
|
|
1083
|
-
// ESLint / tsc path-style: ./src/foo.ts:42:5: error message
|
|
1084
|
-
for (const m of combined.matchAll(/^(\.?\/[\w./-]+\.tsx?):(\d+):(\d+):\s+error\s+(.+)$/gm)) {
|
|
1085
|
-
errors.push({ file: m[1] ?? "", line: Number(m[2]), col: Number(m[3]), message: m[4] ?? "" });
|
|
1086
|
-
}
|
|
1087
|
-
// Jest FAIL line: FAIL src/foo.test.ts
|
|
1088
|
-
for (const m of combined.matchAll(/^FAIL\s+([\w./-]+\.test\.[jt]sx?)$/gm)) {
|
|
1089
|
-
errors.push({ file: m[1] ?? "", message: "Test suite failed" });
|
|
1090
|
-
}
|
|
1091
|
-
return errors.slice(0, 10); // cap at 10 to avoid bloating prompts
|
|
1092
|
-
}
|
|
1093
|
-
async function checkNoDiff(repoRoot, spawnImpl) {
|
|
1094
|
-
const result = await runSubprocess("git", ["diff", "--name-only", "HEAD"], {
|
|
1095
|
-
cwd: repoRoot,
|
|
1096
|
-
timeoutMs: 5000,
|
|
1097
|
-
spawnImpl
|
|
1098
|
-
});
|
|
1099
|
-
return result.exitCode === 0 && result.stdout.trim().length === 0;
|
|
1100
|
-
}
|