@mrrlin-dev/mcp 0.4.129 → 0.4.131

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/bin.cjs +611 -189
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,7 +10,7 @@ The bridge auto-picks up new `@mrrlin-dev/mcp` versions after `npm install -g`.
10
10
 
11
11
  - **L1 (self-update loop):** every 15 minutes (env-tunable) the bridge hashes its own `bin.cjs` and, on change, drains the current Codex turn cleanly and exits via `bridgeShutdown`. PM2 restarts. New code loads.
12
12
  - **L2 (postinstall nudge):** `npm install -g @mrrlin-dev/mcp@latest` triggers a `SIGUSR2` to every running PM2 bridge, collapsing the L1 wait to seconds.
13
- - **L3 (in-chat update offer):** every 6 hours the bridge checks `https://registry.npmjs.org/@mrrlin-dev/mcp/latest`. When a newer version is published, on your next chat turn the Director explains *why* to update (latest fixes/improvements, changelog link) and offers a choice — let it run the update, or run `npm install -g @mrrlin-dev/mcp@latest` yourself. Say "yes" → Director runs the install itself. 48h cooldown if you decline.
13
+ - **L3 (in-chat update offer):** every 6 hours the bridge checks `https://registry.npmjs.org/@mrrlin-dev/mcp/latest`. When a newer version is published, on your next chat turn the Director gives a short, generic reason to update (latest fixes/improvements) and offers a choice — let it run the update, or run `npm install -g @mrrlin-dev/mcp@latest` yourself. Say "yes" → Director runs the install itself. 48h cooldown if you decline. The offer hands the Director only the installed and latest version numbers from the registry check: it is told not to fetch the changelog or any other page, so no changelog text reaches the turn that can run the install. The changelog link is in the inbox notice (L3.3).
14
14
  - **L3.3 (inbox mirror):** the same fire also files a persistent `question` in the active project's inbox ("A newer version of the mrrlin bridge is available…", with the changelog link), so the update isn't only an ephemeral per-turn aside. The body is version-agnostic on purpose: the inbox dedup collapses it to one open row per project (reused across versions), and it re-appears at most once per 48h cooldown after you decide it. It's an opportunistic per-project signal, not a guaranteed cross-project notice.
15
15
  - **L4 (process hygiene):** the bridge refuses to start a second instance on the same port and warns about orphan `mrrlin-mcp director-bridge` processes at startup.
16
16
 
package/dist/bin.cjs CHANGED
@@ -45604,7 +45604,7 @@ ${input.prompt}` : input.prompt;
45604
45604
  if (input.model) {
45605
45605
  args.push("-m", input.model);
45606
45606
  }
45607
- args.push(fullPrompt);
45607
+ args.push("-");
45608
45608
  return new Promise((resolve3) => {
45609
45609
  let settled = false;
45610
45610
  const timerRef = { id: void 0 };
@@ -45617,7 +45617,7 @@ ${input.prompt}` : input.prompt;
45617
45617
  resolve3(result);
45618
45618
  };
45619
45619
  const options = {
45620
- stdio: ["ignore", "pipe", "pipe"],
45620
+ stdio: ["pipe", "pipe", "pipe"],
45621
45621
  cwd: input.cwd,
45622
45622
  env: process.env,
45623
45623
  shell: false
@@ -45635,6 +45635,9 @@ ${input.prompt}` : input.prompt;
45635
45635
  finish({ ok: false, error: `Failed to spawn codex exec: ${errMessage(err)}` });
45636
45636
  return;
45637
45637
  }
45638
+ child.stdin?.on("error", () => {
45639
+ });
45640
+ child.stdin?.end(fullPrompt);
45638
45641
  let stdout = "";
45639
45642
  let timedOut = false;
45640
45643
  timerRef.id = setTimeout(() => {
@@ -45691,7 +45694,6 @@ function runClaudeExec(input, deps) {
45691
45694
  if (input.model) {
45692
45695
  args.push("--model", input.model);
45693
45696
  }
45694
- args.push(input.prompt);
45695
45697
  return new Promise((resolve3) => {
45696
45698
  let settled = false;
45697
45699
  const timerRef = { id: void 0 };
@@ -45704,7 +45706,7 @@ function runClaudeExec(input, deps) {
45704
45706
  resolve3(result);
45705
45707
  };
45706
45708
  const options = {
45707
- stdio: ["ignore", "pipe", "pipe"],
45709
+ stdio: ["pipe", "pipe", "pipe"],
45708
45710
  cwd: input.cwd,
45709
45711
  env: process.env,
45710
45712
  shell: false
@@ -45722,6 +45724,9 @@ function runClaudeExec(input, deps) {
45722
45724
  finish({ ok: false, error: `Failed to spawn ${executable} exec: ${errMessage2(err)}` });
45723
45725
  return;
45724
45726
  }
45727
+ child.stdin?.on("error", () => {
45728
+ });
45729
+ child.stdin?.end(input.prompt);
45725
45730
  let stdout = "";
45726
45731
  let timedOut = false;
45727
45732
  timerRef.id = setTimeout(() => {
@@ -48596,6 +48601,68 @@ function resolveExternalAgentsCliInvocation(override) {
48596
48601
  }
48597
48602
  }
48598
48603
 
48604
+ // src/redact.ts
48605
+ var REDACTED = "[REDACTED]";
48606
+ var SECRET_PATTERNS = [
48607
+ // Mrrlin agent secret: mrrlin_agent_<base64url selector>.<base64url secret>. base64url uses - and
48608
+ // _, which the generic base64 run below does not cover, so most real secrets slipped through it.
48609
+ /mrrlin_agent_[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g,
48610
+ // GitHub tokens: ghp_ (classic PAT), gho_ (OAuth), ghu_ (user-to-server), ghs_ (server-to-server,
48611
+ // e.g. an App installation token), ghr_ (refresh token).
48612
+ /gh[oprsu]_[A-Za-z0-9]{20,}/g,
48613
+ /github_pat_[A-Za-z0-9_]{20,}/g,
48614
+ // GitHub fine-grained PAT
48615
+ /sk-ant-[A-Za-z0-9_-]{20,}/g,
48616
+ // Anthropic key or OAuth token (sk-ant-api03-..., sk-ant-oat01-...)
48617
+ /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
48618
+ // AWS access key id (long-term AKIA, temporary ASIA)
48619
+ /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi,
48620
+ // Authorization: Bearer <token>
48621
+ /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
48622
+ // JWT (header.payload.signature)
48623
+ /\b[A-Fa-f0-9]{32,}\b/g,
48624
+ // long hex run (hashes, hex secrets)
48625
+ /\b[A-Za-z0-9+/]{40,}={0,2}\b/g
48626
+ // long base64-ish run (opaque tokens)
48627
+ ];
48628
+ function redact(input) {
48629
+ let out = input;
48630
+ for (const re of SECRET_PATTERNS) out = out.replace(re, REDACTED);
48631
+ return out;
48632
+ }
48633
+ var SECRET_FORMAT_PATTERNS = [
48634
+ /\bmrrlin_agent_[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{32,}/g,
48635
+ // Mrrlin agent secret (selector.secret)
48636
+ /\bgh[oprsu]_[A-Za-z0-9]{20,}/g,
48637
+ // GitHub classic/OAuth/user/server/refresh tokens
48638
+ /\bgithub_pat_[A-Za-z0-9_]{20,}/g,
48639
+ // GitHub fine-grained PAT
48640
+ /\bsk-ant-[A-Za-z0-9_-]{20,}/g,
48641
+ // Anthropic API key or OAuth token
48642
+ /\bsk-(?:proj|svcacct|admin|or-v1)-[A-Za-z0-9_-]{20,}/g,
48643
+ // OpenAI project/service/admin, OpenRouter
48644
+ /\bsk-[A-Za-z0-9]{32,}\b/g,
48645
+ // OpenAI legacy, DeepSeek
48646
+ /\bgsk_[A-Za-z0-9]{20,}/g,
48647
+ // Groq
48648
+ /\bAIza[A-Za-z0-9_-]{35}/g,
48649
+ // Google API key
48650
+ /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
48651
+ // AWS access key id
48652
+ /\bxox[abposr]-[A-Za-z0-9-]{10,}/g,
48653
+ // Slack token
48654
+ /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY-----/g,
48655
+ /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
48656
+ // JWT (header.payload.signature)
48657
+ // Authorization: Bearer <token>, only when the token is token-shaped: 20+ chars with a digit.
48658
+ /\bBearer\s+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{20,}=*/g
48659
+ ];
48660
+ function redactSecretFormats(input) {
48661
+ let out = input;
48662
+ for (const re of SECRET_FORMAT_PATTERNS) out = out.replace(re, REDACTED);
48663
+ return out;
48664
+ }
48665
+
48599
48666
  // src/consensus/provider-limits.ts
48600
48667
  var import_node_child_process11 = require("node:child_process");
48601
48668
  var DEFAULT_STATUS_TIMEOUT_MS = 1e4;
@@ -48776,7 +48843,7 @@ function parseDispatch(agentId, stdout, exitCode) {
48776
48843
  async function dispatchHead(agentId, prompt, timeoutMs, deps) {
48777
48844
  const spawn9 = deps?.spawn ?? import_node_child_process12.spawn;
48778
48845
  const { command, prefixArgs } = resolveExternalAgentsCliInvocation(deps?.externalAgentsBin);
48779
- const args = ["dispatch", agentId, "--json", prompt];
48846
+ const args = ["dispatch", agentId, "--json", redactSecretFormats(prompt)];
48780
48847
  const r = await runExternalAgents(command, [...prefixArgs, ...args], "dispatch", timeoutMs, spawn9);
48781
48848
  if (r.error) return { ok: false, error: r.error };
48782
48849
  return parseDispatch(agentId, r.stdout, r.code);
@@ -57071,28 +57138,6 @@ var import_node_fs6 = require("node:fs");
57071
57138
  var import_promises6 = require("node:fs/promises");
57072
57139
  var import_node_path12 = require("node:path");
57073
57140
 
57074
- // src/redact.ts
57075
- var REDACTED = "[REDACTED]";
57076
- var SECRET_PATTERNS = [
57077
- /ghp_[A-Za-z0-9]{20,}/g,
57078
- // GitHub personal access token (classic)
57079
- /github_pat_[A-Za-z0-9_]{20,}/g,
57080
- // GitHub fine-grained PAT
57081
- /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi,
57082
- // Authorization: Bearer <token>
57083
- /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
57084
- // JWT (header.payload.signature)
57085
- /\b[A-Fa-f0-9]{32,}\b/g,
57086
- // long hex run (hashes, hex secrets)
57087
- /\b[A-Za-z0-9+/]{40,}={0,2}\b/g
57088
- // long base64-ish run (opaque tokens)
57089
- ];
57090
- function redact(input) {
57091
- let out = input;
57092
- for (const re of SECRET_PATTERNS) out = out.replace(re, REDACTED);
57093
- return out;
57094
- }
57095
-
57096
57141
  // src/telemetry/projector.ts
57097
57142
  var import_node_crypto8 = require("node:crypto");
57098
57143
  function isRecord4(x) {
@@ -57331,6 +57376,37 @@ function extractSeqAndTurnId(payload) {
57331
57376
  turnId: typeof p.turnId === "string" ? p.turnId : void 0
57332
57377
  };
57333
57378
  }
57379
+ var LOG_DIR_MODE = 448;
57380
+ var LOG_FILE_MODE = 384;
57381
+ function tightenBridgeLogPermissions(logsDir, opts = {}) {
57382
+ if ((opts.platform ?? process.platform) === "win32") return;
57383
+ const uid = opts.uid ?? process.getuid?.();
57384
+ if (uid === void 0) return;
57385
+ try {
57386
+ const dir = (0, import_node_fs6.lstatSync)(logsDir);
57387
+ if (!dir.isDirectory()) return;
57388
+ if (dir.uid === uid && (dir.mode & 511) !== (dir.mode & LOG_DIR_MODE)) {
57389
+ (0, import_node_fs6.chmodSync)(logsDir, dir.mode & LOG_DIR_MODE);
57390
+ }
57391
+ } catch {
57392
+ return;
57393
+ }
57394
+ let names;
57395
+ try {
57396
+ names = (0, import_node_fs6.readdirSync)(logsDir);
57397
+ } catch {
57398
+ return;
57399
+ }
57400
+ for (const name of names) {
57401
+ const fp = (0, import_node_path12.join)(logsDir, name);
57402
+ try {
57403
+ const st = (0, import_node_fs6.lstatSync)(fp);
57404
+ if (!st.isFile() || st.uid !== uid) continue;
57405
+ if ((st.mode & 511) !== (st.mode & LOG_FILE_MODE)) (0, import_node_fs6.chmodSync)(fp, st.mode & LOG_FILE_MODE);
57406
+ } catch {
57407
+ }
57408
+ }
57409
+ }
57334
57410
  function resolveBridgeLogEnabled(env = process.env) {
57335
57411
  const raw = (env.MRRLIN_DIRECTOR_BRIDGE_LOG ?? "").trim().toLowerCase();
57336
57412
  return !(raw === "0" || raw === "false");
@@ -57340,10 +57416,11 @@ function resolvePromptsEnabled(env = process.env) {
57340
57416
  return !(raw === "0" || raw === "false");
57341
57417
  }
57342
57418
  function createBridgeLogger(stateDir, env = process.env) {
57343
- if (!resolveBridgeLogEnabled(env)) return NOOP_LOGGER;
57344
57419
  const logsDir = (0, import_node_path12.join)(stateDir, "logs");
57420
+ tightenBridgeLogPermissions(logsDir);
57421
+ if (!resolveBridgeLogEnabled(env)) return NOOP_LOGGER;
57345
57422
  try {
57346
- (0, import_node_fs6.mkdirSync)(logsDir, { recursive: true });
57423
+ (0, import_node_fs6.mkdirSync)(logsDir, { recursive: true, mode: LOG_DIR_MODE });
57347
57424
  } catch {
57348
57425
  return NOOP_LOGGER;
57349
57426
  }
@@ -57402,7 +57479,7 @@ function createBridgeLogger(stateDir, env = process.env) {
57402
57479
  if (idsToday.includes(sessionId)) return;
57403
57480
  sessionsObj[day] = [...idsToday, sessionId];
57404
57481
  const tmpPath = `${sessionsPath}.tmp`;
57405
- (0, import_node_fs6.writeFileSync)(tmpPath, JSON.stringify(sessionsObj));
57482
+ (0, import_node_fs6.writeFileSync)(tmpPath, JSON.stringify(sessionsObj), { mode: LOG_FILE_MODE });
57406
57483
  (0, import_node_fs6.renameSync)(tmpPath, sessionsPath);
57407
57484
  };
57408
57485
  const write = (dir, rec) => {
@@ -57494,7 +57571,7 @@ function createBridgeLogger(stateDir, env = process.env) {
57494
57571
  ...recordingObligationUnresolved ? { recordingObligationUnresolved } : {},
57495
57572
  ...recordingObligationMultiResolve ? { recordingObligationMultiResolve } : {}
57496
57573
  }) + "\n";
57497
- (0, import_node_fs6.appendFileSync)(fileFor(), line);
57574
+ (0, import_node_fs6.appendFileSync)(fileFor(), line, { mode: LOG_FILE_MODE });
57498
57575
  if (rec.sessionId !== null) gainDay(rec.sessionId);
57499
57576
  } catch {
57500
57577
  }
@@ -57953,10 +58030,62 @@ var import_node_child_process14 = require("node:child_process");
57953
58030
  var import_node_fs9 = require("node:fs");
57954
58031
  var import_node_os8 = require("node:os");
57955
58032
  var import_node_path16 = __toESM(require("node:path"), 1);
57956
- var SYSTEM_PATH_PREFIXES = ["/etc", "/usr", "/bin", "/sbin", "/var", "/System", "/Library", "/opt", "/private"];
58033
+ var SYSTEM_PATH_PREFIXES = [
58034
+ "/etc",
58035
+ "/usr",
58036
+ "/bin",
58037
+ "/sbin",
58038
+ "/var",
58039
+ "/System",
58040
+ "/Library",
58041
+ "/opt",
58042
+ "/private",
58043
+ "/Applications",
58044
+ "/boot",
58045
+ "/dev",
58046
+ "/lib",
58047
+ "/lib32",
58048
+ "/lib64",
58049
+ "/nix",
58050
+ "/proc",
58051
+ "/run",
58052
+ "/snap",
58053
+ "/sys"
58054
+ ];
58055
+ var SYSTEM_PATH_EXCEPTIONS = ["/run/media"];
58056
+ var SENSITIVE_HOME_ENTRIES = [
58057
+ ".ssh",
58058
+ ".gnupg",
58059
+ ".aws",
58060
+ ".azure",
58061
+ ".config",
58062
+ ".docker",
58063
+ ".kube",
58064
+ ".local",
58065
+ ".password-store",
58066
+ ".codex",
58067
+ ".claude",
58068
+ ".gemini",
58069
+ ".mrrlin",
58070
+ "AppData",
58071
+ "Library"
58072
+ ];
58073
+ var SENSITIVE_HOME_EXCEPTIONS = [
58074
+ import_node_path16.default.join("Library", "CloudStorage"),
58075
+ import_node_path16.default.join("Library", "Mobile Documents")
58076
+ ];
57957
58077
  function isUnder(child, parent) {
57958
58078
  return child === parent || child.startsWith(parent.endsWith(import_node_path16.default.sep) ? parent : `${parent}${import_node_path16.default.sep}`);
57959
58079
  }
58080
+ function isUnderFolded(child, parent) {
58081
+ return isUnder(child.toLowerCase(), parent.toLowerCase());
58082
+ }
58083
+ function sensitiveHomeEntry(realPath, realHome) {
58084
+ const isInside = (entry) => isUnderFolded(realPath, import_node_path16.default.join(realHome, entry));
58085
+ if (SENSITIVE_HOME_EXCEPTIONS.some(isInside)) return null;
58086
+ const hit = SENSITIVE_HOME_ENTRIES.find(isInside);
58087
+ return hit === void 0 ? null : import_node_path16.default.join(realHome, hit);
58088
+ }
57960
58089
  function checkLocalPathRules(input, options = {}) {
57961
58090
  const trimmed = input.trim();
57962
58091
  if (!import_node_path16.default.isAbsolute(trimmed)) return { ok: false, code: "not_absolute", detail: `Path must be absolute, got: ${trimmed}` };
@@ -57967,19 +58096,32 @@ function checkLocalPathRules(input, options = {}) {
57967
58096
  return { ok: false, code: "not_found", detail: `Path does not exist or is not accessible: ${trimmed}` };
57968
58097
  }
57969
58098
  const systemPrefixes = options.systemPrefixes ?? SYSTEM_PATH_PREFIXES;
58099
+ const systemExceptions = options.systemPrefixExceptions ?? SYSTEM_PATH_EXCEPTIONS;
57970
58100
  const home = options.homeDir ?? (0, import_node_os8.homedir)();
57971
58101
  let realHome = home;
57972
58102
  try {
57973
58103
  realHome = (import_node_fs9.realpathSync.native ?? import_node_fs9.realpathSync)(home);
57974
58104
  } catch {
57975
58105
  }
57976
- if (realPath === import_node_path16.default.parse(realPath).root || realPath === realHome) {
57977
- return { ok: false, code: "forbidden_path", detail: `Refusing the home directory or a filesystem root: ${realPath}` };
58106
+ if (realPath === import_node_path16.default.parse(realPath).root || isUnderFolded(realHome, realPath)) {
58107
+ return {
58108
+ ok: false,
58109
+ code: "forbidden_path",
58110
+ detail: `Refusing the home directory, a folder containing it, or a filesystem root: ${realPath}`
58111
+ };
57978
58112
  }
57979
- const prefix = systemPrefixes.find((candidate) => isUnder(realPath, candidate));
58113
+ const prefix = systemExceptions.some((exception) => isUnderFolded(realPath, exception)) ? void 0 : systemPrefixes.find((candidate) => isUnderFolded(realPath, candidate));
57980
58114
  if (prefix) {
57981
58115
  return { ok: false, code: "forbidden_path", detail: `Refusing a path under the system prefix ${prefix}: ${realPath}` };
57982
58116
  }
58117
+ const sensitive = sensitiveHomeEntry(realPath, realHome);
58118
+ if (sensitive) {
58119
+ return {
58120
+ ok: false,
58121
+ code: "forbidden_path",
58122
+ detail: `Refusing ${realPath}: ${sensitive} holds credentials or tool configuration, not a project.`
58123
+ };
58124
+ }
57983
58125
  const uid = options.uid ?? (typeof process.getuid === "function" ? process.getuid() : void 0);
57984
58126
  let fd;
57985
58127
  try {
@@ -58146,6 +58288,13 @@ async function withCheckoutLock(locksDir, slug, fn, options = {}) {
58146
58288
  await release();
58147
58289
  }
58148
58290
  }
58291
+ function isDeliveryLockHeld(locksDir, key) {
58292
+ try {
58293
+ return import_proper_lockfile.default.checkSync((0, import_node_path17.join)(locksDir, `${key}.${DELIVERY_LOCK_NAME}.lock`), { stale: 5 * 6e4, realpath: false });
58294
+ } catch {
58295
+ return false;
58296
+ }
58297
+ }
58149
58298
 
58150
58299
  // src/delivery/local-git.ts
58151
58300
  var import_node_child_process15 = require("node:child_process");
@@ -58247,6 +58396,29 @@ function git(repo, args) {
58247
58396
  env: { ...buildLocalStepEnv(), GIT_TERMINAL_PROMPT: "0" }
58248
58397
  }).trim();
58249
58398
  }
58399
+ function gitRaw(repo, args) {
58400
+ return (0, import_node_child_process15.execFileSync)("git", gitArgs(repo, args), {
58401
+ encoding: "utf8",
58402
+ timeout: 3e4,
58403
+ stdio: ["ignore", "pipe", "pipe"],
58404
+ env: { ...buildLocalStepEnv(), GIT_TERMINAL_PROMPT: "0" }
58405
+ });
58406
+ }
58407
+ function gitTx(repo, args, stdin) {
58408
+ const result = (0, import_node_child_process15.spawnSync)("git", gitArgs(repo, args), {
58409
+ encoding: "utf8",
58410
+ timeout: 3e4,
58411
+ input: stdin,
58412
+ stdio: ["pipe", "pipe", "pipe"],
58413
+ env: { ...buildLocalStepEnv(), GIT_TERMINAL_PROMPT: "0" }
58414
+ });
58415
+ return {
58416
+ status: typeof result.status === "number" ? result.status : 1,
58417
+ stdout: (result.stdout ?? "").trim(),
58418
+ stderr: `${result.stderr ?? ""}${result.error ? `
58419
+ ${result.error.message}` : ""}`.trim()
58420
+ };
58421
+ }
58250
58422
  function gitTry(repo, args) {
58251
58423
  const result = (0, import_node_child_process15.spawnSync)("git", gitArgs(repo, args), {
58252
58424
  encoding: "utf8",
@@ -58468,6 +58640,13 @@ async function deliverLocalGit(input) {
58468
58640
  evidenceFile
58469
58641
  };
58470
58642
  }
58643
+ function indexOutside(worktreePath, files) {
58644
+ const conflicted = new Set(files);
58645
+ return git(worktreePath, ["ls-files", "-s"]).split("\n").filter((line) => {
58646
+ const match = /^(\d+) ([0-9a-f]+) (\d)\t(.*)$/.exec(line);
58647
+ return match !== null && match[3] === "0" && !conflicted.has(match[4]);
58648
+ }).sort().join("\n");
58649
+ }
58471
58650
  function integrationCommitMessage(defaultBranch, runId) {
58472
58651
  return `mrrlin: integrate ${defaultBranch} into ${runId}`;
58473
58652
  }
@@ -58492,21 +58671,22 @@ function integrateDefaultBranch(input) {
58492
58671
  return { state: "merged", defaultTipSha, mergeSha: git(input.worktreePath, ["rev-parse", "HEAD"]), preMergeHead };
58493
58672
  }
58494
58673
  const files = unmergedPaths(input.worktreePath);
58495
- if (files.length > 0) return { state: "conflict", defaultTipSha, files, preMergeHead };
58674
+ if (files.length > 0) {
58675
+ return { state: "conflict", defaultTipSha, files, preMergeHead, outsideConflicts: indexOutside(input.worktreePath, files) };
58676
+ }
58496
58677
  gitTry(input.worktreePath, ["merge", "--abort"]);
58497
58678
  throw new Error(`local_integration_failed: ${merge2.stderr || merge2.stdout}`);
58498
58679
  }
58499
58680
  function restoreRunBranch(input) {
58681
+ if (!isDeliveryLockHeld(input.deliveryLock.locksDir, input.deliveryLock.key)) {
58682
+ throw new Error(`restore_without_delivery_lock: ${input.runId} must be restored inside the delivery phase`);
58683
+ }
58500
58684
  const ref = `refs/heads/mrrlin/run-${input.runId}`;
58501
- abortIntegration({ worktreePath: input.worktreePath });
58502
- const tip = gitTry(input.worktreePath, ["rev-parse", "--verify", "-q", ref]).stdout || null;
58503
- if (tip !== input.expectedCurrent) return { restored: false, branchTip: tip };
58504
- if (tip !== input.preMergeHead) {
58505
- const moved = gitTry(input.worktreePath, ["update-ref", ref, input.preMergeHead, input.expectedCurrent]);
58506
- if (moved.status !== 0) {
58507
- return { restored: false, branchTip: gitTry(input.worktreePath, ["rev-parse", "--verify", "-q", ref]).stdout || null };
58508
- }
58685
+ const moved = gitTry(input.worktreePath, ["update-ref", ref, input.preMergeHead, input.expectedCurrent]);
58686
+ if (moved.status !== 0) {
58687
+ return { restored: false, branchTip: gitTry(input.worktreePath, ["rev-parse", "--verify", "-q", ref]).stdout || null };
58509
58688
  }
58689
+ abortIntegration({ worktreePath: input.worktreePath });
58510
58690
  git(input.worktreePath, ["checkout", "-q", "-f", `mrrlin/run-${input.runId}`]);
58511
58691
  git(input.worktreePath, ["reset", "-q", "--hard"]);
58512
58692
  git(input.worktreePath, ["clean", "-q", "-f", "-d"]);
@@ -58530,6 +58710,34 @@ function verifyIntegrationResolved(input) {
58530
58710
  if (withMarkers.length > 0) {
58531
58711
  return { ok: false, reason: "conflict_markers", detail: withMarkers.join(", ") };
58532
58712
  }
58713
+ if (input.afterAgentTurn) {
58714
+ if (gitTry(input.worktreePath, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]).status !== 0) {
58715
+ return {
58716
+ ok: false,
58717
+ reason: "merge_committed_by_agent",
58718
+ detail: "the merge is no longer in progress: the agent committed or aborted it instead of only staging"
58719
+ };
58720
+ }
58721
+ const conflicted = new Set(input.files);
58722
+ const strays = gitRaw(input.worktreePath, ["status", "--porcelain", "--untracked-files=all"]).split("\n").filter((line) => line.length > 3).filter((line) => line.startsWith("??") || line[1] !== " ").map((line) => {
58723
+ const entry = line.slice(3);
58724
+ return entry.includes(" -> ") ? entry.split(" -> ")[1] : entry;
58725
+ }).filter((file2) => !conflicted.has(file2));
58726
+ if (strays.length > 0) {
58727
+ return { ok: false, reason: "changed_outside_conflicts", detail: strays.join(", ") };
58728
+ }
58729
+ if (input.outsideConflicts !== void 0) {
58730
+ const now = indexOutside(input.worktreePath, input.files);
58731
+ if (now !== input.outsideConflicts) {
58732
+ const before = new Set(input.outsideConflicts.split("\n"));
58733
+ const after = new Set(now.split("\n"));
58734
+ const changed = /* @__PURE__ */ new Set();
58735
+ for (const line of [...before].filter((l) => !after.has(l))) changed.add(line.split(" ")[1] ?? line);
58736
+ for (const line of [...after].filter((l) => !before.has(l))) changed.add(line.split(" ")[1] ?? line);
58737
+ return { ok: false, reason: "changed_outside_conflicts", detail: [...changed].filter(Boolean).join(", ") };
58738
+ }
58739
+ }
58740
+ }
58533
58741
  if (gitTry(input.worktreePath, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]).status === 0) {
58534
58742
  const commit = gitTry(input.worktreePath, [
58535
58743
  "-c",
@@ -58588,29 +58796,37 @@ function applyLocalRun(input) {
58588
58796
  }
58589
58797
  }
58590
58798
  input.beforeRefUpdate?.();
58591
- if (input.runBranchGuard) {
58592
- const runTip = gitTry(repo, ["rev-parse", "--verify", "-q", input.runBranchGuard.ref]).stdout || null;
58593
- if (runTip !== input.runBranchGuard.sha) {
58594
- return {
58595
- applied: false,
58596
- reason: "run_branch_changed",
58597
- detail: `${input.runBranchGuard.ref} is at ${runTip ? runTip.slice(0, 12) : "nothing"}, not the signed ${input.runBranchGuard.sha.slice(0, 12)}`
58598
- };
58599
- }
58600
- }
58601
- const update = gitTry(repo, [
58799
+ input.beforeRefTransaction?.();
58800
+ const transaction = [
58801
+ "start",
58802
+ ...input.runBranchGuard ? [`verify ${input.runBranchGuard.ref} ${input.runBranchGuard.sha}`] : [],
58803
+ `update ${ref} ${input.headSha} ${old}`,
58804
+ "commit",
58805
+ ""
58806
+ ].join("\n");
58807
+ const update = gitTx(repo, [
58602
58808
  "-c",
58603
58809
  `core.hooksPath=${input.hooksDir}`,
58604
58810
  "update-ref",
58605
58811
  "-m",
58606
58812
  `mrrlin: land ${input.headSha.slice(0, 12)}`,
58607
- ref,
58608
- input.headSha,
58609
- old
58610
- ]);
58813
+ "--stdin"
58814
+ ], transaction);
58611
58815
  if (update.status !== 0) {
58612
- return { applied: false, reason: "default_branch_moved", detail: update.stderr || update.stdout };
58816
+ const detail = update.stderr || update.stdout;
58817
+ if (input.runBranchGuard) {
58818
+ const runTip = gitTry(repo, ["rev-parse", "--verify", "-q", input.runBranchGuard.ref]).stdout || null;
58819
+ if (runTip !== input.runBranchGuard.sha) {
58820
+ return {
58821
+ applied: false,
58822
+ reason: "run_branch_changed",
58823
+ detail: `${input.runBranchGuard.ref} is at ${runTip ? runTip.slice(0, 12) : "nothing"}, not the signed ${input.runBranchGuard.sha.slice(0, 12)}`
58824
+ };
58825
+ }
58826
+ }
58827
+ return { applied: false, reason: "default_branch_moved", detail };
58613
58828
  }
58829
+ input.afterRefUpdate?.();
58614
58830
  const updated = [];
58615
58831
  for (const holder of holders) {
58616
58832
  const moved = gitTry(holder, ["read-tree", "-m", "-u", old, input.headSha]);
@@ -58635,8 +58851,30 @@ function applyLocalRun(input) {
58635
58851
  if (/overwritten/.test(detail)) return { applied: false, reason: "would_overwrite_files", detail };
58636
58852
  return { applied: false, reason: "ff_failed", detail };
58637
58853
  }
58854
+ const known = new Set(holders.map((holder) => realpathOr(holder)));
58855
+ const appeared = worktreesWithBranch(repo, input.defaultBranch).filter((holder) => !known.has(realpathOr(holder)));
58856
+ const nowRef = gitTry(repo, ["rev-parse", "--verify", "-q", ref]).stdout || "missing";
58857
+ const off = holders.filter((holder) => {
58858
+ const head = gitTry(holder, ["rev-parse", "HEAD"]).stdout;
58859
+ const indexTree = gitTry(holder, ["write-tree"]).stdout;
58860
+ return head !== input.headSha || indexTree !== gitTry(holder, ["rev-parse", `${input.headSha}^{tree}`]).stdout;
58861
+ });
58862
+ if (nowRef !== input.headSha || off.length > 0 || appeared.length > 0) {
58863
+ return {
58864
+ applied: false,
58865
+ reason: "landing_partial",
58866
+ detail: `${ref} is at ${nowRef.slice(0, 12)} (was ${old.slice(0, 12)}, landing ${input.headSha.slice(0, 12)}); worktrees whose HEAD or index is not the landed commit: ${off.length > 0 ? off.join(", ") : "none"}; worktrees that started holding ${input.defaultBranch} during the landing: ${appeared.length > 0 ? appeared.join(", ") : "none"}`
58867
+ };
58868
+ }
58638
58869
  return { applied: true, previousSha: old };
58639
58870
  }
58871
+ function realpathOr(target) {
58872
+ try {
58873
+ return (0, import_node_fs11.realpathSync)(target);
58874
+ } catch {
58875
+ return target;
58876
+ }
58877
+ }
58640
58878
  function worktreesWithBranch(repo, branch) {
58641
58879
  const holders = [];
58642
58880
  for (const block of git(repo, ["worktree", "list", "--porcelain"]).split(/\n\s*\n/)) {
@@ -58787,6 +59025,46 @@ async function hasCommitsToPush(opts, execGit = defaultGitExec) {
58787
59025
  ).trim();
58788
59026
  return Number(count2) > 0;
58789
59027
  }
59028
+ var AGENT_CONTROLLED_GIT_CONFIG = [
59029
+ "^url\\.",
59030
+ // url.<base>.insteadOf / pushInsteadOf: rewrite where origin points
59031
+ "^remote\\..*\\.(pushurl|proxy|vcs)$",
59032
+ // push elsewhere, through a proxy, or via a helper program
59033
+ "^core\\.(sshcommand|askpass|gitproxy)$",
59034
+ // commands run by the transport
59035
+ "^credential\\..*helper$",
59036
+ // credential.helper and credential.<url>.helper
59037
+ "^http\\..*(proxy|sslverify|curloptresolve)$",
59038
+ // send the HTTPS push (and its token) elsewhere
59039
+ "^include\\.",
59040
+ // include.path: a file whose keys --local alone would not read
59041
+ "^includeif\\."
59042
+ // includeIf.<condition>.path: same, conditionally
59043
+ ].join("|");
59044
+ function gitExitStatus(error51) {
59045
+ const status = error51?.status;
59046
+ return typeof status === "number" ? status : null;
59047
+ }
59048
+ function agentControlledGitConfig(worktreePath, env, execGit) {
59049
+ const read = (args, what) => {
59050
+ try {
59051
+ return execGit(["-C", worktreePath, "config", ...args], env);
59052
+ } catch (error51) {
59053
+ if (gitExitStatus(error51) === 1) return null;
59054
+ const detail = error51 instanceof Error ? error51.message.split("\n")[0] : String(error51);
59055
+ throw new Error(`push aborted: could not read the worktree's ${what} git config: ${detail}`);
59056
+ }
59057
+ };
59058
+ const query = (scope) => {
59059
+ const args = [scope, "--includes", "--name-only", "--get-regexp", AGENT_CONTROLLED_GIT_CONFIG];
59060
+ const keys = read(args, scope.slice(2)) ?? "";
59061
+ return keys.split("\n").map((line) => line.trim()).filter(Boolean);
59062
+ };
59063
+ const found = query("--local");
59064
+ const worktreeConfig = read(["--local", "--bool", "--get", "extensions.worktreeconfig"], "local");
59065
+ if (worktreeConfig?.trim() === "true") found.push(...query("--worktree"));
59066
+ return [...new Set(found)];
59067
+ }
58790
59068
  async function doPushRunBranch(opts, execGit) {
58791
59069
  const branch = worktreeBranch(opts.runId);
58792
59070
  const hooksDir = prepareDisabledGitHooksDir(opts.locksDir, opts.runId);
@@ -58800,17 +59078,11 @@ async function doPushRunBranch(opts, execGit) {
58800
59078
  if (opts.expectedSha && localSha !== opts.expectedSha) {
58801
59079
  throw new Error(`push aborted: ${branch} is ${localSha}, expected ${opts.expectedSha}`);
58802
59080
  }
58803
- let localConfig = "";
58804
- try {
58805
- localConfig = execGit(
58806
- ["-C", opts.worktreePath, "config", "--local", "--get-regexp", "^(url\\.|remote\\..*\\.pushurl|core\\.sshcommand)"],
58807
- pushEnv
59081
+ const planted = agentControlledGitConfig(opts.worktreePath, pushEnv, execGit);
59082
+ if (planted.length > 0) {
59083
+ throw new Error(
59084
+ `push aborted: agent-controlled git config present in worktree: ${planted.join(", ")}. Move these settings to your global git config (git config --global) or remove them from the repository's .git/config, then retry.`
58808
59085
  );
58809
- } catch {
58810
- localConfig = "";
58811
- }
58812
- if (localConfig.trim()) {
58813
- throw new Error(`push aborted: agent-controlled git config present in worktree: ${localConfig.trim().split("\n")[0]}`);
58814
59086
  }
58815
59087
  let originUrl = "";
58816
59088
  try {
@@ -58890,6 +59162,14 @@ async function sweepOrphanWorktrees(opts) {
58890
59162
  }
58891
59163
 
58892
59164
  // src/local-apply.ts
59165
+ function readIntegratedTip(localEvidenceDir, runId) {
59166
+ try {
59167
+ const evidence = JSON.parse((0, import_node_fs13.readFileSync)(import_node_path20.default.join(localEvidenceDir, `${runId}.json`), "utf8"));
59168
+ return typeof evidence.integration?.defaultTipSha === "string" ? evidence.integration.defaultTipSha : null;
59169
+ } catch {
59170
+ return null;
59171
+ }
59172
+ }
58893
59173
  function gitEnv() {
58894
59174
  return { ...buildLocalStepEnv(), GIT_TERMINAL_PROMPT: "0" };
58895
59175
  }
@@ -58917,6 +59197,7 @@ function localLandingNote(outcome, ctx) {
58917
59197
  conflict: `${taskId} could not be merged with the newest ${d}: conflicts in ${detail}. Ask the Director to create a follow-up task that reconciles ${taskId} with the current ${d}.`,
58918
59198
  build_failed: `${taskId} merged with the newest ${d}, but the build or tests failed afterwards (${detail}). Ask the Director to create a follow-up task to fix it.`,
58919
59199
  ff_failed: `${taskId} could not be placed into your ${d} branch: git refused the update (${detail}). Ask the Director to apply ${taskId} again.`,
59200
+ run_reclaimed: `${taskId} is finished, but it was not placed into your ${d} branch automatically because another Mrrlin bridge took over run ${ctx.runId}. Ask the Director to apply ${taskId} again.`,
58920
59201
  landing_partial: `${taskId} was only partly placed into your ${d} branch, and Mrrlin could not undo it cleanly (${detail}). Your work is not lost, but please do not change the project folder yet: ask the Director to help you check it.`,
58921
59202
  run_branch_changed: `${taskId} was not placed into your ${d} branch: its branch mrrlin/run-${ctx.runId} got new commits after the task was finished and checked, so nothing was changed. Ask the Director to restart the run if those commits belong in it.`,
58922
59203
  delivery_ref_mismatch: `${taskId} was not placed into your ${d} branch: the run's recorded result (${detail}) does not belong to this project and run ${ctx.runId}, so nothing was changed. Ask the Director to look into run ${ctx.runId}.`
@@ -58930,8 +59211,17 @@ function overwrittenFilesDetail(detail) {
58930
59211
  async function fileLocalLandingNote(client, projectSlug, outcome, ctx) {
58931
59212
  const detail = outcome === "would_overwrite_files" ? overwrittenFilesDetail(ctx.detail) : ctx.detail;
58932
59213
  const note = localLandingNote(outcome, { ...ctx, detail });
58933
- await client.createInboxItem(projectSlug, { id: note.id, kind: "info", taskId: ctx.taskId, body: note.body }).catch(() => {
58934
- });
59214
+ const logFailure = (step) => (error51) => console.warn(
59215
+ JSON.stringify({
59216
+ event: "local_landing_note_failed",
59217
+ step,
59218
+ projectSlug,
59219
+ runId: ctx.runId,
59220
+ outcome,
59221
+ error: error51 instanceof Error ? error51.message : String(error51)
59222
+ })
59223
+ );
59224
+ await client.createInboxItem(projectSlug, { id: note.id, kind: "info", taskId: ctx.taskId, body: note.body }).catch(logFailure("inbox_note"));
58935
59225
  await client.appendExecutionArtifact(projectSlug, ctx.runId, {
58936
59226
  id: `EA-${import_node_crypto13.default.randomUUID()}`,
58937
59227
  kind: "checkpoint",
@@ -58943,8 +59233,7 @@ async function fileLocalLandingNote(client, projectSlug, outcome, ctx) {
58943
59233
  previousSha: ctx.previousSha,
58944
59234
  headSha: ctx.headSha
58945
59235
  }
58946
- }).catch(() => {
58947
- });
59236
+ }).catch(logFailure("local_apply_artifact"));
58948
59237
  }
58949
59238
  async function applyLocalRunForTool(deps, input) {
58950
59239
  const { projectSlug, runId } = input;
@@ -59011,13 +59300,16 @@ async function applyLocalRunForTool(deps, input) {
59011
59300
  return refuse2("delivery_ref_mismatch", run3.deliveryRef ?? "");
59012
59301
  }
59013
59302
  const artifacts = await deps.client.listExecutionArtifacts(projectSlug, runId);
59014
- const signed = artifacts.some((artifact) => {
59015
- const receipt = artifact.payload?.[LOCAL_BUILD_RECEIPT_PAYLOAD_KEY];
59016
- return artifact.kind === "verification" && receipt?.localBuild?.headSha === delivered.headSha;
59017
- });
59303
+ const signed = artifacts.filter((artifact) => artifact.kind === "verification").map(
59304
+ (artifact) => artifact.payload?.[LOCAL_BUILD_RECEIPT_PAYLOAD_KEY]?.localBuild
59305
+ ).find((localBuild) => localBuild?.headSha === delivered.headSha);
59018
59306
  if (!signed) {
59019
59307
  return refuse2("delivery_ref_mismatch", `${run3.deliveryRef ?? ""} has no matching signed build receipt`);
59020
59308
  }
59309
+ const expectedTip = readIntegratedTip(deps.localEvidenceDir, runId) ?? (typeof signed.baseSha === "string" ? signed.baseSha : null);
59310
+ if (!expectedTip) {
59311
+ return refuse2("delivery_ref_mismatch", `${run3.deliveryRef ?? ""}: the ${defaultBranch} commit it was built on is unknown`);
59312
+ }
59021
59313
  const tip = gitRead(checkout, ["rev-parse", "--verify", "-q", `refs/heads/${runBranch}`]);
59022
59314
  if (tip !== delivered.headSha) {
59023
59315
  if (tip && gitRead(checkout, ["merge-base", "--is-ancestor", delivered.headSha, tip]) !== null) {
@@ -59057,6 +59349,15 @@ async function applyLocalRunForTool(deps, input) {
59057
59349
  };
59058
59350
  const reintegrate = async (worktreePath) => {
59059
59351
  const integration = integrateDefaultBranch({ worktreePath, defaultBranch, runId, hooksDir });
59352
+ if (integration.state === "up_to_date") {
59353
+ record2({ integration, applied: false, reason: "default_branch_moved", signedHead: delivered.headSha });
59354
+ return {
59355
+ outcome: "default_branch_moved",
59356
+ previousSha: null,
59357
+ headSha: delivered.headSha,
59358
+ detail: `${defaultBranch} was moved back to ${integration.defaultTipSha.slice(0, 12)}`
59359
+ };
59360
+ }
59060
59361
  if (integration.state === "conflict") {
59061
59362
  record2({ integration, applied: false, reason: "conflict", files: integration.files });
59062
59363
  return { outcome: "conflict", previousSha: null, headSha: delivered.headSha, detail: integration.files.join(", ") };
@@ -59067,24 +59368,40 @@ async function applyLocalRunForTool(deps, input) {
59067
59368
  record2({ integration, applied: false, reason: "build_failed", headSha, steps: build.steps, logs: build.logs });
59068
59369
  return { outcome: "build_failed", previousSha: null, headSha, detail: build.failed.command };
59069
59370
  }
59371
+ const changedByBuild = (0, import_node_child_process17.execFileSync)("git", ["-C", worktreePath, "status", "--porcelain", "--untracked-files=no"], {
59372
+ encoding: "utf8",
59373
+ stdio: ["ignore", "pipe", "pipe"],
59374
+ timeout: 3e4,
59375
+ env: gitEnv()
59376
+ }).split("\n").filter((line) => line.length > 3).map((line) => line.slice(3));
59377
+ if (changedByBuild.length > 0) {
59378
+ const detail = `the build changed tracked files: ${changedByBuild.join(", ")}`;
59379
+ record2({ integration, applied: false, reason: "build_failed", headSha, detail, steps: build.steps, logs: build.logs });
59380
+ return { outcome: "build_failed", previousSha: null, headSha, detail };
59381
+ }
59070
59382
  const second = await land(headSha, integration.defaultTipSha);
59071
59383
  record2({
59072
59384
  integration,
59073
59385
  applied: second.applied,
59074
59386
  reason: second.applied ? null : second.reason,
59075
59387
  headSha,
59388
+ // The landed commit is a new integration merge whose parents are [signed head, default tip].
59389
+ landedSha: second.applied ? headSha : null,
59390
+ signedHead: delivered.headSha,
59391
+ parents: (gitRead(worktreePath, ["rev-list", "--parents", "-n", "1", headSha]) ?? "").split(" ").slice(1),
59076
59392
  steps: build.steps,
59077
59393
  logs: build.logs
59078
59394
  });
59079
59395
  if (second.applied) return { outcome: "applied", previousSha: second.previousSha, headSha, detail: "" };
59080
59396
  return { outcome: second.reason, previousSha: null, headSha, detail: second.detail };
59081
59397
  };
59398
+ deps.onQueuedForDeliveryLock?.();
59082
59399
  const landing = await withCheckoutLock(
59083
59400
  deps.locksDir,
59084
59401
  deliveryLockKey(checkout),
59085
59402
  async () => {
59086
59403
  if (!runBranchStillSigned()) return branchChanged();
59087
- const first = await land(delivered.headSha);
59404
+ const first = await land(delivered.headSha, expectedTip);
59088
59405
  if (first.applied) return { outcome: "applied", previousSha: first.previousSha, headSha: delivered.headSha, detail: "" };
59089
59406
  if (first.reason !== "default_branch_moved") {
59090
59407
  return { outcome: first.reason, previousSha: null, headSha: delivered.headSha, detail: first.detail };
@@ -60221,8 +60538,8 @@ var mcpToolDescriptions = {
60221
60538
  [mcpToolNames.restartExecutionRun]: "Restart a parked execution run (handoff_requested or handoff_queued) back to pending so the queue can resume it.",
60222
60539
  [mcpToolNames.stopExecutionRun]: "Operator stop: park an in-flight execution run at handoff_requested with blockingReason operator_stopped. Parks rather than cancels, so restart_execution_run can resume it later. Refuses (409) a run already at a terminal status.",
60223
60540
  [mcpToolNames.resolveHandoff]: "Resolve a tool_assisted handoff for an execution run (outcome success|failure). Success returns the task to the board; failure opens a new handoff cycle. Idempotent.",
60224
- [mcpToolNames.registerLocalCheckout]: "Register the operator-local git checkout path for `projectSlug` so execution-run workers run code on this machine instead of the cloud. Validates the path is a git repo whose `origin` remote matches `repo`, then writes the (slug -> absolute path) entry to the operator-local checkout registry. For a project with no GitHub (code kept only on this computer), omit `repo`: the path must then be the top of a git repository owned by the operator, not the home folder or a system folder. Call this after locating the checkout on disk (e.g. via filesystem search) and confirming the choice with the user in chat \u2014 they should NOT have to paste the path into Settings. Returns `{ projectSlug, path, confirmedAt }`. Idempotent: re-registering overwrites the prior entry.",
60225
- [mcpToolNames.initLocalRepo]: "For a project with no GitHub whose folder is not a git repository yet: run `git init` and one initial commit in the operator's own folder, then register it. Call ONLY after asking the operator in chat, in plain words, and getting an explicit yes; pass operatorConsent: true. Refuses a folder already inside a git repository, the home folder, a filesystem root or a system folder. Adds a default .gitignore (node_modules, build output, .env files) only when the folder has none.",
60541
+ [mcpToolNames.registerLocalCheckout]: "Register the operator-local git checkout path for `projectSlug` so execution-run workers run code on this machine instead of the cloud. Validates the path is a git repo whose `origin` remote matches `repo`, then writes the (slug -> absolute path) entry to the operator-local checkout registry. For a project with no GitHub (code kept only on this computer), omit `repo`: the path must then be the top of a git repository owned by the operator, not the home folder, a system folder or a credentials/settings folder such as ~/.ssh, ~/.aws or ~/.config. Call this after locating the checkout on disk (e.g. via filesystem search) and confirming the choice with the user in chat \u2014 they should NOT have to paste the path into Settings. Returns `{ projectSlug, path, confirmedAt }`. Idempotent: re-registering overwrites the prior entry.",
60542
+ [mcpToolNames.initLocalRepo]: "For a project with no GitHub whose folder is not a git repository yet: run `git init` and one initial commit in the operator's own folder, then register it. Call ONLY after asking the operator in chat, in plain words, and getting an explicit yes; pass operatorConsent: true. Refuses a folder already inside a git repository, the home folder or any folder containing it, a filesystem root, a system folder, or a credentials/settings folder such as ~/.ssh, ~/.aws, ~/.gnupg or ~/.config. Adds a default .gitignore (node_modules, build output, .env files) only when the folder has none.",
60226
60543
  [mcpToolNames.setProjectCodeHost]: "Record where this project's code lives, after the operator answered the code-host question: local (keep it on this computer), github or gitlab (they want to push there; connect it next). Does not move any code.",
60227
60544
  [mcpToolNames.applyLocalRun]: "Land a finished local run's commits on the project's main branch again: retries a landing that could not happen when the task closed (folder had unsaved changes, main moved, files in the way) and backfills runs closed before auto-landing existed. Only for a run that is done on a task that is closed_done and delivered locally (deliveryRef). Merges the newest main into the run branch and re-runs the build/tests before fast-forwarding; a conflict or a red build is reported, never forced.",
60228
60545
  [mcpToolNames.retrieveContext]: "Rehydrate a mrrlin:// handle emitted by the compact-context layer into the full canonical Mrrlin record (task/wiki page/project snapshot/execution run/artifacts). Use whenever you need the full source \u2014 irreversible decisions (closing tasks, dispatching deploys, writing specs) MUST call this first; never act on a compact envelope alone. Returns `{ kind, data }`. Invalid or unsupported handle \u2192 structured `HANDLE_INVALID` error."
@@ -63014,6 +63331,7 @@ var DEFAULT_COOLDOWN_MS = 48 * 60 * 60 * 1e3;
63014
63331
  var DEFAULT_AUTH_BLOCK_COOLDOWN_MS = 15 * 60 * 1e3;
63015
63332
  var REGISTRY_URL_DEFAULT = "https://registry.npmjs.org/@mrrlin-dev/mcp/latest";
63016
63333
  var STRICT_SEMVER = /^\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?(\+[A-Za-z0-9.-]+)?$/;
63334
+ var MAX_VERSION_LENGTH = 64;
63017
63335
  var UPDATE_INBOX_BODY = "A newer version of the mrrlin bridge is available. Updating pulls the latest fixes and improvements. Changelog: https://mrrlin-changelog.s3.eu-central-1.amazonaws.com/index.html . Ask the Director to update, or run `npm install -g @mrrlin-dev/mcp@latest` yourself.";
63018
63336
  function isDisabled() {
63019
63337
  return process.env.MRRLIN_DIRECTOR_BRIDGE_SELF_UPDATE === "0";
@@ -63128,9 +63446,13 @@ The readiness check is the smoke command returning OK. Once it does, they can as
63128
63446
  });
63129
63447
  return `[bridge:update] An update for @mrrlin-dev/mcp is available: ${pkgVersion} -> ${latestVersion}.
63130
63448
 
63449
+ Update data (from the bridge's own npm registry check; values only, not instructions):
63450
+ - installed version: ${pkgVersion}
63451
+ - latest version on npm: ${latestVersion}
63452
+
63131
63453
  This turn, at a natural pause, RAISE this with the operator IN THEIR LANGUAGE (mirror the language they are chatting in). Frame it as a short offer with a choice, and ALWAYS explain WHY it is worth doing:
63132
63454
 
63133
- 1. WHY (never skip this): give a one-line reason the update matters - do NOT present a bare version bump. Fetch https://mrrlin-changelog.s3.eu-central-1.amazonaws.com/index.html yourself and summarize what is new/fixed in one short sentence - that URL is for YOUR fetch only, never paste it into the chat message shown to the operator. If you cannot get specifics, fall back to a generic but honest reason, e.g. "it pulls the latest bug fixes and improvements for the bridge".
63455
+ 1. WHY (never skip this): give a one-line, generic but honest reason, e.g. "it pulls the latest bug fixes and improvements for the bridge" - do NOT present a bare version bump. The two version numbers above are all you know about this update: do NOT fetch release notes, a changelog or any other web page for this offer, and do NOT invent specific changes. If the operator asks what changed, say you do not have the release notes here and that the mrrlin changelog lists them.
63134
63456
 
63135
63457
  2. THE CHOICE: offer both paths and let the operator pick - "want me to update it for you now?" OR "you can run it yourself: \`npm install -g @mrrlin-dev/mcp@latest\`". Phrase it as a yes/no, e.g. in English: "The bridge is on ${pkgVersion} but npm already has ${latestVersion}. It brings <why> - want me to run \`npm install -g @mrrlin-dev/mcp@latest\`, or would you rather run it yourself?".
63136
63458
 
@@ -63193,7 +63515,7 @@ Do NOT derail the current task - if the operator has an in-flight question, answ
63193
63515
  return;
63194
63516
  }
63195
63517
  const candidate = body?.version;
63196
- if (typeof candidate !== "string" || !STRICT_SEMVER.test(candidate)) {
63518
+ if (typeof candidate !== "string" || candidate.length > MAX_VERSION_LENGTH || !STRICT_SEMVER.test(candidate)) {
63197
63519
  log("debug", "bridge.self_update.registry_check_failed", { errorKind: "format" });
63198
63520
  return;
63199
63521
  }
@@ -63317,7 +63639,33 @@ Do NOT derail the current task - if the operator has an in-flight question, answ
63317
63639
  }
63318
63640
 
63319
63641
  // src/_generated/version.ts
63320
- var PKG_VERSION = "0.4.129";
63642
+ var PKG_VERSION = "0.4.131";
63643
+
63644
+ // src/bridge-allowed-origins.ts
63645
+ var DEFAULT_DIRECTOR_BRIDGE_ALLOWED_ORIGINS = [
63646
+ "http://localhost:*",
63647
+ "http://127.0.0.1:*",
63648
+ "http://[::1]:*",
63649
+ // Production web.
63650
+ "https://app.mrrlin.com"
63651
+ ];
63652
+ var STAGING_DIRECTOR_BRIDGE_ALLOWED_ORIGIN = "https://staging.mrrlin.com";
63653
+ function defaultDirectorBridgeAllowedOrigins(apiBaseUrl) {
63654
+ const origins = [...DEFAULT_DIRECTOR_BRIDGE_ALLOWED_ORIGINS];
63655
+ if (isStagingApiBaseUrl(apiBaseUrl)) origins.push(STAGING_DIRECTOR_BRIDGE_ALLOWED_ORIGIN);
63656
+ return origins;
63657
+ }
63658
+ function readDirectorBridgeAllowedOrigins(env = process.env) {
63659
+ const raw = (env.MRRLIN_DIRECTOR_BRIDGE_ALLOWED_ORIGINS ?? "").trim();
63660
+ if (!raw) {
63661
+ return defaultDirectorBridgeAllowedOrigins(
63662
+ readDirectorBridgeApiBaseUrl(env.MRRLIN_API_BASE_URL ?? "")
63663
+ );
63664
+ }
63665
+ if (raw === "*" || raw.toLowerCase() === "all") return null;
63666
+ const parts = raw.split(",").map((p) => p.trim().replace(/\/$/, "")).filter(Boolean);
63667
+ return parts.length > 0 ? parts : null;
63668
+ }
63321
63669
 
63322
63670
  // src/receipt-signing.ts
63323
63671
  var import_node_crypto17 = __toESM(require("node:crypto"), 1);
@@ -65001,6 +65349,8 @@ function startSchedulerTickLoop(deps) {
65001
65349
  // src/retention/pruner.ts
65002
65350
  var import_promises11 = require("node:fs/promises");
65003
65351
  var import_node_path31 = __toESM(require("node:path"), 1);
65352
+ var LOG_DIR_MODE2 = 448;
65353
+ var LOG_FILE_MODE2 = 384;
65004
65354
  function protectedDayFloor(protectedSinceMs) {
65005
65355
  return new Date(protectedSinceMs).toISOString().slice(0, 10);
65006
65356
  }
@@ -65111,7 +65461,7 @@ async function prune(stateDir, bounds) {
65111
65461
  if (fileExists) {
65112
65462
  for (const day of daysRemoved) Reflect.deleteProperty(sessionsObj, day);
65113
65463
  const tmpPath = `${sessionsPath}.tmp`;
65114
- await (0, import_promises11.writeFile)(tmpPath, JSON.stringify(sessionsObj));
65464
+ await (0, import_promises11.writeFile)(tmpPath, JSON.stringify(sessionsObj), { mode: LOG_FILE_MODE2 });
65115
65465
  await (0, import_promises11.rename)(tmpPath, sessionsPath);
65116
65466
  }
65117
65467
  }
@@ -65120,10 +65470,10 @@ async function prune(stateDir, bounds) {
65120
65470
  async function emitPruneLog(stateDir, daysRemoved, reasonByDay, bytesRemoved, oldestDayRemaining) {
65121
65471
  const logsDir = import_node_path31.default.join(stateDir, "logs");
65122
65472
  const logPath = import_node_path31.default.join(logsDir, ".pruned.jsonl");
65123
- await (0, import_promises11.mkdir)(logsDir, { recursive: true });
65473
+ await (0, import_promises11.mkdir)(logsDir, { recursive: true, mode: LOG_DIR_MODE2 });
65124
65474
  const record2 = { ts: Date.now(), dir: "logs", daysRemoved, reasonByDay, bytesRemoved, oldestDayRemaining };
65125
65475
  await (0, import_promises11.appendFile)(logPath, `${JSON.stringify(record2)}
65126
- `);
65476
+ `, { mode: LOG_FILE_MODE2 });
65127
65477
  }
65128
65478
 
65129
65479
  // src/telemetry/turn-aggregator.ts
@@ -68197,6 +68547,7 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
68197
68547
  if (localGitMode) {
68198
68548
  const deliveryWorktree = worktreePath;
68199
68549
  const deliveryTask = readiness.task;
68550
+ const deliveryKey = deliveryLockKey(localCheckoutRealPath ?? checkout?.path ?? deliveryWorktree);
68200
68551
  const deliveryHeartbeat = setInterval(() => {
68201
68552
  void deps.client.touchExecutionRun(projectSlug, runId, leaseId).catch(() => {
68202
68553
  });
@@ -68206,7 +68557,7 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
68206
68557
  deps.locksDir ?? "/tmp/mrrlin-locks",
68207
68558
  // Keyed by the checkout folder, not the project: two projects registered to one folder
68208
68559
  // serialize their delivery phases too.
68209
- deliveryLockKey(localCheckoutRealPath ?? checkout?.path ?? deliveryWorktree),
68560
+ deliveryKey,
68210
68561
  async () => {
68211
68562
  if (!await runHasLease(deps, projectSlug, runId, leaseId)) return;
68212
68563
  const integrated = await integrateBeforeReceipt(deps, {
@@ -68220,20 +68571,50 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
68220
68571
  codex,
68221
68572
  threadId,
68222
68573
  effort: effortDecision?.level,
68223
- remainingTurnBudgetMs
68574
+ remainingTurnBudgetMs,
68575
+ deliveryLock: { locksDir: deps.locksDir ?? "/tmp/mrrlin-locks", key: deliveryKey }
68224
68576
  });
68225
68577
  if (!integrated.proceed) return;
68226
- await deliverLocalRun(deps, {
68227
- projectSlug,
68228
- task: deliveryTask,
68229
- runId,
68230
- leaseId,
68231
- worktreePath: deliveryWorktree,
68232
- checkoutRealPath: localCheckoutRealPath ?? checkout?.path ?? deliveryWorktree,
68233
- baseSha: localBaseSha,
68234
- defaultBranch: resolvedDefaultBranch ?? "main",
68235
- integration: integrated.integration ?? null
68236
- });
68578
+ const delivery = { settled: false };
68579
+ try {
68580
+ await deliverLocalRun(deps, {
68581
+ projectSlug,
68582
+ task: deliveryTask,
68583
+ runId,
68584
+ leaseId,
68585
+ worktreePath: deliveryWorktree,
68586
+ checkoutRealPath: localCheckoutRealPath ?? checkout?.path ?? deliveryWorktree,
68587
+ baseSha: localBaseSha,
68588
+ defaultBranch: resolvedDefaultBranch ?? "main",
68589
+ integration: integrated.integration ?? null,
68590
+ delivery
68591
+ });
68592
+ } catch (error51) {
68593
+ const merge2 = integrated.integration;
68594
+ if (delivery.settled || !merge2?.mergeSha) throw error51;
68595
+ const message = error51 instanceof Error ? error51.message : String(error51);
68596
+ const acted = await failRun(deps, projectSlug, deliveryTask, runId, leaseId, `local_delivery_failed: ${message}`);
68597
+ const state = await readSettlementState(deps, projectSlug, deliveryTask.id, runId, 1);
68598
+ const settledElsewhere = !state || state.runDone || state.taskTerminal;
68599
+ if ((!acted || /lease/i.test(message)) && !settledElsewhere) {
68600
+ try {
68601
+ restoreRunBranch({
68602
+ worktreePath: deliveryWorktree,
68603
+ runId,
68604
+ preMergeHead: merge2.preMergeHead,
68605
+ expectedCurrent: merge2.mergeSha,
68606
+ deliveryLock: { locksDir: deps.locksDir ?? "/tmp/mrrlin-locks", key: deliveryKey }
68607
+ });
68608
+ } catch (restoreError) {
68609
+ console.warn(JSON.stringify({
68610
+ event: "local_integration_restore_failed",
68611
+ projectSlug,
68612
+ runId,
68613
+ error: restoreError instanceof Error ? restoreError.message : String(restoreError)
68614
+ }));
68615
+ }
68616
+ }
68617
+ }
68237
68618
  },
68238
68619
  { name: DELIVERY_LOCK_NAME, maxWaitMs: DELIVERY_LOCK_MAX_WAIT_MS }
68239
68620
  ).finally(() => clearInterval(deliveryHeartbeat));
@@ -68990,7 +69371,13 @@ async function integrateBeforeReceipt(deps, ctx) {
68990
69371
  const readRunTip = async () => gitOutput(worktreePath, ["rev-parse", "--verify", "-q", `refs/heads/${worktreeBranch(runId)}`]).catch(() => null);
68991
69372
  const restore2 = () => {
68992
69373
  if (!preMergeHead || !branchSeen) return;
68993
- const restored = restoreRunBranch({ worktreePath, runId, preMergeHead, expectedCurrent: branchSeen });
69374
+ const restored = restoreRunBranch({
69375
+ worktreePath,
69376
+ runId,
69377
+ preMergeHead,
69378
+ expectedCurrent: branchSeen,
69379
+ deliveryLock: ctx.deliveryLock
69380
+ });
68994
69381
  if (!restored.restored) {
68995
69382
  console.warn(JSON.stringify({ event: "local_integration_restore_refused", projectSlug, runId, branchTip: restored.branchTip }));
68996
69383
  }
@@ -69066,7 +69453,10 @@ async function integrateBeforeReceipt(deps, ctx) {
69066
69453
  defaultTipSha: result.defaultTipSha,
69067
69454
  preMergeHead: result.preMergeHead,
69068
69455
  files: result.files,
69069
- hooksDir
69456
+ hooksDir,
69457
+ // The agent only stages: the merge must still be open, and nothing outside the conflict may change.
69458
+ afterAgentTurn: true,
69459
+ outsideConflicts: result.outsideConflicts
69070
69460
  });
69071
69461
  branchSeen = await readRunTip();
69072
69462
  if (!verdict.ok) {
@@ -69111,19 +69501,49 @@ async function integrateBeforeReceipt(deps, ctx) {
69111
69501
  }
69112
69502
  async function deliverLocalRun(deps, input) {
69113
69503
  const { projectSlug, task, runId, leaseId } = input;
69504
+ const restoreForLostLease = async () => {
69505
+ if (!input.integration?.mergeSha) return;
69506
+ const settledNow = await readSettlementState(deps, projectSlug, task.id, runId, 1);
69507
+ if (!settledNow || settledNow.runDone || settledNow.taskTerminal) {
69508
+ console.warn(JSON.stringify({
69509
+ event: "local_integration_restore_skipped",
69510
+ projectSlug,
69511
+ runId,
69512
+ reason: settledNow ? "run_settled" : "state_unreadable"
69513
+ }));
69514
+ return;
69515
+ }
69516
+ try {
69517
+ restoreRunBranch({
69518
+ worktreePath: input.worktreePath,
69519
+ runId,
69520
+ preMergeHead: input.integration.preMergeHead,
69521
+ expectedCurrent: input.integration.mergeSha,
69522
+ // deliverLocalRun runs inside driveRun's delivery lock, keyed by this same checkout.
69523
+ deliveryLock: { locksDir: deps.locksDir ?? "/tmp/mrrlin-locks", key: deliveryLockKey(input.checkoutRealPath) }
69524
+ });
69525
+ } catch (error51) {
69526
+ console.warn(JSON.stringify({
69527
+ event: "local_integration_restore_failed",
69528
+ projectSlug,
69529
+ runId,
69530
+ error: error51 instanceof Error ? error51.message : String(error51)
69531
+ }));
69532
+ }
69533
+ };
69534
+ const handOff = async (reason) => {
69535
+ if (!await runHasLease(deps, projectSlug, runId, leaseId)) {
69536
+ await restoreForLostLease();
69537
+ return;
69538
+ }
69539
+ if (!await failRun(deps, projectSlug, task, runId, leaseId, reason)) await restoreForLostLease();
69540
+ };
69114
69541
  if (!input.baseSha) {
69115
- await failRun(deps, projectSlug, task, runId, leaseId, `local_delivery_failed: run ${runId} has no recorded base commit.`);
69542
+ await handOff(`local_delivery_failed: run ${runId} has no recorded base commit.`);
69116
69543
  return;
69117
69544
  }
69118
69545
  if (!deps.localEvidenceDir) {
69119
- await failRun(
69120
- deps,
69121
- projectSlug,
69122
- task,
69123
- runId,
69124
- leaseId,
69125
- `local_delivery_failed: run ${runId} has no bridge evidence directory configured (localEvidenceDir).`
69126
- );
69546
+ await handOff(`local_delivery_failed: run ${runId} has no bridge evidence directory configured (localEvidenceDir).`);
69127
69547
  return;
69128
69548
  }
69129
69549
  const evidenceDir = deps.localEvidenceDir;
@@ -69140,14 +69560,7 @@ async function deliverLocalRun(deps, input) {
69140
69560
  integration: input.integration ?? null
69141
69561
  });
69142
69562
  } catch (error51) {
69143
- await failRun(
69144
- deps,
69145
- projectSlug,
69146
- task,
69147
- runId,
69148
- leaseId,
69149
- `local_delivery_failed: ${error51 instanceof Error ? error51.message : String(error51)}`
69150
- );
69563
+ await handOff(`local_delivery_failed: ${error51 instanceof Error ? error51.message : String(error51)}`);
69151
69564
  return;
69152
69565
  }
69153
69566
  if (!outcome.ok) {
@@ -69155,24 +69568,11 @@ async function deliverLocalRun(deps, input) {
69155
69568
  await deps.client.updateExecutionRun(projectSlug, runId, { headSha: outcome.evidence.headSha }).catch(() => {
69156
69569
  });
69157
69570
  }
69158
- await failRun(deps, projectSlug, task, runId, leaseId, outcome.detail);
69571
+ await handOff(outcome.detail);
69159
69572
  return;
69160
69573
  }
69161
- const restoreForLostLease = () => {
69162
- if (input.integration?.mergeSha) {
69163
- try {
69164
- restoreRunBranch({
69165
- worktreePath: input.worktreePath,
69166
- runId,
69167
- preMergeHead: input.integration.preMergeHead,
69168
- expectedCurrent: input.integration.mergeSha
69169
- });
69170
- } catch {
69171
- }
69172
- }
69173
- };
69174
69574
  if (!await runHasLease(deps, projectSlug, runId, leaseId)) {
69175
- restoreForLostLease();
69575
+ await restoreForLostLease();
69176
69576
  return;
69177
69577
  }
69178
69578
  await deps.client.updateExecutionRun(projectSlug, runId, {
@@ -69200,33 +69600,19 @@ async function deliverLocalRun(deps, input) {
69200
69600
  const existing = await deps.client.listExecutionArtifacts(projectSlug, runId).catch(() => []);
69201
69601
  const found = existing.find((candidate) => candidate.id === artifactId);
69202
69602
  if (!found) {
69203
- await failRun(
69204
- deps,
69205
- projectSlug,
69206
- task,
69207
- runId,
69208
- leaseId,
69209
- `local_delivery_failed: could not record the build receipt: ${error51 instanceof Error ? error51.message : String(error51)}`
69210
- );
69603
+ await handOff(`local_delivery_failed: could not record the build receipt: ${error51 instanceof Error ? error51.message : String(error51)}`);
69211
69604
  return;
69212
69605
  }
69213
69606
  artifact = found;
69214
69607
  }
69215
69608
  if (!deps.signingProvider) {
69216
- await failRun(
69217
- deps,
69218
- projectSlug,
69219
- task,
69220
- runId,
69221
- leaseId,
69222
- `closure_signing_unavailable: run ${runId} delivered ${outcome.deliveryRef} with a green bridge-run build, but this bridge has no receipt signing key (MRRLIN_SIGNING_KEY_ID / MRRLIN_SIGNING_PRIVATE_KEY), so the task cannot be closed. Configure signing and restart the run.`
69223
- );
69609
+ await handOff(`closure_signing_unavailable: run ${runId} delivered ${outcome.deliveryRef} with a green bridge-run build, but this bridge has no receipt signing key (MRRLIN_SIGNING_KEY_ID / MRRLIN_SIGNING_PRIVATE_KEY), so the task cannot be closed. Configure signing and restart the run.`);
69224
69610
  return;
69225
69611
  }
69226
69612
  const stored = artifact?.payload ?? {};
69227
69613
  const contentHash = String(stored.contentHash ?? stored.content_hash ?? stored.hash ?? "");
69228
69614
  if (!await runHasLease(deps, projectSlug, runId, leaseId)) {
69229
- restoreForLostLease();
69615
+ await restoreForLostLease();
69230
69616
  return;
69231
69617
  }
69232
69618
  try {
@@ -69246,20 +69632,48 @@ async function deliverLocalRun(deps, input) {
69246
69632
  }
69247
69633
  );
69248
69634
  } catch (error51) {
69249
- await failRun(
69250
- deps,
69635
+ const message = error51 instanceof Error ? error51.message : String(error51);
69636
+ const outcome2 = await readSettlementState(deps, projectSlug, task.id, runId, SETTLEMENT_READBACK_ATTEMPTS);
69637
+ if (!outcome2 || outcome2.taskStatus !== "closed_done" || !outcome2.runDone) {
69638
+ await handOff(`local_delivery_settlement_failed: ${message}`);
69639
+ return;
69640
+ }
69641
+ console.warn(JSON.stringify({
69642
+ event: "settlement_outcome_recovered",
69251
69643
  projectSlug,
69252
- task,
69253
69644
  runId,
69254
- leaseId,
69255
- `local_delivery_settlement_failed: ${error51 instanceof Error ? error51.message : String(error51)}`
69256
- );
69257
- return;
69645
+ taskId: task.id,
69646
+ error: message
69647
+ }));
69258
69648
  }
69649
+ if (input.delivery) input.delivery.settled = true;
69259
69650
  const locksDir = deps.locksDir ?? "/tmp/mrrlin-locks";
69260
69651
  const headSha = outcome.evidence.headSha;
69261
- const settled = await deps.client.getExecutionRun(projectSlug, runId).catch(() => null);
69262
- if (!settled || settled.leaseId != null && settled.leaseId !== leaseId) return;
69652
+ const logLanding = (event, extra) => console.warn(JSON.stringify({ event, projectSlug, runId, taskId: task.id, headSha, ...extra }));
69653
+ logLanding("landing_started", { defaultBranch: input.defaultBranch });
69654
+ let settledLease;
69655
+ for (let attempt = 1; attempt <= LANDING_RUN_READ_ATTEMPTS; attempt += 1) {
69656
+ try {
69657
+ settledLease = (await deps.client.getExecutionRun(projectSlug, runId)).leaseId ?? null;
69658
+ break;
69659
+ } catch (error51) {
69660
+ logLanding("landing_run_read_failed", { attempt, error: error51 instanceof Error ? error51.message : String(error51) });
69661
+ if (attempt < LANDING_RUN_READ_ATTEMPTS) await delayMs(LANDING_RUN_READ_BACKOFF_MS * attempt);
69662
+ }
69663
+ }
69664
+ if (settledLease != null && settledLease !== leaseId) {
69665
+ logLanding("landing_skipped", { reason: "run_reclaimed", leaseHolder: settledLease });
69666
+ await fileLocalLandingNote(deps.client, projectSlug, "run_reclaimed", {
69667
+ runId,
69668
+ taskId: task.id,
69669
+ title: task.title,
69670
+ defaultBranch: input.defaultBranch,
69671
+ previousSha: null,
69672
+ headSha,
69673
+ detail: ""
69674
+ });
69675
+ return;
69676
+ }
69263
69677
  const apply = deps.applyLocalRun ?? applyLocalRun;
69264
69678
  let landing;
69265
69679
  try {
@@ -69274,12 +69688,18 @@ async function deliverLocalRun(deps, input) {
69274
69688
  hooksDir,
69275
69689
  // The tip the run was integrated with: any other tip (even an ancestor after a user's reset)
69276
69690
  // is default_branch_moved, never fast-forwarded over.
69277
- ...input.integration ? { expectedOldSha: input.integration.defaultTipSha } : {}
69691
+ ...input.integration ? { expectedOldSha: input.integration.defaultTipSha } : {},
69692
+ // Land only while the run branch still ends at the head that was just signed.
69693
+ runBranchGuard: { ref: `refs/heads/${worktreeBranch(runId)}`, sha: headSha }
69278
69694
  })
69279
69695
  );
69280
69696
  } catch (error51) {
69281
69697
  landing = { applied: false, reason: "ff_failed", detail: error51 instanceof Error ? error51.message : String(error51) };
69282
69698
  }
69699
+ logLanding("landing_result", {
69700
+ outcome: landing.applied ? "applied" : landing.reason,
69701
+ ...landing.applied ? { previousSha: landing.previousSha } : { detail: landing.detail }
69702
+ });
69283
69703
  await fileLocalLandingNote(deps.client, projectSlug, landing.applied ? "applied" : landing.reason, {
69284
69704
  runId,
69285
69705
  taskId: task.id,
@@ -69299,14 +69719,33 @@ async function deliverLocalRun(deps, input) {
69299
69719
  }).catch(() => {
69300
69720
  });
69301
69721
  }
69722
+ var SETTLEMENT_READBACK_ATTEMPTS = 3;
69723
+ async function readSettlementState(deps, projectSlug, taskId, runId, attempts) {
69724
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
69725
+ try {
69726
+ const current2 = await deps.client.getTask(projectSlug, taskId);
69727
+ const run3 = await deps.client.getExecutionRun(projectSlug, runId);
69728
+ return {
69729
+ taskStatus: current2.status,
69730
+ taskTerminal: mrrlinTerminalTaskStatuses.includes(current2.status),
69731
+ runDone: run3.status === "done"
69732
+ };
69733
+ } catch {
69734
+ if (attempt < attempts) await delayMs(LANDING_RUN_READ_BACKOFF_MS * attempt);
69735
+ }
69736
+ }
69737
+ return null;
69738
+ }
69739
+ var LANDING_RUN_READ_ATTEMPTS = 3;
69740
+ var LANDING_RUN_READ_BACKOFF_MS = 250;
69302
69741
  async function failRun(deps, projectSlug, task, runId, leaseId, reason, reasonCode) {
69303
69742
  let currentRun;
69304
69743
  try {
69305
69744
  currentRun = await deps.client.getExecutionRun(projectSlug, runId);
69306
69745
  } catch {
69307
- return;
69746
+ return false;
69308
69747
  }
69309
- if (currentRun.status !== "running" || currentRun.leaseId !== leaseId) return;
69748
+ if (currentRun.status !== "running" || currentRun.leaseId !== leaseId) return false;
69310
69749
  let handoffReason = reason;
69311
69750
  let handoffReasonCode = reasonCode;
69312
69751
  const providerLimit = detectProviderLimitFromErrorText(reason);
@@ -69343,7 +69782,7 @@ async function failRun(deps, projectSlug, task, runId, leaseId, reason, reasonCo
69343
69782
  }
69344
69783
  }).catch(() => {
69345
69784
  });
69346
- return;
69785
+ return true;
69347
69786
  } catch {
69348
69787
  }
69349
69788
  }
@@ -69382,7 +69821,7 @@ async function failRun(deps, projectSlug, task, runId, leaseId, reason, reasonCo
69382
69821
  }
69383
69822
  }).catch(() => {
69384
69823
  });
69385
- return;
69824
+ return true;
69386
69825
  } catch {
69387
69826
  }
69388
69827
  }
@@ -69408,7 +69847,7 @@ async function failRun(deps, projectSlug, task, runId, leaseId, reason, reasonCo
69408
69847
  }
69409
69848
  }).catch(() => {
69410
69849
  });
69411
- return;
69850
+ return true;
69412
69851
  } catch {
69413
69852
  }
69414
69853
  }
@@ -69421,6 +69860,7 @@ async function failRun(deps, projectSlug, task, runId, leaseId, reason, reasonCo
69421
69860
  } catch {
69422
69861
  await notifyOperatorHandoff(deps, projectSlug, task, runId, handoffReason);
69423
69862
  }
69863
+ return true;
69424
69864
  }
69425
69865
  async function reconcileTaskAwaitingMerge(deps, projectSlug, task, runId, reason) {
69426
69866
  if (task.status === "handoff_needed") return;
@@ -76035,13 +76475,14 @@ async function handleAgentsDispatch(deps, request) {
76035
76475
  if (request.prompt.length > MAX_PROMPT_LEN) {
76036
76476
  return finalizeReply(invalidInput("prompt", `must be at most ${MAX_PROMPT_LEN} characters`), scrubSet);
76037
76477
  }
76478
+ const outboundPrompt = scrubDeep(redactSecretFormats(request.prompt), scrubSet);
76038
76479
  const args = [
76039
76480
  "dispatch",
76040
76481
  request.id,
76041
76482
  "--json",
76042
76483
  ...request.transport ? ["--transport", request.transport] : [],
76043
76484
  ...request.effort ? ["--effort", request.effort] : [],
76044
- request.prompt
76485
+ outboundPrompt
76045
76486
  ];
76046
76487
  const result = await deps.run(args, { budgetMs: BUDGET_DISPATCH_MS });
76047
76488
  if (result.code !== 0) {
@@ -76056,19 +76497,6 @@ async function handleAgentsDispatch(deps, request) {
76056
76497
 
76057
76498
  // src/director-bridge.ts
76058
76499
  var JSON_GPT_MODEL_REF_RE = /"(?:slug|model)"\s*:\s*"(gpt-[a-z0-9.-]+)"/gi;
76059
- var DEFAULT_DIRECTOR_BRIDGE_ALLOWED_ORIGINS = [
76060
- "http://localhost:*",
76061
- "http://127.0.0.1:*",
76062
- "http://[::1]:*",
76063
- // Staging decoupled onto the stable custom domain staging.mrrlin.com on 2026-08-14 (Cloudflare DNS
76064
- // in front of the mrrlin3 Vercel project). The Vercel-owned web alias (mrrlin-web-mrrlin3.vercel.app)
76065
- // and the old app.mrrlin.dev origin are dropped from the defaults: the browser now reaches the
76066
- // bridge from the owned domain, which is stable across future Vercel project/team moves.
76067
- // See docs/reference/deployments.md.
76068
- "https://staging.mrrlin.com",
76069
- // Production web.
76070
- "https://app.mrrlin.com"
76071
- ];
76072
76500
  var COMPACT_REQUEST_HISTORY_MAX_ITEMS = 500;
76073
76501
  function isValidChatHistoryMessage(value) {
76074
76502
  if (!value || typeof value !== "object") return false;
@@ -76832,13 +77260,6 @@ async function runOrphanWorktreeSweep(client, opts) {
76832
77260
  function readStateDir() {
76833
77261
  return resolveStateDir(process.env);
76834
77262
  }
76835
- function readAllowedOrigins() {
76836
- const raw = (process.env.MRRLIN_DIRECTOR_BRIDGE_ALLOWED_ORIGINS ?? "").trim();
76837
- if (!raw) return [...DEFAULT_DIRECTOR_BRIDGE_ALLOWED_ORIGINS];
76838
- if (raw === "*" || raw.toLowerCase() === "all") return null;
76839
- const parts = raw.split(",").map((p) => p.trim().replace(/\/$/, "")).filter(Boolean);
76840
- return parts.length > 0 ? parts : null;
76841
- }
76842
77263
  function originMatchesAllowedPattern(origin, allowedOrigin) {
76843
77264
  if (!allowedOrigin.endsWith(":*")) return origin === allowedOrigin;
76844
77265
  try {
@@ -78534,7 +78955,7 @@ function parseConfig() {
78534
78955
  const host = (process.env.MRRLIN_DIRECTOR_BRIDGE_HOST ?? "127.0.0.1").trim() || "127.0.0.1";
78535
78956
  const port = readEnvInt("MRRLIN_DIRECTOR_BRIDGE_PORT", 8791);
78536
78957
  const stateDir = readStateDir();
78537
- const allowedOrigins = readAllowedOrigins();
78958
+ const allowedOrigins = readDirectorBridgeAllowedOrigins(process.env);
78538
78959
  const token = readOrCreateBridgeToken(stateDir);
78539
78960
  const codexCwd = process.env.MRRLIN_DIRECTOR_BRIDGE_CODEX_CWD?.trim() || void 0;
78540
78961
  const codexExecutable = process.env.MRRLIN_DIRECTOR_BRIDGE_CODEX_BIN?.trim() || void 0;
@@ -81469,7 +81890,8 @@ Usage:
81469
81890
  - MRRLIN_DIRECTOR_BRIDGE_HOST (default 127.0.0.1)
81470
81891
  - MRRLIN_DIRECTOR_BRIDGE_PORT (default 8791)
81471
81892
  - MRRLIN_DIRECTOR_BRIDGE_ALLOWED_ORIGINS (optional comma-separated
81472
- override; default allows localhost, staging, and production)
81893
+ override; default allows localhost and production,
81894
+ plus staging when MRRLIN_API_BASE_URL is the staging API)
81473
81895
  - MRRLIN_DIRECTOR_BRIDGE_TOKEN (optional; auto-generated if unset)
81474
81896
  - MRRLIN_DIRECTOR_BRIDGE_IDLE_TTL_SECONDS (default 1800)
81475
81897
  - MRRLIN_DIRECTOR_BRIDGE_CODEX_CWD (optional)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.4.129",
3
+ "version": "0.4.131",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mrrlin-mcp": "dist/bin.cjs"