@martinloop/mcp 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/resources.js +15 -3
  2. package/dist/server.js +40 -8
  3. package/dist/tools/doctor.js +5 -4
  4. package/dist/tools/get-status.d.ts +2 -0
  5. package/dist/tools/get-status.js +1 -0
  6. package/dist/tools/preflight.js +1 -1
  7. package/dist/tools/run-loop.d.ts +6 -1
  8. package/dist/tools/run-loop.js +22 -7
  9. package/dist/tools/tool-errors.js +1 -1
  10. package/dist/tools/tool-response.js +9 -2
  11. package/dist/tools/tool-support.d.ts +3 -2
  12. package/dist/tools/tool-support.js +5 -3
  13. package/dist/vendor/adapters/claude-cli.d.ts +2 -2
  14. package/dist/vendor/adapters/claude-cli.js +59 -34
  15. package/dist/vendor/adapters/index.d.ts +1 -1
  16. package/dist/vendor/adapters/index.js +1 -1
  17. package/dist/vendor/adapters/openai-compatible.d.ts +2 -3
  18. package/dist/vendor/adapters/openai-compatible.js +33 -7
  19. package/dist/vendor/adapters/verifier-only.d.ts +9 -0
  20. package/dist/vendor/adapters/verifier-only.js +68 -0
  21. package/dist/vendor/contracts/index.d.ts +16 -6
  22. package/dist/vendor/contracts/index.js +1 -1
  23. package/dist/vendor/contracts/verified-handoff.d.ts +6 -0
  24. package/dist/vendor/contracts/verified-handoff.js +5 -0
  25. package/dist/vendor/core/grounding.d.ts +2 -1
  26. package/dist/vendor/core/grounding.js +40 -5
  27. package/dist/vendor/core/index.d.ts +4 -6
  28. package/dist/vendor/core/index.js +25 -19
  29. package/dist/vendor/core/persistence/runs-reader.d.ts +2 -0
  30. package/dist/vendor/core/policy.js +6 -6
  31. package/dist/vendor/core/red-blue/red-phase.d.ts +2 -1
  32. package/dist/vendor/core/red-blue/red-phase.js +3 -3
  33. package/dist/vendor/core/red-blue/risk-tiers.d.ts +2 -4
  34. package/dist/vendor/core/red-blue/risk-tiers.js +1 -3
  35. package/dist/vendor/core/routing.d.ts +0 -42
  36. package/dist/vendor/core/routing.js +4 -154
  37. package/dist/vendor/core/verified-handoff.d.ts +6 -1
  38. package/dist/vendor/core/verified-handoff.js +28 -0
  39. package/package.json +6 -2
  40. package/server.json +2 -2
  41. package/dist/vendor/adapters/stub-direct-provider.d.ts +0 -8
  42. package/dist/vendor/adapters/stub-direct-provider.js +0 -12
@@ -19,8 +19,8 @@ export { compileExecutionPolicy } from "./policy-compiler.js";
19
19
  export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
20
20
  export { calculateAvoidedUsd, calculateLoopAvoidedUsd } from "./savings.js";
21
21
  export type { AvoidedUsdInput } from "./savings.js";
22
- export { classifyRoute, evaluatePreworkBurnPolicy, resolveModelForTier, selectBestEngine } from "./routing.js";
23
- export type { RouteDecision, RouteClassificationInput, AvailableEngine } from "./routing.js";
22
+ export { classifyRoute, evaluatePreworkBurnPolicy } from "./routing.js";
23
+ export type { RouteDecision, RouteClassificationInput } from "./routing.js";
24
24
  export { validateTestIntegrity, snapshotTestFiles, identifyNewTests, findTestFiles, isTestFile, extractImports, hasTrivialAssertionsOnly, importsPreExistingCode } from "./test-integrity.js";
25
25
  export type { TestIntegrityReport, TestIntegrityFinding } from "./test-integrity.js";
26
26
  export { analyzeRepoStyle, buildPromptDirectives } from "./repo-analyzer.js";
@@ -29,7 +29,7 @@ export { runContextIntegrityPrecheck } from "./context-integrity.js";
29
29
  export type { ContextIntegrityPrecheck, ContextIntegrityVerdict } from "./context-integrity.js";
30
30
  export { compilePromptPacket } from "./compiler.js";
31
31
  export type { PromptPacket, CompilerAdapterRequest } from "./compiler.js";
32
- export { buildVerifiedHandoff, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
32
+ export { buildVerifiedHandoff, deriveVerifiedHandoffExecutionBoundary, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
33
33
  export type { BoundVerifierEvidence, BuildVerifiedHandoffInput, VerifierExecutionBinding, } from "./verified-handoff.js";
34
34
  export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
35
35
  export type { AttemptArtifacts, LedgerEvent, LedgerEventKind, LoopAttemptRecord, LoopRunRecord, ReceiptIntegrityChainEntry, RunContract, RunStore, StoredReceiptIntegrityMaterial } from "./persistence/index.js";
@@ -142,7 +142,7 @@ export interface MartinAdapter {
142
142
  label: string;
143
143
  metadata: {
144
144
  providerId: string;
145
- model: string;
145
+ model?: string;
146
146
  transport?: "cli" | "http" | "routed_http";
147
147
  capabilities?: {
148
148
  preflight?: boolean;
@@ -155,7 +155,6 @@ export interface MartinAdapter {
155
155
  [key: string]: unknown;
156
156
  };
157
157
  execute(request: MartinAdapterRequest): Promise<MartinAdapterResult>;
158
- withModel?(model: string): MartinAdapter;
159
158
  }
160
159
  export interface DistilledContext {
161
160
  focus: string;
@@ -193,7 +192,6 @@ export interface RunMartinInput {
193
192
  now?: () => string;
194
193
  idFactory?: (prefix: string) => string;
195
194
  maxRecentAttempts?: number;
196
- fallbackModels?: string[];
197
195
  fallbackAdapters?: MartinAdapter[];
198
196
  receiptScope?: ReceiptScope;
199
197
  /** Optional persistence store. When provided, runMartin writes artifacts on each lifecycle event. */
@@ -22,7 +22,7 @@ export { compileExecutionPolicy } from "./policy-compiler.js";
22
22
  export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
23
23
  export { calculateAvoidedUsd, calculateLoopAvoidedUsd } from "./savings.js";
24
24
  // ─── Routing economics ─────────────────────────────────────────────────────
25
- export { classifyRoute, evaluatePreworkBurnPolicy, resolveModelForTier, selectBestEngine } from "./routing.js";
25
+ export { classifyRoute, evaluatePreworkBurnPolicy } from "./routing.js";
26
26
  // ─── Test Integrity Validation ──────────────────────────────────────────────
27
27
  export { validateTestIntegrity, snapshotTestFiles, identifyNewTests, findTestFiles, isTestFile, extractImports, hasTrivialAssertionsOnly, importsPreExistingCode } from "./test-integrity.js";
28
28
  // ─── Repo Style Analyzer ────────────────────────────────────────────────────
@@ -32,7 +32,7 @@ export { runContextIntegrityPrecheck } from "./context-integrity.js";
32
32
  // ─── Prompt packet compiler ──────────────────────────────────────────────────
33
33
  export { compilePromptPacket } from "./compiler.js";
34
34
  // ─── Verified Handoff builder ────────────────────────────────────────────────
35
- export { buildVerifiedHandoff, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
35
+ export { buildVerifiedHandoff, deriveVerifiedHandoffExecutionBoundary, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
36
36
  // ─── Persistence (RunStore, LedgerEvent, FileRunStore) ──────────────────────
37
37
  export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
38
38
  export { compileAndPersistContext } from "./persistence/index.js";
@@ -265,11 +265,6 @@ export async function runMartin(input) {
265
265
  }));
266
266
  }
267
267
  }
268
- const DEFAULT_FALLBACK_MODELS = [
269
- "claude-haiku-4-5",
270
- "claude-sonnet-4-6",
271
- "claude-opus-4-6"
272
- ];
273
268
  const adapterChain = [input.adapter, ...(input.fallbackAdapters ?? [])];
274
269
  let currentAdapterIndex = 0;
275
270
  let currentAdapter = adapterChain[currentAdapterIndex] ?? input.adapter;
@@ -803,7 +798,7 @@ export async function runMartin(input) {
803
798
  attemptId,
804
799
  index: currentAttemptIndex,
805
800
  adapterId: executingAdapter.adapterId,
806
- model: executingAdapter.metadata.model,
801
+ ...(executingAdapter.metadata.model ? { model: executingAdapter.metadata.model } : {}),
807
802
  startedAt: attemptStartedAt,
808
803
  completedAt: attemptCompletedAt,
809
804
  summary: result.summary,
@@ -864,13 +859,6 @@ export async function runMartin(input) {
864
859
  adapterSwitched = true;
865
860
  }
866
861
  }
867
- if (failure.recommendedIntervention === "change_model" && currentAdapter.withModel) {
868
- const fallbackModels = input.fallbackModels ?? DEFAULT_FALLBACK_MODELS;
869
- const nextModel = fallbackModels[loop.attempts.length % fallbackModels.length];
870
- if (nextModel) {
871
- currentAdapter = currentAdapter.withModel(nextModel);
872
- }
873
- }
874
862
  loop = appendLoopEvent(loop, {
875
863
  type: "failure.classified",
876
864
  lifecycleState: "running",
@@ -1138,12 +1126,17 @@ export async function runMartin(input) {
1138
1126
  // Uses the task's repoRoot to build/load the grounding index, then scans any diff
1139
1127
  let groundingScanResult;
1140
1128
  const patchDiff = buildPatchDiff(result, changedFiles);
1141
- if (patchDiff && input.task.repoRoot) {
1129
+ const workspaceMutationCapable = executingAdapter.metadata.capabilities?.workspaceMutations !== false;
1130
+ const hasReportedOrObservedChanges = changedFiles.length > 0 || (result.execution?.changedFiles?.length ?? 0) > 0;
1131
+ const groundingEvidenceRequired = result.status === "completed" && workspaceMutationCapable && hasReportedOrObservedChanges;
1132
+ let groundingEvidenceAvailable = !groundingEvidenceRequired;
1133
+ if (groundingEvidenceRequired && patchDiff && input.task.repoRoot) {
1142
1134
  try {
1143
1135
  const groundingIndex = await loadOrBuildRepoGroundingIndex(input.task.repoRoot);
1144
1136
  groundingScanResult = reconcileExistingGroundingFiles(scanPatchForGroundingViolations(patchDiff, groundingIndex, {
1145
1137
  allowedPaths: input.task.allowedPaths
1146
1138
  }), input.task.repoRoot, rollbackBoundary);
1139
+ groundingEvidenceAvailable = true;
1147
1140
  if (input.store && groundingScanResult.violations.length > 0) {
1148
1141
  await input.store.appendLedger(loop.loopId, makeLedgerEvent({
1149
1142
  kind: "grounding.violations_found",
@@ -1159,10 +1152,22 @@ export async function runMartin(input) {
1159
1152
  }
1160
1153
  }
1161
1154
  catch {
1162
- // Grounding scan is best-effort — never fail the loop because of a scan error
1155
+ // Availability is persisted below so scanner failures and missing
1156
+ // grounding inputs follow the same recover-forward trust contract.
1163
1157
  }
1164
1158
  }
1165
- const shouldEvaluatePatchTruth = result.status === "completed" && tracksWorkspaceMutations;
1159
+ if (groundingEvidenceRequired && !groundingEvidenceAvailable) {
1160
+ // The completed work remains useful, but unavailable grounding evidence
1161
+ // cannot be converted into a zero-violation governance claim.
1162
+ loop = {
1163
+ ...loop,
1164
+ metadata: {
1165
+ ...loop.metadata,
1166
+ groundingEvidenceStatus: "unavailable"
1167
+ }
1168
+ };
1169
+ }
1170
+ const shouldEvaluatePatchTruth = result.status === "completed" && tracksWorkspaceMutations && groundingEvidenceAvailable;
1166
1171
  let patchDecision;
1167
1172
  if (shouldEvaluatePatchTruth) {
1168
1173
  patchDecision = evaluatePatchDecision({
@@ -1548,7 +1553,8 @@ function getUsageProvenance(usage) {
1548
1553
  const COST_PROVENANCE_RANK = {
1549
1554
  unavailable: 0,
1550
1555
  estimated: 1,
1551
- actual: 2
1556
+ calculated: 2,
1557
+ actual: 3
1552
1558
  };
1553
1559
  /**
1554
1560
  * Aggregates cost provenance across attempts. The cumulative loop provenance
@@ -6,6 +6,7 @@
6
6
  * - legacy root JSONL files: <runsRoot>/*.jsonl
7
7
  * - canonical run trees: <runsRoot>/<loopId>/loop-record.json
8
8
  */
9
+ import type { CostProvenance } from "../../contracts/index.js";
9
10
  export interface LoopAttemptRecord {
10
11
  index: number;
11
12
  model?: string;
@@ -32,6 +33,7 @@ export interface LoopRunRecord {
32
33
  tokensIn: number;
33
34
  tokensOut: number;
34
35
  avoidedUsd?: number;
36
+ provenance?: CostProvenance;
35
37
  };
36
38
  attempts: LoopAttemptRecord[];
37
39
  task: {
@@ -152,14 +152,14 @@ export function classifyFailure(input) {
152
152
  failureClass: "logic_error",
153
153
  rationale: "Oscillating failure pattern detected — switching strategy to break the cycle.",
154
154
  retryable: true,
155
- recommendedIntervention: "change_model"
155
+ recommendedIntervention: "compress_context"
156
156
  };
157
157
  }
158
158
  return {
159
159
  failureClass: repeatedFailure ?? "logic_error",
160
160
  rationale: "The loop is still failing to produce a correct implementation.",
161
161
  retryable: true,
162
- recommendedIntervention: repeatedFailure === "logic_error" ? "change_model" : "compress_context"
162
+ recommendedIntervention: "compress_context"
163
163
  };
164
164
  }
165
165
  export function evaluateCostGovernor(input) {
@@ -318,7 +318,7 @@ function mapClassHintToAssessment(classHint, attempts) {
318
318
  failureClass: "hallucination",
319
319
  rationale: "Structural evidence: suspiciously short or trivial agent response.",
320
320
  retryable: true,
321
- recommendedIntervention: repeatedFailure === "hallucination" ? "change_model" : "run_verifier"
321
+ recommendedIntervention: repeatedFailure === "hallucination" ? "compress_context" : "run_verifier"
322
322
  };
323
323
  case "verification_failure":
324
324
  case "test_regression":
@@ -333,7 +333,7 @@ function mapClassHintToAssessment(classHint, attempts) {
333
333
  failureClass: classHint,
334
334
  rationale: "Structural hint provided by adapter.",
335
335
  retryable: true,
336
- recommendedIntervention: repeatedFailure ? "change_model" : "compress_context"
336
+ recommendedIntervention: "compress_context"
337
337
  };
338
338
  }
339
339
  }
@@ -472,7 +472,7 @@ export function selectRecoveryRecipe(evidence) {
472
472
  return {
473
473
  recipe: "strategy_swap",
474
474
  rationale: "Very low diff novelty across repeated retries. Swap strategy.",
475
- intervention: "change_model"
475
+ intervention: "compress_context"
476
476
  };
477
477
  }
478
478
  return {
@@ -485,7 +485,7 @@ export function selectRecoveryRecipe(evidence) {
485
485
  return {
486
486
  recipe: "downgrade_model",
487
487
  rationale: "Cost efficiency is degrading or retries are exhausted. Downgrade the model.",
488
- intervention: "change_model"
488
+ intervention: "compress_context"
489
489
  };
490
490
  }
491
491
  return {
@@ -20,6 +20,7 @@ export interface MockModelClient {
20
20
  findings: RedFinding[];
21
21
  tokensUsed: number;
22
22
  costUsd: number;
23
+ model?: string;
23
24
  }>;
24
25
  }
25
26
  export interface RunRedPhaseOptions {
@@ -47,7 +48,7 @@ export interface PatchInput {
47
48
  *
48
49
  * - baseline: programmatic probes only, no model call
49
50
  * - high_risk: paranoid programmatic scan, no model call
50
- * - release_critical: paranoid scan + one Haiku model call
51
+ * - release_critical: paranoid scan + one configured model-client call
51
52
  */
52
53
  export declare function runRedPhase(patch: PatchInput, tier: RiskTier, blueBudgetUsd: number, options?: RunRedPhaseOptions): Promise<RedFindings>;
53
54
  /**
@@ -1,4 +1,4 @@
1
- import { PROBE_COUNTS, RED_PHASE_MODEL, resolveRedBudgetPolicy } from "./risk-tiers.js";
1
+ import { PROBE_COUNTS, resolveRedBudgetPolicy } from "./risk-tiers.js";
2
2
  // ─── Programmatic probes ──────────────────────────────────────────────────────
3
3
  function runProgrammaticProbes(patch, paranoid) {
4
4
  const findings = [];
@@ -62,7 +62,7 @@ function runProgrammaticProbes(patch, paranoid) {
62
62
  *
63
63
  * - baseline: programmatic probes only, no model call
64
64
  * - high_risk: paranoid programmatic scan, no model call
65
- * - release_critical: paranoid scan + one Haiku model call
65
+ * - release_critical: paranoid scan + one configured model-client call
66
66
  */
67
67
  export async function runRedPhase(patch, tier, blueBudgetUsd, options = {}) {
68
68
  const policy = resolveRedBudgetPolicy(tier, blueBudgetUsd);
@@ -77,7 +77,7 @@ export async function runRedPhase(patch, tier, blueBudgetUsd, options = {}) {
77
77
  const result = await options.modelClient.complete(prompt);
78
78
  findings = [...findings, ...result.findings];
79
79
  modelCallMade = true;
80
- modelUsed = RED_PHASE_MODEL;
80
+ modelUsed = result.model;
81
81
  budgetUsedUsd += result.costUsd;
82
82
  }
83
83
  const result = {
@@ -4,7 +4,7 @@ export interface RedBudgetPolicy {
4
4
  blueBudgetUsd: number;
5
5
  /** Cap on Red phase spend: 30% / 100% / 150% of Blue */
6
6
  redBudgetCapUsd: number;
7
- /** Only release_critical permits a Haiku model call */
7
+ /** Only release_critical permits an optional configured model-client call. */
8
8
  modelCallAllowed: boolean;
9
9
  }
10
10
  /**
@@ -15,8 +15,6 @@ export declare function resolveRedBudgetPolicy(tier: RiskTier, blueBudgetUsd: nu
15
15
  * Probe counts per tier.
16
16
  * baseline = standard 6-probe sweep
17
17
  * high_risk = paranoid 12-probe sweep
18
- * release_critical = paranoid 12-probe sweep + model
18
+ * release_critical = paranoid 12-probe sweep + configured model client
19
19
  */
20
20
  export declare const PROBE_COUNTS: Record<RiskTier, number>;
21
- /** The only model ever permitted in the Red phase. */
22
- export declare const RED_PHASE_MODEL: "claude-haiku-4-5-20251001";
@@ -21,12 +21,10 @@ export function resolveRedBudgetPolicy(tier, blueBudgetUsd) {
21
21
  * Probe counts per tier.
22
22
  * baseline = standard 6-probe sweep
23
23
  * high_risk = paranoid 12-probe sweep
24
- * release_critical = paranoid 12-probe sweep + model
24
+ * release_critical = paranoid 12-probe sweep + configured model client
25
25
  */
26
26
  export const PROBE_COUNTS = {
27
27
  baseline: 6,
28
28
  high_risk: 12,
29
29
  release_critical: 12
30
30
  };
31
- /** The only model ever permitted in the Red phase. */
32
- export const RED_PHASE_MODEL = "claude-haiku-4-5-20251001";
@@ -15,14 +15,6 @@ export interface RouteDecision {
15
15
  blockedSteps: string[];
16
16
  compressed: boolean;
17
17
  compressionSummary?: string;
18
- /**
19
- * Recommended model tier based on task complexity and route.
20
- * Follows cost-aware routing: direct → haiku/sonnet, manager → sonnet,
21
- * consensus → opus. Never use a more expensive model than the task needs.
22
- */
23
- recommendedModelTier: "haiku" | "sonnet" | "opus";
24
- /** Estimated cost saving vs always using sonnet */
25
- estimatedSavingVsSonnetUsd: number;
26
18
  }
27
19
  export interface RouteClassificationInput {
28
20
  objective: string;
@@ -37,40 +29,6 @@ export interface RouteClassificationInput {
37
29
  /** Historical success rate for similar tasks with direct execution (0-1). */
38
30
  historicalDirectSuccessRate?: number;
39
31
  }
40
- /**
41
- * Resolve a tier to a concrete model ID for a specific engine.
42
- * Allows downstream adapters to switch models automatically.
43
- */
44
- export declare function resolveModelForTier(tier: RouteDecision["recommendedModelTier"], engine: "claude" | "codex" | "gemini" | "openai" | string): string;
45
- /**
46
- * Available engine registry — describes what's installed and its capability.
47
- * Populated by doctor/preflight from PATH and env var detection.
48
- */
49
- export interface AvailableEngine {
50
- id: string;
51
- available: boolean;
52
- /** Cost tier: cheap < mid < expensive */
53
- costTier: "cheap" | "mid" | "expensive";
54
- /** Capability tier: determines which task types this engine can handle */
55
- capabilityTier: "haiku" | "sonnet" | "opus";
56
- }
57
- /**
58
- * Select the best available engine for a given tier requirement.
59
- *
60
- * When multiple engines are available, picks the cheapest that meets
61
- * the capability requirement. Falls back gracefully:
62
- * - If required tier unavailable, upgrades to next tier of same engine
63
- * - If only one engine family available, uses its best equivalent
64
- *
65
- * @param requiredTier - Minimum capability tier needed
66
- * @param availableEngines - What's installed and authenticated
67
- * @returns Best engine ID and concrete model to use
68
- */
69
- export declare function selectBestEngine(requiredTier: RouteDecision["recommendedModelTier"], availableEngines: AvailableEngine[]): {
70
- engineId: string;
71
- model: string;
72
- reasoning: string;
73
- };
74
32
  export declare function classifyRoute(input: RouteClassificationInput): RouteDecision;
75
33
  /**
76
34
  * Checks whether a run's Pre Work Burn exceeds the routing policy limits.
@@ -84,144 +84,6 @@ function calculateDirectConfidence(signals, input) {
84
84
  // ---------------------------------------------------------------------------
85
85
  // Public API
86
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
87
  export function classifyRoute(input) {
226
88
  const policy = { ...DEFAULT_ROUTING_POLICY, ...input.policy };
227
89
  const signals = extractComplexitySignals(input);
@@ -230,7 +92,6 @@ export function classifyRoute(input) {
230
92
  const blockedSteps = [];
231
93
  // Policy override: forced mode
232
94
  if (policy.mode === "direct") {
233
- const tier = selectModelTier("direct");
234
95
  return {
235
96
  selectedMode: "direct",
236
97
  confidence: 1,
@@ -239,13 +100,10 @@ export function classifyRoute(input) {
239
100
  reason: ["Policy forces direct execution mode."],
240
101
  blockedSteps: ["manager", "planner", "router", "consensus"],
241
102
  compressed: true,
242
- compressionSummary: "Policy: direct mode — skipping all orchestration.",
243
- recommendedModelTier: tier,
244
- estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
103
+ compressionSummary: "Policy: direct mode — skipping all orchestration."
245
104
  };
246
105
  }
247
106
  if (policy.mode === "consensus") {
248
- const tier = selectModelTier("consensus");
249
107
  return {
250
108
  selectedMode: "consensus",
251
109
  confidence: 0.5,
@@ -253,9 +111,7 @@ export function classifyRoute(input) {
253
111
  expectedPreworkBurnPct: 35,
254
112
  reason: ["Policy forces consensus mode."],
255
113
  blockedSteps: [],
256
- compressed: false,
257
- recommendedModelTier: tier,
258
- estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
114
+ compressed: false
259
115
  };
260
116
  }
261
117
  // Adaptive routing
@@ -268,7 +124,6 @@ export function classifyRoute(input) {
268
124
  if (signals.scopeRestricted)
269
125
  reason.push("Path scope is restricted.");
270
126
  blockedSteps.push("manager", "consensus");
271
- const tier = selectModelTier("direct");
272
127
  return {
273
128
  selectedMode: "direct",
274
129
  confidence: directConfidence,
@@ -277,9 +132,7 @@ export function classifyRoute(input) {
277
132
  reason,
278
133
  blockedSteps,
279
134
  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)
135
+ compressionSummary: `Compressed from manager→planner→worker to direct worker. Estimated savings: ${String(Math.round(input.budgetUsd * 0.3 * 100) / 100)} USD.`
283
136
  };
284
137
  }
285
138
  // Complex task — needs orchestration
@@ -293,7 +146,6 @@ export function classifyRoute(input) {
293
146
  reason.push(`Touches ${String(signals.fileCount)}+ files.`);
294
147
  const needsConsensus = signals.hasSecurityKeywords && signals.hasMigrationKeywords;
295
148
  const mode = needsConsensus ? "consensus" : "manager";
296
- const tier = selectModelTier(mode);
297
149
  return {
298
150
  selectedMode: mode,
299
151
  confidence: directConfidence,
@@ -301,9 +153,7 @@ export function classifyRoute(input) {
301
153
  expectedPreworkBurnPct: needsConsensus ? 40 : 25,
302
154
  reason,
303
155
  blockedSteps: needsConsensus ? [] : ["consensus"],
304
- compressed: false,
305
- recommendedModelTier: tier,
306
- estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
156
+ compressed: false
307
157
  };
308
158
  }
309
159
  function estimateDirectCost(budgetUsd) {
@@ -1,4 +1,4 @@
1
- import type { LoopRecord, ReceiptIntegritySummary, TestIntegrityStatus, TestIntegrityVerdict, VerifiedHandoffOutcome, VerifiedHandoffRecoveryV1, VerifiedHandoffRequirementV1, VerifiedHandoffScopeV1, VerifiedHandoffTestIntegrityV1, VerifiedHandoffV1 } from "../contracts/index.js";
1
+ import type { ExecutionMode, LoopRecord, ReceiptIntegritySummary, TestIntegrityStatus, TestIntegrityVerdict, VerifiedHandoffOutcome, VerifiedHandoffRecoveryV1, VerifiedHandoffRequirementV1, VerifiedHandoffScopeV1, VerifiedHandoffTestIntegrityV1, VerifiedHandoffV1 } from "../contracts/index.js";
2
2
  export declare function toTestIntegrityVerdict(status: TestIntegrityStatus): TestIntegrityVerdict;
3
3
  export interface VerifierExecutionBinding {
4
4
  runId: string;
@@ -30,8 +30,13 @@ export declare function resolveVerifiedHandoffOutcome(input: {
30
30
  changedFileCount?: number;
31
31
  definitionOfDonePreSatisfied?: boolean;
32
32
  evidenceContradicted?: boolean;
33
+ governanceClaimEligible?: boolean;
33
34
  unresolvedWorkCount: number;
34
35
  }): VerifiedHandoffOutcome;
36
+ export declare function deriveVerifiedHandoffExecutionBoundary(loop: LoopRecord): {
37
+ executionMode: ExecutionMode;
38
+ governanceClaimEligible: boolean;
39
+ };
35
40
  export interface BuildVerifiedHandoffInput {
36
41
  loop: LoopRecord;
37
42
  generatedAt?: string;
@@ -57,6 +57,7 @@ export function resolveVerifiedHandoffOutcome(input) {
57
57
  return "STOPPED";
58
58
  }
59
59
  if (input.executionStatus === "failed" ||
60
+ input.governanceClaimEligible === false ||
60
61
  input.evidenceContradicted === true ||
61
62
  (input.mutationRequired === true &&
62
63
  (input.changedFileCount ?? 0) === 0 &&
@@ -80,6 +81,29 @@ export function resolveVerifiedHandoffOutcome(input) {
80
81
  }
81
82
  return "NEEDS_REVIEW";
82
83
  }
84
+ const SIMULATED_ADAPTER_PATTERN = /(?:^|:)(?:stub|fixture|synthetic)(?::|$)/u;
85
+ function isExecutionMode(value) {
86
+ return value === "governed" || value === "verification_only" || value === "simulated";
87
+ }
88
+ export function deriveVerifiedHandoffExecutionBoundary(loop) {
89
+ const adapterIds = loop.attempts.map((attempt) => attempt.adapterId);
90
+ const hasSimulatedAdapter = adapterIds.some((adapterId) => adapterId === "direct:proof:no-mutation" ||
91
+ SIMULATED_ADAPTER_PATTERN.test(adapterId));
92
+ const hasVerifierOnlyAdapter = adapterIds.some((adapterId) => adapterId === "direct:verifier:verify-only");
93
+ const persistedMode = isExecutionMode(loop.metadata["executionMode"])
94
+ ? loop.metadata["executionMode"]
95
+ : undefined;
96
+ const groundingEvidenceAvailable = loop.metadata["groundingEvidenceStatus"] !== "unavailable";
97
+ const executionMode = hasSimulatedAdapter
98
+ ? "simulated"
99
+ : hasVerifierOnlyAdapter
100
+ ? "verification_only"
101
+ : persistedMode ?? (adapterIds.length > 0 ? "governed" : "simulated");
102
+ return {
103
+ executionMode,
104
+ governanceClaimEligible: executionMode === "governed" && adapterIds.length > 0 && groundingEvidenceAvailable,
105
+ };
106
+ }
83
107
  function toEvidenceStatus(status) {
84
108
  switch (status) {
85
109
  case "passed":
@@ -132,6 +156,7 @@ export function buildVerifiedHandoff(input) {
132
156
  summary: input.testIntegrity?.summary ??
133
157
  "Test integrity was not evaluated for this run.",
134
158
  };
159
+ const executionBoundary = deriveVerifiedHandoffExecutionBoundary(input.loop);
135
160
  const outcome = resolveVerifiedHandoffOutcome({
136
161
  lifecycleState: input.loop.lifecycleState,
137
162
  executionStatus: input.executionStatus,
@@ -143,6 +168,7 @@ export function buildVerifiedHandoff(input) {
143
168
  changedFileCount: scope.changedFiles.length,
144
169
  definitionOfDonePreSatisfied: input.definitionOfDonePreSatisfied,
145
170
  evidenceContradicted: input.evidenceContradicted ?? input.verification.status === "contradicted",
171
+ governanceClaimEligible: executionBoundary.governanceClaimEligible,
146
172
  unresolvedWorkCount: unresolvedWork.length,
147
173
  });
148
174
  const handoffId = `vh_${createHash("sha256")
@@ -163,6 +189,8 @@ export function buildVerifiedHandoff(input) {
163
189
  verificationPlan: input.loop.task.verificationPlan,
164
190
  },
165
191
  outcome,
192
+ executionMode: executionBoundary.executionMode,
193
+ governanceClaimEligible: executionBoundary.governanceClaimEligible,
166
194
  sourceStatus: {
167
195
  status: input.loop.status,
168
196
  lifecycleState: input.loop.lifecycleState,