@mrrlin-dev/mcp 0.3.67 → 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 +534 -93
  2. package/package.json +4 -4
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) => {
@@ -20697,6 +20700,18 @@ function retargetRetiredApiBaseUrl(input) {
20697
20700
  function retiredApiHostnames() {
20698
20701
  return Object.keys(RETIRED_API_BASE_URLS);
20699
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
+ }
20700
20715
  function isLoopbackUrl(input) {
20701
20716
  try {
20702
20717
  const url2 = new URL(input);
@@ -20843,11 +20858,12 @@ async function installBrowser(opts = {}) {
20843
20858
  function deriveServeEnvFromProcessEnv(env) {
20844
20859
  const out = {};
20845
20860
  const rawApiUrl = env.MRRLIN_API_BASE_URL?.trim();
20861
+ const bridgeWantsStaging = bridgeUrlImpliesStaging(env.MRRLIN_BRIDGE_URL);
20846
20862
  const apiUrl = rawApiUrl ? retargetRetiredApiBaseUrl(rawApiUrl).baseUrl : void 0;
20847
20863
  if (apiUrl && !isStagingApiBaseUrl(apiUrl)) {
20848
20864
  out.MRRLIN_API_BASE_URL = apiUrl;
20849
20865
  if (env.MRRLIN_API_ALLOW_REMOTE === "1") out.MRRLIN_API_ALLOW_REMOTE = "1";
20850
- } else if (apiUrl || env.MRRLIN_STAGING === "1") {
20866
+ } else if (apiUrl || env.MRRLIN_STAGING === "1" || bridgeWantsStaging) {
20851
20867
  out.MRRLIN_STAGING = "1";
20852
20868
  }
20853
20869
  return Object.keys(out).length > 0 ? out : void 0;
@@ -21006,7 +21022,7 @@ function blockEnvHasRetiredApiUrl(actual) {
21006
21022
  }
21007
21023
  function renderEnvPreservedNotice(blockId, envKeys) {
21008
21024
  const keys = [...envKeys].sort().join(", ");
21009
- 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.`;
21010
21026
  }
21011
21027
  async function pathExists(target) {
21012
21028
  try {
@@ -36387,6 +36403,9 @@ var openApiOperationIds = {
36387
36403
  getExecutionRun: "getExecutionRun",
36388
36404
  getProjectSchedulerPolicy: "getProjectSchedulerPolicy",
36389
36405
  getProjectDeployWorkflow: "getProjectDeployWorkflow",
36406
+ getDeployTopology: "getDeployTopology",
36407
+ writeDeployTopology: "writeDeployTopology",
36408
+ getTaskDeployTarget: "getTaskDeployTarget",
36390
36409
  getSchedulerPendingTasks: "getSchedulerPendingTasks",
36391
36410
  wakeDirector: "wakeDirector",
36392
36411
  getGithubInstallationRepositories: "getGithubInstallationRepositories",
@@ -36395,6 +36414,8 @@ var openApiOperationIds = {
36395
36414
  getTask: "getTask",
36396
36415
  archiveTask: "archiveTask",
36397
36416
  unarchiveTask: "unarchiveTask",
36417
+ takeOverTask: "takeOverTask",
36418
+ closeTaskAsWontDo: "closeTaskAsWontDo",
36398
36419
  getWikiPage: "getWikiPage",
36399
36420
  handleGithubInstallCallback: "handleGithubInstallCallback",
36400
36421
  listExecutionArtifacts: "listExecutionArtifacts",
@@ -36883,6 +36904,32 @@ var SUPPORTED_EXECUTION_DOMAINS = Object.keys(SUPPORTED_EXECUTION_POLICY_BY_DOMA
36883
36904
  var taxonomyCategorySchema = external_exports.string().min(1).regex(/^[a-z][a-z0-9-]*$/);
36884
36905
  var taxonomySubcategorySchema = external_exports.string().min(1).regex(/^[a-z][a-z0-9-]*$/);
36885
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
+ ]);
36886
36933
  var mrrlinTaskSchema = external_exports.object({
36887
36934
  // Output-optional (not defaulted-required) so existing task fixtures/mocks across the
36888
36935
  // codebase need no churn; DB rows always resolve it to `string | null` via taskRowToTask.
@@ -36897,6 +36944,9 @@ var mrrlinTaskSchema = external_exports.object({
36897
36944
  autonomyReason: external_exports.string().min(1).max(240),
36898
36945
  blockedBy: external_exports.array(mrrlinTaskIdSchema),
36899
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(),
36900
36950
  directorThreadId: external_exports.string().nullable().default(null),
36901
36951
  dueDate: external_exports.string().date().nullable(),
36902
36952
  followUpAt: external_exports.string().datetime().nullable(),
@@ -36905,6 +36955,9 @@ var mrrlinTaskSchema = external_exports.object({
36905
36955
  latestJudgement: external_exports.string().nullable().default(null),
36906
36956
  latestJudgementAt: external_exports.string().datetime().nullable().default(null),
36907
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(),
36908
36961
  notes: external_exports.string().nullable().default(null),
36909
36962
  // Raw operator request captured at task creation (operator-authored ONLY, never Director-generated).
36910
36963
  // The intent-aware spec gate judges the spec against this; null/absent ⇒ the gate fail-closes.
@@ -36914,6 +36967,14 @@ var mrrlinTaskSchema = external_exports.object({
36914
36967
  planId: mrrlinPlanIdSchema.nullable(),
36915
36968
  specApproved: mrrlinSpecApprovedSchema.optional(),
36916
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(),
36917
36978
  status: mrrlinTaskStatusSchema,
36918
36979
  subcategory: taxonomySubcategorySchema,
36919
36980
  tags: mrrlinTaskTagsSchema,
@@ -36976,6 +37037,12 @@ var mrrlinWikiPageSchema = external_exports.object({
36976
37037
  var mrrlinMarkSpecReadyRequestSchema = external_exports.object({
36977
37038
  ready: external_exports.boolean()
36978
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
+ });
36979
37046
  var mrrlinGithubBindingSchema = external_exports.object({
36980
37047
  appInstallationState: external_exports.enum(["installed", "missing"]),
36981
37048
  installationId: external_exports.number().int().positive().nullable().default(null),
@@ -37133,6 +37200,47 @@ function refineTaskCreateDomain(data, ctx) {
37133
37200
  }
37134
37201
  var mrrlinTaskCreateSchema = mrrlinTaskCreateShapeSchema.superRefine(refineTaskCreateDomain);
37135
37202
  var mrrlinTaskCreateWithoutIdSchema = mrrlinTaskCreateShapeSchema.omit({ id: true }).superRefine(refineTaskCreateDomain);
37203
+ var mrrlinDeployWorkflowEnvSchema = external_exports.enum(["dev", "prod"]);
37204
+ var mrrlinTaskDeployTargetInputSchema = external_exports.object({
37205
+ env: mrrlinDeployWorkflowEnvSchema,
37206
+ targetLabel: external_exports.string().min(1),
37207
+ workflowId: external_exports.number().int().positive(),
37208
+ workflowPath: external_exports.string().min(1),
37209
+ workflowSha: external_exports.string().min(1),
37210
+ topologyFingerprint: external_exports.string().min(1)
37211
+ });
37212
+ var deployTopologyResponseSchema = external_exports.object({
37213
+ data: external_exports.object({
37214
+ fingerprint: external_exports.string().nullable(),
37215
+ snapshot: external_exports.array(external_exports.object({ id: external_exports.number(), path: external_exports.string(), sha: external_exports.string(), state: external_exports.string() })).nullable(),
37216
+ entries: external_exports.array(external_exports.object({
37217
+ label: external_exports.string(),
37218
+ env: mrrlinDeployWorkflowEnvSchema,
37219
+ workflowId: external_exports.number(),
37220
+ workflowPath: external_exports.string(),
37221
+ workflowSha: external_exports.string()
37222
+ }))
37223
+ })
37224
+ });
37225
+ var mrrlinDeployTopologyWriteSchema = external_exports.object({
37226
+ fingerprint: external_exports.string().min(1),
37227
+ entries: external_exports.array(external_exports.object({
37228
+ label: external_exports.string().min(1),
37229
+ env: mrrlinDeployWorkflowEnvSchema,
37230
+ workflowId: external_exports.number().int().positive(),
37231
+ workflowPath: external_exports.string().min(1),
37232
+ workflowSha: external_exports.string().min(1)
37233
+ }))
37234
+ });
37235
+ var taskDeployTargetResponseSchema = external_exports.object({
37236
+ data: external_exports.object({
37237
+ targetLabel: external_exports.string(),
37238
+ workflowId: external_exports.number(),
37239
+ workflowPath: external_exports.string(),
37240
+ workflowSha: external_exports.string(),
37241
+ topologyFingerprint: external_exports.string()
37242
+ })
37243
+ });
37136
37244
  var mrrlinTaskUpdateSchema = external_exports.object({
37137
37245
  actor: external_exports.string().min(1).optional(),
37138
37246
  assignee: mrrlinTaskAssigneeSchema.optional(),
@@ -37155,8 +37263,13 @@ var mrrlinTaskUpdateSchema = external_exports.object({
37155
37263
  title: external_exports.string().min(1).optional(),
37156
37264
  type: mrrlinTaskTypeSchema.optional(),
37157
37265
  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) => {
37266
+ verificationDueAt: external_exports.string().datetime().nullable().optional(),
37267
+ deployTargets: external_exports.array(mrrlinTaskDeployTargetInputSchema).optional(),
37268
+ // design.md D5 step 6 hint (task 3.16): the deploy gate's own bookkeeping write when the
37269
+ // fingerprint moved but the topology still maps the label to the same workflow. Never sent
37270
+ // by any other caller.
37271
+ reverifyRequested: external_exports.boolean().optional()
37272
+ }).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
37273
  if (value.autonomyLevel !== void 0 && value.autonomyReason === void 0) {
37161
37274
  context.addIssue({
37162
37275
  code: external_exports.ZodIssueCode.custom,
@@ -37240,6 +37353,7 @@ var mrrlinWikiPageUpdateSchema = external_exports.object({
37240
37353
  }).refine((value) => Object.keys(value).length > 0, { message: "WikiPageUpdate requires at least one field" });
37241
37354
  var mrrlinWikiPageListFiltersSchema = external_exports.object({
37242
37355
  folder: external_exports.string().min(1).optional(),
37356
+ id: mrrlinWikiPageIdSchema.optional(),
37243
37357
  q: external_exports.string().min(1).optional(),
37244
37358
  taskId: mrrlinTaskIdSchema.optional()
37245
37359
  });
@@ -37816,7 +37930,6 @@ var mrrlinProjectSchedulerPolicyUpsertSchema = external_exports.object({
37816
37930
  var projectSchedulerPolicyResponseSchema = external_exports.object({
37817
37931
  data: mrrlinProjectSchedulerPolicySchema
37818
37932
  });
37819
- var mrrlinDeployWorkflowEnvSchema = external_exports.enum(["dev", "prod"]);
37820
37933
  var mrrlinDeployWorkflowSourceSchema = external_exports.enum(["agent", "operator"]);
37821
37934
  var mrrlinDeployWorkflowStateSchema = external_exports.enum(["resolved", "ambiguous"]);
37822
37935
  var mrrlinProjectDeployWorkflowSchema = external_exports.object({
@@ -37945,7 +38058,8 @@ var mrrlinInFlightSchedulerEntrySchema = external_exports.discriminatedUnion("ki
37945
38058
  ]);
37946
38059
  var mrrlinIneligibleTaskSchema = external_exports.object({
37947
38060
  task: mrrlinTaskSchema,
37948
- autonomyLevel: mrrlinAutonomyLevelSchema
38061
+ autonomyLevel: mrrlinAutonomyLevelSchema,
38062
+ reason: external_exports.string().nullable().optional()
37949
38063
  });
37950
38064
  var schedulerPendingTasksResponseSchema = external_exports.object({
37951
38065
  data: external_exports.object({
@@ -37969,6 +38083,12 @@ var schedulerPendingTasksResponseSchema = external_exports.object({
37969
38083
  // person actually running the project) is visible as a large number instead of the autopilot
37970
38084
  // silently doing nothing.
37971
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
+ })),
37972
38092
  // Additive: true when the operator-web-presence gate is the reason nothing new is starting
37973
38093
  // (autonomyPaused takes precedence when both apply). Computed by the same shared helper the
37974
38094
  // tick uses, so this page can never claim a task is starting when the tick would decline it.
@@ -38072,6 +38192,7 @@ var mrrlinInboxItemSchema = external_exports.object({
38072
38192
  projectSlug: mrrlinProjectSlugSchema,
38073
38193
  questionOptions: mrrlinInboxQuestionOptionsSchema.nullable().default(null),
38074
38194
  readAt: external_exports.string().datetime().nullable(),
38195
+ reason: external_exports.string().nullable().optional(),
38075
38196
  status: mrrlinInboxItemStatusSchema,
38076
38197
  taskId: mrrlinTaskIdSchema.nullable()
38077
38198
  }).superRefine((value, context) => {
@@ -38918,6 +39039,22 @@ function createMrrlinClient(config2) {
38918
39039
  });
38919
39040
  return taskResponseSchema.parse(body).data;
38920
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
+ },
38921
39058
  async updateWikiPage(projectSlug, pageId, patch) {
38922
39059
  const body = await request(`${projectPath(projectSlug)}/wiki/${encodeURIComponent(pageId)}`, {
38923
39060
  body: JSON.stringify(patch),
@@ -39301,6 +39438,29 @@ function createMrrlinClient(config2) {
39301
39438
  throw error51;
39302
39439
  }
39303
39440
  },
39441
+ async getDeployTopology(projectSlug) {
39442
+ const body = await request(`${projectPath(projectSlug)}/deploy-topology`);
39443
+ return deployTopologyResponseSchema.parse(body).data;
39444
+ },
39445
+ async writeDeployTopology(projectSlug, input) {
39446
+ const body = await request(`${projectPath(projectSlug)}/deploy-topology`, {
39447
+ body: JSON.stringify(mrrlinDeployTopologyWriteSchema.parse(input)),
39448
+ headers: { "content-type": "application/json" },
39449
+ method: "PUT"
39450
+ });
39451
+ return deployTopologyResponseSchema.parse(body).data;
39452
+ },
39453
+ async getTaskDeployTarget(projectSlug, taskId, env) {
39454
+ try {
39455
+ const body = await request(`${projectPath(projectSlug)}/tasks/${taskId}/deploy-target/${env}`);
39456
+ return taskDeployTargetResponseSchema.parse(body).data;
39457
+ } catch (error51) {
39458
+ if (error51 instanceof MrrlinApiError && error51.status === 404 && error51.code === "DEPLOY_TARGET_UNSET") {
39459
+ return null;
39460
+ }
39461
+ throw error51;
39462
+ }
39463
+ },
39304
39464
  async upsertProjectDeployWorkflow(projectSlug, env, input) {
39305
39465
  const body = await request(`${projectPath(projectSlug)}/deploy-workflows/${env}`, {
39306
39466
  body: JSON.stringify(input),
@@ -39975,7 +40135,7 @@ function startCodexProbe(deps = {}) {
39975
40135
  }
39976
40136
 
39977
40137
  // src/_generated/version.ts
39978
- var PKG_VERSION = "0.3.67";
40138
+ var PKG_VERSION = "0.4.0";
39979
40139
 
39980
40140
  // src/api-base-url-migrate.ts
39981
40141
  var import_node_fs2 = __toESM(require("node:fs"), 1);
@@ -42355,6 +42515,111 @@ ${body}`;
42355
42515
  }
42356
42516
  return { markdown: body, tldrChars };
42357
42517
  }
42518
+ function extractDeployTarget(markdown) {
42519
+ const section = /(## Deploy Target\s*\n)([\s\S]*?)(?=\n## |$)/i.exec(markdown);
42520
+ if (!section || section.index === void 0) {
42521
+ return { markdown, deployTarget: null };
42522
+ }
42523
+ const remaining = markdown.slice(0, section.index) + markdown.slice(section.index + section[0].length);
42524
+ const fenced = /```(?:json)?\s*\n([\s\S]*?)```/m.exec(section[2] ?? "");
42525
+ let deployTarget = null;
42526
+ if (fenced) {
42527
+ try {
42528
+ const raw = JSON.parse(fenced[1] ?? "");
42529
+ if (raw !== null && typeof raw === "object" && typeof raw.label === "string") {
42530
+ const parsed = { label: raw.label };
42531
+ if (Array.isArray(raw.derive)) {
42532
+ const derive = raw.derive.filter(isDeployTopologyEntry);
42533
+ if (derive.length === raw.derive.length) parsed.derive = derive;
42534
+ }
42535
+ deployTarget = parsed;
42536
+ }
42537
+ } catch {
42538
+ deployTarget = null;
42539
+ }
42540
+ }
42541
+ return { markdown: remaining, deployTarget };
42542
+ }
42543
+ function isDeployTopologyEntry(value) {
42544
+ if (value === null || typeof value !== "object") return false;
42545
+ const e = value;
42546
+ 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;
42547
+ }
42548
+ function buildDeployTargetPromptSection(ctx) {
42549
+ if (ctx.kind === "closed_menu") {
42550
+ const labels = [...new Set(ctx.entries.map((entry) => entry.label))];
42551
+ return [
42552
+ ``,
42553
+ `--- DEPLOY TARGET (required final section) ---`,
42554
+ `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:`,
42555
+ ...labels.map((label) => `- ${label}`),
42556
+ ``,
42557
+ `Append a final section \`## Deploy Target\` whose ONLY content is a single fenced json block of exactly this shape:`,
42558
+ "```json",
42559
+ `{"label": "<one of the menu labels above>"}`,
42560
+ "```"
42561
+ ].join("\n");
42562
+ }
42563
+ return [
42564
+ ``,
42565
+ `--- DEPLOY TARGET (required final section) ---`,
42566
+ `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:`,
42567
+ ``,
42568
+ JSON.stringify(ctx.snapshot, null, 2),
42569
+ ``,
42570
+ `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".`,
42571
+ `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.`,
42572
+ `Then pick the ONE \`label\` this task deploys under, from among the labels you just derived.`,
42573
+ ``,
42574
+ `Append a final section \`## Deploy Target\` whose ONLY content is a single fenced json block of exactly this shape:`,
42575
+ "```json",
42576
+ `{"label": "<label this task deploys under>", "derive": [{"label":"<target label>","env":"dev"|"prod","workflowId":N,"workflowPath":"<path>","workflowSha":"<blob sha>"}, ...]}`,
42577
+ "```",
42578
+ ``,
42579
+ `\`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.`
42580
+ ].join("\n");
42581
+ }
42582
+ function resolveDeployTargetRows(canonical, deployTarget, autoDeploy, fingerprint) {
42583
+ if (autoDeploy === "off") return {};
42584
+ const filtered = canonical.filter((entry) => entry.label === deployTarget.label);
42585
+ const requiredEnvs = autoDeploy === "prod" ? ["dev", "prod"] : ["dev"];
42586
+ const missingEnvs = [];
42587
+ const deployTargets = [];
42588
+ for (const env of requiredEnvs) {
42589
+ const match = filtered.find((entry) => entry.env === env);
42590
+ if (!match) {
42591
+ missingEnvs.push(env);
42592
+ continue;
42593
+ }
42594
+ deployTargets.push({
42595
+ env,
42596
+ targetLabel: deployTarget.label,
42597
+ workflowId: match.workflowId,
42598
+ workflowPath: match.workflowPath,
42599
+ workflowSha: match.workflowSha,
42600
+ topologyFingerprint: fingerprint
42601
+ });
42602
+ }
42603
+ if (missingEnvs.length > 0) return { skippedReason: "incomplete_label", missingEnvs };
42604
+ return { deployTargets };
42605
+ }
42606
+ function findDuplicateLabelEnvPairs(entries) {
42607
+ const seen = /* @__PURE__ */ new Set();
42608
+ const duplicateKeys = /* @__PURE__ */ new Set();
42609
+ for (const entry of entries) {
42610
+ const key = `${entry.label}\0${entry.env}`;
42611
+ if (seen.has(key)) duplicateKeys.add(key);
42612
+ seen.add(key);
42613
+ }
42614
+ const pairs = [];
42615
+ for (const key of duplicateKeys) {
42616
+ const parts = key.split("\0");
42617
+ const label = parts[0] ?? "";
42618
+ const env = parts[1] === "prod" ? "prod" : "dev";
42619
+ pairs.push({ label, env });
42620
+ }
42621
+ return pairs;
42622
+ }
42358
42623
  function nextWikiPageId(pages) {
42359
42624
  let max = 0;
42360
42625
  for (const page of pages) {
@@ -42365,7 +42630,7 @@ function nextWikiPageId(pages) {
42365
42630
  }
42366
42631
  return `WK-${max + 1}`;
42367
42632
  }
42368
- function buildDraftSpecPrompt(task, constitution) {
42633
+ function buildDraftSpecPrompt(task, constitution, deployTarget = null) {
42369
42634
  const budget = resolveSpecBudget(task.impact);
42370
42635
  const lines2 = [
42371
42636
  `Write a complete implementation spec for the task below.`,
@@ -42402,6 +42667,7 @@ function buildDraftSpecPrompt(task, constitution) {
42402
42667
  `The \`## TL;DR\` section MUST be at most ${DRAFT_SPEC_TLDR_MAX_CHARS} characters.`,
42403
42668
  `The spec MUST contain at most ${budget.maxRequirements} acceptance-criteria items and no more than ${budget.maxChars} characters total.`
42404
42669
  );
42670
+ if (deployTarget) lines2.push(buildDeployTargetPromptSection(deployTarget));
42405
42671
  return lines2.join("\n");
42406
42672
  }
42407
42673
  function skipped(reason, extra = {}) {
@@ -42450,6 +42716,22 @@ async function runDraftSpec(ctx, input, deps) {
42450
42716
  }
42451
42717
  if (getActiveBlockedByIds(task.blockedBy, blockers).length > 0) return skipped("blocked_by_active");
42452
42718
  }
42719
+ const autoDeploy = task.autoDeploy ?? "off";
42720
+ let topology = null;
42721
+ let deploySkippedReason = null;
42722
+ let deploySkippedDetail = null;
42723
+ if (autoDeploy !== "off") {
42724
+ try {
42725
+ topology = await client.getDeployTopology(projectSlug);
42726
+ } catch {
42727
+ deploySkippedReason = "topology_unavailable";
42728
+ topology = null;
42729
+ }
42730
+ }
42731
+ if (topology && (topology.fingerprint === null || topology.entries.length === 0 && topology.snapshot === null)) {
42732
+ deploySkippedReason = topology.fingerprint === null ? "no_bound_deploy_target" : "topology_snapshot_unavailable";
42733
+ topology = null;
42734
+ }
42453
42735
  let persona;
42454
42736
  try {
42455
42737
  persona = loadPersonaFn(DRAFT_SPEC_PERSONA);
@@ -42478,7 +42760,8 @@ async function runDraftSpec(ctx, input, deps) {
42478
42760
  } catch {
42479
42761
  constitution = null;
42480
42762
  }
42481
- const prompt = buildDraftSpecPrompt(task, constitution);
42763
+ const deployPromptContext = autoDeploy !== "off" && topology ? topology.entries.length > 0 ? { kind: "closed_menu", entries: topology.entries } : { kind: "derive", snapshot: topology.snapshot ?? [] } : null;
42764
+ const prompt = buildDraftSpecPrompt(task, constitution, deployPromptContext);
42482
42765
  const exec = await runExec({
42483
42766
  prompt,
42484
42767
  developerInstructions: persona,
@@ -42493,7 +42776,8 @@ async function runDraftSpec(ctx, input, deps) {
42493
42776
  if (exec.text.trim().length === 0) {
42494
42777
  return { kind: "retry", failureReason: "zero_output", error: "spec authoring produced empty output" };
42495
42778
  }
42496
- const { markdown, tldrChars } = enforceSpecOutputContract(exec.text, taskId, task.title);
42779
+ const { markdown: specMarkdown, deployTarget: parsedDeployTarget } = extractDeployTarget(exec.text);
42780
+ const { markdown, tldrChars } = enforceSpecOutputContract(specMarkdown, taskId, task.title);
42497
42781
  let pageId;
42498
42782
  if (adoptedId) {
42499
42783
  try {
@@ -42553,6 +42837,45 @@ async function runDraftSpec(ctx, input, deps) {
42553
42837
  }
42554
42838
  pageId = created;
42555
42839
  }
42840
+ let deployTargets;
42841
+ if (topology && parsedDeployTarget) {
42842
+ const fingerprint = topology.fingerprint;
42843
+ if (fingerprint === null) {
42844
+ deploySkippedReason = deploySkippedReason ?? "no_bound_deploy_target";
42845
+ } else if (topology.entries.length > 0) {
42846
+ const resolved = resolveDeployTargetRows(topology.entries, parsedDeployTarget, autoDeploy, fingerprint);
42847
+ deployTargets = resolved.deployTargets;
42848
+ if (resolved.skippedReason) {
42849
+ deploySkippedReason = resolved.skippedReason;
42850
+ if (resolved.skippedReason === "incomplete_label") {
42851
+ deploySkippedDetail = `label "${parsedDeployTarget.label}" has no entry for: ${(resolved.missingEnvs ?? []).join(", ")}`;
42852
+ }
42853
+ }
42854
+ } else {
42855
+ const derive = parsedDeployTarget.derive ?? [];
42856
+ const duplicateLabelEnvPairs = findDuplicateLabelEnvPairs(derive);
42857
+ if (derive.length === 0) {
42858
+ deploySkippedReason = "no_derivation";
42859
+ } else if (duplicateLabelEnvPairs.length > 0) {
42860
+ deploySkippedReason = "ambiguous_derivation";
42861
+ deploySkippedDetail = `duplicate label/env pair(s): ${duplicateLabelEnvPairs.map((pair) => `${pair.label}/${pair.env}`).join(", ")}`;
42862
+ } else {
42863
+ try {
42864
+ const written = await client.writeDeployTopology(projectSlug, { fingerprint, entries: derive });
42865
+ const resolved = resolveDeployTargetRows(written.entries, parsedDeployTarget, autoDeploy, fingerprint);
42866
+ deployTargets = resolved.deployTargets;
42867
+ if (resolved.skippedReason) {
42868
+ deploySkippedReason = resolved.skippedReason;
42869
+ if (resolved.skippedReason === "incomplete_label") {
42870
+ deploySkippedDetail = `label "${parsedDeployTarget.label}" has no entry for: ${(resolved.missingEnvs ?? []).join(", ")}`;
42871
+ }
42872
+ }
42873
+ } catch {
42874
+ deploySkippedReason = "topology_write_failed";
42875
+ }
42876
+ }
42877
+ }
42878
+ }
42556
42879
  let fresh;
42557
42880
  try {
42558
42881
  fresh = await client.getTask(projectSlug, taskId);
@@ -42569,8 +42892,12 @@ async function runDraftSpec(ctx, input, deps) {
42569
42892
  if (fresh.specWikiPageId) {
42570
42893
  return skipped("lost_link_race", { pageId, linkedPageId: fresh.specWikiPageId });
42571
42894
  }
42895
+ const linkPatch = { specWikiPageId: pageId };
42896
+ if (deployTargets !== void 0 && deployTargets.length > 0) {
42897
+ linkPatch.deployTargets = deployTargets;
42898
+ }
42572
42899
  try {
42573
- await client.updateTask(projectSlug, taskId, { specWikiPageId: pageId });
42900
+ await client.updateTask(projectSlug, taskId, linkPatch);
42574
42901
  } catch (error51) {
42575
42902
  return {
42576
42903
  kind: "retry",
@@ -42578,18 +42905,20 @@ async function runDraftSpec(ctx, input, deps) {
42578
42905
  error: `failed to link spec page ${pageId} to task ${taskId}: ${errMessage2(error51)}`
42579
42906
  };
42580
42907
  }
42908
+ const drafted = {
42909
+ outcome: "DRAFTED",
42910
+ pageId,
42911
+ adopted: adoptedId !== null,
42912
+ constitution: constitution !== null ? "included" : "missing",
42913
+ tldrChars,
42914
+ relatedContext: "skipped_no_client_method"
42915
+ };
42916
+ if (deploySkippedReason !== null) drafted.deployTargetSkippedReason = deploySkippedReason;
42917
+ if (deploySkippedDetail !== null) drafted.deployTargetSkippedDetail = deploySkippedDetail;
42918
+ drafted.taskTitle = task.title;
42581
42919
  return {
42582
42920
  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
- }),
42921
+ resultJson: JSON.stringify(drafted),
42593
42922
  finalReviewedHash: null
42594
42923
  };
42595
42924
  }
@@ -42607,6 +42936,14 @@ function onComplete(outcome, input) {
42607
42936
  body: `Spec draft for ${input.taskId} skipped (${parsed.reason ?? "unknown"}); nothing changed.`
42608
42937
  };
42609
42938
  }
42939
+ if (parsed.deployTargetSkippedReason === "ambiguous_derivation" || parsed.deployTargetSkippedReason === "incomplete_label") {
42940
+ return {
42941
+ kind: "handoff",
42942
+ taskId: input.taskId,
42943
+ id: `II-deploy-target-${parsed.deployTargetSkippedReason}-${input.taskId}`,
42944
+ 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.`
42945
+ };
42946
+ }
42610
42947
  const taskRef = parsed.taskTitle ? `${input.taskId} \u2014 ${parsed.taskTitle}` : input.taskId;
42611
42948
  return {
42612
42949
  kind: "info",
@@ -42980,34 +43317,42 @@ async function tryProdAutoRevert(deps, projectSlug, run3, task) {
42980
43317
  return null;
42981
43318
  }
42982
43319
  }
42983
- function computeActiveSetFingerprint(workflows) {
42984
- return workflows.map((workflow) => `${workflow.id}:${workflow.path}`).sort().join(",");
42985
- }
42986
- async function resolveWorkflowId(deps, projectSlug, run3, env) {
43320
+ async function resolveWorkflowId(deps, projectSlug, run3, task, env) {
42987
43321
  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
- }
43322
+ if (pinned) {
43323
+ return { workflowId: pinned, workflowPath: null, blockReason: null, pinned: true, reverifyRequested: false };
43009
43324
  }
43010
- return { workflowId: row.workflowId, source: row.source, resolvedFromStore: true };
43325
+ const target = await deps.getTaskDeployTarget(projectSlug, task.id, env);
43326
+ if (!target) {
43327
+ return { workflowId: null, workflowPath: null, blockReason: "deploy_target_unset", pinned: false, reverifyRequested: false };
43328
+ }
43329
+ const snapshot = await deps.getRepoSnapshot(projectSlug);
43330
+ const activeWorkflows = (snapshot?.lastSnapshot ?? []).filter((w) => w.state === "active");
43331
+ const liveMatch = activeWorkflows.find((w) => w.id === target.workflowId);
43332
+ if (!liveMatch || liveMatch.path !== target.workflowPath) {
43333
+ return { workflowId: null, workflowPath: null, blockReason: "deploy_target_inactive", pinned: false, reverifyRequested: false };
43334
+ }
43335
+ if (liveMatch.sha !== target.workflowSha) {
43336
+ return { workflowId: null, workflowPath: null, blockReason: "deploy_target_modified", pinned: false, reverifyRequested: false };
43337
+ }
43338
+ const currentFingerprint = snapshot?.lastFingerprint ?? null;
43339
+ if (currentFingerprint !== null && currentFingerprint === target.topologyFingerprint) {
43340
+ return { workflowId: target.workflowId, workflowPath: target.workflowPath, blockReason: null, pinned: false, reverifyRequested: false };
43341
+ }
43342
+ if (currentFingerprint === null) {
43343
+ deps.recordDeployTargetTelemetry("deploy_target_cache_missing");
43344
+ return { workflowId: null, workflowPath: null, blockReason: "deploy_target_cache_missing", pinned: false, reverifyRequested: false };
43345
+ }
43346
+ const entry = await deps.lookupTopologyEntry(projectSlug, currentFingerprint, target.targetLabel, env);
43347
+ if (!entry) {
43348
+ deps.recordDeployTargetTelemetry("deploy_target_cache_missing");
43349
+ return { workflowId: null, workflowPath: null, blockReason: "deploy_target_cache_missing", pinned: false, reverifyRequested: false };
43350
+ }
43351
+ if (entry.workflowId !== target.workflowId) {
43352
+ deps.recordDeployTargetTelemetry("deploy_target_reassigned");
43353
+ return { workflowId: null, workflowPath: null, blockReason: "deploy_target_reassigned", pinned: false, reverifyRequested: false };
43354
+ }
43355
+ return { workflowId: target.workflowId, workflowPath: target.workflowPath, blockReason: null, pinned: false, reverifyRequested: true };
43011
43356
  }
43012
43357
  async function runDeployGateTick(deps, projectSlug, run3, task) {
43013
43358
  const artifacts = await deps.client.listExecutionArtifacts(projectSlug, run3.id);
@@ -43029,7 +43374,10 @@ async function runDeployGateTick(deps, projectSlug, run3, task) {
43029
43374
  return { kind: "done" };
43030
43375
  }
43031
43376
  const env = activeEnv;
43032
- const resolution = await resolveWorkflowId(deps, projectSlug, run3, env);
43377
+ const resolution = await resolveWorkflowId(deps, projectSlug, run3, task, env);
43378
+ if (resolution.reverifyRequested) {
43379
+ await deps.setTaskReverifyRequested(projectSlug, task.id);
43380
+ }
43033
43381
  const workflowId = resolution.workflowId;
43034
43382
  const attempt = latestByEnv(artifacts, "deploy_attempt", env, { realAttemptsOnly: true });
43035
43383
  if (!attempt) {
@@ -43056,9 +43404,13 @@ async function runDeployGateTick(deps, projectSlug, run3, task) {
43056
43404
  await deps.handoff(projectSlug, task, run3, "verification_steps_missing");
43057
43405
  return { kind: "handoff", reason: "verification_steps_missing" };
43058
43406
  }
43407
+ if (resolution.blockReason) {
43408
+ await deps.handoff(projectSlug, task, run3, `${resolution.blockReason}:${env}`);
43409
+ return { kind: "handoff", reason: `${resolution.blockReason}:${env}` };
43410
+ }
43059
43411
  if (!workflowId) {
43060
- await deps.handoff(projectSlug, task, run3, `workflow_id_missing:${env}`);
43061
- return { kind: "handoff", reason: `workflow_id_missing:${env}` };
43412
+ await deps.handoff(projectSlug, task, run3, `deploy_target_unset:${env}`);
43413
+ return { kind: "handoff", reason: `deploy_target_unset:${env}` };
43062
43414
  }
43063
43415
  if (!run3.branch) {
43064
43416
  await deps.handoff(projectSlug, task, run3, "run_branch_missing");
@@ -43076,19 +43428,13 @@ async function runDeployGateTick(deps, projectSlug, run3, task) {
43076
43428
  try {
43077
43429
  await deps.client.dispatchTaskDeploy(projectSlug, task.id, { env, ref: run3.branch, workflowId: String(workflowId) });
43078
43430
  } 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}` };
43431
+ if (!resolution.pinned && !resolution.reverifyRequested) {
43432
+ deps.recordDeployTargetTelemetry("deploy_dispatch_failed_fingerprint_unchanged");
43087
43433
  }
43088
43434
  await deps.handoff(projectSlug, task, run3, `workflow_dispatch_failed:${env}:${String(error51).slice(0, 120)}`);
43089
43435
  return { kind: "handoff", reason: `workflow_dispatch_failed:${env}` };
43090
43436
  }
43091
- if (resolution.resolvedFromStore) {
43437
+ if (!resolution.pinned) {
43092
43438
  await deps.client.updateExecutionRun(projectSlug, run3.id, {
43093
43439
  [env === "dev" ? "devWorkflowId" : "prodWorkflowId"]: workflowId
43094
43440
  });
@@ -43731,9 +44077,14 @@ var BASE_INSTRUCTIONS_PREFIX = [
43731
44077
  " if you are blocked and a human must decide before the work can continue, leave it at `handoff_needed`.",
43732
44078
  "",
43733
44079
  "File artifacts:",
43734
- "- Decide BEFORE writing your result whether the operator will scan it, compare across it,",
43735
- " re-read it later, keep it, or share it. If so it is a file, not inline text \u2014 judge by use,",
43736
- " 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.",
43737
44088
  "- When your output is a file (markdown report, HTML, screenshot), upload it with",
43738
44089
  " `upload_artifact` and embed the returned markdown link \u2014 never inline large blobs",
43739
44090
  " into chat, spec, or note text.",
@@ -45180,7 +45531,35 @@ async function runBackstopSweep(deps, drive = (slug, id) => driveRun(deps, slug,
45180
45531
  await deployGateHandoff(deps, projectSlug, t, r.id, reason, r.leaseId);
45181
45532
  },
45182
45533
  budgetSeconds: deps.deployGate.budgetSeconds ?? deployPollBudgetSeconds(),
45183
- now: () => /* @__PURE__ */ new Date()
45534
+ now: () => /* @__PURE__ */ new Date(),
45535
+ getTaskDeployTarget: async (projectSlug, taskId, env) => deps.client.getTaskDeployTarget(projectSlug, taskId, env),
45536
+ getRepoSnapshot: async (projectSlug) => {
45537
+ try {
45538
+ const topology = await deps.client.getDeployTopology(projectSlug);
45539
+ return { lastFingerprint: topology.fingerprint, lastSnapshot: topology.snapshot };
45540
+ } catch {
45541
+ return null;
45542
+ }
45543
+ },
45544
+ lookupTopologyEntry: async (projectSlug, fingerprint, label, env) => {
45545
+ try {
45546
+ const topology = await deps.client.getDeployTopology(projectSlug);
45547
+ if (topology.fingerprint !== fingerprint) return null;
45548
+ const entry = topology.entries.find((e) => e.label === label && e.env === env);
45549
+ return entry ? { workflowId: entry.workflowId, workflowPath: entry.workflowPath, workflowSha: entry.workflowSha } : null;
45550
+ } catch {
45551
+ return null;
45552
+ }
45553
+ },
45554
+ setTaskReverifyRequested: async (projectSlug, taskId) => {
45555
+ await deps.client.updateTask(projectSlug, taskId, { reverifyRequested: true }).catch(() => {
45556
+ });
45557
+ },
45558
+ recordDeployTargetTelemetry: (counter) => {
45559
+ console.log(
45560
+ JSON.stringify({ event: "deploy_target_telemetry", counter, projectSlug: slug, taskId: task.id })
45561
+ );
45562
+ }
45184
45563
  };
45185
45564
  await runDeployGateTick(gateDeps, slug, run3, task);
45186
45565
  } catch {
@@ -50366,12 +50745,13 @@ var DEFAULT_DIRECTOR_BRIDGE_ALLOWED_ORIGINS = [
50366
50745
  "http://localhost:*",
50367
50746
  "http://127.0.0.1:*",
50368
50747
  "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",
50748
+ // Staging decoupled onto the stable custom domain staging.mrrlin.com on 2026-08-14 (Cloudflare DNS
50749
+ // in front of the mrrlin3 Vercel project). The Vercel-owned web alias (mrrlin-web-mrrlin3.vercel.app)
50750
+ // and the old app.mrrlin.dev origin are dropped from the defaults: the browser now reaches the
50751
+ // bridge from the owned domain, which is stable across future Vercel project/team moves.
50752
+ // See docs/reference/deployments.md.
50753
+ "https://staging.mrrlin.com",
50754
+ // Production web (unchanged).
50375
50755
  "https://mrrlin-web.vercel.app"
50376
50756
  ];
50377
50757
  function normalizeReasoningEffort(value) {
@@ -50669,9 +51049,17 @@ function buildThreadStartParams(msg, config2, desiredModel) {
50669
51049
  " is not measurable, re-set `verificationDueAt` to a new date with a reason.",
50670
51050
  "",
50671
51051
  "File artifacts contract \u2014 decide the delivery medium BEFORE you compose the reply:",
50672
- "- First ask what the operator will DO with this output. If they will scan it, compare across",
50673
- " it, re-read it later, keep it, or share it, it belongs in a FILE. If they need to know one",
50674
- " 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.",
50675
51063
  "- When it is a file: write it (`.md` for linear prose, `.html` when structure or layout",
50676
51064
  " carries meaning), upload with `upload_artifact` (class=temp for one-off downloads,",
50677
51065
  " class=durable when a spec/evidence page references it; pass taskId when known), and paste",
@@ -51843,7 +52231,18 @@ function createBridgeMessageHandler(deps) {
51843
52231
  });
51844
52232
  const mapper = thread.mapper;
51845
52233
  mapper.reset();
51846
- 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 });
51847
52246
  const sendInTurn = (msg2) => {
51848
52247
  const buf = thread.replay;
51849
52248
  if (!buf) {
@@ -52529,7 +52928,7 @@ async function startDirectorBridge() {
52529
52928
  if (!body) return;
52530
52929
  const id = `II-MCPUPD-${(0, import_node_crypto8.randomUUID)()}`;
52531
52930
  try {
52532
- const row = await client.createInboxItem(projectSlug, { id, kind: "question", body });
52931
+ const row = await client.createInboxItem(projectSlug, { id, kind: "info", body });
52533
52932
  process.stderr.write(
52534
52933
  `[mrrlin-mcp director-bridge] info bridge.self_update.inbox_filed ${JSON.stringify({
52535
52934
  projectSlug,
@@ -52549,6 +52948,7 @@ async function startDirectorBridge() {
52549
52948
  );
52550
52949
  }
52551
52950
  },
52951
+ listAnsweredQuestions: (projectSlug, taskId) => client.listInboxItems(projectSlug, { status: "acknowledged", kind: "question", taskId }),
52552
52952
  // External-agents control plane: the real spawn runner, with the EXTERNAL_AGENTS_CLI operator
52553
52953
  // escape hatch bound here (the handlers stay spawn-free and this is the only production seam).
52554
52954
  runExternalAgentsCli: (args, opts) => runExternalAgentsCli(args, { ...opts, envCliPath: process.env["EXTERNAL_AGENTS_CLI"] })
@@ -52963,11 +53363,13 @@ function renderContextPrefix(context) {
52963
53363
  const urlPath = context?.urlPath?.trim() ? context.urlPath.trim() : void 0;
52964
53364
  const focusTaskId = context?.focusTaskId?.trim() ? context.focusTaskId.trim() : void 0;
52965
53365
  const directorSessionId = context?.directorSessionId?.trim() ? context.directorSessionId.trim() : void 0;
53366
+ const answeredQuestions = context?.answeredQuestions?.length ? context.answeredQuestions : void 0;
52966
53367
  const payload = {};
52967
53368
  if (projectSlug) payload.projectSlug = projectSlug;
52968
53369
  if (urlPath) payload.urlPath = urlPath;
52969
53370
  if (focusTaskId) payload.focusTaskId = focusTaskId;
52970
53371
  if (directorSessionId) payload.directorSessionId = directorSessionId;
53372
+ if (answeredQuestions) payload.answeredQuestions = answeredQuestions;
52971
53373
  const keys = Object.keys(payload);
52972
53374
  if (keys.length === 0) return "";
52973
53375
  return `Context (JSON): ${JSON.stringify(payload)}`;
@@ -57546,6 +57948,8 @@ var mcpToolNames = {
57546
57948
  resetDirectorThread: "reset_director_thread",
57547
57949
  archiveTask: "archive_task",
57548
57950
  unarchiveTask: "unarchive_task",
57951
+ takeOverTask: "take_over_task",
57952
+ closeTaskAsWontDo: "close_task_as_wont_do",
57549
57953
  updateWikiPage: "update_wiki_page",
57550
57954
  markSpecReady: "mark_spec_ready",
57551
57955
  runSpecConsensus: "run_spec_consensus",
@@ -58052,6 +58456,8 @@ var updateTaskInputSchema = taskScopedSchema.merge(mrrlinTaskUpdateSchema);
58052
58456
  var resetDirectorThreadInputSchema = taskScopedSchema;
58053
58457
  var archiveTaskInputSchema = taskScopedSchema;
58054
58458
  var unarchiveTaskInputSchema = taskScopedSchema;
58459
+ var takeOverTaskInputSchema = taskScopedSchema.merge(mrrlinTakeOverTaskRequestSchema);
58460
+ var closeTaskAsWontDoInputSchema = taskScopedSchema.merge(mrrlinCloseTaskAsWontDoRequestSchema);
58055
58461
  var listPlansInputSchema = projectScopedSchema;
58056
58462
  var createPlanInputSchema = projectScopedSchema.merge(mrrlinPlanCreateSchema);
58057
58463
  var updatePlanInputSchema = planScopedSchema.merge(mrrlinPlanUpdateSchema);
@@ -58209,6 +58615,8 @@ var mcpToolInputSchemas = {
58209
58615
  [mcpToolNames.resetDirectorThread]: resetDirectorThreadInputSchema,
58210
58616
  [mcpToolNames.archiveTask]: archiveTaskInputSchema,
58211
58617
  [mcpToolNames.unarchiveTask]: unarchiveTaskInputSchema,
58618
+ [mcpToolNames.takeOverTask]: takeOverTaskInputSchema,
58619
+ [mcpToolNames.closeTaskAsWontDo]: closeTaskAsWontDoInputSchema,
58212
58620
  [mcpToolNames.updateWikiPage]: updateWikiPageInputSchema,
58213
58621
  [mcpToolNames.markSpecReady]: markSpecReadyInputSchema,
58214
58622
  [mcpToolNames.runSpecConsensus]: runSpecConsensusInputSchema,
@@ -58278,6 +58686,8 @@ var mcpToolOperationIds = {
58278
58686
  [mcpToolNames.resetDirectorThread]: openApiOperationIds.resetDirectorThread,
58279
58687
  [mcpToolNames.archiveTask]: openApiOperationIds.archiveTask,
58280
58688
  [mcpToolNames.unarchiveTask]: openApiOperationIds.unarchiveTask,
58689
+ [mcpToolNames.takeOverTask]: openApiOperationIds.takeOverTask,
58690
+ [mcpToolNames.closeTaskAsWontDo]: openApiOperationIds.closeTaskAsWontDo,
58281
58691
  [mcpToolNames.updateWikiPage]: openApiOperationIds.updateWikiPage,
58282
58692
  [mcpToolNames.markSpecReady]: openApiOperationIds.markSpecReady,
58283
58693
  [mcpToolNames.listInboxItems]: openApiOperationIds.listInboxItems,
@@ -58329,6 +58739,8 @@ var mcpToolDescriptions = {
58329
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.",
58330
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.",
58331
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.",
58332
58744
  [mcpToolNames.updateWikiPage]: "Update a markdown Wiki page.",
58333
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.",
58334
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.',
@@ -58776,6 +59188,20 @@ function createMrrlinTools(options) {
58776
59188
  "Unable to unarchive task.",
58777
59189
  async (c, { projectSlug, taskId }) => await c.unarchiveTask(projectSlug, taskId)
58778
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
+ ),
58779
59205
  [mcpToolNames.updateWikiPage]: makeTool(
58780
59206
  client,
58781
59207
  mcpToolNames.updateWikiPage,
@@ -59398,13 +59824,15 @@ function installService(deps) {
59398
59824
  }
59399
59825
  const resolvedBins = bins.filter((b) => b !== null);
59400
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);
59401
59829
  const text = renderEcosystemConfig({
59402
59830
  cwd: resolved.cwd,
59403
59831
  home,
59404
59832
  codexHome: deps.env.CODEX_HOME?.trim() || void 0,
59405
59833
  pathEnv,
59406
- staging: deps.env.MRRLIN_STAGING === "1",
59407
- apiBaseUrl: deps.env.MRRLIN_API_BASE_URL?.trim() || void 0
59834
+ staging,
59835
+ apiBaseUrl
59408
59836
  });
59409
59837
  const ecoPath = import_node_path21.default.join(home, ".mrrlin", "ecosystem.config.cjs");
59410
59838
  deps.writeFile(ecoPath, text);
@@ -59527,22 +59955,22 @@ Usage:
59527
59955
  mrrlin-mcp install-browser Idempotently install the dedicated, pinned
59528
59956
  Chromium into ~/.mrrlin/browser/browsers (its own
59529
59957
  copy, isolated from the operator's Chrome and
59530
- codex-chrome). Run by install-codex; safe to
59958
+ codex-chrome). Run by configure bridge; safe to
59531
59959
  re-run. Exits non-zero on download failure.
59532
59960
 
59533
59961
  mrrlin-mcp browser-serve Launch the vendored @playwright/mcp against the
59534
59962
  dedicated persistent profile
59535
59963
  (~/.mrrlin/browser/profile, headed). This is the
59536
- command install-codex wires for the
59964
+ command configure bridge wires for the
59537
59965
  [mcp_servers.mrrlin-browser] block.
59538
59966
 
59539
59967
  mrrlin-mcp external-agents-serve
59540
59968
  Launch the bundled @mrrlin-dev/external-agents
59541
59969
  MCP server (multi-LLM dispatch pool). This is the
59542
- command install-codex wires for the
59970
+ command configure bridge wires for the
59543
59971
  [mcp_servers.external-agents] block.
59544
59972
 
59545
- mrrlin-mcp install-codex Idempotently register Mrrlin in
59973
+ mrrlin-mcp configure bridge Idempotently register Mrrlin in
59546
59974
  [--force] ~/.codex/config.toml (or CODEX_HOME). Adds an
59547
59975
  [mcp_servers.mrrlin] block. Development
59548
59976
  checkouts register the local dist/bin.cjs;
@@ -59551,12 +59979,12 @@ Usage:
59551
59979
  Bakes API-target env vars into the block as an
59552
59980
  inline env table so \`serve\` boots even when the
59553
59981
  local 127.0.0.1:8787 is not running. Setup guide:
59554
- MRRLIN_STAGING=1 mrrlin-mcp install-codex
59982
+ MRRLIN_STAGING=1 mrrlin-mcp configure bridge
59555
59983
  is the canonical path (staging alias + remote-
59556
59984
  allow gate in one flag). For a non-staging remote,
59557
59985
  pass BOTH the URL AND the allow gate:
59558
59986
  MRRLIN_API_BASE_URL=https://... \\
59559
- MRRLIN_API_ALLOW_REMOTE=1 mrrlin-mcp install-codex
59987
+ MRRLIN_API_ALLOW_REMOTE=1 mrrlin-mcp configure bridge
59560
59988
  Without an allow gate, \`serve\` still refuses any
59561
59989
  non-loopback URL (see \`mrrlin-mcp serve\` above).
59562
59990
  URL and MRRLIN_STAGING are mutually exclusive at
@@ -59669,8 +60097,8 @@ Usage:
59669
60097
  mrrlin-mcp --help Show this help.
59670
60098
 
59671
60099
  Operator notes:
59672
- - install-codex is idempotent: a second run with the same target is a no-op.
59673
- - 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
59674
60102
  using the local dist/bin.cjs; npm installs use
59675
60103
  \`mrrlin-mcp serve\`.
59676
60104
  `;
@@ -59705,16 +60133,29 @@ async function main() {
59705
60133
  process.exitCode = r.status ?? 1;
59706
60134
  return;
59707
60135
  }
59708
- case "install-codex": {
59709
- 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");
59710
60151
  const binPath = resolveSelfBinPath();
59711
60152
  try {
59712
60153
  const b = await installBrowser();
59713
- 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}
59714
60155
  `);
59715
60156
  } catch (error51) {
59716
60157
  process.stderr.write(
59717
- `[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)}
59718
60159
  `
59719
60160
  );
59720
60161
  process.exit(2);
@@ -59725,18 +60166,18 @@ async function main() {
59725
60166
  force,
59726
60167
  serveEnv
59727
60168
  });
59728
- process.stderr.write(`[mrrlin-mcp install-codex] ${result.action} ${result.configPath}
60169
+ process.stderr.write(`[mrrlin-mcp configure bridge] ${result.action} ${result.configPath}
59729
60170
  `);
59730
60171
  for (const preserved of result.preservedEnvBlocks) {
59731
60172
  process.stderr.write(`${renderEnvPreservedNotice(preserved.blockId, preserved.envKeys)}
59732
60173
  `);
59733
60174
  }
59734
60175
  for (const p of result.prompts) {
59735
- 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}
59736
60177
  `);
59737
60178
  if (p.action === "skipped-modified") {
59738
60179
  process.stderr.write(
59739
- `[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)
59740
60181
  `
59741
60182
  );
59742
60183
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.3.67",
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/client": "0.0.0",
24
25
  "@mrrlin/codex-client": "0.0.0",
25
26
  "@mrrlin/director-e2e": "0.0.0",
26
- "@mrrlin/client": "0.0.0",
27
27
  "@mrrlin/schemas": "0.0.0",
28
- "@mrrlin/wiki": "0.0.0",
29
- "@mrrlin/tsconfig": "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",