@mrrlin-dev/mcp 0.4.22 → 0.4.27

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 (2) hide show
  1. package/dist/bin.cjs +304 -46
  2. package/package.json +3 -3
package/dist/bin.cjs CHANGED
@@ -20815,6 +20815,66 @@ function browserServeArgs(homeDir = import_node_os.default.homedir()) {
20815
20815
  env: { PLAYWRIGHT_BROWSERS_PATH: browsersPath }
20816
20816
  };
20817
20817
  }
20818
+ function defaultListProcesses() {
20819
+ const r = (0, import_node_child_process.spawnSync)("ps", ["-ww", "-eo", "pid,ppid,command"], { encoding: "utf8" });
20820
+ const lines2 = (r.stdout ?? "").split("\n").slice(1);
20821
+ const out = [];
20822
+ for (const line of lines2) {
20823
+ const m = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
20824
+ if (!m || m[1] === void 0 || m[2] === void 0 || m[3] === void 0) continue;
20825
+ out.push({ pid: Number(m[1]), ppid: Number(m[2]), command: m[3] });
20826
+ }
20827
+ return out;
20828
+ }
20829
+ function defaultIsAlive(pid) {
20830
+ try {
20831
+ process.kill(pid, 0);
20832
+ return true;
20833
+ } catch (err) {
20834
+ return err.code === "EPERM";
20835
+ }
20836
+ }
20837
+ function defaultKill(pid, signal) {
20838
+ try {
20839
+ process.kill(pid, signal);
20840
+ return true;
20841
+ } catch (err) {
20842
+ if (err.code === "ESRCH") return true;
20843
+ return false;
20844
+ }
20845
+ }
20846
+ function safeKill(kill, pid, signal) {
20847
+ try {
20848
+ return kill(pid, signal);
20849
+ } catch {
20850
+ return false;
20851
+ }
20852
+ }
20853
+ function reapOrphanedBrowserProcesses(opts = {}) {
20854
+ const homeDir = opts.homeDir ?? import_node_os.default.homedir();
20855
+ const { profilePath } = browserPaths(homeDir);
20856
+ const listProcesses = opts.listProcesses ?? defaultListProcesses;
20857
+ const isAlive = opts.isAlive ?? defaultIsAlive;
20858
+ const kill = opts.kill ?? defaultKill;
20859
+ const selfPid = opts.selfPid ?? process.pid;
20860
+ const allProcesses = listProcesses();
20861
+ const candidates = allProcesses.filter(
20862
+ (p) => p.pid !== selfPid && p.command.includes("@playwright/mcp") && p.command.includes(profilePath)
20863
+ );
20864
+ const killedPids = [];
20865
+ const killedChildPids = [];
20866
+ for (const proc of candidates) {
20867
+ if (!isAlive(proc.ppid)) {
20868
+ for (const child of allProcesses) {
20869
+ if (child.ppid === proc.pid) {
20870
+ if (safeKill(kill, child.pid, "SIGKILL")) killedChildPids.push(child.pid);
20871
+ }
20872
+ }
20873
+ if (safeKill(kill, proc.pid, "SIGTERM")) killedPids.push(proc.pid);
20874
+ }
20875
+ }
20876
+ return { checked: candidates.length, killedPids, killedChildPids };
20877
+ }
20818
20878
  function externalAgentsServeArgs() {
20819
20879
  return {
20820
20880
  command: process.execPath,
@@ -21418,14 +21478,23 @@ async function verifyProviderReadiness(provider, run3, opts = {}) {
21418
21478
  const checks = { cli: false, version: false, auth: false, mcpReachable: false };
21419
21479
  const details = [];
21420
21480
  let cliResult;
21481
+ let cliUnreachable = false;
21421
21482
  try {
21422
21483
  cliResult = run3(["--version"]);
21423
21484
  checks.cli = cliResult.status === 0;
21485
+ if (!checks.cli) {
21486
+ cliUnreachable = cliResult.status === null;
21487
+ details.push(
21488
+ cliUnreachable ? `cli: \`${provider}\` binary not found on PATH` : `cli: \`${provider} --version\` exited with code ${cliResult.status}`
21489
+ );
21490
+ }
21424
21491
  } catch {
21492
+ cliUnreachable = true;
21493
+ details.push(`cli: \`${provider}\` binary not found on PATH`);
21425
21494
  }
21426
- if (!checks.cli) details.push("cli: binary not available or exited non-zero");
21427
21495
  if (provider === "codex") {
21428
21496
  checks.version = true;
21497
+ } else if (cliUnreachable) {
21429
21498
  } else {
21430
21499
  const raw = (cliResult?.stdout || cliResult?.stderr) ?? "";
21431
21500
  const version2 = parseCodexVersion(raw);
@@ -21440,6 +21509,7 @@ async function verifyProviderReadiness(provider, run3, opts = {}) {
21440
21509
  } catch {
21441
21510
  details.push(`auth: ${authPath} not found`);
21442
21511
  }
21512
+ } else if (cliUnreachable) {
21443
21513
  } else {
21444
21514
  try {
21445
21515
  const result = run3(["auth", "status"]);
@@ -21456,6 +21526,8 @@ async function verifyProviderReadiness(provider, run3, opts = {}) {
21456
21526
  checks.mcpReachable = isExternalAgentsWrapper(parsed.mcp_servers?.["external-agents"]);
21457
21527
  } catch {
21458
21528
  }
21529
+ if (!checks.mcpReachable) details.push("mcpReachable: external-agents server entry missing or invalid");
21530
+ } else if (cliUnreachable) {
21459
21531
  } else {
21460
21532
  const configPath = resolveClaudeConfigPath2(opts);
21461
21533
  try {
@@ -21464,8 +21536,8 @@ async function verifyProviderReadiness(provider, run3, opts = {}) {
21464
21536
  checks.mcpReachable = isExternalAgentsWrapper(parsed.mcpServers?.["external-agents"]);
21465
21537
  } catch {
21466
21538
  }
21539
+ if (!checks.mcpReachable) details.push("mcpReachable: external-agents server entry missing or invalid");
21467
21540
  }
21468
- if (!checks.mcpReachable) details.push("mcpReachable: external-agents server entry missing or invalid");
21469
21541
  const ready = checks.cli && checks.version && checks.auth && checks.mcpReachable;
21470
21542
  return { ready, checks, details };
21471
21543
  }
@@ -37017,7 +37089,10 @@ var ASYNC_JOB_FAILURE_REASONS = [
37017
37089
  "provider_unavailable",
37018
37090
  "spec_changed",
37019
37091
  "stale_lease",
37020
- "consensus_blocked"
37092
+ "consensus_blocked",
37093
+ // async-flow-throughput Slice 1: the spec drafter rejected its own output for exceeding the
37094
+ // per-impact acceptance-criteria / character budget. Retryable via the normal async-job path.
37095
+ "spec_over_budget"
37021
37096
  ];
37022
37097
  var mrrlinAsyncJobStatusSchema = external_exports.enum(ASYNC_JOB_STATUSES);
37023
37098
  var mrrlinAsyncJobFailureReasonSchema = external_exports.enum(ASYNC_JOB_FAILURE_REASONS);
@@ -41218,6 +41293,82 @@ function describeError(error51) {
41218
41293
  const detail = detailFromCause(error51.cause);
41219
41294
  return detail ? `${error51.message} (${truncate(detail)})` : error51.message;
41220
41295
  }
41296
+ function errorKind(error51) {
41297
+ if (!(error51 instanceof Error)) return null;
41298
+ const cause = error51.cause;
41299
+ if (!cause || typeof cause !== "object") return null;
41300
+ const direct = cause.code;
41301
+ if (typeof direct === "string") return direct;
41302
+ const errors = cause.errors;
41303
+ if (Array.isArray(errors) && errors.length > 0) {
41304
+ const first = errors[0]?.code;
41305
+ if (typeof first === "string") return first;
41306
+ }
41307
+ return null;
41308
+ }
41309
+ var DEFAULT_FAILURE_LOG_WINDOW_SECONDS = 60;
41310
+ function failureLogWindowMs(env = process.env) {
41311
+ const raw = (env.MRRLIN_BRIDGE_FAILURE_LOG_WINDOW_SECONDS ?? "").trim();
41312
+ const parsed = Number.parseInt(raw, 10);
41313
+ const seconds = Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_FAILURE_LOG_WINDOW_SECONDS;
41314
+ return seconds * 1e3;
41315
+ }
41316
+ function createFailureLog(opts = {}) {
41317
+ const windowMs = opts.windowMs ?? failureLogWindowMs();
41318
+ const now = opts.now ?? Date.now;
41319
+ const lastEmit = /* @__PURE__ */ new Map();
41320
+ const counts = /* @__PURE__ */ new Map();
41321
+ let windowStartedAt = null;
41322
+ return {
41323
+ /** Decide whether this failure should be printed. Returns the suppressed count to include in
41324
+ * the line when it is. */
41325
+ shouldEmit(operation, kind) {
41326
+ const prev = lastEmit.get(operation);
41327
+ const sameKind = prev != null && prev.kind === kind;
41328
+ const withinWindow = prev != null && now() - prev.at < windowMs;
41329
+ if (sameKind && withinWindow) {
41330
+ prev.suppressed += 1;
41331
+ return { emit: false, suppressed: prev.suppressed };
41332
+ }
41333
+ const suppressed = sameKind ? prev?.suppressed ?? 0 : 0;
41334
+ lastEmit.set(operation, { kind, at: now(), suppressed: 0 });
41335
+ return { emit: true, suppressed };
41336
+ },
41337
+ /** A success resets the operation's suppression state, so the next failure always prints. */
41338
+ noteSuccess(operation) {
41339
+ lastEmit.delete(operation);
41340
+ },
41341
+ /** Count a failure for the degraded report. Only the breaker-tracked operations are counted. */
41342
+ recordFailure(operation, kind) {
41343
+ if (windowStartedAt == null) windowStartedAt = now();
41344
+ const entry = counts.get(operation) ?? { failures: 0, lastErrorKind: null };
41345
+ entry.failures += 1;
41346
+ entry.lastErrorKind = kind;
41347
+ counts.set(operation, entry);
41348
+ },
41349
+ /** Snapshot the window and start a fresh one. Always returns all three operations, reporting
41350
+ * zeros for a clean window — it never fabricates a degraded state and never omits the key. */
41351
+ drainWindow() {
41352
+ const startedAt = windowStartedAt;
41353
+ const report = {
41354
+ windowStartedAt: startedAt == null ? null : new Date(startedAt).toISOString(),
41355
+ operations: {
41356
+ schedulerTick: counts.get("schedulerTick") ?? { failures: 0, lastErrorKind: null },
41357
+ claimSweep: counts.get("claimSweep") ?? { failures: 0, lastErrorKind: null },
41358
+ asyncSweep: counts.get("asyncSweep") ?? { failures: 0, lastErrorKind: null }
41359
+ }
41360
+ };
41361
+ counts.clear();
41362
+ windowStartedAt = null;
41363
+ return report;
41364
+ }
41365
+ };
41366
+ }
41367
+ function failureLogLine(label, error51, suppressed) {
41368
+ const tail2 = suppressed > 0 ? ` (${suppressed} identical suppressed since the last line)` : "";
41369
+ return `[mrrlin-mcp director-bridge] ${label} failed: ${describeError(error51)}${tail2}
41370
+ `;
41371
+ }
41221
41372
 
41222
41373
  // src/token-refresher.ts
41223
41374
  function createTokenRefresher(opts) {
@@ -41232,12 +41383,19 @@ function createTokenRefresher(opts) {
41232
41383
  opts.persist(token);
41233
41384
  last = token;
41234
41385
  }
41386
+ opts.failureLog?.noteSuccess("tokenRefresh");
41235
41387
  opts.onOk?.();
41236
41388
  } catch (error51) {
41237
- process.stderr.write(
41238
- `[mrrlin-mcp director-bridge] token refresh failed (will retry next tick): ${describeError(error51)}
41389
+ const label = "token refresh (will retry next tick)";
41390
+ if (opts.failureLog) {
41391
+ const { emit, suppressed } = opts.failureLog.shouldEmit("tokenRefresh", errorKind(error51));
41392
+ if (emit) process.stderr.write(failureLogLine(label, error51, suppressed));
41393
+ } else {
41394
+ process.stderr.write(
41395
+ `[mrrlin-mcp director-bridge] token refresh failed (will retry next tick): ${describeError(error51)}
41239
41396
  `
41240
- );
41397
+ );
41398
+ }
41241
41399
  opts.onError?.(error51);
41242
41400
  }
41243
41401
  }
@@ -41664,7 +41822,7 @@ Do NOT derail the current task - if the operator has an in-flight question, answ
41664
41822
  }
41665
41823
 
41666
41824
  // src/_generated/version.ts
41667
- var PKG_VERSION = "0.4.22";
41825
+ var PKG_VERSION = "0.4.27";
41668
41826
 
41669
41827
  // src/api-base-url-migrate.ts
41670
41828
  var import_node_fs3 = __toESM(require("node:fs"), 1);
@@ -42120,6 +42278,32 @@ function computeNextTickDelayMs(nextWakeAt, nowMs, opts = {}) {
42120
42278
  function count(value) {
42121
42279
  return Array.isArray(value) ? value.length : 0;
42122
42280
  }
42281
+ var KNOWN_STOP_REASONS = new Set(mrrlinStopReasonSchema.options);
42282
+ function tallyReasons(value) {
42283
+ const out = {};
42284
+ if (!Array.isArray(value)) return out;
42285
+ for (const entry of value) {
42286
+ const raw = entry?.reason;
42287
+ const trimmed = typeof raw === "string" ? raw.trim() : "";
42288
+ const key = KNOWN_STOP_REASONS.has(trimmed) ? trimmed : "unknown";
42289
+ out[key] = (out[key] ?? 0) + 1;
42290
+ }
42291
+ return out;
42292
+ }
42293
+ function mergeReasons(parts) {
42294
+ const out = {};
42295
+ for (const part of parts) {
42296
+ for (const [key, n] of Object.entries(part)) out[key] = (out[key] ?? 0) + n;
42297
+ }
42298
+ return out;
42299
+ }
42300
+ function schedulerTickFailureLine(deps, error51) {
42301
+ const kind = errorKind(error51);
42302
+ deps.failureLog?.recordFailure("schedulerTick", kind);
42303
+ if (!deps.failureLog) return failureLogLine("scheduler tick", error51, 0);
42304
+ const { emit, suppressed } = deps.failureLog.shouldEmit("schedulerTick", kind);
42305
+ return emit ? failureLogLine("scheduler tick", error51, suppressed) : "";
42306
+ }
42123
42307
  function dispatchTickWork(deps, results) {
42124
42308
  if (!deps.dispatchEnqueued) return;
42125
42309
  for (const result of results) {
@@ -42150,8 +42334,7 @@ async function attemptSchedulerTick(deps) {
42150
42334
  const { response, parsedBody } = bridgeHealth ? await withTimeout(parseTick) : await parseTick(new AbortController().signal);
42151
42335
  if (!response.ok) {
42152
42336
  process.stderr.write(
42153
- `[mrrlin-mcp director-bridge] scheduler tick failed: HTTP ${response.status} from /internal/scheduler/run
42154
- `
42337
+ schedulerTickFailureLine(deps, new Error(`HTTP ${response.status} from /internal/scheduler/run`))
42155
42338
  );
42156
42339
  bridgeHealth?.recordFailure("schedulerTick", new Error(`HTTP ${response.status}`));
42157
42340
  return { nextWakeAt: null, failed: true };
@@ -42174,6 +42357,9 @@ async function attemptSchedulerTick(deps) {
42174
42357
  enqueuedSpecDrafts: results.reduce((sum, r) => sum + count(r.enqueuedSpecDrafts), 0),
42175
42358
  autoApprovedSpecs: results.reduce((sum, r) => sum + count(r.autoApprovedSpecs), 0),
42176
42359
  skippedTasks: results.reduce((sum, r) => sum + count(r.skippedTasks), 0),
42360
+ // Slice 4: always present. A tick with no skips emits `{}`, never an absent key and never
42361
+ // null, so a consumer never has to distinguish "no skips" from "this bridge is too old".
42362
+ skippedReasons: mergeReasons(results.map((r) => tallyReasons(r.skippedTasks))),
42177
42363
  failures: results.reduce((sum, r) => sum + count(r.failures), 0)
42178
42364
  },
42179
42365
  perProject: results.map((r) => ({
@@ -42184,18 +42370,31 @@ async function attemptSchedulerTick(deps) {
42184
42370
  enqueuedSpecDrafts: count(r.enqueuedSpecDrafts),
42185
42371
  autoApprovedSpecs: count(r.autoApprovedSpecs),
42186
42372
  skippedTasks: count(r.skippedTasks),
42373
+ skippedReasons: tallyReasons(r.skippedTasks),
42187
42374
  failures: count(r.failures)
42188
42375
  })),
42189
- nextWakeAt: typeof data.nextWakeAt === "string" ? data.nextWakeAt : null
42376
+ nextWakeAt: typeof data.nextWakeAt === "string" ? data.nextWakeAt : null,
42377
+ // Slice 3: partial degradation had no signal — the breaker only trips on TOTAL failure, so
42378
+ // ~2 800 connect timeouts a day were invisible outside raw stderr. This rides a payload the
42379
+ // operator already reads. Always present, always all three tracked operations, zeros for a
42380
+ // clean window: it never fabricates a degraded state and never omits the key.
42381
+ degraded: deps.failureLog?.drainWindow() ?? {
42382
+ windowStartedAt: null,
42383
+ operations: {
42384
+ schedulerTick: { failures: 0, lastErrorKind: null },
42385
+ claimSweep: { failures: 0, lastErrorKind: null },
42386
+ asyncSweep: { failures: 0, lastErrorKind: null }
42387
+ }
42388
+ }
42190
42389
  }
42191
42390
  });
42192
42391
  dispatchTickWork(deps, results);
42392
+ deps.failureLog?.noteSuccess("schedulerTick");
42193
42393
  await bridgeHealth?.recordSuccess();
42194
42394
  return { nextWakeAt: typeof data.nextWakeAt === "string" ? data.nextWakeAt : null, failed: false };
42195
42395
  } catch (error51) {
42196
42396
  process.stderr.write(
42197
- `[mrrlin-mcp director-bridge] scheduler tick failed: ${describeError(error51)}
42198
- `
42397
+ schedulerTickFailureLine(deps, error51)
42199
42398
  );
42200
42399
  bridgeHealth?.recordFailure("schedulerTick", error51);
42201
42400
  return { nextWakeAt: null, failed: true };
@@ -43033,6 +43232,16 @@ function parseAcceptanceCriteria(markdown) {
43033
43232
  }
43034
43233
  return items.length > 0 ? { items } : null;
43035
43234
  }
43235
+ function resolveSpecBudget(impact) {
43236
+ switch (impact?.toLowerCase()) {
43237
+ case "high":
43238
+ return { maxRequirements: 12, maxChars: 25e3 };
43239
+ case "medium":
43240
+ return { maxRequirements: 6, maxChars: 12e3 };
43241
+ default:
43242
+ return { maxRequirements: 3, maxChars: 6e3 };
43243
+ }
43244
+ }
43036
43245
 
43037
43246
  // src/consensus/spec-gate.ts
43038
43247
  var SEED_ABSENCE_QUESTION = "What are the requirements for this task? There is no operator request on record, so the spec cannot be validated against your intent \u2014 please state what this task must do.";
@@ -43911,7 +44120,7 @@ function renderCriteriaBlock(criteria) {
43911
44120
  return criteria.map((c, i) => `${i + 1}. [ ] ${c}`).join("\n");
43912
44121
  }
43913
44122
  async function buildDeliverableGateDeps(args) {
43914
- const { client, projectSlug, deliverablePageId, declaredOutcome, cwd, codexExecutable, runId, seat } = args;
44123
+ const { client, projectSlug, deliverablePageId, attemptOutcome, cwd, codexExecutable, runId, seat } = args;
43915
44124
  const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
43916
44125
  const timeoutMs = Number(process.env["MRRLIN_CONSENSUS_HEAD_TIMEOUT_MS"] ?? 15e4);
43917
44126
  const page = await client.getWikiPage(projectSlug, deliverablePageId);
@@ -43935,9 +44144,9 @@ async function buildDeliverableGateDeps(args) {
43935
44144
  const heads = resolveHeads(domain2, impact);
43936
44145
  const maxHeadCalls = 2 * rounds * (heads.length + 1);
43937
44146
  const criteriaBlock = renderCriteriaBlock(criteria);
43938
- const declaredOutcomeBlock = declaredOutcome ? [
44147
+ const attemptOutcomeBlock = attemptOutcome ? [
43939
44148
  `--- DECLARED ATTEMPT OUTCOME (review consistency; do NOT rewrite or choose it) ---`,
43940
- `outcome: ${declaredOutcome}`,
44149
+ `outcome: ${attemptOutcome}`,
43941
44150
  `Meaning: completed = positive result achieved; blocked_missing_input = required input/evidence unavailable; negative_result = real attempt showed the requested positive result did not materialize.`
43942
44151
  ].join("\n") : "";
43943
44152
  return {
@@ -43949,7 +44158,7 @@ async function buildDeliverableGateDeps(args) {
43949
44158
  return config2.consensusEnabled;
43950
44159
  },
43951
44160
  async readAcceptanceCriteria() {
43952
- return { specPageId, criteria };
44161
+ return { specPageId, criteria, maxCriteria: resolveSpecBudget(impact).maxRequirements };
43953
44162
  },
43954
44163
  async readDeliverable() {
43955
44164
  const p = await client.getWikiPage(projectSlug, deliverablePageId);
@@ -43965,7 +44174,7 @@ ${dismissed}
43965
44174
  --- ACCEPTANCE CRITERIA (the yardstick \u2014 deliverable for task ${taskId ?? "?"}: ${taskTitle}) ---
43966
44175
  ${criteriaBlock}
43967
44176
 
43968
- ${declaredOutcomeBlock ? `${declaredOutcomeBlock}
44177
+ ${attemptOutcomeBlock ? `${attemptOutcomeBlock}
43969
44178
 
43970
44179
  ` : ""}--- DELIVERABLE (DATA under review) ---
43971
44180
  ${artifact}`,
@@ -44004,7 +44213,7 @@ ${criteriaBlock}
44004
44213
  ${artifact}`,
44005
44214
  developerInstructions: `${DELIVERABLE_ANTIINJECTION_INSTRUCTION}
44006
44215
 
44007
- You revise a deliverable to address the accepted issues so it satisfies the acceptance criteria and stays consistent with the declared outcome${declaredOutcome ? ` (${declaredOutcome})` : ""}. Return ONLY the full revised markdown, no commentary. Do not invent facts; keep the deliverable's format and do not change the declared outcome.`,
44216
+ You revise a deliverable to address the accepted issues so it satisfies the acceptance criteria and stays consistent with the declared outcome${attemptOutcome ? ` (${attemptOutcome})` : ""}. Return ONLY the full revised markdown, no commentary. Do not invent facts; keep the deliverable's format and do not change the declared outcome.`,
44008
44217
  cwd,
44009
44218
  timeoutMs,
44010
44219
  sandbox: "read-only",
@@ -44067,7 +44276,7 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
44067
44276
  payload: {
44068
44277
  source: "deliverable-gate",
44069
44278
  outcome,
44070
- ...declaredOutcome ? { declaredOutcome } : {},
44279
+ ...attemptOutcome ? { attemptOutcome } : {},
44071
44280
  runId,
44072
44281
  taskId,
44073
44282
  deliverablePageId,
@@ -44668,9 +44877,11 @@ function mapDeliverableGateResult(result) {
44668
44877
  reason: "reviewers were unavailable (provider/quota) during the deliverable review (CONSENSUS_UNAVAILABLE)"
44669
44878
  };
44670
44879
  case "CONSENSUS_BLOCKED": {
44880
+ const over = result.yardstickOverBudget;
44881
+ const yardstick = over ? ` The yardstick is over budget: spec ${over.specPageId ?? "(unknown page)"} carries ${over.criteria} acceptance criteria against a maximum of ${over.maxCriteria} for this task's impact, so re-draft the spec rather than re-running the work.` : "";
44671
44882
  return {
44672
44883
  action: "handoff",
44673
- reason: `reviewers did not converge; ${formatDeliverableGateDiagnostics(result.diagnostics ?? [])} (CONSENSUS_BLOCKED)`
44884
+ reason: `reviewers did not converge; ${formatDeliverableGateDiagnostics(result.diagnostics ?? [])} (CONSENSUS_BLOCKED)${yardstick}`
44674
44885
  };
44675
44886
  }
44676
44887
  case "MISSING_ACCEPTANCE_CRITERIA":
@@ -44696,17 +44907,6 @@ var SPECS_FOLDER = "Specs";
44696
44907
  var DRAFT_SPEC_TLDR_MAX_CHARS = 300;
44697
44908
  var DRAFT_SPEC_PERSONA = "spec-author";
44698
44909
  var PAGE_ID_MAX_ATTEMPTS = 5;
44699
- function resolveSpecBudget(impact) {
44700
- const tier = impact?.toLowerCase();
44701
- switch (tier) {
44702
- case "high":
44703
- return { maxRequirements: 12, maxChars: 25e3 };
44704
- case "medium":
44705
- return { maxRequirements: 6, maxChars: 12e3 };
44706
- default:
44707
- return { maxRequirements: 3, maxChars: 6e3 };
44708
- }
44709
- }
44710
44910
  function defaultSleep(ms) {
44711
44911
  return new Promise((resolve) => setTimeout(resolve, ms));
44712
44912
  }
@@ -44732,6 +44932,25 @@ ${body}`;
44732
44932
  }
44733
44933
  return { markdown: body, tldrChars };
44734
44934
  }
44935
+ function checkSpecBudget(markdown, impact) {
44936
+ const budget = resolveSpecBudget(impact);
44937
+ const parsed = parseAcceptanceCriteria(markdown);
44938
+ const criteria = parsed?.items.length ?? 0;
44939
+ const chars = markdown.length;
44940
+ const overCriteria = parsed != null && criteria > budget.maxRequirements;
44941
+ const overChars = chars > budget.maxChars;
44942
+ if (!overCriteria && !overChars) return null;
44943
+ const faults = [];
44944
+ if (overCriteria) faults.push(`${criteria} acceptance-criteria items (maximum ${budget.maxRequirements})`);
44945
+ if (overChars) faults.push(`${chars} characters (maximum ${budget.maxChars})`);
44946
+ return {
44947
+ criteria,
44948
+ chars,
44949
+ maxCriteria: budget.maxRequirements,
44950
+ maxChars: budget.maxChars,
44951
+ message: `spec draft is over budget: ${faults.join(" and ")}`
44952
+ };
44953
+ }
44735
44954
  function extractDeployTarget(markdown) {
44736
44955
  const section = /(## Deploy Target\s*\n)([\s\S]*?)(?=\n## |$)/i.exec(markdown);
44737
44956
  if (!section || section.index === void 0) {
@@ -44999,6 +45218,10 @@ async function runDraftSpec(ctx, input, deps) {
44999
45218
  }
45000
45219
  const { markdown: specMarkdown, deployTarget: parsedDeployTarget } = extractDeployTarget(exec.text);
45001
45220
  const { markdown, tldrChars } = enforceSpecOutputContract(specMarkdown, taskId, task.title);
45221
+ const overBudget = checkSpecBudget(markdown, task.impact);
45222
+ if (overBudget) {
45223
+ return { kind: "retry", failureReason: "spec_over_budget", error: overBudget.message };
45224
+ }
45002
45225
  let pageId;
45003
45226
  if (adoptedId) {
45004
45227
  try {
@@ -47675,7 +47898,7 @@ async function driveNonCodeRun(deps, projectSlug, runId, leaseId, task, domain2)
47675
47898
  taskId: task.id,
47676
47899
  runId,
47677
47900
  deliverablePageId: pageId,
47678
- declaredOutcome: parsedResult.outcome
47901
+ attemptOutcome: parsedResult.outcome
47679
47902
  });
47680
47903
  } catch (error51) {
47681
47904
  await failRun(
@@ -48535,6 +48758,7 @@ async function runDeliverableGate(target, deps) {
48535
48758
  const deliverable = await deps.readDeliverable();
48536
48759
  let currentHash = deliverable.contentHash;
48537
48760
  let conflicted = false;
48761
+ const yardstickOverBudget = ac.maxCriteria != null && ac.criteria.length > ac.maxCriteria ? { specPageId: ac.specPageId, criteria: ac.criteria.length, maxCriteria: ac.maxCriteria } : void 0;
48538
48762
  const result = await runConsensus({
48539
48763
  artifact: deliverable.markdownBody,
48540
48764
  heads: deps.heads,
@@ -48576,7 +48800,7 @@ async function runDeliverableGate(target, deps) {
48576
48800
  }
48577
48801
  await deps.writeEvidence({
48578
48802
  deliverablePageId: deliverable.pageId,
48579
- ...target.declaredOutcome ? { declaredOutcome: target.declaredOutcome } : {},
48803
+ ...target.attemptOutcome ? { attemptOutcome: target.attemptOutcome } : {},
48580
48804
  outcome: result.outcome,
48581
48805
  rounds: result.rounds,
48582
48806
  history: result.history,
@@ -48588,7 +48812,10 @@ async function runDeliverableGate(target, deps) {
48588
48812
  outcome: "CONSENSUS_BLOCKED",
48589
48813
  rounds: result.rounds,
48590
48814
  diagnostics: diagnostics ?? [],
48591
- ...result.escalation ? { escalation: result.escalation } : {}
48815
+ ...result.escalation ? { escalation: result.escalation } : {},
48816
+ // Slice 2: attached ONLY on the blocked path. A converged review needs no explanation, and a
48817
+ // transient outcome (changed-during-review, reviewers unavailable) is not about the yardstick.
48818
+ ...yardstickOverBudget ? { yardstickOverBudget } : {}
48592
48819
  };
48593
48820
  }
48594
48821
  if (result.outcome === "CONVERGED") {
@@ -54052,6 +54279,7 @@ function buildThreadStartParams(msg, config2, desiredModel) {
54052
54279
  "- When creating a task, classify and supply the `domain` field explicitly based on the nature of the work: pass `domain:'code'` for software engineering work, `domain:'seo'` for SEO/optimization, `domain:'marketing'` for marketing/growth, or `domain:'content'` for content creation/writing \u2014 the same way you select category/subcategory. A follow-up task spawned with `parentTaskId` inherits its parent's domain automatically and requires no explicit domain.",
54053
54280
  '- When a task needs a browser (login flows, UI verification/clicks, scraping), pass `prerequisites:[{mcp:"mrrlin-browser",reason:"..."}]` to `create_execution_run`. If that run later fails into a handoff, the operator gets a one-click "Launch browser & resume" that brings up the dedicated browser as the turn starts.',
54054
54281
  "- When you actually drive a browser (calling `browser_navigate`, `browser_click`, `browser_snapshot`, `browser_type`, etc.), invoke those tools from the **`mrrlin-browser`** MCP server ONLY. Do NOT use tools from any other browser MCP (`playwright`, `chrome-devtools`, `puppeteer`, etc.) even if they are also connected on this operator's box \u2014 those use a shared/default Playwright profile that is fragile across runs. Only `mrrlin-browser` uses this project's dedicated Chromium + persistent operator profile (`~/.mrrlin/browser/profile`). If your host exposes multiple browser MCPs with overlapping tool names, prefer the `mrrlin-browser` server unconditionally.",
54282
+ "- Exactly one browser session exists for this operator, shared across every concurrent chat and task on their box \u2014 a deliberate tradeoff, not a bug, so a second concurrent user of the browser is expected, not exceptional. If a `mrrlin-browser` call fails with `Browser is already in use for ...`, that means another one of the operator's own chats or tasks is driving the browser RIGHT NOW. Do not present this as a broken tool or a mysterious error: tell the operator plainly that the browser is busy with another task, ask them to wait for it to finish (or close it), and offer to retry shortly. Never retry-loop this silently.",
54055
54283
  "",
54056
54284
  'Research playbook \u2014 when the user asks about a topic, an area, or "what do we have on X":',
54057
54285
  "- Tasks are the source of truth. Start with `list_tasks` using `q=<keyword>`; the filter is a",
@@ -54146,6 +54374,14 @@ function buildThreadStartParams(msg, config2, desiredModel) {
54146
54374
  "- A failed fetch is a signal to change tool, never a finding. NEVER report that you could not",
54147
54375
  " check, verify, or reach a publicly reachable target until you have actually tried the",
54148
54376
  " browser on it in this turn.",
54377
+ "- An internal Mrrlin route (`/deliverables/...`, `/wiki/...`, `/specs/...`, `/items/...`,",
54378
+ " `/memory/...`) is NEVER a browser target, even when it appears as a literal URL/path in a",
54379
+ " message or a screenshot. Resolve it through the typed tools instead: `list_wiki_pages` (by",
54380
+ " `folder`/`q`) or `search_wiki_pages` to find the page, then `get_wiki_page` for its content;",
54381
+ " `get_task`/`list_tasks` for `/items/<id>`. Opening these in `mrrlin-browser` is slower, can",
54382
+ " hit an operator login/consent wall the automation cannot clear on its own, and needlessly",
54383
+ " occupies the one shared browser session. Only reach for the browser on a Mrrlin route after a",
54384
+ " typed tool call for it has actually failed.",
54149
54385
  "",
54150
54386
  "Initiative contract \u2014 do the obvious next step instead of offering it:",
54151
54387
  "- When a next step is cheap, clearly wanted, and finishable in this turn, just do it and",
@@ -55796,6 +56032,11 @@ function checkTelemetryDefaultProject(env, warn) {
55796
56032
  }
55797
56033
  async function startDirectorBridge() {
55798
56034
  const config2 = parseConfig();
56035
+ const failureLog = createFailureLog();
56036
+ const suppressedPushLine = (label, error51) => {
56037
+ const { emit, suppressed } = failureLog.shouldEmit(label, errorKind(error51));
56038
+ return emit ? failureLogLine(label, error51, suppressed) : "";
56039
+ };
55799
56040
  let telemetryPipelineRunning = false;
55800
56041
  checkTelemetryDefaultProject(process.env, (m) => console.error(m));
55801
56042
  const turnLock = createTurnLock();
@@ -55880,6 +56121,7 @@ async function startDirectorBridge() {
55880
56121
  provider: tokenProvider,
55881
56122
  persist: persistOperatorToken,
55882
56123
  intervalMs: tokenRefreshIntervalMs(),
56124
+ failureLog,
55883
56125
  onOk: () => health.setApiAuth("ok"),
55884
56126
  onError: (e) => {
55885
56127
  if (e instanceof AgentTokenProviderError && e.code === "REVOKED_OR_EXPIRED") {
@@ -56010,17 +56252,17 @@ async function startDirectorBridge() {
56010
56252
  // Builds the gate deps (resolving domain/heads/criteria from the page's task + spec) and runs the
56011
56253
  // same convergence loop the spec-gate uses. runId is threaded for verdict provenance (decision 10).
56012
56254
  // Authoring uses the defaults (runAgentExec + loadPersona) — no extra wiring needed for those.
56013
- runDeliverableGate: async ({ projectSlug, runId, deliverablePageId, declaredOutcome }) => {
56255
+ runDeliverableGate: async ({ projectSlug, runId, deliverablePageId, attemptOutcome }) => {
56014
56256
  const gateDeps = await buildDeliverableGateDeps({
56015
56257
  client,
56016
56258
  projectSlug,
56017
56259
  deliverablePageId,
56018
- declaredOutcome,
56260
+ attemptOutcome,
56019
56261
  cwd: config2.codexCwd ?? process.cwd(),
56020
56262
  codexExecutable: config2.codexExecutable,
56021
56263
  runId
56022
56264
  });
56023
- return runDeliverableGate({ projectSlug, deliverablePageId, declaredOutcome }, gateDeps);
56265
+ return runDeliverableGate({ projectSlug, deliverablePageId, attemptOutcome }, gateDeps);
56024
56266
  }
56025
56267
  };
56026
56268
  const sessionSinks = createSessionSinkRegistry();
@@ -56402,12 +56644,13 @@ async function startDirectorBridge() {
56402
56644
  const runTrackedSweep = async (id, label, fn) => {
56403
56645
  try {
56404
56646
  await fn();
56647
+ failureLog.noteSuccess(id);
56405
56648
  await bridgeHealth.recordSuccess();
56406
56649
  } catch (error51) {
56407
- process.stderr.write(
56408
- `[mrrlin-mcp director-bridge] ${label} failed: ${describeError(error51)}
56409
- `
56410
- );
56650
+ const kind = errorKind(error51);
56651
+ failureLog.recordFailure(id, kind);
56652
+ const { emit, suppressed } = failureLog.shouldEmit(id, kind);
56653
+ if (emit) process.stderr.write(failureLogLine(label, error51, suppressed));
56411
56654
  bridgeHealth.recordFailure(id, error51);
56412
56655
  await bridgeHealth.maybeCrash();
56413
56656
  }
@@ -56543,6 +56786,7 @@ async function startDirectorBridge() {
56543
56786
  fetchImpl: authedFetch,
56544
56787
  logger: bridgeLogger,
56545
56788
  bridgeHealth,
56789
+ failureLog,
56546
56790
  dispatchEnqueued: (projectSlug, work) => {
56547
56791
  if (work.kind === "execution_run") driveRunNow(projectSlug, work.runId);
56548
56792
  else driveAsyncNow(projectSlug, work.jobId);
@@ -56572,8 +56816,7 @@ async function startDirectorBridge() {
56572
56816
  agentPoolLoopHandle = startAgentPoolLoop({
56573
56817
  push: (snapshot) => client.putOperatorAgentPool(snapshot),
56574
56818
  onError: (err) => process.stderr.write(
56575
- `[mrrlin-mcp director-bridge] agent-pool snapshot push failed: ${describeError(err)}
56576
- `
56819
+ suppressedPushLine("agent-pool snapshot push", err)
56577
56820
  )
56578
56821
  });
56579
56822
  }
@@ -56582,8 +56825,7 @@ async function startDirectorBridge() {
56582
56825
  providerReadinessLoopHandle = startProviderReadinessLoop({
56583
56826
  push: (readiness) => client.putOperatorProviderReadiness(readiness),
56584
56827
  onError: (err) => process.stderr.write(
56585
- `[mrrlin-mcp director-bridge] provider-readiness push failed: ${describeError(err)}
56586
- `
56828
+ suppressedPushLine("provider-readiness push", err)
56587
56829
  )
56588
56830
  });
56589
56831
  }
@@ -64147,7 +64389,8 @@ function installService(deps) {
64147
64389
  deps.log("pm2 not found on PATH. Install it first: npm i -g pm2");
64148
64390
  return { ok: false };
64149
64391
  }
64150
- const resolvedBins = bins.filter((b) => b !== null);
64392
+ const claudeBin = deps.which("claude");
64393
+ const resolvedBins = [...bins, claudeBin].filter((b) => b !== null);
64151
64394
  const pathEnv = Array.from(new Set(resolvedBins.map((b) => import_node_path28.default.dirname(b)))).join(":");
64152
64395
  const apiBaseUrl = deps.env.MRRLIN_API_BASE_URL?.trim() || void 0;
64153
64396
  const staging = deps.env.MRRLIN_STAGING === "1" || !apiBaseUrl && bridgeUrlImpliesStaging(deps.env.MRRLIN_BRIDGE_URL);
@@ -64544,6 +64787,13 @@ Usage:
64544
64787
  - MRRLIN_DIRECTOR_BRIDGE_EXEC_TURN_TIMEOUT_SECONDS (DEPRECATED;
64545
64788
  unset by default. When set, an extra per-turn hard cap: the
64546
64789
  effective turn deadline becomes min(this, remaining run budget))
64790
+ - MRRLIN_BRIDGE_FAILURE_LOG_WINDOW_SECONDS (default 60;
64791
+ how long an IDENTICAL failure of the same periodic operation
64792
+ is suppressed on stderr before it prints again, carrying the
64793
+ count it swallowed. A changed error kind, a different
64794
+ operation, or the first failure after a success always
64795
+ prints immediately. Failures are counted for the tick's
64796
+ scheduler-tick degraded block whether or not the line prints)
64547
64797
  - MRRLIN_DIRECTOR_BRIDGE_CONTEXT_MAX_ARTIFACTS (default 50;
64548
64798
  most-recent artifacts kept in the execution context bundle)
64549
64799
 
@@ -64620,6 +64870,14 @@ async function main() {
64620
64870
  return;
64621
64871
  }
64622
64872
  case "browser-serve": {
64873
+ const reaped = reapOrphanedBrowserProcesses();
64874
+ if (reaped.killedPids.length > 0) {
64875
+ const childNote = reaped.killedChildPids.length > 0 ? ` (plus ${reaped.killedChildPids.length} browser child process(es) they had already opened: ${reaped.killedChildPids.join(", ")})` : "";
64876
+ process.stderr.write(
64877
+ `[mrrlin-mcp browser-serve] reaped ${reaped.killedPids.length} orphaned browser process(es) with a dead parent: ${reaped.killedPids.join(", ")}${childNote}
64878
+ `
64879
+ );
64880
+ }
64623
64881
  const { command: command2, args, env } = browserServeArgs();
64624
64882
  const r = (0, import_node_child_process18.spawnSync)(command2, args, { stdio: "inherit", env: { ...process.env, ...env } });
64625
64883
  process.exitCode = r.status ?? 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.4.22",
3
+ "version": "0.4.27",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mrrlin-mcp": "dist/bin.cjs"
@@ -24,9 +24,9 @@
24
24
  "@mrrlin/client": "0.0.0",
25
25
  "@mrrlin/agent-client": "0.0.0",
26
26
  "@mrrlin/director-e2e": "0.0.0",
27
+ "@mrrlin/wiki": "0.0.0",
27
28
  "@mrrlin/schemas": "0.0.0",
28
- "@mrrlin/tsconfig": "0.0.0",
29
- "@mrrlin/wiki": "0.0.0"
29
+ "@mrrlin/tsconfig": "0.0.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "@iarna/toml": "^2.2.5",