@mrrlin-dev/mcp 0.3.67 → 0.3.68

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 +355 -65
  2. package/package.json +6 -6
package/dist/bin.cjs CHANGED
@@ -20658,14 +20658,17 @@ var toml = __toESM(require_toml(), 1);
20658
20658
 
20659
20659
  // src/api-base-url.ts
20660
20660
  var DEFAULT_API_BASE_URL = "http://127.0.0.1:8787";
20661
- var STAGING_API_BASE_URL = "https://mrrlin-api-mrrlin3.vercel.app";
20661
+ var STAGING_API_BASE_URL = "https://api-staging.mrrlin.com";
20662
20662
  function normalizeUrl(value) {
20663
20663
  return value.replace(/\/$/, "");
20664
20664
  }
20665
20665
  var RETIRED_API_BASE_URLS = {
20666
20666
  // Retired 2026-07-28 when staging moved to the mrrlin2 Vercel team; serves a decommissioned
20667
20667
  // Supabase project and now answers `GET /health` with 402 DEPLOYMENT_DISABLED. Added in 0.3.50.
20668
- "mrrlin-api-fnnzzz-fnnzzzs-projects.vercel.app": STAGING_API_BASE_URL
20668
+ "mrrlin-api-fnnzzz-fnnzzzs-projects.vercel.app": STAGING_API_BASE_URL,
20669
+ // Retired 2026-08-14 when staging decoupled onto the stable custom domain api-staging.mrrlin.com.
20670
+ // A persisted operator configuration may still pin the mrrlin3 Vercel alias; retarget it in one hop.
20671
+ "mrrlin-api-mrrlin3.vercel.app": STAGING_API_BASE_URL
20669
20672
  };
20670
20673
  function isSameApiHost(a, b) {
20671
20674
  const origin = (value) => {
@@ -36387,6 +36390,9 @@ var openApiOperationIds = {
36387
36390
  getExecutionRun: "getExecutionRun",
36388
36391
  getProjectSchedulerPolicy: "getProjectSchedulerPolicy",
36389
36392
  getProjectDeployWorkflow: "getProjectDeployWorkflow",
36393
+ getDeployTopology: "getDeployTopology",
36394
+ writeDeployTopology: "writeDeployTopology",
36395
+ getTaskDeployTarget: "getTaskDeployTarget",
36390
36396
  getSchedulerPendingTasks: "getSchedulerPendingTasks",
36391
36397
  wakeDirector: "wakeDirector",
36392
36398
  getGithubInstallationRepositories: "getGithubInstallationRepositories",
@@ -37133,6 +37139,47 @@ function refineTaskCreateDomain(data, ctx) {
37133
37139
  }
37134
37140
  var mrrlinTaskCreateSchema = mrrlinTaskCreateShapeSchema.superRefine(refineTaskCreateDomain);
37135
37141
  var mrrlinTaskCreateWithoutIdSchema = mrrlinTaskCreateShapeSchema.omit({ id: true }).superRefine(refineTaskCreateDomain);
37142
+ var mrrlinDeployWorkflowEnvSchema = external_exports.enum(["dev", "prod"]);
37143
+ var mrrlinTaskDeployTargetInputSchema = external_exports.object({
37144
+ env: mrrlinDeployWorkflowEnvSchema,
37145
+ targetLabel: external_exports.string().min(1),
37146
+ workflowId: external_exports.number().int().positive(),
37147
+ workflowPath: external_exports.string().min(1),
37148
+ workflowSha: external_exports.string().min(1),
37149
+ topologyFingerprint: external_exports.string().min(1)
37150
+ });
37151
+ var deployTopologyResponseSchema = external_exports.object({
37152
+ data: external_exports.object({
37153
+ fingerprint: external_exports.string().nullable(),
37154
+ snapshot: external_exports.array(external_exports.object({ id: external_exports.number(), path: external_exports.string(), sha: external_exports.string(), state: external_exports.string() })).nullable(),
37155
+ entries: external_exports.array(external_exports.object({
37156
+ label: external_exports.string(),
37157
+ env: mrrlinDeployWorkflowEnvSchema,
37158
+ workflowId: external_exports.number(),
37159
+ workflowPath: external_exports.string(),
37160
+ workflowSha: external_exports.string()
37161
+ }))
37162
+ })
37163
+ });
37164
+ var mrrlinDeployTopologyWriteSchema = external_exports.object({
37165
+ fingerprint: external_exports.string().min(1),
37166
+ entries: external_exports.array(external_exports.object({
37167
+ label: external_exports.string().min(1),
37168
+ env: mrrlinDeployWorkflowEnvSchema,
37169
+ workflowId: external_exports.number().int().positive(),
37170
+ workflowPath: external_exports.string().min(1),
37171
+ workflowSha: external_exports.string().min(1)
37172
+ }))
37173
+ });
37174
+ var taskDeployTargetResponseSchema = external_exports.object({
37175
+ data: external_exports.object({
37176
+ targetLabel: external_exports.string(),
37177
+ workflowId: external_exports.number(),
37178
+ workflowPath: external_exports.string(),
37179
+ workflowSha: external_exports.string(),
37180
+ topologyFingerprint: external_exports.string()
37181
+ })
37182
+ });
37136
37183
  var mrrlinTaskUpdateSchema = external_exports.object({
37137
37184
  actor: external_exports.string().min(1).optional(),
37138
37185
  assignee: mrrlinTaskAssigneeSchema.optional(),
@@ -37155,8 +37202,13 @@ var mrrlinTaskUpdateSchema = external_exports.object({
37155
37202
  title: external_exports.string().min(1).optional(),
37156
37203
  type: mrrlinTaskTypeSchema.optional(),
37157
37204
  followUpAt: external_exports.string().datetime().nullable().optional(),
37158
- verificationDueAt: external_exports.string().datetime().nullable().optional()
37159
- }).refine((value) => value.assignee !== void 0 || value.autoDeploy !== void 0 || value.domain !== void 0 || value.autonomyLevel !== void 0 || value.autonomyReason !== void 0 || value.category !== void 0 || value.directorThreadId !== void 0 || value.dueDate !== void 0 || value.followUpAt !== void 0 || value.impact !== void 0 || value.notes !== void 0 || value.planId !== void 0 || value.specApproved !== void 0 || value.specWikiPageId !== void 0 || value.status !== void 0 || value.subcategory !== void 0 || value.tags !== void 0 || value.title !== void 0 || value.type !== void 0 || value.verificationDueAt !== void 0, { message: "TaskUpdate requires at least one field" }).superRefine((value, context) => {
37205
+ verificationDueAt: external_exports.string().datetime().nullable().optional(),
37206
+ deployTargets: external_exports.array(mrrlinTaskDeployTargetInputSchema).optional(),
37207
+ // design.md D5 step 6 hint (task 3.16): the deploy gate's own bookkeeping write when the
37208
+ // fingerprint moved but the topology still maps the label to the same workflow. Never sent
37209
+ // by any other caller.
37210
+ reverifyRequested: external_exports.boolean().optional()
37211
+ }).refine((value) => value.assignee !== void 0 || value.autoDeploy !== void 0 || value.domain !== void 0 || value.autonomyLevel !== void 0 || value.autonomyReason !== void 0 || value.category !== void 0 || value.directorThreadId !== void 0 || value.dueDate !== void 0 || value.followUpAt !== void 0 || value.impact !== void 0 || value.notes !== void 0 || value.planId !== void 0 || value.specApproved !== void 0 || value.specWikiPageId !== void 0 || value.status !== void 0 || value.subcategory !== void 0 || value.tags !== void 0 || value.title !== void 0 || value.type !== void 0 || value.verificationDueAt !== void 0 || value.deployTargets !== void 0 || value.reverifyRequested !== void 0, { message: "TaskUpdate requires at least one field" }).superRefine((value, context) => {
37160
37212
  if (value.autonomyLevel !== void 0 && value.autonomyReason === void 0) {
37161
37213
  context.addIssue({
37162
37214
  code: external_exports.ZodIssueCode.custom,
@@ -37240,6 +37292,7 @@ var mrrlinWikiPageUpdateSchema = external_exports.object({
37240
37292
  }).refine((value) => Object.keys(value).length > 0, { message: "WikiPageUpdate requires at least one field" });
37241
37293
  var mrrlinWikiPageListFiltersSchema = external_exports.object({
37242
37294
  folder: external_exports.string().min(1).optional(),
37295
+ id: mrrlinWikiPageIdSchema.optional(),
37243
37296
  q: external_exports.string().min(1).optional(),
37244
37297
  taskId: mrrlinTaskIdSchema.optional()
37245
37298
  });
@@ -37816,7 +37869,6 @@ var mrrlinProjectSchedulerPolicyUpsertSchema = external_exports.object({
37816
37869
  var projectSchedulerPolicyResponseSchema = external_exports.object({
37817
37870
  data: mrrlinProjectSchedulerPolicySchema
37818
37871
  });
37819
- var mrrlinDeployWorkflowEnvSchema = external_exports.enum(["dev", "prod"]);
37820
37872
  var mrrlinDeployWorkflowSourceSchema = external_exports.enum(["agent", "operator"]);
37821
37873
  var mrrlinDeployWorkflowStateSchema = external_exports.enum(["resolved", "ambiguous"]);
37822
37874
  var mrrlinProjectDeployWorkflowSchema = external_exports.object({
@@ -39301,6 +39353,29 @@ function createMrrlinClient(config2) {
39301
39353
  throw error51;
39302
39354
  }
39303
39355
  },
39356
+ async getDeployTopology(projectSlug) {
39357
+ const body = await request(`${projectPath(projectSlug)}/deploy-topology`);
39358
+ return deployTopologyResponseSchema.parse(body).data;
39359
+ },
39360
+ async writeDeployTopology(projectSlug, input) {
39361
+ const body = await request(`${projectPath(projectSlug)}/deploy-topology`, {
39362
+ body: JSON.stringify(mrrlinDeployTopologyWriteSchema.parse(input)),
39363
+ headers: { "content-type": "application/json" },
39364
+ method: "PUT"
39365
+ });
39366
+ return deployTopologyResponseSchema.parse(body).data;
39367
+ },
39368
+ async getTaskDeployTarget(projectSlug, taskId, env) {
39369
+ try {
39370
+ const body = await request(`${projectPath(projectSlug)}/tasks/${taskId}/deploy-target/${env}`);
39371
+ return taskDeployTargetResponseSchema.parse(body).data;
39372
+ } catch (error51) {
39373
+ if (error51 instanceof MrrlinApiError && error51.status === 404 && error51.code === "DEPLOY_TARGET_UNSET") {
39374
+ return null;
39375
+ }
39376
+ throw error51;
39377
+ }
39378
+ },
39304
39379
  async upsertProjectDeployWorkflow(projectSlug, env, input) {
39305
39380
  const body = await request(`${projectPath(projectSlug)}/deploy-workflows/${env}`, {
39306
39381
  body: JSON.stringify(input),
@@ -39975,7 +40050,7 @@ function startCodexProbe(deps = {}) {
39975
40050
  }
39976
40051
 
39977
40052
  // src/_generated/version.ts
39978
- var PKG_VERSION = "0.3.67";
40053
+ var PKG_VERSION = "0.3.68";
39979
40054
 
39980
40055
  // src/api-base-url-migrate.ts
39981
40056
  var import_node_fs2 = __toESM(require("node:fs"), 1);
@@ -42355,6 +42430,111 @@ ${body}`;
42355
42430
  }
42356
42431
  return { markdown: body, tldrChars };
42357
42432
  }
42433
+ function extractDeployTarget(markdown) {
42434
+ const section = /(## Deploy Target\s*\n)([\s\S]*?)(?=\n## |$)/i.exec(markdown);
42435
+ if (!section || section.index === void 0) {
42436
+ return { markdown, deployTarget: null };
42437
+ }
42438
+ const remaining = markdown.slice(0, section.index) + markdown.slice(section.index + section[0].length);
42439
+ const fenced = /```(?:json)?\s*\n([\s\S]*?)```/m.exec(section[2] ?? "");
42440
+ let deployTarget = null;
42441
+ if (fenced) {
42442
+ try {
42443
+ const raw = JSON.parse(fenced[1] ?? "");
42444
+ if (raw !== null && typeof raw === "object" && typeof raw.label === "string") {
42445
+ const parsed = { label: raw.label };
42446
+ if (Array.isArray(raw.derive)) {
42447
+ const derive = raw.derive.filter(isDeployTopologyEntry);
42448
+ if (derive.length === raw.derive.length) parsed.derive = derive;
42449
+ }
42450
+ deployTarget = parsed;
42451
+ }
42452
+ } catch {
42453
+ deployTarget = null;
42454
+ }
42455
+ }
42456
+ return { markdown: remaining, deployTarget };
42457
+ }
42458
+ function isDeployTopologyEntry(value) {
42459
+ if (value === null || typeof value !== "object") return false;
42460
+ const e = value;
42461
+ return typeof e.label === "string" && e.label.length > 0 && (e.env === "dev" || e.env === "prod") && typeof e.workflowId === "number" && Number.isInteger(e.workflowId) && e.workflowId > 0 && typeof e.workflowPath === "string" && e.workflowPath.length > 0 && typeof e.workflowSha === "string" && e.workflowSha.length > 0;
42462
+ }
42463
+ function buildDeployTargetPromptSection(ctx) {
42464
+ if (ctx.kind === "closed_menu") {
42465
+ const labels = [...new Set(ctx.entries.map((entry) => entry.label))];
42466
+ return [
42467
+ ``,
42468
+ `--- DEPLOY TARGET (required final section) ---`,
42469
+ `This project's deploy topology is already resolved for its current workflow inventory. Pick the ONE label this task deploys under from this CLOSED menu \u2014 never invent a new label:`,
42470
+ ...labels.map((label) => `- ${label}`),
42471
+ ``,
42472
+ `Append a final section \`## Deploy Target\` whose ONLY content is a single fenced json block of exactly this shape:`,
42473
+ "```json",
42474
+ `{"label": "<one of the menu labels above>"}`,
42475
+ "```"
42476
+ ].join("\n");
42477
+ }
42478
+ return [
42479
+ ``,
42480
+ `--- DEPLOY TARGET (required final section) ---`,
42481
+ `This project's deploy topology is NOT yet resolved for its current workflow inventory. Below is the raw GitHub workflow list (workflowId / path / blob sha / state); every value used below must be copied VERBATIM from it \u2014 never invented:`,
42482
+ ``,
42483
+ JSON.stringify(ctx.snapshot, null, 2),
42484
+ ``,
42485
+ `First derive the FULL label map: identify every deploy target in the snapshot, give each a short kebab-case \`label\`, and classify each workflow into an \`env\` of exactly "dev" or "prod".`,
42486
+ `Heuristic for dev vs prod (there is no fixed naming rule): prefer an explicit \`dev-\`/\`prod-\` filename prefix, then a \`/dev/\` or \`/prod/\` directory segment; if neither applies, group the workflows serving the same target and classify from the pair's context.`,
42487
+ `Then pick the ONE \`label\` this task deploys under, from among the labels you just derived.`,
42488
+ ``,
42489
+ `Append a final section \`## Deploy Target\` whose ONLY content is a single fenced json block of exactly this shape:`,
42490
+ "```json",
42491
+ `{"label": "<label this task deploys under>", "derive": [{"label":"<target label>","env":"dev"|"prod","workflowId":N,"workflowPath":"<path>","workflowSha":"<blob sha>"}, ...]}`,
42492
+ "```",
42493
+ ``,
42494
+ `\`derive\` is the FULL map of every deploy target you can identify in the snapshot (not just this task's pick), so later tasks reuse this derivation instead of re-deriving. \`env\` must be exactly "dev" or "prod". \`workflowId\`/\`workflowPath\`/\`workflowSha\` must be copied verbatim from the snapshot above.`
42495
+ ].join("\n");
42496
+ }
42497
+ function resolveDeployTargetRows(canonical, deployTarget, autoDeploy, fingerprint) {
42498
+ if (autoDeploy === "off") return {};
42499
+ const filtered = canonical.filter((entry) => entry.label === deployTarget.label);
42500
+ const requiredEnvs = autoDeploy === "prod" ? ["dev", "prod"] : ["dev"];
42501
+ const missingEnvs = [];
42502
+ const deployTargets = [];
42503
+ for (const env of requiredEnvs) {
42504
+ const match = filtered.find((entry) => entry.env === env);
42505
+ if (!match) {
42506
+ missingEnvs.push(env);
42507
+ continue;
42508
+ }
42509
+ deployTargets.push({
42510
+ env,
42511
+ targetLabel: deployTarget.label,
42512
+ workflowId: match.workflowId,
42513
+ workflowPath: match.workflowPath,
42514
+ workflowSha: match.workflowSha,
42515
+ topologyFingerprint: fingerprint
42516
+ });
42517
+ }
42518
+ if (missingEnvs.length > 0) return { skippedReason: "incomplete_label", missingEnvs };
42519
+ return { deployTargets };
42520
+ }
42521
+ function findDuplicateLabelEnvPairs(entries) {
42522
+ const seen = /* @__PURE__ */ new Set();
42523
+ const duplicateKeys = /* @__PURE__ */ new Set();
42524
+ for (const entry of entries) {
42525
+ const key = `${entry.label}\0${entry.env}`;
42526
+ if (seen.has(key)) duplicateKeys.add(key);
42527
+ seen.add(key);
42528
+ }
42529
+ const pairs = [];
42530
+ for (const key of duplicateKeys) {
42531
+ const parts = key.split("\0");
42532
+ const label = parts[0] ?? "";
42533
+ const env = parts[1] === "prod" ? "prod" : "dev";
42534
+ pairs.push({ label, env });
42535
+ }
42536
+ return pairs;
42537
+ }
42358
42538
  function nextWikiPageId(pages) {
42359
42539
  let max = 0;
42360
42540
  for (const page of pages) {
@@ -42365,7 +42545,7 @@ function nextWikiPageId(pages) {
42365
42545
  }
42366
42546
  return `WK-${max + 1}`;
42367
42547
  }
42368
- function buildDraftSpecPrompt(task, constitution) {
42548
+ function buildDraftSpecPrompt(task, constitution, deployTarget = null) {
42369
42549
  const budget = resolveSpecBudget(task.impact);
42370
42550
  const lines2 = [
42371
42551
  `Write a complete implementation spec for the task below.`,
@@ -42402,6 +42582,7 @@ function buildDraftSpecPrompt(task, constitution) {
42402
42582
  `The \`## TL;DR\` section MUST be at most ${DRAFT_SPEC_TLDR_MAX_CHARS} characters.`,
42403
42583
  `The spec MUST contain at most ${budget.maxRequirements} acceptance-criteria items and no more than ${budget.maxChars} characters total.`
42404
42584
  );
42585
+ if (deployTarget) lines2.push(buildDeployTargetPromptSection(deployTarget));
42405
42586
  return lines2.join("\n");
42406
42587
  }
42407
42588
  function skipped(reason, extra = {}) {
@@ -42450,6 +42631,22 @@ async function runDraftSpec(ctx, input, deps) {
42450
42631
  }
42451
42632
  if (getActiveBlockedByIds(task.blockedBy, blockers).length > 0) return skipped("blocked_by_active");
42452
42633
  }
42634
+ const autoDeploy = task.autoDeploy ?? "off";
42635
+ let topology = null;
42636
+ let deploySkippedReason = null;
42637
+ let deploySkippedDetail = null;
42638
+ if (autoDeploy !== "off") {
42639
+ try {
42640
+ topology = await client.getDeployTopology(projectSlug);
42641
+ } catch {
42642
+ deploySkippedReason = "topology_unavailable";
42643
+ topology = null;
42644
+ }
42645
+ }
42646
+ if (topology && (topology.fingerprint === null || topology.entries.length === 0 && topology.snapshot === null)) {
42647
+ deploySkippedReason = topology.fingerprint === null ? "no_bound_deploy_target" : "topology_snapshot_unavailable";
42648
+ topology = null;
42649
+ }
42453
42650
  let persona;
42454
42651
  try {
42455
42652
  persona = loadPersonaFn(DRAFT_SPEC_PERSONA);
@@ -42478,7 +42675,8 @@ async function runDraftSpec(ctx, input, deps) {
42478
42675
  } catch {
42479
42676
  constitution = null;
42480
42677
  }
42481
- const prompt = buildDraftSpecPrompt(task, constitution);
42678
+ const deployPromptContext = autoDeploy !== "off" && topology ? topology.entries.length > 0 ? { kind: "closed_menu", entries: topology.entries } : { kind: "derive", snapshot: topology.snapshot ?? [] } : null;
42679
+ const prompt = buildDraftSpecPrompt(task, constitution, deployPromptContext);
42482
42680
  const exec = await runExec({
42483
42681
  prompt,
42484
42682
  developerInstructions: persona,
@@ -42493,7 +42691,8 @@ async function runDraftSpec(ctx, input, deps) {
42493
42691
  if (exec.text.trim().length === 0) {
42494
42692
  return { kind: "retry", failureReason: "zero_output", error: "spec authoring produced empty output" };
42495
42693
  }
42496
- const { markdown, tldrChars } = enforceSpecOutputContract(exec.text, taskId, task.title);
42694
+ const { markdown: specMarkdown, deployTarget: parsedDeployTarget } = extractDeployTarget(exec.text);
42695
+ const { markdown, tldrChars } = enforceSpecOutputContract(specMarkdown, taskId, task.title);
42497
42696
  let pageId;
42498
42697
  if (adoptedId) {
42499
42698
  try {
@@ -42553,6 +42752,45 @@ async function runDraftSpec(ctx, input, deps) {
42553
42752
  }
42554
42753
  pageId = created;
42555
42754
  }
42755
+ let deployTargets;
42756
+ if (topology && parsedDeployTarget) {
42757
+ const fingerprint = topology.fingerprint;
42758
+ if (fingerprint === null) {
42759
+ deploySkippedReason = deploySkippedReason ?? "no_bound_deploy_target";
42760
+ } else if (topology.entries.length > 0) {
42761
+ const resolved = resolveDeployTargetRows(topology.entries, parsedDeployTarget, autoDeploy, fingerprint);
42762
+ deployTargets = resolved.deployTargets;
42763
+ if (resolved.skippedReason) {
42764
+ deploySkippedReason = resolved.skippedReason;
42765
+ if (resolved.skippedReason === "incomplete_label") {
42766
+ deploySkippedDetail = `label "${parsedDeployTarget.label}" has no entry for: ${(resolved.missingEnvs ?? []).join(", ")}`;
42767
+ }
42768
+ }
42769
+ } else {
42770
+ const derive = parsedDeployTarget.derive ?? [];
42771
+ const duplicateLabelEnvPairs = findDuplicateLabelEnvPairs(derive);
42772
+ if (derive.length === 0) {
42773
+ deploySkippedReason = "no_derivation";
42774
+ } else if (duplicateLabelEnvPairs.length > 0) {
42775
+ deploySkippedReason = "ambiguous_derivation";
42776
+ deploySkippedDetail = `duplicate label/env pair(s): ${duplicateLabelEnvPairs.map((pair) => `${pair.label}/${pair.env}`).join(", ")}`;
42777
+ } else {
42778
+ try {
42779
+ const written = await client.writeDeployTopology(projectSlug, { fingerprint, entries: derive });
42780
+ const resolved = resolveDeployTargetRows(written.entries, parsedDeployTarget, autoDeploy, fingerprint);
42781
+ deployTargets = resolved.deployTargets;
42782
+ if (resolved.skippedReason) {
42783
+ deploySkippedReason = resolved.skippedReason;
42784
+ if (resolved.skippedReason === "incomplete_label") {
42785
+ deploySkippedDetail = `label "${parsedDeployTarget.label}" has no entry for: ${(resolved.missingEnvs ?? []).join(", ")}`;
42786
+ }
42787
+ }
42788
+ } catch {
42789
+ deploySkippedReason = "topology_write_failed";
42790
+ }
42791
+ }
42792
+ }
42793
+ }
42556
42794
  let fresh;
42557
42795
  try {
42558
42796
  fresh = await client.getTask(projectSlug, taskId);
@@ -42569,8 +42807,12 @@ async function runDraftSpec(ctx, input, deps) {
42569
42807
  if (fresh.specWikiPageId) {
42570
42808
  return skipped("lost_link_race", { pageId, linkedPageId: fresh.specWikiPageId });
42571
42809
  }
42810
+ const linkPatch = { specWikiPageId: pageId };
42811
+ if (deployTargets !== void 0 && deployTargets.length > 0) {
42812
+ linkPatch.deployTargets = deployTargets;
42813
+ }
42572
42814
  try {
42573
- await client.updateTask(projectSlug, taskId, { specWikiPageId: pageId });
42815
+ await client.updateTask(projectSlug, taskId, linkPatch);
42574
42816
  } catch (error51) {
42575
42817
  return {
42576
42818
  kind: "retry",
@@ -42578,18 +42820,20 @@ async function runDraftSpec(ctx, input, deps) {
42578
42820
  error: `failed to link spec page ${pageId} to task ${taskId}: ${errMessage2(error51)}`
42579
42821
  };
42580
42822
  }
42823
+ const drafted = {
42824
+ outcome: "DRAFTED",
42825
+ pageId,
42826
+ adopted: adoptedId !== null,
42827
+ constitution: constitution !== null ? "included" : "missing",
42828
+ tldrChars,
42829
+ relatedContext: "skipped_no_client_method"
42830
+ };
42831
+ if (deploySkippedReason !== null) drafted.deployTargetSkippedReason = deploySkippedReason;
42832
+ if (deploySkippedDetail !== null) drafted.deployTargetSkippedDetail = deploySkippedDetail;
42833
+ drafted.taskTitle = task.title;
42581
42834
  return {
42582
42835
  kind: "done",
42583
- resultJson: JSON.stringify({
42584
- outcome: "DRAFTED",
42585
- pageId,
42586
- adopted: adoptedId !== null,
42587
- constitution: constitution !== null ? "included" : "missing",
42588
- tldrChars,
42589
- relatedContext: "skipped_no_client_method",
42590
- // Carried so onComplete can name the task in the operator notification without a re-read.
42591
- taskTitle: task.title
42592
- }),
42836
+ resultJson: JSON.stringify(drafted),
42593
42837
  finalReviewedHash: null
42594
42838
  };
42595
42839
  }
@@ -42607,6 +42851,14 @@ function onComplete(outcome, input) {
42607
42851
  body: `Spec draft for ${input.taskId} skipped (${parsed.reason ?? "unknown"}); nothing changed.`
42608
42852
  };
42609
42853
  }
42854
+ if (parsed.deployTargetSkippedReason === "ambiguous_derivation" || parsed.deployTargetSkippedReason === "incomplete_label") {
42855
+ return {
42856
+ kind: "handoff",
42857
+ taskId: input.taskId,
42858
+ id: `II-deploy-target-${parsed.deployTargetSkippedReason}-${input.taskId}`,
42859
+ body: `Spec for ${input.taskId} was drafted (page ${parsed.pageId ?? "?"} in Specs/), but its deploy target needs a human: ${parsed.deployTargetSkippedDetail ?? parsed.deployTargetSkippedReason}. The spec itself proceeds to the normal consensus review; the deploy target stays unresolved until this is fixed.`
42860
+ };
42861
+ }
42610
42862
  const taskRef = parsed.taskTitle ? `${input.taskId} \u2014 ${parsed.taskTitle}` : input.taskId;
42611
42863
  return {
42612
42864
  kind: "info",
@@ -42980,34 +43232,42 @@ async function tryProdAutoRevert(deps, projectSlug, run3, task) {
42980
43232
  return null;
42981
43233
  }
42982
43234
  }
42983
- function computeActiveSetFingerprint(workflows) {
42984
- return workflows.map((workflow) => `${workflow.id}:${workflow.path}`).sort().join(",");
42985
- }
42986
- async function resolveWorkflowId(deps, projectSlug, run3, env) {
43235
+ async function resolveWorkflowId(deps, projectSlug, run3, task, env) {
42987
43236
  const pinned = env === "dev" ? run3.devWorkflowId : run3.prodWorkflowId;
42988
- if (pinned) return { workflowId: pinned, source: null, resolvedFromStore: false };
42989
- const row = await deps.client.getProjectDeployWorkflow(projectSlug, env);
42990
- if (!row || row.state !== "resolved" || row.workflowId === null) {
42991
- return { workflowId: null, source: null, resolvedFromStore: false };
42992
- }
42993
- if (row.source !== "operator") {
42994
- const liveWorkflows = await deps.client.listGithubWorkflows(projectSlug);
42995
- const activeWorkflows = liveWorkflows.filter((workflow) => workflow.state === "active");
42996
- const isLive = activeWorkflows.some((workflow) => String(workflow.id) === String(row.workflowId));
42997
- if (!isLive) return { workflowId: null, source: null, resolvedFromStore: false };
42998
- const liveFingerprint = computeActiveSetFingerprint(activeWorkflows);
42999
- if (!row.reverifyRequested && row.activeSetFingerprint !== null && liveFingerprint !== row.activeSetFingerprint) {
43000
- await deps.client.upsertProjectDeployWorkflow(projectSlug, env, {
43001
- workflowId: row.workflowId,
43002
- workflowPath: row.workflowPath,
43003
- reason: row.reason,
43004
- state: row.state,
43005
- activeSetFingerprint: row.activeSetFingerprint,
43006
- reverifyRequested: true
43007
- });
43008
- }
43237
+ if (pinned) {
43238
+ return { workflowId: pinned, workflowPath: null, blockReason: null, pinned: true, reverifyRequested: false };
43239
+ }
43240
+ const target = await deps.getTaskDeployTarget(projectSlug, task.id, env);
43241
+ if (!target) {
43242
+ return { workflowId: null, workflowPath: null, blockReason: "deploy_target_unset", pinned: false, reverifyRequested: false };
43243
+ }
43244
+ const snapshot = await deps.getRepoSnapshot(projectSlug);
43245
+ const activeWorkflows = (snapshot?.lastSnapshot ?? []).filter((w) => w.state === "active");
43246
+ const liveMatch = activeWorkflows.find((w) => w.id === target.workflowId);
43247
+ if (!liveMatch || liveMatch.path !== target.workflowPath) {
43248
+ return { workflowId: null, workflowPath: null, blockReason: "deploy_target_inactive", pinned: false, reverifyRequested: false };
43249
+ }
43250
+ if (liveMatch.sha !== target.workflowSha) {
43251
+ return { workflowId: null, workflowPath: null, blockReason: "deploy_target_modified", pinned: false, reverifyRequested: false };
43252
+ }
43253
+ const currentFingerprint = snapshot?.lastFingerprint ?? null;
43254
+ if (currentFingerprint !== null && currentFingerprint === target.topologyFingerprint) {
43255
+ return { workflowId: target.workflowId, workflowPath: target.workflowPath, blockReason: null, pinned: false, reverifyRequested: false };
43256
+ }
43257
+ if (currentFingerprint === null) {
43258
+ deps.recordDeployTargetTelemetry("deploy_target_cache_missing");
43259
+ return { workflowId: null, workflowPath: null, blockReason: "deploy_target_cache_missing", pinned: false, reverifyRequested: false };
43009
43260
  }
43010
- return { workflowId: row.workflowId, source: row.source, resolvedFromStore: true };
43261
+ const entry = await deps.lookupTopologyEntry(projectSlug, currentFingerprint, target.targetLabel, env);
43262
+ if (!entry) {
43263
+ deps.recordDeployTargetTelemetry("deploy_target_cache_missing");
43264
+ return { workflowId: null, workflowPath: null, blockReason: "deploy_target_cache_missing", pinned: false, reverifyRequested: false };
43265
+ }
43266
+ if (entry.workflowId !== target.workflowId) {
43267
+ deps.recordDeployTargetTelemetry("deploy_target_reassigned");
43268
+ return { workflowId: null, workflowPath: null, blockReason: "deploy_target_reassigned", pinned: false, reverifyRequested: false };
43269
+ }
43270
+ return { workflowId: target.workflowId, workflowPath: target.workflowPath, blockReason: null, pinned: false, reverifyRequested: true };
43011
43271
  }
43012
43272
  async function runDeployGateTick(deps, projectSlug, run3, task) {
43013
43273
  const artifacts = await deps.client.listExecutionArtifacts(projectSlug, run3.id);
@@ -43029,7 +43289,10 @@ async function runDeployGateTick(deps, projectSlug, run3, task) {
43029
43289
  return { kind: "done" };
43030
43290
  }
43031
43291
  const env = activeEnv;
43032
- const resolution = await resolveWorkflowId(deps, projectSlug, run3, env);
43292
+ const resolution = await resolveWorkflowId(deps, projectSlug, run3, task, env);
43293
+ if (resolution.reverifyRequested) {
43294
+ await deps.setTaskReverifyRequested(projectSlug, task.id);
43295
+ }
43033
43296
  const workflowId = resolution.workflowId;
43034
43297
  const attempt = latestByEnv(artifacts, "deploy_attempt", env, { realAttemptsOnly: true });
43035
43298
  if (!attempt) {
@@ -43056,9 +43319,13 @@ async function runDeployGateTick(deps, projectSlug, run3, task) {
43056
43319
  await deps.handoff(projectSlug, task, run3, "verification_steps_missing");
43057
43320
  return { kind: "handoff", reason: "verification_steps_missing" };
43058
43321
  }
43322
+ if (resolution.blockReason) {
43323
+ await deps.handoff(projectSlug, task, run3, `${resolution.blockReason}:${env}`);
43324
+ return { kind: "handoff", reason: `${resolution.blockReason}:${env}` };
43325
+ }
43059
43326
  if (!workflowId) {
43060
- await deps.handoff(projectSlug, task, run3, `workflow_id_missing:${env}`);
43061
- return { kind: "handoff", reason: `workflow_id_missing:${env}` };
43327
+ await deps.handoff(projectSlug, task, run3, `deploy_target_unset:${env}`);
43328
+ return { kind: "handoff", reason: `deploy_target_unset:${env}` };
43062
43329
  }
43063
43330
  if (!run3.branch) {
43064
43331
  await deps.handoff(projectSlug, task, run3, "run_branch_missing");
@@ -43076,19 +43343,13 @@ async function runDeployGateTick(deps, projectSlug, run3, task) {
43076
43343
  try {
43077
43344
  await deps.client.dispatchTaskDeploy(projectSlug, task.id, { env, ref: run3.branch, workflowId: String(workflowId) });
43078
43345
  } catch (error51) {
43079
- if (resolution.source === "operator") {
43080
- await deps.handoff(
43081
- projectSlug,
43082
- task,
43083
- run3,
43084
- `deploy_workflow_operator_choice_stale:${env}:${String(error51).slice(0, 120)}`
43085
- );
43086
- return { kind: "handoff", reason: `deploy_workflow_operator_choice_stale:${env}` };
43346
+ if (!resolution.pinned && !resolution.reverifyRequested) {
43347
+ deps.recordDeployTargetTelemetry("deploy_dispatch_failed_fingerprint_unchanged");
43087
43348
  }
43088
43349
  await deps.handoff(projectSlug, task, run3, `workflow_dispatch_failed:${env}:${String(error51).slice(0, 120)}`);
43089
43350
  return { kind: "handoff", reason: `workflow_dispatch_failed:${env}` };
43090
43351
  }
43091
- if (resolution.resolvedFromStore) {
43352
+ if (!resolution.pinned) {
43092
43353
  await deps.client.updateExecutionRun(projectSlug, run3.id, {
43093
43354
  [env === "dev" ? "devWorkflowId" : "prodWorkflowId"]: workflowId
43094
43355
  });
@@ -45180,7 +45441,35 @@ async function runBackstopSweep(deps, drive = (slug, id) => driveRun(deps, slug,
45180
45441
  await deployGateHandoff(deps, projectSlug, t, r.id, reason, r.leaseId);
45181
45442
  },
45182
45443
  budgetSeconds: deps.deployGate.budgetSeconds ?? deployPollBudgetSeconds(),
45183
- now: () => /* @__PURE__ */ new Date()
45444
+ now: () => /* @__PURE__ */ new Date(),
45445
+ getTaskDeployTarget: async (projectSlug, taskId, env) => deps.client.getTaskDeployTarget(projectSlug, taskId, env),
45446
+ getRepoSnapshot: async (projectSlug) => {
45447
+ try {
45448
+ const topology = await deps.client.getDeployTopology(projectSlug);
45449
+ return { lastFingerprint: topology.fingerprint, lastSnapshot: topology.snapshot };
45450
+ } catch {
45451
+ return null;
45452
+ }
45453
+ },
45454
+ lookupTopologyEntry: async (projectSlug, fingerprint, label, env) => {
45455
+ try {
45456
+ const topology = await deps.client.getDeployTopology(projectSlug);
45457
+ if (topology.fingerprint !== fingerprint) return null;
45458
+ const entry = topology.entries.find((e) => e.label === label && e.env === env);
45459
+ return entry ? { workflowId: entry.workflowId, workflowPath: entry.workflowPath, workflowSha: entry.workflowSha } : null;
45460
+ } catch {
45461
+ return null;
45462
+ }
45463
+ },
45464
+ setTaskReverifyRequested: async (projectSlug, taskId) => {
45465
+ await deps.client.updateTask(projectSlug, taskId, { reverifyRequested: true }).catch(() => {
45466
+ });
45467
+ },
45468
+ recordDeployTargetTelemetry: (counter) => {
45469
+ console.log(
45470
+ JSON.stringify({ event: "deploy_target_telemetry", counter, projectSlug: slug, taskId: task.id })
45471
+ );
45472
+ }
45184
45473
  };
45185
45474
  await runDeployGateTick(gateDeps, slug, run3, task);
45186
45475
  } catch {
@@ -50366,12 +50655,13 @@ var DEFAULT_DIRECTOR_BRIDGE_ALLOWED_ORIGINS = [
50366
50655
  "http://localhost:*",
50367
50656
  "http://127.0.0.1:*",
50368
50657
  "http://[::1]:*",
50369
- // Staging Vercel team migrated 2026-07-28 (see docs/reference/deployments.md). The prior
50370
- // fnnzzz-fnnzzzs-projects web alias was dropped once no operator was left on an old build.
50371
- // mrrlin2 hit usage limits and was replaced by mrrlin3 on 2026-08-11; the mrrlin2 web alias
50372
- // was dropped the same day since no operator was left on that build either.
50373
- "https://mrrlin-web-mrrlin3.vercel.app",
50374
- "https://app.mrrlin.dev",
50658
+ // Staging decoupled onto the stable custom domain staging.mrrlin.com on 2026-08-14 (Cloudflare DNS
50659
+ // in front of the mrrlin3 Vercel project). The Vercel-owned web alias (mrrlin-web-mrrlin3.vercel.app)
50660
+ // and the old app.mrrlin.dev origin are dropped from the defaults: the browser now reaches the
50661
+ // bridge from the owned domain, which is stable across future Vercel project/team moves.
50662
+ // See docs/reference/deployments.md.
50663
+ "https://staging.mrrlin.com",
50664
+ // Production web (unchanged).
50375
50665
  "https://mrrlin-web.vercel.app"
50376
50666
  ];
50377
50667
  function normalizeReasoningEffort(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.3.67",
3
+ "version": "0.3.68",
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/codex-client": "0.0.0",
25
- "@mrrlin/director-e2e": "0.0.0",
26
- "@mrrlin/client": "0.0.0",
27
- "@mrrlin/schemas": "0.0.0",
28
24
  "@mrrlin/wiki": "0.0.0",
29
- "@mrrlin/tsconfig": "0.0.0"
25
+ "@mrrlin/schemas": "0.0.0",
26
+ "@mrrlin/client": "0.0.0",
27
+ "@mrrlin/tsconfig": "0.0.0",
28
+ "@mrrlin/director-e2e": "0.0.0",
29
+ "@mrrlin/codex-client": "0.0.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "@iarna/toml": "^2.2.5",