@mrrlin-dev/mcp 0.3.45 → 0.3.47

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 +96 -27
  2. package/package.json +3 -3
package/dist/bin.cjs CHANGED
@@ -36550,6 +36550,7 @@ var mrrlinAsyncJobSchema = external_exports.object({
36550
36550
  failureReason: mrrlinAsyncJobFailureReasonSchema.nullable().default(null),
36551
36551
  requestedHash: external_exports.string().nullable().default(null),
36552
36552
  finalReviewedHash: external_exports.string().nullable().default(null),
36553
+ supersededAt: external_exports.string().nullable().default(null),
36553
36554
  attemptCount: external_exports.number().int().nonnegative(),
36554
36555
  leaseId: external_exports.string().nullable().default(null),
36555
36556
  claimedAt: external_exports.string().nullable().default(null),
@@ -36714,7 +36715,7 @@ var mrrlinDeliverablePayloadSchema = external_exports.object({
36714
36715
  contentHash: external_exports.string().min(1),
36715
36716
  format: external_exports.enum(["markdown"])
36716
36717
  });
36717
- var SUPPORTED_EXECUTION_DOMAINS = ["code"];
36718
+ var SUPPORTED_EXECUTION_DOMAINS = ["code", "seo"];
36718
36719
  function isExecutionDomainSupported(domain2) {
36719
36720
  return SUPPORTED_EXECUTION_DOMAINS.includes(domain2);
36720
36721
  }
@@ -37510,7 +37511,8 @@ var mrrlinProjectSchedulerPolicySchema = external_exports.object({
37510
37511
  prodGraceSeconds: external_exports.number().int().nonnegative(),
37511
37512
  projectSlug: mrrlinProjectSlugSchema,
37512
37513
  updatedAt: external_exports.string().datetime(),
37513
- wakeFloor: external_exports.string().datetime().nullable()
37514
+ wakeFloor: external_exports.string().datetime().nullable(),
37515
+ maxConsecutiveJobFailures: external_exports.number().int().positive()
37514
37516
  });
37515
37517
  var mrrlinProjectSchedulerPolicyUpsertSchema = external_exports.object({
37516
37518
  autoSpecApproval: mrrlinAutoSpecApprovalSchema.optional(),
@@ -37519,7 +37521,8 @@ var mrrlinProjectSchedulerPolicyUpsertSchema = external_exports.object({
37519
37521
  eligibleAutonomyLevels: external_exports.array(mrrlinAutonomyLevelSchema).min(1).optional(),
37520
37522
  maxConcurrentAutonomousRuns: external_exports.number().int().nonnegative().optional(),
37521
37523
  prodGraceSeconds: external_exports.number().int().nonnegative().optional(),
37522
- wakeFloor: external_exports.string().datetime().nullable().optional()
37524
+ wakeFloor: external_exports.string().datetime().nullable().optional(),
37525
+ maxConsecutiveJobFailures: external_exports.number().int().positive().optional()
37523
37526
  }).refine((value) => Object.keys(value).length > 0, {
37524
37527
  message: "ProjectSchedulerPolicyUpsert requires at least one field"
37525
37528
  });
@@ -37680,7 +37683,10 @@ var mrrlinInboxItemActionPayloadSchema = external_exports.object({
37680
37683
  runId: mrrlinExecutionRunIdSchema,
37681
37684
  // Optional (additive): when true, approving the proposal creates a full code-execution run
37682
37685
  // (worktree + push/PR path). Scheduler proposals set it; legacy payloads omit it.
37683
- codeExecution: external_exports.boolean().optional()
37686
+ codeExecution: external_exports.boolean().optional(),
37687
+ // Optional scheduler recovery action carried by an info item.
37688
+ retryToolName: external_exports.string().min(1).optional(),
37689
+ retryDedupKey: external_exports.string().min(1).optional()
37684
37690
  });
37685
37691
  var mrrlinInboxQuestionOptionSchema = external_exports.object({
37686
37692
  answer: external_exports.string().trim().min(1),
@@ -38401,6 +38407,7 @@ function createMrrlinClient(config2) {
38401
38407
  },
38402
38408
  async listInboxItems(projectSlug, filters = {}) {
38403
38409
  const url2 = appendQuery(`${projectPath(projectSlug)}/inbox`, {
38410
+ groupId: filters.groupId,
38404
38411
  kind: filters.kind,
38405
38412
  readState: filters.readState,
38406
38413
  status: filters.status,
@@ -39110,7 +39117,7 @@ function startCodexProbe(deps = {}) {
39110
39117
  }
39111
39118
 
39112
39119
  // src/_generated/version.ts
39113
- var PKG_VERSION = "0.3.45";
39120
+ var PKG_VERSION = "0.3.47";
39114
39121
 
39115
39122
  // src/api-base-url.ts
39116
39123
  var DEFAULT_API_BASE_URL = "http://127.0.0.1:8787";
@@ -39269,14 +39276,14 @@ function startSchedulerTickLoop(deps) {
39269
39276
  let stopped = false;
39270
39277
  let timer = null;
39271
39278
  const env = deps.env ?? process.env;
39272
- const schedule = (delayMs) => {
39279
+ const schedule = (delayMs2) => {
39273
39280
  if (stopped) return;
39274
39281
  timer = setTimeout(() => {
39275
39282
  void fireSchedulerTick(deps).then(({ nextWakeAt }) => {
39276
39283
  if (stopped) return;
39277
39284
  schedule(computeNextTickDelayMs(nextWakeAt, Date.now(), { env }));
39278
39285
  });
39279
- }, delayMs);
39286
+ }, delayMs2);
39280
39287
  timer.unref?.();
39281
39288
  };
39282
39289
  schedule(computeNextTickDelayMs((/* @__PURE__ */ new Date()).toISOString(), Date.now(), { env }));
@@ -41358,12 +41365,16 @@ var EXECUTION_TURN_BUDGET_PREFIX = "Execution turn exceeded the run wall-clock b
41358
41365
  var EXECUTION_TURN_TIMEOUT_RETRY_CODE = "execution_turn_timeout_retry";
41359
41366
  var EXECUTION_TURN_TIMEOUT_RETRY_SUMMARY = "Execution turn timed out; requeued for one automatic retry.";
41360
41367
  var EXECUTION_TURN_TIMEOUT_RETRY_CURSOR = "execution_turn_timeout_retry:1";
41368
+ var AWAITING_MERGE_STATUS_RETRY_DELAYS_MS = [50, 100];
41361
41369
  function readPositiveIntEnv(name, fallback) {
41362
41370
  const raw = (process.env[name] ?? "").trim();
41363
41371
  if (!raw) return fallback;
41364
41372
  const parsed = Number.parseInt(raw, 10);
41365
41373
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
41366
41374
  }
41375
+ function delayMs(ms) {
41376
+ return new Promise((resolve) => setTimeout(resolve, ms));
41377
+ }
41367
41378
  function execTurnIdleTimeoutMs() {
41368
41379
  return readPositiveIntEnv("MRRLIN_DIRECTOR_BRIDGE_EXEC_TURN_IDLE_TIMEOUT_SECONDS", DEFAULT_EXEC_TURN_IDLE_TIMEOUT_SECONDS) * 1e3;
41369
41380
  }
@@ -42387,19 +42398,42 @@ async function reconcileTaskAwaitingMerge(deps, projectSlug, task, runId, reason
42387
42398
  }
42388
42399
  }).catch(() => {
42389
42400
  });
42401
+ let lastError = null;
42402
+ for (let attempt = 1; attempt <= AWAITING_MERGE_STATUS_RETRY_DELAYS_MS.length + 1; attempt += 1) {
42403
+ try {
42404
+ await deps.client.updateTask(projectSlug, task.id, { status: "handoff_needed" });
42405
+ return;
42406
+ } catch (error51) {
42407
+ lastError = error51;
42408
+ const retryDelay = AWAITING_MERGE_STATUS_RETRY_DELAYS_MS[attempt - 1];
42409
+ if (retryDelay !== void 0) {
42410
+ await delayMs(retryDelay);
42411
+ }
42412
+ }
42413
+ }
42390
42414
  try {
42391
- await deps.client.updateTask(projectSlug, task.id, { status: "handoff_needed" });
42392
- } catch (error51) {
42393
- console.warn(
42394
- JSON.stringify({
42395
- event: "reconcile_awaiting_merge_failed",
42396
- projectSlug,
42397
- taskId: task.id,
42398
- runId,
42399
- error: error51 instanceof Error ? error51.message : String(error51)
42400
- })
42401
- );
42415
+ await deps.client.updateExecutionRun(projectSlug, runId, { status: "handoff_queued" });
42416
+ } catch {
42417
+ }
42418
+ try {
42419
+ await deps.client.createInboxItem(projectSlug, {
42420
+ id: `II-${import_node_crypto4.default.randomUUID()}`,
42421
+ kind: "handoff",
42422
+ taskId: task.id,
42423
+ body: `Task ${task.id} (${task.title}) still needs operator follow-up: ${reason}. Mrrlin could not move the task back to Handoff needed automatically.`
42424
+ });
42425
+ } catch {
42402
42426
  }
42427
+ console.warn(
42428
+ JSON.stringify({
42429
+ event: "reconcile_awaiting_merge_failed",
42430
+ projectSlug,
42431
+ taskId: task.id,
42432
+ runId,
42433
+ error: lastError instanceof Error ? lastError.message : String(lastError),
42434
+ attempts: AWAITING_MERGE_STATUS_RETRY_DELAYS_MS.length + 1
42435
+ })
42436
+ );
42403
42437
  }
42404
42438
  async function notifyOperatorHandoff(deps, projectSlug, task, runId, reason) {
42405
42439
  try {
@@ -42560,7 +42594,12 @@ async function driveAsyncJob(deps, projectSlug, jobId) {
42560
42594
  let outcome = { kind: "failed", failureReason: "provider_unavailable", error: "did not run" };
42561
42595
  try {
42562
42596
  for (; ; ) {
42563
- const begun = await deps.client.beginAsyncJobAttempt(projectSlug, jobId, leaseId, deriveHash());
42597
+ const begun = await deps.client.beginAsyncJobAttempt(
42598
+ projectSlug,
42599
+ jobId,
42600
+ leaseId,
42601
+ await deriveRequestedHash(deps.client, claimed.toolName, input)
42602
+ );
42564
42603
  if (!begun) return;
42565
42604
  if (begun.attemptCount > ASYNC_JOB_MAX_ATTEMPTS) {
42566
42605
  outcome = { kind: "failed", failureReason: "provider_unavailable", error: "exceeded retry budget" };
@@ -42611,8 +42650,17 @@ async function driveAsyncJob(deps, projectSlug, jobId) {
42611
42650
  }
42612
42651
  if (descriptor2.wakeDirector) deps.wake(projectSlug, claimed.directorSessionId, delivery.body, delivery.kind);
42613
42652
  }
42614
- function deriveHash() {
42615
- return `attempt-${Date.now()}`;
42653
+ async function deriveRequestedHash(client, toolName, input) {
42654
+ if (input.requestedHash) return input.requestedHash;
42655
+ if (toolName === "run_spec_consensus" && input.pageId) {
42656
+ const page = await client.getWikiPage(input.projectSlug, input.pageId);
42657
+ return page.contentHash ?? import_node_crypto5.default.createHash("sha256").update(page.markdownBody).digest("hex");
42658
+ }
42659
+ if (toolName === "draft_task_spec" && input.taskId) {
42660
+ const task = await client.getTask(input.projectSlug, input.taskId);
42661
+ return task.updatedAt ?? import_node_crypto5.default.createHash("sha256").update(JSON.stringify(task)).digest("hex");
42662
+ }
42663
+ return import_node_crypto5.default.createHash("sha256").update(JSON.stringify(input)).digest("hex");
42616
42664
  }
42617
42665
  async function runAsyncBackstopSweep(deps, drive = (slug, jobId) => driveAsyncJob(deps, slug, jobId)) {
42618
42666
  const projects = await deps.client.listProjects();
@@ -47490,7 +47538,7 @@ function buildThreadStartParams(msg, config2, desiredModel) {
47490
47538
  " bullets (`- AND ...` as needed). Scenarios make the spec verifiable.",
47491
47539
  "- Keep behavior contracts in the spec; implementation details belong in `## Implementation Plan`.",
47492
47540
  "- Apply this format ONLY to Specs/{id} pages; do not retro-fit it onto other wiki page types.",
47493
- "- 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 spec is review-only \u2014 do NOT call `create_execution_run` (non-code execution is not yet supported and fail-closes); tell the operator the spec was reviewed.",
47541
+ "- 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): supported domains like `seo` proceed to `create_execution_run` after consensus converges; unsupported domains fail-close with `non_code_execution_unsupported`. Report the spec's review outcome to the operator.",
47494
47542
  '- 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.',
47495
47543
  "- 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.",
47496
47544
  "",
@@ -53658,8 +53706,8 @@ async function cachedResult(client, input) {
53658
53706
  }
53659
53707
  return { status: "already_converged", payload: { outcome: "CONVERGED", contentHash: page.contentHash } };
53660
53708
  }
53661
- if (review?.outcome === "CONSENSUS_BLOCKED") {
53662
- return { status: "already_reviewed", payload: { outcome: "CONSENSUS_BLOCKED", contentHash: page.contentHash } };
53709
+ if (review?.outcome === "CONSENSUS_BLOCKED" || review?.outcome === "CONSENSUS_BLOCKED_CLARIFICATION") {
53710
+ return { status: "already_reviewed", payload: { outcome: review.outcome, contentHash: page.contentHash } };
53663
53711
  }
53664
53712
  return null;
53665
53713
  }
@@ -53708,6 +53756,27 @@ async function run(ctx, input) {
53708
53756
  }
53709
53757
  if (result.outcome === "CONSENSUS_BLOCKED") {
53710
53758
  const clarifications = result.clarifications ?? [];
53759
+ if (clarifications.length > 0) {
53760
+ try {
53761
+ const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
53762
+ if (page) {
53763
+ const domain2 = await resolveSpecDomain(ctx.client, input.projectSlug, {
53764
+ id: page.id,
53765
+ taskId: page.taskId ?? null
53766
+ });
53767
+ await ctx.client.recordSpecConsensusReview(input.projectSlug, input.pageId, {
53768
+ contentHash: domainScopedReviewHash(page.contentHash, domain2),
53769
+ outcome: "CONSENSUS_BLOCKED_CLARIFICATION",
53770
+ rounds: 0
53771
+ });
53772
+ }
53773
+ } catch (error52) {
53774
+ process.stderr.write(
53775
+ `[mrrlin-mcp consensus] clarification verdict write failed for ${input.projectSlug}/${input.pageId}: ${error52 instanceof Error ? error52.message : String(error52)}
53776
+ `
53777
+ );
53778
+ }
53779
+ }
53711
53780
  let filedQuestionCount = 0;
53712
53781
  if (clarifications.length > 0) {
53713
53782
  const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
@@ -53731,7 +53800,7 @@ async function run(ctx, input) {
53731
53800
  taskId: scopeId,
53732
53801
  body: clarification.question,
53733
53802
  origin: "gate",
53734
- ...questionOptions ? { questionOptions } : {},
53803
+ questionOptions,
53735
53804
  ...groupId ? { groupId } : {}
53736
53805
  });
53737
53806
  } catch {
@@ -53751,7 +53820,7 @@ async function run(ctx, input) {
53751
53820
  planId: scopeId,
53752
53821
  body: clarification.question,
53753
53822
  origin: "gate",
53754
- ...questionOptions ? { questionOptions } : {},
53823
+ questionOptions,
53755
53824
  ...groupId ? { groupId } : {}
53756
53825
  });
53757
53826
  } catch {
@@ -54806,7 +54875,7 @@ var mcpToolDescriptions = {
54806
54875
  [mcpToolNames.resetDirectorThread]: "Clear a task's persisted Director execution thread handle so the next execution run starts a fresh provider thread. Records a director_thread_reset task event.",
54807
54876
  [mcpToolNames.updateWikiPage]: "Update a markdown Wiki page.",
54808
54877
  [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.",
54809
- [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 spec is REVIEW-ONLY: do NOT call create_execution_run (it fail-closes with non_code_execution_unsupported until Phase 2) \u2014 report that the spec was reviewed and end the turn.',
54878
+ [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): supported domains like `seo` proceed to `create_execution_run` after consensus; unsupported domains fail-close with `non_code_execution_unsupported`. Report the spec\'s review outcome to the operator.',
54810
54879
  [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).`,
54811
54880
  [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.",
54812
54881
  [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.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.3.45",
3
+ "version": "0.3.47",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mrrlin-mcp": "dist/bin.cjs"
@@ -21,17 +21,17 @@
21
21
  "@types/ws": "^8.18.1",
22
22
  "esbuild": "^0.24.0",
23
23
  "tsx": "^4.22.3",
24
- "@mrrlin/schemas": "0.0.0",
25
24
  "@mrrlin/director-e2e": "0.0.0",
26
25
  "@mrrlin/wiki": "0.0.0",
27
26
  "@mrrlin/client": "0.0.0",
28
27
  "@mrrlin/codex-client": "0.0.0",
28
+ "@mrrlin/schemas": "0.0.0",
29
29
  "@mrrlin/tsconfig": "0.0.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "@iarna/toml": "^2.2.5",
33
33
  "@modelcontextprotocol/sdk": "^1.0.0",
34
- "@mrrlin-dev/external-agents": "^0.32.0",
34
+ "@mrrlin-dev/external-agents": "latest",
35
35
  "@playwright/mcp": "0.0.76",
36
36
  "playwright-core": "1.61.0-alpha-1781023400000",
37
37
  "proper-lockfile": "^4.1.2",