@martinloop/mcp 0.3.8 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +148 -105
  2. package/dist/resources.js +148 -148
  3. package/dist/server.js +0 -0
  4. package/dist/tools/preflight.d.ts +0 -2
  5. package/dist/tools/preflight.js +2 -42
  6. package/dist/vendor/adapters/openai-compatible.js +6 -6
  7. package/dist/vendor/contracts/index.d.ts +0 -2
  8. package/dist/vendor/contracts/index.js +0 -2
  9. package/dist/vendor/core/index.d.ts +0 -2
  10. package/dist/vendor/core/index.js +0 -2
  11. package/package.json +86 -83
  12. package/server.json +21 -21
  13. package/LICENSE +0 -216
  14. package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +0 -146
  15. package/dist/vendor/adapters/adapters/src/claude-cli.js +0 -1100
  16. package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +0 -103
  17. package/dist/vendor/adapters/adapters/src/cli-bridge.js +0 -569
  18. package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +0 -78
  19. package/dist/vendor/adapters/adapters/src/codex-launcher.js +0 -663
  20. package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +0 -10
  21. package/dist/vendor/adapters/adapters/src/direct-provider.js +0 -40
  22. package/dist/vendor/adapters/adapters/src/index.d.ts +0 -8
  23. package/dist/vendor/adapters/adapters/src/index.js +0 -8
  24. package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +0 -62
  25. package/dist/vendor/adapters/adapters/src/openai-compatible.js +0 -306
  26. package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +0 -17
  27. package/dist/vendor/adapters/adapters/src/runtime-support.js +0 -59
  28. package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +0 -8
  29. package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +0 -40
  30. package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +0 -8
  31. package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +0 -12
  32. package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +0 -9
  33. package/dist/vendor/adapters/adapters/src/verifier-only.js +0 -63
  34. package/dist/vendor/adapters/contracts/src/context-graph.d.ts +0 -48
  35. package/dist/vendor/adapters/contracts/src/context-graph.js +0 -26
  36. package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +0 -78
  37. package/dist/vendor/adapters/contracts/src/execution-policy.js +0 -43
  38. package/dist/vendor/adapters/contracts/src/governance.d.ts +0 -24
  39. package/dist/vendor/adapters/contracts/src/governance.js +0 -11
  40. package/dist/vendor/adapters/contracts/src/identity.d.ts +0 -29
  41. package/dist/vendor/adapters/contracts/src/identity.js +0 -30
  42. package/dist/vendor/adapters/contracts/src/index.d.ts +0 -412
  43. package/dist/vendor/adapters/contracts/src/index.js +0 -225
  44. package/dist/vendor/adapters/contracts/src/operator.d.ts +0 -19
  45. package/dist/vendor/adapters/contracts/src/operator.js +0 -11
  46. package/dist/vendor/adapters/contracts/src/trajectory.d.ts +0 -26
  47. package/dist/vendor/adapters/contracts/src/trajectory.js +0 -27
  48. package/dist/vendor/adapters/core/src/compiler.d.ts +0 -52
  49. package/dist/vendor/adapters/core/src/compiler.js +0 -52
  50. package/dist/vendor/adapters/core/src/context-graph.d.ts +0 -3
  51. package/dist/vendor/adapters/core/src/context-graph.js +0 -371
  52. package/dist/vendor/adapters/core/src/context-integrity.d.ts +0 -26
  53. package/dist/vendor/adapters/core/src/context-integrity.js +0 -80
  54. package/dist/vendor/adapters/core/src/grounding.d.ts +0 -38
  55. package/dist/vendor/adapters/core/src/grounding.js +0 -273
  56. package/dist/vendor/adapters/core/src/identity.d.ts +0 -20
  57. package/dist/vendor/adapters/core/src/identity.js +0 -86
  58. package/dist/vendor/adapters/core/src/index.d.ts +0 -186
  59. package/dist/vendor/adapters/core/src/index.js +0 -1495
  60. package/dist/vendor/adapters/core/src/leash.d.ts +0 -48
  61. package/dist/vendor/adapters/core/src/leash.js +0 -497
  62. package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +0 -18
  63. package/dist/vendor/adapters/core/src/persistence/compiler.js +0 -34
  64. package/dist/vendor/adapters/core/src/persistence/index.d.ts +0 -10
  65. package/dist/vendor/adapters/core/src/persistence/index.js +0 -5
  66. package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +0 -39
  67. package/dist/vendor/adapters/core/src/persistence/integrity.js +0 -252
  68. package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +0 -23
  69. package/dist/vendor/adapters/core/src/persistence/ledger.js +0 -9
  70. package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +0 -63
  71. package/dist/vendor/adapters/core/src/persistence/memory-store.js +0 -115
  72. package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +0 -66
  73. package/dist/vendor/adapters/core/src/persistence/runs-reader.js +0 -140
  74. package/dist/vendor/adapters/core/src/persistence/store.d.ts +0 -89
  75. package/dist/vendor/adapters/core/src/persistence/store.js +0 -125
  76. package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +0 -85
  77. package/dist/vendor/adapters/core/src/persistence/trace-store.js +0 -125
  78. package/dist/vendor/adapters/core/src/policy-compiler.d.ts +0 -2
  79. package/dist/vendor/adapters/core/src/policy-compiler.js +0 -195
  80. package/dist/vendor/adapters/core/src/policy.d.ts +0 -156
  81. package/dist/vendor/adapters/core/src/policy.js +0 -704
  82. package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +0 -53
  83. package/dist/vendor/adapters/core/src/repo-analyzer.js +0 -263
  84. package/dist/vendor/adapters/core/src/rollback.d.ts +0 -15
  85. package/dist/vendor/adapters/core/src/rollback.js +0 -253
  86. package/dist/vendor/adapters/core/src/routing.d.ts +0 -82
  87. package/dist/vendor/adapters/core/src/routing.js +0 -336
  88. package/dist/vendor/adapters/core/src/test-integrity.d.ts +0 -64
  89. package/dist/vendor/adapters/core/src/test-integrity.js +0 -197
  90. package/dist/vendor/adapters/core/src/trajectory.d.ts +0 -18
  91. package/dist/vendor/adapters/core/src/trajectory.js +0 -295
  92. package/dist/vendor/contracts/delivery.d.ts +0 -47
  93. package/dist/vendor/contracts/delivery.js +0 -18
  94. package/dist/vendor/core/delivery/index.d.ts +0 -6
  95. package/dist/vendor/core/delivery/index.js +0 -4
  96. package/dist/vendor/core/delivery/message-client.d.ts +0 -18
  97. package/dist/vendor/core/delivery/message-client.js +0 -38
  98. package/dist/vendor/core/delivery/message-ledger.d.ts +0 -35
  99. package/dist/vendor/core/delivery/message-ledger.js +0 -97
  100. package/dist/vendor/core/delivery/message-schema.d.ts +0 -16
  101. package/dist/vendor/core/delivery/message-schema.js +0 -111
  102. package/dist/vendor/core/delivery/update-check.d.ts +0 -19
  103. package/dist/vendor/core/delivery/update-check.js +0 -107
@@ -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
- }