@martinloop/mcp 0.6.3 → 0.6.4

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.6.3";
1
+ export declare const MARTIN_MCP_PACKAGE_VERSION = "0.6.4";
@@ -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.6.3";
3
+ export const MARTIN_MCP_PACKAGE_VERSION = "0.6.4";
@@ -37,6 +37,9 @@ export interface LoopPreview {
37
37
  remainingTokens?: number;
38
38
  lastAttempt?: AttemptSummary;
39
39
  routingEconomics?: Record<string, unknown>;
40
+ /** Present when an attempt is executing but not yet in the completed attempts list.
41
+ * Indicates the backend is actively running — callers must NOT start a concurrent recovery run. */
42
+ activeAttemptId?: string;
40
43
  }
41
44
  export interface AttemptArtifactFiles {
42
45
  directory: string;
@@ -141,6 +141,19 @@ export function buildLoopPreview(loop) {
141
141
  attemptsUsed: loop.attempts.length
142
142
  });
143
143
  const lastAttempt = loop.attempts.at(-1);
144
+ // Detect an in-flight attempt: an attempt.started event whose attemptId has
145
+ // no matching entry in loop.attempts (completed attempts only). This is the
146
+ // signal callers need to distinguish "backend actively executing" from
147
+ // "nothing has started yet" after a client transport timeout.
148
+ const completedAttemptIds = new Set(loop.attempts.map((a) => a.attemptId).filter(Boolean));
149
+ const activeAttemptEvent = (loop.events ?? [])
150
+ .filter((e) => e.type === "attempt.started")
151
+ .at(-1);
152
+ const activeAttemptId = activeAttemptEvent !== undefined &&
153
+ typeof activeAttemptEvent.payload["attemptId"] === "string" &&
154
+ !completedAttemptIds.has(activeAttemptEvent.payload["attemptId"])
155
+ ? activeAttemptEvent.payload["attemptId"]
156
+ : undefined;
144
157
  return {
145
158
  loopId: loop.loopId,
146
159
  title: loop.task?.title ?? loop.loopId,
@@ -161,7 +174,8 @@ export function buildLoopPreview(loop) {
161
174
  ? { remainingTokens: costState.remainingTokens }
162
175
  : {}),
163
176
  ...(lastAttempt ? { lastAttempt: buildAttemptSummary(lastAttempt) } : {}),
164
- ...(loop.routingEconomics ? { routingEconomics: buildRoutingEconomicsSummary(loop.routingEconomics) } : {})
177
+ ...(loop.routingEconomics ? { routingEconomics: buildRoutingEconomicsSummary(loop.routingEconomics) } : {}),
178
+ ...(activeAttemptId !== undefined ? { activeAttemptId } : {})
165
179
  };
166
180
  }
167
181
  function buildRoutingEconomicsSummary(economics) {
@@ -33,24 +33,28 @@ function flagContext(help, flag) {
33
33
  const index = lines.findIndex((line) => line.includes(flag));
34
34
  if (index < 0)
35
35
  return "";
36
- return lines.slice(Math.max(0, index - 1), Math.min(lines.length, index + 3)).join("\n");
36
+ let end = index + 1;
37
+ while (end < lines.length && end < index + 24) {
38
+ if (/^\s*(?:-\w,\s*)?--[a-z][\w-]*/iu.test(lines[end] ?? ""))
39
+ break;
40
+ end += 1;
41
+ }
42
+ return lines.slice(Math.max(0, index - 1), end).join("\n");
37
43
  }
38
44
  function parseAdvertisedValues(help, flag) {
39
45
  const flagIndex = help.indexOf(flag);
40
46
  if (flagIndex < 0)
41
47
  return [];
42
48
  const tail = help.slice(flagIndex);
43
- const nextFlagOffset = tail.slice(flag.length).search(/\n\s*-/u);
49
+ const nextFlagOffset = tail.slice(flag.length).search(/\n\s*(?:-\w,\s*)?--[a-z][\w-]*/iu);
44
50
  const scoped = nextFlagOffset < 0
45
51
  ? tail
46
52
  : tail.slice(0, flag.length + nextFlagOffset);
47
- const match = scoped.match(/possible values:\s*([^\]\n]+)/iu);
48
- if (!match?.[1])
49
- return [];
50
- return match[1]
51
- .split(",")
52
- .map((value) => value.trim().replace(/[\].]$/u, ""))
53
- .filter(Boolean);
53
+ const inline = scoped.match(/possible values:[ \t]*([^\]\r\n]+)/iu)?.[1];
54
+ if (inline?.trim()) {
55
+ return inline.split(",").map((value) => value.trim().replace(/[\].]$/u, "")).filter(Boolean);
56
+ }
57
+ return [...scoped.matchAll(/^\s*-\s+([a-z][a-z0-9-]*):/gimu)].map((match) => match[1]).filter(Boolean);
54
58
  }
55
59
  function buildInjectedSpawnPlan(binaryPath, args, platform) {
56
60
  if (platform !== "win32")
@@ -214,7 +218,8 @@ export function resolveCodexAutonomyCandidates(profile, intent = DEFAULT_AGENT_E
214
218
  binaryPath: profile.binaryPath,
215
219
  intent,
216
220
  strategy: "sandbox+approval",
217
- sandboxValue: "workspace-write"
221
+ sandboxValue: "workspace-write",
222
+ ...(profile.approvalPolicy?.values?.includes("never") ? { approvalValue: "never" } : {})
218
223
  });
219
224
  }
220
225
  // Keep provider automation as a compatibility fallback. It is never trusted
@@ -1,7 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { dirname, extname, join, resolve } from "node:path";
4
- import { tmpdir } from "node:os";
4
+ import { userInfo } from "node:os";
5
5
  import { normalizeProviderExecutionTimeoutMs } from "../contracts/index.js";
6
6
  import { createSpawnPlan, resolveNpmShimScript } from "./cli-bridge.js";
7
7
  import { buildCodexExecArgs, buildCodexStdin, cacheCodexCapabilityProfile, probeCodexCapabilities, resolveCodexAutonomyCandidates } from "./codex-capabilities.js";
@@ -437,7 +437,10 @@ export function probeCodexLaunch(input) {
437
437
  for (const transport of transports) {
438
438
  const marker = `.martin-codex-write-probe-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`;
439
439
  const markerPath = join(input.workingDirectory, marker);
440
- const outsideRoot = mkdtempSync(join(tmpdir(), "martin-codex-outside-probe-"));
440
+ // Codex workspace-write intentionally permits the host temp directory,
441
+ // and the repo can be nested inside a broader writable host workspace.
442
+ // Probe from the user-home boundary so the denial canary is outside both.
443
+ const outsideRoot = mkdtempSync(join(userInfo().homedir, ".martin-codex-outside-probe-"));
441
444
  const outsideMarkerPath = join(outsideRoot, ".martin-codex-outside-probe.tmp");
442
445
  const prompt = buildMarkerPrompt(marker, outsideMarkerPath);
443
446
  let args;
@@ -1,4 +1,3 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { DEFAULT_AGENT_EXECUTION_INTENT, normalizeProviderExecutionTimeoutMs, appendLoopEvent, createLoopRecord } from "../contracts/index.js";
3
2
  import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe } from "./policy.js";
4
3
  import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
@@ -776,6 +775,12 @@ export async function runMartin(input) {
776
775
  capturedAt: attemptStartedAt
777
776
  })
778
777
  : undefined;
778
+ // Persist attempt.started state before blocking on provider execution.
779
+ // If the MCP client transport times out (default 300 s), a status query against
780
+ // the stable run ID must see this attempt as active rather than observing
781
+ // attempts=0 with no in-progress state — which falsely implies nothing happened
782
+ // and causes callers to start a concurrent recovery run before the backend finishes.
783
+ await persistLoopRecordIfSupported(input.store, loop);
779
784
  let result;
780
785
  try {
781
786
  result = await executingAdapter.execute({
@@ -1136,7 +1141,7 @@ export async function runMartin(input) {
1136
1141
  // VERIFY: Run grounding scan on patch diff if available
1137
1142
  // Uses the task's repoRoot to build/load the grounding index, then scans any diff
1138
1143
  let groundingScanResult;
1139
- const patchDiff = buildPatchDiff(result, changedFiles, input.task.repoRoot);
1144
+ const patchDiff = buildPatchDiff(result, changedFiles);
1140
1145
  if (patchDiff && input.task.repoRoot) {
1141
1146
  try {
1142
1147
  const groundingIndex = await loadOrBuildRepoGroundingIndex(input.task.repoRoot);
@@ -1569,34 +1574,20 @@ function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
1569
1574
  }
1570
1575
  return listAttemptChangedFilesSinceBoundary({ repoRoot, boundary: rollbackBoundary });
1571
1576
  }
1572
- function buildPatchDiff(result, changedFiles, repoRoot) {
1577
+ function buildPatchDiff(result, changedFiles) {
1573
1578
  // Use structured diff stats to build a minimal diff header if no raw diff is available
1574
- const files = result.execution?.changedFiles?.length ? result.execution.changedFiles : changedFiles;
1575
- if (files.length > 0) {
1576
- return files
1577
- .map((file) => buildSyntheticDiffHeader(file, repoRoot))
1579
+ if (result.execution?.changedFiles?.length) {
1580
+ // Build a synthetic diff header from changed file list
1581
+ return result.execution.changedFiles
1582
+ .map((file) => `--- a/${file}\n+++ b/${file}\n@@ -0,0 +1 @@\n+`)
1578
1583
  .join("\n");
1579
1584
  }
1580
- return undefined;
1581
- }
1582
- function buildSyntheticDiffHeader(file, repoRoot) {
1583
- const normalizedFile = file.replace(/\\/gu, "/");
1584
- const isNewWorkspaceFile = repoRoot ? isUntrackedWorkspaceFile(repoRoot, normalizedFile) : false;
1585
- const before = isNewWorkspaceFile ? "/dev/null" : `a/${normalizedFile}`;
1586
- return `--- ${before}\n+++ b/${normalizedFile}\n@@ -0,0 +1 @@\n+`;
1587
- }
1588
- function isUntrackedWorkspaceFile(repoRoot, file) {
1589
- const result = spawnSync("git", ["ls-files", "--others", "--exclude-standard", "--", file], {
1590
- cwd: repoRoot,
1591
- encoding: "utf8"
1592
- });
1593
- if (result.status !== 0 || typeof result.stdout !== "string") {
1594
- return false;
1585
+ if (changedFiles.length > 0) {
1586
+ return changedFiles
1587
+ .map((file) => `--- a/${file}\n+++ b/${file}\n@@ -0,0 +1 @@\n+`)
1588
+ .join("\n");
1595
1589
  }
1596
- return result.stdout
1597
- .split(/\r?\n/u)
1598
- .map((line) => line.trim().replace(/\\/gu, "/"))
1599
- .includes(file);
1590
+ return undefined;
1600
1591
  }
1601
1592
  function createBudgetSettlement(input) {
1602
1593
  const totalActualUsd = getUsageUsd(input.usage);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@martinloop/mcp",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "mcpName": "io.github.Keesan12/martin-loop",
5
5
  "private": false,
6
6
  "type": "module",
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.6.3",
10
+ "version": "0.6.4",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@martinloop/mcp",
15
- "version": "0.6.3",
15
+ "version": "0.6.4",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }