@mrrlin-dev/mcp 0.3.68 → 0.4.1

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 +406 -119
  2. package/package.json +6 -6
package/dist/bin.cjs CHANGED
@@ -20700,6 +20700,18 @@ function retargetRetiredApiBaseUrl(input) {
20700
20700
  function retiredApiHostnames() {
20701
20701
  return Object.keys(RETIRED_API_BASE_URLS);
20702
20702
  }
20703
+ function bridgeUrlImpliesStaging(input) {
20704
+ const raw = input?.trim();
20705
+ if (!raw) return false;
20706
+ if (isLoopbackUrl(raw)) return true;
20707
+ let hostname3;
20708
+ try {
20709
+ hostname3 = new URL(raw).hostname.toLowerCase();
20710
+ } catch {
20711
+ return false;
20712
+ }
20713
+ return hostname3 === "staging.mrrlin.com";
20714
+ }
20703
20715
  function isLoopbackUrl(input) {
20704
20716
  try {
20705
20717
  const url2 = new URL(input);
@@ -20846,11 +20858,12 @@ async function installBrowser(opts = {}) {
20846
20858
  function deriveServeEnvFromProcessEnv(env) {
20847
20859
  const out = {};
20848
20860
  const rawApiUrl = env.MRRLIN_API_BASE_URL?.trim();
20861
+ const bridgeWantsStaging = bridgeUrlImpliesStaging(env.MRRLIN_BRIDGE_URL);
20849
20862
  const apiUrl = rawApiUrl ? retargetRetiredApiBaseUrl(rawApiUrl).baseUrl : void 0;
20850
20863
  if (apiUrl && !isStagingApiBaseUrl(apiUrl)) {
20851
20864
  out.MRRLIN_API_BASE_URL = apiUrl;
20852
20865
  if (env.MRRLIN_API_ALLOW_REMOTE === "1") out.MRRLIN_API_ALLOW_REMOTE = "1";
20853
- } else if (apiUrl || env.MRRLIN_STAGING === "1") {
20866
+ } else if (apiUrl || env.MRRLIN_STAGING === "1" || bridgeWantsStaging) {
20854
20867
  out.MRRLIN_STAGING = "1";
20855
20868
  }
20856
20869
  return Object.keys(out).length > 0 ? out : void 0;
@@ -21009,7 +21022,7 @@ function blockEnvHasRetiredApiUrl(actual) {
21009
21022
  }
21010
21023
  function renderEnvPreservedNotice(blockId, envKeys) {
21011
21024
  const keys = [...envKeys].sort().join(", ");
21012
- return `[mrrlin-mcp install-codex] kept your existing [mcp_servers.${blockId}] env keys (${keys}) so your API target is preserved. Re-run with --force to replace or remove it.`;
21025
+ return `[mrrlin-mcp configure bridge] kept your existing [mcp_servers.${blockId}] env keys (${keys}) so your API target is preserved. Re-run with --force to replace or remove it.`;
21013
21026
  }
21014
21027
  async function pathExists(target) {
21015
21028
  try {
@@ -36401,6 +36414,8 @@ var openApiOperationIds = {
36401
36414
  getTask: "getTask",
36402
36415
  archiveTask: "archiveTask",
36403
36416
  unarchiveTask: "unarchiveTask",
36417
+ takeOverTask: "takeOverTask",
36418
+ closeTaskAsWontDo: "closeTaskAsWontDo",
36404
36419
  getWikiPage: "getWikiPage",
36405
36420
  handleGithubInstallCallback: "handleGithubInstallCallback",
36406
36421
  listExecutionArtifacts: "listExecutionArtifacts",
@@ -36461,6 +36476,7 @@ var openApiOperationIds = {
36461
36476
  getOperatorSettings: "getOperatorSettings",
36462
36477
  updateOperatorSettings: "updateOperatorSettings",
36463
36478
  recordOperatorPresence: "recordOperatorPresence",
36479
+ pauseAutopilotForLimit: "pauseAutopilotForLimit",
36464
36480
  getAgentPool: "getAgentPool",
36465
36481
  putAgentPool: "putAgentPool",
36466
36482
  getConsensusConfig: "getConsensusConfig",
@@ -36507,6 +36523,13 @@ var mrrlinOperatorSettingsSchema = external_exports.object({
36507
36523
  activeProjectSlug: external_exports.string().min(1).nullable(),
36508
36524
  // The last time an authenticated BROWSER operator session was observed. Null means never seen.
36509
36525
  lastPresenceAt: external_exports.string().datetime().nullable(),
36526
+ // autopilot-limit-pause: READ-ONLY status for an automatic provider-limit pause. These are never
36527
+ // writable via mrrlinOperatorSettingsUpdateSchema (an explicit operator write clears them
36528
+ // server-side). Non-null autoPauseSource => the current pause is machine-owned; autoPauseResetAt
36529
+ // is the absolute reset time (null = indefinite, manual resume required).
36530
+ autoPauseSource: external_exports.string().min(1).nullable(),
36531
+ autoPauseReason: external_exports.string().min(1).nullable(),
36532
+ autoPauseResetAt: external_exports.string().datetime().nullable(),
36510
36533
  createdAt: external_exports.string().datetime(),
36511
36534
  updatedAt: external_exports.string().datetime()
36512
36535
  });
@@ -36518,6 +36541,11 @@ var mrrlinOperatorSettingsUpdateSchema = external_exports.object({
36518
36541
  message: "OperatorSettingsUpdate requires at least one field"
36519
36542
  });
36520
36543
  var operatorSettingsResponseSchema = external_exports.object({ data: mrrlinOperatorSettingsSchema });
36544
+ var autopilotLimitPauseRequestSchema = external_exports.object({
36545
+ source: external_exports.string().min(1).default("provider_limit"),
36546
+ reason: external_exports.string().min(1).max(500),
36547
+ resetAt: external_exports.string().datetime().nullable().default(null)
36548
+ }).strict();
36521
36549
  var operatorPresenceResponseSchema = external_exports.object({ data: external_exports.object({ lastPresenceAt: external_exports.string().datetime() }) });
36522
36550
  var operatorPresenceRequestSchema = external_exports.object({ projectSlug: external_exports.string().min(1).optional() }).strict().optional();
36523
36551
  var mrrlinConsensusConfigSchema = external_exports.object({ consensusEnabled: external_exports.boolean() });
@@ -36889,6 +36917,32 @@ var SUPPORTED_EXECUTION_DOMAINS = Object.keys(SUPPORTED_EXECUTION_POLICY_BY_DOMA
36889
36917
  var taxonomyCategorySchema = external_exports.string().min(1).regex(/^[a-z][a-z0-9-]*$/);
36890
36918
  var taxonomySubcategorySchema = external_exports.string().min(1).regex(/^[a-z][a-z0-9-]*$/);
36891
36919
  var mrrlinTaskAssigneeSchema = external_exports.union([external_exports.string().regex(/^@/), external_exports.string().email()]).nullable();
36920
+ var mrrlinStopReasonSchema = external_exports.enum([
36921
+ "task_terminal",
36922
+ "active_run_exists",
36923
+ "draft_running",
36924
+ "consensus_running",
36925
+ "spec_changed_during_advance",
36926
+ "run_limit_reached",
36927
+ "backoff_pending",
36928
+ "blocked_by_active",
36929
+ "spec_missing",
36930
+ "spec_draft_failed",
36931
+ "spec_not_specs_folder",
36932
+ "awaiting_clarification",
36933
+ "consensus_blocked",
36934
+ "consensus_run_failed",
36935
+ "consensus_disabled_spec_not_ready",
36936
+ "spec_approval_pending",
36937
+ "awaiting_handoff",
36938
+ "assigned_to_other",
36939
+ "attempts_exhausted",
36940
+ "daily_job_budget_exhausted",
36941
+ "taken_over",
36942
+ "operator_absent",
36943
+ "autonomy_paused",
36944
+ "unknown"
36945
+ ]);
36892
36946
  var mrrlinTaskSchema = external_exports.object({
36893
36947
  // Output-optional (not defaulted-required) so existing task fixtures/mocks across the
36894
36948
  // codebase need no churn; DB rows always resolve it to `string | null` via taskRowToTask.
@@ -36903,6 +36957,9 @@ var mrrlinTaskSchema = external_exports.object({
36903
36957
  autonomyReason: external_exports.string().min(1).max(240),
36904
36958
  blockedBy: external_exports.array(mrrlinTaskIdSchema),
36905
36959
  category: taxonomyCategorySchema,
36960
+ // Slice 3 (task-stall-visibility): why a task was closed as won't-do -- populated only by
36961
+ // closeTaskAsWontDo. Output-optional so existing task fixtures need no churn.
36962
+ closeReason: external_exports.string().nullable().optional(),
36906
36963
  directorThreadId: external_exports.string().nullable().default(null),
36907
36964
  dueDate: external_exports.string().date().nullable(),
36908
36965
  followUpAt: external_exports.string().datetime().nullable(),
@@ -36911,6 +36968,9 @@ var mrrlinTaskSchema = external_exports.object({
36911
36968
  latestJudgement: external_exports.string().nullable().default(null),
36912
36969
  latestJudgementAt: external_exports.string().datetime().nullable().default(null),
36913
36970
  latestJudgementKind: external_exports.enum(["autonomy", "handoff"]).nullable().default(null),
36971
+ // Slice 3: the retry-backoff timer for exactly spec_draft_failed/consensus_run_failed -- a task
36972
+ // whose next_attempt_at is in the future is skipped from admission (backoff_pending).
36973
+ nextAttemptAt: external_exports.string().datetime().nullable().optional(),
36914
36974
  notes: external_exports.string().nullable().default(null),
36915
36975
  // Raw operator request captured at task creation (operator-authored ONLY, never Director-generated).
36916
36976
  // The intent-aware spec gate judges the spec against this; null/absent ⇒ the gate fail-closes.
@@ -36920,6 +36980,14 @@ var mrrlinTaskSchema = external_exports.object({
36920
36980
  planId: mrrlinPlanIdSchema.nullable(),
36921
36981
  specApproved: mrrlinSpecApprovedSchema.optional(),
36922
36982
  specWikiPageId: mrrlinWikiPageIdSchema.nullable(),
36983
+ // Slice 3: episode bookkeeping alongside the async_jobs-derived countEpisodeAttempts --
36984
+ // stallAttemptCount backs the exponential-backoff formula, stallRearmed guards against
36985
+ // re-arming the same episode's ceiling more than once.
36986
+ stallAttemptCount: external_exports.number().int().nonnegative().optional(),
36987
+ stallRearmed: external_exports.boolean().optional(),
36988
+ stallNextAction: external_exports.string().nullable().optional(),
36989
+ stallReason: mrrlinStopReasonSchema.nullable().optional(),
36990
+ stallSince: external_exports.string().datetime().nullable().optional(),
36923
36991
  status: mrrlinTaskStatusSchema,
36924
36992
  subcategory: taxonomySubcategorySchema,
36925
36993
  tags: mrrlinTaskTagsSchema,
@@ -36982,6 +37050,12 @@ var mrrlinWikiPageSchema = external_exports.object({
36982
37050
  var mrrlinMarkSpecReadyRequestSchema = external_exports.object({
36983
37051
  ready: external_exports.boolean()
36984
37052
  });
37053
+ var mrrlinTakeOverTaskRequestSchema = external_exports.object({
37054
+ assignee: external_exports.string().min(1)
37055
+ });
37056
+ var mrrlinCloseTaskAsWontDoRequestSchema = external_exports.object({
37057
+ reason: external_exports.string().min(1)
37058
+ });
36985
37059
  var mrrlinGithubBindingSchema = external_exports.object({
36986
37060
  appInstallationState: external_exports.enum(["installed", "missing"]),
36987
37061
  installationId: external_exports.number().int().positive().nullable().default(null),
@@ -37997,7 +38071,8 @@ var mrrlinInFlightSchedulerEntrySchema = external_exports.discriminatedUnion("ki
37997
38071
  ]);
37998
38072
  var mrrlinIneligibleTaskSchema = external_exports.object({
37999
38073
  task: mrrlinTaskSchema,
38000
- autonomyLevel: mrrlinAutonomyLevelSchema
38074
+ autonomyLevel: mrrlinAutonomyLevelSchema,
38075
+ reason: external_exports.string().nullable().optional()
38001
38076
  });
38002
38077
  var schedulerPendingTasksResponseSchema = external_exports.object({
38003
38078
  data: external_exports.object({
@@ -38021,6 +38096,12 @@ var schedulerPendingTasksResponseSchema = external_exports.object({
38021
38096
  // person actually running the project) is visible as a large number instead of the autopilot
38022
38097
  // silently doing nothing.
38023
38098
  excludedByOwnershipCount: external_exports.number().int().min(0),
38099
+ // Additive: the same ownership exclusion grouped by assignee. This reveals who the
38100
+ // autopilot is waiting for without returning task ids or titles in this endpoint.
38101
+ ownershipExcludedAssignees: external_exports.array(external_exports.object({
38102
+ assignee: external_exports.string(),
38103
+ count: external_exports.number().int().min(1)
38104
+ })),
38024
38105
  // Additive: true when the operator-web-presence gate is the reason nothing new is starting
38025
38106
  // (autonomyPaused takes precedence when both apply). Computed by the same shared helper the
38026
38107
  // tick uses, so this page can never claim a task is starting when the tick would decline it.
@@ -38055,7 +38136,7 @@ var wakeDirectorResponseSchema = external_exports.object({
38055
38136
  }))
38056
38137
  })
38057
38138
  });
38058
- var mrrlinInboxItemKindSchema = external_exports.enum(["proposal", "check_in", "question", "handoff", "info"]);
38139
+ var mrrlinInboxItemKindSchema = external_exports.enum(["proposal", "check_in", "question", "handoff", "info", "completed"]);
38059
38140
  var mrrlinInboxItemStatusSchema = external_exports.enum(["pending", "approved", "rejected", "acknowledged"]);
38060
38141
  var mrrlinInboxItemIdSchema = external_exports.string().regex(/^II-[A-Za-z0-9._-]+$/);
38061
38142
  var mrrlinInboxItemActionPayloadSchema = external_exports.object({
@@ -38124,6 +38205,7 @@ var mrrlinInboxItemSchema = external_exports.object({
38124
38205
  projectSlug: mrrlinProjectSlugSchema,
38125
38206
  questionOptions: mrrlinInboxQuestionOptionsSchema.nullable().default(null),
38126
38207
  readAt: external_exports.string().datetime().nullable(),
38208
+ reason: external_exports.string().nullable().optional(),
38127
38209
  status: mrrlinInboxItemStatusSchema,
38128
38210
  taskId: mrrlinTaskIdSchema.nullable()
38129
38211
  }).superRefine((value, context) => {
@@ -38514,6 +38596,36 @@ function addTurnTokenUsage(total, turn) {
38514
38596
  };
38515
38597
  }
38516
38598
 
38599
+ // ../../packages/codex-client/dist/rate-limit.js
38600
+ var EXHAUSTION_USED_PERCENT = 100;
38601
+ function windowExhausted(window) {
38602
+ if (!window)
38603
+ return false;
38604
+ return typeof window.usedPercent === "number" && Number.isFinite(window.usedPercent) && window.usedPercent >= EXHAUSTION_USED_PERCENT;
38605
+ }
38606
+ function normalizeResetEpochSeconds(resetsAt, nowMs) {
38607
+ if (typeof resetsAt !== "number" || !Number.isFinite(resetsAt))
38608
+ return null;
38609
+ const resetMs = resetsAt * 1e3;
38610
+ if (resetMs <= nowMs)
38611
+ return null;
38612
+ return new Date(resetMs).toISOString();
38613
+ }
38614
+ function detectExhaustedProviderLimit(snapshot, nowMs) {
38615
+ if (!snapshot)
38616
+ return null;
38617
+ const reachedType = typeof snapshot.rateLimitReachedType === "string" ? snapshot.rateLimitReachedType.trim() : "";
38618
+ const exhausted = reachedType.length > 0 || windowExhausted(snapshot.primary) || windowExhausted(snapshot.secondary);
38619
+ if (!exhausted)
38620
+ return null;
38621
+ const candidates = [
38622
+ normalizeResetEpochSeconds(snapshot.primary?.resetsAt, nowMs),
38623
+ normalizeResetEpochSeconds(snapshot.secondary?.resetsAt, nowMs)
38624
+ ].filter((value) => value !== null);
38625
+ const resetAt = candidates.length > 0 ? candidates.reduce((a, b) => a > b ? a : b) : null;
38626
+ return { exhausted: true, resetAt };
38627
+ }
38628
+
38517
38629
  // ../../packages/client/dist/agent-token-provider.js
38518
38630
  var AgentTokenProviderError = class extends Error {
38519
38631
  code;
@@ -38970,6 +39082,22 @@ function createMrrlinClient(config2) {
38970
39082
  });
38971
39083
  return taskResponseSchema.parse(body).data;
38972
39084
  },
39085
+ async takeOverTask(projectSlug, taskId, assignee) {
39086
+ const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/take-over`, {
39087
+ method: "POST",
39088
+ headers: { "content-type": "application/json" },
39089
+ body: JSON.stringify({ assignee })
39090
+ });
39091
+ return taskResponseSchema.parse(body).data;
39092
+ },
39093
+ async closeTaskAsWontDo(projectSlug, taskId, reason) {
39094
+ const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/wont-do`, {
39095
+ method: "POST",
39096
+ headers: { "content-type": "application/json" },
39097
+ body: JSON.stringify({ reason })
39098
+ });
39099
+ return taskResponseSchema.parse(body).data;
39100
+ },
38973
39101
  async updateWikiPage(projectSlug, pageId, patch) {
38974
39102
  const body = await request(`${projectPath(projectSlug)}/wiki/${encodeURIComponent(pageId)}`, {
38975
39103
  body: JSON.stringify(patch),
@@ -39408,6 +39536,15 @@ function createMrrlinClient(config2) {
39408
39536
  });
39409
39537
  return operatorSettingsResponseSchema.parse(body).data;
39410
39538
  },
39539
+ async pauseAutopilotForLimit(input) {
39540
+ const parsed = autopilotLimitPauseRequestSchema.parse(input);
39541
+ const body = await request(`/operator/autopilot/limit-pause`, {
39542
+ body: JSON.stringify(parsed),
39543
+ headers: { "content-type": "application/json" },
39544
+ method: "POST"
39545
+ });
39546
+ return operatorSettingsResponseSchema.parse(body).data;
39547
+ },
39411
39548
  async recordOperatorPresence(input) {
39412
39549
  const projectSlug = input?.projectSlug;
39413
39550
  const body = await request(`/operator/presence`, {
@@ -40050,7 +40187,7 @@ function startCodexProbe(deps = {}) {
40050
40187
  }
40051
40188
 
40052
40189
  // src/_generated/version.ts
40053
- var PKG_VERSION = "0.3.68";
40190
+ var PKG_VERSION = "0.4.1";
40054
40191
 
40055
40192
  // src/api-base-url-migrate.ts
40056
40193
  var import_node_fs2 = __toESM(require("node:fs"), 1);
@@ -42861,7 +42998,7 @@ function onComplete(outcome, input) {
42861
42998
  }
42862
42999
  const taskRef = parsed.taskTitle ? `${input.taskId} \u2014 ${parsed.taskTitle}` : input.taskId;
42863
43000
  return {
42864
- kind: "info",
43001
+ kind: "completed",
42865
43002
  taskId: input.taskId,
42866
43003
  body: `Spec drafted automatically for ${taskRef} \u2014 page ${parsed.pageId ?? "?"} in Specs/. The next scheduler tick will run the consensus review.`
42867
43004
  };
@@ -43921,6 +44058,10 @@ function executionTurnBudgetMessage(budgetMs) {
43921
44058
  function isExecutionTurnTimeoutReason(reason) {
43922
44059
  return reason.startsWith(EXECUTION_TURN_TIMEOUT_PREFIX) || reason.startsWith(EXECUTION_TURN_STALL_PREFIX);
43923
44060
  }
44061
+ function reportAutopilotLimitPause(client, reason, resetAt) {
44062
+ void client.pauseAutopilotForLimit({ source: "provider_limit", reason, resetAt }).catch(() => {
44063
+ });
44064
+ }
43924
44065
  function isDeliverableGateRetryableReason(reason) {
43925
44066
  return DELIVERABLE_GATE_RETRYABLE_REASON_PREFIXES.some((prefix) => reason.startsWith(prefix));
43926
44067
  }
@@ -43992,20 +44133,24 @@ var BASE_INSTRUCTIONS_PREFIX = [
43992
44133
  " if you are blocked and a human must decide before the work can continue, leave it at `handoff_needed`.",
43993
44134
  "",
43994
44135
  "File artifacts:",
43995
- "- Decide BEFORE writing your result whether the operator will scan it, compare across it,",
43996
- " re-read it later, keep it, or share it. If so it is a file, not inline text \u2014 judge by use,",
43997
- " not by topic, domain, or data type.",
43998
- "- When your output is a file (markdown report, HTML, screenshot), upload it with",
43999
- " `upload_artifact` and embed the returned markdown link \u2014 never inline large blobs",
44000
- " into chat, spec, or note text.",
44136
+ "- Decide BEFORE writing your result, by PROVENANCE: output assembled from data you RETRIEVED",
44137
+ " this run (API, browser, analytics, database, a search over project records) that comes out as",
44138
+ " a multi-record or multi-dimension result is a FILE, not inline text.",
44139
+ "- Your own synthesis or comparison stays inline however structured it looks, and so does a",
44140
+ " retrieval yielding one value. Both conditions must hold before it becomes a file. When it",
44141
+ " qualifies, produce the file; never merely offer one.",
44142
+ "- Mixed output counts as retrieved: if any material part came from a retrieval, treat the whole",
44143
+ " result as retrieved rather than splitting it between inline text and a file.",
44144
+ "- When your output is a file (markdown report, HTML), call `deliver_file`: pass the body inline",
44145
+ " as `content` and embed the returned markdown link. It needs no writable path, so it is the",
44146
+ " default even when this run path cannot write files \u2014 never inline large blobs into chat,",
44147
+ " spec, or note text.",
44001
44148
  "- class=temp for one-off operator hand-offs (48h); class=durable for anything a spec",
44002
44149
  " or evidence page will reference (180 days). Always pass taskId/runId when known.",
44003
- "- Anything user-facing delivered as a file MUST be uploaded. Files are capped at 5MB \u2014",
44004
- " split or summarize larger outputs.",
44005
- "- `upload_artifact` needs a real local file path, so it presupposes you can write one. When this",
44006
- " run path CAN write files, produce the artifact normally \u2014 that is the default. ONLY when it",
44007
- " cannot write, do NOT attempt the file: put the summary in your result text and state what a",
44008
- " file would have held. Never fake a path you did not write.",
44150
+ "- Anything user-facing delivered as a file MUST go through `deliver_file`. Files are capped at",
44151
+ " 5MB \u2014 split or summarize larger outputs.",
44152
+ "- Use `upload_artifact` only for a file already on disk (e.g. a screenshot the browser wrote);",
44153
+ " it needs a real local path, so never fake a path you did not write.",
44009
44154
  "",
44010
44155
  "Produced URLs \u2014 the operator's per-page links come only from you:",
44011
44156
  "- Call `record_produced_url` after publishing EACH web page this task produced, so the task UI",
@@ -44335,6 +44480,17 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
44335
44480
  }
44336
44481
  const usage = !notifThreadId || notifThreadId === threadId ? extractTurnTokenUsage(method, params) : null;
44337
44482
  if (usage) turnUsage = usage;
44483
+ if (method === "account/rateLimits/updated") {
44484
+ const snapshot = params?.rateLimits ?? null;
44485
+ const signal = detectExhaustedProviderLimit(snapshot, Date.now());
44486
+ if (signal) {
44487
+ reportAutopilotLimitPause(
44488
+ deps.client,
44489
+ "Codex provider limit exhausted during an autonomous execution run.",
44490
+ signal.resetAt
44491
+ );
44492
+ }
44493
+ }
44338
44494
  const events = mapper.mapNotification(
44339
44495
  { method, params },
44340
44496
  threadId
@@ -50959,13 +51115,22 @@ function buildThreadStartParams(msg, config2, desiredModel) {
50959
51115
  " is not measurable, re-set `verificationDueAt` to a new date with a reason.",
50960
51116
  "",
50961
51117
  "File artifacts contract \u2014 decide the delivery medium BEFORE you compose the reply:",
50962
- "- First ask what the operator will DO with this output. If they will scan it, compare across",
50963
- " it, re-read it later, keep it, or share it, it belongs in a FILE. If they need to know one",
50964
- " thing right now, chat alone is right. Judge by use, not by topic, domain, or data type.",
50965
- "- When it is a file: write it (`.md` for linear prose, `.html` when structure or layout",
50966
- " carries meaning), upload with `upload_artifact` (class=temp for one-off downloads,",
50967
- " class=durable when a spec/evidence page references it; pass taskId when known), and paste",
50968
- " the returned markdown link into your reply.",
51118
+ "- Decide by PROVENANCE, not by predicting what the operator will do later. If this answer is",
51119
+ " assembled from data you RETRIEVED this turn (an HTTP/API call, the browser, analytics, a",
51120
+ " database, a search over project records) AND it comes out as a multi-record or",
51121
+ " multi-dimension result rather than one fact or a few values, it belongs in a FILE.",
51122
+ "- Your own synthesis, comparison, or judgement belongs in chat, however structured it looks.",
51123
+ " A retrieval that yields one number or one line also belongs in chat. Both conditions must",
51124
+ " hold before it becomes a file.",
51125
+ "- Mixed answers count as retrieved: if any material part of the result came from a retrieval,",
51126
+ " treat the whole answer as retrieved rather than splitting it between chat and a file.",
51127
+ '- When it qualifies, PRODUCE the file. Offering one ("I can give you this as CSV/JSON") is the',
51128
+ " failure, not the courtesy.",
51129
+ "- When it is a file: choose the format (`.md` for linear prose, `.html` when structure or",
51130
+ " layout carries meaning), then call `deliver_file` \u2014 pass the body inline as `content` and it",
51131
+ " needs no local file (class=temp for one-off downloads, class=durable when a spec/evidence",
51132
+ " page references it; pass taskId when known), and paste the returned markdown link into your",
51133
+ " reply. Use `upload_artifact` only for a file that already exists on disk (e.g. a screenshot).",
50969
51134
  "- The chat reply must still stand alone as a complete answer even if the file is never opened:",
50970
51135
  " the headline finding, the few points that matter, then the link. Never reduce it to",
50971
51136
  ' "see attached" or "details in the report".',
@@ -51202,6 +51367,10 @@ var DEFAULT_WORKTREE_RETENTION_HOURS = 48;
51202
51367
  function worktreeRetentionMs() {
51203
51368
  return readEnvInt("MRRLIN_DIRECTOR_WORKTREE_RETENTION_HOURS", DEFAULT_WORKTREE_RETENTION_HOURS) * 36e5;
51204
51369
  }
51370
+ function reportAutopilotLimitPause2(client, reason, resetAt) {
51371
+ void client.pauseAutopilotForLimit({ source: "provider_limit", reason, resetAt }).catch(() => {
51372
+ });
51373
+ }
51205
51374
  async function runOrphanWorktreeSweep(client, opts) {
51206
51375
  const cutoff = Date.now() - worktreeRetentionMs();
51207
51376
  const isReapable = (worktreePath) => {
@@ -52133,7 +52302,18 @@ function createBridgeMessageHandler(deps) {
52133
52302
  });
52134
52303
  const mapper = thread.mapper;
52135
52304
  mapper.reset();
52136
- const contextPrefix = renderContextPrefix({ ...msg.context, directorSessionId });
52305
+ const contextFocusTaskId = msg.context?.focusTaskId?.trim();
52306
+ const contextProjectSlug = msg.context?.projectSlug?.trim();
52307
+ let answeredQuestions;
52308
+ if (deps.listAnsweredQuestions && contextFocusTaskId && contextProjectSlug) {
52309
+ try {
52310
+ const acknowledged = await deps.listAnsweredQuestions(contextProjectSlug, contextFocusTaskId);
52311
+ const withAnswers = acknowledged.filter((item) => Boolean(item.answer)).map((item) => ({ answer: item.answer, question: item.body }));
52312
+ answeredQuestions = withAnswers.length ? withAnswers : void 0;
52313
+ } catch {
52314
+ }
52315
+ }
52316
+ const contextPrefix = renderContextPrefix({ ...msg.context, answeredQuestions, directorSessionId });
52137
52317
  const sendInTurn = (msg2) => {
52138
52318
  const buf = thread.replay;
52139
52319
  if (!buf) {
@@ -52201,7 +52381,13 @@ function createBridgeMessageHandler(deps) {
52201
52381
  pokeActivity();
52202
52382
  if (method === "account/rateLimits/updated") {
52203
52383
  const snapshot = params?.rateLimits ?? null;
52204
- if (snapshot) sendInTurn({ type: "rateLimits", directorSessionId, rateLimits: snapshot });
52384
+ if (snapshot) {
52385
+ sendInTurn({ type: "rateLimits", directorSessionId, rateLimits: snapshot });
52386
+ const signal = detectExhaustedProviderLimit(snapshot, Date.now());
52387
+ if (signal) {
52388
+ deps.reportLimitPause?.("Codex provider limit exhausted during a Director turn.", signal.resetAt);
52389
+ }
52390
+ }
52205
52391
  }
52206
52392
  const events = mapper.mapNotification({ method, params }, threadId);
52207
52393
  for (const event of events) {
@@ -52303,6 +52489,10 @@ ${effectiveText}` : effectiveText;
52303
52489
  forward(event);
52304
52490
  }
52305
52491
  if (outputEvents === 0) {
52492
+ deps.reportLimitPause?.(
52493
+ "Director turn completed without output; Codex likely hit its usage or credits limit.",
52494
+ null
52495
+ );
52306
52496
  sendInTurn({
52307
52497
  type: "error",
52308
52498
  directorSessionId,
@@ -52800,6 +52990,7 @@ async function startDirectorBridge() {
52800
52990
  turnInactivity: () => ({ idleMs: chatTurnIdleMs(), maxMs: chatTurnMaxMs() }),
52801
52991
  checkoutRegistry,
52802
52992
  setCodexAuth: (s) => health.setCodexAuth(s),
52993
+ reportLimitPause: (reason, resetAt) => reportAutopilotLimitPause2(client, reason, resetAt),
52803
52994
  getPairingQr: async () => {
52804
52995
  if (!relayConfigured || !remoteRelay) return { status: "not-configured" };
52805
52996
  const payload = remoteRelay.qrPayload;
@@ -52819,7 +53010,7 @@ async function startDirectorBridge() {
52819
53010
  if (!body) return;
52820
53011
  const id = `II-MCPUPD-${(0, import_node_crypto8.randomUUID)()}`;
52821
53012
  try {
52822
- const row = await client.createInboxItem(projectSlug, { id, kind: "question", body });
53013
+ const row = await client.createInboxItem(projectSlug, { id, kind: "info", body });
52823
53014
  process.stderr.write(
52824
53015
  `[mrrlin-mcp director-bridge] info bridge.self_update.inbox_filed ${JSON.stringify({
52825
53016
  projectSlug,
@@ -52839,6 +53030,7 @@ async function startDirectorBridge() {
52839
53030
  );
52840
53031
  }
52841
53032
  },
53033
+ listAnsweredQuestions: (projectSlug, taskId) => client.listInboxItems(projectSlug, { status: "acknowledged", kind: "question", taskId }),
52842
53034
  // External-agents control plane: the real spawn runner, with the EXTERNAL_AGENTS_CLI operator
52843
53035
  // escape hatch bound here (the handlers stay spawn-free and this is the only production seam).
52844
53036
  runExternalAgentsCli: (args, opts) => runExternalAgentsCli(args, { ...opts, envCliPath: process.env["EXTERNAL_AGENTS_CLI"] })
@@ -53253,11 +53445,13 @@ function renderContextPrefix(context) {
53253
53445
  const urlPath = context?.urlPath?.trim() ? context.urlPath.trim() : void 0;
53254
53446
  const focusTaskId = context?.focusTaskId?.trim() ? context.focusTaskId.trim() : void 0;
53255
53447
  const directorSessionId = context?.directorSessionId?.trim() ? context.directorSessionId.trim() : void 0;
53448
+ const answeredQuestions = context?.answeredQuestions?.length ? context.answeredQuestions : void 0;
53256
53449
  const payload = {};
53257
53450
  if (projectSlug) payload.projectSlug = projectSlug;
53258
53451
  if (urlPath) payload.urlPath = urlPath;
53259
53452
  if (focusTaskId) payload.focusTaskId = focusTaskId;
53260
53453
  if (directorSessionId) payload.directorSessionId = directorSessionId;
53454
+ if (answeredQuestions) payload.answeredQuestions = answeredQuestions;
53261
53455
  const keys = Object.keys(payload);
53262
53456
  if (keys.length === 0) return "";
53263
53457
  return `Context (JSON): ${JSON.stringify(payload)}`;
@@ -57271,30 +57465,9 @@ var HUMANIZE_TEMPLATE_BY_KIND = {
57271
57465
  missing_requirement: "A required part of the task is still missing from the spec. What should it say?",
57272
57466
  acceptance_criteria_detail: "The direction is clear, but the spec still needs a sharper success check. What should count as done?"
57273
57467
  };
57274
- var HUMANIZE_CONTEXT_CAP = 200;
57275
- function truncateToSentenceBoundary(text, cap) {
57276
- if (text.length <= cap) return text;
57277
- const sentences = text.match(/.+?[.!?](?=\s|$)/g);
57278
- if (sentences) {
57279
- let kept = "";
57280
- let keptCount = 0;
57281
- for (const sentence of sentences) {
57282
- const candidate = kept ? `${kept} ${sentence.trim()}` : sentence.trim();
57283
- if (candidate.length > cap) break;
57284
- kept = candidate;
57285
- keptCount++;
57286
- }
57287
- if (kept) return keptCount < sentences.length ? `${kept} \u2026` : kept;
57288
- }
57289
- const cutIdx = text.lastIndexOf(" ", cap);
57290
- return text.slice(0, cutIdx > 0 ? cutIdx : cap).trim() + "\u2026";
57291
- }
57292
57468
  function humanizeClarificationBody(clarification) {
57293
57469
  const prefix = HUMANIZE_TEMPLATE_BY_KIND[clarification.kind];
57294
- const context = truncateToSentenceBoundary(
57295
- clarification.rawQuestion.replace(/\b(?:parseable|SPEC_IDENTITY_MISMATCH|machine-usable)\b/gi, "").replace(/\s+/g, " ").trim(),
57296
- HUMANIZE_CONTEXT_CAP
57297
- );
57470
+ const context = clarification.rawQuestion.replace(/\b(?:parseable|SPEC_IDENTITY_MISMATCH|machine-usable)\b/gi, "").replace(/\s+/g, " ").trim();
57298
57471
  return context ? `${prefix} ${context}` : prefix;
57299
57472
  }
57300
57473
  var defaultNativeSpecGateBuilders = { buildSpecGateDeps, runSpecGate };
@@ -57589,7 +57762,7 @@ function onComplete2(outcome, input) {
57589
57762
  };
57590
57763
  }
57591
57764
  return {
57592
- kind: "info",
57765
+ kind: "completed",
57593
57766
  ...taskId ? { taskId } : {},
57594
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.`
57595
57768
  };
@@ -57720,7 +57893,7 @@ function onComplete3(outcome, input) {
57720
57893
  };
57721
57894
  }
57722
57895
  return {
57723
- kind: "info",
57896
+ kind: "completed",
57724
57897
  ...taskId ? { taskId } : {},
57725
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.`
57726
57899
  };
@@ -57836,11 +58009,14 @@ var mcpToolNames = {
57836
58009
  resetDirectorThread: "reset_director_thread",
57837
58010
  archiveTask: "archive_task",
57838
58011
  unarchiveTask: "unarchive_task",
58012
+ takeOverTask: "take_over_task",
58013
+ closeTaskAsWontDo: "close_task_as_wont_do",
57839
58014
  updateWikiPage: "update_wiki_page",
57840
58015
  markSpecReady: "mark_spec_ready",
57841
58016
  runSpecConsensus: "run_spec_consensus",
57842
58017
  runCodeReviewerGate: "run_code_reviewer_gate",
57843
58018
  uploadArtifact: "upload_artifact",
58019
+ deliverFile: "deliver_file",
57844
58020
  listArtifactFiles: "list_artifact_files",
57845
58021
  reclaimExecutionRunLease: "reclaim_execution_run_lease",
57846
58022
  restartExecutionRun: "restart_execution_run",
@@ -58342,6 +58518,8 @@ var updateTaskInputSchema = taskScopedSchema.merge(mrrlinTaskUpdateSchema);
58342
58518
  var resetDirectorThreadInputSchema = taskScopedSchema;
58343
58519
  var archiveTaskInputSchema = taskScopedSchema;
58344
58520
  var unarchiveTaskInputSchema = taskScopedSchema;
58521
+ var takeOverTaskInputSchema = taskScopedSchema.merge(mrrlinTakeOverTaskRequestSchema);
58522
+ var closeTaskAsWontDoInputSchema = taskScopedSchema.merge(mrrlinCloseTaskAsWontDoRequestSchema);
58345
58523
  var listPlansInputSchema = projectScopedSchema;
58346
58524
  var createPlanInputSchema = projectScopedSchema.merge(mrrlinPlanCreateSchema);
58347
58525
  var updatePlanInputSchema = planScopedSchema.merge(mrrlinPlanUpdateSchema);
@@ -58424,6 +58602,20 @@ var uploadArtifactInputSchema = projectScopedSchema.merge(
58424
58602
  description: external_exports.string().optional().describe("Shown in the run's artifact history; ignored when runId is not provided.")
58425
58603
  })
58426
58604
  );
58605
+ var deliverFileInputSchema = projectScopedSchema.merge(
58606
+ external_exports.object({
58607
+ filename: external_exports.string().min(1).describe(
58608
+ "Bare filename with extension (e.g. 'notes.md'). No path separators or '..' allowed."
58609
+ ),
58610
+ content: external_exports.string().describe("UTF-8 text content of the file."),
58611
+ class: mrrlinArtifactFileClassSchema.describe(
58612
+ "temp = 48h one-off hand-off; durable = 180d, for spec/evidence references."
58613
+ ),
58614
+ taskId: mrrlinTaskIdSchema.optional(),
58615
+ runId: mrrlinExecutionRunIdSchema.optional(),
58616
+ description: external_exports.string().optional().describe("Shown in the run's artifact history; ignored when runId is not provided.")
58617
+ })
58618
+ );
58427
58619
  var listArtifactFilesInputSchema = projectScopedSchema.merge(
58428
58620
  external_exports.object({
58429
58621
  taskId: mrrlinTaskIdSchema.optional(),
@@ -58499,6 +58691,8 @@ var mcpToolInputSchemas = {
58499
58691
  [mcpToolNames.resetDirectorThread]: resetDirectorThreadInputSchema,
58500
58692
  [mcpToolNames.archiveTask]: archiveTaskInputSchema,
58501
58693
  [mcpToolNames.unarchiveTask]: unarchiveTaskInputSchema,
58694
+ [mcpToolNames.takeOverTask]: takeOverTaskInputSchema,
58695
+ [mcpToolNames.closeTaskAsWontDo]: closeTaskAsWontDoInputSchema,
58502
58696
  [mcpToolNames.updateWikiPage]: updateWikiPageInputSchema,
58503
58697
  [mcpToolNames.markSpecReady]: markSpecReadyInputSchema,
58504
58698
  [mcpToolNames.runSpecConsensus]: runSpecConsensusInputSchema,
@@ -58507,6 +58701,7 @@ var mcpToolInputSchemas = {
58507
58701
  [mcpToolNames.createInboxItem]: createInboxItemInputSchema,
58508
58702
  [mcpToolNames.decideInboxItem]: decideInboxItemInputSchema,
58509
58703
  [mcpToolNames.uploadArtifact]: uploadArtifactInputSchema,
58704
+ [mcpToolNames.deliverFile]: deliverFileInputSchema,
58510
58705
  [mcpToolNames.listArtifactFiles]: listArtifactFilesInputSchema,
58511
58706
  [mcpToolNames.reclaimExecutionRunLease]: reclaimExecutionRunLeaseInputSchema,
58512
58707
  [mcpToolNames.restartExecutionRun]: restartExecutionRunInputSchema,
@@ -58568,6 +58763,8 @@ var mcpToolOperationIds = {
58568
58763
  [mcpToolNames.resetDirectorThread]: openApiOperationIds.resetDirectorThread,
58569
58764
  [mcpToolNames.archiveTask]: openApiOperationIds.archiveTask,
58570
58765
  [mcpToolNames.unarchiveTask]: openApiOperationIds.unarchiveTask,
58766
+ [mcpToolNames.takeOverTask]: openApiOperationIds.takeOverTask,
58767
+ [mcpToolNames.closeTaskAsWontDo]: openApiOperationIds.closeTaskAsWontDo,
58571
58768
  [mcpToolNames.updateWikiPage]: openApiOperationIds.updateWikiPage,
58572
58769
  [mcpToolNames.markSpecReady]: openApiOperationIds.markSpecReady,
58573
58770
  [mcpToolNames.listInboxItems]: openApiOperationIds.listInboxItems,
@@ -58619,6 +58816,8 @@ var mcpToolDescriptions = {
58619
58816
  [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.",
58620
58817
  [mcpToolNames.archiveTask]: "Archive a task: hides it from default reads, removes it from plan progress and scheduler consideration, cancels its non-terminal execution runs, and closes its open inbox items. Reversible \u2014 the project owner can restore it with unarchive_task. Records an 'archived' task event.",
58621
58818
  [mcpToolNames.unarchiveTask]: "Restore an archived task to its original status and visibility. Owner-only \u2014 refused with NOT_PROJECT_OWNER for any other caller. Does not resurrect the execution runs or inbox items that were cancelled/closed when the task was archived. Records a 'restored' task event.",
58819
+ [mcpToolNames.takeOverTask]: "Take ownership of a task by assigning it to the provided assignee through the dedicated take-over route.",
58820
+ [mcpToolNames.closeTaskAsWontDo]: "Close a task as won't-do with a required human-readable reason recorded on the task.",
58622
58821
  [mcpToolNames.updateWikiPage]: "Update a markdown Wiki page.",
58623
58822
  [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.",
58624
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.',
@@ -58640,6 +58839,7 @@ var mcpToolDescriptions = {
58640
58839
  [mcpToolNames.createInboxItem]: "Create an operator-inbox item (proposal, check_in, question, handoff, or info) for a project.",
58641
58840
  [mcpToolNames.decideInboxItem]: "Approve, reject, or acknowledge an operator-inbox item. Rejection requires a reason.",
58642
58841
  [mcpToolNames.uploadArtifact]: "Upload a local file (md/txt/html/csv/json/pdf/png/jpg/webp/gif, <=5MB) as a project artifact. Returns a stable markdown link to embed in chat/spec/handoff text. Use class=temp for one-off hand-offs, class=durable for anything referenced by specs or evidence. Pass runId to record the file in that run's artifact history; pass taskId to surface it on the task's Artifacts list.",
58842
+ [mcpToolNames.deliverFile]: "This is the DEFAULT way to deliver a file the agent just composed; it takes the body inline as UTF-8 `content` in one call and needs no local file or writable path; `upload_artifact` is for a file that already exists on disk; use class=temp / class=durable and taskId/runId semantics with a 5MB cap.",
58643
58843
  [mcpToolNames.listArtifactFiles]: "List uploaded artifact files for the project (optionally filtered by taskId/runId), newest first.",
58644
58844
  [mcpToolNames.reclaimExecutionRunLease]: "Clear a stale pending execution-run lease when the lease has aged past staleSeconds. Returns the reclaimed run on success, null when the lease is not stale yet, and surfaces a structured conflict when reclaim retries are exhausted.",
58645
58845
  [mcpToolNames.restartExecutionRun]: "Restart a parked execution run (handoff_requested or handoff_queued) back to pending so the queue can resume it.",
@@ -58786,6 +58986,68 @@ function makeReadTool(client, name, fallbackCode, fallbackMessage, handler, proj
58786
58986
  }
58787
58987
  function createMrrlinTools(options) {
58788
58988
  const client = createMrrlinClient(withAuth(options));
58989
+ async function uploadArtifactBytes(c, args) {
58990
+ const { bytes: bytes3, class: artifactClass, contentType, description, filename, projectSlug, runId, taskId } = args;
58991
+ const created = await c.createArtifactFile(projectSlug, {
58992
+ class: artifactClass,
58993
+ contentType,
58994
+ filename,
58995
+ runId,
58996
+ sizeBytes: bytes3.byteLength,
58997
+ taskId
58998
+ });
58999
+ let stage = "upload";
59000
+ try {
59001
+ if (created.upload.kind === "presigned-post") {
59002
+ const form = new FormData();
59003
+ for (const [field, value] of Object.entries(created.upload.fields)) {
59004
+ form.append(field, value);
59005
+ }
59006
+ form.append("file", new Blob([bytes3], { type: contentType }), filename);
59007
+ const bareFetch = options.fetch ?? globalThis.fetch;
59008
+ const uploadResponse = await bareFetch(created.upload.url, { body: form, method: "POST" });
59009
+ if (!uploadResponse.ok) {
59010
+ throw new Error(
59011
+ `Presigned upload failed: ${uploadResponse.status} ${await uploadResponse.text()}`
59012
+ );
59013
+ }
59014
+ } else {
59015
+ await c.uploadArtifactFileContent(projectSlug, created.artifact.id, bytes3, contentType);
59016
+ }
59017
+ stage = "complete";
59018
+ const completed = await c.completeArtifactFile(projectSlug, created.artifact.id);
59019
+ if (completed.runId) {
59020
+ stage = "append-run-artifact";
59021
+ await c.appendExecutionArtifact(projectSlug, completed.runId, {
59022
+ id: `EA-${completed.id}`,
59023
+ kind: "file",
59024
+ payload: {
59025
+ artifactFileId: completed.id,
59026
+ class: completed.class,
59027
+ contentType: completed.contentType,
59028
+ description: description ?? null,
59029
+ filename: completed.filename,
59030
+ sizeBytes: completed.sizeBytes,
59031
+ stableUrl: created.stableUrl
59032
+ }
59033
+ });
59034
+ }
59035
+ const markdown = contentType.startsWith("image/") ? `![${completed.filename}](${created.stableUrl})` : `[${completed.filename}](${created.stableUrl})`;
59036
+ return {
59037
+ artifactFile: completed,
59038
+ markdown,
59039
+ stableUrl: created.stableUrl
59040
+ };
59041
+ } catch (error51) {
59042
+ const original = error51 instanceof MrrlinApiError ? error51.code : error51 instanceof McpToolCodedError ? error51.code : "ARTIFACT_FILE_UPLOAD_FAILED";
59043
+ const message = error51 instanceof Error ? error51.message : String(error51);
59044
+ const note = stage === "append-run-artifact" ? "file uploaded and completed successfully; only the run-history append failed \u2014 do NOT re-upload, reference the stableUrl directly" : "a pending row may remain; it is reaped automatically after 1h";
59045
+ throw new McpToolCodedError(
59046
+ original,
59047
+ `artifactFileId=${created.artifact.id} stableUrl=${created.stableUrl} (failed at ${stage}; ${note}): ${message}`
59048
+ );
59049
+ }
59050
+ }
58789
59051
  return {
58790
59052
  [mcpToolNames.archivePlan]: makeTool(
58791
59053
  client,
@@ -59066,6 +59328,20 @@ function createMrrlinTools(options) {
59066
59328
  "Unable to unarchive task.",
59067
59329
  async (c, { projectSlug, taskId }) => await c.unarchiveTask(projectSlug, taskId)
59068
59330
  ),
59331
+ [mcpToolNames.takeOverTask]: makeTool(
59332
+ client,
59333
+ mcpToolNames.takeOverTask,
59334
+ "TASK_TAKE_OVER_FAILED",
59335
+ "Unable to take over task.",
59336
+ async (c, { projectSlug, taskId, assignee }) => await c.takeOverTask(projectSlug, taskId, assignee)
59337
+ ),
59338
+ [mcpToolNames.closeTaskAsWontDo]: makeTool(
59339
+ client,
59340
+ mcpToolNames.closeTaskAsWontDo,
59341
+ "TASK_CLOSE_AS_WONT_DO_FAILED",
59342
+ "Unable to close task as won't-do.",
59343
+ async (c, { projectSlug, taskId, reason }) => await c.closeTaskAsWontDo(projectSlug, taskId, reason)
59344
+ ),
59069
59345
  [mcpToolNames.updateWikiPage]: makeTool(
59070
59346
  client,
59071
59347
  mcpToolNames.updateWikiPage,
@@ -59339,65 +59615,61 @@ function createMrrlinTools(options) {
59339
59615
  );
59340
59616
  }
59341
59617
  const filename = import_node_path20.default.basename(filePath);
59342
- const created = await c.createArtifactFile(projectSlug, {
59618
+ return uploadArtifactBytes(c, {
59619
+ bytes: bytes3,
59343
59620
  class: artifactClass,
59344
59621
  contentType,
59622
+ description,
59345
59623
  filename,
59624
+ projectSlug,
59346
59625
  runId,
59347
- sizeBytes: bytes3.byteLength,
59348
59626
  taskId
59349
59627
  });
59350
- let stage = "upload";
59351
- try {
59352
- if (created.upload.kind === "presigned-post") {
59353
- const form = new FormData();
59354
- for (const [field, value] of Object.entries(created.upload.fields)) {
59355
- form.append(field, value);
59356
- }
59357
- form.append("file", new Blob([bytes3], { type: contentType }), filename);
59358
- const bareFetch = options.fetch ?? globalThis.fetch;
59359
- const uploadResponse = await bareFetch(created.upload.url, { body: form, method: "POST" });
59360
- if (!uploadResponse.ok) {
59361
- throw new Error(
59362
- `Presigned upload failed: ${uploadResponse.status} ${await uploadResponse.text()}`
59363
- );
59364
- }
59365
- } else {
59366
- await c.uploadArtifactFileContent(projectSlug, created.artifact.id, bytes3, contentType);
59367
- }
59368
- stage = "complete";
59369
- const completed = await c.completeArtifactFile(projectSlug, created.artifact.id);
59370
- if (completed.runId) {
59371
- stage = "append-run-artifact";
59372
- await c.appendExecutionArtifact(projectSlug, completed.runId, {
59373
- id: `EA-${completed.id}`,
59374
- kind: "file",
59375
- payload: {
59376
- artifactFileId: completed.id,
59377
- class: completed.class,
59378
- contentType: completed.contentType,
59379
- description: description ?? null,
59380
- filename: completed.filename,
59381
- sizeBytes: completed.sizeBytes,
59382
- stableUrl: created.stableUrl
59383
- }
59384
- });
59385
- }
59386
- const markdown = contentType.startsWith("image/") ? `![${completed.filename}](${created.stableUrl})` : `[${completed.filename}](${created.stableUrl})`;
59387
- return {
59388
- artifactFile: completed,
59389
- markdown,
59390
- stableUrl: created.stableUrl
59391
- };
59392
- } catch (error51) {
59393
- const original = error51 instanceof MrrlinApiError ? error51.code : error51 instanceof McpToolCodedError ? error51.code : "ARTIFACT_FILE_UPLOAD_FAILED";
59394
- const message = error51 instanceof Error ? error51.message : String(error51);
59395
- const note = stage === "append-run-artifact" ? "file uploaded and completed successfully; only the run-history append failed \u2014 do NOT re-upload, reference the stableUrl directly" : "a pending row may remain; it is reaped automatically after 1h";
59628
+ }
59629
+ ),
59630
+ [mcpToolNames.deliverFile]: makeTool(
59631
+ client,
59632
+ mcpToolNames.deliverFile,
59633
+ "ARTIFACT_FILE_UPLOAD_FAILED",
59634
+ "Unable to deliver artifact file.",
59635
+ async (c, { projectSlug, filename, content, class: artifactClass, taskId, runId, description }) => {
59636
+ if (filename.includes("/") || filename.includes("\\") || filename.includes("..")) {
59637
+ throw new McpToolCodedError(
59638
+ "ARTIFACT_FILE_NAME_INVALID",
59639
+ `Invalid filename '${filename}'; provide a bare filename with no path separators or '..'.`
59640
+ );
59641
+ }
59642
+ const extension2 = import_node_path20.default.extname(filename).toLowerCase();
59643
+ const contentType = ARTIFACT_EXTENSION_TYPES[extension2];
59644
+ if (!contentType) {
59645
+ throw new McpToolCodedError(
59646
+ "ARTIFACT_FILE_TYPE_UNSUPPORTED",
59647
+ `Unsupported artifact extension '${extension2}'. Allowed: ${Object.keys(ARTIFACT_EXTENSION_TYPES).join(", ")}.`
59648
+ );
59649
+ }
59650
+ const bytes3 = new Uint8Array(Buffer.from(content, "utf8"));
59651
+ if (bytes3.byteLength === 0) {
59652
+ throw new McpToolCodedError(
59653
+ "ARTIFACT_FILE_EMPTY",
59654
+ "File is empty (0 bytes); write content before uploading."
59655
+ );
59656
+ }
59657
+ if (bytes3.byteLength > MRRLIN_ARTIFACT_FILE_MAX_BYTES) {
59396
59658
  throw new McpToolCodedError(
59397
- original,
59398
- `artifactFileId=${created.artifact.id} stableUrl=${created.stableUrl} (failed at ${stage}; ${note}): ${message}`
59659
+ "ARTIFACT_FILE_TOO_LARGE",
59660
+ "Artifact exceeds 5MB; split or summarize instead."
59399
59661
  );
59400
59662
  }
59663
+ return uploadArtifactBytes(c, {
59664
+ bytes: bytes3,
59665
+ class: artifactClass,
59666
+ contentType,
59667
+ description,
59668
+ filename,
59669
+ projectSlug,
59670
+ runId,
59671
+ taskId
59672
+ });
59401
59673
  }
59402
59674
  ),
59403
59675
  [mcpToolNames.listArtifactFiles]: makeTool(
@@ -59688,13 +59960,15 @@ function installService(deps) {
59688
59960
  }
59689
59961
  const resolvedBins = bins.filter((b) => b !== null);
59690
59962
  const pathEnv = Array.from(new Set(resolvedBins.map((b) => import_node_path21.default.dirname(b)))).join(":");
59963
+ const apiBaseUrl = deps.env.MRRLIN_API_BASE_URL?.trim() || void 0;
59964
+ const staging = deps.env.MRRLIN_STAGING === "1" || !apiBaseUrl && bridgeUrlImpliesStaging(deps.env.MRRLIN_BRIDGE_URL);
59691
59965
  const text = renderEcosystemConfig({
59692
59966
  cwd: resolved.cwd,
59693
59967
  home,
59694
59968
  codexHome: deps.env.CODEX_HOME?.trim() || void 0,
59695
59969
  pathEnv,
59696
- staging: deps.env.MRRLIN_STAGING === "1",
59697
- apiBaseUrl: deps.env.MRRLIN_API_BASE_URL?.trim() || void 0
59970
+ staging,
59971
+ apiBaseUrl
59698
59972
  });
59699
59973
  const ecoPath = import_node_path21.default.join(home, ".mrrlin", "ecosystem.config.cjs");
59700
59974
  deps.writeFile(ecoPath, text);
@@ -59817,22 +60091,22 @@ Usage:
59817
60091
  mrrlin-mcp install-browser Idempotently install the dedicated, pinned
59818
60092
  Chromium into ~/.mrrlin/browser/browsers (its own
59819
60093
  copy, isolated from the operator's Chrome and
59820
- codex-chrome). Run by install-codex; safe to
60094
+ codex-chrome). Run by configure bridge; safe to
59821
60095
  re-run. Exits non-zero on download failure.
59822
60096
 
59823
60097
  mrrlin-mcp browser-serve Launch the vendored @playwright/mcp against the
59824
60098
  dedicated persistent profile
59825
60099
  (~/.mrrlin/browser/profile, headed). This is the
59826
- command install-codex wires for the
60100
+ command configure bridge wires for the
59827
60101
  [mcp_servers.mrrlin-browser] block.
59828
60102
 
59829
60103
  mrrlin-mcp external-agents-serve
59830
60104
  Launch the bundled @mrrlin-dev/external-agents
59831
60105
  MCP server (multi-LLM dispatch pool). This is the
59832
- command install-codex wires for the
60106
+ command configure bridge wires for the
59833
60107
  [mcp_servers.external-agents] block.
59834
60108
 
59835
- mrrlin-mcp install-codex Idempotently register Mrrlin in
60109
+ mrrlin-mcp configure bridge Idempotently register Mrrlin in
59836
60110
  [--force] ~/.codex/config.toml (or CODEX_HOME). Adds an
59837
60111
  [mcp_servers.mrrlin] block. Development
59838
60112
  checkouts register the local dist/bin.cjs;
@@ -59841,12 +60115,12 @@ Usage:
59841
60115
  Bakes API-target env vars into the block as an
59842
60116
  inline env table so \`serve\` boots even when the
59843
60117
  local 127.0.0.1:8787 is not running. Setup guide:
59844
- MRRLIN_STAGING=1 mrrlin-mcp install-codex
60118
+ MRRLIN_STAGING=1 mrrlin-mcp configure bridge
59845
60119
  is the canonical path (staging alias + remote-
59846
60120
  allow gate in one flag). For a non-staging remote,
59847
60121
  pass BOTH the URL AND the allow gate:
59848
60122
  MRRLIN_API_BASE_URL=https://... \\
59849
- MRRLIN_API_ALLOW_REMOTE=1 mrrlin-mcp install-codex
60123
+ MRRLIN_API_ALLOW_REMOTE=1 mrrlin-mcp configure bridge
59850
60124
  Without an allow gate, \`serve\` still refuses any
59851
60125
  non-loopback URL (see \`mrrlin-mcp serve\` above).
59852
60126
  URL and MRRLIN_STAGING are mutually exclusive at
@@ -59959,8 +60233,8 @@ Usage:
59959
60233
  mrrlin-mcp --help Show this help.
59960
60234
 
59961
60235
  Operator notes:
59962
- - install-codex is idempotent: a second run with the same target is a no-op.
59963
- - install-codex auto-detects how it was installed. Development checkouts keep
60236
+ - configure bridge is idempotent: a second run with the same target is a no-op.
60237
+ - configure bridge auto-detects how it was installed. Development checkouts keep
59964
60238
  using the local dist/bin.cjs; npm installs use
59965
60239
  \`mrrlin-mcp serve\`.
59966
60240
  `;
@@ -59995,16 +60269,29 @@ async function main() {
59995
60269
  process.exitCode = r.status ?? 1;
59996
60270
  return;
59997
60271
  }
59998
- case "install-codex": {
59999
- const force = rest.includes("--force");
60272
+ case "configure": {
60273
+ const [target, ...configureRest] = rest;
60274
+ if (target !== "bridge") {
60275
+ process.stderr.write(
60276
+ `configure requires a target. The only supported form is:
60277
+ mrrlin-mcp configure bridge [--force]
60278
+ `
60279
+ );
60280
+ if (target !== void 0) {
60281
+ process.stderr.write(`Unknown configure target: ${target}
60282
+ `);
60283
+ }
60284
+ process.exit(2);
60285
+ }
60286
+ const force = configureRest.includes("--force");
60000
60287
  const binPath = resolveSelfBinPath();
60001
60288
  try {
60002
60289
  const b = await installBrowser();
60003
- process.stderr.write(`[mrrlin-mcp install-codex] install-browser ${b.action} ${b.browsersPath}
60290
+ process.stderr.write(`[mrrlin-mcp configure bridge] install-browser ${b.action} ${b.browsersPath}
60004
60291
  `);
60005
60292
  } catch (error51) {
60006
60293
  process.stderr.write(
60007
- `[mrrlin-mcp install-codex] install-browser FAILED \u2014 config.toml left untouched: ${error51 instanceof Error ? error51.message : String(error51)}
60294
+ `[mrrlin-mcp configure bridge] install-browser FAILED \u2014 config.toml left untouched: ${error51 instanceof Error ? error51.message : String(error51)}
60008
60295
  `
60009
60296
  );
60010
60297
  process.exit(2);
@@ -60015,18 +60302,18 @@ async function main() {
60015
60302
  force,
60016
60303
  serveEnv
60017
60304
  });
60018
- process.stderr.write(`[mrrlin-mcp install-codex] ${result.action} ${result.configPath}
60305
+ process.stderr.write(`[mrrlin-mcp configure bridge] ${result.action} ${result.configPath}
60019
60306
  `);
60020
60307
  for (const preserved of result.preservedEnvBlocks) {
60021
60308
  process.stderr.write(`${renderEnvPreservedNotice(preserved.blockId, preserved.envKeys)}
60022
60309
  `);
60023
60310
  }
60024
60311
  for (const p of result.prompts) {
60025
- process.stderr.write(`[mrrlin-mcp install-codex] prompt ${p.name}: ${p.action} ${p.promptPath}
60312
+ process.stderr.write(`[mrrlin-mcp configure bridge] prompt ${p.name}: ${p.action} ${p.promptPath}
60026
60313
  `);
60027
60314
  if (p.action === "skipped-modified") {
60028
60315
  process.stderr.write(
60029
- `[mrrlin-mcp install-codex] prompt ${p.name}: kept your local edits; run with --force-prompts to overwrite (or delete ${p.promptPath} to take the new bundled version)
60316
+ `[mrrlin-mcp configure bridge] prompt ${p.name}: kept your local edits; run with --force-prompts to overwrite (or delete ${p.promptPath} to take the new bundled version)
60030
60317
  `
60031
60318
  );
60032
60319
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.3.68",
3
+ "version": "0.4.1",
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/wiki": "0.0.0",
25
- "@mrrlin/schemas": "0.0.0",
26
24
  "@mrrlin/client": "0.0.0",
27
- "@mrrlin/tsconfig": "0.0.0",
25
+ "@mrrlin/codex-client": "0.0.0",
28
26
  "@mrrlin/director-e2e": "0.0.0",
29
- "@mrrlin/codex-client": "0.0.0"
27
+ "@mrrlin/schemas": "0.0.0",
28
+ "@mrrlin/tsconfig": "0.0.0",
29
+ "@mrrlin/wiki": "0.0.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "@iarna/toml": "^2.2.5",
@@ -47,7 +47,7 @@
47
47
  "dev": "tsx watch src/bin.ts serve",
48
48
  "lint": "eslint .",
49
49
  "start": "node dist/bin.cjs serve",
50
- "test": "node scripts/write-version.mjs && node --import tsx --test $(find src -name '*.test.ts' | sort | tr '\\n' ' ')",
50
+ "test": "pnpm --filter @mrrlin/codex-client build && node scripts/write-version.mjs && node --import tsx --test $(find src -name '*.test.ts' | sort | tr '\\n' ' ')",
51
51
  "typecheck": "node scripts/write-version.mjs && pnpm --filter @mrrlin/schemas build && pnpm --filter @mrrlin/client build && pnpm --filter @mrrlin/wiki build && pnpm --filter @mrrlin/codex-client build && pnpm --filter @mrrlin/director-e2e build && tsc -p tsconfig.json --noEmit"
52
52
  }
53
53
  }