@mrrlin-dev/mcp 0.4.1 → 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"];
@@ -38187,6 +38207,9 @@ var mrrlinInboxQuestionOptionsSchema = external_exports.array(mrrlinInboxQuestio
38187
38207
  });
38188
38208
  }
38189
38209
  });
38210
+ var REVIEW_AGAIN_ANSWER = "Review the current spec again without changing it.";
38211
+ var EDIT_SPEC_ANSWER = "Hold the task until the spec is edited by hand.";
38212
+ var PROCEED_ANYWAY_ANSWER = "Proceed without agreement; record the unsettled points on the spec.";
38190
38213
  var mrrlinInboxItemSchema = external_exports.object({
38191
38214
  actionPayload: mrrlinInboxItemActionPayloadSchema.nullable(),
38192
38215
  answer: external_exports.string().nullable(),
@@ -40187,7 +40210,7 @@ function startCodexProbe(deps = {}) {
40187
40210
  }
40188
40211
 
40189
40212
  // src/_generated/version.ts
40190
- var PKG_VERSION = "0.4.1";
40213
+ var PKG_VERSION = "0.4.4";
40191
40214
 
40192
40215
  // src/api-base-url-migrate.ts
40193
40216
  var import_node_fs2 = __toESM(require("node:fs"), 1);
@@ -40989,7 +41012,7 @@ function startAgentPoolLoop(deps) {
40989
41012
 
40990
41013
  // src/execution-consumer.ts
40991
41014
  var import_node_child_process9 = require("node:child_process");
40992
- var import_node_crypto5 = __toESM(require("node:crypto"), 1);
41015
+ var import_node_crypto4 = __toESM(require("node:crypto"), 1);
40993
41016
  var import_node_path10 = __toESM(require("node:path"), 1);
40994
41017
  var import_node_util = require("node:util");
40995
41018
 
@@ -41312,7 +41335,7 @@ async function runConsensus(input) {
41312
41335
  let decision;
41313
41336
  for (let round = 1; round <= effectiveRounds; round++) {
41314
41337
  if (calls + input.heads.length > input.maxHeadCalls) {
41315
- return { outcome: "CONSENSUS_BLOCKED", rounds: round - 1, finalArtifact: artifact, history, ...decision ? { escalation: decision } : {} };
41338
+ return { outcome: "CONSENSUS_BLOCKED", rounds: round - 1, finalArtifact: artifact, history, unresolved: history[history.length - 1]?.accepted ?? [], ...decision ? { escalation: decision } : {} };
41316
41339
  }
41317
41340
  const dismissedBlock = dismissedFwd.length ? `PREVIOUSLY DISMISSED (do not re-raise unless new information):
41318
41341
  ${dismissedFwd.slice(-20).join("\n")}` : "";
@@ -41334,7 +41357,7 @@ ${dismissedFwd.slice(-20).join("\n")}` : "";
41334
41357
  (r) => r.res.ok
41335
41358
  );
41336
41359
  if (responding.length === 0) {
41337
- return { outcome: "CONSENSUS_UNAVAILABLE", rounds: round, finalArtifact: artifact, history };
41360
+ return { outcome: "CONSENSUS_UNAVAILABLE", rounds: round, finalArtifact: artifact, history, unresolved: [] };
41338
41361
  }
41339
41362
  const headOutcomes = input.heads.map((persona) => ({
41340
41363
  persona,
@@ -41360,7 +41383,7 @@ ${dismissedFwd.slice(-20).join("\n")}` : "";
41360
41383
  let adj;
41361
41384
  if (issues.length > 0) {
41362
41385
  if (calls + 1 > input.maxHeadCalls) {
41363
- return { outcome: "CONSENSUS_BLOCKED", rounds: round, finalArtifact: artifact, history, ...decision ? { escalation: decision } : {} };
41386
+ return { outcome: "CONSENSUS_BLOCKED", rounds: round, finalArtifact: artifact, history, unresolved: history[history.length - 1]?.accepted ?? [], ...decision ? { escalation: decision } : {} };
41364
41387
  }
41365
41388
  calls++;
41366
41389
  try {
@@ -41374,7 +41397,7 @@ ${dismissedFwd.slice(-20).join("\n")}` : "";
41374
41397
  history.push({ round, accepted: adj.accepted, dismissed: adj.dismissed, headOutcomes });
41375
41398
  adj.dismissed.forEach((d) => dismissedFwd.push(`- ${d.id}: ${d.reason}`));
41376
41399
  if (adj.accepted.length === 0) {
41377
- return { outcome: "CONVERGED", rounds: round, finalArtifact: artifact, history, ...decision ? { escalation: decision } : {} };
41400
+ return { outcome: "CONVERGED", rounds: round, finalArtifact: artifact, history, unresolved: [], ...decision ? { escalation: decision } : {} };
41378
41401
  }
41379
41402
  if (round === effectiveRounds) {
41380
41403
  if (!decision) {
@@ -41384,12 +41407,12 @@ ${dismissedFwd.slice(-20).join("\n")}` : "";
41384
41407
  escalated = true;
41385
41408
  effectiveRounds = Math.min(effectiveRounds + decision.grantedRounds, input.rounds * 2);
41386
41409
  } else {
41387
- return { outcome: "CONSENSUS_BLOCKED", rounds: round, finalArtifact: artifact, history, ...decision ? { escalation: decision } : {} };
41410
+ return { outcome: "CONSENSUS_BLOCKED", rounds: round, finalArtifact: artifact, history, unresolved: history[history.length - 1]?.accepted ?? [], ...decision ? { escalation: decision } : {} };
41388
41411
  }
41389
41412
  }
41390
41413
  artifact = await input.revise(artifact, adj.accepted);
41391
41414
  }
41392
- return { outcome: "CONSENSUS_BLOCKED", rounds: effectiveRounds, finalArtifact: artifact, history };
41415
+ return { outcome: "CONSENSUS_BLOCKED", rounds: effectiveRounds, finalArtifact: artifact, history, unresolved: history[history.length - 1]?.accepted ?? [] };
41393
41416
  }
41394
41417
 
41395
41418
  // src/consensus/spec-identity.ts
@@ -41681,14 +41704,15 @@ async function runSpecGate(_target, deps, opts) {
41681
41704
  });
41682
41705
  if (result.outcome === "CONVERGED") {
41683
41706
  await deps.promoteSpecReady(currentHash);
41684
- return { outcome: "CONVERGED" };
41707
+ return { outcome: "CONVERGED", headOutcomes: result.history.at(-1)?.headOutcomes ?? [] };
41685
41708
  }
41686
41709
  if (result.outcome === "CONSENSUS_BLOCKED") {
41687
41710
  return {
41688
41711
  outcome: "CONSENSUS_BLOCKED",
41689
41712
  ...keptClarifications.length > 0 ? { operatorClarifications: keptClarifications } : {},
41690
41713
  ...droppedByCap.length > 0 ? { droppedByCap } : {},
41691
- ...result.escalation ? { escalation: result.escalation } : {}
41714
+ ...result.escalation ? { escalation: result.escalation } : {},
41715
+ unresolved: result.unresolved
41692
41716
  };
41693
41717
  }
41694
41718
  return { outcome: result.outcome };
@@ -41730,6 +41754,63 @@ async function resolveSpecDomain(client, projectSlug, page) {
41730
41754
  }
41731
41755
  }
41732
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
+
41733
41814
  // src/consensus/code-gate-git.ts
41734
41815
  var import_node_child_process7 = require("node:child_process");
41735
41816
  var DIFF_MAX_BYTES = 2e5;
@@ -42169,12 +42250,22 @@ ${artifact}`,
42169
42250
  }
42170
42251
  const outcome = r["outcome"];
42171
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
+ });
42172
42261
  const { domain: domain2 } = await loadIntent();
42173
42262
  try {
42174
42263
  await client.recordSpecConsensusReview(projectSlug, specPageId, {
42175
42264
  contentHash: domainScopedReviewHash2(contentHash, domain2),
42176
42265
  outcome: String(outcome),
42177
- rounds: typeof rounds2 === "number" ? rounds2 : 0
42266
+ rounds: typeof rounds2 === "number" ? rounds2 : 0,
42267
+ ...history !== void 0 ? { history } : {},
42268
+ ...highlights ? { highlights } : {}
42178
42269
  });
42179
42270
  } catch (err) {
42180
42271
  process.stderr.write(
@@ -42382,11 +42473,21 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
42382
42473
  if (typeof contentHash !== "string" || contentHash.length === 0) return;
42383
42474
  const outcome = String(r["outcome"]);
42384
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
+ });
42385
42484
  try {
42386
42485
  await client.recordSpecConsensusReview(projectSlug, deliverablePageId, {
42387
42486
  contentHash: domainScopedReviewHash2(contentHash, domain2),
42388
42487
  outcome,
42389
- rounds: rounds2
42488
+ rounds: rounds2,
42489
+ ...history !== void 0 ? { history } : {},
42490
+ ...highlights ? { highlights } : {}
42390
42491
  });
42391
42492
  } catch (err) {
42392
42493
  process.stderr.write(
@@ -42396,6 +42497,7 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
42396
42497
  }
42397
42498
  if (runId && taskId) {
42398
42499
  try {
42500
+ const diagnostics = Array.isArray(r["diagnostics"]) ? r["diagnostics"] : void 0;
42399
42501
  await client.appendExecutionArtifact(projectSlug, runId, {
42400
42502
  id: `EA-${import_node_crypto2.default.randomUUID()}`,
42401
42503
  kind: "verification",
@@ -42405,7 +42507,8 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
42405
42507
  runId,
42406
42508
  taskId,
42407
42509
  deliverablePageId,
42408
- contentHash
42510
+ contentHash,
42511
+ ...diagnostics ? { diagnostics } : {}
42409
42512
  }
42410
42513
  });
42411
42514
  } catch {
@@ -42416,6 +42519,35 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
42416
42519
  }
42417
42520
 
42418
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
+ }
42419
42551
  function mapDeliverableGateResult(result) {
42420
42552
  switch (result.outcome) {
42421
42553
  case "DELIVERABLE_CHANGED_DURING_REVIEW":
@@ -42429,17 +42561,9 @@ function mapDeliverableGateResult(result) {
42429
42561
  reason: "reviewers were unavailable (provider/quota) during the deliverable review (CONSENSUS_UNAVAILABLE)"
42430
42562
  };
42431
42563
  case "CONSENSUS_BLOCKED": {
42432
- const clarifications = result.clarifications ?? [];
42433
- if (clarifications.length > 0) {
42434
- return {
42435
- action: "questions",
42436
- clarifications,
42437
- reason: "reviewers did not converge; unresolved scope/ambiguity/correctness issues were filed as operator questions (CONSENSUS_BLOCKED)"
42438
- };
42439
- }
42440
42564
  return {
42441
42565
  action: "handoff",
42442
- reason: "reviewers did not converge and raised no clarification-eligible issues (CONSENSUS_BLOCKED)"
42566
+ reason: `reviewers did not converge; ${formatDeliverableGateDiagnostics(result.diagnostics ?? [])} (CONSENSUS_BLOCKED)`
42443
42567
  };
42444
42568
  }
42445
42569
  case "MISSING_ACCEPTANCE_CRITERIA":
@@ -42458,74 +42582,8 @@ function mapDeliverableGateResult(result) {
42458
42582
  }
42459
42583
  }
42460
42584
 
42461
- // src/consensus/deliverable-gate-questions.ts
42462
- var import_node_crypto3 = require("node:crypto");
42463
- function buildDeliverableQuestionOptions(category) {
42464
- if (category === "correctness") {
42465
- return [
42466
- {
42467
- label: "Deliverable is correct",
42468
- description: "The flagged claim is accurate as written; no change needed.",
42469
- answer: "Confirm the deliverable's content is correct as written; the reviewer's concern does not require a change."
42470
- },
42471
- {
42472
- label: "Deliverable needs correction",
42473
- description: "The flagged claim is wrong or unsupported; the deliverable needs revision.",
42474
- answer: "The deliverable is incorrect here and must be revised to fix the flagged claim before it can be accepted."
42475
- }
42476
- ];
42477
- }
42478
- if (category === "scope") {
42479
- return [
42480
- {
42481
- label: "Coverage is sufficient",
42482
- description: "The deliverable's current coverage of the acceptance criteria is sufficient as written.",
42483
- answer: "Accept the deliverable's current scope/coverage as sufficient against the acceptance criteria."
42484
- },
42485
- {
42486
- label: "Coverage is missing",
42487
- description: "The deliverable is missing required coverage and needs revision to address it.",
42488
- answer: "The deliverable is missing required coverage and must be revised to address the flagged gap."
42489
- }
42490
- ];
42491
- }
42492
- return [
42493
- {
42494
- label: "Interpretation is fine",
42495
- description: "The deliverable's current interpretation of the ambiguous point is acceptable as written.",
42496
- answer: "Accept the deliverable's current interpretation of the ambiguous point as written."
42497
- },
42498
- {
42499
- label: "Needs clarification",
42500
- description: "The ambiguous point needs to be resolved and the deliverable revised accordingly.",
42501
- answer: "The deliverable must be revised to resolve the flagged ambiguity explicitly."
42502
- }
42503
- ];
42504
- }
42505
- async function fileDeliverableGateQuestions(client, projectSlug, taskId, clarifications) {
42506
- let filed = 0;
42507
- for (const clarification of clarifications) {
42508
- const id = `II-${taskId}-deliverable-gate-${(0, import_node_crypto3.createHash)("sha256").update(clarification.question).digest("hex").slice(0, 10)}`;
42509
- try {
42510
- await client.createInboxItem(projectSlug, {
42511
- id,
42512
- kind: "question",
42513
- taskId,
42514
- body: clarification.question,
42515
- origin: "gate",
42516
- questionOptions: buildDeliverableQuestionOptions(clarification.category)
42517
- });
42518
- filed += 1;
42519
- } catch (error51) {
42520
- const message = error51 instanceof Error ? error51.message : String(error51);
42521
- if (/inbox_items_pkey/.test(message)) filed += 1;
42522
- }
42523
- }
42524
- return filed;
42525
- }
42526
-
42527
42585
  // src/async-tools/draft-spec.ts
42528
- var import_node_crypto4 = require("node:crypto");
42586
+ var import_node_crypto3 = require("node:crypto");
42529
42587
  var DRAFT_SPEC_TOOL_NAME = "draft_task_spec";
42530
42588
  var SPECS_FOLDER = "Specs";
42531
42589
  var DRAFT_SPEC_TLDR_MAX_CHARS = 300;
@@ -43004,7 +43062,7 @@ function onComplete(outcome, input) {
43004
43062
  };
43005
43063
  }
43006
43064
  const summary = outcome.error;
43007
- 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);
43008
43066
  return {
43009
43067
  kind: "info",
43010
43068
  id: `II-sched-draft-failed-${input.taskId}-${hash3}`,
@@ -43036,9 +43094,9 @@ function renderMilestone(m, ctx) {
43036
43094
  return s;
43037
43095
  }
43038
43096
  case "consensus_converged":
43039
- return "Spec review passed \u2014 no issues found";
43097
+ return "The reviewers agreed on the spec";
43040
43098
  case "consensus_converged_needs_approval": {
43041
- let s = "Spec review passed. Please review and approve";
43099
+ let s = "The reviewers agreed on the spec. Please review and approve";
43042
43100
  if (ctx.specUrl) s += ` \u2014 ${ctx.specUrl}`;
43043
43101
  if (ctx.tldr) s += `. Summary: ${ctx.tldr}`;
43044
43102
  return s;
@@ -44306,7 +44364,7 @@ function isBridgeGeneratedArtifact(artifact) {
44306
44364
  return typeof payload === "object" && payload !== null && "event" in payload;
44307
44365
  }
44308
44366
  function newArtifactId() {
44309
- return `EA-${import_node_crypto5.default.randomUUID()}`;
44367
+ return `EA-${import_node_crypto4.default.randomUUID()}`;
44310
44368
  }
44311
44369
  var BLAST_RADIUS_FLOOR_PATTERNS = [/^supabase\/migrations\//, /^packages\/db\/src\/schema\.ts$/];
44312
44370
  function isBlastRadiusFloorPath(path19) {
@@ -44561,7 +44619,7 @@ async function driveRun(deps, projectSlug, runId) {
44561
44619
  if (!claimed) return;
44562
44620
  if (!claimed.leaseId) {
44563
44621
  await deps.client.createInboxItem(projectSlug, {
44564
- id: `II-${import_node_crypto5.default.randomUUID()}`,
44622
+ id: `II-${import_node_crypto4.default.randomUUID()}`,
44565
44623
  kind: "handoff",
44566
44624
  taskId: claimed.taskId,
44567
44625
  body: `Execution run ${runId} was claimed but returned no leaseId. This indicates DB/API corruption and requires operator attention.`
@@ -45351,10 +45409,6 @@ async function driveNonCodeRun(deps, projectSlug, runId, leaseId, task, domain2)
45351
45409
  }
45352
45410
  if (gateResult.outcome !== "CONVERGED") {
45353
45411
  const mapped = mapDeliverableGateResult(gateResult);
45354
- if (mapped.action === "questions") {
45355
- await fileDeliverableGateQuestions(deps.client, projectSlug, task.id, mapped.clarifications).catch(() => {
45356
- });
45357
- }
45358
45412
  await failRun(
45359
45413
  deps,
45360
45414
  projectSlug,
@@ -45484,7 +45538,7 @@ async function reconcileTaskAwaitingMerge(deps, projectSlug, task, runId, reason
45484
45538
  }
45485
45539
  try {
45486
45540
  await deps.client.createInboxItem(projectSlug, {
45487
- id: `II-${import_node_crypto5.default.randomUUID()}`,
45541
+ id: `II-${import_node_crypto4.default.randomUUID()}`,
45488
45542
  kind: "handoff",
45489
45543
  taskId: task.id,
45490
45544
  body: `Task ${task.id} (${task.title}) still needs operator follow-up: ${reason}. Mrrlin could not move the task back to Handoff needed automatically.`
@@ -45519,7 +45573,7 @@ async function notifyOperatorHandoff(deps, projectSlug, task, runId, reason) {
45519
45573
  }
45520
45574
  try {
45521
45575
  await deps.client.createInboxItem(projectSlug, {
45522
- id: `II-${import_node_crypto5.default.randomUUID()}`,
45576
+ id: `II-${import_node_crypto4.default.randomUUID()}`,
45523
45577
  kind: "handoff",
45524
45578
  taskId: task.id,
45525
45579
  body: `Execution run ${runId} for task ${task.id} (${task.title}) needs operator attention: ${reason}`
@@ -45657,7 +45711,7 @@ async function deployGateHandoff(deps, projectSlug, task, runId, reason, leaseId
45657
45711
  }).catch(() => {
45658
45712
  });
45659
45713
  await deps.client.createInboxItem(projectSlug, {
45660
- id: `II-${import_node_crypto5.default.randomUUID()}`,
45714
+ id: `II-${import_node_crypto4.default.randomUUID()}`,
45661
45715
  kind: "handoff",
45662
45716
  taskId: task.id,
45663
45717
  body: `Deploy gate for run ${runId} (task ${task.id} \u2014 ${task.title}) needs operator attention: ${reason}`
@@ -45676,7 +45730,7 @@ function maxLeaseReclaims() {
45676
45730
  }
45677
45731
 
45678
45732
  // src/async-job-consumer.ts
45679
- var import_node_crypto6 = __toESM(require("node:crypto"), 1);
45733
+ var import_node_crypto5 = __toESM(require("node:crypto"), 1);
45680
45734
 
45681
45735
  // src/async-tools/registry.ts
45682
45736
  var registry2 = /* @__PURE__ */ new Map();
@@ -45761,7 +45815,7 @@ async function driveAsyncJob(deps, projectSlug, jobId) {
45761
45815
  const delivery = descriptor2.onComplete(outcome, input);
45762
45816
  if (!delivery.suppressInbox) {
45763
45817
  await deps.client.createInboxItem(projectSlug, {
45764
- id: delivery.id ?? `II-${import_node_crypto6.default.randomUUID()}`,
45818
+ id: delivery.id ?? `II-${import_node_crypto5.default.randomUUID()}`,
45765
45819
  kind: delivery.kind,
45766
45820
  body: delivery.body,
45767
45821
  ...delivery.taskId ? { taskId: delivery.taskId } : {}
@@ -45774,13 +45828,13 @@ async function deriveRequestedHash(client, toolName, input) {
45774
45828
  if (input.requestedHash) return input.requestedHash;
45775
45829
  if (toolName === "run_spec_consensus" && input.pageId) {
45776
45830
  const page = await client.getWikiPage(input.projectSlug, input.pageId);
45777
- 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");
45778
45832
  }
45779
45833
  if (toolName === "draft_task_spec" && input.taskId) {
45780
45834
  const task = await client.getTask(input.projectSlug, input.taskId);
45781
- 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");
45782
45836
  }
45783
- 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");
45784
45838
  }
45785
45839
  async function runAsyncBackstopSweep(deps, drive = (slug, jobId) => driveAsyncJob(deps, slug, jobId)) {
45786
45840
  const projects = await deps.client.listProjects();
@@ -45839,26 +45893,34 @@ async function runDeliverableGate(_target, deps) {
45839
45893
  });
45840
45894
  return { outcome: "DELIVERABLE_CHANGED_DURING_REVIEW" };
45841
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
+ }
45842
45905
  await deps.writeEvidence({
45843
45906
  deliverablePageId: deliverable.pageId,
45844
45907
  outcome: result.outcome,
45845
45908
  rounds: result.rounds,
45846
- contentHash: currentHash
45909
+ history: result.history,
45910
+ contentHash: currentHash,
45911
+ ...diagnostics ? { diagnostics } : {}
45847
45912
  });
45848
45913
  if (result.outcome === "CONSENSUS_BLOCKED") {
45849
- const accepted = result.history.at(-1)?.accepted ?? [];
45850
- const clarifications = accepted.filter((issue2) => issue2.category === "scope" || issue2.category === "ambiguity" || issue2.category === "correctness").map((issue2) => ({
45851
- question: issue2.text,
45852
- whyBlocking: `reviewer raised a ${issue2.category} issue`,
45853
- category: issue2.category
45854
- }));
45855
45914
  return {
45856
45915
  outcome: "CONSENSUS_BLOCKED",
45857
45916
  rounds: result.rounds,
45858
- ...clarifications.length > 0 ? { clarifications } : {},
45917
+ diagnostics: diagnostics ?? [],
45859
45918
  ...result.escalation ? { escalation: result.escalation } : {}
45860
45919
  };
45861
45920
  }
45921
+ if (result.outcome === "CONVERGED") {
45922
+ return { outcome: result.outcome, rounds: result.rounds, headOutcomes: result.history.at(-1)?.headOutcomes ?? [] };
45923
+ }
45862
45924
  return { outcome: result.outcome, rounds: result.rounds };
45863
45925
  }
45864
45926
 
@@ -46169,7 +46231,7 @@ function applyToolBusyNotification(clock, method, params) {
46169
46231
  }
46170
46232
 
46171
46233
  // src/worker-token.ts
46172
- var import_node_crypto7 = require("node:crypto");
46234
+ var import_node_crypto6 = require("node:crypto");
46173
46235
  var import_node_fs9 = require("node:fs");
46174
46236
  var import_node_os6 = __toESM(require("node:os"), 1);
46175
46237
  var import_node_path13 = __toESM(require("node:path"), 1);
@@ -46200,7 +46262,7 @@ function readOrCreateWorkerToken() {
46200
46262
  if (existing) return existing;
46201
46263
  const dir = workerDir();
46202
46264
  (0, import_node_fs9.mkdirSync)(dir, { recursive: true, mode: 448 });
46203
- const token = (0, import_node_crypto7.randomBytes)(32).toString("hex");
46265
+ const token = (0, import_node_crypto6.randomBytes)(32).toString("hex");
46204
46266
  (0, import_node_fs9.writeFileSync)(workerTokenPath(), `${token}
46205
46267
  `, { encoding: "utf8", mode: 384 });
46206
46268
  return token;
@@ -51054,7 +51116,7 @@ function buildThreadStartParams(msg, config2, desiredModel) {
51054
51116
  " bullets (`- AND ...` as needed). Scenarios make the spec verifiable.",
51055
51117
  "- Keep behavior contracts in the spec; implementation details belong in `## Implementation Plan`.",
51056
51118
  "- Apply this format ONLY to Specs/{id} pages; do not retro-fit it onto other wiki page types.",
51057
- "- 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.',
51058
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.",
51059
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.',
51060
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.",
@@ -51489,7 +51551,7 @@ function createBridgeRequestListener(opts) {
51489
51551
  res.end(JSON.stringify({ ok: false, error: "Origin not allowed." }));
51490
51552
  return;
51491
51553
  }
51492
- const nonce = import_node_crypto8.default.randomBytes(16).toString("base64url");
51554
+ const nonce = import_node_crypto7.default.randomBytes(16).toString("base64url");
51493
51555
  res.writeHead(200, { "content-type": "application/json" });
51494
51556
  res.end(JSON.stringify({ ok: true, wsUrl: `ws://${host}:${port}/ws`, token, nonce }));
51495
51557
  return;
@@ -51770,7 +51832,7 @@ function createBridgeMessageHandler(deps) {
51770
51832
  }
51771
51833
  }
51772
51834
  socket.on("message", async (data) => {
51773
- const spanId = import_node_crypto8.default.randomUUID();
51835
+ const spanId = import_node_crypto7.default.randomUUID();
51774
51836
  if (logCtx) {
51775
51837
  logCtx.span.id = spanId;
51776
51838
  logCtx.span.at = Date.now();
@@ -52286,7 +52348,7 @@ function createBridgeMessageHandler(deps) {
52286
52348
  thread.inFlight = true;
52287
52349
  pendingTurnSessions.delete(directorSessionId);
52288
52350
  thread.replay = createReplayBuffer({
52289
- turnId: (0, import_node_crypto8.randomUUID)(),
52351
+ turnId: (0, import_node_crypto7.randomUUID)(),
52290
52352
  spanId,
52291
52353
  ...deps.retentionMs !== void 0 ? { retentionMs: deps.retentionMs } : {},
52292
52354
  ...deps.ackLingerMs !== void 0 ? { ackLingerMs: deps.ackLingerMs } : {}
@@ -52854,7 +52916,7 @@ async function startDirectorBridge() {
52854
52916
  };
52855
52917
  const sessionSinks = createSessionSinkRegistry();
52856
52918
  const tombstones = /* @__PURE__ */ new Map();
52857
- const bridgeInstanceId = (0, import_node_crypto8.randomUUID)();
52919
+ const bridgeInstanceId = (0, import_node_crypto7.randomUUID)();
52858
52920
  const asyncConsumerDeps = {
52859
52921
  client,
52860
52922
  run: wrappedTurnLock.run,
@@ -53008,7 +53070,7 @@ async function startDirectorBridge() {
53008
53070
  fileUpdateInboxItem: async (projectSlug) => {
53009
53071
  const body = selfUpdateHandle.updateInboxBody();
53010
53072
  if (!body) return;
53011
- const id = `II-MCPUPD-${(0, import_node_crypto8.randomUUID)()}`;
53073
+ const id = `II-MCPUPD-${(0, import_node_crypto7.randomUUID)()}`;
53012
53074
  try {
53013
53075
  const row = await client.createInboxItem(projectSlug, { id, kind: "info", body });
53014
53076
  process.stderr.write(
@@ -53038,7 +53100,7 @@ async function startDirectorBridge() {
53038
53100
  const bridgeLogger = createBridgeLogger(config2.stateDir, process.env);
53039
53101
  let bridgeBinShaShort = "unknown00000";
53040
53102
  try {
53041
- 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);
53042
53104
  } catch {
53043
53105
  }
53044
53106
  attachBridgeWss(server, {
@@ -53400,7 +53462,7 @@ function timingSafeTokenEqual(provided, expected) {
53400
53462
  const a = Buffer.from(provided);
53401
53463
  const b = Buffer.from(expected);
53402
53464
  if (a.length !== b.length) return false;
53403
- return import_node_crypto8.default.timingSafeEqual(a, b);
53465
+ return import_node_crypto7.default.timingSafeEqual(a, b);
53404
53466
  }
53405
53467
  function readDispatchBody(req) {
53406
53468
  return new Promise((resolve, reject) => {
@@ -53429,7 +53491,7 @@ function readOrCreateBridgeToken(stateDir) {
53429
53491
  } catch {
53430
53492
  }
53431
53493
  import_node_fs15.default.mkdirSync(stateDir, { recursive: true, mode: 448 });
53432
- const token = import_node_crypto8.default.randomBytes(32).toString("base64url");
53494
+ const token = import_node_crypto7.default.randomBytes(32).toString("base64url");
53433
53495
  import_node_fs15.default.writeFileSync(tokenPath, `${token}
53434
53496
  `, { mode: 384 });
53435
53497
  return token;
@@ -56814,7 +56876,7 @@ var StdioServerTransport = class {
56814
56876
  };
56815
56877
 
56816
56878
  // src/tools.ts
56817
- var import_node_crypto11 = require("node:crypto");
56879
+ var import_node_crypto10 = require("node:crypto");
56818
56880
  var import_promises3 = require("node:fs/promises");
56819
56881
  var import_node_path20 = __toESM(require("node:path"), 1);
56820
56882
  var import_node_os10 = __toESM(require("node:os"), 1);
@@ -57355,7 +57417,7 @@ async function resolveActiveRunHead(input, deps) {
57355
57417
  }
57356
57418
 
57357
57419
  // src/async-tools/consensus.ts
57358
- var import_node_crypto9 = require("node:crypto");
57420
+ var import_node_crypto8 = require("node:crypto");
57359
57421
 
57360
57422
  // src/consensus/question-similarity.ts
57361
57423
  var SUPPRESSION_SIMILARITY_THRESHOLD = 0.6;
@@ -57384,6 +57446,41 @@ function isCoveredByAdjudicated(rawQuestion, adjudicated) {
57384
57446
  return null;
57385
57447
  }
57386
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
+
57387
57484
  // src/async-tools/consensus.ts
57388
57485
  function normalizeClarificationQuestion(question) {
57389
57486
  return question.replace(/\s+/g, " ").trim().replace(/[?.!]+$/, "").trim();
@@ -57470,6 +57567,44 @@ function humanizeClarificationBody(clarification) {
57470
57567
  const context = clarification.rawQuestion.replace(/\b(?:parseable|SPEC_IDENTITY_MISMATCH|machine-usable)\b/gi, "").replace(/\s+/g, " ").trim();
57471
57568
  return context ? `${prefix} ${context}` : prefix;
57472
57569
  }
57570
+ var BLOCK_DECISION_OPENER = "The reviewers could not agree on this spec";
57571
+ var BLOCK_BODY_TRAILER = " How should I proceed?";
57572
+ function renderUnresolvedPoints(unresolved) {
57573
+ return unresolved.map(
57574
+ (issue2) => issue2.text.replace(/\b(?:parseable|SPEC_IDENTITY_MISMATCH|machine-usable)\b/gi, "").replace(/\s+/g, " ").trim()
57575
+ ).join("; ");
57576
+ }
57577
+ function renderBlockDecisionBody(unresolved) {
57578
+ if (unresolved.length === 0) {
57579
+ return `${BLOCK_DECISION_OPENER}, and their objections could not be read back.${BLOCK_BODY_TRAILER}`;
57580
+ }
57581
+ return `${BLOCK_DECISION_OPENER}. Still unsettled: ${renderUnresolvedPoints(unresolved)}.${BLOCK_BODY_TRAILER}`;
57582
+ }
57583
+ function buildBlockDecisionOptions(diagnosis, reviewAgainAvailable) {
57584
+ const recommendReviewAgain = reviewAgainAvailable && (diagnosis === "INFRA" || diagnosis === "SUBSTANCE");
57585
+ const recommendEditSpec = !reviewAgainAvailable || diagnosis === "AMBIGUITY";
57586
+ const options = [];
57587
+ if (reviewAgainAvailable) {
57588
+ options.push({
57589
+ label: "Review again",
57590
+ description: "Send the current spec back for another review.",
57591
+ answer: REVIEW_AGAIN_ANSWER,
57592
+ ...recommendReviewAgain ? { recommended: true } : {}
57593
+ });
57594
+ }
57595
+ options.push({
57596
+ label: "Edit spec",
57597
+ description: "Open the spec and change it by hand; the review restarts on save.",
57598
+ answer: EDIT_SPEC_ANSWER,
57599
+ ...recommendEditSpec ? { recommended: true } : {}
57600
+ });
57601
+ options.push({
57602
+ label: "Proceed anyway",
57603
+ description: "Start the work with these points unsettled and recorded.",
57604
+ answer: PROCEED_ANYWAY_ANSWER
57605
+ });
57606
+ return options;
57607
+ }
57473
57608
  var defaultNativeSpecGateBuilders = { buildSpecGateDeps, runSpecGate };
57474
57609
  async function runNativeSpecGate(ctx, input, builders = defaultNativeSpecGateBuilders) {
57475
57610
  const deps = builders.buildSpecGateDeps({
@@ -57516,6 +57651,7 @@ function stripHumanizeOpener(body) {
57516
57651
  }
57517
57652
  return body;
57518
57653
  }
57654
+ var DID_NOT_CONVERGE_ERROR = "Consensus blocked: reviewers did not converge within budget.";
57519
57655
  async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
57520
57656
  const operatorClarifications = result.operatorClarifications ?? [];
57521
57657
  const diagnostics = result.diagnostics ?? [];
@@ -57523,9 +57659,10 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
57523
57659
  const taskId = page?.taskId ?? null;
57524
57660
  if (!resolvedTaskId && taskId) resolvedTaskId = taskId;
57525
57661
  const adjudicated = [];
57662
+ let taskInboxItems = [];
57526
57663
  if (taskId) {
57527
57664
  try {
57528
- const taskInboxItems = await ctx.client.listInboxItems(input.projectSlug, { taskId });
57665
+ taskInboxItems = await ctx.client.listInboxItems(input.projectSlug, { taskId });
57529
57666
  for (const item of taskInboxItems) {
57530
57667
  if (item.origin === "gate" && (item.status === "acknowledged" || item.status === "rejected")) {
57531
57668
  adjudicated.push({ body: stripHumanizeOpener(item.body), answer: item.answer, status: item.status });
@@ -57554,9 +57691,9 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
57554
57691
  }
57555
57692
  if (taskId) {
57556
57693
  const scopeId = taskId;
57557
- 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;
57558
57695
  for (const clarification of toFile) {
57559
- 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)}`;
57560
57697
  try {
57561
57698
  const questionOptions = buildGateQuestionOptions(clarification.rawQuestion, clarification.category);
57562
57699
  await ctx.client.createInboxItem(input.projectSlug, {
@@ -57574,9 +57711,9 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
57574
57711
  filedQuestionCount = toFile.length;
57575
57712
  } else if (planId) {
57576
57713
  const scopeId = planId;
57577
- 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;
57578
57715
  for (const clarification of toFile) {
57579
- 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)}`;
57580
57717
  try {
57581
57718
  const questionOptions = buildGateQuestionOptions(clarification.rawQuestion, clarification.category);
57582
57719
  await ctx.client.createInboxItem(input.projectSlug, {
@@ -57594,6 +57731,29 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
57594
57731
  filedQuestionCount = toFile.length;
57595
57732
  }
57596
57733
  }
57734
+ if (filedQuestionCount === 0 && diagnostics.length === 0 && taskId && page) {
57735
+ const domain2 = await resolveSpecDomain(ctx.client, input.projectSlug, {
57736
+ id: page.id,
57737
+ taskId: page.taskId ?? null
57738
+ });
57739
+ const reviewHash = domainScopedReviewHash2(page.contentHash, domain2);
57740
+ const baseBlockItemId = `II-${taskId}-gateblock-${(0, import_node_crypto8.createHash)("sha256").update(reviewHash).digest("hex").slice(0, 10)}`;
57741
+ const reviewAgainAvailable = !taskInboxItems.some(
57742
+ (item) => item.id === baseBlockItemId && item.status !== "pending" && item.answer === REVIEW_AGAIN_ANSWER
57743
+ );
57744
+ const blockItemId = reviewAgainAvailable ? baseBlockItemId : `${baseBlockItemId}-2`;
57745
+ try {
57746
+ await ctx.client.createInboxItem(input.projectSlug, {
57747
+ id: blockItemId,
57748
+ kind: "question",
57749
+ taskId,
57750
+ body: renderBlockDecisionBody(result.unresolved ?? []),
57751
+ origin: "gate_block",
57752
+ questionOptions: buildBlockDecisionOptions(result.escalation?.diagnosis, reviewAgainAvailable)
57753
+ });
57754
+ } catch {
57755
+ }
57756
+ }
57597
57757
  if (operatorClarifications.length > 0) {
57598
57758
  try {
57599
57759
  if (page) {
@@ -57630,14 +57790,14 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
57630
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.";
57631
57791
  }
57632
57792
  } else if (operatorClarifications.length > 0 && toFile.length === 0 && suppressedAsAdjudicated.length > 0) {
57633
- 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.`;
57634
57794
  } else if (operatorClarifications.length > 0) {
57635
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.`;
57636
57796
  } else {
57637
- error51 = "Consensus blocked: reviewers did not converge within budget.";
57797
+ error51 = DID_NOT_CONVERGE_ERROR;
57638
57798
  }
57639
57799
  if (result.escalation) {
57640
- error51 += ` (diagnosis: ${result.escalation.diagnosis})`;
57800
+ error51 += ` (reason: ${result.escalation.diagnosis})`;
57641
57801
  }
57642
57802
  return {
57643
57803
  kind: "failed",
@@ -57715,6 +57875,24 @@ async function run(ctx, input, builders = defaultNativeSpecGateBuilders) {
57715
57875
  } catch {
57716
57876
  }
57717
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
+ };
57718
57896
  if (result.outcome === "CONSENSUS_BLOCKED") {
57719
57897
  return handleConsensusBlocked(result, ctx, input, resolvedTaskId);
57720
57898
  }
@@ -57725,9 +57903,10 @@ async function run(ctx, input, builders = defaultNativeSpecGateBuilders) {
57725
57903
  case "CONVERGED":
57726
57904
  case "SKIPPED":
57727
57905
  await resolveTitle();
57906
+ if (result.outcome === "CONVERGED") await resolveHighlights2();
57728
57907
  return {
57729
57908
  kind: "done",
57730
- resultJson: JSON.stringify(result),
57909
+ resultJson: JSON.stringify({ ...result, highlights }),
57731
57910
  finalReviewedHash: null,
57732
57911
  ...resolvedTaskId ? { taskId: resolvedTaskId } : {},
57733
57912
  ...resolvedTaskTitle ? { taskTitle: resolvedTaskTitle } : {}
@@ -57750,21 +57929,31 @@ function onComplete2(outcome, input) {
57750
57929
  const taskRef = taskId ? outcome.kind === "done" && outcome.taskTitle ? `${taskId} \u2014 ${outcome.taskTitle}` : taskId : null;
57751
57930
  if (outcome.kind === "done") {
57752
57931
  let specOutcome = "CONVERGED";
57932
+ let headOutcomes = [];
57933
+ let findings = [];
57753
57934
  try {
57754
- 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 ?? [];
57755
57939
  } catch {
57756
57940
  }
57757
57941
  if (specOutcome === "SKIPPED") {
57758
57942
  return {
57759
57943
  kind: "info",
57760
57944
  ...taskId ? { taskId } : {},
57761
- 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.`
57762
57946
  };
57763
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")}` : "";
57764
57953
  return {
57765
57954
  kind: "completed",
57766
57955
  ...taskId ? { taskId } : {},
57767
- 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}`
57768
57957
  };
57769
57958
  }
57770
57959
  if (outcome.kind === "failed" && (outcome.filedQuestionCount ?? 0) > 0) {
@@ -57776,6 +57965,14 @@ function onComplete2(outcome, input) {
57776
57965
  body: taskRef ? `Consensus blocked on ${taskRef} (Specs/${input.pageId}); filed ${n} clarifying question${n === 1 ? "" : "s"} \u2014 answer to unblock the run.` : `Consensus blocked on Specs/${input.pageId}; filed ${n} clarifying question${n === 1 ? "" : "s"} \u2014 answer to unblock the run.`
57777
57966
  };
57778
57967
  }
57968
+ if (outcome.kind === "failed" && outcome.error.startsWith(DID_NOT_CONVERGE_ERROR) && taskId) {
57969
+ return {
57970
+ kind: "info",
57971
+ suppressInbox: true,
57972
+ ...taskId ? { taskId } : {},
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.`
57974
+ };
57975
+ }
57779
57976
  const handoffTaskRef = taskRef ? ` (${taskRef})` : "";
57780
57977
  return {
57781
57978
  kind: "handoff",
@@ -57830,6 +58027,24 @@ async function resolveTaskScope(ctx, input, { withTitle }) {
57830
58027
  }
57831
58028
  return { taskId, taskTitle };
57832
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
+ }
57833
58048
  async function run2(ctx, input) {
57834
58049
  const deps = await buildDeliverableGateDeps({
57835
58050
  client: ctx.client,
@@ -57844,9 +58059,10 @@ async function run2(ctx, input) {
57844
58059
  );
57845
58060
  if (result.outcome === "CONVERGED" || result.outcome === "SKIPPED") {
57846
58061
  const { taskId: taskId2, taskTitle } = await resolveTaskScope(ctx, input, { withTitle: true });
58062
+ const highlights = result.outcome === "CONVERGED" ? await resolveHighlights(ctx, input) : null;
57847
58063
  return {
57848
58064
  kind: "done",
57849
- resultJson: JSON.stringify(result),
58065
+ resultJson: JSON.stringify({ ...result, highlights }),
57850
58066
  finalReviewedHash: null,
57851
58067
  ...taskId2 ? { taskId: taskId2 } : {},
57852
58068
  ...taskTitle ? { taskTitle } : {}
@@ -57861,18 +58077,10 @@ async function run2(ctx, input) {
57861
58077
  };
57862
58078
  }
57863
58079
  const { taskId } = await resolveTaskScope(ctx, input, { withTitle: false });
57864
- let filedQuestionCount = 0;
57865
- if (mapped.action === "questions") {
57866
- if (taskId) {
57867
- filedQuestionCount = await fileDeliverableGateQuestions(ctx.client, input.projectSlug, taskId, mapped.clarifications);
57868
- }
57869
- }
57870
- const error51 = filedQuestionCount > 0 ? `Consensus blocked on ${filedQuestionCount} clarification(s); filed as operator question(s).` : mapped.reason;
57871
58080
  return {
57872
58081
  kind: "failed",
57873
58082
  failureReason: "consensus_blocked",
57874
- error: error51,
57875
- filedQuestionCount,
58083
+ error: mapped.reason,
57876
58084
  ...taskId ? { taskId } : {}
57877
58085
  };
57878
58086
  }
@@ -57881,30 +58089,31 @@ function onComplete3(outcome, input) {
57881
58089
  const taskRef = taskId ? outcome.kind === "done" && outcome.taskTitle ? `${taskId} \u2014 ${outcome.taskTitle}` : taskId : null;
57882
58090
  if (outcome.kind === "done") {
57883
58091
  let specOutcome = "CONVERGED";
58092
+ let headOutcomes = [];
58093
+ let findings = [];
57884
58094
  try {
57885
- 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 ?? [];
57886
58099
  } catch {
57887
58100
  }
57888
58101
  if (specOutcome === "SKIPPED") {
57889
58102
  return {
57890
58103
  kind: "info",
57891
58104
  ...taskId ? { taskId } : {},
57892
- 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.`
57893
58106
  };
57894
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")}` : "";
57895
58113
  return {
57896
58114
  kind: "completed",
57897
58115
  ...taskId ? { taskId } : {},
57898
- 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.`
57899
- };
57900
- }
57901
- if (outcome.kind === "failed" && (outcome.filedQuestionCount ?? 0) > 0) {
57902
- const n = outcome.filedQuestionCount ?? 0;
57903
- return {
57904
- kind: "info",
57905
- suppressInbox: true,
57906
- ...taskId ? { taskId } : {},
57907
- 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}`
57908
58117
  };
57909
58118
  }
57910
58119
  return {
@@ -57923,7 +58132,7 @@ var deliverableConsensusDescriptor = {
57923
58132
  };
57924
58133
 
57925
58134
  // src/async-tools/enqueue.ts
57926
- var import_node_crypto10 = __toESM(require("node:crypto"), 1);
58135
+ var import_node_crypto9 = __toESM(require("node:crypto"), 1);
57927
58136
  async function enqueueAsyncTool(client, descriptor2, input) {
57928
58137
  if (descriptor2.cachedResult) {
57929
58138
  const cached2 = await descriptor2.cachedResult(client, input);
@@ -57936,7 +58145,7 @@ async function enqueueAsyncTool(client, descriptor2, input) {
57936
58145
  if (active) {
57937
58146
  return { status: "already_running", jobId: active.id, message: "Already running; will notify in inbox." };
57938
58147
  }
57939
- const jobId = `AJ-${import_node_crypto10.default.randomUUID()}`;
58148
+ const jobId = `AJ-${import_node_crypto9.default.randomUUID()}`;
57940
58149
  try {
57941
58150
  await client.createAsyncJob(input.projectSlug, {
57942
58151
  id: jobId,
@@ -58820,7 +59029,7 @@ var mcpToolDescriptions = {
58820
59029
  [mcpToolNames.closeTaskAsWontDo]: "Close a task as won't-do with a required human-readable reason recorded on the task.",
58821
59030
  [mcpToolNames.updateWikiPage]: "Update a markdown Wiki page.",
58822
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.",
58823
- [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.',
58824
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).`,
58825
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.",
58826
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.',
@@ -59512,7 +59721,7 @@ function createMrrlinTools(options) {
59512
59721
  "PRODUCED_URL_RECORD_FAILED",
59513
59722
  "Unable to record produced URL.",
59514
59723
  async (c, { projectSlug, runId, url: url2, label }) => {
59515
- 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")}`;
59516
59725
  const payload = label ? { url: url2, label } : { url: url2 };
59517
59726
  return await c.appendExecutionArtifact(projectSlug, runId, {
59518
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.1",
3
+ "version": "0.4.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mrrlin-mcp": "dist/bin.cjs"
@@ -21,9 +21,9 @@
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
25
  "@mrrlin/director-e2e": "0.0.0",
26
+ "@mrrlin/client": "0.0.0",
27
27
  "@mrrlin/schemas": "0.0.0",
28
28
  "@mrrlin/tsconfig": "0.0.0",
29
29
  "@mrrlin/wiki": "0.0.0"