@mrrlin-dev/mcp 0.4.21 → 0.4.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.cjs +383 -33
  2. package/package.json +4 -4
package/dist/bin.cjs CHANGED
@@ -20815,6 +20815,66 @@ function browserServeArgs(homeDir = import_node_os.default.homedir()) {
20815
20815
  env: { PLAYWRIGHT_BROWSERS_PATH: browsersPath }
20816
20816
  };
20817
20817
  }
20818
+ function defaultListProcesses() {
20819
+ const r = (0, import_node_child_process.spawnSync)("ps", ["-ww", "-eo", "pid,ppid,command"], { encoding: "utf8" });
20820
+ const lines2 = (r.stdout ?? "").split("\n").slice(1);
20821
+ const out = [];
20822
+ for (const line of lines2) {
20823
+ const m = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
20824
+ if (!m || m[1] === void 0 || m[2] === void 0 || m[3] === void 0) continue;
20825
+ out.push({ pid: Number(m[1]), ppid: Number(m[2]), command: m[3] });
20826
+ }
20827
+ return out;
20828
+ }
20829
+ function defaultIsAlive(pid) {
20830
+ try {
20831
+ process.kill(pid, 0);
20832
+ return true;
20833
+ } catch (err) {
20834
+ return err.code === "EPERM";
20835
+ }
20836
+ }
20837
+ function defaultKill(pid, signal) {
20838
+ try {
20839
+ process.kill(pid, signal);
20840
+ return true;
20841
+ } catch (err) {
20842
+ if (err.code === "ESRCH") return true;
20843
+ return false;
20844
+ }
20845
+ }
20846
+ function safeKill(kill, pid, signal) {
20847
+ try {
20848
+ return kill(pid, signal);
20849
+ } catch {
20850
+ return false;
20851
+ }
20852
+ }
20853
+ function reapOrphanedBrowserProcesses(opts = {}) {
20854
+ const homeDir = opts.homeDir ?? import_node_os.default.homedir();
20855
+ const { profilePath } = browserPaths(homeDir);
20856
+ const listProcesses = opts.listProcesses ?? defaultListProcesses;
20857
+ const isAlive = opts.isAlive ?? defaultIsAlive;
20858
+ const kill = opts.kill ?? defaultKill;
20859
+ const selfPid = opts.selfPid ?? process.pid;
20860
+ const allProcesses = listProcesses();
20861
+ const candidates = allProcesses.filter(
20862
+ (p) => p.pid !== selfPid && p.command.includes("@playwright/mcp") && p.command.includes(profilePath)
20863
+ );
20864
+ const killedPids = [];
20865
+ const killedChildPids = [];
20866
+ for (const proc of candidates) {
20867
+ if (!isAlive(proc.ppid)) {
20868
+ for (const child of allProcesses) {
20869
+ if (child.ppid === proc.pid) {
20870
+ if (safeKill(kill, child.pid, "SIGKILL")) killedChildPids.push(child.pid);
20871
+ }
20872
+ }
20873
+ if (safeKill(kill, proc.pid, "SIGTERM")) killedPids.push(proc.pid);
20874
+ }
20875
+ }
20876
+ return { checked: candidates.length, killedPids, killedChildPids };
20877
+ }
20818
20878
  function externalAgentsServeArgs() {
20819
20879
  return {
20820
20880
  command: process.execPath,
@@ -36811,6 +36871,7 @@ var openApiOperationIds = {
36811
36871
  retireThread: "retireThread",
36812
36872
  applyIntentVerdict: "applyIntentVerdict",
36813
36873
  armIntentCheck: "armIntentCheck",
36874
+ settleExecutionRunOutcome: "settleExecutionRunOutcome",
36814
36875
  // Run-scoped, model-facing half of the resume contract (D8). Distinct from upsertTaskState:
36815
36876
  // that one is the server's own write (whole record, explicit lastRunSeq), this one is the
36816
36877
  // executor reporting narrative only, with the ordering value derived server-side from the run.
@@ -37434,8 +37495,17 @@ var mrrlinExecutionArtifactKindSchema = external_exports.enum([
37434
37495
  "produced_url",
37435
37496
  "resume_spec",
37436
37497
  "deliverable",
37437
- "pull_request_state"
37498
+ "pull_request_state",
37499
+ "non_code_outcome"
37438
37500
  ]);
37501
+ var mrrlinNonCodeOutcomeSchema = external_exports.enum(["completed", "blocked_missing_input", "negative_result"]);
37502
+ var mrrlinIntentProvenanceRefSchema = external_exports.object({
37503
+ kind: external_exports.enum(["director_chat_message", "wiki_page"]),
37504
+ id: external_exports.string().min(1),
37505
+ role: external_exports.enum(["user", "assistant", "system"]).optional(),
37506
+ contentHash: external_exports.string().min(1).optional()
37507
+ });
37508
+ var mrrlinIntentProvenanceSchema = external_exports.array(mrrlinIntentProvenanceRefSchema).default([]);
37439
37509
  var mrrlinDeployAttemptPayloadSchema = external_exports.object({
37440
37510
  env: external_exports.enum(["dev", "prod"]),
37441
37511
  workflowId: external_exports.number().int(),
@@ -37541,6 +37611,8 @@ var mrrlinTaskSchema = external_exports.object({
37541
37611
  // Output-optional (not .default) so existing task fixtures need no churn; DB rows always resolve it
37542
37612
  // to `string | null` via taskRowToTask.
37543
37613
  operatorSeedRequest: external_exports.string().max(8e3).nullable().optional(),
37614
+ resolvedIntent: external_exports.string().min(1).max(4e3).nullable().optional(),
37615
+ intentProvenance: mrrlinIntentProvenanceSchema.nullable().optional(),
37544
37616
  planId: mrrlinPlanIdSchema.nullable(),
37545
37617
  specApproved: mrrlinSpecApprovedSchema.optional(),
37546
37618
  specWikiPageId: mrrlinWikiPageIdSchema.nullable(),
@@ -37566,6 +37638,8 @@ var mrrlinTaskSchema = external_exports.object({
37566
37638
  var mrrlinTaskSummarySchema = mrrlinTaskSchema.omit({
37567
37639
  notes: true,
37568
37640
  operatorSeedRequest: true,
37641
+ resolvedIntent: true,
37642
+ intentProvenance: true,
37569
37643
  intentRestatement: true,
37570
37644
  latestJudgement: true
37571
37645
  });
@@ -37776,6 +37850,8 @@ var mrrlinTaskCreateShapeSchema = external_exports.object({
37776
37850
  notes: external_exports.string().nullable().optional(),
37777
37851
  parentTaskId: mrrlinTaskIdSchema.nullable().optional(),
37778
37852
  operatorSeedRequest: external_exports.string().max(8e3).nullable().optional(),
37853
+ resolvedIntent: external_exports.string().min(1).max(4e3).nullable().optional(),
37854
+ intentProvenance: mrrlinIntentProvenanceSchema.nullable().optional(),
37779
37855
  planId: mrrlinPlanIdSchema.nullable().optional(),
37780
37856
  specWikiPageId: mrrlinWikiPageIdSchema.nullable().optional(),
37781
37857
  status: mrrlinTaskCreateStatusSchema.optional(),
@@ -38206,7 +38282,11 @@ var mrrlinExecutionRunSchema = external_exports.object({
38206
38282
  cachedInputTokens: external_exports.number().int().nullable(),
38207
38283
  outputTokens: external_exports.number().int().nullable(),
38208
38284
  effort: external_exports.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).nullable(),
38209
- effortEscalationReason: external_exports.string().min(1).nullable()
38285
+ effortEscalationReason: external_exports.string().min(1).nullable(),
38286
+ nonCodeOutcome: mrrlinNonCodeOutcomeSchema.nullable().default(null),
38287
+ resultSummary: external_exports.string().min(1).max(300).nullable().default(null),
38288
+ deliverablePageId: mrrlinWikiPageIdSchema.nullable().default(null),
38289
+ outcomeAcceptedAt: external_exports.string().datetime().nullable().default(null)
38210
38290
  });
38211
38291
  var mrrlinTaskStateSchema = external_exports.object({
38212
38292
  projectSlug: mrrlinProjectSlugSchema,
@@ -38348,6 +38428,13 @@ var mrrlinRestartExecutionRunSchema = external_exports.object({
38348
38428
  var mrrlinExecutionRunTouchSchema = external_exports.object({
38349
38429
  leaseId: external_exports.string().min(1)
38350
38430
  });
38431
+ var mrrlinSettleExecutionRunOutcomeSchema = external_exports.object({
38432
+ outcome: mrrlinNonCodeOutcomeSchema,
38433
+ resultSummary: external_exports.string().trim().min(1).max(300),
38434
+ deliverablePageId: mrrlinWikiPageIdSchema,
38435
+ contentHash: external_exports.string().min(1),
38436
+ leaseId: external_exports.string().min(1)
38437
+ });
38351
38438
  var mrrlinExecutionArtifactSchema = external_exports.object({
38352
38439
  createdAt: external_exports.string().datetime(),
38353
38440
  id: mrrlinExecutionArtifactIdSchema,
@@ -40845,6 +40932,20 @@ function createMrrlinClient(config2) {
40845
40932
  throw error51;
40846
40933
  }
40847
40934
  },
40935
+ async settleExecutionRunOutcome(projectSlug, runId, input) {
40936
+ try {
40937
+ const body = await request(`${projectPath(projectSlug)}/execution-runs/${encodeURIComponent(runId)}/settle-outcome`, {
40938
+ body: JSON.stringify(mrrlinSettleExecutionRunOutcomeSchema.parse(input)),
40939
+ headers: { "content-type": "application/json" },
40940
+ method: "POST"
40941
+ });
40942
+ return executionRunResponseSchema.parse(body).data;
40943
+ } catch (error51) {
40944
+ if (error51 instanceof MrrlinApiError && error51.status === 409)
40945
+ return null;
40946
+ throw error51;
40947
+ }
40948
+ },
40848
40949
  async getTaskState(projectSlug, taskId) {
40849
40950
  const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/state`);
40850
40951
  return taskStateResponseSchema.parse(body).data;
@@ -41623,7 +41724,7 @@ Do NOT derail the current task - if the operator has an in-flight question, answ
41623
41724
  }
41624
41725
 
41625
41726
  // src/_generated/version.ts
41626
- var PKG_VERSION = "0.4.21";
41727
+ var PKG_VERSION = "0.4.23";
41627
41728
 
41628
41729
  // src/api-base-url-migrate.ts
41629
41730
  var import_node_fs3 = __toESM(require("node:fs"), 1);
@@ -43870,7 +43971,7 @@ function renderCriteriaBlock(criteria) {
43870
43971
  return criteria.map((c, i) => `${i + 1}. [ ] ${c}`).join("\n");
43871
43972
  }
43872
43973
  async function buildDeliverableGateDeps(args) {
43873
- const { client, projectSlug, deliverablePageId, cwd, codexExecutable, runId, seat } = args;
43974
+ const { client, projectSlug, deliverablePageId, declaredOutcome, cwd, codexExecutable, runId, seat } = args;
43874
43975
  const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
43875
43976
  const timeoutMs = Number(process.env["MRRLIN_CONSENSUS_HEAD_TIMEOUT_MS"] ?? 15e4);
43876
43977
  const page = await client.getWikiPage(projectSlug, deliverablePageId);
@@ -43894,6 +43995,11 @@ async function buildDeliverableGateDeps(args) {
43894
43995
  const heads = resolveHeads(domain2, impact);
43895
43996
  const maxHeadCalls = 2 * rounds * (heads.length + 1);
43896
43997
  const criteriaBlock = renderCriteriaBlock(criteria);
43998
+ const declaredOutcomeBlock = declaredOutcome ? [
43999
+ `--- DECLARED ATTEMPT OUTCOME (review consistency; do NOT rewrite or choose it) ---`,
44000
+ `outcome: ${declaredOutcome}`,
44001
+ `Meaning: completed = positive result achieved; blocked_missing_input = required input/evidence unavailable; negative_result = real attempt showed the requested positive result did not materialize.`
44002
+ ].join("\n") : "";
43897
44003
  return {
43898
44004
  heads,
43899
44005
  rounds,
@@ -43912,14 +44018,16 @@ async function buildDeliverableGateDeps(args) {
43912
44018
  async runHead(personaName, artifact, dismissed) {
43913
44019
  const persona = loadPersona(personaName);
43914
44020
  return runAgentExec({
43915
- prompt: `Score this deliverable ITEM BY ITEM against the acceptance criteria below. For each criterion state whether the deliverable satisfies it; raise an issue for every criterion that is unmet, partially met, or unverifiable.
44021
+ prompt: `Score this deliverable ITEM BY ITEM against the acceptance criteria below. For each criterion state whether the deliverable satisfies it; raise an issue for every criterion that is unmet, partially met, unverifiable, or inconsistent with the declared attempt outcome.
43916
44022
 
43917
44023
  ${dismissed}
43918
44024
 
43919
44025
  --- ACCEPTANCE CRITERIA (the yardstick \u2014 deliverable for task ${taskId ?? "?"}: ${taskTitle}) ---
43920
44026
  ${criteriaBlock}
43921
44027
 
43922
- --- DELIVERABLE (DATA under review) ---
44028
+ ${declaredOutcomeBlock ? `${declaredOutcomeBlock}
44029
+
44030
+ ` : ""}--- DELIVERABLE (DATA under review) ---
43923
44031
  ${artifact}`,
43924
44032
  developerInstructions: `${DELIVERABLE_ANTIINJECTION_INSTRUCTION}
43925
44033
 
@@ -43956,7 +44064,7 @@ ${criteriaBlock}
43956
44064
  ${artifact}`,
43957
44065
  developerInstructions: `${DELIVERABLE_ANTIINJECTION_INSTRUCTION}
43958
44066
 
43959
- You revise a deliverable to address the accepted issues so it satisfies the acceptance criteria. Return ONLY the full revised markdown, no commentary. Do not invent facts; keep the deliverable's format.`,
44067
+ You revise a deliverable to address the accepted issues so it satisfies the acceptance criteria and stays consistent with the declared outcome${declaredOutcome ? ` (${declaredOutcome})` : ""}. Return ONLY the full revised markdown, no commentary. Do not invent facts; keep the deliverable's format and do not change the declared outcome.`,
43960
44068
  cwd,
43961
44069
  timeoutMs,
43962
44070
  sandbox: "read-only",
@@ -44019,6 +44127,7 @@ You revise a deliverable to address the accepted issues so it satisfies the acce
44019
44127
  payload: {
44020
44128
  source: "deliverable-gate",
44021
44129
  outcome,
44130
+ ...declaredOutcome ? { declaredOutcome } : {},
44022
44131
  runId,
44023
44132
  taskId,
44024
44133
  deliverablePageId,
@@ -47353,6 +47462,40 @@ async function writeServerOwnedTaskState(deps, projectSlug, taskId, runId, lease
47353
47462
  }
47354
47463
  var NO_FILE_CHANGES_THIS_RUN = "no file changes recorded in this run";
47355
47464
  var NO_VERIFICATION_THIS_RUN = "not run in this run";
47465
+ var NON_CODE_RESULT_OPEN = "<<<MRRLIN_NON_CODE_RESULT_V1";
47466
+ var NON_CODE_RESULT_CLOSE = "MRRLIN_NON_CODE_RESULT_V1>>>";
47467
+ var NEGATIVE_RESULT_EVIDENCE_FOLDER = "Evidence";
47468
+ var NEGATIVE_RESULT_EVIDENCE_TITLE_PREFIX = "Negative result evidence for ";
47469
+ function parseNonCodeResultEnvelope(stdout) {
47470
+ const text = stdout.startsWith("\uFEFF") ? stdout.slice(1) : stdout;
47471
+ const lines2 = text.split(/\r?\n/);
47472
+ if (lines2[0] !== NON_CODE_RESULT_OPEN) {
47473
+ throw new Error("missing MRRLIN_NON_CODE_RESULT_V1 opening delimiter on the first non-BOM line");
47474
+ }
47475
+ if (!lines2[1] || lines2[2] !== NON_CODE_RESULT_CLOSE) {
47476
+ throw new Error("malformed MRRLIN_NON_CODE_RESULT_V1 envelope");
47477
+ }
47478
+ let payload;
47479
+ try {
47480
+ payload = JSON.parse(lines2[1]);
47481
+ } catch (error51) {
47482
+ throw new Error(`invalid MRRLIN_NON_CODE_RESULT_V1 JSON: ${error51 instanceof Error ? error51.message : String(error51)}`);
47483
+ }
47484
+ const record2 = payload;
47485
+ const outcome = mrrlinNonCodeOutcomeSchema.parse(record2.outcome);
47486
+ if (typeof record2.resultSummary !== "string") throw new Error("resultSummary must be a string");
47487
+ const resultSummary = record2.resultSummary.trim();
47488
+ if (resultSummary.length < 1 || resultSummary.length > 300) {
47489
+ throw new Error("resultSummary must be 1-300 characters");
47490
+ }
47491
+ const bodyLines = lines2.slice(3);
47492
+ if (bodyLines[0] === "") bodyLines.shift();
47493
+ const markdownBody = bodyLines.join("\n").trim();
47494
+ if (markdownBody.length === 0) {
47495
+ throw new Error("markdown deliverable body must be non-empty");
47496
+ }
47497
+ return { outcome, resultSummary, markdownBody };
47498
+ }
47356
47499
  function buildDeliverableAuthorPrompt(task, specMarkdown) {
47357
47500
  return [
47358
47501
  `Produce the deliverable for the task below. Write the FULL deliverable content as markdown.`,
@@ -47369,8 +47512,21 @@ function buildDeliverableAuthorPrompt(task, specMarkdown) {
47369
47512
  specMarkdown,
47370
47513
  ``,
47371
47514
  `--- OUTPUT CONTRACT (strict) ---`,
47372
- `Return ONLY the finished deliverable as markdown, no commentary or meta-notes. Satisfy every`,
47373
- `item in the spec's "## Acceptance criteria" checklist.`
47515
+ `Return ONLY this exact envelope followed by the markdown deliverable body:`,
47516
+ ``,
47517
+ NON_CODE_RESULT_OPEN,
47518
+ `{"outcome":"completed","resultSummary":"One concise sentence, 1-300 chars."}`,
47519
+ NON_CODE_RESULT_CLOSE,
47520
+ ``,
47521
+ `<markdown deliverable body>`,
47522
+ ``,
47523
+ `Choose exactly one outcome:`,
47524
+ `- completed: the requested positive result was achieved.`,
47525
+ `- blocked_missing_input: required input/evidence is unavailable, so the work remains actionable.`,
47526
+ `- negative_result: the work was actually attempted and valid evidence shows the requested positive result did not materialize.`,
47527
+ ``,
47528
+ `The markdown body must justify the declared outcome and satisfy every applicable item in the spec's`,
47529
+ `"## Acceptance criteria" checklist. Do not add commentary before the opening delimiter.`
47374
47530
  ].join("\n");
47375
47531
  }
47376
47532
  async function writeDeliverablePage(client, projectSlug, task, markdown) {
@@ -47407,6 +47563,50 @@ async function writeDeliverablePage(client, projectSlug, task, markdown) {
47407
47563
  }
47408
47564
  throw new Error(`could not allocate a deliverable page id for ${task.id} (last conflict: ${lastConflict})`);
47409
47565
  }
47566
+ async function writeNegativeResultEvidencePage(client, projectSlug, task, input) {
47567
+ const evidencePath = `/evidence/${task.id.toLowerCase()}-non-code-result`;
47568
+ const title = `${NEGATIVE_RESULT_EVIDENCE_TITLE_PREFIX}${task.id}`;
47569
+ const markdownBody = [
47570
+ `# ${title}`,
47571
+ ``,
47572
+ `Outcome: negative_result`,
47573
+ ``,
47574
+ `Summary: ${input.resultSummary}`,
47575
+ ``,
47576
+ `Deliverable: Deliverables/${input.deliverablePageId}`
47577
+ ].join("\n");
47578
+ const existing = (await client.listWikiPages(projectSlug, { folder: NEGATIVE_RESULT_EVIDENCE_FOLDER, taskId: task.id })).find((page) => page.path === evidencePath);
47579
+ if (existing) {
47580
+ await client.updateWikiPage(projectSlug, existing.id, {
47581
+ markdownBody,
47582
+ taskId: task.id,
47583
+ title
47584
+ });
47585
+ return;
47586
+ }
47587
+ let lastConflict = "";
47588
+ for (let attempt = 1; attempt <= DELIVERABLE_PAGE_ID_MAX_ATTEMPTS; attempt += 1) {
47589
+ const candidate = nextWikiPageId(await client.listWikiPages(projectSlug));
47590
+ try {
47591
+ await client.createWikiPage(projectSlug, {
47592
+ id: candidate,
47593
+ folder: NEGATIVE_RESULT_EVIDENCE_FOLDER,
47594
+ path: evidencePath,
47595
+ title,
47596
+ markdownBody,
47597
+ taskId: task.id
47598
+ });
47599
+ return;
47600
+ } catch (error51) {
47601
+ if (error51 instanceof MrrlinApiError && error51.status === 409 && attempt < DELIVERABLE_PAGE_ID_MAX_ATTEMPTS) {
47602
+ lastConflict = error51.message;
47603
+ continue;
47604
+ }
47605
+ throw error51;
47606
+ }
47607
+ }
47608
+ throw new Error(`could not allocate negative-result evidence page for ${task.id} (last conflict: ${lastConflict})`);
47609
+ }
47410
47610
  async function driveNonCodeRun(deps, projectSlug, runId, leaseId, task, domain2) {
47411
47611
  const personaName = resolveProducerPersona(domain2);
47412
47612
  if (!personaName) {
@@ -47466,7 +47666,7 @@ async function driveNonCodeRun(deps, projectSlug, runId, leaseId, task, domain2)
47466
47666
  });
47467
47667
  }, deps.touchIntervalMs);
47468
47668
  touchTimer.unref?.();
47469
- let deliverableMarkdown;
47669
+ let parsedResult;
47470
47670
  try {
47471
47671
  const exec = await runExec({
47472
47672
  prompt: buildDeliverableAuthorPrompt(task, specMarkdown),
@@ -47480,18 +47680,26 @@ async function driveNonCodeRun(deps, projectSlug, runId, leaseId, task, domain2)
47480
47680
  await failRun(deps, projectSlug, task, runId, leaseId, `deliverable_authoring_failed: ${exec.error}`);
47481
47681
  return;
47482
47682
  }
47483
- if (exec.text.trim().length === 0) {
47484
- await failRun(deps, projectSlug, task, runId, leaseId, "deliverable_authoring_empty: producer returned no content.");
47683
+ try {
47684
+ parsedResult = parseNonCodeResultEnvelope(exec.text);
47685
+ } catch (error51) {
47686
+ await failRun(
47687
+ deps,
47688
+ projectSlug,
47689
+ task,
47690
+ runId,
47691
+ leaseId,
47692
+ `non_code_result_envelope_invalid: ${error51 instanceof Error ? error51.message : String(error51)}`
47693
+ );
47485
47694
  return;
47486
47695
  }
47487
- deliverableMarkdown = exec.text.trim();
47488
47696
  } finally {
47489
47697
  clearInterval(touchTimer);
47490
47698
  }
47491
47699
  let pageId;
47492
47700
  let contentHash;
47493
47701
  try {
47494
- ({ pageId, contentHash } = await writeDeliverablePage(deps.client, projectSlug, task, deliverableMarkdown));
47702
+ ({ pageId, contentHash } = await writeDeliverablePage(deps.client, projectSlug, task, parsedResult.markdownBody));
47495
47703
  } catch (error51) {
47496
47704
  await failRun(
47497
47705
  deps,
@@ -47522,7 +47730,13 @@ async function driveNonCodeRun(deps, projectSlug, runId, leaseId, task, domain2)
47522
47730
  }
47523
47731
  let gateResult;
47524
47732
  try {
47525
- gateResult = await deps.runDeliverableGate({ projectSlug, taskId: task.id, runId, deliverablePageId: pageId });
47733
+ gateResult = await deps.runDeliverableGate({
47734
+ projectSlug,
47735
+ taskId: task.id,
47736
+ runId,
47737
+ deliverablePageId: pageId,
47738
+ declaredOutcome: parsedResult.outcome
47739
+ });
47526
47740
  } catch (error51) {
47527
47741
  await failRun(
47528
47742
  deps,
@@ -47546,14 +47760,52 @@ async function driveNonCodeRun(deps, projectSlug, runId, leaseId, task, domain2)
47546
47760
  );
47547
47761
  return;
47548
47762
  }
47549
- if (!await runHasLease(deps, projectSlug, runId, leaseId)) return;
47550
- await deps.client.updateTask(projectSlug, task.id, { status: "closed_done" }).catch(() => {
47551
- });
47552
- await deps.client.updateExecutionRun(projectSlug, runId, {
47553
- status: "done",
47554
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
47555
- leaseId
47556
- });
47763
+ if (parsedResult.outcome === "negative_result") {
47764
+ try {
47765
+ await writeNegativeResultEvidencePage(deps.client, projectSlug, task, {
47766
+ deliverablePageId: pageId,
47767
+ resultSummary: parsedResult.resultSummary
47768
+ });
47769
+ } catch (error51) {
47770
+ await failRun(
47771
+ deps,
47772
+ projectSlug,
47773
+ task,
47774
+ runId,
47775
+ leaseId,
47776
+ `negative_result_evidence_write_failed: ${error51 instanceof Error ? error51.message : String(error51)}`
47777
+ );
47778
+ return;
47779
+ }
47780
+ }
47781
+ try {
47782
+ const settled = await deps.client.settleExecutionRunOutcome(projectSlug, runId, {
47783
+ leaseId,
47784
+ outcome: parsedResult.outcome,
47785
+ resultSummary: parsedResult.resultSummary,
47786
+ deliverablePageId: pageId,
47787
+ contentHash
47788
+ });
47789
+ if (!settled && await runHasLease(deps, projectSlug, runId, leaseId)) {
47790
+ await failRun(
47791
+ deps,
47792
+ projectSlug,
47793
+ task,
47794
+ runId,
47795
+ leaseId,
47796
+ `non_code_outcome_settlement_conflict: settlement was rejected while this consumer still held the lease.`
47797
+ );
47798
+ }
47799
+ } catch (error51) {
47800
+ await failRun(
47801
+ deps,
47802
+ projectSlug,
47803
+ task,
47804
+ runId,
47805
+ leaseId,
47806
+ `non_code_outcome_settlement_failed: ${error51 instanceof Error ? error51.message : String(error51)}`
47807
+ );
47808
+ }
47557
47809
  }
47558
47810
  async function resetWedgedThread(deps, projectSlug, taskId, interruptFailed) {
47559
47811
  if (!interruptFailed) return;
@@ -48065,6 +48317,22 @@ function createBridgeLogger(stateDir, env = process.env) {
48065
48317
  }
48066
48318
  const includePrompts = resolvePromptsEnabled(env);
48067
48319
  const fileFor = () => (0, import_node_path16.join)(logsDir, `bridge-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.jsonl`);
48320
+ const MAX_TRACKED_SESSIONS = 64;
48321
+ const contextSlugBySession = /* @__PURE__ */ new Map();
48322
+ const rememberContextSlug = (sessionId, payload) => {
48323
+ if (!payload || typeof payload !== "object") return;
48324
+ const ctx = payload.context;
48325
+ if (!ctx || typeof ctx !== "object") return;
48326
+ const raw = ctx.projectSlug;
48327
+ const slug = typeof raw === "string" ? raw.trim() : "";
48328
+ if (!slug) return;
48329
+ contextSlugBySession.delete(sessionId);
48330
+ contextSlugBySession.set(sessionId, slug);
48331
+ if (contextSlugBySession.size > MAX_TRACKED_SESSIONS) {
48332
+ const oldest = contextSlugBySession.keys().next();
48333
+ if (!oldest.done) contextSlugBySession.delete(oldest.value);
48334
+ }
48335
+ };
48068
48336
  const gainDay = (sessionId) => {
48069
48337
  const sessionsPath = (0, import_node_path16.join)(logsDir, ".sessions.json");
48070
48338
  let sessionsObj = {};
@@ -48094,6 +48362,23 @@ function createBridgeLogger(stateDir, env = process.env) {
48094
48362
  projectionFailed = true;
48095
48363
  }
48096
48364
  }
48365
+ if (rec.sessionId !== null && rec.type === "turn") rememberContextSlug(rec.sessionId, rec.payload);
48366
+ let projectScopeMismatch = null;
48367
+ if (rec.sessionId !== null && projection?.kind === "tool_use" && typeof projection.projectSlug === "string") {
48368
+ const expected = contextSlugBySession.get(rec.sessionId);
48369
+ if (expected !== void 0 && expected !== projection.projectSlug) {
48370
+ projectScopeMismatch = { expected, actual: projection.projectSlug };
48371
+ console.warn(
48372
+ JSON.stringify({
48373
+ event: "bridge.project_scope_mismatch",
48374
+ sessionId: rec.sessionId,
48375
+ toolName: projection.toolName,
48376
+ expected,
48377
+ actual: projection.projectSlug
48378
+ })
48379
+ );
48380
+ }
48381
+ }
48097
48382
  const { payload: redactedPayload, redactedCount } = redactMessage(rec.payload, { includePrompts });
48098
48383
  const line = JSON.stringify({
48099
48384
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -48105,7 +48390,8 @@ function createBridgeLogger(stateDir, env = process.env) {
48105
48390
  payload: redactedPayload,
48106
48391
  redactedCount,
48107
48392
  projection,
48108
- ...projectionFailed ? { projectionFailed: true } : {}
48393
+ ...projectionFailed ? { projectionFailed: true } : {},
48394
+ ...projectScopeMismatch ? { projectScopeMismatch } : {}
48109
48395
  }) + "\n";
48110
48396
  (0, import_node_fs8.appendFileSync)(fileFor(), line);
48111
48397
  if (rec.sessionId !== null) gainDay(rec.sessionId);
@@ -48302,7 +48588,7 @@ async function runAsyncBackstopSweep(deps, drive = (slug, jobId) => driveAsyncJo
48302
48588
  }
48303
48589
 
48304
48590
  // src/consensus/deliverable-gate.ts
48305
- async function runDeliverableGate(_target, deps) {
48591
+ async function runDeliverableGate(target, deps) {
48306
48592
  if (!await deps.consensusEnabled()) return { outcome: "SKIPPED" };
48307
48593
  const ac = await deps.readAcceptanceCriteria();
48308
48594
  if (!ac || ac.criteria.length === 0) return { outcome: "MISSING_ACCEPTANCE_CRITERIA" };
@@ -48350,6 +48636,7 @@ async function runDeliverableGate(_target, deps) {
48350
48636
  }
48351
48637
  await deps.writeEvidence({
48352
48638
  deliverablePageId: deliverable.pageId,
48639
+ ...target.declaredOutcome ? { declaredOutcome: target.declaredOutcome } : {},
48353
48640
  outcome: result.outcome,
48354
48641
  rounds: result.rounds,
48355
48642
  history: result.history,
@@ -53781,6 +54068,18 @@ function buildThreadStartParams(msg, config2, desiredModel) {
53781
54068
  "order, separated by a blank line. Do NOT paraphrase, summarise, or rewrite any of it, and do",
53782
54069
  "NOT add words of your own.",
53783
54070
  "",
54071
+ "Set resolvedIntent to the accepted task intent in your own concise words only after the outcome",
54072
+ "is resolved enough to create the task. Use it to capture the actionable understanding that came",
54073
+ "from the conversation and, when needed, retrieved wiki context. Do NOT use resolvedIntent as a",
54074
+ "replacement for operatorSeedRequest: the seed stays raw operator wording; resolvedIntent is the",
54075
+ "structured interpretation.",
54076
+ "",
54077
+ "When the turn Context JSON includes contextMessages, cite the messages you actually used in",
54078
+ 'intentProvenance as refs like {kind:"director_chat_message",id:"...",role:"user"}. If a wiki',
54079
+ 'page resolved part of the intent, cite it as {kind:"wiki_page",id:"WK-..."}. Do not put copied',
54080
+ "message text in intentProvenance. Leave it unset when there is no accepted resolved intent; use",
54081
+ "an explicit empty array only when you intentionally verified there are no useful references.",
54082
+ "",
53784
54083
  "If a task has no real operator request behind it at all (pure system bookkeeping nobody asked",
53785
54084
  "for), do NOT invent one to fill this field \u2014 leave operatorSeedRequest unset. Never fabricate",
53786
54085
  "operator wording; an unset seed is handled downstream, a fabricated one is not.",
@@ -53813,6 +54112,7 @@ function buildThreadStartParams(msg, config2, desiredModel) {
53813
54112
  "- 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.",
53814
54113
  '- 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.',
53815
54114
  "- 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.",
54115
+ "- Exactly one browser session exists for this operator, shared across every concurrent chat and task on their box \u2014 a deliberate tradeoff, not a bug, so a second concurrent user of the browser is expected, not exceptional. If a `mrrlin-browser` call fails with `Browser is already in use for ...`, that means another one of the operator's own chats or tasks is driving the browser RIGHT NOW. Do not present this as a broken tool or a mysterious error: tell the operator plainly that the browser is busy with another task, ask them to wait for it to finish (or close it), and offer to retry shortly. Never retry-loop this silently.",
53816
54116
  "",
53817
54117
  'Research playbook \u2014 when the user asks about a topic, an area, or "what do we have on X":',
53818
54118
  "- Tasks are the source of truth. Start with `list_tasks` using `q=<keyword>`; the filter is a",
@@ -53907,6 +54207,14 @@ function buildThreadStartParams(msg, config2, desiredModel) {
53907
54207
  "- A failed fetch is a signal to change tool, never a finding. NEVER report that you could not",
53908
54208
  " check, verify, or reach a publicly reachable target until you have actually tried the",
53909
54209
  " browser on it in this turn.",
54210
+ "- An internal Mrrlin route (`/deliverables/...`, `/wiki/...`, `/specs/...`, `/items/...`,",
54211
+ " `/memory/...`) is NEVER a browser target, even when it appears as a literal URL/path in a",
54212
+ " message or a screenshot. Resolve it through the typed tools instead: `list_wiki_pages` (by",
54213
+ " `folder`/`q`) or `search_wiki_pages` to find the page, then `get_wiki_page` for its content;",
54214
+ " `get_task`/`list_tasks` for `/items/<id>`. Opening these in `mrrlin-browser` is slower, can",
54215
+ " hit an operator login/consent wall the automation cannot clear on its own, and needlessly",
54216
+ " occupies the one shared browser session. Only reach for the browser on a Mrrlin route after a",
54217
+ " typed tool call for it has actually failed.",
53910
54218
  "",
53911
54219
  "Initiative contract \u2014 do the obvious next step instead of offering it:",
53912
54220
  "- When a next step is cheap, clearly wanted, and finishable in this turn, just do it and",
@@ -55771,16 +56079,17 @@ async function startDirectorBridge() {
55771
56079
  // Builds the gate deps (resolving domain/heads/criteria from the page's task + spec) and runs the
55772
56080
  // same convergence loop the spec-gate uses. runId is threaded for verdict provenance (decision 10).
55773
56081
  // Authoring uses the defaults (runAgentExec + loadPersona) — no extra wiring needed for those.
55774
- runDeliverableGate: async ({ projectSlug, runId, deliverablePageId }) => {
56082
+ runDeliverableGate: async ({ projectSlug, runId, deliverablePageId, declaredOutcome }) => {
55775
56083
  const gateDeps = await buildDeliverableGateDeps({
55776
56084
  client,
55777
56085
  projectSlug,
55778
56086
  deliverablePageId,
56087
+ declaredOutcome,
55779
56088
  cwd: config2.codexCwd ?? process.cwd(),
55780
56089
  codexExecutable: config2.codexExecutable,
55781
56090
  runId
55782
56091
  });
55783
- return runDeliverableGate({ projectSlug, deliverablePageId }, gateDeps);
56092
+ return runDeliverableGate({ projectSlug, deliverablePageId, declaredOutcome }, gateDeps);
55784
56093
  }
55785
56094
  };
55786
56095
  const sessionSinks = createSessionSinkRegistry();
@@ -56548,12 +56857,16 @@ function renderContextPrefix(context) {
56548
56857
  const urlPath = context?.urlPath?.trim() ? context.urlPath.trim() : void 0;
56549
56858
  const focusTaskId = context?.focusTaskId?.trim() ? context.focusTaskId.trim() : void 0;
56550
56859
  const directorSessionId = context?.directorSessionId?.trim() ? context.directorSessionId.trim() : void 0;
56860
+ const contextMessages = context?.contextMessages?.filter(
56861
+ (message) => (message.role === "user" || message.role === "assistant") && typeof message.id === "string" && message.id.trim() && typeof message.content === "string" && message.content.length > 0
56862
+ );
56551
56863
  const answeredQuestions = context?.answeredQuestions?.length ? context.answeredQuestions : void 0;
56552
56864
  const payload = {};
56553
56865
  if (projectSlug) payload.projectSlug = projectSlug;
56554
56866
  if (urlPath) payload.urlPath = urlPath;
56555
56867
  if (focusTaskId) payload.focusTaskId = focusTaskId;
56556
56868
  if (directorSessionId) payload.directorSessionId = directorSessionId;
56869
+ if (contextMessages?.length) payload.contextMessages = contextMessages;
56557
56870
  if (answeredQuestions) payload.answeredQuestions = answeredQuestions;
56558
56871
  const keys = Object.keys(payload);
56559
56872
  if (keys.length === 0) return "";
@@ -62871,6 +63184,31 @@ function makeReadTool(client, name, fallbackCode, fallbackMessage, handler, proj
62871
63184
  }
62872
63185
  };
62873
63186
  }
63187
+ var agentProviderCache = null;
63188
+ var AGENT_PROVIDER_CACHE_TTL_MS = 3e4;
63189
+ async function getAgentProviderForPanel(client, opts = {}) {
63190
+ const now = (opts.now ?? (() => Date.now()))();
63191
+ if (agentProviderCache !== null && now - agentProviderCache.fetchedAt < AGENT_PROVIDER_CACHE_TTL_MS) {
63192
+ return agentProviderCache.value;
63193
+ }
63194
+ try {
63195
+ const settings = await client.getOperatorSettings();
63196
+ agentProviderCache = { value: settings.agentProvider, fetchedAt: now };
63197
+ return settings.agentProvider;
63198
+ } catch (err) {
63199
+ console.warn(
63200
+ JSON.stringify({
63201
+ event: "agent_provider_for_panel_fetch_failed",
63202
+ hadCache: agentProviderCache !== null,
63203
+ error: err instanceof Error ? err.message : String(err)
63204
+ })
63205
+ );
63206
+ if (agentProviderCache !== null) {
63207
+ return agentProviderCache.value;
63208
+ }
63209
+ return null;
63210
+ }
63211
+ }
62874
63212
  function createMrrlinTools(options) {
62875
63213
  const client = createMrrlinClient(withAuth(options));
62876
63214
  async function uploadArtifactBytes(c, args) {
@@ -63337,13 +63675,17 @@ function createMrrlinTools(options) {
63337
63675
  codexExecutable: process.env.MRRLIN_DIRECTOR_BRIDGE_CODEX_BIN?.trim() || void 0,
63338
63676
  extraHeads
63339
63677
  });
63340
- const activeCodeRunHead = await resolveActiveRunHead({
63341
- heads: ["code-reviewer", ...extraHeads ?? []],
63342
- persistedProvider: getSelectedProvider()
63343
- });
63678
+ const persistedProvider = await getAgentProviderForPanel(c);
63679
+ let activeCodeRunHead = null;
63680
+ if (persistedProvider != null) {
63681
+ activeCodeRunHead = await resolveActiveRunHead({
63682
+ heads: ["code-reviewer", ...extraHeads ?? []],
63683
+ persistedProvider
63684
+ });
63685
+ }
63344
63686
  if (activeCodeRunHead) deps.runHead = activeCodeRunHead;
63345
63687
  const codeGateResult = await runCodeGate({ projectSlug, baseRef }, deps);
63346
- if (process.env[SHADOW_ENV_FLAG]) {
63688
+ if (process.env[SHADOW_ENV_FLAG] && persistedProvider != null) {
63347
63689
  try {
63348
63690
  const diff = await readDiff({ cwd: process.cwd(), baseRef, run: defaultRunCmd });
63349
63691
  await maybeRunShadow({
@@ -63351,7 +63693,7 @@ function createMrrlinTools(options) {
63351
63693
  artifact: diff,
63352
63694
  heads: ["code-reviewer", ...extraHeads ?? []],
63353
63695
  correlationId: `code:${baseRef}`,
63354
- persistedProvider: getSelectedProvider()
63696
+ persistedProvider
63355
63697
  });
63356
63698
  } catch {
63357
63699
  }
@@ -64347,6 +64689,14 @@ async function main() {
64347
64689
  return;
64348
64690
  }
64349
64691
  case "browser-serve": {
64692
+ const reaped = reapOrphanedBrowserProcesses();
64693
+ if (reaped.killedPids.length > 0) {
64694
+ const childNote = reaped.killedChildPids.length > 0 ? ` (plus ${reaped.killedChildPids.length} browser child process(es) they had already opened: ${reaped.killedChildPids.join(", ")})` : "";
64695
+ process.stderr.write(
64696
+ `[mrrlin-mcp browser-serve] reaped ${reaped.killedPids.length} orphaned browser process(es) with a dead parent: ${reaped.killedPids.join(", ")}${childNote}
64697
+ `
64698
+ );
64699
+ }
64350
64700
  const { command: command2, args, env } = browserServeArgs();
64351
64701
  const r = (0, import_node_child_process18.spawnSync)(command2, args, { stdio: "inherit", env: { ...process.env, ...env } });
64352
64702
  process.exitCode = r.status ?? 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.4.21",
3
+ "version": "0.4.23",
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/agent-client": "0.0.0",
25
24
  "@mrrlin/client": "0.0.0",
25
+ "@mrrlin/agent-client": "0.0.0",
26
26
  "@mrrlin/director-e2e": "0.0.0",
27
+ "@mrrlin/schemas": "0.0.0",
27
28
  "@mrrlin/tsconfig": "0.0.0",
28
- "@mrrlin/wiki": "0.0.0",
29
- "@mrrlin/schemas": "0.0.0"
29
+ "@mrrlin/wiki": "0.0.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "@iarna/toml": "^2.2.5",