@mrrlin-dev/mcp 0.3.68 → 0.4.0

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 +180 -29
  2. package/package.json +5 -5
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",
@@ -36889,6 +36904,32 @@ var SUPPORTED_EXECUTION_DOMAINS = Object.keys(SUPPORTED_EXECUTION_POLICY_BY_DOMA
36889
36904
  var taxonomyCategorySchema = external_exports.string().min(1).regex(/^[a-z][a-z0-9-]*$/);
36890
36905
  var taxonomySubcategorySchema = external_exports.string().min(1).regex(/^[a-z][a-z0-9-]*$/);
36891
36906
  var mrrlinTaskAssigneeSchema = external_exports.union([external_exports.string().regex(/^@/), external_exports.string().email()]).nullable();
36907
+ var mrrlinStopReasonSchema = external_exports.enum([
36908
+ "task_terminal",
36909
+ "active_run_exists",
36910
+ "draft_running",
36911
+ "consensus_running",
36912
+ "spec_changed_during_advance",
36913
+ "run_limit_reached",
36914
+ "backoff_pending",
36915
+ "blocked_by_active",
36916
+ "spec_missing",
36917
+ "spec_draft_failed",
36918
+ "spec_not_specs_folder",
36919
+ "awaiting_clarification",
36920
+ "consensus_blocked",
36921
+ "consensus_run_failed",
36922
+ "consensus_disabled_spec_not_ready",
36923
+ "spec_approval_pending",
36924
+ "awaiting_handoff",
36925
+ "assigned_to_other",
36926
+ "attempts_exhausted",
36927
+ "daily_job_budget_exhausted",
36928
+ "taken_over",
36929
+ "operator_absent",
36930
+ "autonomy_paused",
36931
+ "unknown"
36932
+ ]);
36892
36933
  var mrrlinTaskSchema = external_exports.object({
36893
36934
  // Output-optional (not defaulted-required) so existing task fixtures/mocks across the
36894
36935
  // codebase need no churn; DB rows always resolve it to `string | null` via taskRowToTask.
@@ -36903,6 +36944,9 @@ var mrrlinTaskSchema = external_exports.object({
36903
36944
  autonomyReason: external_exports.string().min(1).max(240),
36904
36945
  blockedBy: external_exports.array(mrrlinTaskIdSchema),
36905
36946
  category: taxonomyCategorySchema,
36947
+ // Slice 3 (task-stall-visibility): why a task was closed as won't-do -- populated only by
36948
+ // closeTaskAsWontDo. Output-optional so existing task fixtures need no churn.
36949
+ closeReason: external_exports.string().nullable().optional(),
36906
36950
  directorThreadId: external_exports.string().nullable().default(null),
36907
36951
  dueDate: external_exports.string().date().nullable(),
36908
36952
  followUpAt: external_exports.string().datetime().nullable(),
@@ -36911,6 +36955,9 @@ var mrrlinTaskSchema = external_exports.object({
36911
36955
  latestJudgement: external_exports.string().nullable().default(null),
36912
36956
  latestJudgementAt: external_exports.string().datetime().nullable().default(null),
36913
36957
  latestJudgementKind: external_exports.enum(["autonomy", "handoff"]).nullable().default(null),
36958
+ // Slice 3: the retry-backoff timer for exactly spec_draft_failed/consensus_run_failed -- a task
36959
+ // whose next_attempt_at is in the future is skipped from admission (backoff_pending).
36960
+ nextAttemptAt: external_exports.string().datetime().nullable().optional(),
36914
36961
  notes: external_exports.string().nullable().default(null),
36915
36962
  // Raw operator request captured at task creation (operator-authored ONLY, never Director-generated).
36916
36963
  // The intent-aware spec gate judges the spec against this; null/absent ⇒ the gate fail-closes.
@@ -36920,6 +36967,14 @@ var mrrlinTaskSchema = external_exports.object({
36920
36967
  planId: mrrlinPlanIdSchema.nullable(),
36921
36968
  specApproved: mrrlinSpecApprovedSchema.optional(),
36922
36969
  specWikiPageId: mrrlinWikiPageIdSchema.nullable(),
36970
+ // Slice 3: episode bookkeeping alongside the async_jobs-derived countEpisodeAttempts --
36971
+ // stallAttemptCount backs the exponential-backoff formula, stallRearmed guards against
36972
+ // re-arming the same episode's ceiling more than once.
36973
+ stallAttemptCount: external_exports.number().int().nonnegative().optional(),
36974
+ stallRearmed: external_exports.boolean().optional(),
36975
+ stallNextAction: external_exports.string().nullable().optional(),
36976
+ stallReason: mrrlinStopReasonSchema.nullable().optional(),
36977
+ stallSince: external_exports.string().datetime().nullable().optional(),
36923
36978
  status: mrrlinTaskStatusSchema,
36924
36979
  subcategory: taxonomySubcategorySchema,
36925
36980
  tags: mrrlinTaskTagsSchema,
@@ -36982,6 +37037,12 @@ var mrrlinWikiPageSchema = external_exports.object({
36982
37037
  var mrrlinMarkSpecReadyRequestSchema = external_exports.object({
36983
37038
  ready: external_exports.boolean()
36984
37039
  });
37040
+ var mrrlinTakeOverTaskRequestSchema = external_exports.object({
37041
+ assignee: external_exports.string().min(1)
37042
+ });
37043
+ var mrrlinCloseTaskAsWontDoRequestSchema = external_exports.object({
37044
+ reason: external_exports.string().min(1)
37045
+ });
36985
37046
  var mrrlinGithubBindingSchema = external_exports.object({
36986
37047
  appInstallationState: external_exports.enum(["installed", "missing"]),
36987
37048
  installationId: external_exports.number().int().positive().nullable().default(null),
@@ -37997,7 +38058,8 @@ var mrrlinInFlightSchedulerEntrySchema = external_exports.discriminatedUnion("ki
37997
38058
  ]);
37998
38059
  var mrrlinIneligibleTaskSchema = external_exports.object({
37999
38060
  task: mrrlinTaskSchema,
38000
- autonomyLevel: mrrlinAutonomyLevelSchema
38061
+ autonomyLevel: mrrlinAutonomyLevelSchema,
38062
+ reason: external_exports.string().nullable().optional()
38001
38063
  });
38002
38064
  var schedulerPendingTasksResponseSchema = external_exports.object({
38003
38065
  data: external_exports.object({
@@ -38021,6 +38083,12 @@ var schedulerPendingTasksResponseSchema = external_exports.object({
38021
38083
  // person actually running the project) is visible as a large number instead of the autopilot
38022
38084
  // silently doing nothing.
38023
38085
  excludedByOwnershipCount: external_exports.number().int().min(0),
38086
+ // Additive: the same ownership exclusion grouped by assignee. This reveals who the
38087
+ // autopilot is waiting for without returning task ids or titles in this endpoint.
38088
+ ownershipExcludedAssignees: external_exports.array(external_exports.object({
38089
+ assignee: external_exports.string(),
38090
+ count: external_exports.number().int().min(1)
38091
+ })),
38024
38092
  // Additive: true when the operator-web-presence gate is the reason nothing new is starting
38025
38093
  // (autonomyPaused takes precedence when both apply). Computed by the same shared helper the
38026
38094
  // tick uses, so this page can never claim a task is starting when the tick would decline it.
@@ -38124,6 +38192,7 @@ var mrrlinInboxItemSchema = external_exports.object({
38124
38192
  projectSlug: mrrlinProjectSlugSchema,
38125
38193
  questionOptions: mrrlinInboxQuestionOptionsSchema.nullable().default(null),
38126
38194
  readAt: external_exports.string().datetime().nullable(),
38195
+ reason: external_exports.string().nullable().optional(),
38127
38196
  status: mrrlinInboxItemStatusSchema,
38128
38197
  taskId: mrrlinTaskIdSchema.nullable()
38129
38198
  }).superRefine((value, context) => {
@@ -38970,6 +39039,22 @@ function createMrrlinClient(config2) {
38970
39039
  });
38971
39040
  return taskResponseSchema.parse(body).data;
38972
39041
  },
39042
+ async takeOverTask(projectSlug, taskId, assignee) {
39043
+ const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/take-over`, {
39044
+ method: "POST",
39045
+ headers: { "content-type": "application/json" },
39046
+ body: JSON.stringify({ assignee })
39047
+ });
39048
+ return taskResponseSchema.parse(body).data;
39049
+ },
39050
+ async closeTaskAsWontDo(projectSlug, taskId, reason) {
39051
+ const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/wont-do`, {
39052
+ method: "POST",
39053
+ headers: { "content-type": "application/json" },
39054
+ body: JSON.stringify({ reason })
39055
+ });
39056
+ return taskResponseSchema.parse(body).data;
39057
+ },
38973
39058
  async updateWikiPage(projectSlug, pageId, patch) {
38974
39059
  const body = await request(`${projectPath(projectSlug)}/wiki/${encodeURIComponent(pageId)}`, {
38975
39060
  body: JSON.stringify(patch),
@@ -40050,7 +40135,7 @@ function startCodexProbe(deps = {}) {
40050
40135
  }
40051
40136
 
40052
40137
  // src/_generated/version.ts
40053
- var PKG_VERSION = "0.3.68";
40138
+ var PKG_VERSION = "0.4.0";
40054
40139
 
40055
40140
  // src/api-base-url-migrate.ts
40056
40141
  var import_node_fs2 = __toESM(require("node:fs"), 1);
@@ -43992,9 +44077,14 @@ var BASE_INSTRUCTIONS_PREFIX = [
43992
44077
  " if you are blocked and a human must decide before the work can continue, leave it at `handoff_needed`.",
43993
44078
  "",
43994
44079
  "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.",
44080
+ "- Decide BEFORE writing your result, by PROVENANCE: output assembled from data you RETRIEVED",
44081
+ " this run (API, browser, analytics, database, a search over project records) that comes out as",
44082
+ " a multi-record or multi-dimension result is a FILE, not inline text.",
44083
+ "- Your own synthesis or comparison stays inline however structured it looks, and so does a",
44084
+ " retrieval yielding one value. Both conditions must hold before it becomes a file. When it",
44085
+ " qualifies, produce the file; never merely offer one.",
44086
+ "- Mixed output counts as retrieved: if any material part came from a retrieval, treat the whole",
44087
+ " result as retrieved rather than splitting it between inline text and a file.",
43998
44088
  "- When your output is a file (markdown report, HTML, screenshot), upload it with",
43999
44089
  " `upload_artifact` and embed the returned markdown link \u2014 never inline large blobs",
44000
44090
  " into chat, spec, or note text.",
@@ -50959,9 +51049,17 @@ function buildThreadStartParams(msg, config2, desiredModel) {
50959
51049
  " is not measurable, re-set `verificationDueAt` to a new date with a reason.",
50960
51050
  "",
50961
51051
  "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.",
51052
+ "- Decide by PROVENANCE, not by predicting what the operator will do later. If this answer is",
51053
+ " assembled from data you RETRIEVED this turn (an HTTP/API call, the browser, analytics, a",
51054
+ " database, a search over project records) AND it comes out as a multi-record or",
51055
+ " multi-dimension result rather than one fact or a few values, it belongs in a FILE.",
51056
+ "- Your own synthesis, comparison, or judgement belongs in chat, however structured it looks.",
51057
+ " A retrieval that yields one number or one line also belongs in chat. Both conditions must",
51058
+ " hold before it becomes a file.",
51059
+ "- Mixed answers count as retrieved: if any material part of the result came from a retrieval,",
51060
+ " treat the whole answer as retrieved rather than splitting it between chat and a file.",
51061
+ '- When it qualifies, PRODUCE the file. Offering one ("I can give you this as CSV/JSON") is the',
51062
+ " failure, not the courtesy.",
50965
51063
  "- When it is a file: write it (`.md` for linear prose, `.html` when structure or layout",
50966
51064
  " carries meaning), upload with `upload_artifact` (class=temp for one-off downloads,",
50967
51065
  " class=durable when a spec/evidence page references it; pass taskId when known), and paste",
@@ -52133,7 +52231,18 @@ function createBridgeMessageHandler(deps) {
52133
52231
  });
52134
52232
  const mapper = thread.mapper;
52135
52233
  mapper.reset();
52136
- const contextPrefix = renderContextPrefix({ ...msg.context, directorSessionId });
52234
+ const contextFocusTaskId = msg.context?.focusTaskId?.trim();
52235
+ const contextProjectSlug = msg.context?.projectSlug?.trim();
52236
+ let answeredQuestions;
52237
+ if (deps.listAnsweredQuestions && contextFocusTaskId && contextProjectSlug) {
52238
+ try {
52239
+ const acknowledged = await deps.listAnsweredQuestions(contextProjectSlug, contextFocusTaskId);
52240
+ const withAnswers = acknowledged.filter((item) => Boolean(item.answer)).map((item) => ({ answer: item.answer, question: item.body }));
52241
+ answeredQuestions = withAnswers.length ? withAnswers : void 0;
52242
+ } catch {
52243
+ }
52244
+ }
52245
+ const contextPrefix = renderContextPrefix({ ...msg.context, answeredQuestions, directorSessionId });
52137
52246
  const sendInTurn = (msg2) => {
52138
52247
  const buf = thread.replay;
52139
52248
  if (!buf) {
@@ -52819,7 +52928,7 @@ async function startDirectorBridge() {
52819
52928
  if (!body) return;
52820
52929
  const id = `II-MCPUPD-${(0, import_node_crypto8.randomUUID)()}`;
52821
52930
  try {
52822
- const row = await client.createInboxItem(projectSlug, { id, kind: "question", body });
52931
+ const row = await client.createInboxItem(projectSlug, { id, kind: "info", body });
52823
52932
  process.stderr.write(
52824
52933
  `[mrrlin-mcp director-bridge] info bridge.self_update.inbox_filed ${JSON.stringify({
52825
52934
  projectSlug,
@@ -52839,6 +52948,7 @@ async function startDirectorBridge() {
52839
52948
  );
52840
52949
  }
52841
52950
  },
52951
+ listAnsweredQuestions: (projectSlug, taskId) => client.listInboxItems(projectSlug, { status: "acknowledged", kind: "question", taskId }),
52842
52952
  // External-agents control plane: the real spawn runner, with the EXTERNAL_AGENTS_CLI operator
52843
52953
  // escape hatch bound here (the handlers stay spawn-free and this is the only production seam).
52844
52954
  runExternalAgentsCli: (args, opts) => runExternalAgentsCli(args, { ...opts, envCliPath: process.env["EXTERNAL_AGENTS_CLI"] })
@@ -53253,11 +53363,13 @@ function renderContextPrefix(context) {
53253
53363
  const urlPath = context?.urlPath?.trim() ? context.urlPath.trim() : void 0;
53254
53364
  const focusTaskId = context?.focusTaskId?.trim() ? context.focusTaskId.trim() : void 0;
53255
53365
  const directorSessionId = context?.directorSessionId?.trim() ? context.directorSessionId.trim() : void 0;
53366
+ const answeredQuestions = context?.answeredQuestions?.length ? context.answeredQuestions : void 0;
53256
53367
  const payload = {};
53257
53368
  if (projectSlug) payload.projectSlug = projectSlug;
53258
53369
  if (urlPath) payload.urlPath = urlPath;
53259
53370
  if (focusTaskId) payload.focusTaskId = focusTaskId;
53260
53371
  if (directorSessionId) payload.directorSessionId = directorSessionId;
53372
+ if (answeredQuestions) payload.answeredQuestions = answeredQuestions;
53261
53373
  const keys = Object.keys(payload);
53262
53374
  if (keys.length === 0) return "";
53263
53375
  return `Context (JSON): ${JSON.stringify(payload)}`;
@@ -57836,6 +57948,8 @@ var mcpToolNames = {
57836
57948
  resetDirectorThread: "reset_director_thread",
57837
57949
  archiveTask: "archive_task",
57838
57950
  unarchiveTask: "unarchive_task",
57951
+ takeOverTask: "take_over_task",
57952
+ closeTaskAsWontDo: "close_task_as_wont_do",
57839
57953
  updateWikiPage: "update_wiki_page",
57840
57954
  markSpecReady: "mark_spec_ready",
57841
57955
  runSpecConsensus: "run_spec_consensus",
@@ -58342,6 +58456,8 @@ var updateTaskInputSchema = taskScopedSchema.merge(mrrlinTaskUpdateSchema);
58342
58456
  var resetDirectorThreadInputSchema = taskScopedSchema;
58343
58457
  var archiveTaskInputSchema = taskScopedSchema;
58344
58458
  var unarchiveTaskInputSchema = taskScopedSchema;
58459
+ var takeOverTaskInputSchema = taskScopedSchema.merge(mrrlinTakeOverTaskRequestSchema);
58460
+ var closeTaskAsWontDoInputSchema = taskScopedSchema.merge(mrrlinCloseTaskAsWontDoRequestSchema);
58345
58461
  var listPlansInputSchema = projectScopedSchema;
58346
58462
  var createPlanInputSchema = projectScopedSchema.merge(mrrlinPlanCreateSchema);
58347
58463
  var updatePlanInputSchema = planScopedSchema.merge(mrrlinPlanUpdateSchema);
@@ -58499,6 +58615,8 @@ var mcpToolInputSchemas = {
58499
58615
  [mcpToolNames.resetDirectorThread]: resetDirectorThreadInputSchema,
58500
58616
  [mcpToolNames.archiveTask]: archiveTaskInputSchema,
58501
58617
  [mcpToolNames.unarchiveTask]: unarchiveTaskInputSchema,
58618
+ [mcpToolNames.takeOverTask]: takeOverTaskInputSchema,
58619
+ [mcpToolNames.closeTaskAsWontDo]: closeTaskAsWontDoInputSchema,
58502
58620
  [mcpToolNames.updateWikiPage]: updateWikiPageInputSchema,
58503
58621
  [mcpToolNames.markSpecReady]: markSpecReadyInputSchema,
58504
58622
  [mcpToolNames.runSpecConsensus]: runSpecConsensusInputSchema,
@@ -58568,6 +58686,8 @@ var mcpToolOperationIds = {
58568
58686
  [mcpToolNames.resetDirectorThread]: openApiOperationIds.resetDirectorThread,
58569
58687
  [mcpToolNames.archiveTask]: openApiOperationIds.archiveTask,
58570
58688
  [mcpToolNames.unarchiveTask]: openApiOperationIds.unarchiveTask,
58689
+ [mcpToolNames.takeOverTask]: openApiOperationIds.takeOverTask,
58690
+ [mcpToolNames.closeTaskAsWontDo]: openApiOperationIds.closeTaskAsWontDo,
58571
58691
  [mcpToolNames.updateWikiPage]: openApiOperationIds.updateWikiPage,
58572
58692
  [mcpToolNames.markSpecReady]: openApiOperationIds.markSpecReady,
58573
58693
  [mcpToolNames.listInboxItems]: openApiOperationIds.listInboxItems,
@@ -58619,6 +58739,8 @@ var mcpToolDescriptions = {
58619
58739
  [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
58740
  [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
58741
  [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.",
58742
+ [mcpToolNames.takeOverTask]: "Take ownership of a task by assigning it to the provided assignee through the dedicated take-over route.",
58743
+ [mcpToolNames.closeTaskAsWontDo]: "Close a task as won't-do with a required human-readable reason recorded on the task.",
58622
58744
  [mcpToolNames.updateWikiPage]: "Update a markdown Wiki page.",
58623
58745
  [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
58746
  [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.',
@@ -59066,6 +59188,20 @@ function createMrrlinTools(options) {
59066
59188
  "Unable to unarchive task.",
59067
59189
  async (c, { projectSlug, taskId }) => await c.unarchiveTask(projectSlug, taskId)
59068
59190
  ),
59191
+ [mcpToolNames.takeOverTask]: makeTool(
59192
+ client,
59193
+ mcpToolNames.takeOverTask,
59194
+ "TASK_TAKE_OVER_FAILED",
59195
+ "Unable to take over task.",
59196
+ async (c, { projectSlug, taskId, assignee }) => await c.takeOverTask(projectSlug, taskId, assignee)
59197
+ ),
59198
+ [mcpToolNames.closeTaskAsWontDo]: makeTool(
59199
+ client,
59200
+ mcpToolNames.closeTaskAsWontDo,
59201
+ "TASK_CLOSE_AS_WONT_DO_FAILED",
59202
+ "Unable to close task as won't-do.",
59203
+ async (c, { projectSlug, taskId, reason }) => await c.closeTaskAsWontDo(projectSlug, taskId, reason)
59204
+ ),
59069
59205
  [mcpToolNames.updateWikiPage]: makeTool(
59070
59206
  client,
59071
59207
  mcpToolNames.updateWikiPage,
@@ -59688,13 +59824,15 @@ function installService(deps) {
59688
59824
  }
59689
59825
  const resolvedBins = bins.filter((b) => b !== null);
59690
59826
  const pathEnv = Array.from(new Set(resolvedBins.map((b) => import_node_path21.default.dirname(b)))).join(":");
59827
+ const apiBaseUrl = deps.env.MRRLIN_API_BASE_URL?.trim() || void 0;
59828
+ const staging = deps.env.MRRLIN_STAGING === "1" || !apiBaseUrl && bridgeUrlImpliesStaging(deps.env.MRRLIN_BRIDGE_URL);
59691
59829
  const text = renderEcosystemConfig({
59692
59830
  cwd: resolved.cwd,
59693
59831
  home,
59694
59832
  codexHome: deps.env.CODEX_HOME?.trim() || void 0,
59695
59833
  pathEnv,
59696
- staging: deps.env.MRRLIN_STAGING === "1",
59697
- apiBaseUrl: deps.env.MRRLIN_API_BASE_URL?.trim() || void 0
59834
+ staging,
59835
+ apiBaseUrl
59698
59836
  });
59699
59837
  const ecoPath = import_node_path21.default.join(home, ".mrrlin", "ecosystem.config.cjs");
59700
59838
  deps.writeFile(ecoPath, text);
@@ -59817,22 +59955,22 @@ Usage:
59817
59955
  mrrlin-mcp install-browser Idempotently install the dedicated, pinned
59818
59956
  Chromium into ~/.mrrlin/browser/browsers (its own
59819
59957
  copy, isolated from the operator's Chrome and
59820
- codex-chrome). Run by install-codex; safe to
59958
+ codex-chrome). Run by configure bridge; safe to
59821
59959
  re-run. Exits non-zero on download failure.
59822
59960
 
59823
59961
  mrrlin-mcp browser-serve Launch the vendored @playwright/mcp against the
59824
59962
  dedicated persistent profile
59825
59963
  (~/.mrrlin/browser/profile, headed). This is the
59826
- command install-codex wires for the
59964
+ command configure bridge wires for the
59827
59965
  [mcp_servers.mrrlin-browser] block.
59828
59966
 
59829
59967
  mrrlin-mcp external-agents-serve
59830
59968
  Launch the bundled @mrrlin-dev/external-agents
59831
59969
  MCP server (multi-LLM dispatch pool). This is the
59832
- command install-codex wires for the
59970
+ command configure bridge wires for the
59833
59971
  [mcp_servers.external-agents] block.
59834
59972
 
59835
- mrrlin-mcp install-codex Idempotently register Mrrlin in
59973
+ mrrlin-mcp configure bridge Idempotently register Mrrlin in
59836
59974
  [--force] ~/.codex/config.toml (or CODEX_HOME). Adds an
59837
59975
  [mcp_servers.mrrlin] block. Development
59838
59976
  checkouts register the local dist/bin.cjs;
@@ -59841,12 +59979,12 @@ Usage:
59841
59979
  Bakes API-target env vars into the block as an
59842
59980
  inline env table so \`serve\` boots even when the
59843
59981
  local 127.0.0.1:8787 is not running. Setup guide:
59844
- MRRLIN_STAGING=1 mrrlin-mcp install-codex
59982
+ MRRLIN_STAGING=1 mrrlin-mcp configure bridge
59845
59983
  is the canonical path (staging alias + remote-
59846
59984
  allow gate in one flag). For a non-staging remote,
59847
59985
  pass BOTH the URL AND the allow gate:
59848
59986
  MRRLIN_API_BASE_URL=https://... \\
59849
- MRRLIN_API_ALLOW_REMOTE=1 mrrlin-mcp install-codex
59987
+ MRRLIN_API_ALLOW_REMOTE=1 mrrlin-mcp configure bridge
59850
59988
  Without an allow gate, \`serve\` still refuses any
59851
59989
  non-loopback URL (see \`mrrlin-mcp serve\` above).
59852
59990
  URL and MRRLIN_STAGING are mutually exclusive at
@@ -59959,8 +60097,8 @@ Usage:
59959
60097
  mrrlin-mcp --help Show this help.
59960
60098
 
59961
60099
  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
60100
+ - configure bridge is idempotent: a second run with the same target is a no-op.
60101
+ - configure bridge auto-detects how it was installed. Development checkouts keep
59964
60102
  using the local dist/bin.cjs; npm installs use
59965
60103
  \`mrrlin-mcp serve\`.
59966
60104
  `;
@@ -59995,16 +60133,29 @@ async function main() {
59995
60133
  process.exitCode = r.status ?? 1;
59996
60134
  return;
59997
60135
  }
59998
- case "install-codex": {
59999
- const force = rest.includes("--force");
60136
+ case "configure": {
60137
+ const [target, ...configureRest] = rest;
60138
+ if (target !== "bridge") {
60139
+ process.stderr.write(
60140
+ `configure requires a target. The only supported form is:
60141
+ mrrlin-mcp configure bridge [--force]
60142
+ `
60143
+ );
60144
+ if (target !== void 0) {
60145
+ process.stderr.write(`Unknown configure target: ${target}
60146
+ `);
60147
+ }
60148
+ process.exit(2);
60149
+ }
60150
+ const force = configureRest.includes("--force");
60000
60151
  const binPath = resolveSelfBinPath();
60001
60152
  try {
60002
60153
  const b = await installBrowser();
60003
- process.stderr.write(`[mrrlin-mcp install-codex] install-browser ${b.action} ${b.browsersPath}
60154
+ process.stderr.write(`[mrrlin-mcp configure bridge] install-browser ${b.action} ${b.browsersPath}
60004
60155
  `);
60005
60156
  } catch (error51) {
60006
60157
  process.stderr.write(
60007
- `[mrrlin-mcp install-codex] install-browser FAILED \u2014 config.toml left untouched: ${error51 instanceof Error ? error51.message : String(error51)}
60158
+ `[mrrlin-mcp configure bridge] install-browser FAILED \u2014 config.toml left untouched: ${error51 instanceof Error ? error51.message : String(error51)}
60008
60159
  `
60009
60160
  );
60010
60161
  process.exit(2);
@@ -60015,18 +60166,18 @@ async function main() {
60015
60166
  force,
60016
60167
  serveEnv
60017
60168
  });
60018
- process.stderr.write(`[mrrlin-mcp install-codex] ${result.action} ${result.configPath}
60169
+ process.stderr.write(`[mrrlin-mcp configure bridge] ${result.action} ${result.configPath}
60019
60170
  `);
60020
60171
  for (const preserved of result.preservedEnvBlocks) {
60021
60172
  process.stderr.write(`${renderEnvPreservedNotice(preserved.blockId, preserved.envKeys)}
60022
60173
  `);
60023
60174
  }
60024
60175
  for (const p of result.prompts) {
60025
- process.stderr.write(`[mrrlin-mcp install-codex] prompt ${p.name}: ${p.action} ${p.promptPath}
60176
+ process.stderr.write(`[mrrlin-mcp configure bridge] prompt ${p.name}: ${p.action} ${p.promptPath}
60026
60177
  `);
60027
60178
  if (p.action === "skipped-modified") {
60028
60179
  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)
60180
+ `[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
60181
  `
60031
60182
  );
60032
60183
  }
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.0",
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",