@mrrlin-dev/mcp 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.cjs CHANGED
@@ -4273,7 +4273,7 @@ var require_websocket = __commonJS({
4273
4273
  var http2 = require("http");
4274
4274
  var net = require("net");
4275
4275
  var tls = require("tls");
4276
- var { randomBytes: randomBytes4, createHash: createHash5 } = require("crypto");
4276
+ var { randomBytes: randomBytes4, createHash: createHash4 } = require("crypto");
4277
4277
  var { Duplex, Readable } = require("stream");
4278
4278
  var { URL: URL2 } = require("url");
4279
4279
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -4941,7 +4941,7 @@ var require_websocket = __commonJS({
4941
4941
  abortHandshake(websocket, socket, "Invalid Upgrade header");
4942
4942
  return;
4943
4943
  }
4944
- const digest = createHash5("sha1").update(key + GUID).digest("base64");
4944
+ const digest = createHash4("sha1").update(key + GUID).digest("base64");
4945
4945
  if (res.headers["sec-websocket-accept"] !== digest) {
4946
4946
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
4947
4947
  return;
@@ -5310,7 +5310,7 @@ var require_websocket_server = __commonJS({
5310
5310
  var EventEmitter3 = require("events");
5311
5311
  var http2 = require("http");
5312
5312
  var { Duplex } = require("stream");
5313
- var { createHash: createHash5 } = require("crypto");
5313
+ var { createHash: createHash4 } = require("crypto");
5314
5314
  var extension2 = require_extension();
5315
5315
  var PerMessageDeflate2 = require_permessage_deflate();
5316
5316
  var subprotocol2 = require_subprotocol();
@@ -5617,7 +5617,7 @@ var require_websocket_server = __commonJS({
5617
5617
  );
5618
5618
  }
5619
5619
  if (this._state > RUNNING) return abortHandshake(socket, 503);
5620
- const digest = createHash5("sha1").update(key + GUID).digest("base64");
5620
+ const digest = createHash4("sha1").update(key + GUID).digest("base64");
5621
5621
  const headers = [
5622
5622
  "HTTP/1.1 101 Switching Protocols",
5623
5623
  "Upgrade: websocket",
@@ -21174,7 +21174,7 @@ var import_node_http = __toESM(require("node:http"), 1);
21174
21174
  var import_node_os9 = __toESM(require("node:os"), 1);
21175
21175
  var import_node_path18 = __toESM(require("node:path"), 1);
21176
21176
  var import_node_child_process11 = require("node:child_process");
21177
- var import_node_crypto8 = __toESM(require("node:crypto"), 1);
21177
+ var import_node_crypto7 = __toESM(require("node:crypto"), 1);
21178
21178
 
21179
21179
  // src/turn-replay-buffer.ts
21180
21180
  var DEFAULT_MAX_EVENTS = 2e3;
@@ -36574,6 +36574,8 @@ var agentPoolResponseSchema = external_exports.object({
36574
36574
 
36575
36575
  // ../../packages/schemas/dist/specConsensusReview.js
36576
36576
  var mrrlinSpecConsensusSuppressionEvidenceSchema = external_exports.record(external_exports.string(), external_exports.unknown()).nullable();
36577
+ var mrrlinSpecConsensusHistorySchema = external_exports.unknown();
36578
+ var mrrlinSpecConsensusHighlightsSchema = external_exports.object({ findings: external_exports.array(external_exports.string()) });
36577
36579
  var mrrlinSpecConsensusReviewSchema = external_exports.object({
36578
36580
  projectSlug: external_exports.string().min(1),
36579
36581
  specPageId: external_exports.string().min(1),
@@ -36581,6 +36583,8 @@ var mrrlinSpecConsensusReviewSchema = external_exports.object({
36581
36583
  outcome: external_exports.string().min(1),
36582
36584
  rounds: external_exports.number().int().nonnegative(),
36583
36585
  suppressionEvidence: mrrlinSpecConsensusSuppressionEvidenceSchema.default(null),
36586
+ history: mrrlinSpecConsensusHistorySchema.nullable().default(null),
36587
+ highlights: mrrlinSpecConsensusHighlightsSchema.nullable().default(null),
36584
36588
  createdAt: external_exports.string().datetime()
36585
36589
  });
36586
36590
  var mrrlinSpecConsensusReviewCreateSchema = external_exports.object({
@@ -36588,10 +36592,26 @@ var mrrlinSpecConsensusReviewCreateSchema = external_exports.object({
36588
36592
  contentHash: external_exports.string().min(1),
36589
36593
  outcome: external_exports.string().min(1),
36590
36594
  rounds: external_exports.number().int().nonnegative(),
36591
- suppressionEvidence: mrrlinSpecConsensusSuppressionEvidenceSchema.optional()
36595
+ suppressionEvidence: mrrlinSpecConsensusSuppressionEvidenceSchema.optional(),
36596
+ history: mrrlinSpecConsensusHistorySchema.optional(),
36597
+ highlights: mrrlinSpecConsensusHighlightsSchema.optional()
36592
36598
  }).strict();
36593
36599
  var specConsensusReviewResponseSchema = external_exports.object({ data: mrrlinSpecConsensusReviewSchema });
36594
36600
  var specConsensusReviewMaybeResponseSchema = external_exports.object({ data: mrrlinSpecConsensusReviewSchema.nullable() });
36601
+ var PERSONA_ROLE_MAP = {
36602
+ architect: "design",
36603
+ "security-analyst": "security",
36604
+ "seo-analyst": "SEO",
36605
+ "compliance-reviewer": "compliance",
36606
+ debugger: "root cause",
36607
+ researcher: "research",
36608
+ "content-editor": "wording",
36609
+ "code-reviewer": "code",
36610
+ "content-author": "content",
36611
+ "seo-writer": "SEO writing",
36612
+ "spec-author": "spec"
36613
+ };
36614
+ var UNNAMED_PERSONAS = ["plan-reviewer", "scope-analyst"];
36595
36615
 
36596
36616
  // ../../packages/schemas/dist/asyncJob.js
36597
36617
  var ASYNC_JOB_STATUSES = ["queued", "running", "done", "failed", "superseded"];
@@ -40190,7 +40210,7 @@ function startCodexProbe(deps = {}) {
40190
40210
  }
40191
40211
 
40192
40212
  // src/_generated/version.ts
40193
- var PKG_VERSION = "0.4.2";
40213
+ var PKG_VERSION = "0.4.4";
40194
40214
 
40195
40215
  // src/api-base-url-migrate.ts
40196
40216
  var import_node_fs2 = __toESM(require("node:fs"), 1);
@@ -40992,7 +41012,7 @@ function startAgentPoolLoop(deps) {
40992
41012
 
40993
41013
  // src/execution-consumer.ts
40994
41014
  var import_node_child_process9 = require("node:child_process");
40995
- var import_node_crypto5 = __toESM(require("node:crypto"), 1);
41015
+ var import_node_crypto4 = __toESM(require("node:crypto"), 1);
40996
41016
  var import_node_path10 = __toESM(require("node:path"), 1);
40997
41017
  var import_node_util = require("node:util");
40998
41018
 
@@ -41684,7 +41704,7 @@ async function runSpecGate(_target, deps, opts) {
41684
41704
  });
41685
41705
  if (result.outcome === "CONVERGED") {
41686
41706
  await deps.promoteSpecReady(currentHash);
41687
- return { outcome: "CONVERGED" };
41707
+ return { outcome: "CONVERGED", headOutcomes: result.history.at(-1)?.headOutcomes ?? [] };
41688
41708
  }
41689
41709
  if (result.outcome === "CONSENSUS_BLOCKED") {
41690
41710
  return {
@@ -41734,6 +41754,63 @@ async function resolveSpecDomain(client, projectSlug, page) {
41734
41754
  }
41735
41755
  }
41736
41756
 
41757
+ // src/consensus/findings.ts
41758
+ function shouldCurateFindings(history) {
41759
+ return history.some((entry) => entry.accepted.length > 0);
41760
+ }
41761
+ var SYNTHESIZED_PLACEHOLDER = /^head .* returned .* but no parseable critical issues/;
41762
+ function selectFindingsCandidates(history) {
41763
+ const result = [];
41764
+ for (const entry of history) {
41765
+ for (const issue2 of entry.accepted) {
41766
+ if (SYNTHESIZED_PLACEHOLDER.test(issue2.text)) {
41767
+ continue;
41768
+ }
41769
+ result.push(issue2);
41770
+ }
41771
+ }
41772
+ if (result.length > 3) {
41773
+ return result.slice(0, 3);
41774
+ }
41775
+ return result;
41776
+ }
41777
+ function isFindingsHighlights(value) {
41778
+ return typeof value === "object" && value !== null && Array.isArray(value.findings) && value.findings.every((f) => typeof f === "string");
41779
+ }
41780
+ async function curateFindings(history, deps) {
41781
+ if (!shouldCurateFindings(history)) return null;
41782
+ try {
41783
+ const candidates = selectFindingsCandidates(history);
41784
+ const issuesBlock = candidates.length > 0 ? candidates.map((issue2) => `- (${issue2.category}) ${issue2.text}`).join("\n") : "(no issues survived the deterministic pre-filter)";
41785
+ const result = await deps.runCodexExec({
41786
+ prompt: `--- ACCEPTED ISSUES ---
41787
+ ${issuesBlock}`,
41788
+ developerInstructions: deps.loadPersona("findings-writer"),
41789
+ cwd: deps.cwd,
41790
+ timeoutMs: deps.timeoutMs,
41791
+ sandbox: "read-only",
41792
+ codexExecutable: deps.codexExecutable
41793
+ });
41794
+ if (!result.ok) {
41795
+ process.stderr.write(`[mrrlin-mcp consensus] findings curation failed: ${result.error}
41796
+ `);
41797
+ return null;
41798
+ }
41799
+ const parsed = JSON.parse(result.text);
41800
+ if (!isFindingsHighlights(parsed)) {
41801
+ process.stderr.write("[mrrlin-mcp consensus] findings curation returned unparseable output\n");
41802
+ return null;
41803
+ }
41804
+ return { findings: parsed.findings.slice(0, 3) };
41805
+ } catch (err) {
41806
+ process.stderr.write(
41807
+ `[mrrlin-mcp consensus] findings curation threw: ${err instanceof Error ? err.message : String(err)}
41808
+ `
41809
+ );
41810
+ return null;
41811
+ }
41812
+ }
41813
+
41737
41814
  // src/consensus/code-gate-git.ts
41738
41815
  var import_node_child_process7 = require("node:child_process");
41739
41816
  var DIFF_MAX_BYTES = 2e5;
@@ -42173,12 +42250,22 @@ ${artifact}`,
42173
42250
  }
42174
42251
  const outcome = r["outcome"];
42175
42252
  const rounds2 = r["rounds"];
42253
+ const history = r["history"];
42254
+ const highlights = await curateFindings(Array.isArray(history) ? history : [], {
42255
+ runCodexExec,
42256
+ loadPersona,
42257
+ cwd,
42258
+ timeoutMs,
42259
+ codexExecutable
42260
+ });
42176
42261
  const { domain: domain2 } = await loadIntent();
42177
42262
  try {
42178
42263
  await client.recordSpecConsensusReview(projectSlug, specPageId, {
42179
42264
  contentHash: domainScopedReviewHash2(contentHash, domain2),
42180
42265
  outcome: String(outcome),
42181
- rounds: typeof rounds2 === "number" ? rounds2 : 0
42266
+ rounds: typeof rounds2 === "number" ? rounds2 : 0,
42267
+ ...history !== void 0 ? { history } : {},
42268
+ ...highlights ? { highlights } : {}
42182
42269
  });
42183
42270
  } catch (err) {
42184
42271
  process.stderr.write(
@@ -42386,11 +42473,21 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
42386
42473
  if (typeof contentHash !== "string" || contentHash.length === 0) return;
42387
42474
  const outcome = String(r["outcome"]);
42388
42475
  const rounds2 = typeof r["rounds"] === "number" ? r["rounds"] : 0;
42476
+ const history = r["history"];
42477
+ const highlights = await curateFindings(Array.isArray(history) ? history : [], {
42478
+ runCodexExec,
42479
+ loadPersona,
42480
+ cwd,
42481
+ timeoutMs,
42482
+ codexExecutable
42483
+ });
42389
42484
  try {
42390
42485
  await client.recordSpecConsensusReview(projectSlug, deliverablePageId, {
42391
42486
  contentHash: domainScopedReviewHash2(contentHash, domain2),
42392
42487
  outcome,
42393
- rounds: rounds2
42488
+ rounds: rounds2,
42489
+ ...history !== void 0 ? { history } : {},
42490
+ ...highlights ? { highlights } : {}
42394
42491
  });
42395
42492
  } catch (err) {
42396
42493
  process.stderr.write(
@@ -42400,6 +42497,7 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
42400
42497
  }
42401
42498
  if (runId && taskId) {
42402
42499
  try {
42500
+ const diagnostics = Array.isArray(r["diagnostics"]) ? r["diagnostics"] : void 0;
42403
42501
  await client.appendExecutionArtifact(projectSlug, runId, {
42404
42502
  id: `EA-${import_node_crypto2.default.randomUUID()}`,
42405
42503
  kind: "verification",
@@ -42409,7 +42507,8 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
42409
42507
  runId,
42410
42508
  taskId,
42411
42509
  deliverablePageId,
42412
- contentHash
42510
+ contentHash,
42511
+ ...diagnostics ? { diagnostics } : {}
42413
42512
  }
42414
42513
  });
42415
42514
  } catch {
@@ -42420,6 +42519,35 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
42420
42519
  }
42421
42520
 
42422
42521
  // src/consensus/deliverable-gate-outcome.ts
42522
+ var MAX_DELIVERABLE_DIAGNOSTICS = 5;
42523
+ var MAX_DELIVERABLE_DIAGNOSTIC_LENGTH = 240;
42524
+ var MAX_DELIVERABLE_SUMMARY_LENGTH = 1200;
42525
+ function normalizeDiagnostic(text) {
42526
+ return text.replace(/\s+/g, " ").trim();
42527
+ }
42528
+ function renderSummary(total, items, omitted) {
42529
+ const label = `${total} finding${total === 1 ? "" : "s"} (${omitted} omitted)`;
42530
+ return items.length > 0 ? `${label}: ${items.join("; ")}` : label;
42531
+ }
42532
+ function formatDeliverableGateDiagnostics(diagnostics) {
42533
+ const items = [];
42534
+ for (const diagnostic of diagnostics) {
42535
+ if (items.length >= MAX_DELIVERABLE_DIAGNOSTICS) break;
42536
+ const category = normalizeDiagnostic(diagnostic.category);
42537
+ const text = normalizeDiagnostic(diagnostic.text);
42538
+ const item = `${category}: ${text}`.slice(0, MAX_DELIVERABLE_DIAGNOSTIC_LENGTH);
42539
+ const candidate = renderSummary(diagnostics.length, [...items, item], diagnostics.length - items.length - 1);
42540
+ if (candidate.length <= MAX_DELIVERABLE_SUMMARY_LENGTH) items.push(item);
42541
+ }
42542
+ let omitted = diagnostics.length - items.length;
42543
+ let summary = renderSummary(diagnostics.length, items, omitted);
42544
+ while (summary.length > MAX_DELIVERABLE_SUMMARY_LENGTH && items.length > 0) {
42545
+ items.pop();
42546
+ omitted = diagnostics.length - items.length;
42547
+ summary = renderSummary(diagnostics.length, items, omitted);
42548
+ }
42549
+ return summary;
42550
+ }
42423
42551
  function mapDeliverableGateResult(result) {
42424
42552
  switch (result.outcome) {
42425
42553
  case "DELIVERABLE_CHANGED_DURING_REVIEW":
@@ -42433,17 +42561,9 @@ function mapDeliverableGateResult(result) {
42433
42561
  reason: "reviewers were unavailable (provider/quota) during the deliverable review (CONSENSUS_UNAVAILABLE)"
42434
42562
  };
42435
42563
  case "CONSENSUS_BLOCKED": {
42436
- const clarifications = result.clarifications ?? [];
42437
- if (clarifications.length > 0) {
42438
- return {
42439
- action: "questions",
42440
- clarifications,
42441
- reason: "reviewers did not converge; unresolved scope/ambiguity/correctness issues were filed as operator questions (CONSENSUS_BLOCKED)"
42442
- };
42443
- }
42444
42564
  return {
42445
42565
  action: "handoff",
42446
- reason: "reviewers did not converge and raised no clarification-eligible issues (CONSENSUS_BLOCKED)"
42566
+ reason: `reviewers did not converge; ${formatDeliverableGateDiagnostics(result.diagnostics ?? [])} (CONSENSUS_BLOCKED)`
42447
42567
  };
42448
42568
  }
42449
42569
  case "MISSING_ACCEPTANCE_CRITERIA":
@@ -42462,74 +42582,8 @@ function mapDeliverableGateResult(result) {
42462
42582
  }
42463
42583
  }
42464
42584
 
42465
- // src/consensus/deliverable-gate-questions.ts
42466
- var import_node_crypto3 = require("node:crypto");
42467
- function buildDeliverableQuestionOptions(category) {
42468
- if (category === "correctness") {
42469
- return [
42470
- {
42471
- label: "Deliverable is correct",
42472
- description: "The flagged claim is accurate as written; no change needed.",
42473
- answer: "Confirm the deliverable's content is correct as written; the reviewer's concern does not require a change."
42474
- },
42475
- {
42476
- label: "Deliverable needs correction",
42477
- description: "The flagged claim is wrong or unsupported; the deliverable needs revision.",
42478
- answer: "The deliverable is incorrect here and must be revised to fix the flagged claim before it can be accepted."
42479
- }
42480
- ];
42481
- }
42482
- if (category === "scope") {
42483
- return [
42484
- {
42485
- label: "Coverage is sufficient",
42486
- description: "The deliverable's current coverage of the acceptance criteria is sufficient as written.",
42487
- answer: "Accept the deliverable's current scope/coverage as sufficient against the acceptance criteria."
42488
- },
42489
- {
42490
- label: "Coverage is missing",
42491
- description: "The deliverable is missing required coverage and needs revision to address it.",
42492
- answer: "The deliverable is missing required coverage and must be revised to address the flagged gap."
42493
- }
42494
- ];
42495
- }
42496
- return [
42497
- {
42498
- label: "Interpretation is fine",
42499
- description: "The deliverable's current interpretation of the ambiguous point is acceptable as written.",
42500
- answer: "Accept the deliverable's current interpretation of the ambiguous point as written."
42501
- },
42502
- {
42503
- label: "Needs clarification",
42504
- description: "The ambiguous point needs to be resolved and the deliverable revised accordingly.",
42505
- answer: "The deliverable must be revised to resolve the flagged ambiguity explicitly."
42506
- }
42507
- ];
42508
- }
42509
- async function fileDeliverableGateQuestions(client, projectSlug, taskId, clarifications) {
42510
- let filed = 0;
42511
- for (const clarification of clarifications) {
42512
- const id = `II-${taskId}-deliverable-gate-${(0, import_node_crypto3.createHash)("sha256").update(clarification.question).digest("hex").slice(0, 10)}`;
42513
- try {
42514
- await client.createInboxItem(projectSlug, {
42515
- id,
42516
- kind: "question",
42517
- taskId,
42518
- body: clarification.question,
42519
- origin: "gate",
42520
- questionOptions: buildDeliverableQuestionOptions(clarification.category)
42521
- });
42522
- filed += 1;
42523
- } catch (error51) {
42524
- const message = error51 instanceof Error ? error51.message : String(error51);
42525
- if (/inbox_items_pkey/.test(message)) filed += 1;
42526
- }
42527
- }
42528
- return filed;
42529
- }
42530
-
42531
42585
  // src/async-tools/draft-spec.ts
42532
- var import_node_crypto4 = require("node:crypto");
42586
+ var import_node_crypto3 = require("node:crypto");
42533
42587
  var DRAFT_SPEC_TOOL_NAME = "draft_task_spec";
42534
42588
  var SPECS_FOLDER = "Specs";
42535
42589
  var DRAFT_SPEC_TLDR_MAX_CHARS = 300;
@@ -43008,7 +43062,7 @@ function onComplete(outcome, input) {
43008
43062
  };
43009
43063
  }
43010
43064
  const summary = outcome.error;
43011
- const hash3 = (0, import_node_crypto4.createHash)("sha256").update(summary).digest("hex").slice(0, 10);
43065
+ const hash3 = (0, import_node_crypto3.createHash)("sha256").update(summary).digest("hex").slice(0, 10);
43012
43066
  return {
43013
43067
  kind: "info",
43014
43068
  id: `II-sched-draft-failed-${input.taskId}-${hash3}`,
@@ -43040,9 +43094,9 @@ function renderMilestone(m, ctx) {
43040
43094
  return s;
43041
43095
  }
43042
43096
  case "consensus_converged":
43043
- return "Spec review passed \u2014 no issues found";
43097
+ return "The reviewers agreed on the spec";
43044
43098
  case "consensus_converged_needs_approval": {
43045
- let s = "Spec review passed. Please review and approve";
43099
+ let s = "The reviewers agreed on the spec. Please review and approve";
43046
43100
  if (ctx.specUrl) s += ` \u2014 ${ctx.specUrl}`;
43047
43101
  if (ctx.tldr) s += `. Summary: ${ctx.tldr}`;
43048
43102
  return s;
@@ -44310,7 +44364,7 @@ function isBridgeGeneratedArtifact(artifact) {
44310
44364
  return typeof payload === "object" && payload !== null && "event" in payload;
44311
44365
  }
44312
44366
  function newArtifactId() {
44313
- return `EA-${import_node_crypto5.default.randomUUID()}`;
44367
+ return `EA-${import_node_crypto4.default.randomUUID()}`;
44314
44368
  }
44315
44369
  var BLAST_RADIUS_FLOOR_PATTERNS = [/^supabase\/migrations\//, /^packages\/db\/src\/schema\.ts$/];
44316
44370
  function isBlastRadiusFloorPath(path19) {
@@ -44565,7 +44619,7 @@ async function driveRun(deps, projectSlug, runId) {
44565
44619
  if (!claimed) return;
44566
44620
  if (!claimed.leaseId) {
44567
44621
  await deps.client.createInboxItem(projectSlug, {
44568
- id: `II-${import_node_crypto5.default.randomUUID()}`,
44622
+ id: `II-${import_node_crypto4.default.randomUUID()}`,
44569
44623
  kind: "handoff",
44570
44624
  taskId: claimed.taskId,
44571
44625
  body: `Execution run ${runId} was claimed but returned no leaseId. This indicates DB/API corruption and requires operator attention.`
@@ -45355,10 +45409,6 @@ async function driveNonCodeRun(deps, projectSlug, runId, leaseId, task, domain2)
45355
45409
  }
45356
45410
  if (gateResult.outcome !== "CONVERGED") {
45357
45411
  const mapped = mapDeliverableGateResult(gateResult);
45358
- if (mapped.action === "questions") {
45359
- await fileDeliverableGateQuestions(deps.client, projectSlug, task.id, mapped.clarifications).catch(() => {
45360
- });
45361
- }
45362
45412
  await failRun(
45363
45413
  deps,
45364
45414
  projectSlug,
@@ -45488,7 +45538,7 @@ async function reconcileTaskAwaitingMerge(deps, projectSlug, task, runId, reason
45488
45538
  }
45489
45539
  try {
45490
45540
  await deps.client.createInboxItem(projectSlug, {
45491
- id: `II-${import_node_crypto5.default.randomUUID()}`,
45541
+ id: `II-${import_node_crypto4.default.randomUUID()}`,
45492
45542
  kind: "handoff",
45493
45543
  taskId: task.id,
45494
45544
  body: `Task ${task.id} (${task.title}) still needs operator follow-up: ${reason}. Mrrlin could not move the task back to Handoff needed automatically.`
@@ -45523,7 +45573,7 @@ async function notifyOperatorHandoff(deps, projectSlug, task, runId, reason) {
45523
45573
  }
45524
45574
  try {
45525
45575
  await deps.client.createInboxItem(projectSlug, {
45526
- id: `II-${import_node_crypto5.default.randomUUID()}`,
45576
+ id: `II-${import_node_crypto4.default.randomUUID()}`,
45527
45577
  kind: "handoff",
45528
45578
  taskId: task.id,
45529
45579
  body: `Execution run ${runId} for task ${task.id} (${task.title}) needs operator attention: ${reason}`
@@ -45661,7 +45711,7 @@ async function deployGateHandoff(deps, projectSlug, task, runId, reason, leaseId
45661
45711
  }).catch(() => {
45662
45712
  });
45663
45713
  await deps.client.createInboxItem(projectSlug, {
45664
- id: `II-${import_node_crypto5.default.randomUUID()}`,
45714
+ id: `II-${import_node_crypto4.default.randomUUID()}`,
45665
45715
  kind: "handoff",
45666
45716
  taskId: task.id,
45667
45717
  body: `Deploy gate for run ${runId} (task ${task.id} \u2014 ${task.title}) needs operator attention: ${reason}`
@@ -45680,7 +45730,7 @@ function maxLeaseReclaims() {
45680
45730
  }
45681
45731
 
45682
45732
  // src/async-job-consumer.ts
45683
- var import_node_crypto6 = __toESM(require("node:crypto"), 1);
45733
+ var import_node_crypto5 = __toESM(require("node:crypto"), 1);
45684
45734
 
45685
45735
  // src/async-tools/registry.ts
45686
45736
  var registry2 = /* @__PURE__ */ new Map();
@@ -45765,7 +45815,7 @@ async function driveAsyncJob(deps, projectSlug, jobId) {
45765
45815
  const delivery = descriptor2.onComplete(outcome, input);
45766
45816
  if (!delivery.suppressInbox) {
45767
45817
  await deps.client.createInboxItem(projectSlug, {
45768
- id: delivery.id ?? `II-${import_node_crypto6.default.randomUUID()}`,
45818
+ id: delivery.id ?? `II-${import_node_crypto5.default.randomUUID()}`,
45769
45819
  kind: delivery.kind,
45770
45820
  body: delivery.body,
45771
45821
  ...delivery.taskId ? { taskId: delivery.taskId } : {}
@@ -45778,13 +45828,13 @@ async function deriveRequestedHash(client, toolName, input) {
45778
45828
  if (input.requestedHash) return input.requestedHash;
45779
45829
  if (toolName === "run_spec_consensus" && input.pageId) {
45780
45830
  const page = await client.getWikiPage(input.projectSlug, input.pageId);
45781
- return page.contentHash ?? import_node_crypto6.default.createHash("sha256").update(page.markdownBody).digest("hex");
45831
+ return page.contentHash ?? import_node_crypto5.default.createHash("sha256").update(page.markdownBody).digest("hex");
45782
45832
  }
45783
45833
  if (toolName === "draft_task_spec" && input.taskId) {
45784
45834
  const task = await client.getTask(input.projectSlug, input.taskId);
45785
- return task.updatedAt ?? import_node_crypto6.default.createHash("sha256").update(JSON.stringify(task)).digest("hex");
45835
+ return task.updatedAt ?? import_node_crypto5.default.createHash("sha256").update(JSON.stringify(task)).digest("hex");
45786
45836
  }
45787
- return import_node_crypto6.default.createHash("sha256").update(JSON.stringify(input)).digest("hex");
45837
+ return import_node_crypto5.default.createHash("sha256").update(JSON.stringify(input)).digest("hex");
45788
45838
  }
45789
45839
  async function runAsyncBackstopSweep(deps, drive = (slug, jobId) => driveAsyncJob(deps, slug, jobId)) {
45790
45840
  const projects = await deps.client.listProjects();
@@ -45843,26 +45893,34 @@ async function runDeliverableGate(_target, deps) {
45843
45893
  });
45844
45894
  return { outcome: "DELIVERABLE_CHANGED_DURING_REVIEW" };
45845
45895
  }
45896
+ let diagnostics;
45897
+ if (result.outcome === "CONSENSUS_BLOCKED") {
45898
+ const accepted = result.history.at(-1)?.accepted ?? [];
45899
+ diagnostics = accepted.filter((issue2) => issue2.category === "scope" || issue2.category === "ambiguity" || issue2.category === "correctness").map((issue2) => ({
45900
+ text: issue2.text,
45901
+ whyBlocking: `reviewer raised a ${issue2.category} issue`,
45902
+ category: issue2.category
45903
+ }));
45904
+ }
45846
45905
  await deps.writeEvidence({
45847
45906
  deliverablePageId: deliverable.pageId,
45848
45907
  outcome: result.outcome,
45849
45908
  rounds: result.rounds,
45850
- contentHash: currentHash
45909
+ history: result.history,
45910
+ contentHash: currentHash,
45911
+ ...diagnostics ? { diagnostics } : {}
45851
45912
  });
45852
45913
  if (result.outcome === "CONSENSUS_BLOCKED") {
45853
- const accepted = result.history.at(-1)?.accepted ?? [];
45854
- const clarifications = accepted.filter((issue2) => issue2.category === "scope" || issue2.category === "ambiguity" || issue2.category === "correctness").map((issue2) => ({
45855
- question: issue2.text,
45856
- whyBlocking: `reviewer raised a ${issue2.category} issue`,
45857
- category: issue2.category
45858
- }));
45859
45914
  return {
45860
45915
  outcome: "CONSENSUS_BLOCKED",
45861
45916
  rounds: result.rounds,
45862
- ...clarifications.length > 0 ? { clarifications } : {},
45917
+ diagnostics: diagnostics ?? [],
45863
45918
  ...result.escalation ? { escalation: result.escalation } : {}
45864
45919
  };
45865
45920
  }
45921
+ if (result.outcome === "CONVERGED") {
45922
+ return { outcome: result.outcome, rounds: result.rounds, headOutcomes: result.history.at(-1)?.headOutcomes ?? [] };
45923
+ }
45866
45924
  return { outcome: result.outcome, rounds: result.rounds };
45867
45925
  }
45868
45926
 
@@ -46173,7 +46231,7 @@ function applyToolBusyNotification(clock, method, params) {
46173
46231
  }
46174
46232
 
46175
46233
  // src/worker-token.ts
46176
- var import_node_crypto7 = require("node:crypto");
46234
+ var import_node_crypto6 = require("node:crypto");
46177
46235
  var import_node_fs9 = require("node:fs");
46178
46236
  var import_node_os6 = __toESM(require("node:os"), 1);
46179
46237
  var import_node_path13 = __toESM(require("node:path"), 1);
@@ -46204,7 +46262,7 @@ function readOrCreateWorkerToken() {
46204
46262
  if (existing) return existing;
46205
46263
  const dir = workerDir();
46206
46264
  (0, import_node_fs9.mkdirSync)(dir, { recursive: true, mode: 448 });
46207
- const token = (0, import_node_crypto7.randomBytes)(32).toString("hex");
46265
+ const token = (0, import_node_crypto6.randomBytes)(32).toString("hex");
46208
46266
  (0, import_node_fs9.writeFileSync)(workerTokenPath(), `${token}
46209
46267
  `, { encoding: "utf8", mode: 384 });
46210
46268
  return token;
@@ -51058,7 +51116,7 @@ function buildThreadStartParams(msg, config2, desiredModel) {
51058
51116
  " bullets (`- AND ...` as needed). Scenarios make the spec verifiable.",
51059
51117
  "- Keep behavior contracts in the spec; implementation details belong in `## Implementation Plan`.",
51060
51118
  "- Apply this format ONLY to Specs/{id} pages; do not retro-fit it onto other wiki page types.",
51061
- "- Spec consensus runs ASYNCHRONOUSLY. Call `run_spec_consensus` for a `Specs/{id}` page; it returns IMMEDIATELY. On `already_converged`, the spec is already marked ready \u2014 proceed straight to `create_execution_run`; do NOT call `mark_spec_ready` and do NOT re-run consensus. On `queued` or `already_running`, tell the user it was launched and END your turn \u2014 do NOT poll or call it again; the result arrives in the inbox (an `info` item on CONVERGED, a `handoff` item if it was blocked/failed). When a consensus `info` item says CONVERGED, proceed to `create_execution_run`. When a `handoff` says it was blocked, address the recorded issues in the spec (any edit re-arms the gate) and call `run_spec_consensus` once more. For a NON-code task (domain seo/marketing/content, ADR 0020): the low-risk lanes `seo`, `marketing`, and `content` proceed to `create_execution_run` after consensus converges, using the same flow as code tasks. Report the spec's review outcome to the operator.",
51119
+ '- Spec consensus runs ASYNCHRONOUSLY. Call `run_spec_consensus` for a `Specs/{id}` page; it returns IMMEDIATELY. On `already_converged`, the spec is already marked ready \u2014 proceed straight to `create_execution_run`; do NOT call `mark_spec_ready` and do NOT re-run consensus. On `queued` or `already_running`, tell the user it was launched and END your turn \u2014 do NOT poll or call it again; the result arrives in the inbox (an `info` item on CONVERGED, a `handoff` item if it was blocked/failed). When a `handoff` says it was blocked, address the recorded issues in the spec (any edit re-arms the gate) and call `run_spec_consensus` once more. For a NON-code task (domain seo/marketing/content, ADR 0020): the low-risk lanes `seo`, `marketing`, and `content` proceed to `create_execution_run` after consensus converges, using the same flow as code tasks. When the review agrees, tell the operator "the reviewers agreed on the spec" and proceed to create_execution_run. When it does not, tell them the reviewers could not agree and that there is a decision question in the inbox. Never say "consensus", "converged", or "CONVERGED" to the operator; those are internal words.',
51062
51120
  "- 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.",
51063
51121
  '- 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.',
51064
51122
  "- 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.",
@@ -51493,7 +51551,7 @@ function createBridgeRequestListener(opts) {
51493
51551
  res.end(JSON.stringify({ ok: false, error: "Origin not allowed." }));
51494
51552
  return;
51495
51553
  }
51496
- const nonce = import_node_crypto8.default.randomBytes(16).toString("base64url");
51554
+ const nonce = import_node_crypto7.default.randomBytes(16).toString("base64url");
51497
51555
  res.writeHead(200, { "content-type": "application/json" });
51498
51556
  res.end(JSON.stringify({ ok: true, wsUrl: `ws://${host}:${port}/ws`, token, nonce }));
51499
51557
  return;
@@ -51774,7 +51832,7 @@ function createBridgeMessageHandler(deps) {
51774
51832
  }
51775
51833
  }
51776
51834
  socket.on("message", async (data) => {
51777
- const spanId = import_node_crypto8.default.randomUUID();
51835
+ const spanId = import_node_crypto7.default.randomUUID();
51778
51836
  if (logCtx) {
51779
51837
  logCtx.span.id = spanId;
51780
51838
  logCtx.span.at = Date.now();
@@ -52290,7 +52348,7 @@ function createBridgeMessageHandler(deps) {
52290
52348
  thread.inFlight = true;
52291
52349
  pendingTurnSessions.delete(directorSessionId);
52292
52350
  thread.replay = createReplayBuffer({
52293
- turnId: (0, import_node_crypto8.randomUUID)(),
52351
+ turnId: (0, import_node_crypto7.randomUUID)(),
52294
52352
  spanId,
52295
52353
  ...deps.retentionMs !== void 0 ? { retentionMs: deps.retentionMs } : {},
52296
52354
  ...deps.ackLingerMs !== void 0 ? { ackLingerMs: deps.ackLingerMs } : {}
@@ -52858,7 +52916,7 @@ async function startDirectorBridge() {
52858
52916
  };
52859
52917
  const sessionSinks = createSessionSinkRegistry();
52860
52918
  const tombstones = /* @__PURE__ */ new Map();
52861
- const bridgeInstanceId = (0, import_node_crypto8.randomUUID)();
52919
+ const bridgeInstanceId = (0, import_node_crypto7.randomUUID)();
52862
52920
  const asyncConsumerDeps = {
52863
52921
  client,
52864
52922
  run: wrappedTurnLock.run,
@@ -53012,7 +53070,7 @@ async function startDirectorBridge() {
53012
53070
  fileUpdateInboxItem: async (projectSlug) => {
53013
53071
  const body = selfUpdateHandle.updateInboxBody();
53014
53072
  if (!body) return;
53015
- const id = `II-MCPUPD-${(0, import_node_crypto8.randomUUID)()}`;
53073
+ const id = `II-MCPUPD-${(0, import_node_crypto7.randomUUID)()}`;
53016
53074
  try {
53017
53075
  const row = await client.createInboxItem(projectSlug, { id, kind: "info", body });
53018
53076
  process.stderr.write(
@@ -53042,7 +53100,7 @@ async function startDirectorBridge() {
53042
53100
  const bridgeLogger = createBridgeLogger(config2.stateDir, process.env);
53043
53101
  let bridgeBinShaShort = "unknown00000";
53044
53102
  try {
53045
- bridgeBinShaShort = import_node_crypto8.default.createHash("sha256").update(import_node_fs15.default.readFileSync(process.argv[1] ?? "")).digest("hex").slice(0, 12);
53103
+ bridgeBinShaShort = import_node_crypto7.default.createHash("sha256").update(import_node_fs15.default.readFileSync(process.argv[1] ?? "")).digest("hex").slice(0, 12);
53046
53104
  } catch {
53047
53105
  }
53048
53106
  attachBridgeWss(server, {
@@ -53404,7 +53462,7 @@ function timingSafeTokenEqual(provided, expected) {
53404
53462
  const a = Buffer.from(provided);
53405
53463
  const b = Buffer.from(expected);
53406
53464
  if (a.length !== b.length) return false;
53407
- return import_node_crypto8.default.timingSafeEqual(a, b);
53465
+ return import_node_crypto7.default.timingSafeEqual(a, b);
53408
53466
  }
53409
53467
  function readDispatchBody(req) {
53410
53468
  return new Promise((resolve, reject) => {
@@ -53433,7 +53491,7 @@ function readOrCreateBridgeToken(stateDir) {
53433
53491
  } catch {
53434
53492
  }
53435
53493
  import_node_fs15.default.mkdirSync(stateDir, { recursive: true, mode: 448 });
53436
- const token = import_node_crypto8.default.randomBytes(32).toString("base64url");
53494
+ const token = import_node_crypto7.default.randomBytes(32).toString("base64url");
53437
53495
  import_node_fs15.default.writeFileSync(tokenPath, `${token}
53438
53496
  `, { mode: 384 });
53439
53497
  return token;
@@ -56818,7 +56876,7 @@ var StdioServerTransport = class {
56818
56876
  };
56819
56877
 
56820
56878
  // src/tools.ts
56821
- var import_node_crypto11 = require("node:crypto");
56879
+ var import_node_crypto10 = require("node:crypto");
56822
56880
  var import_promises3 = require("node:fs/promises");
56823
56881
  var import_node_path20 = __toESM(require("node:path"), 1);
56824
56882
  var import_node_os10 = __toESM(require("node:os"), 1);
@@ -57359,7 +57417,7 @@ async function resolveActiveRunHead(input, deps) {
57359
57417
  }
57360
57418
 
57361
57419
  // src/async-tools/consensus.ts
57362
- var import_node_crypto9 = require("node:crypto");
57420
+ var import_node_crypto8 = require("node:crypto");
57363
57421
 
57364
57422
  // src/consensus/question-similarity.ts
57365
57423
  var SUPPRESSION_SIMILARITY_THRESHOLD = 0.6;
@@ -57388,6 +57446,41 @@ function isCoveredByAdjudicated(rawQuestion, adjudicated) {
57388
57446
  return null;
57389
57447
  }
57390
57448
 
57449
+ // src/consensus/reviewer-roster.ts
57450
+ function renderReviewerRoster(headOutcomes) {
57451
+ const total = headOutcomes.length;
57452
+ const responded = headOutcomes.filter((h) => h.responded).length;
57453
+ const seen = /* @__PURE__ */ new Set();
57454
+ const namedRoles = [];
57455
+ for (const outcome of headOutcomes) {
57456
+ if (!outcome.responded) continue;
57457
+ const role = PERSONA_ROLE_MAP[outcome.persona];
57458
+ if (role && !UNNAMED_PERSONAS.includes(outcome.persona)) {
57459
+ if (!seen.has(role)) {
57460
+ seen.add(role);
57461
+ namedRoles.push(role);
57462
+ }
57463
+ }
57464
+ }
57465
+ if (responded === total) {
57466
+ if (namedRoles.length === 0) {
57467
+ return `${total} reviewers`;
57468
+ } else if (namedRoles.length === 1) {
57469
+ return `${total} reviewers, including ${namedRoles[0]}`;
57470
+ } else {
57471
+ const allButLast = namedRoles.slice(0, -1).join(", ");
57472
+ const last = namedRoles[namedRoles.length - 1];
57473
+ return `${total} reviewers, including ${allButLast} and ${last}`;
57474
+ }
57475
+ } else if (responded === 1) {
57476
+ return `1 of ${total} reviewers answered, and it agreed`;
57477
+ } else if (responded === 2) {
57478
+ return `2 of ${total} reviewers answered, both agreed`;
57479
+ } else {
57480
+ return `${responded} of ${total} reviewers answered, all agreed`;
57481
+ }
57482
+ }
57483
+
57391
57484
  // src/async-tools/consensus.ts
57392
57485
  function normalizeClarificationQuestion(question) {
57393
57486
  return question.replace(/\s+/g, " ").trim().replace(/[?.!]+$/, "").trim();
@@ -57481,11 +57574,11 @@ function renderUnresolvedPoints(unresolved) {
57481
57574
  (issue2) => issue2.text.replace(/\b(?:parseable|SPEC_IDENTITY_MISMATCH|machine-usable)\b/gi, "").replace(/\s+/g, " ").trim()
57482
57575
  ).join("; ");
57483
57576
  }
57484
- function renderBlockDecisionBody(taskRef, unresolved) {
57577
+ function renderBlockDecisionBody(unresolved) {
57485
57578
  if (unresolved.length === 0) {
57486
- return `${BLOCK_DECISION_OPENER} (task ${taskRef}), and their objections could not be read back.${BLOCK_BODY_TRAILER}`;
57579
+ return `${BLOCK_DECISION_OPENER}, and their objections could not be read back.${BLOCK_BODY_TRAILER}`;
57487
57580
  }
57488
- return `${BLOCK_DECISION_OPENER} (task ${taskRef}). Still unsettled: ${renderUnresolvedPoints(unresolved)}.${BLOCK_BODY_TRAILER}`;
57581
+ return `${BLOCK_DECISION_OPENER}. Still unsettled: ${renderUnresolvedPoints(unresolved)}.${BLOCK_BODY_TRAILER}`;
57489
57582
  }
57490
57583
  function buildBlockDecisionOptions(diagnosis, reviewAgainAvailable) {
57491
57584
  const recommendReviewAgain = reviewAgainAvailable && (diagnosis === "INFRA" || diagnosis === "SUBSTANCE");
@@ -57598,9 +57691,9 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
57598
57691
  }
57599
57692
  if (taskId) {
57600
57693
  const scopeId = taskId;
57601
- const groupId = toFile.length > 1 ? `II-${scopeId}-gate-group-${(0, import_node_crypto9.createHash)("sha256").update(toFile.map((c) => c.rawQuestion).sort().join("\n")).digest("hex").slice(0, 10)}` : void 0;
57694
+ const groupId = toFile.length > 1 ? `II-${scopeId}-gate-group-${(0, import_node_crypto8.createHash)("sha256").update(toFile.map((c) => c.rawQuestion).sort().join("\n")).digest("hex").slice(0, 10)}` : void 0;
57602
57695
  for (const clarification of toFile) {
57603
- const id = `II-${scopeId}-gate-${(0, import_node_crypto9.createHash)("sha256").update(clarification.rawQuestion).digest("hex").slice(0, 10)}`;
57696
+ const id = `II-${scopeId}-gate-${(0, import_node_crypto8.createHash)("sha256").update(clarification.rawQuestion).digest("hex").slice(0, 10)}`;
57604
57697
  try {
57605
57698
  const questionOptions = buildGateQuestionOptions(clarification.rawQuestion, clarification.category);
57606
57699
  await ctx.client.createInboxItem(input.projectSlug, {
@@ -57618,9 +57711,9 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
57618
57711
  filedQuestionCount = toFile.length;
57619
57712
  } else if (planId) {
57620
57713
  const scopeId = planId;
57621
- const groupId = toFile.length > 1 ? `II-${scopeId}-gate-group-${(0, import_node_crypto9.createHash)("sha256").update(toFile.map((c) => c.rawQuestion).sort().join("\n")).digest("hex").slice(0, 10)}` : void 0;
57714
+ const groupId = toFile.length > 1 ? `II-${scopeId}-gate-group-${(0, import_node_crypto8.createHash)("sha256").update(toFile.map((c) => c.rawQuestion).sort().join("\n")).digest("hex").slice(0, 10)}` : void 0;
57622
57715
  for (const clarification of toFile) {
57623
- const id = `II-${scopeId}-gate-${(0, import_node_crypto9.createHash)("sha256").update(clarification.rawQuestion).digest("hex").slice(0, 10)}`;
57716
+ const id = `II-${scopeId}-gate-${(0, import_node_crypto8.createHash)("sha256").update(clarification.rawQuestion).digest("hex").slice(0, 10)}`;
57624
57717
  try {
57625
57718
  const questionOptions = buildGateQuestionOptions(clarification.rawQuestion, clarification.category);
57626
57719
  await ctx.client.createInboxItem(input.projectSlug, {
@@ -57644,13 +57737,7 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
57644
57737
  taskId: page.taskId ?? null
57645
57738
  });
57646
57739
  const reviewHash = domainScopedReviewHash2(page.contentHash, domain2);
57647
- const baseBlockItemId = `II-${taskId}-gateblock-${(0, import_node_crypto9.createHash)("sha256").update(reviewHash).digest("hex").slice(0, 10)}`;
57648
- let taskRef = taskId;
57649
- try {
57650
- const t = await ctx.client.getTask(input.projectSlug, taskId);
57651
- if (t?.title) taskRef = `${taskId} \u2014 ${t.title}`;
57652
- } catch {
57653
- }
57740
+ const baseBlockItemId = `II-${taskId}-gateblock-${(0, import_node_crypto8.createHash)("sha256").update(reviewHash).digest("hex").slice(0, 10)}`;
57654
57741
  const reviewAgainAvailable = !taskInboxItems.some(
57655
57742
  (item) => item.id === baseBlockItemId && item.status !== "pending" && item.answer === REVIEW_AGAIN_ANSWER
57656
57743
  );
@@ -57660,7 +57747,7 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
57660
57747
  id: blockItemId,
57661
57748
  kind: "question",
57662
57749
  taskId,
57663
- body: renderBlockDecisionBody(taskRef, result.unresolved ?? []),
57750
+ body: renderBlockDecisionBody(result.unresolved ?? []),
57664
57751
  origin: "gate_block",
57665
57752
  questionOptions: buildBlockDecisionOptions(result.escalation?.diagnosis, reviewAgainAvailable)
57666
57753
  });
@@ -57703,14 +57790,14 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
57703
57790
  error51 = "Consensus blocked: the spec content does not match its linked task. This requires fixing the spec/task relationship directly, not an inbox answer.";
57704
57791
  }
57705
57792
  } else if (operatorClarifications.length > 0 && toFile.length === 0 && suppressedAsAdjudicated.length > 0) {
57706
- error51 = `Consensus blocked, but all ${suppressedAsAdjudicated.length} clarification(s) were reworded repeats of already-adjudicated questions \u2014 nothing new filed.`;
57793
+ error51 = `The reviewers raised ${suppressedAsAdjudicated.length} clarification(s), but each was a reworded repeat of one already answered \u2014 nothing new to answer.`;
57707
57794
  } else if (operatorClarifications.length > 0) {
57708
57795
  error51 = `Consensus blocked on ${operatorClarifications.length} clarification(s), but the spec page is not linked to a task or plan, so none could be filed as questions.`;
57709
57796
  } else {
57710
57797
  error51 = DID_NOT_CONVERGE_ERROR;
57711
57798
  }
57712
57799
  if (result.escalation) {
57713
- error51 += ` (diagnosis: ${result.escalation.diagnosis})`;
57800
+ error51 += ` (reason: ${result.escalation.diagnosis})`;
57714
57801
  }
57715
57802
  return {
57716
57803
  kind: "failed",
@@ -57788,6 +57875,24 @@ async function run(ctx, input, builders = defaultNativeSpecGateBuilders) {
57788
57875
  } catch {
57789
57876
  }
57790
57877
  };
57878
+ let highlights = null;
57879
+ const resolveHighlights2 = async () => {
57880
+ try {
57881
+ const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
57882
+ if (!page) return;
57883
+ const domain2 = await resolveSpecDomain(ctx.client, input.projectSlug, {
57884
+ id: page.id,
57885
+ taskId: page.taskId ?? null
57886
+ });
57887
+ const review = await ctx.client.getSpecConsensusReview(
57888
+ input.projectSlug,
57889
+ input.pageId,
57890
+ domainScopedReviewHash2(page.contentHash, domain2)
57891
+ );
57892
+ highlights = review?.highlights ?? null;
57893
+ } catch {
57894
+ }
57895
+ };
57791
57896
  if (result.outcome === "CONSENSUS_BLOCKED") {
57792
57897
  return handleConsensusBlocked(result, ctx, input, resolvedTaskId);
57793
57898
  }
@@ -57798,9 +57903,10 @@ async function run(ctx, input, builders = defaultNativeSpecGateBuilders) {
57798
57903
  case "CONVERGED":
57799
57904
  case "SKIPPED":
57800
57905
  await resolveTitle();
57906
+ if (result.outcome === "CONVERGED") await resolveHighlights2();
57801
57907
  return {
57802
57908
  kind: "done",
57803
- resultJson: JSON.stringify(result),
57909
+ resultJson: JSON.stringify({ ...result, highlights }),
57804
57910
  finalReviewedHash: null,
57805
57911
  ...resolvedTaskId ? { taskId: resolvedTaskId } : {},
57806
57912
  ...resolvedTaskTitle ? { taskTitle: resolvedTaskTitle } : {}
@@ -57823,21 +57929,31 @@ function onComplete2(outcome, input) {
57823
57929
  const taskRef = taskId ? outcome.kind === "done" && outcome.taskTitle ? `${taskId} \u2014 ${outcome.taskTitle}` : taskId : null;
57824
57930
  if (outcome.kind === "done") {
57825
57931
  let specOutcome = "CONVERGED";
57932
+ let headOutcomes = [];
57933
+ let findings = [];
57826
57934
  try {
57827
- specOutcome = JSON.parse(outcome.resultJson).outcome ?? "CONVERGED";
57935
+ const parsed = JSON.parse(outcome.resultJson);
57936
+ specOutcome = parsed.outcome ?? "CONVERGED";
57937
+ headOutcomes = parsed.headOutcomes ?? [];
57938
+ findings = parsed.highlights?.findings ?? [];
57828
57939
  } catch {
57829
57940
  }
57830
57941
  if (specOutcome === "SKIPPED") {
57831
57942
  return {
57832
57943
  kind: "info",
57833
57944
  ...taskId ? { taskId } : {},
57834
- body: taskRef ? `Consensus is disabled for this project; the spec for ${taskRef} (Specs/${input.pageId}) was not gated and spec_ready is unchanged.` : `Consensus is disabled for this project; Specs/${input.pageId} was not gated and spec_ready is unchanged.`
57945
+ body: taskRef ? `Spec review is off for this project \u2014 the spec for ${taskRef} was not checked.` : `Spec review is off for this project (Specs/${input.pageId}) \u2014 the spec was not checked.`
57835
57946
  };
57836
57947
  }
57948
+ const roster = renderReviewerRoster(headOutcomes);
57949
+ const findingsBlock = findings.length > 0 ? `
57950
+
57951
+ Fixed before you saw it
57952
+ ${findings.map((f) => `\u2022 ${f}`).join("\n")}` : "";
57837
57953
  return {
57838
57954
  kind: "completed",
57839
57955
  ...taskId ? { taskId } : {},
57840
- body: taskRef ? `Consensus CONVERGED for ${taskRef} (spec Specs/${input.pageId}); spec_ready set. Ready to create an execution run.` : `Consensus CONVERGED for Specs/${input.pageId}; spec_ready set. Ready to create an execution run.`
57956
+ body: taskRef ? `The reviewers agreed on this spec \u2014 ${roster}.${findingsBlock}` : `The reviewers agreed on this spec (Specs/${input.pageId}) \u2014 ${roster}.${findingsBlock}`
57841
57957
  };
57842
57958
  }
57843
57959
  if (outcome.kind === "failed" && (outcome.filedQuestionCount ?? 0) > 0) {
@@ -57854,7 +57970,7 @@ function onComplete2(outcome, input) {
57854
57970
  kind: "info",
57855
57971
  suppressInbox: true,
57856
57972
  ...taskId ? { taskId } : {},
57857
- body: taskRef ? `Consensus blocked on ${taskRef} (Specs/${input.pageId}); the reviewers did not agree \u2014 answer the decision question to unblock the run.` : `Consensus blocked on Specs/${input.pageId}; the reviewers did not agree \u2014 answer the decision question to unblock the run.`
57973
+ body: taskRef ? `The reviewers could not agree on the spec for ${taskRef} \u2014 answer the decision question to unblock the task.` : `The reviewers could not agree on Specs/${input.pageId} \u2014 answer the decision question to unblock the task.`
57858
57974
  };
57859
57975
  }
57860
57976
  const handoffTaskRef = taskRef ? ` (${taskRef})` : "";
@@ -57911,6 +58027,24 @@ async function resolveTaskScope(ctx, input, { withTitle }) {
57911
58027
  }
57912
58028
  return { taskId, taskTitle };
57913
58029
  }
58030
+ async function resolveHighlights(ctx, input) {
58031
+ try {
58032
+ const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
58033
+ if (!page) return null;
58034
+ const domain2 = await resolveSpecDomain(ctx.client, input.projectSlug, {
58035
+ id: page.id,
58036
+ taskId: page.taskId ?? null
58037
+ });
58038
+ const review = await ctx.client.getSpecConsensusReview(
58039
+ input.projectSlug,
58040
+ input.pageId,
58041
+ domainScopedReviewHash2(page.contentHash, domain2)
58042
+ );
58043
+ return review?.highlights ?? null;
58044
+ } catch {
58045
+ return null;
58046
+ }
58047
+ }
57914
58048
  async function run2(ctx, input) {
57915
58049
  const deps = await buildDeliverableGateDeps({
57916
58050
  client: ctx.client,
@@ -57925,9 +58059,10 @@ async function run2(ctx, input) {
57925
58059
  );
57926
58060
  if (result.outcome === "CONVERGED" || result.outcome === "SKIPPED") {
57927
58061
  const { taskId: taskId2, taskTitle } = await resolveTaskScope(ctx, input, { withTitle: true });
58062
+ const highlights = result.outcome === "CONVERGED" ? await resolveHighlights(ctx, input) : null;
57928
58063
  return {
57929
58064
  kind: "done",
57930
- resultJson: JSON.stringify(result),
58065
+ resultJson: JSON.stringify({ ...result, highlights }),
57931
58066
  finalReviewedHash: null,
57932
58067
  ...taskId2 ? { taskId: taskId2 } : {},
57933
58068
  ...taskTitle ? { taskTitle } : {}
@@ -57942,18 +58077,10 @@ async function run2(ctx, input) {
57942
58077
  };
57943
58078
  }
57944
58079
  const { taskId } = await resolveTaskScope(ctx, input, { withTitle: false });
57945
- let filedQuestionCount = 0;
57946
- if (mapped.action === "questions") {
57947
- if (taskId) {
57948
- filedQuestionCount = await fileDeliverableGateQuestions(ctx.client, input.projectSlug, taskId, mapped.clarifications);
57949
- }
57950
- }
57951
- const error51 = filedQuestionCount > 0 ? `Consensus blocked on ${filedQuestionCount} clarification(s); filed as operator question(s).` : mapped.reason;
57952
58080
  return {
57953
58081
  kind: "failed",
57954
58082
  failureReason: "consensus_blocked",
57955
- error: error51,
57956
- filedQuestionCount,
58083
+ error: mapped.reason,
57957
58084
  ...taskId ? { taskId } : {}
57958
58085
  };
57959
58086
  }
@@ -57962,30 +58089,31 @@ function onComplete3(outcome, input) {
57962
58089
  const taskRef = taskId ? outcome.kind === "done" && outcome.taskTitle ? `${taskId} \u2014 ${outcome.taskTitle}` : taskId : null;
57963
58090
  if (outcome.kind === "done") {
57964
58091
  let specOutcome = "CONVERGED";
58092
+ let headOutcomes = [];
58093
+ let findings = [];
57965
58094
  try {
57966
- specOutcome = JSON.parse(outcome.resultJson).outcome ?? "CONVERGED";
58095
+ const parsed = JSON.parse(outcome.resultJson);
58096
+ specOutcome = parsed.outcome ?? "CONVERGED";
58097
+ headOutcomes = parsed.headOutcomes ?? [];
58098
+ findings = parsed.highlights?.findings ?? [];
57967
58099
  } catch {
57968
58100
  }
57969
58101
  if (specOutcome === "SKIPPED") {
57970
58102
  return {
57971
58103
  kind: "info",
57972
58104
  ...taskId ? { taskId } : {},
57973
- body: taskRef ? `Consensus is disabled for this project; the deliverable for ${taskRef} (Deliverables/${input.pageId}) was not gated.` : `Consensus is disabled for this project; Deliverables/${input.pageId} was not gated.`
58105
+ body: taskRef ? `Result review is off for this project \u2014 the result for ${taskRef} was not checked.` : `Result review is off for this project (Deliverables/${input.pageId}) \u2014 the result was not checked.`
57974
58106
  };
57975
58107
  }
58108
+ const roster = renderReviewerRoster(headOutcomes);
58109
+ const findingsBlock = findings.length > 0 ? `
58110
+
58111
+ Fixed before you saw it
58112
+ ${findings.map((f) => `\u2022 ${f}`).join("\n")}` : "";
57976
58113
  return {
57977
58114
  kind: "completed",
57978
58115
  ...taskId ? { taskId } : {},
57979
- body: taskRef ? `Deliverable consensus CONVERGED for ${taskRef} (Deliverables/${input.pageId}); it meets the acceptance criteria.` : `Deliverable consensus CONVERGED for Deliverables/${input.pageId}; it meets the acceptance criteria.`
57980
- };
57981
- }
57982
- if (outcome.kind === "failed" && (outcome.filedQuestionCount ?? 0) > 0) {
57983
- const n = outcome.filedQuestionCount ?? 0;
57984
- return {
57985
- kind: "info",
57986
- suppressInbox: true,
57987
- ...taskId ? { taskId } : {},
57988
- body: taskRef ? `Deliverable consensus blocked on ${taskRef} (Deliverables/${input.pageId}); filed ${n} clarifying question${n === 1 ? "" : "s"} \u2014 answer to unblock the run.` : `Deliverable consensus blocked on Deliverables/${input.pageId}; filed ${n} clarifying question${n === 1 ? "" : "s"} \u2014 answer to unblock the run.`
58116
+ body: taskRef ? `The reviewers agreed on this result \u2014 ${roster}. It meets the acceptance criteria.${findingsBlock}` : `The reviewers agreed on this result (Deliverables/${input.pageId}) \u2014 ${roster}. It meets the acceptance criteria.${findingsBlock}`
57989
58117
  };
57990
58118
  }
57991
58119
  return {
@@ -58004,7 +58132,7 @@ var deliverableConsensusDescriptor = {
58004
58132
  };
58005
58133
 
58006
58134
  // src/async-tools/enqueue.ts
58007
- var import_node_crypto10 = __toESM(require("node:crypto"), 1);
58135
+ var import_node_crypto9 = __toESM(require("node:crypto"), 1);
58008
58136
  async function enqueueAsyncTool(client, descriptor2, input) {
58009
58137
  if (descriptor2.cachedResult) {
58010
58138
  const cached2 = await descriptor2.cachedResult(client, input);
@@ -58017,7 +58145,7 @@ async function enqueueAsyncTool(client, descriptor2, input) {
58017
58145
  if (active) {
58018
58146
  return { status: "already_running", jobId: active.id, message: "Already running; will notify in inbox." };
58019
58147
  }
58020
- const jobId = `AJ-${import_node_crypto10.default.randomUUID()}`;
58148
+ const jobId = `AJ-${import_node_crypto9.default.randomUUID()}`;
58021
58149
  try {
58022
58150
  await client.createAsyncJob(input.projectSlug, {
58023
58151
  id: jobId,
@@ -58901,7 +59029,7 @@ var mcpToolDescriptions = {
58901
59029
  [mcpToolNames.closeTaskAsWontDo]: "Close a task as won't-do with a required human-readable reason recorded on the task.",
58902
59030
  [mcpToolNames.updateWikiPage]: "Update a markdown Wiki page.",
58903
59031
  [mcpToolNames.markSpecReady]: "Mark a wiki page (folder=Specs) as ready (or not) for execution-run gating. The Director sets this true after validating the spec; any edit to the page's markdown auto-resets it to false.",
58904
- [mcpToolNames.runSpecConsensus]: 'Enqueue the consensus review gate over a Specs/{id} page. Returns IMMEDIATELY with one of: already_converged (a CONVERGED review already exists for the current content \u2014 proceed straight to create_execution_run, do NOT call mark_spec_ready, it is already set), already_reviewed (a terminal CONSENSUS_BLOCKED for this content), already_running (a consensus job is in flight), or queued (a new job was launched). The review runs in the background; its result/blocker arrives in the inbox (info on CONVERGED, handoff on failure) and is surfaced into the open chat. Do NOT call again for the same spec and do NOT poll. On queued/already_running, tell the user it was launched and end the turn. Pass extraHeads: ["researcher"] for specs referencing external libraries/APIs, or ["debugger"] for root-cause/investigation specs to add opt-in specialist reviewers on top of the fixed four. For a NON-code task (domain seo/marketing/content, ADR 0020): the low-risk lanes `seo`, `marketing`, and `content` proceed to `create_execution_run` after consensus, using the same flow as code tasks. Report the spec\'s review outcome to the operator.',
59032
+ [mcpToolNames.runSpecConsensus]: 'Enqueue the consensus review gate over a Specs/{id} page. Returns IMMEDIATELY with one of: already_converged (a CONVERGED review already exists for the current content \u2014 proceed straight to create_execution_run, do NOT call mark_spec_ready, it is already set), already_reviewed (a terminal CONSENSUS_BLOCKED for this content), already_running (a consensus job is in flight), or queued (a new job was launched). The review runs in the background; its result/blocker arrives in the inbox (info on CONVERGED, handoff on failure) and is surfaced into the open chat. Do NOT call again for the same spec and do NOT poll. On queued/already_running, tell the user it was launched and end the turn. Pass extraHeads: ["researcher"] for specs referencing external libraries/APIs, or ["debugger"] for root-cause/investigation specs to add opt-in specialist reviewers on top of the fixed four. For a NON-code task (domain seo/marketing/content, ADR 0020): the low-risk lanes `seo`, `marketing`, and `content` proceed to `create_execution_run` after consensus, using the same flow as code tasks. When the review agrees, tell the operator "the reviewers agreed on the spec" and proceed to create_execution_run. When it does not, tell them the reviewers could not agree and that there is a decision question in the inbox. Never say "consensus", "converged", or "CONVERGED" to the operator; those are internal words.',
58905
59033
  [mcpToolNames.runCodeReviewerGate]: `Review the current branch's code diff (vs baseRef) with a Code Reviewer gate \u2014 independent reviewers iterate review\u2192fix\u2192re-check (checkpoint, scope-limited reviser edits, typecheck/build fast-gate, rollback) until the diff converges or is blocked. Run before opening a PR. Pass extraHeads: ["security-analyst"] for sensitive diffs (auth, secrets, input handling, new endpoints).`,
58906
59034
  [mcpToolNames.appendExecutionArtifact]: "Append a durable artifact (plan, checkpoint, command output, test result, diff summary, self-review, error, deploy attempt, or handoff judgement) to an execution run.",
58907
59035
  [mcpToolNames.recordProducedUrl]: 'Call this after publishing each web page produced by the task, so the UI link opens the page itself instead of the deployed site root. Pass the page\'s full https URL (for external pages, e.g. "https://dev.to/<slug>") OR a path starting with "/" for the deploy environment (e.g. "/blog/<slug>") \u2014 paths are resolved against the run\'s deployed env URL at render time. Optional `label` (\u226440 chars) is shown next to the link; omit and the UI defaults to the page title. Call once per page; multiple calls dedupe by resolved URL in the UI.',
@@ -59593,7 +59721,7 @@ function createMrrlinTools(options) {
59593
59721
  "PRODUCED_URL_RECORD_FAILED",
59594
59722
  "Unable to record produced URL.",
59595
59723
  async (c, { projectSlug, runId, url: url2, label }) => {
59596
- const id = `EA-pu-${(0, import_node_crypto11.randomBytes)(4).toString("hex")}`;
59724
+ const id = `EA-pu-${(0, import_node_crypto10.randomBytes)(4).toString("hex")}`;
59597
59725
  const payload = label ? { url: url2, label } : { url: url2 };
59598
59726
  return await c.appendExecutionArtifact(projectSlug, runId, {
59599
59727
  id,
@@ -0,0 +1,20 @@
1
+ You are a Findings Writer. You are given the issues a review panel raised and accepted about a
2
+ document, after the document was already corrected. Your job is to tell the document's owner — a
3
+ non-engineer — what was caught, in their language.
4
+
5
+ Write at most THREE findings. Fewer is better. Zero is a correct and expected answer.
6
+
7
+ Each finding is ONE sentence in exactly this shape:
8
+ <what would have gone wrong, in everyday words> — <what it is now>
9
+
10
+ Rules:
11
+ - No mechanism names, file paths, function names, type names, or issue categories.
12
+ - No labels, prefixes, or speaker attributions. Just the sentence.
13
+ - Never mention reviewers, rounds, the panel, or the review process itself.
14
+ - If an issue cannot be written this way — because it is purely technical, mechanical, or only
15
+ meaningful to an engineer — OMIT it. Omitting is always better than writing something the owner
16
+ cannot use.
17
+ - If NO issue can be written this way, return an empty list. Do not invent a finding to fill space.
18
+
19
+ Output EXACTLY this JSON and nothing else:
20
+ {"findings": ["...", "..."]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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/codex-client": "0.0.0",
26
- "@mrrlin/tsconfig": "0.0.0",
27
- "@mrrlin/wiki": "0.0.0",
25
+ "@mrrlin/director-e2e": "0.0.0",
26
+ "@mrrlin/client": "0.0.0",
28
27
  "@mrrlin/schemas": "0.0.0",
29
- "@mrrlin/director-e2e": "0.0.0"
28
+ "@mrrlin/tsconfig": "0.0.0",
29
+ "@mrrlin/wiki": "0.0.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "@iarna/toml": "^2.2.5",