@mrrlin-dev/mcp 0.4.23 → 0.4.28

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 +250 -46
  2. package/package.json +4 -4
package/dist/bin.cjs CHANGED
@@ -20875,6 +20875,17 @@ function reapOrphanedBrowserProcesses(opts = {}) {
20875
20875
  }
20876
20876
  return { checked: candidates.length, killedPids, killedChildPids };
20877
20877
  }
20878
+ function checkBrowserBusy(opts = {}) {
20879
+ const homeDir = opts.homeDir ?? import_node_os.default.homedir();
20880
+ const { profilePath } = browserPaths(homeDir);
20881
+ const listProcesses = opts.listProcesses ?? defaultListProcesses;
20882
+ const flag = `--user-data-dir=${profilePath}`;
20883
+ const flagSpaced = `--user-data-dir ${profilePath}`;
20884
+ const holder = listProcesses().find(
20885
+ (p) => (p.command.includes(flag) || p.command.includes(flagSpaced)) && !p.command.includes("@playwright/mcp") && !p.command.includes("--type=")
20886
+ );
20887
+ return { busy: holder !== void 0, holderPid: holder?.pid ?? null };
20888
+ }
20878
20889
  function externalAgentsServeArgs() {
20879
20890
  return {
20880
20891
  command: process.execPath,
@@ -21478,14 +21489,23 @@ async function verifyProviderReadiness(provider, run3, opts = {}) {
21478
21489
  const checks = { cli: false, version: false, auth: false, mcpReachable: false };
21479
21490
  const details = [];
21480
21491
  let cliResult;
21492
+ let cliUnreachable = false;
21481
21493
  try {
21482
21494
  cliResult = run3(["--version"]);
21483
21495
  checks.cli = cliResult.status === 0;
21496
+ if (!checks.cli) {
21497
+ cliUnreachable = cliResult.status === null;
21498
+ details.push(
21499
+ cliUnreachable ? `cli: \`${provider}\` binary not found on PATH` : `cli: \`${provider} --version\` exited with code ${cliResult.status}`
21500
+ );
21501
+ }
21484
21502
  } catch {
21503
+ cliUnreachable = true;
21504
+ details.push(`cli: \`${provider}\` binary not found on PATH`);
21485
21505
  }
21486
- if (!checks.cli) details.push("cli: binary not available or exited non-zero");
21487
21506
  if (provider === "codex") {
21488
21507
  checks.version = true;
21508
+ } else if (cliUnreachable) {
21489
21509
  } else {
21490
21510
  const raw = (cliResult?.stdout || cliResult?.stderr) ?? "";
21491
21511
  const version2 = parseCodexVersion(raw);
@@ -21500,6 +21520,7 @@ async function verifyProviderReadiness(provider, run3, opts = {}) {
21500
21520
  } catch {
21501
21521
  details.push(`auth: ${authPath} not found`);
21502
21522
  }
21523
+ } else if (cliUnreachable) {
21503
21524
  } else {
21504
21525
  try {
21505
21526
  const result = run3(["auth", "status"]);
@@ -21516,6 +21537,8 @@ async function verifyProviderReadiness(provider, run3, opts = {}) {
21516
21537
  checks.mcpReachable = isExternalAgentsWrapper(parsed.mcp_servers?.["external-agents"]);
21517
21538
  } catch {
21518
21539
  }
21540
+ if (!checks.mcpReachable) details.push("mcpReachable: external-agents server entry missing or invalid");
21541
+ } else if (cliUnreachable) {
21519
21542
  } else {
21520
21543
  const configPath = resolveClaudeConfigPath2(opts);
21521
21544
  try {
@@ -21524,8 +21547,8 @@ async function verifyProviderReadiness(provider, run3, opts = {}) {
21524
21547
  checks.mcpReachable = isExternalAgentsWrapper(parsed.mcpServers?.["external-agents"]);
21525
21548
  } catch {
21526
21549
  }
21550
+ if (!checks.mcpReachable) details.push("mcpReachable: external-agents server entry missing or invalid");
21527
21551
  }
21528
- if (!checks.mcpReachable) details.push("mcpReachable: external-agents server entry missing or invalid");
21529
21552
  const ready = checks.cli && checks.version && checks.auth && checks.mcpReachable;
21530
21553
  return { ready, checks, details };
21531
21554
  }
@@ -37077,7 +37100,10 @@ var ASYNC_JOB_FAILURE_REASONS = [
37077
37100
  "provider_unavailable",
37078
37101
  "spec_changed",
37079
37102
  "stale_lease",
37080
- "consensus_blocked"
37103
+ "consensus_blocked",
37104
+ // async-flow-throughput Slice 1: the spec drafter rejected its own output for exceeding the
37105
+ // per-impact acceptance-criteria / character budget. Retryable via the normal async-job path.
37106
+ "spec_over_budget"
37081
37107
  ];
37082
37108
  var mrrlinAsyncJobStatusSchema = external_exports.enum(ASYNC_JOB_STATUSES);
37083
37109
  var mrrlinAsyncJobFailureReasonSchema = external_exports.enum(ASYNC_JOB_FAILURE_REASONS);
@@ -41278,6 +41304,82 @@ function describeError(error51) {
41278
41304
  const detail = detailFromCause(error51.cause);
41279
41305
  return detail ? `${error51.message} (${truncate(detail)})` : error51.message;
41280
41306
  }
41307
+ function errorKind(error51) {
41308
+ if (!(error51 instanceof Error)) return null;
41309
+ const cause = error51.cause;
41310
+ if (!cause || typeof cause !== "object") return null;
41311
+ const direct = cause.code;
41312
+ if (typeof direct === "string") return direct;
41313
+ const errors = cause.errors;
41314
+ if (Array.isArray(errors) && errors.length > 0) {
41315
+ const first = errors[0]?.code;
41316
+ if (typeof first === "string") return first;
41317
+ }
41318
+ return null;
41319
+ }
41320
+ var DEFAULT_FAILURE_LOG_WINDOW_SECONDS = 60;
41321
+ function failureLogWindowMs(env = process.env) {
41322
+ const raw = (env.MRRLIN_BRIDGE_FAILURE_LOG_WINDOW_SECONDS ?? "").trim();
41323
+ const parsed = Number.parseInt(raw, 10);
41324
+ const seconds = Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_FAILURE_LOG_WINDOW_SECONDS;
41325
+ return seconds * 1e3;
41326
+ }
41327
+ function createFailureLog(opts = {}) {
41328
+ const windowMs = opts.windowMs ?? failureLogWindowMs();
41329
+ const now = opts.now ?? Date.now;
41330
+ const lastEmit = /* @__PURE__ */ new Map();
41331
+ const counts = /* @__PURE__ */ new Map();
41332
+ let windowStartedAt = null;
41333
+ return {
41334
+ /** Decide whether this failure should be printed. Returns the suppressed count to include in
41335
+ * the line when it is. */
41336
+ shouldEmit(operation, kind) {
41337
+ const prev = lastEmit.get(operation);
41338
+ const sameKind = prev != null && prev.kind === kind;
41339
+ const withinWindow = prev != null && now() - prev.at < windowMs;
41340
+ if (sameKind && withinWindow) {
41341
+ prev.suppressed += 1;
41342
+ return { emit: false, suppressed: prev.suppressed };
41343
+ }
41344
+ const suppressed = sameKind ? prev?.suppressed ?? 0 : 0;
41345
+ lastEmit.set(operation, { kind, at: now(), suppressed: 0 });
41346
+ return { emit: true, suppressed };
41347
+ },
41348
+ /** A success resets the operation's suppression state, so the next failure always prints. */
41349
+ noteSuccess(operation) {
41350
+ lastEmit.delete(operation);
41351
+ },
41352
+ /** Count a failure for the degraded report. Only the breaker-tracked operations are counted. */
41353
+ recordFailure(operation, kind) {
41354
+ if (windowStartedAt == null) windowStartedAt = now();
41355
+ const entry = counts.get(operation) ?? { failures: 0, lastErrorKind: null };
41356
+ entry.failures += 1;
41357
+ entry.lastErrorKind = kind;
41358
+ counts.set(operation, entry);
41359
+ },
41360
+ /** Snapshot the window and start a fresh one. Always returns all three operations, reporting
41361
+ * zeros for a clean window — it never fabricates a degraded state and never omits the key. */
41362
+ drainWindow() {
41363
+ const startedAt = windowStartedAt;
41364
+ const report = {
41365
+ windowStartedAt: startedAt == null ? null : new Date(startedAt).toISOString(),
41366
+ operations: {
41367
+ schedulerTick: counts.get("schedulerTick") ?? { failures: 0, lastErrorKind: null },
41368
+ claimSweep: counts.get("claimSweep") ?? { failures: 0, lastErrorKind: null },
41369
+ asyncSweep: counts.get("asyncSweep") ?? { failures: 0, lastErrorKind: null }
41370
+ }
41371
+ };
41372
+ counts.clear();
41373
+ windowStartedAt = null;
41374
+ return report;
41375
+ }
41376
+ };
41377
+ }
41378
+ function failureLogLine(label, error51, suppressed) {
41379
+ const tail2 = suppressed > 0 ? ` (${suppressed} identical suppressed since the last line)` : "";
41380
+ return `[mrrlin-mcp director-bridge] ${label} failed: ${describeError(error51)}${tail2}
41381
+ `;
41382
+ }
41281
41383
 
41282
41384
  // src/token-refresher.ts
41283
41385
  function createTokenRefresher(opts) {
@@ -41292,12 +41394,19 @@ function createTokenRefresher(opts) {
41292
41394
  opts.persist(token);
41293
41395
  last = token;
41294
41396
  }
41397
+ opts.failureLog?.noteSuccess("tokenRefresh");
41295
41398
  opts.onOk?.();
41296
41399
  } catch (error51) {
41297
- process.stderr.write(
41298
- `[mrrlin-mcp director-bridge] token refresh failed (will retry next tick): ${describeError(error51)}
41400
+ const label = "token refresh (will retry next tick)";
41401
+ if (opts.failureLog) {
41402
+ const { emit, suppressed } = opts.failureLog.shouldEmit("tokenRefresh", errorKind(error51));
41403
+ if (emit) process.stderr.write(failureLogLine(label, error51, suppressed));
41404
+ } else {
41405
+ process.stderr.write(
41406
+ `[mrrlin-mcp director-bridge] token refresh failed (will retry next tick): ${describeError(error51)}
41299
41407
  `
41300
- );
41408
+ );
41409
+ }
41301
41410
  opts.onError?.(error51);
41302
41411
  }
41303
41412
  }
@@ -41724,7 +41833,7 @@ Do NOT derail the current task - if the operator has an in-flight question, answ
41724
41833
  }
41725
41834
 
41726
41835
  // src/_generated/version.ts
41727
- var PKG_VERSION = "0.4.23";
41836
+ var PKG_VERSION = "0.4.28";
41728
41837
 
41729
41838
  // src/api-base-url-migrate.ts
41730
41839
  var import_node_fs3 = __toESM(require("node:fs"), 1);
@@ -42180,6 +42289,32 @@ function computeNextTickDelayMs(nextWakeAt, nowMs, opts = {}) {
42180
42289
  function count(value) {
42181
42290
  return Array.isArray(value) ? value.length : 0;
42182
42291
  }
42292
+ var KNOWN_STOP_REASONS = new Set(mrrlinStopReasonSchema.options);
42293
+ function tallyReasons(value) {
42294
+ const out = {};
42295
+ if (!Array.isArray(value)) return out;
42296
+ for (const entry of value) {
42297
+ const raw = entry?.reason;
42298
+ const trimmed = typeof raw === "string" ? raw.trim() : "";
42299
+ const key = KNOWN_STOP_REASONS.has(trimmed) ? trimmed : "unknown";
42300
+ out[key] = (out[key] ?? 0) + 1;
42301
+ }
42302
+ return out;
42303
+ }
42304
+ function mergeReasons(parts) {
42305
+ const out = {};
42306
+ for (const part of parts) {
42307
+ for (const [key, n] of Object.entries(part)) out[key] = (out[key] ?? 0) + n;
42308
+ }
42309
+ return out;
42310
+ }
42311
+ function schedulerTickFailureLine(deps, error51) {
42312
+ const kind = errorKind(error51);
42313
+ deps.failureLog?.recordFailure("schedulerTick", kind);
42314
+ if (!deps.failureLog) return failureLogLine("scheduler tick", error51, 0);
42315
+ const { emit, suppressed } = deps.failureLog.shouldEmit("schedulerTick", kind);
42316
+ return emit ? failureLogLine("scheduler tick", error51, suppressed) : "";
42317
+ }
42183
42318
  function dispatchTickWork(deps, results) {
42184
42319
  if (!deps.dispatchEnqueued) return;
42185
42320
  for (const result of results) {
@@ -42210,8 +42345,7 @@ async function attemptSchedulerTick(deps) {
42210
42345
  const { response, parsedBody } = bridgeHealth ? await withTimeout(parseTick) : await parseTick(new AbortController().signal);
42211
42346
  if (!response.ok) {
42212
42347
  process.stderr.write(
42213
- `[mrrlin-mcp director-bridge] scheduler tick failed: HTTP ${response.status} from /internal/scheduler/run
42214
- `
42348
+ schedulerTickFailureLine(deps, new Error(`HTTP ${response.status} from /internal/scheduler/run`))
42215
42349
  );
42216
42350
  bridgeHealth?.recordFailure("schedulerTick", new Error(`HTTP ${response.status}`));
42217
42351
  return { nextWakeAt: null, failed: true };
@@ -42234,6 +42368,9 @@ async function attemptSchedulerTick(deps) {
42234
42368
  enqueuedSpecDrafts: results.reduce((sum, r) => sum + count(r.enqueuedSpecDrafts), 0),
42235
42369
  autoApprovedSpecs: results.reduce((sum, r) => sum + count(r.autoApprovedSpecs), 0),
42236
42370
  skippedTasks: results.reduce((sum, r) => sum + count(r.skippedTasks), 0),
42371
+ // Slice 4: always present. A tick with no skips emits `{}`, never an absent key and never
42372
+ // null, so a consumer never has to distinguish "no skips" from "this bridge is too old".
42373
+ skippedReasons: mergeReasons(results.map((r) => tallyReasons(r.skippedTasks))),
42237
42374
  failures: results.reduce((sum, r) => sum + count(r.failures), 0)
42238
42375
  },
42239
42376
  perProject: results.map((r) => ({
@@ -42244,18 +42381,31 @@ async function attemptSchedulerTick(deps) {
42244
42381
  enqueuedSpecDrafts: count(r.enqueuedSpecDrafts),
42245
42382
  autoApprovedSpecs: count(r.autoApprovedSpecs),
42246
42383
  skippedTasks: count(r.skippedTasks),
42384
+ skippedReasons: tallyReasons(r.skippedTasks),
42247
42385
  failures: count(r.failures)
42248
42386
  })),
42249
- nextWakeAt: typeof data.nextWakeAt === "string" ? data.nextWakeAt : null
42387
+ nextWakeAt: typeof data.nextWakeAt === "string" ? data.nextWakeAt : null,
42388
+ // Slice 3: partial degradation had no signal — the breaker only trips on TOTAL failure, so
42389
+ // ~2 800 connect timeouts a day were invisible outside raw stderr. This rides a payload the
42390
+ // operator already reads. Always present, always all three tracked operations, zeros for a
42391
+ // clean window: it never fabricates a degraded state and never omits the key.
42392
+ degraded: deps.failureLog?.drainWindow() ?? {
42393
+ windowStartedAt: null,
42394
+ operations: {
42395
+ schedulerTick: { failures: 0, lastErrorKind: null },
42396
+ claimSweep: { failures: 0, lastErrorKind: null },
42397
+ asyncSweep: { failures: 0, lastErrorKind: null }
42398
+ }
42399
+ }
42250
42400
  }
42251
42401
  });
42252
42402
  dispatchTickWork(deps, results);
42403
+ deps.failureLog?.noteSuccess("schedulerTick");
42253
42404
  await bridgeHealth?.recordSuccess();
42254
42405
  return { nextWakeAt: typeof data.nextWakeAt === "string" ? data.nextWakeAt : null, failed: false };
42255
42406
  } catch (error51) {
42256
42407
  process.stderr.write(
42257
- `[mrrlin-mcp director-bridge] scheduler tick failed: ${describeError(error51)}
42258
- `
42408
+ schedulerTickFailureLine(deps, error51)
42259
42409
  );
42260
42410
  bridgeHealth?.recordFailure("schedulerTick", error51);
42261
42411
  return { nextWakeAt: null, failed: true };
@@ -43093,6 +43243,16 @@ function parseAcceptanceCriteria(markdown) {
43093
43243
  }
43094
43244
  return items.length > 0 ? { items } : null;
43095
43245
  }
43246
+ function resolveSpecBudget(impact) {
43247
+ switch (impact?.toLowerCase()) {
43248
+ case "high":
43249
+ return { maxRequirements: 12, maxChars: 25e3 };
43250
+ case "medium":
43251
+ return { maxRequirements: 6, maxChars: 12e3 };
43252
+ default:
43253
+ return { maxRequirements: 3, maxChars: 6e3 };
43254
+ }
43255
+ }
43096
43256
 
43097
43257
  // src/consensus/spec-gate.ts
43098
43258
  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.";
@@ -43971,7 +44131,7 @@ function renderCriteriaBlock(criteria) {
43971
44131
  return criteria.map((c, i) => `${i + 1}. [ ] ${c}`).join("\n");
43972
44132
  }
43973
44133
  async function buildDeliverableGateDeps(args) {
43974
- const { client, projectSlug, deliverablePageId, declaredOutcome, cwd, codexExecutable, runId, seat } = args;
44134
+ const { client, projectSlug, deliverablePageId, attemptOutcome, cwd, codexExecutable, runId, seat } = args;
43975
44135
  const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
43976
44136
  const timeoutMs = Number(process.env["MRRLIN_CONSENSUS_HEAD_TIMEOUT_MS"] ?? 15e4);
43977
44137
  const page = await client.getWikiPage(projectSlug, deliverablePageId);
@@ -43995,9 +44155,9 @@ async function buildDeliverableGateDeps(args) {
43995
44155
  const heads = resolveHeads(domain2, impact);
43996
44156
  const maxHeadCalls = 2 * rounds * (heads.length + 1);
43997
44157
  const criteriaBlock = renderCriteriaBlock(criteria);
43998
- const declaredOutcomeBlock = declaredOutcome ? [
44158
+ const attemptOutcomeBlock = attemptOutcome ? [
43999
44159
  `--- DECLARED ATTEMPT OUTCOME (review consistency; do NOT rewrite or choose it) ---`,
44000
- `outcome: ${declaredOutcome}`,
44160
+ `outcome: ${attemptOutcome}`,
44001
44161
  `Meaning: completed = positive result achieved; blocked_missing_input = required input/evidence unavailable; negative_result = real attempt showed the requested positive result did not materialize.`
44002
44162
  ].join("\n") : "";
44003
44163
  return {
@@ -44009,7 +44169,7 @@ async function buildDeliverableGateDeps(args) {
44009
44169
  return config2.consensusEnabled;
44010
44170
  },
44011
44171
  async readAcceptanceCriteria() {
44012
- return { specPageId, criteria };
44172
+ return { specPageId, criteria, maxCriteria: resolveSpecBudget(impact).maxRequirements };
44013
44173
  },
44014
44174
  async readDeliverable() {
44015
44175
  const p = await client.getWikiPage(projectSlug, deliverablePageId);
@@ -44025,7 +44185,7 @@ ${dismissed}
44025
44185
  --- ACCEPTANCE CRITERIA (the yardstick \u2014 deliverable for task ${taskId ?? "?"}: ${taskTitle}) ---
44026
44186
  ${criteriaBlock}
44027
44187
 
44028
- ${declaredOutcomeBlock ? `${declaredOutcomeBlock}
44188
+ ${attemptOutcomeBlock ? `${attemptOutcomeBlock}
44029
44189
 
44030
44190
  ` : ""}--- DELIVERABLE (DATA under review) ---
44031
44191
  ${artifact}`,
@@ -44064,7 +44224,7 @@ ${criteriaBlock}
44064
44224
  ${artifact}`,
44065
44225
  developerInstructions: `${DELIVERABLE_ANTIINJECTION_INSTRUCTION}
44066
44226
 
44067
- 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.`,
44227
+ 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.`,
44068
44228
  cwd,
44069
44229
  timeoutMs,
44070
44230
  sandbox: "read-only",
@@ -44127,7 +44287,7 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
44127
44287
  payload: {
44128
44288
  source: "deliverable-gate",
44129
44289
  outcome,
44130
- ...declaredOutcome ? { declaredOutcome } : {},
44290
+ ...attemptOutcome ? { attemptOutcome } : {},
44131
44291
  runId,
44132
44292
  taskId,
44133
44293
  deliverablePageId,
@@ -44728,9 +44888,11 @@ function mapDeliverableGateResult(result) {
44728
44888
  reason: "reviewers were unavailable (provider/quota) during the deliverable review (CONSENSUS_UNAVAILABLE)"
44729
44889
  };
44730
44890
  case "CONSENSUS_BLOCKED": {
44891
+ const over = result.yardstickOverBudget;
44892
+ 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.` : "";
44731
44893
  return {
44732
44894
  action: "handoff",
44733
- reason: `reviewers did not converge; ${formatDeliverableGateDiagnostics(result.diagnostics ?? [])} (CONSENSUS_BLOCKED)`
44895
+ reason: `reviewers did not converge; ${formatDeliverableGateDiagnostics(result.diagnostics ?? [])} (CONSENSUS_BLOCKED)${yardstick}`
44734
44896
  };
44735
44897
  }
44736
44898
  case "MISSING_ACCEPTANCE_CRITERIA":
@@ -44756,17 +44918,6 @@ var SPECS_FOLDER = "Specs";
44756
44918
  var DRAFT_SPEC_TLDR_MAX_CHARS = 300;
44757
44919
  var DRAFT_SPEC_PERSONA = "spec-author";
44758
44920
  var PAGE_ID_MAX_ATTEMPTS = 5;
44759
- function resolveSpecBudget(impact) {
44760
- const tier = impact?.toLowerCase();
44761
- switch (tier) {
44762
- case "high":
44763
- return { maxRequirements: 12, maxChars: 25e3 };
44764
- case "medium":
44765
- return { maxRequirements: 6, maxChars: 12e3 };
44766
- default:
44767
- return { maxRequirements: 3, maxChars: 6e3 };
44768
- }
44769
- }
44770
44921
  function defaultSleep(ms) {
44771
44922
  return new Promise((resolve) => setTimeout(resolve, ms));
44772
44923
  }
@@ -44792,6 +44943,25 @@ ${body}`;
44792
44943
  }
44793
44944
  return { markdown: body, tldrChars };
44794
44945
  }
44946
+ function checkSpecBudget(markdown, impact) {
44947
+ const budget = resolveSpecBudget(impact);
44948
+ const parsed = parseAcceptanceCriteria(markdown);
44949
+ const criteria = parsed?.items.length ?? 0;
44950
+ const chars = markdown.length;
44951
+ const overCriteria = parsed != null && criteria > budget.maxRequirements;
44952
+ const overChars = chars > budget.maxChars;
44953
+ if (!overCriteria && !overChars) return null;
44954
+ const faults = [];
44955
+ if (overCriteria) faults.push(`${criteria} acceptance-criteria items (maximum ${budget.maxRequirements})`);
44956
+ if (overChars) faults.push(`${chars} characters (maximum ${budget.maxChars})`);
44957
+ return {
44958
+ criteria,
44959
+ chars,
44960
+ maxCriteria: budget.maxRequirements,
44961
+ maxChars: budget.maxChars,
44962
+ message: `spec draft is over budget: ${faults.join(" and ")}`
44963
+ };
44964
+ }
44795
44965
  function extractDeployTarget(markdown) {
44796
44966
  const section = /(## Deploy Target\s*\n)([\s\S]*?)(?=\n## |$)/i.exec(markdown);
44797
44967
  if (!section || section.index === void 0) {
@@ -45059,6 +45229,10 @@ async function runDraftSpec(ctx, input, deps) {
45059
45229
  }
45060
45230
  const { markdown: specMarkdown, deployTarget: parsedDeployTarget } = extractDeployTarget(exec.text);
45061
45231
  const { markdown, tldrChars } = enforceSpecOutputContract(specMarkdown, taskId, task.title);
45232
+ const overBudget = checkSpecBudget(markdown, task.impact);
45233
+ if (overBudget) {
45234
+ return { kind: "retry", failureReason: "spec_over_budget", error: overBudget.message };
45235
+ }
45062
45236
  let pageId;
45063
45237
  if (adoptedId) {
45064
45238
  try {
@@ -47735,7 +47909,7 @@ async function driveNonCodeRun(deps, projectSlug, runId, leaseId, task, domain2)
47735
47909
  taskId: task.id,
47736
47910
  runId,
47737
47911
  deliverablePageId: pageId,
47738
- declaredOutcome: parsedResult.outcome
47912
+ attemptOutcome: parsedResult.outcome
47739
47913
  });
47740
47914
  } catch (error51) {
47741
47915
  await failRun(
@@ -48595,6 +48769,7 @@ async function runDeliverableGate(target, deps) {
48595
48769
  const deliverable = await deps.readDeliverable();
48596
48770
  let currentHash = deliverable.contentHash;
48597
48771
  let conflicted = false;
48772
+ const yardstickOverBudget = ac.maxCriteria != null && ac.criteria.length > ac.maxCriteria ? { specPageId: ac.specPageId, criteria: ac.criteria.length, maxCriteria: ac.maxCriteria } : void 0;
48598
48773
  const result = await runConsensus({
48599
48774
  artifact: deliverable.markdownBody,
48600
48775
  heads: deps.heads,
@@ -48636,7 +48811,7 @@ async function runDeliverableGate(target, deps) {
48636
48811
  }
48637
48812
  await deps.writeEvidence({
48638
48813
  deliverablePageId: deliverable.pageId,
48639
- ...target.declaredOutcome ? { declaredOutcome: target.declaredOutcome } : {},
48814
+ ...target.attemptOutcome ? { attemptOutcome: target.attemptOutcome } : {},
48640
48815
  outcome: result.outcome,
48641
48816
  rounds: result.rounds,
48642
48817
  history: result.history,
@@ -48648,7 +48823,10 @@ async function runDeliverableGate(target, deps) {
48648
48823
  outcome: "CONSENSUS_BLOCKED",
48649
48824
  rounds: result.rounds,
48650
48825
  diagnostics: diagnostics ?? [],
48651
- ...result.escalation ? { escalation: result.escalation } : {}
48826
+ ...result.escalation ? { escalation: result.escalation } : {},
48827
+ // Slice 2: attached ONLY on the blocked path. A converged review needs no explanation, and a
48828
+ // transient outcome (changed-during-review, reviewers unavailable) is not about the yardstick.
48829
+ ...yardstickOverBudget ? { yardstickOverBudget } : {}
48652
48830
  };
48653
48831
  }
48654
48832
  if (result.outcome === "CONVERGED") {
@@ -55865,6 +56043,11 @@ function checkTelemetryDefaultProject(env, warn) {
55865
56043
  }
55866
56044
  async function startDirectorBridge() {
55867
56045
  const config2 = parseConfig();
56046
+ const failureLog = createFailureLog();
56047
+ const suppressedPushLine = (label, error51) => {
56048
+ const { emit, suppressed } = failureLog.shouldEmit(label, errorKind(error51));
56049
+ return emit ? failureLogLine(label, error51, suppressed) : "";
56050
+ };
55868
56051
  let telemetryPipelineRunning = false;
55869
56052
  checkTelemetryDefaultProject(process.env, (m) => console.error(m));
55870
56053
  const turnLock = createTurnLock();
@@ -55949,6 +56132,7 @@ async function startDirectorBridge() {
55949
56132
  provider: tokenProvider,
55950
56133
  persist: persistOperatorToken,
55951
56134
  intervalMs: tokenRefreshIntervalMs(),
56135
+ failureLog,
55952
56136
  onOk: () => health.setApiAuth("ok"),
55953
56137
  onError: (e) => {
55954
56138
  if (e instanceof AgentTokenProviderError && e.code === "REVOKED_OR_EXPIRED") {
@@ -56079,17 +56263,17 @@ async function startDirectorBridge() {
56079
56263
  // Builds the gate deps (resolving domain/heads/criteria from the page's task + spec) and runs the
56080
56264
  // same convergence loop the spec-gate uses. runId is threaded for verdict provenance (decision 10).
56081
56265
  // Authoring uses the defaults (runAgentExec + loadPersona) — no extra wiring needed for those.
56082
- runDeliverableGate: async ({ projectSlug, runId, deliverablePageId, declaredOutcome }) => {
56266
+ runDeliverableGate: async ({ projectSlug, runId, deliverablePageId, attemptOutcome }) => {
56083
56267
  const gateDeps = await buildDeliverableGateDeps({
56084
56268
  client,
56085
56269
  projectSlug,
56086
56270
  deliverablePageId,
56087
- declaredOutcome,
56271
+ attemptOutcome,
56088
56272
  cwd: config2.codexCwd ?? process.cwd(),
56089
56273
  codexExecutable: config2.codexExecutable,
56090
56274
  runId
56091
56275
  });
56092
- return runDeliverableGate({ projectSlug, deliverablePageId, declaredOutcome }, gateDeps);
56276
+ return runDeliverableGate({ projectSlug, deliverablePageId, attemptOutcome }, gateDeps);
56093
56277
  }
56094
56278
  };
56095
56279
  const sessionSinks = createSessionSinkRegistry();
@@ -56471,12 +56655,13 @@ async function startDirectorBridge() {
56471
56655
  const runTrackedSweep = async (id, label, fn) => {
56472
56656
  try {
56473
56657
  await fn();
56658
+ failureLog.noteSuccess(id);
56474
56659
  await bridgeHealth.recordSuccess();
56475
56660
  } catch (error51) {
56476
- process.stderr.write(
56477
- `[mrrlin-mcp director-bridge] ${label} failed: ${describeError(error51)}
56478
- `
56479
- );
56661
+ const kind = errorKind(error51);
56662
+ failureLog.recordFailure(id, kind);
56663
+ const { emit, suppressed } = failureLog.shouldEmit(id, kind);
56664
+ if (emit) process.stderr.write(failureLogLine(label, error51, suppressed));
56480
56665
  bridgeHealth.recordFailure(id, error51);
56481
56666
  await bridgeHealth.maybeCrash();
56482
56667
  }
@@ -56612,6 +56797,7 @@ async function startDirectorBridge() {
56612
56797
  fetchImpl: authedFetch,
56613
56798
  logger: bridgeLogger,
56614
56799
  bridgeHealth,
56800
+ failureLog,
56615
56801
  dispatchEnqueued: (projectSlug, work) => {
56616
56802
  if (work.kind === "execution_run") driveRunNow(projectSlug, work.runId);
56617
56803
  else driveAsyncNow(projectSlug, work.jobId);
@@ -56641,8 +56827,7 @@ async function startDirectorBridge() {
56641
56827
  agentPoolLoopHandle = startAgentPoolLoop({
56642
56828
  push: (snapshot) => client.putOperatorAgentPool(snapshot),
56643
56829
  onError: (err) => process.stderr.write(
56644
- `[mrrlin-mcp director-bridge] agent-pool snapshot push failed: ${describeError(err)}
56645
- `
56830
+ suppressedPushLine("agent-pool snapshot push", err)
56646
56831
  )
56647
56832
  });
56648
56833
  }
@@ -56651,8 +56836,7 @@ async function startDirectorBridge() {
56651
56836
  providerReadinessLoopHandle = startProviderReadinessLoop({
56652
56837
  push: (readiness) => client.putOperatorProviderReadiness(readiness),
56653
56838
  onError: (err) => process.stderr.write(
56654
- `[mrrlin-mcp director-bridge] provider-readiness push failed: ${describeError(err)}
56655
- `
56839
+ suppressedPushLine("provider-readiness push", err)
56656
56840
  )
56657
56841
  });
56658
56842
  }
@@ -64216,7 +64400,8 @@ function installService(deps) {
64216
64400
  deps.log("pm2 not found on PATH. Install it first: npm i -g pm2");
64217
64401
  return { ok: false };
64218
64402
  }
64219
- const resolvedBins = bins.filter((b) => b !== null);
64403
+ const claudeBin = deps.which("claude");
64404
+ const resolvedBins = [...bins, claudeBin].filter((b) => b !== null);
64220
64405
  const pathEnv = Array.from(new Set(resolvedBins.map((b) => import_node_path28.default.dirname(b)))).join(":");
64221
64406
  const apiBaseUrl = deps.env.MRRLIN_API_BASE_URL?.trim() || void 0;
64222
64407
  const staging = deps.env.MRRLIN_STAGING === "1" || !apiBaseUrl && bridgeUrlImpliesStaging(deps.env.MRRLIN_BRIDGE_URL);
@@ -64525,6 +64710,13 @@ Usage:
64525
64710
  command configure bridge wires for the
64526
64711
  [mcp_servers.mrrlin-browser] block.
64527
64712
 
64713
+ mrrlin-mcp browser-status Read-only check: is the shared browser session
64714
+ actually occupied right now? Prints
64715
+ {"busy":bool,"holderPid":number|null} to stdout.
64716
+ A building block for admission control (not yet
64717
+ wired into the scheduler or Director turn-start
64718
+ \u2014 this only answers the question).
64719
+
64528
64720
  mrrlin-mcp external-agents-serve
64529
64721
  Launch the bundled @mrrlin-dev/external-agents
64530
64722
  MCP server (multi-LLM dispatch pool). This is the
@@ -64613,6 +64805,13 @@ Usage:
64613
64805
  - MRRLIN_DIRECTOR_BRIDGE_EXEC_TURN_TIMEOUT_SECONDS (DEPRECATED;
64614
64806
  unset by default. When set, an extra per-turn hard cap: the
64615
64807
  effective turn deadline becomes min(this, remaining run budget))
64808
+ - MRRLIN_BRIDGE_FAILURE_LOG_WINDOW_SECONDS (default 60;
64809
+ how long an IDENTICAL failure of the same periodic operation
64810
+ is suppressed on stderr before it prints again, carrying the
64811
+ count it swallowed. A changed error kind, a different
64812
+ operation, or the first failure after a success always
64813
+ prints immediately. Failures are counted for the tick's
64814
+ scheduler-tick degraded block whether or not the line prints)
64616
64815
  - MRRLIN_DIRECTOR_BRIDGE_CONTEXT_MAX_ARTIFACTS (default 50;
64617
64816
  most-recent artifacts kept in the execution context bundle)
64618
64817
 
@@ -64702,6 +64901,11 @@ async function main() {
64702
64901
  process.exitCode = r.status ?? 1;
64703
64902
  return;
64704
64903
  }
64904
+ case "browser-status": {
64905
+ process.stdout.write(`${JSON.stringify(checkBrowserBusy())}
64906
+ `);
64907
+ return;
64908
+ }
64705
64909
  case "external-agents-serve": {
64706
64910
  const { command: command2, args } = externalAgentsServeArgs();
64707
64911
  const r = (0, import_node_child_process18.spawnSync)(command2, args, { stdio: "inherit", env: process.env });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.4.23",
3
+ "version": "0.4.28",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mrrlin-mcp": "dist/bin.cjs"
@@ -21,12 +21,12 @@
21
21
  "@types/ws": "^8.18.1",
22
22
  "esbuild": "^0.24.0",
23
23
  "tsx": "^4.22.3",
24
- "@mrrlin/client": "0.0.0",
25
24
  "@mrrlin/agent-client": "0.0.0",
26
25
  "@mrrlin/director-e2e": "0.0.0",
27
- "@mrrlin/schemas": "0.0.0",
26
+ "@mrrlin/client": "0.0.0",
28
27
  "@mrrlin/tsconfig": "0.0.0",
29
- "@mrrlin/wiki": "0.0.0"
28
+ "@mrrlin/wiki": "0.0.0",
29
+ "@mrrlin/schemas": "0.0.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "@iarna/toml": "^2.2.5",