@mrrlin-dev/mcp 0.3.27 → 0.3.36

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 +928 -56
  2. package/package.json +3 -3
package/dist/bin.cjs CHANGED
@@ -20645,7 +20645,7 @@ var require_dist = __commonJS({
20645
20645
 
20646
20646
  // src/bin.ts
20647
20647
  var import_node_path19 = __toESM(require("node:path"), 1);
20648
- var import_node_child_process9 = require("node:child_process");
20648
+ var import_node_child_process12 = require("node:child_process");
20649
20649
  var import_node_fs13 = require("node:fs");
20650
20650
  var import_node_os11 = require("node:os");
20651
20651
  var import_node_path20 = __toESM(require("node:path"), 1);
@@ -20747,6 +20747,9 @@ function mrrlinTable(binPath, distribution, serveEnv) {
20747
20747
  }
20748
20748
  return { ...base, startup_timeout_sec: 30 };
20749
20749
  }
20750
+ function externalAgentsTable() {
20751
+ return { command: "external-agents-mcp", args: [], startup_timeout_sec: 30 };
20752
+ }
20750
20753
  function browserTable(binPath, distribution, homeDir) {
20751
20754
  const base = distribution === "npm" ? { command: "mrrlin-mcp", args: ["browser-serve"] } : { command: "node", args: [binPath, "browser-serve"] };
20752
20755
  return { ...base, startup_timeout_sec: 30, env: { PLAYWRIGHT_BROWSERS_PATH: browserPaths(homeDir).browsersPath } };
@@ -20767,6 +20770,11 @@ function managedBlocks(options, distribution) {
20767
20770
  id: "mrrlin-browser",
20768
20771
  expected: browserTable(options.binPath, distribution, homeDir),
20769
20772
  blockRe: blockRegex("mrrlin-browser")
20773
+ },
20774
+ {
20775
+ id: "external-agents",
20776
+ expected: externalAgentsTable(),
20777
+ blockRe: blockRegex("external-agents")
20770
20778
  }
20771
20779
  ];
20772
20780
  }
@@ -20802,10 +20810,11 @@ function renderBlock(block) {
20802
20810
  var KNOWN_MRRLIN_ENV_KEYS = /* @__PURE__ */ new Set(["MRRLIN_STAGING", "MRRLIN_API_BASE_URL", "MRRLIN_API_ALLOW_REMOTE"]);
20803
20811
  function commandArgsTimeoutEqual(candidate, expected) {
20804
20812
  if (candidate.command !== expected.command) return false;
20805
- if (!Array.isArray(candidate.args)) return false;
20806
- if (candidate.args.length !== expected.args.length) return false;
20813
+ const actualArgs = candidate.args === void 0 && expected.args.length === 0 ? [] : Array.isArray(candidate.args) ? candidate.args : null;
20814
+ if (actualArgs === null) return false;
20815
+ if (actualArgs.length !== expected.args.length) return false;
20807
20816
  for (let index = 0; index < expected.args.length; index += 1) {
20808
- if (candidate.args[index] !== expected.args[index]) return false;
20817
+ if (actualArgs[index] !== expected.args[index]) return false;
20809
20818
  }
20810
20819
  if (candidate.startup_timeout_sec !== void 0 && candidate.startup_timeout_sec !== expected.startup_timeout_sec) {
20811
20820
  return false;
@@ -20981,7 +20990,7 @@ var import_node_fs11 = __toESM(require("node:fs"), 1);
20981
20990
  var import_node_http = __toESM(require("node:http"), 1);
20982
20991
  var import_node_os7 = __toESM(require("node:os"), 1);
20983
20992
  var import_node_path13 = __toESM(require("node:path"), 1);
20984
- var import_node_child_process7 = require("node:child_process");
20993
+ var import_node_child_process9 = require("node:child_process");
20985
20994
  var import_node_crypto7 = __toESM(require("node:crypto"), 1);
20986
20995
 
20987
20996
  // src/turn-replay-buffer.ts
@@ -36377,6 +36386,8 @@ var openApiOperationIds = {
36377
36386
  listArtifactFiles: "listArtifactFiles",
36378
36387
  getOperatorSettings: "getOperatorSettings",
36379
36388
  updateOperatorSettings: "updateOperatorSettings",
36389
+ getAgentPool: "getAgentPool",
36390
+ putAgentPool: "putAgentPool",
36380
36391
  getConsensusConfig: "getConsensusConfig",
36381
36392
  updateWikiPageCas: "updateWikiPageCas",
36382
36393
  promoteSpecReadyCas: "promoteSpecReadyCas",
@@ -36432,6 +36443,27 @@ var mrrlinOperatorSettingsUpdateSchema = external_exports.object({
36432
36443
  var operatorSettingsResponseSchema = external_exports.object({ data: mrrlinOperatorSettingsSchema });
36433
36444
  var mrrlinConsensusConfigSchema = external_exports.object({ consensusEnabled: external_exports.boolean() });
36434
36445
  var consensusConfigResponseSchema = external_exports.object({ data: mrrlinConsensusConfigSchema });
36446
+ var agentPoolSnapshotSchema = external_exports.object({
36447
+ collected_at: external_exports.string(),
36448
+ status: external_exports.enum(["ok", "unavailable"]),
36449
+ agents: external_exports.array(external_exports.object({
36450
+ id: external_exports.string(),
36451
+ provider: external_exports.string(),
36452
+ tier: external_exports.string(),
36453
+ tags: external_exports.array(external_exports.string()),
36454
+ state: external_exports.string(),
36455
+ calls: external_exports.number(),
36456
+ tokens_in: external_exports.number(),
36457
+ tokens_out: external_exports.number(),
36458
+ success_rate: external_exports.number(),
36459
+ last_used_at: external_exports.string().nullable()
36460
+ })),
36461
+ totals: external_exports.object({ calls: external_exports.number(), tokens_in: external_exports.number(), tokens_out: external_exports.number() }),
36462
+ estimated_savings_usd: external_exports.number()
36463
+ });
36464
+ var agentPoolResponseSchema = external_exports.object({
36465
+ data: external_exports.object({ snapshot: agentPoolSnapshotSchema.nullable() })
36466
+ });
36435
36467
 
36436
36468
  // ../../packages/schemas/dist/specConsensusReview.js
36437
36469
  var mrrlinSpecConsensusReviewSchema = external_exports.object({
@@ -36854,6 +36886,7 @@ var mrrlinTaskCreateSchema = external_exports.object({
36854
36886
  impact: mrrlinTaskImpactSchema.default("medium"),
36855
36887
  notes: external_exports.string().nullable().optional(),
36856
36888
  operatorSeedRequest: external_exports.string().max(8e3).nullable().optional(),
36889
+ parentTaskId: external_exports.string().nullable().optional(),
36857
36890
  planId: mrrlinPlanIdSchema.nullable().optional(),
36858
36891
  specWikiPageId: mrrlinWikiPageIdSchema.nullable().optional(),
36859
36892
  tags: mrrlinTaskTagsSchema.optional(),
@@ -37615,7 +37648,8 @@ var mrrlinInboxItemDecideSchema = external_exports.object({
37615
37648
  var mrrlinInboxItemListFiltersSchema = external_exports.object({
37616
37649
  kind: mrrlinInboxItemKindSchema.optional(),
37617
37650
  readState: external_exports.enum(["read", "unread"]).optional(),
37618
- status: mrrlinInboxItemStatusSchema.optional()
37651
+ status: mrrlinInboxItemStatusSchema.optional(),
37652
+ taskId: mrrlinTaskIdSchema.optional()
37619
37653
  });
37620
37654
  var inboxItemReadResponseSchema = external_exports.object({
37621
37655
  data: external_exports.object({
@@ -38222,7 +38256,8 @@ function createMrrlinClient(config2) {
38222
38256
  const url2 = appendQuery(`${projectPath(projectSlug)}/inbox`, {
38223
38257
  kind: filters.kind,
38224
38258
  readState: filters.readState,
38225
- status: filters.status
38259
+ status: filters.status,
38260
+ taskId: filters.taskId
38226
38261
  });
38227
38262
  const body = await request(url2);
38228
38263
  return inboxItemListResponseSchema.parse(body).data;
@@ -38406,6 +38441,18 @@ function createMrrlinClient(config2) {
38406
38441
  });
38407
38442
  return operatorSettingsResponseSchema.parse(body).data;
38408
38443
  },
38444
+ async getOperatorAgentPool() {
38445
+ const body = await request(`/operator/agent-pool`);
38446
+ return agentPoolResponseSchema.parse(body).data;
38447
+ },
38448
+ async putOperatorAgentPool(snapshot) {
38449
+ const body = await request(`/operator/agent-pool`, {
38450
+ body: JSON.stringify(snapshot),
38451
+ headers: { "content-type": "application/json" },
38452
+ method: "POST"
38453
+ });
38454
+ return agentPoolResponseSchema.parse(body).data;
38455
+ },
38409
38456
  async getConsensusConfig(projectSlug) {
38410
38457
  const body = await request(`${projectPath(projectSlug)}/consensus-config`);
38411
38458
  return consensusConfigResponseSchema.parse(body).data;
@@ -38788,8 +38835,92 @@ Do NOT derail the current task - if the operator has an in-flight question, answ
38788
38835
  };
38789
38836
  }
38790
38837
 
38838
+ // src/codex-version.ts
38839
+ var import_node_child_process3 = require("node:child_process");
38840
+ var import_semver2 = __toESM(require_semver2(), 1);
38841
+ var SEMVER_RE = /\b\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?\b/;
38842
+ var CODEX_PACKAGE = process.env.MRRLIN_CODEX_PACKAGE?.trim() || "@openai/codex";
38843
+ var distTagsUrl = (pkg) => `https://registry.npmjs.org/-/package/${encodeURIComponent(pkg)}/dist-tags`;
38844
+ function parseCodexVersion(raw) {
38845
+ const m = raw.match(SEMVER_RE);
38846
+ return m ? m[0] : null;
38847
+ }
38848
+ var defaultRun = (args) => {
38849
+ const r = (0, import_node_child_process3.spawnSync)("codex", args, { encoding: "utf8", timeout: 8e3, shell: true });
38850
+ return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
38851
+ };
38852
+ function getInstalledCodexVersion(run3) {
38853
+ const r = run3(["--version"]);
38854
+ if (r.status !== 0) return null;
38855
+ return parseCodexVersion(r.stdout || r.stderr);
38856
+ }
38857
+ async function fetchLatestCodexVersion(fetchImpl = globalThis.fetch, pkg = CODEX_PACKAGE) {
38858
+ try {
38859
+ const controller = new AbortController();
38860
+ const t = setTimeout(() => controller.abort(), 5e3);
38861
+ let res;
38862
+ try {
38863
+ res = await fetchImpl(distTagsUrl(pkg), {
38864
+ headers: { Accept: "application/json" },
38865
+ signal: controller.signal
38866
+ });
38867
+ } finally {
38868
+ clearTimeout(t);
38869
+ }
38870
+ if (!res.ok) return null;
38871
+ const body = await res.json();
38872
+ const latest = body?.latest;
38873
+ return typeof latest === "string" && import_semver2.default.valid(latest) ? latest : null;
38874
+ } catch {
38875
+ return null;
38876
+ }
38877
+ }
38878
+ async function computeCodexUpdateState(run3, fetchImpl = globalThis.fetch, pkg = CODEX_PACKAGE) {
38879
+ const version2 = getInstalledCodexVersion(run3);
38880
+ if (!version2 || !import_semver2.default.valid(version2)) return { version: version2, latestVersion: null, updateAvailable: false };
38881
+ const latestVersion = await fetchLatestCodexVersion(fetchImpl, pkg);
38882
+ if (!latestVersion) return { version: version2, latestVersion: null, updateAvailable: false };
38883
+ return { version: version2, latestVersion, updateAvailable: import_semver2.default.gt(latestVersion, version2) };
38884
+ }
38885
+ function codexUpdateCommandFor(state) {
38886
+ return state.updateAvailable ? "codex update" : null;
38887
+ }
38888
+ var DEFAULT_PROBE_INTERVAL_MS = 6 * 60 * 60 * 1e3;
38889
+ var DEFAULT_STARTUP_DELAY_MS = 1e4;
38890
+ function startCodexProbe(deps = {}) {
38891
+ const run3 = deps.run ?? defaultRun;
38892
+ const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
38893
+ const pkg = deps.pkg ?? CODEX_PACKAGE;
38894
+ const intervalMs = deps.intervalMs ?? DEFAULT_PROBE_INTERVAL_MS;
38895
+ const startupDelayMs = deps.startupDelayMs ?? DEFAULT_STARTUP_DELAY_MS;
38896
+ const setIntervalFn = deps.setIntervalImpl ?? setInterval;
38897
+ const clearIntervalFn = deps.clearIntervalImpl ?? clearInterval;
38898
+ const setTimeoutFn = deps.setTimeoutImpl ?? setTimeout;
38899
+ const clearTimeoutFn = deps.clearTimeoutImpl ?? clearTimeout;
38900
+ let state = { version: null, latestVersion: null, updateAvailable: false };
38901
+ const refreshNow = async () => {
38902
+ try {
38903
+ state = await computeCodexUpdateState(run3, fetchImpl, pkg);
38904
+ } catch {
38905
+ }
38906
+ };
38907
+ const startupTimer = setTimeoutFn(() => void refreshNow(), startupDelayMs);
38908
+ startupTimer.unref?.();
38909
+ const timer = setIntervalFn(() => void refreshNow(), intervalMs);
38910
+ timer.unref?.();
38911
+ return {
38912
+ getState: () => state,
38913
+ getUpdateCommand: () => codexUpdateCommandFor(state),
38914
+ refreshNow,
38915
+ stop: () => {
38916
+ clearTimeoutFn(startupTimer);
38917
+ clearIntervalFn(timer);
38918
+ }
38919
+ };
38920
+ }
38921
+
38791
38922
  // src/_generated/version.ts
38792
- var PKG_VERSION = "0.3.27";
38923
+ var PKG_VERSION = "0.3.36";
38793
38924
 
38794
38925
  // src/api-base-url.ts
38795
38926
  var DEFAULT_API_BASE_URL = "http://127.0.0.1:8787";
@@ -38967,8 +39098,192 @@ function startSchedulerTickLoop(deps) {
38967
39098
  };
38968
39099
  }
38969
39100
 
39101
+ // src/agent-pool-loop.ts
39102
+ var import_node_child_process4 = require("node:child_process");
39103
+
39104
+ // src/agent-pool-snapshot.ts
39105
+ function buildAgentPoolSnapshot(statusRows, stats, opts) {
39106
+ if (!opts.available) {
39107
+ return {
39108
+ collected_at: opts.collectedAt,
39109
+ status: "unavailable",
39110
+ agents: [],
39111
+ totals: { calls: 0, tokens_in: 0, tokens_out: 0 },
39112
+ estimated_savings_usd: 0
39113
+ };
39114
+ }
39115
+ const costPer1M = opts.paidCostPer1MTokens ?? 3;
39116
+ const agents = statusRows.map((row) => {
39117
+ const agentStats = stats.by_agent[row.id];
39118
+ const count2 = agentStats?.count ?? 0;
39119
+ const tokensIn = agentStats?.tokens_in ?? 0;
39120
+ const tokensOut = agentStats?.tokens_out ?? 0;
39121
+ let successRate = 0;
39122
+ if (count2 > 0) {
39123
+ const successOutcomes = agentStats?.outcomes.success ?? 0;
39124
+ successRate = Number((successOutcomes / count2 * 100).toFixed(2)) / 100;
39125
+ }
39126
+ return {
39127
+ id: row.id,
39128
+ provider: row.provider,
39129
+ tier: row.tier,
39130
+ tags: row.tags,
39131
+ state: row.state ?? "unknown",
39132
+ calls: count2,
39133
+ tokens_in: tokensIn,
39134
+ tokens_out: tokensOut,
39135
+ success_rate: successRate,
39136
+ last_used_at: row.last_used_at ?? null
39137
+ };
39138
+ });
39139
+ let totalCalls = 0;
39140
+ let totalTokensIn = 0;
39141
+ let totalTokensOut = 0;
39142
+ for (const entry of Object.values(stats.by_agent)) {
39143
+ totalCalls += entry.count;
39144
+ totalTokensIn += entry.tokens_in;
39145
+ totalTokensOut += entry.tokens_out;
39146
+ }
39147
+ let savedTokens = 0;
39148
+ for (const row of statusRows) {
39149
+ if (row.tags.includes("free")) {
39150
+ const agentStats = stats.by_agent[row.id];
39151
+ if (agentStats) {
39152
+ savedTokens += agentStats.tokens_in + agentStats.tokens_out;
39153
+ }
39154
+ }
39155
+ }
39156
+ const estimatedSavings = Number((savedTokens / 1e6 * costPer1M).toFixed(2));
39157
+ return {
39158
+ collected_at: opts.collectedAt,
39159
+ status: "ok",
39160
+ agents,
39161
+ totals: {
39162
+ calls: totalCalls,
39163
+ tokens_in: totalTokensIn,
39164
+ tokens_out: totalTokensOut
39165
+ },
39166
+ estimated_savings_usd: estimatedSavings
39167
+ };
39168
+ }
39169
+
39170
+ // src/agent-pool-collector.ts
39171
+ async function collectAgentPoolSnapshot(deps) {
39172
+ let statusRows;
39173
+ let stats;
39174
+ try {
39175
+ const statusResult = await deps.runCli(["status", "--json"]);
39176
+ if (statusResult.code !== 0) throw new Error("non-zero exit on status");
39177
+ let statusParsed;
39178
+ try {
39179
+ statusParsed = JSON.parse(statusResult.stdout);
39180
+ } catch {
39181
+ throw new Error("invalid status json");
39182
+ }
39183
+ if (!Array.isArray(statusParsed)) throw new Error("status not array");
39184
+ statusRows = statusParsed;
39185
+ const statsResult = await deps.runCli(["stats", "--json"]);
39186
+ if (statsResult.code !== 0) throw new Error("non-zero exit on stats");
39187
+ try {
39188
+ stats = JSON.parse(statsResult.stdout);
39189
+ } catch {
39190
+ throw new Error("invalid stats json");
39191
+ }
39192
+ } catch {
39193
+ return buildAgentPoolSnapshot(
39194
+ [],
39195
+ {},
39196
+ { collectedAt: deps.now(), available: false }
39197
+ );
39198
+ }
39199
+ return buildAgentPoolSnapshot(
39200
+ statusRows,
39201
+ stats,
39202
+ { collectedAt: deps.now(), paidCostPer1MTokens: deps.paidCostPer1MTokens, available: true }
39203
+ );
39204
+ }
39205
+
39206
+ // src/agent-pool-loop.ts
39207
+ var DEFAULT_INTERVAL_MS2 = 6e4;
39208
+ function agentPoolLoopDisabled(env = process.env) {
39209
+ return env["MRRLIN_BRIDGE_AGENT_POOL_DISABLE"] === "1";
39210
+ }
39211
+ function defaultRunCli(args) {
39212
+ const cli = process.env["EXTERNAL_AGENTS_CLI"];
39213
+ const [cmd, baseArgs] = cli ? ["node", [cli, ...args]] : ["external-agents", args];
39214
+ return new Promise((resolve) => {
39215
+ let stdout = "";
39216
+ let settled = false;
39217
+ const done = (code) => {
39218
+ if (!settled) {
39219
+ settled = true;
39220
+ resolve({ stdout, code });
39221
+ }
39222
+ };
39223
+ try {
39224
+ const child = (0, import_node_child_process4.spawn)(cmd, baseArgs, { stdio: ["ignore", "pipe", "ignore"] });
39225
+ const timer = setTimeout(() => {
39226
+ try {
39227
+ child.kill("SIGKILL");
39228
+ } catch {
39229
+ }
39230
+ done(1);
39231
+ }, 5e3);
39232
+ timer.unref?.();
39233
+ child.stdout.on("data", (d) => {
39234
+ stdout += d.toString();
39235
+ if (stdout.length > 2e5) stdout = stdout.slice(-2e5);
39236
+ });
39237
+ child.on("error", () => {
39238
+ clearTimeout(timer);
39239
+ done(1);
39240
+ });
39241
+ child.on("close", (code) => {
39242
+ clearTimeout(timer);
39243
+ done(code ?? 1);
39244
+ });
39245
+ } catch {
39246
+ done(1);
39247
+ }
39248
+ });
39249
+ }
39250
+ async function runAgentPoolCycle(deps) {
39251
+ try {
39252
+ const snapshot = await collectAgentPoolSnapshot({
39253
+ runCli: deps.runCli ?? defaultRunCli,
39254
+ now: deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString())
39255
+ });
39256
+ await deps.push(snapshot);
39257
+ } catch (err) {
39258
+ deps.onError?.(err);
39259
+ }
39260
+ }
39261
+ function startAgentPoolLoop(deps) {
39262
+ const env = deps.env ?? process.env;
39263
+ const intervalMs = deps.intervalMs ?? Number(env["MRRLIN_BRIDGE_AGENT_POOL_INTERVAL_MS"] ?? DEFAULT_INTERVAL_MS2);
39264
+ let stopped = false;
39265
+ let timer = null;
39266
+ const schedule = () => {
39267
+ if (stopped) return;
39268
+ timer = setTimeout(() => {
39269
+ void runAgentPoolCycle(deps).then(() => schedule());
39270
+ }, intervalMs);
39271
+ timer.unref?.();
39272
+ };
39273
+ timer = setTimeout(() => {
39274
+ void runAgentPoolCycle(deps).then(() => schedule());
39275
+ }, 1e3);
39276
+ timer.unref?.();
39277
+ return {
39278
+ stop() {
39279
+ stopped = true;
39280
+ if (timer) clearTimeout(timer);
39281
+ }
39282
+ };
39283
+ }
39284
+
38970
39285
  // src/execution-consumer.ts
38971
- var import_node_child_process6 = require("node:child_process");
39286
+ var import_node_child_process8 = require("node:child_process");
38972
39287
  var import_node_crypto4 = __toESM(require("node:crypto"), 1);
38973
39288
  var import_node_path6 = __toESM(require("node:path"), 1);
38974
39289
  var import_node_util = require("node:util");
@@ -38991,6 +39306,9 @@ function normalizeDomain(value) {
38991
39306
  function resolveHeads(domain2) {
38992
39307
  return [...HEADS_BY_DOMAIN[normalizeDomain(domain2)]];
38993
39308
  }
39309
+ function allReviewHeads() {
39310
+ return [...new Set(Object.values(HEADS_BY_DOMAIN).flat())];
39311
+ }
38994
39312
  var PERSONAS_BY_DOMAIN = {
38995
39313
  seo: "seo-writer",
38996
39314
  content: "content-author",
@@ -39009,7 +39327,7 @@ var import_node_path3 = __toESM(require("node:path"), 1);
39009
39327
  var import_node_url2 = require("node:url");
39010
39328
 
39011
39329
  // src/consensus/codex-exec.ts
39012
- var import_node_child_process3 = require("node:child_process");
39330
+ var import_node_child_process5 = require("node:child_process");
39013
39331
  function extractFinalMessage(stdout) {
39014
39332
  let last = null;
39015
39333
  for (const rawLine of stdout.split(/\r?\n/)) {
@@ -39054,7 +39372,7 @@ function firstString(...values) {
39054
39372
  return null;
39055
39373
  }
39056
39374
  function runCodexExec(input, deps) {
39057
- const spawn2 = deps?.spawn ?? import_node_child_process3.spawn;
39375
+ const spawn3 = deps?.spawn ?? import_node_child_process5.spawn;
39058
39376
  const executable = input.codexExecutable ?? "codex";
39059
39377
  const sandbox = input.sandbox ?? "read-only";
39060
39378
  const fullPrompt = input.developerInstructions.trim() ? `${input.developerInstructions.trim()}
@@ -39086,7 +39404,7 @@ ${input.prompt}` : input.prompt;
39086
39404
  };
39087
39405
  let child;
39088
39406
  try {
39089
- child = spawn2(executable, args, options);
39407
+ child = spawn3(executable, args, options);
39090
39408
  } catch (err) {
39091
39409
  finish({ ok: false, error: `Failed to spawn codex exec: ${errMessage(err)}` });
39092
39410
  return;
@@ -39193,7 +39511,7 @@ function parseAcceptanceCriteria(markdown) {
39193
39511
  }
39194
39512
 
39195
39513
  // src/consensus/code-gate-git.ts
39196
- var import_node_child_process4 = require("node:child_process");
39514
+ var import_node_child_process6 = require("node:child_process");
39197
39515
  var DIFF_MAX_BYTES = 2e5;
39198
39516
  var FASTGATE_OUTPUT_MAX_BYTES = 16e3;
39199
39517
  var GIT_TIMEOUT_MS = 6e4;
@@ -39209,7 +39527,7 @@ var defaultRunCmd = (cmd, args, opts) => new Promise((resolve) => {
39209
39527
  };
39210
39528
  let child;
39211
39529
  try {
39212
- child = (0, import_node_child_process4.spawn)(cmd, args, {
39530
+ child = (0, import_node_child_process6.spawn)(cmd, args, {
39213
39531
  cwd: opts.cwd,
39214
39532
  env: process.env,
39215
39533
  shell: false,
@@ -39455,7 +39773,7 @@ function reviserIdentityInstructions(taskId, title) {
39455
39773
  }
39456
39774
  function buildSpecGateDeps(args) {
39457
39775
  const { client, projectSlug, specPageId, cwd, codexExecutable, extraHeadCount = 0 } = args;
39458
- const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 3);
39776
+ const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
39459
39777
  const maxHeadCalls = rounds * (4 + extraHeadCount + 1);
39460
39778
  const timeoutMs = Number(process.env["MRRLIN_CONSENSUS_HEAD_TIMEOUT_MS"] ?? 15e4);
39461
39779
  let intentPromise = null;
@@ -39469,13 +39787,22 @@ function buildSpecGateDeps(args) {
39469
39787
  const taskId = await resolveGatedTaskId(client, projectSlug, { id: page.id, taskId: page.taskId ?? null });
39470
39788
  if (taskId === null) {
39471
39789
  const project = await client.getProject(projectSlug);
39472
- return { taskId: null, seed: null, title: null, domain: project.defaultDomain };
39790
+ return { taskId: null, seed: null, title: null, domain: project.defaultDomain, notes: null };
39473
39791
  }
39474
39792
  const task = await client.getTask(projectSlug, taskId);
39475
39793
  if (!task) {
39476
39794
  throw new Error(`gated task ${projectSlug}/${taskId} not found for spec ${specPageId} \u2014 cannot gate`);
39477
39795
  }
39478
- return { taskId, seed: task.operatorSeedRequest ?? null, title: task.title ?? "", domain: task.domain };
39796
+ const notes = task.notes ?? null;
39797
+ const rawSeed = task.operatorSeedRequest ?? null;
39798
+ let resolvedSeed = rawSeed;
39799
+ if (rawSeed === null || rawSeed.trim() === "") {
39800
+ const parsed = notes ? parseAcceptanceCriteria(notes) : null;
39801
+ if (parsed !== null && parsed.items.length > 0) {
39802
+ resolvedSeed = "(No operator statement on record for this task. Derived from the task's own notes \u2014 the following acceptance criteria were extracted verbatim:)\n\n" + parsed.items.map((c) => `- ${c}`).join("\n");
39803
+ }
39804
+ }
39805
+ return { taskId, seed: resolvedSeed, title: task.title ?? "", domain: task.domain, notes };
39479
39806
  })();
39480
39807
  }
39481
39808
  return intentPromise;
@@ -39609,7 +39936,7 @@ ${artifact}`,
39609
39936
  }
39610
39937
  function buildCodeGateDeps(args) {
39611
39938
  const { client, projectSlug, baseRef, cwd, codexExecutable, extraHeads } = args;
39612
- const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 3);
39939
+ const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
39613
39940
  const heads = ["code-reviewer", ...extraHeads ?? []];
39614
39941
  const maxHeadCalls = rounds * (heads.length + 1);
39615
39942
  const headTimeoutMs = Number(process.env["MRRLIN_CONSENSUS_HEAD_TIMEOUT_MS"] ?? 15e4);
@@ -39690,7 +40017,7 @@ function renderCriteriaBlock(criteria) {
39690
40017
  }
39691
40018
  async function buildDeliverableGateDeps(args) {
39692
40019
  const { client, projectSlug, deliverablePageId, cwd, codexExecutable, runId } = args;
39693
- const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 3);
40020
+ const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
39694
40021
  const timeoutMs = Number(process.env["MRRLIN_CONSENSUS_HEAD_TIMEOUT_MS"] ?? 15e4);
39695
40022
  const page = await client.getWikiPage(projectSlug, deliverablePageId);
39696
40023
  const taskId = page.taskId ?? null;
@@ -40613,7 +40940,7 @@ async function awaitTurnWithDeadline(opts) {
40613
40940
  }
40614
40941
 
40615
40942
  // src/worktree.ts
40616
- var import_node_child_process5 = require("node:child_process");
40943
+ var import_node_child_process7 = require("node:child_process");
40617
40944
  var import_node_fs4 = require("node:fs");
40618
40945
  var import_node_os3 = require("node:os");
40619
40946
  var import_node_path5 = require("node:path");
@@ -40641,9 +40968,9 @@ async function withCheckoutLock(locksDir, slug, fn) {
40641
40968
  function worktreeBranch(runId) {
40642
40969
  return `mrrlin/run-${runId}`;
40643
40970
  }
40644
- var defaultGitExec = (args, env) => (0, import_node_child_process5.execFileSync)("git", args, { encoding: "utf8", timeout: 3e4, env });
40971
+ var defaultGitExec = (args, env) => (0, import_node_child_process7.execFileSync)("git", args, { encoding: "utf8", timeout: 3e4, env });
40645
40972
  function git(checkout, args) {
40646
- return (0, import_node_child_process5.execFileSync)("git", ["-C", checkout, ...args], {
40973
+ return (0, import_node_child_process7.execFileSync)("git", ["-C", checkout, ...args], {
40647
40974
  encoding: "utf8",
40648
40975
  timeout: 3e4,
40649
40976
  env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
@@ -40848,6 +41175,38 @@ function isExecutionTurnTimeoutReason(reason) {
40848
41175
  function contextMaxArtifacts() {
40849
41176
  return readPositiveIntEnv("MRRLIN_DIRECTOR_BRIDGE_CONTEXT_MAX_ARTIFACTS", DEFAULT_CONTEXT_MAX_ARTIFACTS);
40850
41177
  }
41178
+ var DEFAULT_COMPLETION_TURN_BUDGET_MS = 6e4;
41179
+ var DEFAULT_COMPLETION_TURN_MIN_BUDGET_MS = 1e4;
41180
+ function completionTurnBudgetMs() {
41181
+ return readPositiveIntEnv("MRRLIN_COMPLETION_TURN_BUDGET_MS", DEFAULT_COMPLETION_TURN_BUDGET_MS);
41182
+ }
41183
+ function completionTurnMinBudgetMs() {
41184
+ return readPositiveIntEnv("MRRLIN_COMPLETION_TURN_MIN_BUDGET_MS", DEFAULT_COMPLETION_TURN_MIN_BUDGET_MS);
41185
+ }
41186
+ function isCompletionTurnDisabled() {
41187
+ return (process.env.MRRLIN_COMPLETION_TURN_DISABLED ?? "").trim() === "1";
41188
+ }
41189
+ function buildCompletionTurnPrompt(taskId, currentStatus) {
41190
+ return [
41191
+ `Your work turn ended but task ${taskId} is still in status "${currentStatus}".`,
41192
+ "",
41193
+ "Decide honestly whether the work is complete:",
41194
+ "",
41195
+ "If the work IS genuinely complete:",
41196
+ "1. Update the task notes with a markdown summary of what you accomplished",
41197
+ " (see the notes format contract in your base instructions).",
41198
+ "2. Call update_task with status=closed_done.",
41199
+ "3. Append a completion-evidence artifact (checkpoint or diff_summary)",
41200
+ " to the open run if you haven't already.",
41201
+ "",
41202
+ "If the work is NOT complete \u2014 be honest, do not close incomplete work:",
41203
+ "1. Call update_task with the appropriate status (needs_human, awaiting_verification, etc.)",
41204
+ " and explain what remains in the notes.",
41205
+ "2. Append a handoff_judgement artifact explaining what the operator needs to do.",
41206
+ "",
41207
+ "You must pick one of these paths \u2014 do not leave the task in_progress."
41208
+ ].join("\n");
41209
+ }
40851
41210
  var BASE_INSTRUCTIONS_PREFIX = [
40852
41211
  "You are the Mrrlin Director driving a task toward completion autonomously.",
40853
41212
  "You can call MCP tools to read and write durable project state.",
@@ -40863,6 +41222,9 @@ var BASE_INSTRUCTIONS_PREFIX = [
40863
41222
  "- Close the task with `update_task` status=closed_done ONLY when it is genuinely complete",
40864
41223
  " AND you appended at least one completion-evidence artifact. Otherwise leave the task",
40865
41224
  " open so the run fails into an operator handoff instead of pretending success.",
41225
+ "- For a CODE task the run opens a draft PR for you after your turn; opening that PR is NOT",
41226
+ " completion. Do NOT set closed_done for code work \u2014 leave the task open; the run parks at",
41227
+ " `pull_request` (waiting for merge) and the operator merges. Closure happens after merge.",
40866
41228
  "",
40867
41229
  "File artifacts:",
40868
41230
  "- When your output is a file (markdown report, HTML, screenshot), upload it with",
@@ -40882,6 +41244,23 @@ var BASE_INSTRUCTIONS_PREFIX = [
40882
41244
  " and lose login/cookie state across runs.",
40883
41245
  "- If tool listings show duplicate names across servers, prefer the `mrrlin-browser` one",
40884
41246
  " unconditionally. Do not switch mid-run.",
41247
+ "",
41248
+ "Task description quality contract \u2014 every task you create or update via create_task or update_task:",
41249
+ "- Title: short, actionable, human-readable. Describe what needs to happen, not internal jargon.",
41250
+ ' Good: "Audit crawled-not-indexed pages and fix 404 errors"',
41251
+ ' Bad: "GSC coverage triage crawled-not-indexed + 404"',
41252
+ "- Notes (markdown): write for a human operator scanning a task list.",
41253
+ " First line: one plain-text sentence summarizing the task (no heading).",
41254
+ " Then scale the structure to complexity:",
41255
+ " For simple tasks \u2014 the one-line summary may be enough. No headings required.",
41256
+ " For non-trivial tasks, use these sections as needed:",
41257
+ " ## Context \u2014 why this task exists; link to parent/trigger task if follow-up.",
41258
+ " ## Scope \u2014 concrete actions and acceptance criteria.",
41259
+ " ## References \u2014 links, data, relevant findings (omit if nothing to reference).",
41260
+ " When closing a task (update_task status=closed_done), APPEND a Result section:",
41261
+ " ## Result \u2014 what was accomplished, key findings or metrics, follow-up tasks created.",
41262
+ " Do NOT remove earlier sections \u2014 they remain useful for context.",
41263
+ " Adapt depth to the task: a one-line fix needs one sentence, not four headings.",
40885
41264
  ""
40886
41265
  ];
40887
41266
  var CONSUMER_BASE_INSTRUCTIONS = [
@@ -40896,7 +41275,23 @@ var EXECUTOR_BASE_INSTRUCTIONS = [
40896
41275
  "files and running git/build/test commands WITHIN this worktree. Keep all changes inside the",
40897
41276
  "worktree and do not read the operator's personal secrets or files outside it \u2014 this is a rule",
40898
41277
  "you must follow, not something the environment enforces. Be concise. When you have made the",
40899
- "next increment of progress, stop the turn."
41278
+ "next increment of progress, stop the turn.",
41279
+ // ADR 0022 Phase 3: the executor is the orchestrator, not the sole worker. It has the
41280
+ // external-agents `pick`/`dispatch` MCP tools (a pool of cheap/free models) and should
41281
+ // delegate the bulk substantive work to them in parallel, keeping itself as judge. This
41282
+ // minimises the expensive default model's own token spend while the verify step preserves
41283
+ // quality. If external-agents isn't installed the tools are simply absent — these lines then
41284
+ // read as advice the model can't act on, and it does the work itself (graceful, no-op).
41285
+ "You are the ORCHESTRATOR and JUDGE, not the sole worker. If the external-agents `pick` and",
41286
+ "`dispatch` tools are available, delegate substantive, parallelizable subtasks \u2014 file edits,",
41287
+ "boilerplate, tests, research, mechanical refactors \u2014 to pool subagents via `dispatch`, several",
41288
+ "in parallel where possible, to minimise your own token spend. Keep for yourself only what needs",
41289
+ "your judgment: decomposition, integration, hard reasoning, and final decisions. You MUST verify",
41290
+ "every delegated result against the actual files and tests before it lands \u2014 pool models are",
41291
+ "lower-reliability and a wrong delegated edit is YOUR responsibility; never commit unverified",
41292
+ "delegated output. Delegation is the default for routine bulk work, but do trivial tasks yourself",
41293
+ "when dispatch overhead exceeds the work. If dispatch fails or the pool is unavailable, do the",
41294
+ "work yourself \u2014 never block the run on the pool."
40900
41295
  ].join("\n");
40901
41296
  function artifactKindForEvent(event) {
40902
41297
  switch (event.type) {
@@ -40935,7 +41330,7 @@ var BLAST_RADIUS_FLOOR_PATTERNS = [/^supabase\/migrations\//, /^packages\/db\/sr
40935
41330
  function isBlastRadiusFloorPath(path15) {
40936
41331
  return BLAST_RADIUS_FLOOR_PATTERNS.some((re) => re.test(path15));
40937
41332
  }
40938
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process6.execFile);
41333
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process8.execFile);
40939
41334
  async function defaultListRunDiffPaths(opts) {
40940
41335
  const parse5 = (stdout) => stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
40941
41336
  try {
@@ -41315,10 +41710,44 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
41315
41710
  await failRun(deps, projectSlug, task, runId, leaseId, outcome.error);
41316
41711
  return;
41317
41712
  }
41318
- const readiness = await assertRunCompletionReady(deps, projectSlug, task.id, runId);
41713
+ let readiness = await assertRunCompletionReady(deps, projectSlug, task.id, runId);
41319
41714
  if (!readiness.ok) {
41320
- await failRun(deps, projectSlug, readiness.task, runId, leaseId, readiness.reason);
41321
- return;
41715
+ const completionBudget = Math.min(completionTurnBudgetMs(), remainingTurnBudgetMs());
41716
+ if (isCompletionTurnDisabled() || completionBudget < completionTurnMinBudgetMs()) {
41717
+ await failRun(deps, projectSlug, readiness.task, runId, leaseId, readiness.reason);
41718
+ return;
41719
+ }
41720
+ console.warn(
41721
+ JSON.stringify({
41722
+ event: "completion_turn_triggered",
41723
+ projectSlug,
41724
+ runId,
41725
+ taskId: task.id,
41726
+ previousStatus: readiness.task.status
41727
+ })
41728
+ );
41729
+ const completionPrompt = buildCompletionTurnPrompt(task.id, readiness.task.status);
41730
+ const completionOutcome = await driveTurn(
41731
+ deps,
41732
+ codex,
41733
+ projectSlug,
41734
+ runId,
41735
+ leaseId,
41736
+ threadId,
41737
+ completionPrompt,
41738
+ completionBudget,
41739
+ "completion"
41740
+ );
41741
+ if (!completionOutcome.ok) {
41742
+ await resetWedgedThread(deps, projectSlug, task.id, completionOutcome.interruptFailed);
41743
+ await failRun(deps, projectSlug, readiness.task, runId, leaseId, completionOutcome.error);
41744
+ return;
41745
+ }
41746
+ readiness = await assertRunCompletionReady(deps, projectSlug, task.id, runId);
41747
+ if (!readiness.ok) {
41748
+ await failRun(deps, projectSlug, readiness.task, runId, leaseId, readiness.reason);
41749
+ return;
41750
+ }
41322
41751
  }
41323
41752
  if (useExecutor) {
41324
41753
  if (!worktreePath) {
@@ -41410,6 +41839,13 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
41410
41839
  pullRequestUrl: pr.url,
41411
41840
  status: "pull_request"
41412
41841
  });
41842
+ await reconcileTaskAwaitingMerge(
41843
+ deps,
41844
+ projectSlug,
41845
+ readiness.task,
41846
+ runId,
41847
+ `PR opened (${pr.url}); autoDeploy is off \u2014 the operator merges when ready.`
41848
+ );
41413
41849
  return;
41414
41850
  }
41415
41851
  if (deps.deployGate) {
@@ -41424,7 +41860,7 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
41424
41860
  pullRequestUrl: pr.url,
41425
41861
  status: "pull_request"
41426
41862
  });
41427
- await notifyOperatorHandoff(
41863
+ await reconcileTaskAwaitingMerge(
41428
41864
  deps,
41429
41865
  projectSlug,
41430
41866
  readiness.task,
@@ -41716,6 +42152,34 @@ async function failRun(deps, projectSlug, task, runId, leaseId, reason) {
41716
42152
  await notifyOperatorHandoff(deps, projectSlug, task, runId, reason);
41717
42153
  }
41718
42154
  }
42155
+ async function reconcileTaskAwaitingMerge(deps, projectSlug, task, runId, reason) {
42156
+ if (task.status === "handoff_needed") return;
42157
+ await deps.client.appendExecutionArtifact(projectSlug, runId, {
42158
+ id: newArtifactId(),
42159
+ kind: "handoff_judgement",
42160
+ payload: {
42161
+ verdict: "approved",
42162
+ reason,
42163
+ summary: `Execution run ${runId} opened a PR and is awaiting operator action.`,
42164
+ operatorQuestion: `Task ${task.id} (${task.title}) has an open PR: ${reason}`,
42165
+ source: "execution-consumer"
42166
+ }
42167
+ }).catch(() => {
42168
+ });
42169
+ try {
42170
+ await deps.client.updateTask(projectSlug, task.id, { status: "handoff_needed" });
42171
+ } catch (error51) {
42172
+ console.warn(
42173
+ JSON.stringify({
42174
+ event: "reconcile_awaiting_merge_failed",
42175
+ projectSlug,
42176
+ taskId: task.id,
42177
+ runId,
42178
+ error: error51 instanceof Error ? error51.message : String(error51)
42179
+ })
42180
+ );
42181
+ }
42182
+ }
41719
42183
  async function notifyOperatorHandoff(deps, projectSlug, task, runId, reason) {
41720
42184
  try {
41721
42185
  await deps.client.appendExecutionArtifact(projectSlug, runId, {
@@ -41915,13 +42379,15 @@ async function driveAsyncJob(deps, projectSlug, jobId) {
41915
42379
  }).catch(() => {
41916
42380
  });
41917
42381
  const delivery = descriptor2.onComplete(outcome, input);
41918
- await deps.client.createInboxItem(projectSlug, {
41919
- id: delivery.id ?? `II-${import_node_crypto5.default.randomUUID()}`,
41920
- kind: delivery.kind,
41921
- body: delivery.body,
41922
- ...delivery.taskId ? { taskId: delivery.taskId } : {}
41923
- }).catch(() => {
41924
- });
42382
+ if (!delivery.suppressInbox) {
42383
+ await deps.client.createInboxItem(projectSlug, {
42384
+ id: delivery.id ?? `II-${import_node_crypto5.default.randomUUID()}`,
42385
+ kind: delivery.kind,
42386
+ body: delivery.body,
42387
+ ...delivery.taskId ? { taskId: delivery.taskId } : {}
42388
+ }).catch(() => {
42389
+ });
42390
+ }
41925
42391
  if (descriptor2.wakeDirector) deps.wake(projectSlug, claimed.directorSessionId, delivery.body, delivery.kind);
41926
42392
  }
41927
42393
  function deriveHash() {
@@ -46768,6 +47234,23 @@ function buildThreadStartParams(msg, config2, desiredModel) {
46768
47234
  'or "let me call the \u2026 tool" in ANY language. The operator already sees tool calls in the',
46769
47235
  "transcript. Just call the tools and reply with the outcome \u2014 start your reply with the result.",
46770
47236
  "When creating or updating tasks, always provide `autonomyLevel` and `autonomyReason`.",
47237
+ "",
47238
+ "Task description quality contract \u2014 every task you create or update via create_task or update_task:",
47239
+ "- Title: short, actionable, human-readable. Describe what needs to happen, not internal jargon.",
47240
+ ' Good: "Audit crawled-not-indexed pages and fix 404 errors"',
47241
+ ' Bad: "GSC coverage triage crawled-not-indexed + 404"',
47242
+ "- Notes (markdown): write for a human operator scanning a task list.",
47243
+ " First line: one plain-text sentence summarizing the task (no heading).",
47244
+ " Then scale the structure to complexity:",
47245
+ " For simple tasks \u2014 the one-line summary may be enough. No headings required.",
47246
+ " For non-trivial tasks, use these sections as needed:",
47247
+ " ## Context \u2014 why this task exists; link to parent/trigger task if follow-up.",
47248
+ " ## Scope \u2014 concrete actions and acceptance criteria.",
47249
+ " ## References \u2014 links, data, relevant findings (omit if nothing to reference).",
47250
+ " When closing a task (update_task status=closed_done), APPEND a Result section:",
47251
+ " ## Result \u2014 what was accomplished, key findings or metrics, follow-up tasks created.",
47252
+ " Do NOT remove earlier sections \u2014 they remain useful for context.",
47253
+ " Adapt depth to the task: a one-line fix needs one sentence, not four headings.",
46771
47254
  "When you create a task out of an operator request, set `operatorSeedRequest` to the operator's",
46772
47255
  "VERBATIM words (their request as they wrote it \u2014 NOT your paraphrase or synthesized notes). It is",
46773
47256
  "the spec gate's independent record of intent; omit it only when there is genuinely no operator",
@@ -47017,7 +47500,7 @@ function resolveDefaultBranch(checkoutPath) {
47017
47500
  if (!checkoutPath) return "main";
47018
47501
  const runGit = (args) => {
47019
47502
  try {
47020
- return (0, import_node_child_process7.execFileSync)("git", ["-C", checkoutPath, ...args], {
47503
+ return (0, import_node_child_process9.execFileSync)("git", ["-C", checkoutPath, ...args], {
47021
47504
  encoding: "utf8",
47022
47505
  stdio: ["ignore", "pipe", "ignore"],
47023
47506
  timeout: 15e3,
@@ -47176,7 +47659,7 @@ function sessionIdOf(msg) {
47176
47659
  return typeof id === "string" ? id : null;
47177
47660
  }
47178
47661
  function attachBridgeWss(server, opts) {
47179
- const { token, allowedOrigins, onReady, logger, onPaired, bridgeInstanceId, selfUpdate } = opts;
47662
+ const { token, allowedOrigins, onReady, logger, onPaired, bridgeInstanceId, selfUpdate, codex } = opts;
47180
47663
  const wss = new import_websocket_server.default({ server, path: "/ws" });
47181
47664
  wss.on("connection", (socket, request) => {
47182
47665
  const origin = request.headers.origin;
@@ -47241,7 +47724,15 @@ function attachBridgeWss(server, opts) {
47241
47724
  version: selfUpdate.version,
47242
47725
  binSha: selfUpdate.binSha,
47243
47726
  latestVersion: selfUpdate.getLatestVersion()
47244
- } : {}
47727
+ } : {},
47728
+ ...codex ? (() => {
47729
+ const st = codex.getState();
47730
+ return {
47731
+ codexVersion: st.version,
47732
+ codexLatestVersion: st.latestVersion,
47733
+ codexUpdateCommand: codex.getUpdateCommand()
47734
+ };
47735
+ })() : {}
47245
47736
  });
47246
47737
  onPaired?.();
47247
47738
  onReady?.(socket, request, send, logger ? { logger, span } : void 0);
@@ -47265,13 +47756,14 @@ async function waitFor(pred, timeoutMs) {
47265
47756
  return pred();
47266
47757
  }
47267
47758
  function attachSeqMetadata(msg, seq, turnId) {
47268
- if (msg.type === "event" || msg.type === "rateLimits" || msg.type === "error") {
47759
+ if (msg.type === "event" || msg.type === "rateLimits" || msg.type === "error" || msg.type === "turn-executing") {
47269
47760
  return { ...msg, seq, turnId };
47270
47761
  }
47271
47762
  return msg;
47272
47763
  }
47273
47764
  function createBridgeMessageHandler(deps) {
47274
47765
  const tombstones = deps.tombstones ?? /* @__PURE__ */ new Map();
47766
+ const pendingTurnSessions = /* @__PURE__ */ new Set();
47275
47767
  return (socket, _request, send, logCtx) => {
47276
47768
  const boundSessions = /* @__PURE__ */ new Set();
47277
47769
  socket.on("close", () => {
@@ -47523,6 +48015,14 @@ function createBridgeMessageHandler(deps) {
47523
48015
  interruptBuf.state = stopResult.next;
47524
48016
  recordInterrupted(interruptBuf, Date.now(), deps.suspendedMs?.() ?? 0);
47525
48017
  }
48018
+ } else if (interruptBuf && interruptBuf.state === "live" && !existing.cancel) {
48019
+ const stopResult = transition("live", { kind: "stop" });
48020
+ if (stopResult.next !== "invalid") {
48021
+ interruptBuf.state = stopResult.next;
48022
+ recordInterrupted(interruptBuf, Date.now(), deps.suspendedMs?.() ?? 0);
48023
+ existing.inFlight = false;
48024
+ pendingTurnSessions.delete(directorSessionId2);
48025
+ }
47526
48026
  }
47527
48027
  const codexClient2 = await deps.acquireCodex();
47528
48028
  try {
@@ -47581,6 +48081,18 @@ function createBridgeMessageHandler(deps) {
47581
48081
  sendForSpan({ type: "error", directorSessionId, error: "message is required." });
47582
48082
  return;
47583
48083
  }
48084
+ const busyThread = deps.threads.get(directorSessionId);
48085
+ if (msg.takeover !== true && (pendingTurnSessions.has(directorSessionId) || busyThread?.inFlight)) {
48086
+ sendForSpan({
48087
+ type: "error",
48088
+ directorSessionId,
48089
+ code: "busy",
48090
+ ...busyThread?.replay ? { inFlightTurnId: busyThread.replay.turnId } : {},
48091
+ error: "Director session is busy."
48092
+ });
48093
+ return;
48094
+ }
48095
+ pendingTurnSessions.add(directorSessionId);
47584
48096
  const codexClient = await deps.acquireCodex();
47585
48097
  let thread = deps.threads.get(directorSessionId);
47586
48098
  if (thread?.inFlight && msg.takeover === true) {
@@ -47666,6 +48178,7 @@ function createBridgeMessageHandler(deps) {
47666
48178
  if (/unauthor|not logged in|login|401|auth/i.test(acquireErrMsg)) {
47667
48179
  deps.setCodexAuth?.("expired");
47668
48180
  }
48181
+ pendingTurnSessions.delete(directorSessionId);
47669
48182
  deps.releaseCodex();
47670
48183
  return;
47671
48184
  }
@@ -47677,10 +48190,12 @@ function createBridgeMessageHandler(deps) {
47677
48190
  ...thread.replay ? { inFlightTurnId: thread.replay.turnId } : {},
47678
48191
  error: "Director session is busy."
47679
48192
  });
48193
+ pendingTurnSessions.delete(directorSessionId);
47680
48194
  deps.releaseCodex();
47681
48195
  return;
47682
48196
  }
47683
48197
  thread.inFlight = true;
48198
+ pendingTurnSessions.delete(directorSessionId);
47684
48199
  thread.replay = createReplayBuffer({
47685
48200
  turnId: (0, import_node_crypto7.randomUUID)(),
47686
48201
  spanId,
@@ -47689,6 +48204,7 @@ function createBridgeMessageHandler(deps) {
47689
48204
  });
47690
48205
  thread.lastUsedAt = Date.now();
47691
48206
  const threadId = thread.threadId;
48207
+ const currentReplayBuf = thread.replay;
47692
48208
  sendForSpan({
47693
48209
  type: "turn-started",
47694
48210
  directorSessionId,
@@ -47730,6 +48246,15 @@ function createBridgeMessageHandler(deps) {
47730
48246
  };
47731
48247
  try {
47732
48248
  await deps.runExclusive(async () => {
48249
+ if (currentReplayBuf.state === "interrupted" || currentReplayBuf.state === "completed") {
48250
+ logCtx?.logger.logOut({
48251
+ spanId,
48252
+ sessionId: directorSessionId,
48253
+ type: "<queued-turn-cancelled-drained>",
48254
+ payload: { sessionId: directorSessionId, turnId: currentReplayBuf.turnId }
48255
+ });
48256
+ return;
48257
+ }
47733
48258
  let resolveTurn = null;
47734
48259
  let rejectTurn = null;
47735
48260
  const turnFinished = new Promise((resolve, reject) => {
@@ -47836,6 +48361,7 @@ ${effectiveText}` : effectiveText;
47836
48361
  if (imageDataUri) {
47837
48362
  turnInput.push({ type: "image", url: imageDataUri });
47838
48363
  }
48364
+ sendInTurn({ type: "turn-executing", directorSessionId, turnId: currentReplayBuf.turnId });
47839
48365
  await codexClient.turn.start({
47840
48366
  threadId: thread.threadId,
47841
48367
  input: turnInput,
@@ -47891,7 +48417,9 @@ ${effectiveText}` : effectiveText;
47891
48417
  }
47892
48418
  }
47893
48419
  } finally {
47894
- thread.inFlight = false;
48420
+ if (thread.replay === currentReplayBuf) {
48421
+ thread.inFlight = false;
48422
+ }
47895
48423
  thread.lastUsedAt = Date.now();
47896
48424
  deps.persistThreads();
47897
48425
  deps.releaseCodex();
@@ -47931,7 +48459,7 @@ function parseConfig() {
47931
48459
  }
47932
48460
  function scanForOtherBridges() {
47933
48461
  try {
47934
- const result = (0, import_node_child_process7.spawnSync)("pgrep", ["-f", "mrrlin-mcp director-bridge"], {
48462
+ const result = (0, import_node_child_process9.spawnSync)("pgrep", ["-f", "mrrlin-mcp director-bridge"], {
47935
48463
  encoding: "utf8"
47936
48464
  });
47937
48465
  if (result.status !== 0) return [];
@@ -47984,6 +48512,7 @@ async function startDirectorBridge() {
47984
48512
  );
47985
48513
  }
47986
48514
  });
48515
+ const codexProbeHandle = startCodexProbe();
47987
48516
  const consumerState = { inFlightRunId: null, lastClaimAt: null };
47988
48517
  const health = createServiceHealth();
47989
48518
  const apiBaseUrl = readConfiguredApiBaseUrl();
@@ -48341,6 +48870,10 @@ async function startDirectorBridge() {
48341
48870
  version: PKG_VERSION,
48342
48871
  binSha: bridgeBinShaShort,
48343
48872
  getLatestVersion: () => selfUpdateHandle.getLatestVersion()
48873
+ },
48874
+ codex: {
48875
+ getState: () => codexProbeHandle.getState(),
48876
+ getUpdateCommand: () => codexProbeHandle.getUpdateCommand()
48344
48877
  }
48345
48878
  });
48346
48879
  if (agentSecret && relayUrl) {
@@ -48512,6 +49045,16 @@ async function startDirectorBridge() {
48512
49045
  "[mrrlin-mcp director-bridge] scheduler tick loop disabled (MRRLIN_BRIDGE_SCHEDULER_TICK_DISABLE=1).\n"
48513
49046
  );
48514
49047
  }
49048
+ let agentPoolLoopHandle = null;
49049
+ if (tokenProvider && !agentPoolLoopDisabled(process.env)) {
49050
+ agentPoolLoopHandle = startAgentPoolLoop({
49051
+ push: (snapshot) => client.putOperatorAgentPool(snapshot),
49052
+ onError: (err) => process.stderr.write(
49053
+ `[mrrlin-mcp director-bridge] agent-pool snapshot push failed: ${err instanceof Error ? err.message : String(err)}
49054
+ `
49055
+ )
49056
+ });
49057
+ }
48515
49058
  await new Promise((resolve, reject) => {
48516
49059
  server.on("error", (err) => {
48517
49060
  if (err.code === "EADDRINUSE") {
@@ -48553,8 +49096,10 @@ async function startDirectorBridge() {
48553
49096
  if (claimTimer) clearTimeout(claimTimer);
48554
49097
  },
48555
49098
  () => schedulerTickHandle?.stop(),
49099
+ () => agentPoolLoopHandle?.stop(),
48556
49100
  () => codexKeepalive.dropNow(),
48557
- () => selfUpdateHandle.stop()
49101
+ () => selfUpdateHandle.stop(),
49102
+ () => codexProbeHandle.stop()
48558
49103
  ],
48559
49104
  shutdownCodex: () => codexServerManager.shutdown(),
48560
49105
  exit: (code) => process.exit(code),
@@ -52081,7 +52626,7 @@ var import_promises3 = require("node:fs/promises");
52081
52626
  var import_node_fs12 = require("node:fs");
52082
52627
  var import_node_path15 = __toESM(require("node:path"), 1);
52083
52628
  var import_node_os8 = __toESM(require("node:os"), 1);
52084
- var import_node_child_process8 = require("node:child_process");
52629
+ var import_node_child_process11 = require("node:child_process");
52085
52630
 
52086
52631
  // ../../packages/wiki/dist/index.js
52087
52632
  var import_promises2 = __toESM(require("node:fs/promises"), 1);
@@ -52518,6 +53063,278 @@ async function runCodeGate(_target, deps) {
52518
53063
  }
52519
53064
  }
52520
53065
 
53066
+ // src/consensus/multi-llm-head.ts
53067
+ var import_node_child_process10 = require("node:child_process");
53068
+ function errMessage3(err) {
53069
+ return err instanceof Error ? err.message : String(err);
53070
+ }
53071
+ function runExternalAgents(bin, args, timeoutMs, spawn3) {
53072
+ return new Promise((resolve) => {
53073
+ let settled = false;
53074
+ const timerRef = { id: void 0 };
53075
+ const finish = (r) => {
53076
+ if (settled) return;
53077
+ settled = true;
53078
+ if (timerRef.id) clearTimeout(timerRef.id);
53079
+ resolve(r);
53080
+ };
53081
+ const options = {
53082
+ stdio: ["ignore", "pipe", "pipe"],
53083
+ env: process.env,
53084
+ shell: false
53085
+ // SECURITY: prompt is an argv element; never hand it to a shell.
53086
+ };
53087
+ let child;
53088
+ try {
53089
+ child = spawn3(bin, args, options);
53090
+ } catch (err) {
53091
+ finish({ code: null, stdout: "", error: `spawn ${bin} failed: ${errMessage3(err)}` });
53092
+ return;
53093
+ }
53094
+ let stdout = "";
53095
+ timerRef.id = setTimeout(() => {
53096
+ child.kill("SIGKILL");
53097
+ finish({ code: null, stdout, error: `external-agents ${args[0]} timed out after ${timeoutMs}ms` });
53098
+ }, timeoutMs);
53099
+ child.stdout?.on("data", (chunk) => {
53100
+ stdout += chunk.toString();
53101
+ });
53102
+ child.stderr?.on("data", () => {
53103
+ });
53104
+ child.on("error", (err) => finish({ code: null, stdout, error: `external-agents ${args[0]} error: ${errMessage3(err)}` }));
53105
+ child.on("close", (code) => finish({ code, stdout }));
53106
+ });
53107
+ }
53108
+ async function pickPanel(input, deps) {
53109
+ const spawn3 = deps?.spawn ?? import_node_child_process10.spawn;
53110
+ const bin = deps?.externalAgentsBin ?? "external-agents";
53111
+ const args = [
53112
+ "pick",
53113
+ "--tier-prefer",
53114
+ input.tierPrefer ?? "strong",
53115
+ "--n",
53116
+ String(input.n),
53117
+ "--min-distinct-providers",
53118
+ String(input.n),
53119
+ "--exclude-providers",
53120
+ input.excludeProvider
53121
+ ];
53122
+ const r = await runExternalAgents(bin, args, input.timeoutMs, spawn3);
53123
+ if (r.error) return { ok: false, error: r.error };
53124
+ if (r.code === 3) return { ok: true, agents: [] };
53125
+ if (r.code !== 0) return { ok: false, error: `external-agents pick exited ${r.code}` };
53126
+ const agents = r.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
53127
+ return { ok: true, agents };
53128
+ }
53129
+ async function dispatchHead(agentId, prompt, timeoutMs, deps) {
53130
+ const spawn3 = deps?.spawn ?? import_node_child_process10.spawn;
53131
+ const bin = deps?.externalAgentsBin ?? "external-agents";
53132
+ const args = ["dispatch", agentId, "--json", prompt];
53133
+ const r = await runExternalAgents(bin, args, timeoutMs, spawn3);
53134
+ if (r.error) return { ok: false, error: r.error };
53135
+ const lines2 = r.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
53136
+ const jsonLine = [...lines2].reverse().find((l) => l[0] === "{");
53137
+ if (!jsonLine) return { ok: false, error: `dispatch ${agentId}: no JSON on stdout (exit ${r.code})` };
53138
+ let parsed;
53139
+ try {
53140
+ parsed = JSON.parse(jsonLine);
53141
+ } catch (err) {
53142
+ return { ok: false, error: `dispatch ${agentId}: unparseable JSON (${errMessage3(err)})` };
53143
+ }
53144
+ if (parsed.outcome !== "success") {
53145
+ return { ok: false, error: `dispatch ${agentId}: outcome=${parsed.outcome} (exit ${parsed.exit_code})` };
53146
+ }
53147
+ if (typeof parsed.text !== "string" || parsed.text.trim() === "") {
53148
+ return { ok: false, error: `dispatch ${agentId}: empty text on success` };
53149
+ }
53150
+ return { ok: true, text: parsed.text };
53151
+ }
53152
+ async function buildMultiHeadRunner(input, deps) {
53153
+ const picked = await pickPanel(
53154
+ { n: input.personas.length, excludeProvider: input.excludeProvider, tierPrefer: input.tierPrefer, timeoutMs: input.pickTimeoutMs },
53155
+ deps
53156
+ );
53157
+ if (!picked.ok) return { ok: false, error: picked.error };
53158
+ if (picked.agents.length === 0) return { ok: false, error: "pick returned no agents" };
53159
+ const agents = picked.agents;
53160
+ const assignment = {};
53161
+ input.personas.forEach((persona, i) => {
53162
+ const agent = agents[i % agents.length];
53163
+ if (agent) assignment[persona] = agent;
53164
+ });
53165
+ const runHead = async (persona, artifact, dismissed) => {
53166
+ const agentId = assignment[persona];
53167
+ if (!agentId) return { ok: false, error: `no agent assigned for persona ${persona}` };
53168
+ const instructions = input.personaInstructions[persona] ?? "";
53169
+ const dismissedBlock = dismissed.trim() ? `
53170
+
53171
+ ${dismissed.trim()}` : "";
53172
+ const prompt = instructions.trim() ? `${instructions.trim()}
53173
+
53174
+ ${artifact}${dismissedBlock}` : `${artifact}${dismissedBlock}`;
53175
+ return dispatchHead(agentId, prompt, input.dispatchTimeoutMs, deps);
53176
+ };
53177
+ return { ok: true, runner: { agents, assignment, runHead } };
53178
+ }
53179
+
53180
+ // src/consensus/multi-llm-shadow.ts
53181
+ var SHADOW_LOG_PREFIX = "__MULTI_LLM_SHADOW__";
53182
+ function rollUpVerdict(verdicts) {
53183
+ if (verdicts.includes("REJECT")) return "REJECT";
53184
+ if (verdicts.includes("REQUEST_CHANGES")) return "REQUEST_CHANGES";
53185
+ return "APPROVE";
53186
+ }
53187
+ async function runShadowPanel(input, deps) {
53188
+ const base = { ran: false, correlationId: input.correlationId, productionVerdict: input.productionVerdict };
53189
+ const built = await buildMultiHeadRunner(
53190
+ {
53191
+ personas: input.heads,
53192
+ personaInstructions: input.personaInstructions,
53193
+ excludeProvider: input.excludeProvider,
53194
+ tierPrefer: "strong",
53195
+ pickTimeoutMs: input.pickTimeoutMs,
53196
+ dispatchTimeoutMs: input.dispatchTimeoutMs
53197
+ },
53198
+ deps
53199
+ );
53200
+ if (!built.ok) return { ...base, reason: built.error };
53201
+ const verdicts = [];
53202
+ const perHead = await Promise.all(
53203
+ input.heads.map(async (persona) => {
53204
+ const agent = built.runner.assignment[persona] ?? "(unassigned)";
53205
+ try {
53206
+ const res = await built.runner.runHead(persona, input.artifact, "");
53207
+ if (res.ok) {
53208
+ const parsed = parseVerdict(res.text);
53209
+ return { persona, agent, verdict: parsed.verdict, ok: true };
53210
+ }
53211
+ } catch {
53212
+ }
53213
+ return { persona, agent, verdict: "ERROR", ok: false };
53214
+ })
53215
+ );
53216
+ for (const h of perHead) {
53217
+ if (h.ok && h.verdict !== "ERROR") verdicts.push(h.verdict);
53218
+ }
53219
+ if (verdicts.length === 0) {
53220
+ return { ...base, agents: built.runner.agents, perHead, reason: "no head produced a parseable verdict" };
53221
+ }
53222
+ const shadowVerdict = rollUpVerdict(verdicts);
53223
+ return {
53224
+ ran: true,
53225
+ correlationId: input.correlationId,
53226
+ agents: built.runner.agents,
53227
+ perHead,
53228
+ shadowVerdict,
53229
+ productionVerdict: input.productionVerdict,
53230
+ agrees: shadowVerdict === input.productionVerdict
53231
+ };
53232
+ }
53233
+ async function runShadowPanelSafe(input, deps, logger = (line) => console.error(line)) {
53234
+ let record2;
53235
+ try {
53236
+ record2 = await runShadowPanel(input, deps);
53237
+ } catch (err) {
53238
+ record2 = {
53239
+ ran: false,
53240
+ correlationId: input.correlationId,
53241
+ productionVerdict: input.productionVerdict,
53242
+ reason: `shadow panel threw: ${err instanceof Error ? err.message : String(err)}`
53243
+ };
53244
+ }
53245
+ try {
53246
+ logger(`${SHADOW_LOG_PREFIX} ${JSON.stringify(record2)}`);
53247
+ } catch {
53248
+ }
53249
+ return record2;
53250
+ }
53251
+
53252
+ // src/consensus/multi-llm-shadow-hook.ts
53253
+ var SHADOW_ENV_FLAG = "MRRLIN_CONSENSUS_MULTI_LLM_SHADOW";
53254
+ var ORCHESTRATOR_PROVIDER = "openai";
53255
+ var PICK_TIMEOUT_MS = 15e3;
53256
+ var DISPATCH_TIMEOUT_MS = 6e4;
53257
+ function productionVerdictFromOutcome(outcome) {
53258
+ if (outcome === "CONVERGED") return "APPROVE";
53259
+ if (outcome === "CONSENSUS_BLOCKED") return "REQUEST_CHANGES";
53260
+ return null;
53261
+ }
53262
+ async function maybeRunShadow(input, deps, logger) {
53263
+ const env = input.env ?? process.env;
53264
+ if (!env[SHADOW_ENV_FLAG]) return false;
53265
+ const productionVerdict = productionVerdictFromOutcome(input.outcome);
53266
+ if (productionVerdict === null) return false;
53267
+ try {
53268
+ const heads = input.heads ?? [...resolveHeads(input.domain), ...input.extraHeads ?? []];
53269
+ const personaInstructions = {};
53270
+ for (const name of heads) {
53271
+ try {
53272
+ personaInstructions[name] = loadPersona(name);
53273
+ } catch {
53274
+ personaInstructions[name] = "";
53275
+ }
53276
+ }
53277
+ await runShadowPanelSafe(
53278
+ {
53279
+ heads,
53280
+ personaInstructions,
53281
+ artifact: input.artifact,
53282
+ productionVerdict,
53283
+ excludeProvider: ORCHESTRATOR_PROVIDER,
53284
+ pickTimeoutMs: PICK_TIMEOUT_MS,
53285
+ dispatchTimeoutMs: DISPATCH_TIMEOUT_MS,
53286
+ correlationId: input.correlationId
53287
+ },
53288
+ deps,
53289
+ logger
53290
+ );
53291
+ } catch {
53292
+ }
53293
+ return true;
53294
+ }
53295
+
53296
+ // src/consensus/multi-llm-activate.ts
53297
+ var MULTI_LLM_ENV_FLAG = "MRRLIN_CONSENSUS_MULTI_LLM";
53298
+ var ORCHESTRATOR_PROVIDER2 = "openai";
53299
+ var PICK_TIMEOUT_MS2 = 15e3;
53300
+ var DISPATCH_TIMEOUT_MS2 = 6e4;
53301
+ function multiLlmActive(env = process.env) {
53302
+ if (env["NODE_ENV"] === "production") return false;
53303
+ const flag = env[MULTI_LLM_ENV_FLAG];
53304
+ if (flag === "0" || flag === "false" || flag === "off") return false;
53305
+ return Boolean(flag) || env["MRRLIN_STAGING"] === "1";
53306
+ }
53307
+ async function resolveActiveRunHead(input, deps) {
53308
+ if (!multiLlmActive(input.env ?? process.env)) return null;
53309
+ try {
53310
+ const heads = input.heads ?? [...resolveHeads(input.domain), ...input.extraHeads ?? []];
53311
+ if (heads.length === 0) return null;
53312
+ const personaInstructions = {};
53313
+ for (const name of heads) {
53314
+ try {
53315
+ personaInstructions[name] = loadPersona(name);
53316
+ } catch {
53317
+ personaInstructions[name] = "";
53318
+ }
53319
+ }
53320
+ const built = await buildMultiHeadRunner(
53321
+ {
53322
+ personas: heads,
53323
+ personaInstructions,
53324
+ excludeProvider: ORCHESTRATOR_PROVIDER2,
53325
+ tierPrefer: "strong",
53326
+ pickTimeoutMs: PICK_TIMEOUT_MS2,
53327
+ dispatchTimeoutMs: DISPATCH_TIMEOUT_MS2
53328
+ },
53329
+ deps
53330
+ );
53331
+ if (!built.ok) return null;
53332
+ return built.runner.runHead;
53333
+ } catch {
53334
+ return null;
53335
+ }
53336
+ }
53337
+
52521
53338
  // src/async-tools/consensus.ts
52522
53339
  var import_node_crypto8 = require("node:crypto");
52523
53340
  function buildGateQuestionOptions(category) {
@@ -52581,13 +53398,37 @@ async function run(ctx, input) {
52581
53398
  codexExecutable: ctx.codexExecutable,
52582
53399
  extraHeadCount: input.extraHeads?.length ?? 0
52583
53400
  });
53401
+ const activeRunHead = await resolveActiveRunHead({
53402
+ heads: [...allReviewHeads(), ...input.extraHeads ?? []]
53403
+ });
53404
+ if (activeRunHead) deps.runHead = activeRunHead;
52584
53405
  const result = await runSpecGate(
52585
53406
  { projectSlug: input.projectSlug, specPageId: input.pageId },
52586
53407
  deps,
52587
53408
  input.extraHeads && input.extraHeads.length > 0 ? { extraHeads: input.extraHeads } : void 0
52588
53409
  );
53410
+ if (process.env[SHADOW_ENV_FLAG]) {
53411
+ try {
53412
+ const specPage = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
53413
+ if (specPage) {
53414
+ const shadowDomain = await resolveSpecDomain(ctx.client, input.projectSlug, {
53415
+ id: specPage.id,
53416
+ taskId: specPage.taskId ?? null
53417
+ });
53418
+ await maybeRunShadow({
53419
+ outcome: result.outcome,
53420
+ artifact: specPage.markdownBody,
53421
+ domain: shadowDomain,
53422
+ extraHeads: input.extraHeads,
53423
+ correlationId: `Specs/${input.pageId}`
53424
+ });
53425
+ }
53426
+ } catch {
53427
+ }
53428
+ }
52589
53429
  if (result.outcome === "CONSENSUS_BLOCKED") {
52590
53430
  const clarifications = result.clarifications ?? [];
53431
+ let filedQuestionCount = 0;
52591
53432
  if (clarifications.length > 0) {
52592
53433
  const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
52593
53434
  const taskId = page?.taskId ?? null;
@@ -52612,6 +53453,7 @@ async function run(ctx, input) {
52612
53453
  } catch {
52613
53454
  }
52614
53455
  }
53456
+ filedQuestionCount = clarifications.length;
52615
53457
  } else if (planId) {
52616
53458
  for (const clarification of clarifications) {
52617
53459
  const scopeId = planId;
@@ -52628,13 +53470,18 @@ async function run(ctx, input) {
52628
53470
  } catch {
52629
53471
  }
52630
53472
  }
53473
+ filedQuestionCount = clarifications.length;
52631
53474
  }
52632
53475
  }
52633
- return {
52634
- kind: "failed",
52635
- failureReason: "consensus_blocked",
52636
- error: clarifications.length > 0 ? `Consensus blocked on ${clarifications.length} clarification(s); filed as operator question(s).` : "Consensus blocked: reviewers did not converge within budget."
52637
- };
53476
+ let error51;
53477
+ if (filedQuestionCount > 0) {
53478
+ error51 = `Consensus blocked on ${filedQuestionCount} clarification(s); filed as operator question(s).`;
53479
+ } else if (clarifications.length > 0) {
53480
+ error51 = `Consensus blocked on ${clarifications.length} clarification(s), but the spec page is not linked to a task or plan, so none could be filed as questions.`;
53481
+ } else {
53482
+ error51 = "Consensus blocked: reviewers did not converge within budget.";
53483
+ }
53484
+ return { kind: "failed", failureReason: "consensus_blocked", error: error51, filedQuestionCount };
52638
53485
  }
52639
53486
  switch (result.outcome) {
52640
53487
  case "CONVERGED":
@@ -52670,6 +53517,14 @@ function onComplete2(outcome, input) {
52670
53517
  body: `Consensus CONVERGED for Specs/${input.pageId}; spec_ready set. Ready to create an execution run.`
52671
53518
  };
52672
53519
  }
53520
+ if (outcome.kind === "failed" && (outcome.filedQuestionCount ?? 0) > 0) {
53521
+ const n = outcome.filedQuestionCount ?? 0;
53522
+ return {
53523
+ kind: "info",
53524
+ suppressInbox: true,
53525
+ body: `Consensus blocked on Specs/${input.pageId}; filed ${n} clarifying question${n === 1 ? "" : "s"} \u2014 answer to unblock the run.`
53526
+ };
53527
+ }
52673
53528
  return {
52674
53529
  kind: "handoff",
52675
53530
  body: `Consensus could not complete for Specs/${input.pageId}: ${outcome.error} Needs operator attention.`
@@ -53629,7 +54484,7 @@ var mcpToolDescriptions = {
53629
54484
  [mcpToolNames.appendTaskEvent]: "Append an operator note to a task's activity timeline.",
53630
54485
  [mcpToolNames.createWikiPage]: "Create a markdown Wiki page.",
53631
54486
  [mcpToolNames.deleteTaskDependency]: "Delete a blocked-by relationship.",
53632
- [mcpToolNames.createTask]: "Create a new task in a project. Always starts as status='todo' with a blank assignee.",
54487
+ [mcpToolNames.createTask]: "Create a new task in a project. Always starts as status='todo' with a blank assignee. Pass parentTaskId when spawning a follow-up task to continue work from an existing task \u2014 its operator_seed_request is inherited automatically if you don't supply your own.",
53633
54488
  [mcpToolNames.getProject]: "Fetch a single project summary by slug.",
53634
54489
  [mcpToolNames.getProjectSnapshot]: "Fetch the full project snapshot: GitHub binding, tasks, plans, and wiki pages.",
53635
54490
  [mcpToolNames.getTask]: "Fetch a single task by project slug and task id.",
@@ -54170,7 +55025,24 @@ function createMrrlinTools(options) {
54170
55025
  codexExecutable: process.env.MRRLIN_DIRECTOR_BRIDGE_CODEX_BIN?.trim() || void 0,
54171
55026
  extraHeads
54172
55027
  });
54173
- return await runCodeGate({ projectSlug, baseRef }, deps);
55028
+ const activeCodeRunHead = await resolveActiveRunHead({
55029
+ heads: ["code-reviewer", ...extraHeads ?? []]
55030
+ });
55031
+ if (activeCodeRunHead) deps.runHead = activeCodeRunHead;
55032
+ const codeGateResult = await runCodeGate({ projectSlug, baseRef }, deps);
55033
+ if (process.env[SHADOW_ENV_FLAG]) {
55034
+ try {
55035
+ const diff = await readDiff({ cwd: process.cwd(), baseRef, run: defaultRunCmd });
55036
+ await maybeRunShadow({
55037
+ outcome: codeGateResult.outcome,
55038
+ artifact: diff,
55039
+ heads: ["code-reviewer", ...extraHeads ?? []],
55040
+ correlationId: `code:${baseRef}`
55041
+ });
55042
+ } catch {
55043
+ }
55044
+ }
55045
+ return codeGateResult;
54174
55046
  }
54175
55047
  ),
54176
55048
  [mcpToolNames.listExecutionRuns]: makeTool(
@@ -54421,7 +55293,7 @@ function createMrrlinTools(options) {
54421
55293
  }
54422
55294
  let remoteUrl;
54423
55295
  try {
54424
- remoteUrl = (0, import_node_child_process8.execFileSync)("git", ["-C", resolvedPath, "remote", "get-url", "origin"], {
55296
+ remoteUrl = (0, import_node_child_process11.execFileSync)("git", ["-C", resolvedPath, "remote", "get-url", "origin"], {
54425
55297
  encoding: "utf8",
54426
55298
  stdio: ["ignore", "pipe", "ignore"],
54427
55299
  timeout: 15e3,
@@ -54773,7 +55645,7 @@ async function pathExists3(target) {
54773
55645
  return false;
54774
55646
  }
54775
55647
  }
54776
- var MRRLIN_BLOCK_RE = /(^|\n)\[mcp_servers\.mrrlin(?:-browser)?(?:\]|\.[^\]\n]*\])[\s\S]*?(?=\n\[|$)/g;
55648
+ var MRRLIN_BLOCK_RE = /(^|\n)\[mcp_servers\.(?:mrrlin(?:-browser)?|external-agents)(?:\]|\.[^\]\n]*\])[\s\S]*?(?=\n\[|$)/g;
54777
55649
  async function uninstallCodex(options = {}) {
54778
55650
  const codexHome = resolveCodexHome2(options);
54779
55651
  const configPath = import_node_path18.default.join(codexHome, "config.toml");
@@ -54795,7 +55667,7 @@ async function uninstallCodex(options = {}) {
54795
55667
  };
54796
55668
  throw Object.assign(new Error(err.detail), err);
54797
55669
  }
54798
- if (parsed?.mcp_servers?.["mrrlin"] === void 0 && parsed?.mcp_servers?.["mrrlin-browser"] === void 0) {
55670
+ if (parsed?.mcp_servers?.["mrrlin"] === void 0 && parsed?.mcp_servers?.["mrrlin-browser"] === void 0 && parsed?.mcp_servers?.["external-agents"] === void 0) {
54799
55671
  return { action: "noop", configPath: realPath, prompts: await removePrompts() };
54800
55672
  }
54801
55673
  const stripped = existing.replace(MRRLIN_BLOCK_RE, (match) => match.startsWith("\n") ? "\n" : "").replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "");
@@ -54825,13 +55697,13 @@ async function uninstallPrompts(codexHome, names) {
54825
55697
 
54826
55698
  // src/bin.ts
54827
55699
  function pm2Runner(args) {
54828
- const r = (0, import_node_child_process9.spawnSync)("pm2", args, { encoding: "utf8" });
55700
+ const r = (0, import_node_child_process12.spawnSync)("pm2", args, { encoding: "utf8" });
54829
55701
  process.stderr.write(r.stdout ?? "");
54830
55702
  process.stderr.write(r.stderr ?? "");
54831
55703
  return { code: r.status ?? 1, stdout: r.stdout ?? "" };
54832
55704
  }
54833
55705
  function whichBin(bin) {
54834
- const r = (0, import_node_child_process9.spawnSync)("which", [bin], { encoding: "utf8" });
55706
+ const r = (0, import_node_child_process12.spawnSync)("which", [bin], { encoding: "utf8" });
54835
55707
  const out = (r.stdout ?? "").trim();
54836
55708
  return r.status === 0 && out ? out.split("\n")[0] ?? null : null;
54837
55709
  }
@@ -55007,7 +55879,7 @@ async function main() {
55007
55879
  }
55008
55880
  case "browser-serve": {
55009
55881
  const { command: command2, args, env } = browserServeArgs();
55010
- const r = (0, import_node_child_process9.spawnSync)(command2, args, { stdio: "inherit", env: { ...process.env, ...env } });
55882
+ const r = (0, import_node_child_process12.spawnSync)(command2, args, { stdio: "inherit", env: { ...process.env, ...env } });
55011
55883
  process.exitCode = r.status ?? 1;
55012
55884
  return;
55013
55885
  }