@martinloop/mcp 0.8.9 → 0.8.10

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.
@@ -1 +1 @@
1
- export declare const MARTIN_MCP_PACKAGE_VERSION = "0.8.9";
1
+ export declare const MARTIN_MCP_PACKAGE_VERSION = "0.8.10";
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by build — do not edit by hand.
2
2
  // Source of truth: packages/mcp/package.json#version
3
- export const MARTIN_MCP_PACKAGE_VERSION = "0.8.9";
3
+ export const MARTIN_MCP_PACKAGE_VERSION = "0.8.10";
@@ -64,4 +64,16 @@ export declare function spawnSupervisedProcessForTest(command: string, args: rea
64
64
  * @internal - deterministic pure classification for process-supervisor tests
65
65
  */
66
66
  export declare function classifyPosixProcessGroupSnapshot(output: string, ownedGroupPid: number): "closed" | "alive" | "absent" | "invalid";
67
+ type WindowsProcessTreeRow = {
68
+ ProcessId: number;
69
+ ParentProcessId: number;
70
+ };
71
+ /**
72
+ * Classify descendants that remain visible after a failed termination attempt.
73
+ * Two point-in-time snapshots cannot prove closure across the interval because
74
+ * a descendant may be created and reparented between them. Absence is therefore
75
+ * indeterminate and must retain the original termination failure.
76
+ * @internal - deterministic pure classification for process-supervisor tests
77
+ */
78
+ export declare function classifyWindowsDescendantPostcondition(initialRows: WindowsProcessTreeRow[], postTerminationRows: WindowsProcessTreeRow[], rootPid: number): "closed" | "alive" | "invalid";
67
79
  export {};
@@ -456,6 +456,39 @@ function isErrno(error, code) {
456
456
  return typeof error === "object" && error !== null && "code" in error
457
457
  && String(error.code) === code;
458
458
  }
459
+ /**
460
+ * Classify descendants that remain visible after a failed termination attempt.
461
+ * Two point-in-time snapshots cannot prove closure across the interval because
462
+ * a descendant may be created and reparented between them. Absence is therefore
463
+ * indeterminate and must retain the original termination failure.
464
+ * @internal - deterministic pure classification for process-supervisor tests
465
+ */
466
+ export function classifyWindowsDescendantPostcondition(initialRows, postTerminationRows, rootPid) {
467
+ if (!isOwnedPid(rootPid))
468
+ return "invalid";
469
+ const allRows = [...initialRows, ...postTerminationRows];
470
+ if (allRows.some((row) => !isOwnedPid(row.ProcessId) || !Number.isSafeInteger(row.ParentProcessId))) {
471
+ return "invalid";
472
+ }
473
+ const ownedLineage = new Set([rootPid]);
474
+ const expandLineage = (rows) => {
475
+ let changed = true;
476
+ while (changed) {
477
+ changed = false;
478
+ for (const row of rows) {
479
+ if (ownedLineage.has(row.ParentProcessId) && !ownedLineage.has(row.ProcessId)) {
480
+ ownedLineage.add(row.ProcessId);
481
+ changed = true;
482
+ }
483
+ }
484
+ }
485
+ };
486
+ expandLineage(initialRows);
487
+ expandLineage(postTerminationRows);
488
+ return postTerminationRows.some((row) => ownedLineage.has(row.ProcessId))
489
+ ? "alive"
490
+ : "invalid";
491
+ }
459
492
  async function terminateWindowsDescendantsAfterRootExit(rootPid, graceMs, waitImpl) {
460
493
  let rows;
461
494
  try {
@@ -471,11 +504,16 @@ async function terminateWindowsDescendantsAfterRootExit(rootPid, graceMs, waitIm
471
504
  childrenByParent.set(row.ParentProcessId, children);
472
505
  }
473
506
  const directChildren = childrenByParent.get(rootPid) ?? [];
507
+ let taskkillFailure;
474
508
  for (const childPid of directChildren) {
475
509
  const failure = await runTaskkill(childPid, spawnProcess, graceMs, waitImpl);
476
- if (failure)
477
- return failure;
510
+ taskkillFailure ??= failure;
478
511
  }
512
+ // Once taskkill has failed, a later process-table snapshot cannot prove the
513
+ // complete descendant set closed. Return the bounded failure immediately;
514
+ // starting another PowerShell query here could otherwise stall cleanup.
515
+ if (taskkillFailure)
516
+ return taskkillFailure;
479
517
  return { state: "closed" };
480
518
  }
481
519
  function readWindowsProcessTree() {
@@ -49,6 +49,10 @@ export function createVerifierOnlyAdapter(options = {}) {
49
49
  providerId: "verifier",
50
50
  model: "verify-only",
51
51
  transport: "cli",
52
+ budgetPreflight: {
53
+ pricePerMTokenUsd: 0,
54
+ basis: "Verifier-only execution launches no model provider."
55
+ },
52
56
  capabilities: createAdapterCapabilities({
53
57
  usageSettlement: true,
54
58
  diffArtifacts: true,
@@ -206,6 +206,8 @@ export interface MartinAdapter {
206
206
  budgetPreflight?: {
207
207
  /** Estimated token floor required before the adapter can report first-turn usage. */
208
208
  minimumViableTokens?: number;
209
+ /** Adapter-specific provider price used by the admission estimate. */
210
+ pricePerMTokenUsd?: number;
209
211
  basis?: string;
210
212
  };
211
213
  capabilities?: {
@@ -880,6 +880,7 @@ export async function runMartin(input) {
880
880
  remainingBudgetUsd: distilled.constraints.remainingBudgetUsd,
881
881
  remainingTokens: distilled.constraints.remainingTokens,
882
882
  minimumViableTokens: executingAdapter.metadata.budgetPreflight?.minimumViableTokens,
883
+ pricePerMTokenUsd: executingAdapter.metadata.budgetPreflight?.pricePerMTokenUsd,
883
884
  // D2: pass remaining iterations so the preflight can compute a budget-aware cap
884
885
  // rather than using the old rigid maxUsd * 0.25 that blocked viable attempts.
885
886
  remainingIterations: distilled.constraints.remainingIterations
@@ -385,7 +385,10 @@ function roundUsd(value) {
385
385
  return Math.round(value * 100) / 100;
386
386
  }
387
387
  export function evaluateBudgetPreflight(input) {
388
- const pricePerMToken = input.pricePerMTokenUsd ?? 3.0;
388
+ const declaredPrice = input.pricePerMTokenUsd;
389
+ const validDeclaredPrice = declaredPrice === undefined
390
+ || (Number.isFinite(declaredPrice) && declaredPrice >= 0);
391
+ const pricePerMToken = validDeclaredPrice ? (declaredPrice ?? 3.0) : 3.0;
389
392
  const rawPromptTokens = Math.ceil(input.promptCharCount / 4);
390
393
  const estimatedPromptTokens = Math.ceil(rawPromptTokens * 1.2);
391
394
  const estimatedToolOverheadTokens = 800 + input.attemptCount * 200;
@@ -407,6 +410,13 @@ export function evaluateBudgetPreflight(input) {
407
410
  estimatedAttemptCostUsd,
408
411
  provenance
409
412
  };
413
+ if (!validDeclaredPrice) {
414
+ return {
415
+ allowed: false,
416
+ reason: "Preflight: invalid adapter price; pricePerMTokenUsd must be finite and non-negative.",
417
+ estimate
418
+ };
419
+ }
410
420
  if (input.remainingTokens !== undefined
411
421
  && estimatedMinimumViableTokens > 0
412
422
  && estimatedTotalTokens > input.remainingTokens) {
@@ -1,6 +1,7 @@
1
1
  // SPDX-FileCopyrightText: MartinLoop contributors
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { createHash } from "node:crypto";
4
+ import { posix, win32 } from "node:path";
4
5
  // ---------------------------------------------------------------------------
5
6
  // Mapping: granular TestIntegrityStatus → public TestIntegrityVerdict
6
7
  //
@@ -88,6 +89,65 @@ export function deriveVerifiedHandoffLifecycleTruth(loop) {
88
89
  evidenceAvailable: true,
89
90
  };
90
91
  }
92
+ function sameOptionalVerifierFilesystemPath(actual, expected) {
93
+ if (actual === undefined || expected === undefined) {
94
+ return actual === expected;
95
+ }
96
+ return sameVerifierFilesystemPath(actual, expected);
97
+ }
98
+ function classifyVerifierFilesystemPath(value) {
99
+ // Persisted Windows bindings are expected to be drive-absolute or UNC.
100
+ // Do not use win32.isAbsolute() here because it also classifies POSIX-style
101
+ // root paths such as "/repo" as Windows root-relative paths.
102
+ if (/^[A-Za-z]:[\\/]/u.test(value) || /^\\\\/u.test(value) || /^\/\//u.test(value)) {
103
+ return "windows";
104
+ }
105
+ if (value.startsWith("/")) {
106
+ return "posix";
107
+ }
108
+ return "relative";
109
+ }
110
+ function stripTrailingVerifierSeparators(value, kind) {
111
+ if (kind === "windows") {
112
+ // Preserve drive roots (C:\\) and UNC share roots while removing harmless
113
+ // trailing separators from ordinary persisted paths.
114
+ if (/^[a-z]:\\$/iu.test(value) || /^\\\\[^\\]+\\[^\\]+\\?$/u.test(value)) {
115
+ return value;
116
+ }
117
+ return value.replace(/[\\/]+$/u, "");
118
+ }
119
+ if (kind === "posix") {
120
+ return value === "/" ? value : value.replace(/\/+$/u, "");
121
+ }
122
+ return value;
123
+ }
124
+ function normalizeVerifierFilesystemPath(value) {
125
+ const kind = classifyVerifierFilesystemPath(value);
126
+ if (kind === "windows") {
127
+ return {
128
+ kind,
129
+ value: stripTrailingVerifierSeparators(win32.normalize(value), kind).toLowerCase(),
130
+ };
131
+ }
132
+ if (kind === "posix") {
133
+ return {
134
+ kind,
135
+ value: stripTrailingVerifierSeparators(posix.normalize(value), kind),
136
+ };
137
+ }
138
+ // Relative verifier bindings are legacy/non-canonical. Normalize only their
139
+ // separators/dot segments and keep comparison case-sensitive/fail-closed.
140
+ return {
141
+ kind,
142
+ value: stripTrailingVerifierSeparators(posix.normalize(value.replace(/\\/gu, "/")), kind),
143
+ };
144
+ }
145
+ function sameVerifierFilesystemPath(actual, expected) {
146
+ const normalizedActual = normalizeVerifierFilesystemPath(actual);
147
+ const normalizedExpected = normalizeVerifierFilesystemPath(expected);
148
+ return (normalizedActual.kind === normalizedExpected.kind &&
149
+ normalizedActual.value === normalizedExpected.value);
150
+ }
91
151
  export function verifierActuallyPassed(evidence, expected) {
92
152
  if (!evidence?.passed || !evidence.binding || !evidence.steps) {
93
153
  return false;
@@ -98,8 +158,8 @@ export function verifierActuallyPassed(evidence, expected) {
98
158
  if (evidence.binding.runId !== expected.runId ||
99
159
  evidence.binding.workspaceId !== expected.workspaceId ||
100
160
  (evidence.binding.attemptId ?? undefined) !== (expected.attemptId ?? undefined) ||
101
- evidence.binding.cwd !== expected.cwd ||
102
- (evidence.binding.runsRoot ?? undefined) !== (expected.runsRoot ?? undefined) ||
161
+ !sameVerifierFilesystemPath(evidence.binding.cwd, expected.cwd) ||
162
+ !sameOptionalVerifierFilesystemPath(evidence.binding.runsRoot, expected.runsRoot) ||
103
163
  (evidence.binding.executionProfile ?? undefined) !== (expected.executionProfile ?? undefined) ||
104
164
  JSON.stringify(evidence.binding.allowedNetworkDomains ?? []) !== JSON.stringify(expected.allowedNetworkDomains ?? []) ||
105
165
  JSON.stringify(evidence.binding.commands) !== JSON.stringify(expected.commands)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@martinloop/mcp",
3
- "version": "0.8.9",
3
+ "version": "0.8.10",
4
4
  "mcpName": "io.github.Keesan12/martin-loop",
5
5
  "private": false,
6
6
  "type": "module",
@@ -74,13 +74,9 @@
74
74
  "mcpb:smoke": "node ./mcpb/smoke-mcpb.mjs"
75
75
  },
76
76
  "dependencies": {
77
- "@modelcontextprotocol/sdk": "^1.29.0",
77
+ "@modelcontextprotocol/sdk": "^1.32.2",
78
78
  "@open-policy-agent/opa-wasm": "^1.10.0",
79
- "@opentelemetry/api-logs": "^0.214.0",
80
- "@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
81
- "@opentelemetry/resources": "^2.6.1",
82
- "@opentelemetry/sdk-logs": "^0.214.0",
83
- "ts-morph": "^21.0.0"
79
+ "zod": "^4.6.7"
84
80
  },
85
81
  "devDependencies": {
86
82
  "@anthropic-ai/mcpb": "2.1.2",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/Keesan12/martin-loop",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.8.9",
10
+ "version": "0.8.10",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@martinloop/mcp",
15
- "version": "0.8.9",
15
+ "version": "0.8.10",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }