@martinloop/mcp 0.3.5 → 0.3.7

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 (118) hide show
  1. package/README.md +137 -137
  2. package/dist/discovery-metadata.d.ts +1 -1
  3. package/dist/discovery-metadata.js +2 -0
  4. package/dist/prompts.js +19 -1
  5. package/dist/resources.d.ts +3 -0
  6. package/dist/resources.js +125 -17
  7. package/dist/server-validation.d.ts +1 -1
  8. package/dist/server-validation.js +12 -0
  9. package/dist/server.js +18 -7
  10. package/dist/tools/doctor.js +6 -0
  11. package/dist/tools/run-loop.d.ts +2 -1
  12. package/dist/tools/run-loop.js +5 -1
  13. package/dist/tools/tool-support.d.ts +1 -1
  14. package/dist/tools/tool-support.js +2 -1
  15. package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +146 -0
  16. package/dist/vendor/adapters/adapters/src/claude-cli.js +1100 -0
  17. package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +103 -0
  18. package/dist/vendor/adapters/adapters/src/cli-bridge.js +569 -0
  19. package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +78 -0
  20. package/dist/vendor/adapters/adapters/src/codex-launcher.js +663 -0
  21. package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +10 -0
  22. package/dist/vendor/adapters/adapters/src/direct-provider.js +40 -0
  23. package/dist/vendor/adapters/adapters/src/index.d.ts +8 -0
  24. package/dist/vendor/adapters/adapters/src/index.js +8 -0
  25. package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +62 -0
  26. package/dist/vendor/adapters/adapters/src/openai-compatible.js +306 -0
  27. package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +17 -0
  28. package/dist/vendor/adapters/adapters/src/runtime-support.js +59 -0
  29. package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +8 -0
  30. package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +40 -0
  31. package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +8 -0
  32. package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +12 -0
  33. package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +9 -0
  34. package/dist/vendor/adapters/adapters/src/verifier-only.js +63 -0
  35. package/dist/vendor/adapters/claude-cli.js +21 -20
  36. package/dist/vendor/adapters/cli-bridge.d.ts +7 -0
  37. package/dist/vendor/adapters/cli-bridge.js +94 -8
  38. package/dist/vendor/adapters/codex-launcher.js +18 -7
  39. package/dist/vendor/adapters/contracts/src/context-graph.d.ts +48 -0
  40. package/dist/vendor/adapters/contracts/src/context-graph.js +26 -0
  41. package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +78 -0
  42. package/dist/vendor/adapters/contracts/src/execution-policy.js +43 -0
  43. package/dist/vendor/adapters/contracts/src/governance.d.ts +24 -0
  44. package/dist/vendor/adapters/contracts/src/governance.js +11 -0
  45. package/dist/vendor/adapters/contracts/src/identity.d.ts +29 -0
  46. package/dist/vendor/adapters/contracts/src/identity.js +30 -0
  47. package/dist/vendor/adapters/contracts/src/index.d.ts +412 -0
  48. package/dist/vendor/adapters/contracts/src/index.js +225 -0
  49. package/dist/vendor/adapters/contracts/src/operator.d.ts +19 -0
  50. package/dist/vendor/adapters/contracts/src/operator.js +11 -0
  51. package/dist/vendor/adapters/contracts/src/trajectory.d.ts +26 -0
  52. package/dist/vendor/adapters/contracts/src/trajectory.js +27 -0
  53. package/dist/vendor/adapters/core/src/compiler.d.ts +52 -0
  54. package/dist/vendor/adapters/core/src/compiler.js +52 -0
  55. package/dist/vendor/adapters/core/src/context-graph.d.ts +3 -0
  56. package/dist/vendor/adapters/core/src/context-graph.js +371 -0
  57. package/dist/vendor/adapters/core/src/context-integrity.d.ts +26 -0
  58. package/dist/vendor/adapters/core/src/context-integrity.js +80 -0
  59. package/dist/vendor/adapters/core/src/grounding.d.ts +38 -0
  60. package/dist/vendor/adapters/core/src/grounding.js +273 -0
  61. package/dist/vendor/adapters/core/src/identity.d.ts +20 -0
  62. package/dist/vendor/adapters/core/src/identity.js +86 -0
  63. package/dist/vendor/adapters/core/src/index.d.ts +186 -0
  64. package/dist/vendor/adapters/core/src/index.js +1495 -0
  65. package/dist/vendor/adapters/core/src/leash.d.ts +48 -0
  66. package/dist/vendor/adapters/core/src/leash.js +497 -0
  67. package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +18 -0
  68. package/dist/vendor/adapters/core/src/persistence/compiler.js +34 -0
  69. package/dist/vendor/adapters/core/src/persistence/index.d.ts +10 -0
  70. package/dist/vendor/adapters/core/src/persistence/index.js +5 -0
  71. package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +39 -0
  72. package/dist/vendor/adapters/core/src/persistence/integrity.js +252 -0
  73. package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +23 -0
  74. package/dist/vendor/adapters/core/src/persistence/ledger.js +9 -0
  75. package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +63 -0
  76. package/dist/vendor/adapters/core/src/persistence/memory-store.js +115 -0
  77. package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +66 -0
  78. package/dist/vendor/adapters/core/src/persistence/runs-reader.js +140 -0
  79. package/dist/vendor/adapters/core/src/persistence/store.d.ts +89 -0
  80. package/dist/vendor/adapters/core/src/persistence/store.js +125 -0
  81. package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +85 -0
  82. package/dist/vendor/adapters/core/src/persistence/trace-store.js +125 -0
  83. package/dist/vendor/adapters/core/src/policy-compiler.d.ts +2 -0
  84. package/dist/vendor/adapters/core/src/policy-compiler.js +195 -0
  85. package/dist/vendor/adapters/core/src/policy.d.ts +156 -0
  86. package/dist/vendor/adapters/core/src/policy.js +704 -0
  87. package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +53 -0
  88. package/dist/vendor/adapters/core/src/repo-analyzer.js +263 -0
  89. package/dist/vendor/adapters/core/src/rollback.d.ts +15 -0
  90. package/dist/vendor/adapters/core/src/rollback.js +253 -0
  91. package/dist/vendor/adapters/core/src/routing.d.ts +82 -0
  92. package/dist/vendor/adapters/core/src/routing.js +336 -0
  93. package/dist/vendor/adapters/core/src/test-integrity.d.ts +64 -0
  94. package/dist/vendor/adapters/core/src/test-integrity.js +197 -0
  95. package/dist/vendor/adapters/core/src/trajectory.d.ts +18 -0
  96. package/dist/vendor/adapters/core/src/trajectory.js +295 -0
  97. package/dist/vendor/adapters/openai-compatible.js +84 -51
  98. package/dist/vendor/adapters/verifier-only.js +2 -2
  99. package/dist/vendor/contracts/index.d.ts +2 -2
  100. package/dist/vendor/contracts/index.js +1 -0
  101. package/dist/vendor/core/compiler.js +4 -9
  102. package/dist/vendor/core/index.d.ts +10 -2
  103. package/dist/vendor/core/index.js +32 -91
  104. package/dist/vendor/core/persistence/memory-store.d.ts +63 -0
  105. package/dist/vendor/core/persistence/memory-store.js +115 -0
  106. package/dist/vendor/core/persistence/trace-store.d.ts +85 -0
  107. package/dist/vendor/core/persistence/trace-store.js +125 -0
  108. package/dist/vendor/core/repo-analyzer.d.ts +53 -0
  109. package/dist/vendor/core/repo-analyzer.js +263 -0
  110. package/dist/vendor/core/rollback.js +2 -3
  111. package/dist/vendor/core/routing.d.ts +42 -0
  112. package/dist/vendor/core/routing.js +156 -5
  113. package/dist/vendor/core/test-integrity.d.ts +64 -0
  114. package/dist/vendor/core/test-integrity.js +197 -0
  115. package/dist/workflow-state.d.ts +1 -1
  116. package/dist/workflow-state.js +12 -4
  117. package/package.json +1 -1
  118. package/server.json +2 -2
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Route classification and compression for governed agent runs.
3
+ *
4
+ * Decides whether a task needs manager/planner orchestration or can go
5
+ * directly to a worker, based on task complexity signals. Prevents
6
+ * wasted coordination cost (Pre Work Burn) on simple tasks.
7
+ */
8
+ import { DEFAULT_ROUTING_POLICY } from "../../../contracts/index.js";
9
+ const SECURITY_KEYWORDS = [
10
+ "auth", "authentication", "authorization", "credential", "secret",
11
+ "token", "password", "oauth", "jwt", "session", "csrf", "xss",
12
+ "injection", "encryption", "certificate", "tls", "ssl", "permission"
13
+ ];
14
+ const ARCHITECTURE_KEYWORDS = [
15
+ "refactor", "architecture", "redesign", "restructure", "migration",
16
+ "database schema", "api design", "service boundary", "microservice",
17
+ "event sourcing", "cqrs", "distributed"
18
+ ];
19
+ const MIGRATION_KEYWORDS = [
20
+ "migration", "migrate", "upgrade", "downgrade", "schema change",
21
+ "data migration", "database migration", "breaking change"
22
+ ];
23
+ const MULTI_FILE_KEYWORDS = [
24
+ "across", "all files", "every file", "codebase-wide", "global",
25
+ "rename everywhere", "update all", "system-wide"
26
+ ];
27
+ function extractComplexitySignals(input) {
28
+ const lower = input.objective.toLowerCase();
29
+ return {
30
+ objectiveLength: input.objective.length,
31
+ fileCount: input.scopedFileCount ?? estimateFileCount(input),
32
+ hasSecurityKeywords: SECURITY_KEYWORDS.some((kw) => lower.includes(kw)),
33
+ hasArchitectureKeywords: ARCHITECTURE_KEYWORDS.some((kw) => lower.includes(kw)),
34
+ hasMigrationKeywords: MIGRATION_KEYWORDS.some((kw) => lower.includes(kw)),
35
+ hasMultiFileKeywords: MULTI_FILE_KEYWORDS.some((kw) => lower.includes(kw)),
36
+ verifierCount: input.verificationPlan.length,
37
+ budgetUsd: input.budgetUsd,
38
+ scopeRestricted: (input.allowedPaths?.length ?? 0) > 0 || (input.deniedPaths?.length ?? 0) > 0
39
+ };
40
+ }
41
+ function estimateFileCount(input) {
42
+ const pathCount = (input.allowedPaths?.length ?? 0) + (input.deniedPaths?.length ?? 0);
43
+ if (pathCount > 0)
44
+ return pathCount;
45
+ // Heuristic: short objectives with specific file refs → 1-2 files
46
+ const fileRefs = input.objective.match(/[\w/.-]+\.\w{1,5}/g);
47
+ return fileRefs?.length ?? 1;
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // Confidence scoring
51
+ // ---------------------------------------------------------------------------
52
+ function calculateDirectConfidence(signals, input) {
53
+ let confidence = 0.7; // Base: most tasks can go direct
54
+ // Boost for simple tasks
55
+ if (signals.objectiveLength < 200)
56
+ confidence += 0.1;
57
+ if (signals.fileCount <= 2)
58
+ confidence += 0.1;
59
+ if (signals.scopeRestricted)
60
+ confidence += 0.05;
61
+ if (signals.verifierCount === 1)
62
+ confidence += 0.05;
63
+ // Penalize for complex tasks
64
+ if (signals.hasSecurityKeywords)
65
+ confidence -= 0.25;
66
+ if (signals.hasArchitectureKeywords)
67
+ confidence -= 0.2;
68
+ if (signals.hasMigrationKeywords)
69
+ confidence -= 0.3;
70
+ if (signals.hasMultiFileKeywords)
71
+ confidence -= 0.15;
72
+ if (signals.objectiveLength > 500)
73
+ confidence -= 0.1;
74
+ if (signals.fileCount > 5)
75
+ confidence -= 0.15;
76
+ if (signals.budgetUsd > 10)
77
+ confidence -= 0.1;
78
+ // Historical evidence
79
+ if (input.historicalDirectSuccessRate !== undefined) {
80
+ confidence = confidence * 0.6 + input.historicalDirectSuccessRate * 0.4;
81
+ }
82
+ return Math.max(0, Math.min(1, confidence));
83
+ }
84
+ // ---------------------------------------------------------------------------
85
+ // Public API
86
+ // ---------------------------------------------------------------------------
87
+ // ---------------------------------------------------------------------------
88
+ // Cost-aware model tier routing
89
+ //
90
+ // Engine-agnostic tier system — each engine maps tiers to real model IDs.
91
+ // Never pay for reasoning/orchestration on simple tasks.
92
+ //
93
+ // Tier → concrete model per engine:
94
+ // haiku → claude-haiku-4-5, gpt-4o-mini, gemini-2.5-flash, deepseek-chat, qwen3-8b, llama-3.1-8b
95
+ // sonnet → claude-sonnet-4-6, gpt-4o, gemini-2.5-pro, deepseek-r1, qwen3-32b, llama-3.3-70b
96
+ // opus → claude-opus-4-6, o3, gemini-2.5-ultra, deepseek-r1-671b, qwen3-235b, llama-3.1-405b
97
+ //
98
+ // Pricing reference (USD per 1M tokens, approx 2026):
99
+ // haiku: $0.80 input / $4.00 output (~1x baseline)
100
+ // sonnet: $3.00 input / $15.00 output (~4x haiku)
101
+ // opus: $15.00 input / $75.00 output (~19x haiku)
102
+ // ---------------------------------------------------------------------------
103
+ const SONNET_AVG_COST_PER_USD_BUDGET = 0.35; // sonnet uses ~35% of budget on average
104
+ const HAIKU_AVG_COST_PER_USD_BUDGET = 0.10; // haiku uses ~10% — ~3.5x cheaper
105
+ function selectModelTier(mode) {
106
+ // Route already encodes complexity. Direct = passed the simplicity check → haiku.
107
+ if (mode === "direct")
108
+ return "haiku";
109
+ if (mode === "manager")
110
+ return "sonnet";
111
+ return "opus";
112
+ }
113
+ /**
114
+ * Resolve a tier to a concrete model ID for a specific engine.
115
+ * Allows downstream adapters to switch models automatically.
116
+ */
117
+ export function resolveModelForTier(tier, engine) {
118
+ const matrix = {
119
+ claude: {
120
+ haiku: "claude-haiku-4-5-20251001",
121
+ sonnet: "claude-sonnet-4-6",
122
+ opus: "claude-opus-4-6"
123
+ },
124
+ codex: {
125
+ haiku: "gpt-4o-mini",
126
+ sonnet: "gpt-4.1",
127
+ opus: "o3"
128
+ },
129
+ gemini: {
130
+ haiku: "gemini-2.5-flash",
131
+ sonnet: "gemini-2.5-pro",
132
+ opus: "gemini-2.5-ultra"
133
+ },
134
+ openai: {
135
+ haiku: "gpt-4o-mini",
136
+ sonnet: "gpt-4o",
137
+ opus: "o3"
138
+ },
139
+ deepseek: {
140
+ haiku: "deepseek-chat",
141
+ sonnet: "deepseek-r1",
142
+ opus: "deepseek-r1"
143
+ },
144
+ qwen: {
145
+ haiku: "qwen/qwen3-8b",
146
+ sonnet: "qwen/qwen3-32b",
147
+ opus: "qwen/qwen3-235b-a22b"
148
+ },
149
+ llama: {
150
+ haiku: "meta-llama/llama-3.1-8b-instruct",
151
+ sonnet: "meta-llama/llama-3.3-70b-instruct",
152
+ opus: "meta-llama/llama-3.1-405b-instruct"
153
+ }
154
+ };
155
+ const engineKey = engine.toLowerCase().split("-")[0] ?? engine;
156
+ const claudeMatrix = matrix.claude;
157
+ return matrix[engineKey]?.[tier] ?? claudeMatrix?.[tier] ?? "claude-sonnet-4-6";
158
+ }
159
+ function estimateSavingVsSonnet(tier, budgetUsd) {
160
+ if (tier === "sonnet")
161
+ return 0;
162
+ if (tier === "haiku")
163
+ return Math.round((SONNET_AVG_COST_PER_USD_BUDGET - HAIKU_AVG_COST_PER_USD_BUDGET) * budgetUsd * 100) / 100;
164
+ // opus costs more than sonnet — negative saving
165
+ return Math.round(-1 * SONNET_AVG_COST_PER_USD_BUDGET * budgetUsd * 0.5 * 100) / 100;
166
+ }
167
+ /**
168
+ * Select the best available engine for a given tier requirement.
169
+ *
170
+ * When multiple engines are available, picks the cheapest that meets
171
+ * the capability requirement. Falls back gracefully:
172
+ * - If required tier unavailable, upgrades to next tier of same engine
173
+ * - If only one engine family available, uses its best equivalent
174
+ *
175
+ * @param requiredTier - Minimum capability tier needed
176
+ * @param availableEngines - What's installed and authenticated
177
+ * @returns Best engine ID and concrete model to use
178
+ */
179
+ export function selectBestEngine(requiredTier, availableEngines) {
180
+ const available = availableEngines.filter((e) => e.available);
181
+ if (available.length === 0) {
182
+ // Nothing detected — default to claude (user must install)
183
+ return {
184
+ engineId: "claude",
185
+ model: resolveModelForTier(requiredTier, "claude"),
186
+ reasoning: "No engines detected. Defaulting to claude. Run martin doctor for setup instructions."
187
+ };
188
+ }
189
+ const tierRank = {
190
+ haiku: 0, sonnet: 1, opus: 2
191
+ };
192
+ const requiredRank = tierRank[requiredTier];
193
+ // Find the cheapest engine that meets or exceeds the required tier
194
+ const eligible = available
195
+ .filter((e) => tierRank[e.capabilityTier] >= requiredRank)
196
+ .sort((a, b) => {
197
+ // Sort by cost (cheap first), then by exact tier match
198
+ const costOrder = { cheap: 0, mid: 1, expensive: 2 };
199
+ if (costOrder[a.costTier] !== costOrder[b.costTier]) {
200
+ return costOrder[a.costTier] - costOrder[b.costTier];
201
+ }
202
+ // Prefer exact tier match over overqualified
203
+ const aExact = a.capabilityTier === requiredTier ? 0 : 1;
204
+ const bExact = b.capabilityTier === requiredTier ? 0 : 1;
205
+ return aExact - bExact;
206
+ });
207
+ if (eligible.length > 0) {
208
+ const best = eligible[0];
209
+ const model = resolveModelForTier(requiredTier, best.id);
210
+ return {
211
+ engineId: best.id,
212
+ model,
213
+ reasoning: `Selected ${best.id} (${model}) as cheapest available engine for ${requiredTier}-tier task.`
214
+ };
215
+ }
216
+ // All available engines are below required tier — upgrade to best available
217
+ const best = available.sort((a, b) => tierRank[b.capabilityTier] - tierRank[a.capabilityTier])[0];
218
+ const model = resolveModelForTier(best.capabilityTier, best.id);
219
+ return {
220
+ engineId: best.id,
221
+ model,
222
+ reasoning: `No ${requiredTier}-tier engine available. Upgrading to ${best.id} (${model}) — best available.`
223
+ };
224
+ }
225
+ export function classifyRoute(input) {
226
+ const policy = { ...DEFAULT_ROUTING_POLICY, ...input.policy };
227
+ const signals = extractComplexitySignals(input);
228
+ const directConfidence = calculateDirectConfidence(signals, input);
229
+ const reason = [];
230
+ const blockedSteps = [];
231
+ // Policy override: forced mode
232
+ if (policy.mode === "direct") {
233
+ const tier = selectModelTier("direct");
234
+ return {
235
+ selectedMode: "direct",
236
+ confidence: 1,
237
+ expectedCostUsd: estimateDirectCost(input.budgetUsd),
238
+ expectedPreworkBurnPct: 5,
239
+ reason: ["Policy forces direct execution mode."],
240
+ blockedSteps: ["manager", "planner", "router", "consensus"],
241
+ compressed: true,
242
+ compressionSummary: "Policy: direct mode — skipping all orchestration.",
243
+ recommendedModelTier: tier,
244
+ estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
245
+ };
246
+ }
247
+ if (policy.mode === "consensus") {
248
+ const tier = selectModelTier("consensus");
249
+ return {
250
+ selectedMode: "consensus",
251
+ confidence: 0.5,
252
+ expectedCostUsd: input.budgetUsd * 0.8,
253
+ expectedPreworkBurnPct: 35,
254
+ reason: ["Policy forces consensus mode."],
255
+ blockedSteps: [],
256
+ compressed: false,
257
+ recommendedModelTier: tier,
258
+ estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
259
+ };
260
+ }
261
+ // Adaptive routing
262
+ if (directConfidence >= policy.skipOrchestrationIfConfidenceAbove) {
263
+ reason.push(`Direct confidence ${(directConfidence * 100).toFixed(0)}% exceeds threshold ${(policy.skipOrchestrationIfConfidenceAbove * 100).toFixed(0)}%.`);
264
+ if (signals.objectiveLength < 200)
265
+ reason.push("Short, focused objective.");
266
+ if (signals.fileCount <= 2)
267
+ reason.push(`Scoped to ${String(signals.fileCount)} file(s).`);
268
+ if (signals.scopeRestricted)
269
+ reason.push("Path scope is restricted.");
270
+ blockedSteps.push("manager", "consensus");
271
+ const tier = selectModelTier("direct");
272
+ return {
273
+ selectedMode: "direct",
274
+ confidence: directConfidence,
275
+ expectedCostUsd: estimateDirectCost(input.budgetUsd),
276
+ expectedPreworkBurnPct: 8,
277
+ reason,
278
+ blockedSteps,
279
+ compressed: true,
280
+ compressionSummary: `Compressed from manager→planner→worker to direct worker. Estimated savings: ${String(Math.round(input.budgetUsd * 0.3 * 100) / 100)} USD.`,
281
+ recommendedModelTier: tier,
282
+ estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
283
+ };
284
+ }
285
+ // Complex task — needs orchestration
286
+ if (signals.hasSecurityKeywords)
287
+ reason.push("Security-sensitive task detected.");
288
+ if (signals.hasMigrationKeywords)
289
+ reason.push("Migration task detected.");
290
+ if (signals.hasArchitectureKeywords)
291
+ reason.push("Architectural scope detected.");
292
+ if (signals.fileCount > 5)
293
+ reason.push(`Touches ${String(signals.fileCount)}+ files.`);
294
+ const needsConsensus = signals.hasSecurityKeywords && signals.hasMigrationKeywords;
295
+ const mode = needsConsensus ? "consensus" : "manager";
296
+ const tier = selectModelTier(mode);
297
+ return {
298
+ selectedMode: mode,
299
+ confidence: directConfidence,
300
+ expectedCostUsd: input.budgetUsd * (needsConsensus ? 0.8 : 0.6),
301
+ expectedPreworkBurnPct: needsConsensus ? 40 : 25,
302
+ reason,
303
+ blockedSteps: needsConsensus ? [] : ["consensus"],
304
+ compressed: false,
305
+ recommendedModelTier: tier,
306
+ estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
307
+ };
308
+ }
309
+ function estimateDirectCost(budgetUsd) {
310
+ // Direct execution typically uses 30-50% of budget
311
+ return Math.round(budgetUsd * 0.35 * 100) / 100;
312
+ }
313
+ /**
314
+ * Checks whether a run's Pre Work Burn exceeds the routing policy limits.
315
+ * Returns a block reason if exceeded, undefined if within bounds.
316
+ */
317
+ export function evaluatePreworkBurnPolicy(preworkCostUsd, totalCostUsd, policy = {}) {
318
+ const resolved = { ...DEFAULT_ROUTING_POLICY, ...policy };
319
+ if (!resolved.enabled) {
320
+ return { exceeded: false };
321
+ }
322
+ if (preworkCostUsd > resolved.maxPreworkCostUsd) {
323
+ return {
324
+ exceeded: true,
325
+ reason: `Pre-work cost $${preworkCostUsd.toFixed(2)} exceeds routing policy cap of $${resolved.maxPreworkCostUsd.toFixed(2)}.`
326
+ };
327
+ }
328
+ const burnPct = totalCostUsd > 0 ? (preworkCostUsd / totalCostUsd) * 100 : 0;
329
+ if (burnPct > resolved.maxPreworkBudgetPct) {
330
+ return {
331
+ exceeded: true,
332
+ reason: `Pre Work Burn at ${burnPct.toFixed(0)}% exceeds routing policy cap of ${String(resolved.maxPreworkBudgetPct)}%.`
333
+ };
334
+ }
335
+ return { exceeded: false };
336
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Test Integrity Validator
3
+ *
4
+ * Detects circular test validation — the problem where AI agents generate
5
+ * tests that only pass because they test the agent's own output, not real
6
+ * behavior. A test that passes on both the pre-change and post-change
7
+ * codebase proves nothing about the change.
8
+ *
9
+ * Strategy:
10
+ * 1. Snapshot which test files existed before the agent ran
11
+ * 2. After the run, identify NEW test files the agent created
12
+ * 3. For each new test: check if it imports/references pre-existing code
13
+ * (real test) or only references agent-generated code (circular test)
14
+ * 4. Flag tests that don't exercise any pre-existing function
15
+ *
16
+ * This module provides the analysis primitives. The verification pipeline
17
+ * calls these during the post-run verification step.
18
+ */
19
+ export interface TestIntegrityReport {
20
+ totalNewTests: number;
21
+ realTests: number;
22
+ suspiciousTests: number;
23
+ findings: TestIntegrityFinding[];
24
+ verdict: "pass" | "warn" | "fail";
25
+ summary: string;
26
+ }
27
+ export interface TestIntegrityFinding {
28
+ filePath: string;
29
+ issue: "no_pre_existing_imports" | "self_referential" | "trivial_assertion" | "empty_test";
30
+ severity: "high" | "medium" | "low";
31
+ detail: string;
32
+ }
33
+ /**
34
+ * Detect test files in a directory tree.
35
+ */
36
+ export declare function findTestFiles(rootDir: string): string[];
37
+ /**
38
+ * Check if a filename matches test file patterns.
39
+ */
40
+ export declare function isTestFile(filename: string, fullPath?: string): boolean;
41
+ /**
42
+ * Snapshot existing test files before an agent run.
43
+ */
44
+ export declare function snapshotTestFiles(rootDir: string): Set<string>;
45
+ /**
46
+ * Identify new test files created during an agent run.
47
+ */
48
+ export declare function identifyNewTests(rootDir: string, preRunSnapshot: Set<string>): string[];
49
+ /**
50
+ * Extract import paths from a TypeScript/JavaScript file.
51
+ */
52
+ export declare function extractImports(content: string): string[];
53
+ /**
54
+ * Check if a test file contains only trivial assertions.
55
+ */
56
+ export declare function hasTrivialAssertionsOnly(content: string): boolean;
57
+ /**
58
+ * Check if a test file imports any pre-existing source files.
59
+ */
60
+ export declare function importsPreExistingCode(testContent: string, preRunSourceFiles: Set<string>): boolean;
61
+ /**
62
+ * Validate the integrity of tests created during a governed run.
63
+ */
64
+ export declare function validateTestIntegrity(rootDir: string, preRunTestSnapshot: Set<string>, preRunSourceFiles: Set<string>): TestIntegrityReport;
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Test Integrity Validator
3
+ *
4
+ * Detects circular test validation — the problem where AI agents generate
5
+ * tests that only pass because they test the agent's own output, not real
6
+ * behavior. A test that passes on both the pre-change and post-change
7
+ * codebase proves nothing about the change.
8
+ *
9
+ * Strategy:
10
+ * 1. Snapshot which test files existed before the agent ran
11
+ * 2. After the run, identify NEW test files the agent created
12
+ * 3. For each new test: check if it imports/references pre-existing code
13
+ * (real test) or only references agent-generated code (circular test)
14
+ * 4. Flag tests that don't exercise any pre-existing function
15
+ *
16
+ * This module provides the analysis primitives. The verification pipeline
17
+ * calls these during the post-run verification step.
18
+ */
19
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
20
+ import { join, relative } from "node:path";
21
+ const TEST_FILE_PATTERNS = [
22
+ /\.test\.[tj]sx?$/,
23
+ /\.spec\.[tj]sx?$/,
24
+ /\/__tests__\//,
25
+ /\/tests?\//
26
+ ];
27
+ const TRIVIAL_ASSERTION_PATTERNS = [
28
+ /expect\(true\)\.toBe\(true\)/,
29
+ /expect\(1\)\.toBe\(1\)/,
30
+ /expect\(\)\.toBeUndefined\(\)/,
31
+ /assert\.ok\(true\)/,
32
+ /expect\(.*\)\.toBeDefined\(\)/,
33
+ /expect\(typeof\s+\w+\)\.toBe\(['"]\w+['"]\)/
34
+ ];
35
+ const IMPORT_PATTERN = /(?:import\s+.*from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g;
36
+ /**
37
+ * Detect test files in a directory tree.
38
+ */
39
+ export function findTestFiles(rootDir) {
40
+ const results = [];
41
+ function walk(dir) {
42
+ if (!existsSync(dir))
43
+ return;
44
+ try {
45
+ const entries = readdirSync(dir, { withFileTypes: true });
46
+ for (const entry of entries) {
47
+ const fullPath = join(dir, entry.name);
48
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git")
49
+ continue;
50
+ if (entry.isDirectory()) {
51
+ walk(fullPath);
52
+ }
53
+ else if (entry.isFile() && isTestFile(entry.name, fullPath)) {
54
+ results.push(relative(rootDir, fullPath));
55
+ }
56
+ }
57
+ }
58
+ catch {
59
+ // Permission denied or inaccessible — skip silently
60
+ }
61
+ }
62
+ walk(rootDir);
63
+ return results;
64
+ }
65
+ /**
66
+ * Check if a filename matches test file patterns.
67
+ */
68
+ export function isTestFile(filename, fullPath) {
69
+ const checkPath = fullPath ?? filename;
70
+ return TEST_FILE_PATTERNS.some((pattern) => pattern.test(checkPath));
71
+ }
72
+ /**
73
+ * Snapshot existing test files before an agent run.
74
+ */
75
+ export function snapshotTestFiles(rootDir) {
76
+ return new Set(findTestFiles(rootDir));
77
+ }
78
+ /**
79
+ * Identify new test files created during an agent run.
80
+ */
81
+ export function identifyNewTests(rootDir, preRunSnapshot) {
82
+ const currentTests = findTestFiles(rootDir);
83
+ return currentTests.filter((testPath) => !preRunSnapshot.has(testPath));
84
+ }
85
+ /**
86
+ * Extract import paths from a TypeScript/JavaScript file.
87
+ */
88
+ export function extractImports(content) {
89
+ const imports = [];
90
+ let match;
91
+ const pattern = new RegExp(IMPORT_PATTERN.source, "g");
92
+ while ((match = pattern.exec(content)) !== null) {
93
+ const importPath = match[1] ?? match[2];
94
+ if (importPath)
95
+ imports.push(importPath);
96
+ }
97
+ return imports;
98
+ }
99
+ /**
100
+ * Check if a test file contains only trivial assertions.
101
+ */
102
+ export function hasTrivialAssertionsOnly(content) {
103
+ const assertionLines = content.split("\n").filter((line) => /expect\(|assert\.|should\./i.test(line));
104
+ if (assertionLines.length === 0)
105
+ return true;
106
+ return assertionLines.every((line) => TRIVIAL_ASSERTION_PATTERNS.some((pattern) => pattern.test(line)));
107
+ }
108
+ /**
109
+ * Check if a test file imports any pre-existing source files.
110
+ */
111
+ export function importsPreExistingCode(testContent, preRunSourceFiles) {
112
+ const imports = extractImports(testContent);
113
+ return imports.some((importPath) => {
114
+ // Relative imports that reference pre-existing files
115
+ if (importPath.startsWith(".") || importPath.startsWith("/")) {
116
+ // Normalize: strip extension, check if any pre-existing source matches
117
+ const normalized = importPath.replace(/\.[tj]sx?$/, "");
118
+ return [...preRunSourceFiles].some((srcFile) => {
119
+ const normalizedSrc = srcFile.replace(/\.[tj]sx?$/, "");
120
+ return normalizedSrc.endsWith(normalized) || normalized.endsWith(normalizedSrc);
121
+ });
122
+ }
123
+ // Package imports always count as real (they reference pre-existing deps)
124
+ return true;
125
+ });
126
+ }
127
+ /**
128
+ * Validate the integrity of tests created during a governed run.
129
+ */
130
+ export function validateTestIntegrity(rootDir, preRunTestSnapshot, preRunSourceFiles) {
131
+ const newTests = identifyNewTests(rootDir, preRunTestSnapshot);
132
+ const findings = [];
133
+ for (const testPath of newTests) {
134
+ const fullPath = join(rootDir, testPath);
135
+ let content;
136
+ try {
137
+ content = readFileSync(fullPath, "utf8");
138
+ }
139
+ catch {
140
+ findings.push({
141
+ filePath: testPath,
142
+ issue: "empty_test",
143
+ severity: "high",
144
+ detail: "Test file could not be read."
145
+ });
146
+ continue;
147
+ }
148
+ if (content.trim().length === 0) {
149
+ findings.push({
150
+ filePath: testPath,
151
+ issue: "empty_test",
152
+ severity: "high",
153
+ detail: "Test file is empty."
154
+ });
155
+ continue;
156
+ }
157
+ if (hasTrivialAssertionsOnly(content)) {
158
+ findings.push({
159
+ filePath: testPath,
160
+ issue: "trivial_assertion",
161
+ severity: "high",
162
+ detail: "Test contains only trivial assertions (expect(true).toBe(true) or equivalent)."
163
+ });
164
+ continue;
165
+ }
166
+ if (!importsPreExistingCode(content, preRunSourceFiles)) {
167
+ findings.push({
168
+ filePath: testPath,
169
+ issue: "no_pre_existing_imports",
170
+ severity: "medium",
171
+ detail: "Test does not import any pre-existing source code. May be testing only agent-generated output."
172
+ });
173
+ }
174
+ }
175
+ const suspiciousTests = findings.filter((f) => f.severity === "high").length;
176
+ const realTests = newTests.length - suspiciousTests;
177
+ const verdict = suspiciousTests > 0 && suspiciousTests >= newTests.length * 0.5
178
+ ? "fail"
179
+ : suspiciousTests > 0
180
+ ? "warn"
181
+ : "pass";
182
+ const summary = newTests.length === 0
183
+ ? "No new test files detected."
184
+ : verdict === "pass"
185
+ ? `${newTests.length} new test(s) validated — all reference pre-existing code.`
186
+ : verdict === "warn"
187
+ ? `${suspiciousTests} of ${newTests.length} new test(s) flagged for review.`
188
+ : `Test integrity check failed: ${suspiciousTests} of ${newTests.length} new test(s) appear circular or trivial.`;
189
+ return {
190
+ totalNewTests: newTests.length,
191
+ realTests,
192
+ suspiciousTests,
193
+ findings,
194
+ verdict,
195
+ summary
196
+ };
197
+ }
@@ -0,0 +1,18 @@
1
+ import { type CircuitBreakDecision, type TrajectoryAssessment } from "../../../contracts/index.js";
2
+ export declare function assessTrajectory(input: {
3
+ objective: string;
4
+ verificationPlan?: string[];
5
+ attempts: TrajectoryAttemptLike[];
6
+ }): TrajectoryAssessment;
7
+ export declare function decideCircuitBreak(input: {
8
+ objective: string;
9
+ verificationPlan?: string[];
10
+ attempts: TrajectoryAttemptLike[];
11
+ remainingIterations?: number;
12
+ }): CircuitBreakDecision;
13
+ interface TrajectoryAttemptLike {
14
+ index: number;
15
+ summary?: string;
16
+ failureClass?: string;
17
+ }
18
+ export {};