@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.
- package/dist/bin.cjs +250 -46
- 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
|
-
|
|
41298
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
44158
|
+
const attemptOutcomeBlock = attemptOutcome ? [
|
|
43999
44159
|
`--- DECLARED ATTEMPT OUTCOME (review consistency; do NOT rewrite or choose it) ---`,
|
|
44000
|
-
`outcome: ${
|
|
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
|
-
${
|
|
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${
|
|
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
|
-
...
|
|
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
|
-
|
|
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.
|
|
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,
|
|
56266
|
+
runDeliverableGate: async ({ projectSlug, runId, deliverablePageId, attemptOutcome }) => {
|
|
56083
56267
|
const gateDeps = await buildDeliverableGateDeps({
|
|
56084
56268
|
client,
|
|
56085
56269
|
projectSlug,
|
|
56086
56270
|
deliverablePageId,
|
|
56087
|
-
|
|
56271
|
+
attemptOutcome,
|
|
56088
56272
|
cwd: config2.codexCwd ?? process.cwd(),
|
|
56089
56273
|
codexExecutable: config2.codexExecutable,
|
|
56090
56274
|
runId
|
|
56091
56275
|
});
|
|
56092
|
-
return runDeliverableGate({ projectSlug, deliverablePageId,
|
|
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
|
-
|
|
56477
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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/
|
|
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",
|