@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
package/dist/server.js CHANGED
@@ -16,12 +16,14 @@
16
16
  * Manual start:
17
17
  * node dist/server.js
18
18
  */
19
+ import { createServer as createHttpServer } from "node:http";
19
20
  import { fileURLToPath } from "node:url";
20
21
  import { realpathSync } from "node:fs";
21
22
  import path from "node:path";
22
23
  import { resolveRunsRoot } from "./vendor/core/index.js";
23
24
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
24
25
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
26
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
25
27
  import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
26
28
  import { MARTIN_MCP_PACKAGE_VERSION } from "./package-version.js";
27
29
  import { getMartinPrompt, listMartinPrompts } from "./prompts.js";
@@ -53,6 +55,15 @@ const stringArraySchema = {
53
55
  type: "array",
54
56
  items: { type: "string" }
55
57
  };
58
+ function describeCostProvenance(provenance) {
59
+ if (provenance === "actual")
60
+ return "provider-settled actual";
61
+ if (provenance === "calculated")
62
+ return "calculated from observed usage";
63
+ if (provenance === "estimated")
64
+ return "estimated";
65
+ return "unavailable";
66
+ }
56
67
  const loopPreviewSchema = {
57
68
  type: "object",
58
69
  additionalProperties: true,
@@ -66,6 +77,10 @@ const loopPreviewSchema = {
66
77
  updatedAt: { type: "string" },
67
78
  attempts: { type: "integer" },
68
79
  costUsd: { type: "number" },
80
+ costProvenance: {
81
+ type: "string",
82
+ enum: ["actual", "calculated", "estimated", "unavailable"]
83
+ },
69
84
  avoidedUsd: { type: "number" },
70
85
  pressure: { type: "string" },
71
86
  shouldStop: { type: "boolean" },
@@ -85,6 +100,7 @@ const loopPreviewSchema = {
85
100
  "lifecycleState",
86
101
  "attempts",
87
102
  "costUsd",
103
+ "costProvenance",
88
104
  "avoidedUsd",
89
105
  "pressure",
90
106
  "shouldStop",
@@ -111,9 +127,13 @@ const costSchema = {
111
127
  actualUsd: { type: "number" },
112
128
  avoidedUsd: { type: "number" },
113
129
  tokensIn: { type: "integer" },
114
- tokensOut: { type: "integer" }
130
+ tokensOut: { type: "integer" },
131
+ provenance: {
132
+ type: "string",
133
+ enum: ["actual", "calculated", "estimated", "unavailable"]
134
+ }
115
135
  },
116
- required: ["actualUsd", "avoidedUsd", "tokensIn", "tokensOut"]
136
+ required: ["actualUsd", "avoidedUsd", "tokensIn", "tokensOut", "provenance"]
117
137
  };
118
138
  const verificationSchema = {
119
139
  type: "object",
@@ -246,6 +266,10 @@ const runOutputSchema = {
246
266
  reason: { type: "string" },
247
267
  attempts: { type: "integer" },
248
268
  costUsd: { type: "number" },
269
+ costProvenance: {
270
+ type: "string",
271
+ enum: ["actual", "calculated", "estimated", "unavailable"]
272
+ },
249
273
  verificationPassed: { type: "boolean" },
250
274
  loopId: { type: "string" },
251
275
  pressure: { type: "string" },
@@ -277,6 +301,7 @@ const runOutputSchema = {
277
301
  "reason",
278
302
  "attempts",
279
303
  "costUsd",
304
+ "costProvenance",
280
305
  "verificationPassed",
281
306
  "loopId",
282
307
  "pressure",
@@ -344,6 +369,10 @@ const statusOutputSchema = {
344
369
  lifecycleState: { type: "string" },
345
370
  attempts: { type: "integer" },
346
371
  costUsd: { type: "number" },
372
+ costProvenance: {
373
+ type: "string",
374
+ enum: ["actual", "calculated", "estimated", "unavailable"]
375
+ },
347
376
  avoidedUsd: { type: "number" },
348
377
  pressure: { type: "string" },
349
378
  shouldStop: { type: "boolean" },
@@ -367,6 +396,7 @@ const statusOutputSchema = {
367
396
  "lifecycleState",
368
397
  "attempts",
369
398
  "costUsd",
399
+ "costProvenance",
370
400
  "avoidedUsd",
371
401
  "pressure",
372
402
  "shouldStop",
@@ -1401,6 +1431,10 @@ export function createMartinMcpServer(serverInfo) {
1401
1431
  type: "array",
1402
1432
  items: { type: "string" },
1403
1433
  description: "Optional file paths to scope the estimate."
1434
+ },
1435
+ workingDirectory: {
1436
+ type: "string",
1437
+ description: "Optional workspace path for recording the estimate receipt against the same task root you plan to run."
1404
1438
  }
1405
1439
  },
1406
1440
  required: ["objective"]
@@ -1497,17 +1531,22 @@ export function createMartinMcpServer(serverInfo) {
1497
1531
  });
1498
1532
  }
1499
1533
  const output = await runLoopTool(input);
1500
- return createToolSuccessResult(output, `Run ${output.loopId} is ${output.status}/${output.lifecycleState} after ${output.attempts} attempt(s); spend ${output.costUsd.toFixed(2)} USD.`);
1534
+ return createToolSuccessResult(output, [
1535
+ `Run ${output.loopId} is ${output.status}/${output.lifecycleState}`,
1536
+ `after ${output.attempts} attempt(s); recorded cost ${output.costUsd.toFixed(2)} USD (${describeCostProvenance(output.costProvenance)}).`,
1537
+ `Execution mode: ${output.executionMode}; governance claim eligible:`,
1538
+ `${output.governanceClaimEligible ? "yes" : "no"}.`
1539
+ ].join(" "));
1501
1540
  }
1502
1541
  if (name === "martin_inspect") {
1503
1542
  const input = validateToolInput("martin_inspect", args);
1504
1543
  const output = await inspectLoopTool(input);
1505
- return createToolSuccessResult(output, `Inspected ${output.loopCount} run(s) from ${output.source}; total actual spend ${output.portfolio.totalActualUsd.toFixed(2)} USD.`);
1544
+ return createToolSuccessResult(output, `Inspected ${output.loopCount} run(s) from ${output.source}; total recorded cost ${output.portfolio.totalActualUsd.toFixed(2)} USD.`);
1506
1545
  }
1507
1546
  if (name === "martin_status") {
1508
1547
  const input = validateToolInput("martin_status", args);
1509
1548
  const output = await getStatusTool(input);
1510
- return createToolSuccessResult(output, `Loop ${output.loopId} is ${output.status}/${output.lifecycleState}; pressure is ${output.pressure} with ${output.remainingBudgetUsd.toFixed(2)} USD remaining.`);
1549
+ return createToolSuccessResult(output, `Loop ${output.loopId} is ${output.status}/${output.lifecycleState}; recorded cost is ${output.costUsd.toFixed(2)} USD (${describeCostProvenance(output.costProvenance)}), and pressure is ${output.pressure} with ${output.remainingBudgetUsd.toFixed(2)} USD remaining.`);
1511
1550
  }
1512
1551
  if (name === "martin_doctor") {
1513
1552
  const input = validateToolInput("martin_doctor", args);
@@ -1518,7 +1557,7 @@ export function createMartinMcpServer(serverInfo) {
1518
1557
  workingDirectory: output.environment.workingDirectory,
1519
1558
  engine: input.engine,
1520
1559
  receiptScope: output.receiptScope
1521
- });
1560
+ }).catch(() => { });
1522
1561
  return createToolSuccessResult(output, output.summary);
1523
1562
  }
1524
1563
  if (name === "martin_plan") {
@@ -1535,7 +1574,7 @@ export function createMartinMcpServer(serverInfo) {
1535
1574
  repoRoot: output.workingDirectory,
1536
1575
  runsRoot: resolveRunsRoot(process.env)
1537
1576
  }
1538
- });
1577
+ }).catch(() => { });
1539
1578
  return createToolSuccessResult(output, `Plan ready for ${output.objective} with ${output.risk.level} risk and ${output.approvalRecommendation.replace(/_/gu, " ")} approval.`);
1540
1579
  }
1541
1580
  if (name === "martin_preflight") {
@@ -1553,7 +1592,7 @@ export function createMartinMcpServer(serverInfo) {
1553
1592
  allowedPaths: output.normalized.allowedPaths,
1554
1593
  deniedPaths: output.normalized.deniedPaths,
1555
1594
  budget: output.normalized.budget
1556
- });
1595
+ }).catch(() => { });
1557
1596
  }
1558
1597
  return createToolSuccessResult(output, output.summary);
1559
1598
  }
@@ -1640,8 +1679,8 @@ export function createMartinMcpServer(serverInfo) {
1640
1679
  const engine = input.engine ?? "claude";
1641
1680
  const budgetUsd = input.budgetUsd ?? 5;
1642
1681
  const fileScope = input.fileScope ?? [];
1643
- const workingDirectory = resolveSafeRepoRoot();
1644
1682
  const runsRoot = resolveRunsRoot(process.env);
1683
+ const workingDirectory = input.workingDirectory ?? resolveSafeRepoRoot();
1645
1684
  const route = classifyRoute({
1646
1685
  objective,
1647
1686
  verificationPlan: [],
@@ -1665,17 +1704,22 @@ export function createMartinMcpServer(serverInfo) {
1665
1704
  compressed: route.compressed,
1666
1705
  ...(route.compressionSummary ? { compressionSummary: route.compressionSummary } : {}),
1667
1706
  recommendedBudgetUsd,
1668
- recommendedModelTier: route.recommendedModelTier,
1669
- estimatedSavingVsSonnetUsd: route.estimatedSavingVsSonnetUsd
1707
+ modelAuthority: "agent_or_provider_default"
1670
1708
  };
1671
1709
  await recordMcpWorkflowStep({
1672
1710
  runsRoot,
1673
1711
  step: "estimate",
1674
1712
  workingDirectory,
1675
1713
  objective,
1676
- engine
1677
- });
1678
- return createToolSuccessResult(output, `Estimate: ${route.selectedMode} route (${route.recommendedModelTier}), ~$${route.expectedCostUsd.toFixed(2)} expected cost, ${route.expectedPreworkBurnPct}% pre-work burn. Recommended budget: $${recommendedBudgetUsd.toFixed(2)}.`);
1714
+ engine,
1715
+ receiptScope: {
1716
+ invocationRoot: resolveSafeRepoRoot(),
1717
+ workingDirectory,
1718
+ repoRoot: workingDirectory,
1719
+ runsRoot
1720
+ }
1721
+ }).catch(() => { });
1722
+ return createToolSuccessResult(output, `Estimate: ${route.selectedMode} route, ~$${route.expectedCostUsd.toFixed(2)} expected cost, ${route.expectedPreworkBurnPct}% pre-work burn. Model authority remains with the agent/provider. Recommended budget: $${recommendedBudgetUsd.toFixed(2)}.`);
1679
1723
  }
1680
1724
  return createToolErrorResult(toToolFailure(new Error(`Unknown tool: ${name}`)));
1681
1725
  }
@@ -1698,6 +1742,114 @@ export async function connectMartinMcpStdioServer() {
1698
1742
  await server.connect(transport);
1699
1743
  return server;
1700
1744
  }
1745
+ export function parseMartinMcpServerArgs(argv) {
1746
+ let transport = "stdio";
1747
+ let host = "127.0.0.1";
1748
+ let port = 3033;
1749
+ let path = "/mcp";
1750
+ for (let index = 0; index < argv.length; index += 1) {
1751
+ const token = argv[index];
1752
+ if (token === "--http") {
1753
+ transport = "http";
1754
+ continue;
1755
+ }
1756
+ if (token === "--port") {
1757
+ const next = argv[index + 1];
1758
+ const parsed = Number(next);
1759
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
1760
+ throw new Error(`Invalid --port value '${next ?? ""}'.`);
1761
+ }
1762
+ port = parsed;
1763
+ index += 1;
1764
+ continue;
1765
+ }
1766
+ if (token === "--host") {
1767
+ const next = argv[index + 1];
1768
+ if (!next) {
1769
+ throw new Error("Missing value for --host.");
1770
+ }
1771
+ host = next;
1772
+ index += 1;
1773
+ continue;
1774
+ }
1775
+ if (token === "--path") {
1776
+ const next = argv[index + 1];
1777
+ if (!next || !next.startsWith("/")) {
1778
+ throw new Error("MCP HTTP path must start with '/'.");
1779
+ }
1780
+ path = next;
1781
+ index += 1;
1782
+ }
1783
+ }
1784
+ return transport === "http"
1785
+ ? { transport, host, port, path }
1786
+ : { transport };
1787
+ }
1788
+ export async function connectMartinMcpHttpServer(options = {}) {
1789
+ const host = options.host ?? "127.0.0.1";
1790
+ const port = options.port ?? 3033;
1791
+ const path = options.path ?? "/mcp";
1792
+ const mcpServer = createMartinMcpServer();
1793
+ const transport = new StreamableHTTPServerTransport({
1794
+ sessionIdGenerator: undefined
1795
+ });
1796
+ await mcpServer.connect(transport);
1797
+ const httpServer = createHttpServer(async (req, res) => {
1798
+ try {
1799
+ const requestUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? `${host}:${port}`}`);
1800
+ if (requestUrl.pathname !== path) {
1801
+ res.statusCode = 404;
1802
+ res.setHeader("content-type", "application/json");
1803
+ res.end(JSON.stringify({ error: "not_found", path: requestUrl.pathname }));
1804
+ return;
1805
+ }
1806
+ if (req.method === "POST") {
1807
+ const parsedBody = await readJsonBody(req, res);
1808
+ if (res.writableEnded) {
1809
+ return;
1810
+ }
1811
+ await transport.handleRequest(req, res, parsedBody);
1812
+ return;
1813
+ }
1814
+ if (req.method === "GET" || req.method === "DELETE") {
1815
+ await transport.handleRequest(req, res);
1816
+ return;
1817
+ }
1818
+ res.statusCode = 405;
1819
+ res.setHeader("allow", "GET, POST, DELETE");
1820
+ res.end();
1821
+ }
1822
+ catch (error) {
1823
+ if (!res.headersSent) {
1824
+ res.statusCode = 500;
1825
+ res.setHeader("content-type", "application/json");
1826
+ res.end(JSON.stringify({ error: "internal_error", message: error.message }));
1827
+ }
1828
+ }
1829
+ });
1830
+ await new Promise((resolve, reject) => {
1831
+ httpServer.once("error", reject);
1832
+ httpServer.listen(port, host, () => {
1833
+ httpServer.off("error", reject);
1834
+ resolve();
1835
+ });
1836
+ });
1837
+ const address = httpServer.address();
1838
+ const resolvedPort = typeof address === "object" && address ? address.port : port;
1839
+ return {
1840
+ server: httpServer,
1841
+ host,
1842
+ port: resolvedPort,
1843
+ path,
1844
+ endpoint: `http://${host}:${resolvedPort}${path}`,
1845
+ close: async () => {
1846
+ await transport.close();
1847
+ await new Promise((resolve) => {
1848
+ httpServer.close(() => resolve());
1849
+ });
1850
+ }
1851
+ };
1852
+ }
1701
1853
  export function isDirectExecutionEntry(entryPath, moduleUrl = import.meta.url) {
1702
1854
  if (typeof entryPath !== "string" || entryPath.length === 0) {
1703
1855
  return false;
@@ -1718,5 +1870,30 @@ function realPathOrResolved(filePath) {
1718
1870
  }
1719
1871
  }
1720
1872
  if (isDirectExecution()) {
1721
- await connectMartinMcpStdioServer();
1873
+ const startup = parseMartinMcpServerArgs(process.argv.slice(2));
1874
+ if (startup.transport === "http") {
1875
+ await connectMartinMcpHttpServer(startup);
1876
+ }
1877
+ else {
1878
+ await connectMartinMcpStdioServer();
1879
+ }
1880
+ }
1881
+ async function readJsonBody(req, res) {
1882
+ const chunks = [];
1883
+ for await (const chunk of req) {
1884
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1885
+ }
1886
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
1887
+ if (!raw) {
1888
+ return undefined;
1889
+ }
1890
+ try {
1891
+ return JSON.parse(raw);
1892
+ }
1893
+ catch {
1894
+ res.statusCode = 400;
1895
+ res.setHeader("content-type", "application/json");
1896
+ res.end(JSON.stringify({ error: "invalid_json" }));
1897
+ return undefined;
1898
+ }
1722
1899
  }
@@ -115,10 +115,11 @@ export async function martinDoctorTool(input) {
115
115
  ...(gemini.resolvedPath ? { resolvedPath: gemini.resolvedPath } : {})
116
116
  },
117
117
  openai: {
118
- available: Boolean(process.env.MARTIN_OPENAI_API_KEY),
119
- detail: process.env.MARTIN_OPENAI_API_KEY
120
- ? `OpenAI-compatible endpoint configured (model: ${process.env.MARTIN_OPENAI_MODEL ?? "gpt-4.1-mini"})`
121
- : "Set MARTIN_OPENAI_API_KEY and optionally MARTIN_OPENAI_BASE_URL + MARTIN_OPENAI_MODEL to enable."
118
+ available: Boolean(process.env.MARTIN_OPENAI_MODEL &&
119
+ (process.env.MARTIN_OPENAI_API_KEY || process.env.MARTIN_OPENAI_BASE_URL)),
120
+ detail: process.env.MARTIN_OPENAI_MODEL
121
+ ? `OpenAI-compatible model configured: ${process.env.MARTIN_OPENAI_MODEL}.`
122
+ : "MODEL_CONFIGURATION_REQUIRED: set MARTIN_OPENAI_MODEL and the endpoint/auth required by your provider."
122
123
  }
123
124
  },
124
125
  ...(input.engine ? { requestedEngine: input.engine } : {}),
@@ -1,3 +1,4 @@
1
+ import type { CostProvenance } from "../vendor/contracts/index.js";
1
2
  import { type LoopPreview } from "./tool-support.js";
2
3
  export interface GetStatusInput {
3
4
  /** JSON-serialized LoopRecord. */
@@ -18,6 +19,7 @@ export interface GetStatusOutput {
18
19
  lifecycleState: string;
19
20
  attempts: number;
20
21
  costUsd: number;
22
+ costProvenance: CostProvenance;
21
23
  avoidedUsd: number;
22
24
  pressure: string;
23
25
  shouldStop: boolean;
@@ -31,6 +31,7 @@ export async function getStatusTool(input) {
31
31
  lifecycleState: loop.lifecycleState,
32
32
  attempts: loop.attempts.length,
33
33
  costUsd: loop.cost.actualUsd,
34
+ costProvenance: loop.cost.provenance ?? "unavailable",
34
35
  avoidedUsd: loop.cost.avoidedUsd ?? 0,
35
36
  pressure: costState.pressure,
36
37
  shouldStop: costState.shouldStop,
@@ -1,4 +1,5 @@
1
1
  import { type CodexHostPlatform } from "../vendor/adapters/index.js";
2
+ import type { UpdateAvailableField } from "../vendor/contracts/index.js";
2
3
  import { type MartinEngine } from "./tool-support.js";
3
4
  import { buildPolicyPackDefinition, type MartinPlanProposal, type MartinPolicyPack, type MartinRiskAssessment, type MartinRunContract } from "./workflow-governance.js";
4
5
  export interface MartinPreflightInput {
@@ -108,5 +109,6 @@ export interface MartinPreflightOutput {
108
109
  risk: MartinRiskAssessment;
109
110
  runContract: MartinRunContract;
110
111
  plan: MartinPlanProposal;
112
+ updateAvailable?: UpdateAvailableField;
111
113
  }
112
114
  export declare function martinPreflightTool(input: MartinPreflightInput): Promise<MartinPreflightOutput>;
@@ -1,9 +1,13 @@
1
1
  import { probeCodexLaunch, resolveCliCommandAvailability } from "../vendor/adapters/index.js";
2
- import { resolveRunsRoot } from "../vendor/core/index.js";
2
+ import { fetchSelectedMessage, getMcpInstalledVersion, isCooldownExpired, isDismissed, isNewerVersion, loadDeliveryRecord, recordShown, resolveDefaultLedgerPath, resolveRunsRoot, saveDeliveryRecord, } from "../vendor/core/index.js";
3
+ import { MARTIN_MCP_PACKAGE_VERSION } from "../package-version.js";
3
4
  import { resolveSafeRepoRoot } from "../server-validation.js";
4
5
  import { createSkippedCliAvailability, formatUsd, getEngineAvailability, resolveExecutionMode } from "./tool-support.js";
5
6
  import { buildPlanProposal, normalizeLoopBudget, buildRunContract, buildPolicyPackDefinition, inspectRepoSignals } from "./workflow-governance.js";
6
7
  export async function martinPreflightTool(input) {
8
+ // Fire delivery fetch early so it races with the rest of preflight work.
9
+ const mcpVersion = getMcpInstalledVersion() ?? MARTIN_MCP_PACKAGE_VERSION;
10
+ const deliveryFetchPromise = fetchSelectedMessage({ clientVersion: mcpVersion, clientKind: "mcp", trigger: "version_check" }, { timeoutMs: 3_000 }).catch(() => null);
7
11
  const executionMode = resolveExecutionMode();
8
12
  const workspaceRoot = resolveSafeRepoRoot();
9
13
  const workingDirectory = resolveSafeRepoRoot(input.workingDirectory);
@@ -32,7 +36,7 @@ export async function martinPreflightTool(input) {
32
36
  maxTokens: input.maxTokens
33
37
  });
34
38
  if (!executionMode.liveMode) {
35
- warnings.push("Proof mode is active; preflight only proves configuration shape, not live CLI readiness.");
39
+ warnings.push("Verification-only mode is active; it cannot emit governed VERIFIED or prove live CLI readiness.");
36
40
  }
37
41
  else if (!engineAvailability.available) {
38
42
  warnings.push(`Requested engine '${engine}' is not available on PATH.`);
@@ -140,6 +144,42 @@ export async function martinPreflightTool(input) {
140
144
  policy,
141
145
  risk: runContract.risk,
142
146
  runContract,
143
- plan
147
+ plan,
148
+ ...await resolvePreflightUpdateAvailable(deliveryFetchPromise, mcpVersion)
144
149
  };
145
150
  }
151
+ async function resolvePreflightUpdateAvailable(fetchPromise, currentVersion) {
152
+ try {
153
+ const message = await fetchPromise;
154
+ if (!message)
155
+ return {};
156
+ if (message.action.type !== "upgrade_mcp")
157
+ return {};
158
+ const targetVersion = message.action.targetVersion;
159
+ if (!targetVersion)
160
+ return {};
161
+ if (!isNewerVersion(currentVersion, targetVersion))
162
+ return {};
163
+ const ledgerPath = resolveDefaultLedgerPath();
164
+ const record = loadDeliveryRecord(ledgerPath);
165
+ const nowMs = Date.now();
166
+ if (!isCooldownExpired(record, nowMs))
167
+ return {};
168
+ if (isDismissed(record, message.id))
169
+ return {};
170
+ try {
171
+ saveDeliveryRecord(ledgerPath, recordShown(record, message, nowMs));
172
+ }
173
+ catch { /* ledger write failure must not surface */ }
174
+ return {
175
+ updateAvailable: {
176
+ targetVersion,
177
+ kind: "mcp",
178
+ message: message.body
179
+ }
180
+ };
181
+ }
182
+ catch {
183
+ return {};
184
+ }
185
+ }
@@ -2,7 +2,7 @@ import { buildArtifactSummary, buildBudgetSnapshot, buildCostSnapshot, buildEven
2
2
  import { readRunControlState } from "./run-controls.js";
3
3
  import { martinEvalTool } from "./eval.js";
4
4
  import { assessRunRisk } from "./workflow-governance.js";
5
- import type { ReceiptIntegritySummary, ReceiptScope } from "../vendor/contracts/index.js";
5
+ import type { ReceiptIntegritySummary, ReceiptScope, TerminationEnvelopeV1, VerifiedHandoffV1 } from "../vendor/contracts/index.js";
6
6
  export interface MartinRunDossierInput {
7
7
  file?: string;
8
8
  loopId?: string;
@@ -45,6 +45,8 @@ export interface MartinRunDossierOutput {
45
45
  };
46
46
  evaluation: Awaited<ReturnType<typeof martinEvalTool>>;
47
47
  control: Awaited<ReturnType<typeof readRunControlState>>;
48
+ terminationEnvelope?: TerminationEnvelopeV1;
49
+ verifiedHandoff: VerifiedHandoffV1;
48
50
  format: "json" | "md" | "github-pr";
49
51
  rendered?: string;
50
52
  inspection: {
@@ -4,6 +4,7 @@ import { loadDetailedLoopRecord, readAttemptArtifactFiles, readLedgerEvents } fr
4
4
  import { readRunControlState } from "./run-controls.js";
5
5
  import { martinEvalTool } from "./eval.js";
6
6
  import { assessRunRisk, inspectRepoSignals } from "./workflow-governance.js";
7
+ import { buildVerifiedHandoff } from "../vendor/core/index.js";
7
8
  export async function martinRunDossierTool(input) {
8
9
  const detail = await loadDetailedLoopRecord(input);
9
10
  const ledgerEvents = await readLedgerEvents(detail);
@@ -54,6 +55,7 @@ export async function martinRunDossierTool(input) {
54
55
  cost: buildCostSnapshot(detail.loop.cost),
55
56
  receiptIntegrity: resolveReceiptIntegrity(detail.loop),
56
57
  ...(detail.loop.receiptScope ? { receiptScope: detail.loop.receiptScope } : {}),
58
+ ...(detail.loop.terminationEnvelope ? { terminationEnvelope: detail.loop.terminationEnvelope } : {}),
57
59
  attempts,
58
60
  verification,
59
61
  artifacts: buildArtifactSummary(detail.loop),
@@ -72,7 +74,37 @@ export async function martinRunDossierTool(input) {
72
74
  ...(detail.canonicalLoopRecordPath ? { canonicalLoopRecordPath: detail.canonicalLoopRecordPath } : {}),
73
75
  ...(detail.ledgerPath ? { ledgerPath: detail.ledgerPath } : {})
74
76
  },
75
- warnings: [...detail.warnings, ...verification.warnings]
77
+ warnings: [...detail.warnings, ...verification.warnings],
78
+ verifiedHandoff: buildVerifiedHandoff({
79
+ loop: detail.loop,
80
+ receiptIntegrity: resolveReceiptIntegrity(detail.loop),
81
+ verification: {
82
+ status: verification.status,
83
+ summary: verification.summary ?? "No verification summary recorded.",
84
+ steps: [],
85
+ warnings: verification.warnings,
86
+ },
87
+ scope: {
88
+ status: detail.loop.task?.allowedPaths?.length ||
89
+ detail.loop.task?.deniedPaths?.length
90
+ ? "WITHIN_SCOPE"
91
+ : "NOT_EVALUATED",
92
+ allowedPaths: detail.loop.task?.allowedPaths ?? [],
93
+ deniedPaths: detail.loop.task?.deniedPaths ?? [],
94
+ changedFiles: [],
95
+ violations: [],
96
+ },
97
+ testIntegrity: {
98
+ status: "NOT_EVALUATED",
99
+ verdict: "NOT_EVALUATED",
100
+ protectedPaths: [],
101
+ changedProtectedPaths: [],
102
+ findings: [],
103
+ summary: "Automatic test-integrity evidence was not recorded for this run.",
104
+ },
105
+ unresolvedWork: verification.status === "passed" ? [] : [verification.summary ?? "Verification did not pass."],
106
+ nextAction: review.nextAction,
107
+ }),
76
108
  };
77
109
  if (output.format !== "json") {
78
110
  output.rendered = renderDossier(output);
@@ -81,7 +113,13 @@ export async function martinRunDossierTool(input) {
81
113
  }
82
114
  function renderDossier(output) {
83
115
  const lines = [
84
- output.format === "github-pr" ? "## MartinLoop Run Dossier" : "# MartinLoop Run Dossier",
116
+ output.format === "github-pr" ? "## MartinLoop Verified Handoff" : "# MartinLoop Verified Handoff",
117
+ "",
118
+ `Outcome: ${output.verifiedHandoff.outcome}`,
119
+ `Definition of Done: ${output.verifiedHandoff.definitionOfDone.acceptanceCriteria.length} acceptance criterion/criteria`,
120
+ `Verification: ${output.verifiedHandoff.verification.status}`,
121
+ `Scope: ${output.verifiedHandoff.scope.status}`,
122
+ `Test Integrity: ${output.verifiedHandoff.testIntegrity.verdict}`,
85
123
  "",
86
124
  `Objective: ${output.loop.objective}`,
87
125
  `Run: ${output.loop.loopId}`,
@@ -1,6 +1,6 @@
1
1
  import { type SpawnLike } from "../vendor/adapters/index.js";
2
- import { type RunStore, type RunMartinInput, type RunMartinResult } from "../vendor/core/index.js";
3
- import type { LoopBudget, ReceiptScope } from "../vendor/contracts/index.js";
2
+ import { type RunStore } from "../vendor/core/index.js";
3
+ import type { CostProvenance, ExecutionMode, LoopBudget, ReceiptScope } from "../vendor/contracts/index.js";
4
4
  import { buildArtifactSummary, buildVerificationSummary, buildLoopPreview, type MartinEngine } from "./tool-support.js";
5
5
  export interface RunLoopInput {
6
6
  objective: string;
@@ -23,7 +23,10 @@ export interface RunLoopOutput {
23
23
  reason: string;
24
24
  attempts: number;
25
25
  costUsd: number;
26
+ costProvenance: CostProvenance;
26
27
  verificationPassed: boolean;
28
+ executionMode: ExecutionMode;
29
+ governanceClaimEligible: boolean;
27
30
  loopId: string;
28
31
  pressure: string;
29
32
  shouldStop: boolean;
@@ -46,5 +49,4 @@ export interface RunLoopOutput {
46
49
  }
47
50
  export declare function __setProofModeVerifierSpawnImplForTests(spawnImpl?: SpawnLike): void;
48
51
  export declare function __setRunStoreOverrideForTests(store?: RunStore): void;
49
- export declare function __setRunMartinImplForTests(impl?: (input: RunMartinInput) => Promise<RunMartinResult>): void;
50
52
  export declare function runLoopTool(input: RunLoopInput): Promise<RunLoopOutput>;