@martinloop/mcp 0.3.9 → 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 (113) hide show
  1. package/README.md +82 -141
  2. package/dist/discovery-metadata.d.ts +1 -1
  3. package/dist/discovery-metadata.js +1 -1
  4. package/dist/resources.d.ts +1 -2
  5. package/dist/resources.js +62 -98
  6. package/dist/server-validation.d.ts +1 -1
  7. package/dist/server-validation.js +16 -11
  8. package/dist/server.d.ts +23 -0
  9. package/dist/server.js +192 -15
  10. package/dist/tools/doctor.js +5 -4
  11. package/dist/tools/get-status.d.ts +2 -0
  12. package/dist/tools/get-status.js +1 -0
  13. package/dist/tools/preflight.d.ts +2 -0
  14. package/dist/tools/preflight.js +43 -3
  15. package/dist/tools/run-dossier.d.ts +3 -1
  16. package/dist/tools/run-dossier.js +40 -2
  17. package/dist/tools/run-loop.d.ts +5 -3
  18. package/dist/tools/run-loop.js +15 -9
  19. package/dist/tools/tool-errors.js +1 -1
  20. package/dist/tools/tool-response.js +9 -2
  21. package/dist/tools/tool-support.d.ts +4 -2
  22. package/dist/tools/tool-support.js +6 -15
  23. package/dist/vendor/adapters/claude-cli.d.ts +2 -2
  24. package/dist/vendor/adapters/claude-cli.js +97 -69
  25. package/dist/vendor/adapters/cli-bridge.d.ts +9 -1
  26. package/dist/vendor/adapters/cli-bridge.js +54 -13
  27. package/dist/vendor/adapters/codex-launcher.d.ts +61 -1
  28. package/dist/vendor/adapters/codex-launcher.js +118 -19
  29. package/dist/vendor/adapters/index.d.ts +2 -4
  30. package/dist/vendor/adapters/index.js +2 -4
  31. package/dist/vendor/adapters/openai-compatible.d.ts +2 -3
  32. package/dist/vendor/adapters/openai-compatible.js +80 -15
  33. package/dist/vendor/adapters/verifier-only.js +9 -4
  34. package/dist/vendor/contracts/adapter-capabilities.d.ts +1 -1
  35. package/dist/vendor/contracts/context-handoff.d.ts +100 -0
  36. package/dist/vendor/contracts/context-handoff.js +11 -0
  37. package/dist/vendor/contracts/context-manifest.d.ts +195 -0
  38. package/dist/vendor/contracts/context-manifest.js +17 -0
  39. package/dist/vendor/contracts/context-shadow.d.ts +75 -0
  40. package/dist/vendor/contracts/context-shadow.js +8 -0
  41. package/dist/vendor/contracts/delivery.d.ts +47 -0
  42. package/dist/vendor/contracts/delivery.js +18 -0
  43. package/dist/vendor/contracts/exits.d.ts +126 -0
  44. package/dist/vendor/contracts/exits.js +32 -0
  45. package/dist/vendor/contracts/index.d.ts +55 -6
  46. package/dist/vendor/contracts/index.js +24 -0
  47. package/dist/vendor/contracts/mission.d.ts +102 -0
  48. package/dist/vendor/contracts/mission.js +64 -0
  49. package/dist/vendor/contracts/operator.d.ts +3 -1
  50. package/dist/vendor/contracts/operator.js +2 -1
  51. package/dist/vendor/contracts/verified-handoff.d.ts +106 -0
  52. package/dist/vendor/contracts/verified-handoff.js +40 -0
  53. package/dist/vendor/core/context-chain-gate.d.ts +89 -0
  54. package/dist/vendor/core/context-chain-gate.js +173 -0
  55. package/dist/vendor/core/context-compiler.d.ts +53 -0
  56. package/dist/vendor/core/context-compiler.js +261 -0
  57. package/dist/vendor/core/context-handoff.d.ts +42 -0
  58. package/dist/vendor/core/context-handoff.js +167 -0
  59. package/dist/vendor/core/context-shadow.d.ts +39 -0
  60. package/dist/vendor/core/context-shadow.js +200 -0
  61. package/dist/vendor/core/delivery/index.d.ts +6 -0
  62. package/dist/vendor/core/delivery/index.js +4 -0
  63. package/dist/vendor/core/delivery/message-client.d.ts +18 -0
  64. package/dist/vendor/core/delivery/message-client.js +38 -0
  65. package/dist/vendor/core/delivery/message-ledger.d.ts +35 -0
  66. package/dist/vendor/core/delivery/message-ledger.js +97 -0
  67. package/dist/vendor/core/delivery/message-schema.d.ts +16 -0
  68. package/dist/vendor/core/delivery/message-schema.js +111 -0
  69. package/dist/vendor/core/delivery/update-check.d.ts +19 -0
  70. package/dist/vendor/core/delivery/update-check.js +107 -0
  71. package/dist/vendor/core/exit-signal.d.ts +88 -0
  72. package/dist/vendor/core/exit-signal.js +339 -0
  73. package/dist/vendor/core/exits.d.ts +41 -0
  74. package/dist/vendor/core/exits.js +258 -0
  75. package/dist/vendor/core/grounding.d.ts +2 -1
  76. package/dist/vendor/core/grounding.js +40 -5
  77. package/dist/vendor/core/index.d.ts +47 -6
  78. package/dist/vendor/core/index.js +1102 -670
  79. package/dist/vendor/core/mission/aggregate.d.ts +27 -0
  80. package/dist/vendor/core/mission/aggregate.js +42 -0
  81. package/dist/vendor/core/mission/index.d.ts +2 -0
  82. package/dist/vendor/core/mission/index.js +1 -0
  83. package/dist/vendor/core/persistence/compiler.d.ts +8 -0
  84. package/dist/vendor/core/persistence/compiler.js +156 -0
  85. package/dist/vendor/core/persistence/context-handoff-store.d.ts +68 -0
  86. package/dist/vendor/core/persistence/context-handoff-store.js +112 -0
  87. package/dist/vendor/core/persistence/index.d.ts +4 -0
  88. package/dist/vendor/core/persistence/index.js +2 -0
  89. package/dist/vendor/core/persistence/ledger.d.ts +1 -1
  90. package/dist/vendor/core/persistence/mission-store.d.ts +56 -0
  91. package/dist/vendor/core/persistence/mission-store.js +281 -0
  92. package/dist/vendor/core/persistence/runs-reader.d.ts +2 -0
  93. package/dist/vendor/core/policy.d.ts +2 -0
  94. package/dist/vendor/core/policy.js +11 -8
  95. package/dist/vendor/core/red-blue/red-phase.d.ts +2 -1
  96. package/dist/vendor/core/red-blue/red-phase.js +3 -3
  97. package/dist/vendor/core/red-blue/risk-tiers.d.ts +2 -4
  98. package/dist/vendor/core/red-blue/risk-tiers.js +1 -3
  99. package/dist/vendor/core/rollback.js +15 -1
  100. package/dist/vendor/core/routing.d.ts +0 -42
  101. package/dist/vendor/core/routing.js +4 -154
  102. package/dist/vendor/core/savings.d.ts +13 -0
  103. package/dist/vendor/core/savings.js +22 -0
  104. package/dist/vendor/core/termination-store.d.ts +15 -0
  105. package/dist/vendor/core/termination-store.js +44 -0
  106. package/dist/vendor/core/verified-handoff.d.ts +72 -0
  107. package/dist/vendor/core/verified-handoff.js +238 -0
  108. package/package.json +4 -10
  109. package/server.json +3 -3
  110. package/dist/vendor/adapters/stub-agent-cli.d.ts +0 -8
  111. package/dist/vendor/adapters/stub-agent-cli.js +0 -40
  112. package/dist/vendor/adapters/stub-direct-provider.d.ts +0 -8
  113. package/dist/vendor/adapters/stub-direct-provider.js +0 -12
@@ -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) {
@@ -0,0 +1,13 @@
1
+ import type { LoopRecord } from "../contracts/index.js";
2
+ import type { ExitDecision } from "./policy.js";
3
+ export interface AvoidedUsdInput {
4
+ lifecycleState: LoopRecord["lifecycleState"];
5
+ actualUsd: number;
6
+ uncontrolledBaselineUsd?: number;
7
+ }
8
+ export declare function calculateAvoidedUsd(input: AvoidedUsdInput): number;
9
+ export declare function calculateLoopAvoidedUsd(input: {
10
+ loop: LoopRecord;
11
+ decision: ExitDecision;
12
+ uncontrolledBaselineUsd?: number;
13
+ }): number;
@@ -0,0 +1,22 @@
1
+ export function calculateAvoidedUsd(input) {
2
+ if (input.lifecycleState !== "completed")
3
+ return 0;
4
+ if (!Number.isFinite(input.actualUsd) || input.actualUsd < 0)
5
+ return 0;
6
+ if (input.uncontrolledBaselineUsd === undefined ||
7
+ !Number.isFinite(input.uncontrolledBaselineUsd) ||
8
+ input.uncontrolledBaselineUsd <= input.actualUsd) {
9
+ return 0;
10
+ }
11
+ return roundUsd(input.uncontrolledBaselineUsd - input.actualUsd);
12
+ }
13
+ export function calculateLoopAvoidedUsd(input) {
14
+ return calculateAvoidedUsd({
15
+ lifecycleState: input.decision.lifecycleState,
16
+ actualUsd: input.loop.cost.actualUsd,
17
+ uncontrolledBaselineUsd: input.uncontrolledBaselineUsd
18
+ });
19
+ }
20
+ function roundUsd(value) {
21
+ return Math.round(value * 100) / 100;
22
+ }
@@ -0,0 +1,15 @@
1
+ import type { TerminationEnvelopeV1 } from "../contracts/index.js";
2
+ /**
3
+ * Atomically persist the termination envelope for a governed run.
4
+ *
5
+ * Crash-safe write sequence (mirrors exit-signal.ts atomic publication):
6
+ * 1. Write complete JSON to a unique tmp file (wx, 0o600) in runDirectory
7
+ * 2. Sync and close the tmp file
8
+ * 3. Hard-link tmp → termination.json (EEXIST → slot already occupied)
9
+ * 4. On EEXIST, read and return the existing envelope
10
+ * 5. Unlink the tmp file in finally (best-effort)
11
+ *
12
+ * First terminal exit path wins — competing callers that hit EEXIST get back
13
+ * the envelope that won the race. This is the A1 idempotency guarantee.
14
+ */
15
+ export declare function persistTerminationEnvelope(runDirectory: string, envelope: TerminationEnvelopeV1): Promise<TerminationEnvelopeV1>;
@@ -0,0 +1,44 @@
1
+ import { link, open, readFile, unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ /**
5
+ * Atomically persist the termination envelope for a governed run.
6
+ *
7
+ * Crash-safe write sequence (mirrors exit-signal.ts atomic publication):
8
+ * 1. Write complete JSON to a unique tmp file (wx, 0o600) in runDirectory
9
+ * 2. Sync and close the tmp file
10
+ * 3. Hard-link tmp → termination.json (EEXIST → slot already occupied)
11
+ * 4. On EEXIST, read and return the existing envelope
12
+ * 5. Unlink the tmp file in finally (best-effort)
13
+ *
14
+ * First terminal exit path wins — competing callers that hit EEXIST get back
15
+ * the envelope that won the race. This is the A1 idempotency guarantee.
16
+ */
17
+ export async function persistTerminationEnvelope(runDirectory, envelope) {
18
+ const finalPath = join(runDirectory, "termination.json");
19
+ const tmpPath = join(runDirectory, `.termination-tmp-${randomUUID()}.json`);
20
+ const json = `${JSON.stringify(envelope, null, 2)}\n`;
21
+ let handle;
22
+ try {
23
+ handle = await open(tmpPath, "wx", 0o600);
24
+ await handle.writeFile(json, "utf8");
25
+ await handle.sync();
26
+ await handle.close();
27
+ handle = undefined;
28
+ try {
29
+ await link(tmpPath, finalPath);
30
+ return envelope;
31
+ }
32
+ catch (linkErr) {
33
+ if (linkErr.code !== "EEXIST")
34
+ throw linkErr;
35
+ // Another path won the race — read what's on disk and return it
36
+ const existing = await readFile(finalPath, "utf8");
37
+ return JSON.parse(existing);
38
+ }
39
+ }
40
+ finally {
41
+ await handle?.close().catch(() => undefined);
42
+ await unlink(tmpPath).catch(() => undefined);
43
+ }
44
+ }
@@ -0,0 +1,72 @@
1
+ import type { ExecutionMode, LoopRecord, ReceiptIntegritySummary, TestIntegrityStatus, TestIntegrityVerdict, VerifiedHandoffOutcome, VerifiedHandoffRecoveryV1, VerifiedHandoffRequirementV1, VerifiedHandoffScopeV1, VerifiedHandoffTestIntegrityV1, VerifiedHandoffV1 } from "../contracts/index.js";
2
+ export declare function toTestIntegrityVerdict(status: TestIntegrityStatus): TestIntegrityVerdict;
3
+ export interface VerifierExecutionBinding {
4
+ runId: string;
5
+ workspaceId: string;
6
+ cwd: string;
7
+ commands: string[];
8
+ }
9
+ export interface BoundVerifierEvidence {
10
+ passed: boolean;
11
+ binding?: VerifierExecutionBinding;
12
+ steps?: Array<{
13
+ command: string;
14
+ launched: boolean;
15
+ completed?: boolean;
16
+ crashed?: boolean;
17
+ exitCode?: number;
18
+ timedOut?: boolean;
19
+ }>;
20
+ }
21
+ export declare function verifierActuallyPassed(evidence: BoundVerifierEvidence | null | undefined, expected: VerifierExecutionBinding): boolean;
22
+ export declare function resolveVerifiedHandoffOutcome(input: {
23
+ lifecycleState: LoopRecord["lifecycleState"];
24
+ executionStatus?: "completed" | "write_blocked" | "sandbox_blocked" | "policy_rejected" | "failed";
25
+ verificationStatus: BuildVerifiedHandoffInput["verification"]["status"];
26
+ receiptIntegrity: ReceiptIntegritySummary["state"];
27
+ scopeStatus?: VerifiedHandoffScopeV1["status"];
28
+ testIntegrityStatus?: TestIntegrityStatus;
29
+ mutationRequired?: boolean;
30
+ changedFileCount?: number;
31
+ definitionOfDonePreSatisfied?: boolean;
32
+ evidenceContradicted?: boolean;
33
+ governanceClaimEligible?: boolean;
34
+ unresolvedWorkCount: number;
35
+ }): VerifiedHandoffOutcome;
36
+ export declare function deriveVerifiedHandoffExecutionBoundary(loop: LoopRecord): {
37
+ executionMode: ExecutionMode;
38
+ governanceClaimEligible: boolean;
39
+ };
40
+ export interface BuildVerifiedHandoffInput {
41
+ loop: LoopRecord;
42
+ generatedAt?: string;
43
+ receiptIntegrity: ReceiptIntegritySummary;
44
+ verification: {
45
+ status: "passed" | "failed" | "contradicted" | "not_run";
46
+ summary: string;
47
+ steps: Array<{
48
+ command: string;
49
+ launched: boolean;
50
+ completed?: boolean;
51
+ crashed?: boolean;
52
+ exitCode?: number;
53
+ timedOut?: boolean;
54
+ detail?: string;
55
+ }>;
56
+ warnings: string[];
57
+ binding?: VerifierExecutionBinding;
58
+ };
59
+ executionStatus?: "completed" | "write_blocked" | "sandbox_blocked" | "policy_rejected" | "failed";
60
+ mutationRequired?: boolean;
61
+ definitionOfDonePreSatisfied?: boolean;
62
+ evidenceContradicted?: boolean;
63
+ changedFiles?: string[];
64
+ scope?: Partial<VerifiedHandoffScopeV1>;
65
+ testIntegrity?: Partial<VerifiedHandoffTestIntegrityV1>;
66
+ requirements?: VerifiedHandoffRequirementV1[];
67
+ unresolvedWork?: string[];
68
+ stopReason?: string;
69
+ recovery?: Partial<VerifiedHandoffRecoveryV1>;
70
+ nextAction: string;
71
+ }
72
+ export declare function buildVerifiedHandoff(input: BuildVerifiedHandoffInput): VerifiedHandoffV1;
@@ -0,0 +1,238 @@
1
+ // SPDX-FileCopyrightText: MartinLoop contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { createHash } from "node:crypto";
4
+ // ---------------------------------------------------------------------------
5
+ // Mapping: granular TestIntegrityStatus → public TestIntegrityVerdict
6
+ //
7
+ // A new enum value MUST appear here or TypeScript compilation will fail,
8
+ // ensuring the mapping is always exhaustive.
9
+ // ---------------------------------------------------------------------------
10
+ export function toTestIntegrityVerdict(status) {
11
+ switch (status) {
12
+ case "UNCHANGED":
13
+ case "AUTHORIZED_CHANGE":
14
+ return "VERIFIED";
15
+ case "PREVENTED":
16
+ case "DETECTED_AND_ROLLED_BACK":
17
+ case "DETECTED_NEEDS_REVIEW":
18
+ return "TAMPERING_DETECTED";
19
+ case "NOT_EVALUATED":
20
+ return "NOT_EVALUATED";
21
+ }
22
+ }
23
+ // ---------------------------------------------------------------------------
24
+ // Outcome resolution — deterministic, no hidden defaults
25
+ // ---------------------------------------------------------------------------
26
+ const STOPPED_LIFECYCLE_STATES = new Set([
27
+ "budget_exit",
28
+ "diminishing_returns",
29
+ "stuck_exit",
30
+ "human_escalation",
31
+ ]);
32
+ export function verifierActuallyPassed(evidence, expected) {
33
+ if (!evidence?.passed || !evidence.binding || !evidence.steps) {
34
+ return false;
35
+ }
36
+ if (evidence.binding.runId !== expected.runId ||
37
+ evidence.binding.workspaceId !== expected.workspaceId ||
38
+ evidence.binding.cwd !== expected.cwd ||
39
+ JSON.stringify(evidence.binding.commands) !== JSON.stringify(expected.commands)) {
40
+ return false;
41
+ }
42
+ if (evidence.steps.length !== expected.commands.length) {
43
+ return false;
44
+ }
45
+ return evidence.steps.every((step, index) => step.command === expected.commands[index] &&
46
+ step.launched === true &&
47
+ step.completed === true &&
48
+ step.crashed === false &&
49
+ step.timedOut !== true &&
50
+ step.exitCode === 0);
51
+ }
52
+ export function resolveVerifiedHandoffOutcome(input) {
53
+ if (STOPPED_LIFECYCLE_STATES.has(input.lifecycleState) ||
54
+ input.executionStatus === "write_blocked" ||
55
+ input.executionStatus === "sandbox_blocked" ||
56
+ input.executionStatus === "policy_rejected") {
57
+ return "STOPPED";
58
+ }
59
+ if (input.executionStatus === "failed" ||
60
+ input.governanceClaimEligible === false ||
61
+ input.evidenceContradicted === true ||
62
+ (input.mutationRequired === true &&
63
+ (input.changedFileCount ?? 0) === 0 &&
64
+ input.definitionOfDonePreSatisfied !== true)) {
65
+ return "NEEDS_REVIEW";
66
+ }
67
+ const evidenceTrustworthy = input.receiptIntegrity === "verified";
68
+ const scopeAcceptable = input.scopeStatus === undefined ||
69
+ input.scopeStatus === "WITHIN_SCOPE" ||
70
+ input.scopeStatus === "NOT_EVALUATED";
71
+ const testIntegrityAcceptable = input.testIntegrityStatus === undefined ||
72
+ input.testIntegrityStatus === "UNCHANGED" ||
73
+ input.testIntegrityStatus === "AUTHORIZED_CHANGE" ||
74
+ input.testIntegrityStatus === "NOT_EVALUATED";
75
+ if (input.verificationStatus === "passed" &&
76
+ evidenceTrustworthy &&
77
+ scopeAcceptable &&
78
+ testIntegrityAcceptable &&
79
+ input.unresolvedWorkCount === 0) {
80
+ return "VERIFIED";
81
+ }
82
+ return "NEEDS_REVIEW";
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
+ }
107
+ function toEvidenceStatus(status) {
108
+ switch (status) {
109
+ case "passed":
110
+ return "PASSED";
111
+ case "failed":
112
+ return "FAILED";
113
+ case "contradicted":
114
+ return "CONTRADICTED";
115
+ case "not_run":
116
+ return "NOT_RUN";
117
+ }
118
+ }
119
+ function toCheck(step) {
120
+ const status = !step.launched
121
+ ? "NOT_RUN"
122
+ : step.timedOut
123
+ ? "FAILED"
124
+ : step.exitCode === 0
125
+ ? "PASSED"
126
+ : "FAILED";
127
+ return {
128
+ command: step.command,
129
+ status,
130
+ ...(step.exitCode !== undefined ? { exitCode: step.exitCode } : {}),
131
+ ...(step.timedOut !== undefined ? { timedOut: step.timedOut } : {}),
132
+ ...(step.detail ? { detail: step.detail } : {}),
133
+ };
134
+ }
135
+ // ---------------------------------------------------------------------------
136
+ // Main builder
137
+ // ---------------------------------------------------------------------------
138
+ export function buildVerifiedHandoff(input) {
139
+ const generatedAt = input.generatedAt ?? new Date().toISOString();
140
+ const changedFiles = input.changedFiles ?? [];
141
+ const unresolvedWork = input.unresolvedWork ?? [];
142
+ const scope = {
143
+ status: input.scope?.status ?? "NOT_EVALUATED",
144
+ allowedPaths: input.scope?.allowedPaths ?? input.loop.task.allowedPaths ?? [],
145
+ deniedPaths: input.scope?.deniedPaths ?? input.loop.task.deniedPaths ?? [],
146
+ changedFiles: input.scope?.changedFiles ?? changedFiles,
147
+ violations: input.scope?.violations ?? [],
148
+ };
149
+ const testIntegrityStatus = input.testIntegrity?.status ?? "NOT_EVALUATED";
150
+ const testIntegrity = {
151
+ verdict: toTestIntegrityVerdict(testIntegrityStatus),
152
+ status: testIntegrityStatus,
153
+ protectedPaths: input.testIntegrity?.protectedPaths ?? [],
154
+ changedProtectedPaths: input.testIntegrity?.changedProtectedPaths ?? [],
155
+ findings: input.testIntegrity?.findings ?? [],
156
+ summary: input.testIntegrity?.summary ??
157
+ "Test integrity was not evaluated for this run.",
158
+ };
159
+ const executionBoundary = deriveVerifiedHandoffExecutionBoundary(input.loop);
160
+ const outcome = resolveVerifiedHandoffOutcome({
161
+ lifecycleState: input.loop.lifecycleState,
162
+ executionStatus: input.executionStatus,
163
+ verificationStatus: input.verification.status,
164
+ receiptIntegrity: input.receiptIntegrity.state,
165
+ scopeStatus: scope.status,
166
+ testIntegrityStatus: testIntegrity.status,
167
+ mutationRequired: input.mutationRequired ?? input.loop.task.mutationMode === "edit",
168
+ changedFileCount: scope.changedFiles.length,
169
+ definitionOfDonePreSatisfied: input.definitionOfDonePreSatisfied,
170
+ evidenceContradicted: input.evidenceContradicted ?? input.verification.status === "contradicted",
171
+ governanceClaimEligible: executionBoundary.governanceClaimEligible,
172
+ unresolvedWorkCount: unresolvedWork.length,
173
+ });
174
+ const handoffId = `vh_${createHash("sha256")
175
+ .update(`${input.loop.loopId}:${generatedAt}`)
176
+ .digest("hex")
177
+ .slice(0, 16)}`;
178
+ return {
179
+ schemaVersion: "1.0.0",
180
+ handoffId,
181
+ loopId: input.loop.loopId,
182
+ generatedAt,
183
+ task: {
184
+ title: input.loop.task.title,
185
+ objective: input.loop.task.objective,
186
+ },
187
+ definitionOfDone: {
188
+ acceptanceCriteria: input.loop.task.acceptanceCriteria ?? [],
189
+ verificationPlan: input.loop.task.verificationPlan,
190
+ },
191
+ outcome,
192
+ executionMode: executionBoundary.executionMode,
193
+ governanceClaimEligible: executionBoundary.governanceClaimEligible,
194
+ sourceStatus: {
195
+ status: input.loop.status,
196
+ lifecycleState: input.loop.lifecycleState,
197
+ },
198
+ verification: {
199
+ status: toEvidenceStatus(input.verification.status),
200
+ summary: input.verification.summary,
201
+ checks: input.verification.steps.map(toCheck),
202
+ warnings: input.verification.warnings,
203
+ },
204
+ requirements: input.requirements ?? [],
205
+ scope,
206
+ testIntegrity,
207
+ unresolvedWork,
208
+ ...(input.stopReason ? { stopReason: input.stopReason } : {}),
209
+ recovery: {
210
+ rollbackBoundaryAvailable: input.recovery?.rollbackBoundaryAvailable ?? false,
211
+ rollbackAttempted: input.recovery?.rollbackAttempted ?? false,
212
+ ...(input.recovery?.rollbackSucceeded !== undefined
213
+ ? { rollbackSucceeded: input.recovery.rollbackSucceeded }
214
+ : {}),
215
+ ...(input.recovery?.isolatedRef
216
+ ? { isolatedRef: input.recovery.isolatedRef }
217
+ : {}),
218
+ ...(input.recovery?.nextCommand
219
+ ? { nextCommand: input.recovery.nextCommand }
220
+ : {}),
221
+ summary: input.recovery?.summary ??
222
+ "No explicit recovery evidence was supplied to the handoff builder.",
223
+ },
224
+ usage: {
225
+ attempts: input.loop.attempts.length,
226
+ actualUsd: input.loop.cost.actualUsd,
227
+ ...(input.loop.cost.estimatedUsd !== undefined
228
+ ? { estimatedUsd: input.loop.cost.estimatedUsd }
229
+ : {}),
230
+ tokensIn: input.loop.cost.tokensIn,
231
+ tokensOut: input.loop.cost.tokensOut,
232
+ costProvenance: input.loop.cost.provenance ??
233
+ "unavailable",
234
+ },
235
+ receiptIntegrity: input.receiptIntegrity,
236
+ nextAction: input.nextAction,
237
+ };
238
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@martinloop/mcp",
3
- "version": "0.3.9",
3
+ "version": "0.5.1",
4
4
  "mcpName": "io.github.Keesan12/martin-loop",
5
5
  "private": false,
6
6
  "type": "module",
7
- "description": "Governed MCP server for AI coding agents with budgets, verifier gates, and inspectable runs.",
7
+ "description": "MCP server for AI coding agents — verifier gates, stop limits, and Verified Handoffs.",
8
8
  "license": "Apache-2.0",
9
9
  "author": "MartinLoop contributors",
10
10
  "homepage": "https://martinloop.com/",
@@ -18,18 +18,11 @@
18
18
  },
19
19
  "keywords": [
20
20
  "mcp",
21
- "mcp-server",
22
21
  "model-context-protocol",
23
22
  "martin-loop",
24
23
  "ai-agent",
25
- "ai-governance",
26
- "agent-governance",
27
24
  "claude",
28
- "claude-code",
29
25
  "codex",
30
- "cursor",
31
- "budget-guardrails",
32
- "verifier-gates",
33
26
  "martin_doctor",
34
27
  "martin_triage_runs",
35
28
  "martin_run_dossier",
@@ -68,7 +61,8 @@
68
61
  "start": "node dist/server.js",
69
62
  "inspect:live": "node ./scripts/inspect-live.mjs",
70
63
  "mcpb:build": "node ./mcpb/build-mcpb.mjs",
71
- "mcpb:validate": "mcpb validate ./dist-mcpb/martinloop"
64
+ "mcpb:validate": "mcpb validate ./dist-mcpb/martinloop",
65
+ "mcpb:smoke": "node ./mcpb/smoke-mcpb.mjs"
72
66
  },
73
67
  "dependencies": {
74
68
  "@modelcontextprotocol/sdk": "^1.29.0",