@mrrlin-dev/mcp 0.4.5 → 0.4.6

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 +961 -187
  2. package/package.json +2 -2
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_path25 = __toESM(require("node:path"), 1);
20648
- var import_node_child_process17 = require("node:child_process");
20648
+ var import_node_child_process18 = require("node:child_process");
20649
20649
  var import_node_fs17 = require("node:fs");
20650
20650
  var import_node_os15 = require("node:os");
20651
20651
  var import_node_path26 = __toESM(require("node:path"), 1);
@@ -36801,6 +36801,8 @@ var openApiOperationIds = {
36801
36801
  getTaskState: "getTaskState",
36802
36802
  upsertTaskState: "upsertTaskState",
36803
36803
  retireThread: "retireThread",
36804
+ applyIntentVerdict: "applyIntentVerdict",
36805
+ armIntentCheck: "armIntentCheck",
36804
36806
  // Run-scoped, model-facing half of the resume contract (D8). Distinct from upsertTaskState:
36805
36807
  // that one is the server's own write (whole record, explicit lastRunSeq), this one is the
36806
36808
  // executor reporting narrative only, with the ordering value derived server-side from the run.
@@ -37035,7 +37037,10 @@ var mrrlinAsyncJobSchema = external_exports.object({
37035
37037
  directorSessionId: external_exports.string().nullable().default(null),
37036
37038
  createdAt: external_exports.string(),
37037
37039
  finishedAt: external_exports.string().nullable().default(null),
37038
- updatedAt: external_exports.string()
37040
+ updatedAt: external_exports.string(),
37041
+ // model-tier-planning-and-arbiter task 1.8: the seat minted at claim (or inherited from a
37042
+ // prior attempt on this same lease); NULL only before the first beginAttempt call pins it.
37043
+ agentSeat: external_exports.enum(["codex", "claude"]).nullable().default(null)
37039
37044
  }).strict();
37040
37045
  var asyncJobResponseSchema = external_exports.object({ data: mrrlinAsyncJobSchema });
37041
37046
  var asyncJobMaybeResponseSchema = external_exports.object({ data: mrrlinAsyncJobSchema.nullable() });
@@ -37052,7 +37057,10 @@ var mrrlinAsyncJobTouchSchema = external_exports.object({
37052
37057
  });
37053
37058
  var mrrlinAsyncJobBeginAttemptSchema = external_exports.object({
37054
37059
  leaseId: external_exports.string().min(1),
37055
- requestedHash: external_exports.string().min(1)
37060
+ requestedHash: external_exports.string().min(1),
37061
+ // task 1.8: the caller has already resolved (inherited or minted) a seat for this job by the
37062
+ // time it begins an attempt — required, not optional, so persistence can never be skipped.
37063
+ agentSeat: external_exports.enum(["codex", "claude"])
37056
37064
  });
37057
37065
  var mrrlinAsyncJobTerminalizeSchema = external_exports.object({
37058
37066
  leaseId: external_exports.string().min(1),
@@ -37338,6 +37346,7 @@ var mrrlinStopReasonSchema = external_exports.enum([
37338
37346
  "spec_draft_failed",
37339
37347
  "spec_not_specs_folder",
37340
37348
  "awaiting_clarification",
37349
+ "intent_unconfirmed",
37341
37350
  "consensus_blocked",
37342
37351
  "consensus_run_failed",
37343
37352
  "consensus_disabled_spec_not_ready",
@@ -37379,6 +37388,11 @@ var mrrlinTaskSchema = external_exports.object({
37379
37388
  followUpAt: external_exports.string().datetime().nullable(),
37380
37389
  id: mrrlinTaskIdSchema,
37381
37390
  impact: mrrlinTaskImpactSchema,
37391
+ // early-intent-confirmation: written by the intent-verdict route on either verdict; the
37392
+ // restatement the judge showed the operator, and when the task was last checked. Output-optional
37393
+ // so existing task fixtures need no churn; DB rows always resolve both to `string | null`.
37394
+ intentCheckedAt: external_exports.string().datetime().nullable().optional(),
37395
+ intentRestatement: external_exports.string().nullable().optional(),
37382
37396
  latestJudgement: external_exports.string().nullable().default(null),
37383
37397
  latestJudgementAt: external_exports.string().datetime().nullable().default(null),
37384
37398
  latestJudgementKind: external_exports.enum(["autonomy", "handoff"]).nullable().default(null),
@@ -37413,6 +37427,12 @@ var mrrlinTaskSchema = external_exports.object({
37413
37427
  updatedAt: external_exports.string().datetime().optional(),
37414
37428
  verificationDueAt: external_exports.string().datetime().nullable()
37415
37429
  });
37430
+ var mrrlinTaskSummarySchema = mrrlinTaskSchema.omit({
37431
+ notes: true,
37432
+ operatorSeedRequest: true,
37433
+ intentRestatement: true,
37434
+ latestJudgement: true
37435
+ });
37416
37436
  var mrrlinPlanSchema = external_exports.object({
37417
37437
  closedTaskCount: external_exports.number().int().nonnegative(),
37418
37438
  // ADR 0020 Phase 2 (decision 7): plan-scoped specs resolve their consensus domain through
@@ -37539,14 +37559,19 @@ var mrrlinProjectSummarySchema = external_exports.object({
37539
37559
  slug: mrrlinProjectSlugSchema,
37540
37560
  // ADR 0020: default consensus domain for this project's plan/project-scoped specs. Output-optional
37541
37561
  // (DB column is NOT NULL default 'code') so existing project fixtures/upsert callers need no churn.
37542
- defaultDomain: mrrlinTaskDomainSchema.optional()
37562
+ defaultDomain: mrrlinTaskDomainSchema.optional(),
37563
+ // model-tier-planning-and-arbiter D9: per-project judge switch. NULL/true means the judge runs
37564
+ // as today; false means intent-check.ts skips it entirely. Output-optional for the same reason
37565
+ // defaultDomain is: existing project fixtures/upsert callers need no churn.
37566
+ intentJudgeEnabled: external_exports.boolean().optional()
37543
37567
  });
37544
37568
  var mrrlinProjectUpsertSchema = mrrlinProjectSummarySchema.omit({
37545
37569
  onboardingKickoffState: true,
37546
37570
  slug: true
37547
37571
  });
37548
37572
  var mrrlinProjectRenameSchema = external_exports.object({
37549
- displayName: external_exports.string().trim().min(1).max(120)
37573
+ displayName: external_exports.string().trim().min(1).max(120).optional(),
37574
+ intentJudgeEnabled: external_exports.boolean().optional()
37550
37575
  });
37551
37576
  var ONBOARDING_KICKOFF_TRANSITIONS = [
37552
37577
  { from: null, to: "pending" },
@@ -37601,7 +37626,7 @@ var mrrlinTaskCreateShapeSchema = external_exports.object({
37601
37626
  category: taxonomyCategorySchema,
37602
37627
  dueDate: external_exports.string().date().nullable().optional(),
37603
37628
  followUpAt: external_exports.string().datetime().nullable().optional(),
37604
- id: mrrlinTaskIdSchema,
37629
+ id: mrrlinTaskIdSchema.optional(),
37605
37630
  impact: mrrlinTaskImpactSchema.default("medium"),
37606
37631
  notes: external_exports.string().nullable().optional(),
37607
37632
  parentTaskId: mrrlinTaskIdSchema.nullable().optional(),
@@ -37721,6 +37746,7 @@ var mrrlinTaskListFiltersSchema = external_exports.object({
37721
37746
  subcategory: taxonomySubcategorySchema.optional(),
37722
37747
  type: mrrlinTaskTypeSchema.optional()
37723
37748
  });
37749
+ var mrrlinTaskListViewSchema = external_exports.enum(["summary"]);
37724
37750
  var apiErrorSchema = external_exports.object({
37725
37751
  code: external_exports.string().min(1),
37726
37752
  message: external_exports.string().min(1)
@@ -37950,6 +37976,9 @@ var taskResponseSchema = external_exports.object({
37950
37976
  var taskListResponseSchema = external_exports.object({
37951
37977
  data: external_exports.array(mrrlinTaskSchema)
37952
37978
  });
37979
+ var taskSummaryListResponseSchema = external_exports.object({
37980
+ data: external_exports.array(mrrlinTaskSummarySchema)
37981
+ });
37953
37982
  var planResponseSchema = external_exports.object({
37954
37983
  data: mrrlinPlanSchema
37955
37984
  });
@@ -37995,6 +38024,7 @@ var mrrlinRunPrerequisiteSchema = external_exports.object({
37995
38024
  });
37996
38025
  var mrrlinExecutionRunSchema = external_exports.object({
37997
38026
  actor: external_exports.string().min(1),
38027
+ agentSeat: external_exports.enum(["codex", "claude"]).nullable(),
37998
38028
  baseSha: external_exports.string().min(1).nullable(),
37999
38029
  branch: external_exports.string().min(1).nullable(),
38000
38030
  blockingReason: mrrlinRunBlockingReasonCodeSchema.nullable(),
@@ -38092,6 +38122,7 @@ var mrrlinExecutionRunCreateSchema = external_exports.object({
38092
38122
  worktreeId: external_exports.string().min(1).optional()
38093
38123
  });
38094
38124
  var mrrlinExecutionRunUpdateSchema = external_exports.object({
38125
+ agentSeat: external_exports.enum(["codex", "claude"]).nullable().optional(),
38095
38126
  baseSha: external_exports.string().min(1).nullable().optional(),
38096
38127
  branch: external_exports.string().min(1).nullable().optional(),
38097
38128
  blockingReason: mrrlinRunBlockingReasonCodeSchema.nullable().optional(),
@@ -38635,7 +38666,8 @@ var mrrlinInboxItemSchema = external_exports.object({
38635
38666
  readAt: external_exports.string().datetime().nullable(),
38636
38667
  reason: external_exports.string().nullable().optional(),
38637
38668
  status: mrrlinInboxItemStatusSchema,
38638
- taskId: mrrlinTaskIdSchema.nullable()
38669
+ taskId: mrrlinTaskIdSchema.nullable(),
38670
+ taskTitle: external_exports.string().nullable()
38639
38671
  }).superRefine((value, context) => {
38640
38672
  if (value.kind !== "question" && value.questionOptions !== null) {
38641
38673
  context.addIssue({
@@ -38686,6 +38718,26 @@ var mrrlinInboxItemListFiltersSchema = external_exports.object({
38686
38718
  status: mrrlinInboxItemStatusSchema.optional(),
38687
38719
  taskId: mrrlinTaskIdSchema.optional()
38688
38720
  });
38721
+ var mrrlinIntentVerdictSchema = external_exports.object({
38722
+ verdict: external_exports.enum(["CLEAR", "UNCLEAR"]),
38723
+ restatement: external_exports.string(),
38724
+ questions: external_exports.array(external_exports.string()).max(3)
38725
+ }).superRefine((value, context) => {
38726
+ if (value.verdict === "CLEAR" && value.questions.length !== 0) {
38727
+ context.addIssue({
38728
+ code: external_exports.ZodIssueCode.custom,
38729
+ message: "a CLEAR verdict must carry zero questions",
38730
+ path: ["questions"]
38731
+ });
38732
+ }
38733
+ if (value.verdict === "UNCLEAR" && value.questions.length === 0) {
38734
+ context.addIssue({
38735
+ code: external_exports.ZodIssueCode.custom,
38736
+ message: "an UNCLEAR verdict must carry at least one question",
38737
+ path: ["questions"]
38738
+ });
38739
+ }
38740
+ });
38689
38741
  var inboxItemReadResponseSchema = external_exports.object({
38690
38742
  data: external_exports.object({
38691
38743
  readAt: external_exports.string().datetime()
@@ -39559,9 +39611,12 @@ ${input.prompt}` : input.prompt;
39559
39611
  "--sandbox",
39560
39612
  sandbox,
39561
39613
  "--skip-git-repo-check",
39562
- "--ignore-user-config",
39563
- fullPrompt
39614
+ "--ignore-user-config"
39564
39615
  ];
39616
+ if (input.model) {
39617
+ args.push("-m", input.model);
39618
+ }
39619
+ args.push(fullPrompt);
39565
39620
  return new Promise((resolve) => {
39566
39621
  let settled = false;
39567
39622
  const timerRef = { id: void 0 };
@@ -39636,6 +39691,9 @@ function runClaudeExec(input, deps) {
39636
39691
  if (trimmedInstructions) {
39637
39692
  args.push("--append-system-prompt", trimmedInstructions);
39638
39693
  }
39694
+ if (input.model) {
39695
+ args.push("--model", input.model);
39696
+ }
39639
39697
  args.push(input.prompt);
39640
39698
  return new Promise((resolve) => {
39641
39699
  let settled = false;
@@ -39733,6 +39791,98 @@ function runAgentExec(input, deps, persistedProvider) {
39733
39791
  throw new Error(`not-yet-wired: ${provider.id}`);
39734
39792
  }
39735
39793
 
39794
+ // ../../packages/agent-client/dist/resolved-seat.js
39795
+ var CODEX_STRONG_MODEL_CONSTANT = "gpt-5.5";
39796
+ var CLAUDE_STRONG_MODEL_CONSTANT = "opus";
39797
+ var CODEX_WEAK_MODEL_CONSTANT = "codex-gpt-5.4-mini";
39798
+ var CLAUDE_WEAK_MODEL_CONSTANT = "haiku";
39799
+ var CODEX_STRONG_MODEL_ENV = "MRRLIN_CODEX_STRONG_MODEL";
39800
+ var CLAUDE_STRONG_MODEL_ENV = "MRRLIN_CLAUDE_STRONG_MODEL";
39801
+ function logFallback(providerId, tier, source, value) {
39802
+ console.error(`[resolved-seat] provider=${providerId} ${tier}=${value} (source: ${source})`);
39803
+ }
39804
+ function pickStrongModelFromList(models) {
39805
+ const defaultModel = models.find((m) => m.isDefault === true);
39806
+ if (defaultModel)
39807
+ return defaultModel.id;
39808
+ const nonHidden = models.find((m) => m.hidden !== true);
39809
+ if (nonHidden)
39810
+ return nonHidden.id;
39811
+ return models[0]?.id ?? null;
39812
+ }
39813
+ function pickWeakModelFromList(models, strongModel) {
39814
+ const miniModel = models.find((m) => {
39815
+ const id = m.id.toLowerCase();
39816
+ return id.includes("mini") || id.includes("weak") || id.includes("small");
39817
+ });
39818
+ if (miniModel && miniModel.id !== strongModel)
39819
+ return miniModel.id;
39820
+ return null;
39821
+ }
39822
+ async function mintResolvedSeat(provider, opts) {
39823
+ const capabilities = provider.capabilities();
39824
+ const providerId = provider.id;
39825
+ let strongModel;
39826
+ let weakModel;
39827
+ if (opts?.handle && capabilities.listsModels) {
39828
+ try {
39829
+ const models = await opts.handle.listModels();
39830
+ const picked = pickStrongModelFromList(models);
39831
+ if (picked) {
39832
+ strongModel = picked;
39833
+ } else {
39834
+ throw new Error("empty model list");
39835
+ }
39836
+ } catch {
39837
+ const envValue = process.env[providerId === "codex" ? CODEX_STRONG_MODEL_ENV : CLAUDE_STRONG_MODEL_ENV]?.trim();
39838
+ if (envValue) {
39839
+ strongModel = envValue;
39840
+ logFallback(providerId, "strong", "env", strongModel);
39841
+ } else {
39842
+ strongModel = providerId === "codex" ? CODEX_STRONG_MODEL_CONSTANT : CLAUDE_STRONG_MODEL_CONSTANT;
39843
+ logFallback(providerId, "strong", "constant", strongModel);
39844
+ }
39845
+ }
39846
+ } else {
39847
+ const envValue = process.env[providerId === "codex" ? CODEX_STRONG_MODEL_ENV : CLAUDE_STRONG_MODEL_ENV]?.trim();
39848
+ if (envValue) {
39849
+ strongModel = envValue;
39850
+ logFallback(providerId, "strong", "env", strongModel);
39851
+ } else {
39852
+ strongModel = providerId === "codex" ? CODEX_STRONG_MODEL_CONSTANT : CLAUDE_STRONG_MODEL_CONSTANT;
39853
+ logFallback(providerId, "strong", "constant", strongModel);
39854
+ }
39855
+ }
39856
+ if (providerId === "claude") {
39857
+ weakModel = CLAUDE_WEAK_MODEL_CONSTANT;
39858
+ logFallback(providerId, "weak", "constant", weakModel);
39859
+ } else {
39860
+ if (opts?.handle && capabilities.listsModels) {
39861
+ try {
39862
+ const models = await opts.handle.listModels();
39863
+ const picked = pickWeakModelFromList(models, strongModel);
39864
+ if (picked) {
39865
+ weakModel = picked;
39866
+ } else {
39867
+ weakModel = strongModel;
39868
+ logFallback(providerId, "weak", "degenerate (weak=strong)", weakModel);
39869
+ }
39870
+ } catch {
39871
+ weakModel = CODEX_WEAK_MODEL_CONSTANT;
39872
+ logFallback(providerId, "weak", "constant", weakModel);
39873
+ }
39874
+ } else {
39875
+ weakModel = CODEX_WEAK_MODEL_CONSTANT;
39876
+ logFallback(providerId, "weak", "constant", weakModel);
39877
+ }
39878
+ }
39879
+ return {
39880
+ id: providerId,
39881
+ capabilities,
39882
+ models: { strong: strongModel, weak: weakModel }
39883
+ };
39884
+ }
39885
+
39736
39886
  // ../../packages/client/dist/agent-token-provider.js
39737
39887
  var AgentTokenProviderError = class extends Error {
39738
39888
  code;
@@ -39806,6 +39956,25 @@ function appendQuery(url2, params) {
39806
39956
  const qs = search.toString();
39807
39957
  return qs.length > 0 ? `${url2}?${qs}` : url2;
39808
39958
  }
39959
+ function projectPath(slug) {
39960
+ return `/projects/${encodeURIComponent(slug)}`;
39961
+ }
39962
+ function buildTaskListPath(projectSlug, filters, view) {
39963
+ return appendQuery(`${projectPath(projectSlug)}/tasks`, {
39964
+ archived: filters.archived,
39965
+ assignee: filters.assignee,
39966
+ autonomyLevel: filters.autonomyLevel,
39967
+ blockedBy: filters.blockedBy,
39968
+ category: filters.category,
39969
+ dueDate: filters.dueDate,
39970
+ impact: filters.impact,
39971
+ q: filters.q,
39972
+ status: filters.status,
39973
+ subcategory: filters.subcategory,
39974
+ type: filters.type,
39975
+ view
39976
+ });
39977
+ }
39809
39978
  var MrrlinApiError = class extends Error {
39810
39979
  code;
39811
39980
  method;
@@ -39868,9 +40037,6 @@ function createMrrlinClient(config2) {
39868
40037
  }
39869
40038
  return response.json();
39870
40039
  }
39871
- function projectPath(slug) {
39872
- return `/projects/${encodeURIComponent(slug)}`;
39873
- }
39874
40040
  return {
39875
40041
  async listGithubInstallations() {
39876
40042
  const body = await request(`/github/installations`);
@@ -40117,23 +40283,23 @@ function createMrrlinClient(config2) {
40117
40283
  const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/events`);
40118
40284
  return taskEventListResponseSchema.parse(body).data;
40119
40285
  },
40120
- async listTasks(projectSlug, filters = {}) {
40121
- const path21 = appendQuery(`${projectPath(projectSlug)}/tasks`, {
40122
- archived: filters.archived,
40123
- assignee: filters.assignee,
40124
- autonomyLevel: filters.autonomyLevel,
40125
- blockedBy: filters.blockedBy,
40126
- category: filters.category,
40127
- dueDate: filters.dueDate,
40128
- impact: filters.impact,
40129
- q: filters.q,
40130
- status: filters.status,
40131
- subcategory: filters.subcategory,
40132
- type: filters.type
40133
- });
40286
+ // An object-literal method can't satisfy two overloads with distinct return types the way a
40287
+ // real function overload's implementation signature can (verified: a union return type is
40288
+ // rejected against the first, MrrlinTask[]-only overload) -- the `any` here is contained to
40289
+ // this one implementation signature; MrrlinClient's declared overloads keep callers precise.
40290
+ async listTasks(projectSlug, filters = {}, options) {
40291
+ const path21 = buildTaskListPath(projectSlug, filters, options?.view);
40134
40292
  const body = await request(path21);
40293
+ if (options?.view === "summary") {
40294
+ return taskSummaryListResponseSchema.parse(body).data;
40295
+ }
40135
40296
  return taskListResponseSchema.parse(body).data;
40136
40297
  },
40298
+ async listTaskSummaries(projectSlug, filters = {}) {
40299
+ const path21 = buildTaskListPath(projectSlug, filters, "summary");
40300
+ const body = await request(path21);
40301
+ return taskSummaryListResponseSchema.parse(body).data;
40302
+ },
40137
40303
  async listTaxonomy(projectSlug) {
40138
40304
  const body = await request(`${projectPath(projectSlug)}/taxonomy`);
40139
40305
  return taxonomyListResponseSchema.parse(body).data;
@@ -40311,10 +40477,10 @@ function createMrrlinClient(config2) {
40311
40477
  throw error51;
40312
40478
  }
40313
40479
  },
40314
- async beginAsyncJobAttempt(projectSlug, jobId, leaseId, requestedHash) {
40480
+ async beginAsyncJobAttempt(projectSlug, jobId, leaseId, requestedHash, agentSeat) {
40315
40481
  try {
40316
40482
  const body = await request(`${projectPath(projectSlug)}/async-jobs/${encodeURIComponent(jobId)}/begin-attempt`, {
40317
- body: JSON.stringify({ leaseId, requestedHash }),
40483
+ body: JSON.stringify({ leaseId, requestedHash, agentSeat }),
40318
40484
  headers: { "content-type": "application/json" },
40319
40485
  method: "POST"
40320
40486
  });
@@ -40492,6 +40658,18 @@ function createMrrlinClient(config2) {
40492
40658
  });
40493
40659
  return retireThreadResponseSchema.parse(body).data.retiredThreadId;
40494
40660
  },
40661
+ async armIntentCheck(projectSlug, taskId) {
40662
+ const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/intent-check-arm`, { method: "POST" });
40663
+ return taskResponseSchema.parse(body).data;
40664
+ },
40665
+ async applyIntentVerdict(projectSlug, taskId, verdict) {
40666
+ const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/intent-verdict`, {
40667
+ body: JSON.stringify(mrrlinIntentVerdictSchema.parse(verdict)),
40668
+ headers: { "content-type": "application/json" },
40669
+ method: "POST"
40670
+ });
40671
+ return taskResponseSchema.parse(body).data;
40672
+ },
40495
40673
  async reportTaskState(projectSlug, runId, report) {
40496
40674
  const body = await request(`${projectPath(projectSlug)}/execution-runs/${encodeURIComponent(runId)}/task-state`, {
40497
40675
  body: JSON.stringify(mrrlinTaskStateReportSchema.parse(report)),
@@ -41222,7 +41400,7 @@ Do NOT derail the current task - if the operator has an in-flight question, answ
41222
41400
  }
41223
41401
 
41224
41402
  // src/_generated/version.ts
41225
- var PKG_VERSION = "0.4.5";
41403
+ var PKG_VERSION = "0.4.6";
41226
41404
 
41227
41405
  // src/api-base-url-migrate.ts
41228
41406
  var import_node_fs3 = __toESM(require("node:fs"), 1);
@@ -42188,18 +42366,40 @@ function parseArbiterDecisions(raw) {
42188
42366
  }
42189
42367
 
42190
42368
  // src/consensus/arbiter.ts
42191
- function buildArbiterPrompt(issues) {
42192
- const list = issues.map((i) => `- [${i.id}] (${i.category}) ${i.text}`).join("\n");
42193
- return `Adjudicate each issue below.
42194
-
42195
- ISSUES:
42196
- ${list}`;
42369
+ function buildArbiterPrompt(issues, artifact, panelComposition) {
42370
+ const compositionLine = `Panel composition \u2013 requested: ${panelComposition.requested}, filled: ${panelComposition.filled}, underpowered: ${panelComposition.underpowered}`;
42371
+ const issueList = issues.map((i) => `- [${i.id}] (${i.category}) ${i.text}`).join("\n");
42372
+ const instruction = `You may add a line starting with "PANEL_UNDERPOWERED:" followed by a one\u2011sentence citation of what the panel likely missed given its size. If you add the line, provide a non\u2011empty citation; otherwise omit the line entirely.`;
42373
+ return [
42374
+ instruction,
42375
+ "",
42376
+ "Artifact:",
42377
+ artifact,
42378
+ "",
42379
+ compositionLine,
42380
+ "",
42381
+ "ISSUES:",
42382
+ issueList
42383
+ ].join("\n");
42384
+ }
42385
+ function parsePanelDeclaration(raw) {
42386
+ const lines2 = raw.split(/\r?\n/);
42387
+ for (const line of lines2) {
42388
+ const m = line.match(/^\s*[-*]?\s*PANEL_UNDERPOWERED:\s*(.*)$/i);
42389
+ if (m) {
42390
+ const citation = (m[1] ?? "").trim();
42391
+ if (citation.length === 0) {
42392
+ return { kind: "underpowered_rejected_uncited" };
42393
+ }
42394
+ return { kind: "underpowered", citation };
42395
+ }
42396
+ }
42397
+ return { kind: "not_declared" };
42197
42398
  }
42198
- async function adjudicate(issues, deps) {
42199
- if (issues.length === 0) return { adjudicatedVerdict: "APPROVE", accepted: [], dismissed: [] };
42399
+ async function adjudicate(issues, artifact, panelComposition, deps) {
42200
42400
  const byId = new Map(issues.map((i) => [i.id, i]));
42201
42401
  for (let attempt = 0; attempt < 2; attempt++) {
42202
- const res = await deps.runArbiter(buildArbiterPrompt(issues));
42402
+ const res = await deps.runArbiter(buildArbiterPrompt(issues, artifact, panelComposition));
42203
42403
  if (!res.ok) continue;
42204
42404
  const { decisions } = parseArbiterDecisions(res.text);
42205
42405
  const seen = new Set(decisions.map((d) => d.id));
@@ -42214,12 +42414,28 @@ async function adjudicate(issues, deps) {
42214
42414
  accepted.push(i);
42215
42415
  continue;
42216
42416
  }
42217
- if (d.decision === "accept") accepted.push(i);
42218
- else dismissed.push({ id: i.id, decision: d.decision, reason: d.reason });
42417
+ if (d.decision === "accept") {
42418
+ accepted.push(i);
42419
+ } else {
42420
+ dismissed.push({ id: i.id, decision: d.decision, reason: d.reason });
42421
+ }
42219
42422
  }
42220
- return { adjudicatedVerdict: accepted.length === 0 ? "APPROVE" : "REQUEST_CHANGES", accepted, dismissed };
42423
+ const panelDeclaration = parsePanelDeclaration(res.text);
42424
+ return {
42425
+ adjudicatedVerdict: accepted.length === 0 ? "APPROVE" : "REQUEST_CHANGES",
42426
+ accepted,
42427
+ dismissed,
42428
+ arbiterInfo: { unavailable: null },
42429
+ panelDeclaration
42430
+ };
42221
42431
  }
42222
- return { adjudicatedVerdict: "REQUEST_CHANGES", accepted: [...byId.values()], dismissed: [] };
42432
+ return {
42433
+ adjudicatedVerdict: "REQUEST_CHANGES",
42434
+ accepted: [...byId.values()],
42435
+ dismissed: [],
42436
+ arbiterInfo: { unavailable: { reason: "retry-exhausted" } },
42437
+ panelDeclaration: { kind: "not_declared" }
42438
+ };
42223
42439
  }
42224
42440
 
42225
42441
  // src/consensus/semaphore.ts
@@ -42277,6 +42493,11 @@ function diagnoseStall(history, configuredRounds) {
42277
42493
  }
42278
42494
 
42279
42495
  // src/consensus/loop.ts
42496
+ function sufficiencyRank(v) {
42497
+ if (v.verdict === "proceeded_underpowered") return 2;
42498
+ if (v.verdict === "retried") return 1;
42499
+ return 0;
42500
+ }
42280
42501
  async function runConsensus(input) {
42281
42502
  const sem = createBoundedSemaphore(input.concurrency ?? input.heads.length);
42282
42503
  const history = [];
@@ -42286,9 +42507,24 @@ async function runConsensus(input) {
42286
42507
  let effectiveRounds = input.rounds;
42287
42508
  let escalated = false;
42288
42509
  let decision;
42510
+ let composition = input.panelComposition;
42511
+ let currentRunHead = input.runHead;
42512
+ let repickAttempted = false;
42513
+ let latestArbiterInfo = null;
42514
+ let worstSufficiency = { verdict: "sufficient" };
42289
42515
  for (let round = 1; round <= effectiveRounds; round++) {
42290
42516
  if (calls + input.heads.length > input.maxHeadCalls) {
42291
- return { outcome: "CONSENSUS_BLOCKED", rounds: round - 1, finalArtifact: artifact, history, unresolved: history[history.length - 1]?.accepted ?? [], ...decision ? { escalation: decision } : {} };
42517
+ return {
42518
+ outcome: "CONSENSUS_BLOCKED",
42519
+ rounds: round - 1,
42520
+ finalArtifact: artifact,
42521
+ history,
42522
+ unresolved: history[history.length - 1]?.accepted ?? [],
42523
+ arbiterInfo: null,
42524
+ panelComposition: composition,
42525
+ sufficiencyVerdict: worstSufficiency,
42526
+ ...decision ? { escalation: decision } : {}
42527
+ };
42292
42528
  }
42293
42529
  const dismissedBlock = dismissedFwd.length ? `PREVIOUSLY DISMISSED (do not re-raise unless new information):
42294
42530
  ${dismissedFwd.slice(-20).join("\n")}` : "";
@@ -42297,7 +42533,7 @@ ${dismissedFwd.slice(-20).join("\n")}` : "";
42297
42533
  await sem.acquire();
42298
42534
  try {
42299
42535
  calls++;
42300
- const res = await input.runHead(persona, artifact, dismissedBlock);
42536
+ const res = await currentRunHead(persona, artifact, dismissedBlock);
42301
42537
  return { idx, res };
42302
42538
  } catch (e) {
42303
42539
  return { idx, res: { ok: false, error: String(e) } };
@@ -42310,7 +42546,16 @@ ${dismissedFwd.slice(-20).join("\n")}` : "";
42310
42546
  (r) => r.res.ok
42311
42547
  );
42312
42548
  if (responding.length === 0) {
42313
- return { outcome: "CONSENSUS_UNAVAILABLE", rounds: round, finalArtifact: artifact, history, unresolved: [] };
42549
+ return {
42550
+ outcome: "CONSENSUS_UNAVAILABLE",
42551
+ rounds: round,
42552
+ finalArtifact: artifact,
42553
+ history,
42554
+ unresolved: [],
42555
+ arbiterInfo: null,
42556
+ panelComposition: composition,
42557
+ sufficiencyVerdict: worstSufficiency
42558
+ };
42314
42559
  }
42315
42560
  const headOutcomes = input.heads.map((persona) => ({
42316
42561
  persona,
@@ -42333,24 +42578,68 @@ ${dismissedFwd.slice(-20).join("\n")}` : "";
42333
42578
  });
42334
42579
  }
42335
42580
  }
42581
+ if (calls + 1 > input.maxHeadCalls) {
42582
+ return {
42583
+ outcome: "CONSENSUS_BLOCKED",
42584
+ rounds: round,
42585
+ finalArtifact: artifact,
42586
+ history,
42587
+ unresolved: history[history.length - 1]?.accepted ?? [],
42588
+ arbiterInfo: { unavailable: { reason: "budget-exhausted" } },
42589
+ panelComposition: composition,
42590
+ sufficiencyVerdict: worstSufficiency,
42591
+ ...decision ? { escalation: decision } : {}
42592
+ };
42593
+ }
42594
+ calls++;
42336
42595
  let adj;
42337
- if (issues.length > 0) {
42338
- if (calls + 1 > input.maxHeadCalls) {
42339
- return { outcome: "CONSENSUS_BLOCKED", rounds: round, finalArtifact: artifact, history, unresolved: history[history.length - 1]?.accepted ?? [], ...decision ? { escalation: decision } : {} };
42340
- }
42341
- calls++;
42342
- try {
42343
- adj = await adjudicate(issues, { runArbiter: input.runArbiter });
42344
- } catch {
42345
- adj = { adjudicatedVerdict: "REQUEST_CHANGES", accepted: issues, dismissed: [] };
42596
+ try {
42597
+ adj = await adjudicate(issues, artifact, composition, { runArbiter: input.runArbiter });
42598
+ } catch (e) {
42599
+ adj = {
42600
+ adjudicatedVerdict: "REQUEST_CHANGES",
42601
+ accepted: issues,
42602
+ dismissed: [],
42603
+ arbiterInfo: { unavailable: { reason: "thrown-exception", detail: String(e) } },
42604
+ panelDeclaration: { kind: "not_declared" }
42605
+ };
42606
+ }
42607
+ latestArbiterInfo = adj.arbiterInfo;
42608
+ let thisRoundSufficiency;
42609
+ if (adj.panelDeclaration.kind === "underpowered") {
42610
+ if (!repickAttempted && input.repickPanel) {
42611
+ repickAttempted = true;
42612
+ const rp = await input.repickPanel();
42613
+ if (rp.ok && rp.composition.filled > composition.filled) {
42614
+ composition = rp.composition;
42615
+ currentRunHead = rp.runHead;
42616
+ thisRoundSufficiency = { verdict: "retried", citation: adj.panelDeclaration.citation };
42617
+ } else {
42618
+ thisRoundSufficiency = { verdict: "proceeded_underpowered", citation: adj.panelDeclaration.citation };
42619
+ }
42620
+ } else {
42621
+ thisRoundSufficiency = { verdict: "proceeded_underpowered", citation: adj.panelDeclaration.citation };
42346
42622
  }
42347
42623
  } else {
42348
- adj = { adjudicatedVerdict: "APPROVE", accepted: [], dismissed: [] };
42624
+ thisRoundSufficiency = { verdict: "sufficient" };
42625
+ }
42626
+ if (sufficiencyRank(thisRoundSufficiency) >= sufficiencyRank(worstSufficiency)) {
42627
+ worstSufficiency = thisRoundSufficiency;
42349
42628
  }
42350
42629
  history.push({ round, accepted: adj.accepted, dismissed: adj.dismissed, headOutcomes });
42351
42630
  adj.dismissed.forEach((d) => dismissedFwd.push(`- ${d.id}: ${d.reason}`));
42352
42631
  if (adj.accepted.length === 0) {
42353
- return { outcome: "CONVERGED", rounds: round, finalArtifact: artifact, history, unresolved: [], ...decision ? { escalation: decision } : {} };
42632
+ return {
42633
+ outcome: "CONVERGED",
42634
+ rounds: round,
42635
+ finalArtifact: artifact,
42636
+ history,
42637
+ unresolved: [],
42638
+ arbiterInfo: latestArbiterInfo,
42639
+ panelComposition: composition,
42640
+ sufficiencyVerdict: worstSufficiency,
42641
+ ...decision ? { escalation: decision } : {}
42642
+ };
42354
42643
  }
42355
42644
  if (round === effectiveRounds) {
42356
42645
  if (!decision) {
@@ -42360,12 +42649,31 @@ ${dismissedFwd.slice(-20).join("\n")}` : "";
42360
42649
  escalated = true;
42361
42650
  effectiveRounds = Math.min(effectiveRounds + decision.grantedRounds, input.rounds * 2);
42362
42651
  } else {
42363
- return { outcome: "CONSENSUS_BLOCKED", rounds: round, finalArtifact: artifact, history, unresolved: history[history.length - 1]?.accepted ?? [], ...decision ? { escalation: decision } : {} };
42652
+ return {
42653
+ outcome: "CONSENSUS_BLOCKED",
42654
+ rounds: round,
42655
+ finalArtifact: artifact,
42656
+ history,
42657
+ unresolved: history[history.length - 1]?.accepted ?? [],
42658
+ arbiterInfo: latestArbiterInfo,
42659
+ panelComposition: composition,
42660
+ sufficiencyVerdict: worstSufficiency,
42661
+ ...decision ? { escalation: decision } : {}
42662
+ };
42364
42663
  }
42365
42664
  }
42366
42665
  artifact = await input.revise(artifact, adj.accepted);
42367
42666
  }
42368
- return { outcome: "CONSENSUS_BLOCKED", rounds: effectiveRounds, finalArtifact: artifact, history, unresolved: history[history.length - 1]?.accepted ?? [] };
42667
+ return {
42668
+ outcome: "CONSENSUS_BLOCKED",
42669
+ rounds: effectiveRounds,
42670
+ finalArtifact: artifact,
42671
+ history,
42672
+ unresolved: history[history.length - 1]?.accepted ?? [],
42673
+ arbiterInfo: latestArbiterInfo,
42674
+ panelComposition: composition,
42675
+ sufficiencyVerdict: worstSufficiency
42676
+ };
42369
42677
  }
42370
42678
 
42371
42679
  // src/consensus/spec-identity.ts
@@ -42525,7 +42833,8 @@ function applyClarificationCap(issues, impact) {
42525
42833
  async function runSpecGate(_target, deps, opts) {
42526
42834
  if (!await deps.consensusEnabled()) return { outcome: "SKIPPED" };
42527
42835
  const intent = await deps.readIntent();
42528
- if (intent.taskId !== null && (intent.seed === null || intent.seed.trim() === "")) {
42836
+ const legacyExempt = intent.specApproved === "skipped_legacy";
42837
+ if (!legacyExempt && intent.taskId !== null && (intent.seed === null || intent.seed.trim() === "")) {
42529
42838
  return {
42530
42839
  outcome: "CONSENSUS_BLOCKED",
42531
42840
  operatorClarifications: [
@@ -42592,6 +42901,7 @@ async function runSpecGate(_target, deps, opts) {
42592
42901
  heads,
42593
42902
  rounds: deps.rounds,
42594
42903
  maxHeadCalls: deps.maxHeadCalls,
42904
+ panelComposition: deps.panelComposition ?? { requested: heads.length, filled: 1, underpowered: heads.length > 1 },
42595
42905
  runHead: deps.runHead,
42596
42906
  runArbiter: deps.runArbiter,
42597
42907
  revise: async (artifact, accepted) => {
@@ -42635,7 +42945,10 @@ async function runSpecGate(_target, deps, opts) {
42635
42945
  whyBlocking: "converged spec content belongs to a different task than the one it gates",
42636
42946
  category: "scope"
42637
42947
  }
42638
- ]
42948
+ ],
42949
+ arbiterInfo: result.arbiterInfo,
42950
+ panelComposition: result.panelComposition,
42951
+ sufficiencyVerdict: result.sufficiencyVerdict
42639
42952
  };
42640
42953
  }
42641
42954
  let keptClarifications = [];
@@ -42657,7 +42970,13 @@ async function runSpecGate(_target, deps, opts) {
42657
42970
  });
42658
42971
  if (result.outcome === "CONVERGED") {
42659
42972
  await deps.promoteSpecReady(currentHash);
42660
- return { outcome: "CONVERGED", headOutcomes: result.history.at(-1)?.headOutcomes ?? [] };
42973
+ return {
42974
+ outcome: "CONVERGED",
42975
+ headOutcomes: result.history.at(-1)?.headOutcomes ?? [],
42976
+ arbiterInfo: result.arbiterInfo,
42977
+ panelComposition: result.panelComposition,
42978
+ sufficiencyVerdict: result.sufficiencyVerdict
42979
+ };
42661
42980
  }
42662
42981
  if (result.outcome === "CONSENSUS_BLOCKED") {
42663
42982
  return {
@@ -42665,10 +42984,18 @@ async function runSpecGate(_target, deps, opts) {
42665
42984
  ...keptClarifications.length > 0 ? { operatorClarifications: keptClarifications } : {},
42666
42985
  ...droppedByCap.length > 0 ? { droppedByCap } : {},
42667
42986
  ...result.escalation ? { escalation: result.escalation } : {},
42668
- unresolved: result.unresolved
42987
+ unresolved: result.unresolved,
42988
+ arbiterInfo: result.arbiterInfo,
42989
+ panelComposition: result.panelComposition,
42990
+ sufficiencyVerdict: result.sufficiencyVerdict
42669
42991
  };
42670
42992
  }
42671
- return { outcome: result.outcome };
42993
+ return {
42994
+ outcome: result.outcome,
42995
+ arbiterInfo: result.arbiterInfo,
42996
+ panelComposition: result.panelComposition,
42997
+ sufficiencyVerdict: result.sufficiencyVerdict
42998
+ };
42672
42999
  }
42673
43000
 
42674
43001
  // src/consensus/resolve-gated-task.ts
@@ -43027,6 +43354,15 @@ ${adjudicated.map((q) => `- Q: ${q.body}
43027
43354
  ` : "";
43028
43355
  return `${identity}${intent}${impactLine}${adjudicatedBlock}`;
43029
43356
  }
43357
+ function selectAdjudicated(items) {
43358
+ return items.filter(
43359
+ (item) => item.origin === "gate" && (item.status === "acknowledged" || item.status === "rejected") && item.decidedBy !== "gate"
43360
+ ).map((item) => ({
43361
+ body: item.body,
43362
+ answer: item.answer,
43363
+ status: item.status
43364
+ }));
43365
+ }
43030
43366
  function reviserIdentityInstructions(taskId, title) {
43031
43367
  const lock = taskId ? `This spec gates task ${taskId}${title ? ` ("${title}")` : ""}. You MUST NOT change the spec's task identity or heading, retarget it to a different task id, or invert its direction (e.g. live-submission vs preparation-only). Preserve the leading \`task: ${taskId}\` frontmatter EXACTLY; never change it to another task id. Address ONLY the accepted issues.
43032
43368
 
@@ -43034,7 +43370,7 @@ function reviserIdentityInstructions(taskId, title) {
43034
43370
  return `${lock}You revise a spec to address accepted issues. Return ONLY the full revised markdown, no commentary.`;
43035
43371
  }
43036
43372
  function buildSpecGateDeps(args) {
43037
- const { client, projectSlug, specPageId, cwd, codexExecutable, extraHeadCount = 0 } = args;
43373
+ const { client, projectSlug, specPageId, cwd, codexExecutable, extraHeadCount = 0, seat } = args;
43038
43374
  const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
43039
43375
  const maxHeadCalls = 2 * rounds * (4 + extraHeadCount + 1);
43040
43376
  const timeoutMs = Number(process.env["MRRLIN_CONSENSUS_HEAD_TIMEOUT_MS"] ?? 15e4);
@@ -43072,13 +43408,7 @@ function buildSpecGateDeps(args) {
43072
43408
  }
43073
43409
  }
43074
43410
  }
43075
- const adjudicated = taskInboxItems.filter(
43076
- (item) => item.origin === "gate" && (item.status === "acknowledged" || item.status === "rejected") && item.decidedBy !== "gate"
43077
- ).map((item) => ({
43078
- body: item.body,
43079
- answer: item.answer,
43080
- status: item.status
43081
- }));
43411
+ const adjudicated = selectAdjudicated(taskInboxItems);
43082
43412
  return {
43083
43413
  taskId,
43084
43414
  seed: resolvedSeed,
@@ -43086,7 +43416,8 @@ function buildSpecGateDeps(args) {
43086
43416
  domain: task.domain,
43087
43417
  notes,
43088
43418
  impact: task.impact,
43089
- adjudicated
43419
+ adjudicated,
43420
+ specApproved: task.specApproved ?? null
43090
43421
  };
43091
43422
  })();
43092
43423
  }
@@ -43138,7 +43469,10 @@ ${artifact}`,
43138
43469
  cwd,
43139
43470
  timeoutMs,
43140
43471
  sandbox: "read-only",
43141
- codexExecutable
43472
+ codexExecutable,
43473
+ // model-tier-planning-and-arbiter task 2.5 (D1): the arbiter is pinned to the seat's
43474
+ // strongest model.
43475
+ model: seat?.models.strong
43142
43476
  }
43143
43477
  );
43144
43478
  },
@@ -43230,7 +43564,7 @@ ${artifact}`,
43230
43564
  };
43231
43565
  }
43232
43566
  function buildCodeGateDeps(args) {
43233
- const { client, projectSlug, baseRef, cwd, codexExecutable, extraHeads } = args;
43567
+ const { client, projectSlug, baseRef, cwd, codexExecutable, extraHeads, seat } = args;
43234
43568
  const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
43235
43569
  const heads = ["code-reviewer", ...extraHeads ?? []];
43236
43570
  const maxHeadCalls = 2 * rounds * (heads.length + 1);
@@ -43274,7 +43608,10 @@ ${artifact}`,
43274
43608
  cwd,
43275
43609
  timeoutMs: headTimeoutMs,
43276
43610
  sandbox: "read-only",
43277
- codexExecutable
43611
+ codexExecutable,
43612
+ // model-tier-planning-and-arbiter task 2.5 (D1): the arbiter is pinned to the seat's
43613
+ // strongest model.
43614
+ model: seat?.models.strong
43278
43615
  });
43279
43616
  },
43280
43617
  async runReviser(artifact, accepted) {
@@ -43311,7 +43648,7 @@ function renderCriteriaBlock(criteria) {
43311
43648
  return criteria.map((c, i) => `${i + 1}. [ ] ${c}`).join("\n");
43312
43649
  }
43313
43650
  async function buildDeliverableGateDeps(args) {
43314
- const { client, projectSlug, deliverablePageId, cwd, codexExecutable, runId } = args;
43651
+ const { client, projectSlug, deliverablePageId, cwd, codexExecutable, runId, seat } = args;
43315
43652
  const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 5);
43316
43653
  const timeoutMs = Number(process.env["MRRLIN_CONSENSUS_HEAD_TIMEOUT_MS"] ?? 15e4);
43317
43654
  const page = await client.getWikiPage(projectSlug, deliverablePageId);
@@ -43378,7 +43715,10 @@ ${persona}`,
43378
43715
  cwd,
43379
43716
  timeoutMs,
43380
43717
  sandbox: "read-only",
43381
- codexExecutable
43718
+ codexExecutable,
43719
+ // model-tier-planning-and-arbiter task 2.5 (D1): the arbiter is pinned to the seat's
43720
+ // strongest model.
43721
+ model: seat?.models.strong
43382
43722
  });
43383
43723
  },
43384
43724
  async runReviser(artifact, accepted) {
@@ -43831,7 +44171,11 @@ async function runDraftSpec(ctx, input, deps) {
43831
44171
  cwd: ctx.cwd,
43832
44172
  timeoutMs: draftSpecTimeoutMs(),
43833
44173
  sandbox: "read-only",
43834
- codexExecutable: ctx.codexExecutable
44174
+ codexExecutable: ctx.codexExecutable,
44175
+ // model-tier-planning-and-arbiter task 2.4 (D1): planning is pinned to the seat's strongest
44176
+ // model. Absent when ctx carries no seat (e.g. a hand-built test ctx) — runExec reproduces
44177
+ // today's argv byte-for-byte in that case (task 2.3).
44178
+ model: ctx.seat?.models.strong
43835
44179
  });
43836
44180
  if (!exec.ok) {
43837
44181
  return { kind: "retry", failureReason: "provider_unavailable", error: `spec authoring failed: ${exec.error}` };
@@ -44544,12 +44888,22 @@ async function runDeployGateTick(deps, projectSlug, run3, task) {
44544
44888
  await deps.handoff(projectSlug, task, run3, "verification_steps_missing_at_verify");
44545
44889
  return { kind: "handoff", reason: "verification_steps_missing_at_verify" };
44546
44890
  }
44891
+ let verificationSeat = run3.agentSeat;
44892
+ if (verificationSeat == null) {
44893
+ const seat = await mintResolvedSeat(resolveProvider(process.env, getSelectedProvider()));
44894
+ verificationSeat = seat.id;
44895
+ await deps.client.updateExecutionRun(projectSlug, run3.id, { agentSeat: seat.id }).catch((err) => {
44896
+ console.warn(
44897
+ JSON.stringify({ event: "agent_seat_persist_failed", projectSlug, runId: run3.id, error: String(err) })
44898
+ );
44899
+ });
44900
+ }
44547
44901
  const verdict = await deps.runVerificationTurn({
44548
44902
  env,
44549
44903
  environmentUrl: deployment.environmentUrl,
44550
44904
  verificationSection: section,
44551
44905
  task,
44552
- run: run3
44906
+ run: { ...run3, agentSeat: verificationSeat }
44553
44907
  });
44554
44908
  const verificationAttempt = attemptIndex(artifacts, env, "verification") + 1;
44555
44909
  let outputRef = null;
@@ -45648,7 +46002,18 @@ async function driveRun(deps, projectSlug, runId) {
45648
46002
  maxRuns: threadMaxRuns(),
45649
46003
  graceRuns: threadRetireGraceRuns()
45650
46004
  });
45651
- const resolvedProviderId = resolveProvider(process.env, getSelectedProvider()).id;
46005
+ let resolvedProviderId;
46006
+ if (claimed.agentSeat != null) {
46007
+ resolvedProviderId = claimed.agentSeat;
46008
+ } else {
46009
+ const seat = await mintResolvedSeat(resolveProvider(process.env, getSelectedProvider()));
46010
+ resolvedProviderId = seat.id;
46011
+ await deps.client.updateExecutionRun(projectSlug, runId, { agentSeat: seat.id }).catch((err) => {
46012
+ console.warn(
46013
+ JSON.stringify({ event: "agent_seat_persist_failed", projectSlug, runId, error: String(err) })
46014
+ );
46015
+ });
46016
+ }
45652
46017
  const providerMismatch = task.directorThreadProvider != null && task.directorThreadProvider !== resolvedProviderId;
45653
46018
  const threadIdForAcquire = retirement.retire || providerMismatch ? null : task.directorThreadId;
45654
46019
  if (retirement.retire) {
@@ -45707,12 +46072,12 @@ ${renderTaskStateSection(taskState)}`;
45707
46072
  turnCwd = worktreePath;
45708
46073
  useExecutor = true;
45709
46074
  if (deps.acquireExecutorAgent) {
45710
- const executorAgent = await deps.acquireExecutorAgent(worktreePath);
46075
+ const executorAgent = await deps.acquireExecutorAgent(worktreePath, resolvedProviderId);
45711
46076
  codex = executorAgent.handle;
45712
46077
  disposeCodex = executorAgent.dispose;
45713
46078
  } else {
45714
- codex = await deps.acquireCodex();
45715
- disposeCodex = deps.releaseCodex;
46079
+ codex = await deps.acquireCodex(resolvedProviderId);
46080
+ disposeCodex = () => deps.releaseCodex(resolvedProviderId);
45716
46081
  }
45717
46082
  } catch (error51) {
45718
46083
  const isProviderNotReady = error51 instanceof Error && error51.code === "PROVIDER_NOT_READY";
@@ -45750,8 +46115,8 @@ ${renderTaskStateSection(taskState)}`;
45750
46115
  }
45751
46116
  } else {
45752
46117
  try {
45753
- codex = await deps.acquireCodex();
45754
- disposeCodex = deps.releaseCodex;
46118
+ codex = await deps.acquireCodex(resolvedProviderId);
46119
+ disposeCodex = () => deps.releaseCodex(resolvedProviderId);
45755
46120
  } catch (error51) {
45756
46121
  const detail = error51 instanceof Error ? error51.message : String(error51);
45757
46122
  await failRun(deps, projectSlug, task, runId, leaseId, `codex_acquire_failed: ${detail}`);
@@ -46731,6 +47096,8 @@ async function driveAsyncJob(deps, projectSlug, jobId) {
46731
47096
  const claimed = await deps.client.claimAsyncJob(projectSlug, jobId);
46732
47097
  if (!claimed || !claimed.leaseId) return;
46733
47098
  const leaseId = claimed.leaseId;
47099
+ const seat = claimed.agentSeat != null ? await mintResolvedSeat(resolveProvider(process.env, claimed.agentSeat)) : await mintResolvedSeat(resolveProvider(process.env, getSelectedProvider()));
47100
+ const resolvedProviderId = seat.id;
46734
47101
  const descriptor2 = getAsyncToolDescriptor(claimed.toolName);
46735
47102
  if (!descriptor2) {
46736
47103
  await deps.client.terminalizeAsyncJob(projectSlug, jobId, {
@@ -46755,7 +47122,8 @@ async function driveAsyncJob(deps, projectSlug, jobId) {
46755
47122
  projectSlug,
46756
47123
  jobId,
46757
47124
  leaseId,
46758
- await deriveRequestedHash(deps.client, claimed.toolName, input)
47125
+ await deriveRequestedHash(deps.client, claimed.toolName, input),
47126
+ resolvedProviderId
46759
47127
  );
46760
47128
  if (!begun) return;
46761
47129
  if (begun.attemptCount > ASYNC_JOB_MAX_ATTEMPTS) {
@@ -46766,7 +47134,7 @@ async function driveAsyncJob(deps, projectSlug, jobId) {
46766
47134
  outcome = await deps.run(
46767
47135
  "async",
46768
47136
  () => descriptor2.run(
46769
- { client: deps.client, cwd: deps.cwd, codexExecutable: deps.codexExecutable },
47137
+ { client: deps.client, cwd: deps.cwd, codexExecutable: deps.codexExecutable, seat, projectSlug },
46770
47138
  input
46771
47139
  )
46772
47140
  );
@@ -46796,13 +47164,31 @@ async function driveAsyncJob(deps, projectSlug, jobId) {
46796
47164
  failureReason: outcome.kind === "done" ? null : outcome.failureReason
46797
47165
  }).catch(() => {
46798
47166
  });
47167
+ if (descriptor2.appliesIntentVerdict && outcome.kind === "done" && outcome.taskId) {
47168
+ let rawResult;
47169
+ try {
47170
+ rawResult = JSON.parse(outcome.resultJson);
47171
+ } catch {
47172
+ rawResult = void 0;
47173
+ }
47174
+ const parsedVerdict = mrrlinIntentVerdictSchema.safeParse(rawResult);
47175
+ if (parsedVerdict.success) {
47176
+ await deps.client.applyIntentVerdict(projectSlug, outcome.taskId, parsedVerdict.data).catch(() => {
47177
+ });
47178
+ }
47179
+ }
46799
47180
  const delivery = descriptor2.onComplete(outcome, input);
46800
47181
  if (!delivery.suppressInbox) {
46801
47182
  await deps.client.createInboxItem(projectSlug, {
46802
47183
  id: delivery.id ?? `II-${import_node_crypto6.default.randomUUID()}`,
46803
47184
  kind: delivery.kind,
46804
47185
  body: delivery.body,
46805
- ...delivery.taskId ? { taskId: delivery.taskId } : {}
47186
+ ...delivery.taskId ? { taskId: delivery.taskId } : {},
47187
+ // early-intent-confirmation: without forwarding these, an intent-check question lands
47188
+ // with origin unset, and the adjudicated-set filter (consensus/wiring.ts) that excludes
47189
+ // gate-authored auto-closures by origin silently stops suppressing duplicates for it.
47190
+ ...delivery.origin !== void 0 ? { origin: delivery.origin } : {},
47191
+ ...delivery.questionOptions !== void 0 ? { questionOptions: delivery.questionOptions } : {}
46806
47192
  }).catch(() => {
46807
47193
  });
46808
47194
  }
@@ -46858,6 +47244,10 @@ async function runDeliverableGate(_target, deps) {
46858
47244
  maxHeadCalls: deps.maxHeadCalls,
46859
47245
  runHead: deps.runHead,
46860
47246
  runArbiter: deps.runArbiter,
47247
+ // model-tier-planning-and-arbiter task 2.20: no real multi-head panel reaches this gate yet
47248
+ // (that wiring is a documented, separate gap — see spec-gate.ts/code-gate.ts), so this always
47249
+ // reports the honest single-seat composition rather than a fabricated multi-agent one.
47250
+ panelComposition: { requested: deps.heads.length, filled: 1, underpowered: deps.heads.length > 1 },
46861
47251
  revise: async (artifact, accepted) => {
46862
47252
  const revised = await deps.runReviser(artifact, accepted);
46863
47253
  if (!revised.ok) return artifact;
@@ -46936,48 +47326,58 @@ function createSessionSinkRegistry(maxEntries = DEFAULT_MAX_SESSION_SINKS) {
46936
47326
 
46937
47327
  // src/agent-keepalive.ts
46938
47328
  function createAgentKeepalive(opts) {
46939
- let idleTimer = null;
46940
- let holdCount = 0;
46941
- let managerHeld = false;
46942
- const cancelTimer = () => {
46943
- if (idleTimer) {
46944
- clearTimeout(idleTimer);
46945
- idleTimer = null;
47329
+ const slots = /* @__PURE__ */ new Map();
47330
+ function getSlot(providerId) {
47331
+ let slot = slots.get(providerId);
47332
+ if (!slot) {
47333
+ slot = { idleTimer: null, holdCount: 0, managerHeld: false };
47334
+ slots.set(providerId, slot);
47335
+ }
47336
+ return slot;
47337
+ }
47338
+ const cancelTimer = (slot) => {
47339
+ if (slot.idleTimer) {
47340
+ clearTimeout(slot.idleTimer);
47341
+ slot.idleTimer = null;
46946
47342
  }
46947
47343
  };
46948
47344
  return {
46949
- acquire: async () => {
46950
- cancelTimer();
46951
- const client = await opts.acquire();
46952
- if (managerHeld) {
46953
- opts.release();
47345
+ acquire: async (providerId) => {
47346
+ const slot = getSlot(providerId);
47347
+ cancelTimer(slot);
47348
+ const client = await opts.acquire(providerId);
47349
+ if (slot.managerHeld) {
47350
+ opts.release(providerId);
46954
47351
  } else {
46955
- managerHeld = true;
47352
+ slot.managerHeld = true;
46956
47353
  }
46957
- holdCount += 1;
47354
+ slot.holdCount += 1;
46958
47355
  return client;
46959
47356
  },
46960
- release: () => {
46961
- holdCount = Math.max(0, holdCount - 1);
46962
- if (holdCount > 0) {
47357
+ release: (providerId) => {
47358
+ const slot = getSlot(providerId);
47359
+ slot.holdCount = Math.max(0, slot.holdCount - 1);
47360
+ if (slot.holdCount > 0) {
46963
47361
  return;
46964
47362
  }
46965
- cancelTimer();
46966
- idleTimer = setTimeout(() => {
46967
- idleTimer = null;
46968
- if (managerHeld) {
46969
- managerHeld = false;
46970
- opts.release();
47363
+ cancelTimer(slot);
47364
+ slot.idleTimer = setTimeout(() => {
47365
+ slot.idleTimer = null;
47366
+ if (slot.managerHeld) {
47367
+ slot.managerHeld = false;
47368
+ opts.release(providerId);
46971
47369
  }
46972
- }, opts.idleMs);
46973
- idleTimer.unref?.();
47370
+ }, opts.idleMs(providerId));
47371
+ slot.idleTimer.unref?.();
46974
47372
  },
46975
47373
  dropNow: () => {
46976
- cancelTimer();
46977
- holdCount = 0;
46978
- if (managerHeld) {
46979
- managerHeld = false;
46980
- opts.release();
47374
+ for (const [providerId, slot] of slots) {
47375
+ cancelTimer(slot);
47376
+ slot.holdCount = 0;
47377
+ if (slot.managerHeld) {
47378
+ slot.managerHeld = false;
47379
+ opts.release(providerId);
47380
+ }
46981
47381
  }
46982
47382
  }
46983
47383
  };
@@ -51286,7 +51686,11 @@ function createRunVerificationTurn(deps) {
51286
51686
  if (isBlockedVerificationHost(hostname3)) {
51287
51687
  return { verdict: "fail", summary: `refusing to verify against non-public host: ${hostname3}`, turnRef: null };
51288
51688
  }
51289
- const { handle, dispose, cwd } = await deps.startSandbox([hostname3]);
51689
+ const providerId = input.run.agentSeat;
51690
+ if (providerId == null) {
51691
+ return { verdict: "fail", summary: "run has no resolved agent_seat", turnRef: null };
51692
+ }
51693
+ const { handle, dispose, cwd } = await deps.startSandbox([hostname3], providerId);
51290
51694
  try {
51291
51695
  let text;
51292
51696
  try {
@@ -52105,10 +52509,48 @@ function buildThreadStartParams(msg, config2, desiredModel) {
52105
52509
  " ## Result \u2014 what was accomplished, key findings or metrics, follow-up tasks created.",
52106
52510
  " Do NOT remove earlier sections \u2014 they remain useful for context.",
52107
52511
  " Adapt depth to the task: a one-line fix needs one sentence, not four headings.",
52108
- "When you create a task out of an operator request, set `operatorSeedRequest` to the operator's",
52109
- "VERBATIM words (their request as they wrote it \u2014 NOT your paraphrase or synthesized notes). It is",
52110
- "the spec gate's independent record of intent; omit it only when there is genuinely no operator",
52111
- "request behind the task (pure system bookkeeping) \u2014 note that omitting it makes the gate fail-closed.",
52512
+ // early-intent-confirmation (design.md, "The chat contract"): the core block below is copied
52513
+ // VERBATIM from design.md, per tasks.md 10.1. It replaces an older sentence that also carried
52514
+ // "omit operatorSeedRequest only when there is genuinely no operator request behind the task
52515
+ // (pure system bookkeeping) — omitting it makes the gate fail-closed". design.md's own
52516
+ // replacement text does not restate that exception, so it was initially left out per the
52517
+ // "verbatim" instruction and flagged for the consensus round instead of silently deciding
52518
+ // either way. Round 1 (two of three reviewers, independently) confirmed a real gap: without an
52519
+ // explicit "do not invent one" instruction, the Director could fabricate operator wording for
52520
+ // a task with no real request behind it. The paragraph a few lines below ("If a task has no
52521
+ // real operator request...") closes that gap — it is a NEW clarifying addition, not a
52522
+ // verbatim restoration of the old sentence, so it is deliberately kept separate from the
52523
+ // pinned verbatim block in director-bridge.test.ts.
52524
+ "Before you call create_task out of an operator request, enumerate the aspects needed to state",
52525
+ "the task's outcome. For each aspect that is not yet resolved, resolve it in THIS order:",
52526
+ " 1. the conversation you are already in \u2014 this resolves most of them, and costs nothing;",
52527
+ " 2. search_wiki_pages / retrieve_context.",
52528
+ "Ask the operator ONLY about aspects that neither resolved. When you must ask, reply in the SAME",
52529
+ "turn with one sentence restating your understanding, then at most 3 scope-level questions, and",
52530
+ "do NOT create the task yet. Create it after the operator replies.",
52531
+ "",
52532
+ "Set operatorSeedRequest to the operator's OWN WORDS, verbatim. Select every message in this",
52533
+ "conversation that forms part of the request \u2014 not only the last one \u2014 and concatenate them in",
52534
+ "order, separated by a blank line. Do NOT paraphrase, summarise, or rewrite any of it, and do",
52535
+ "NOT add words of your own.",
52536
+ "",
52537
+ "If a task has no real operator request behind it at all (pure system bookkeeping nobody asked",
52538
+ "for), do NOT invent one to fill this field \u2014 leave operatorSeedRequest unset. Never fabricate",
52539
+ "operator wording; an unset seed is handled downstream, a fabricated one is not.",
52540
+ "",
52541
+ 'A bare last message ("ok, do it", "\u0442\u0430\u043A \u0433\u0443\u0434", "finish it") is useless on its own, so carry the',
52542
+ "earlier messages that give it meaning. If the operator's request spans three turns, the seed",
52543
+ "holds all three. If they truly said only one substantive thing, the seed holds only that.",
52544
+ "",
52545
+ "Three worked examples for the resolution order above:",
52546
+ '- "\u0434\u043E\u0440\u043E\u0431\u0438\u0448" right after discussing a specific task \u2014 resolved from THE CONVERSATION, so do not',
52547
+ " ask. This is the common case: 15 of every 19 apparent gaps close here, for free.",
52548
+ '- "\u0434\u043E\u0434\u0430\u0439 \u043D\u0430 \u043B\u0435\u043D\u0434\u0456\u043D\u0433 \u0431\u043B\u043E\u043A \u044F\u043A \u0443 Native Vibe" when that campaign is documented on the wiki and the',
52549
+ " conversation itself never named it \u2014 resolved from THE WIKI, after the conversation fails to",
52550
+ " name it. Do not ask.",
52551
+ '- "\u0437\u0440\u043E\u0431\u0438 \u044F\u043A \u043C\u0438 \u0434\u043E\u043C\u043E\u0432\u043B\u044F\u043B\u0438\u0441\u044C \u043F\u043E \u0446\u0456\u043D\u0430\u0445" with no pricing decision anywhere in this conversation and',
52552
+ " nothing on the wiki either \u2014 resolved by NEITHER. Ask; this is a decision only the operator",
52553
+ " holds.",
52112
52554
  "",
52113
52555
  "Spec authoring contract \u2014 when creating or updating a `Specs/{id}` wiki page:",
52114
52556
  "- BEGIN the page with YAML frontmatter naming the gated task: a first line `---`, then",
@@ -53066,7 +53508,8 @@ function createBridgeMessageHandler(deps) {
53066
53508
  let bootstrapError;
53067
53509
  const fallbackModels = readCodexFallbackModels();
53068
53510
  try {
53069
- const codexClient2 = await deps.acquireCodex();
53511
+ const bootstrapProviderId = resolveProvider(process.env, getSelectedProvider()).id;
53512
+ const codexClient2 = await deps.acquireCodex(bootstrapProviderId);
53070
53513
  try {
53071
53514
  try {
53072
53515
  const raw = await codexClient2.listModels();
@@ -53100,7 +53543,7 @@ function createBridgeMessageHandler(deps) {
53100
53543
  bootstrapError ??= error51 instanceof Error ? error51.message : String(error51);
53101
53544
  }
53102
53545
  } finally {
53103
- deps.releaseCodex();
53546
+ deps.releaseCodex(bootstrapProviderId);
53104
53547
  }
53105
53548
  } catch (error51) {
53106
53549
  bootstrapError = error51 instanceof Error ? error51.message : String(error51);
@@ -53150,7 +53593,8 @@ function createBridgeMessageHandler(deps) {
53150
53593
  pendingTurnSessions.delete(directorSessionId2);
53151
53594
  }
53152
53595
  }
53153
- const codexClient2 = await deps.acquireCodex();
53596
+ const interruptProviderId = existing.provider ?? resolveProvider(process.env, getSelectedProvider()).id;
53597
+ const codexClient2 = await deps.acquireCodex(interruptProviderId);
53154
53598
  try {
53155
53599
  await codexClient2.interruptTurn(existing.threadId);
53156
53600
  existing.cancel?.();
@@ -53158,7 +53602,7 @@ function createBridgeMessageHandler(deps) {
53158
53602
  } catch (error51) {
53159
53603
  sendForSpan({ type: "error", directorSessionId: directorSessionId2, error: error51 instanceof Error ? error51.message : String(error51) });
53160
53604
  } finally {
53161
- deps.releaseCodex();
53605
+ deps.releaseCodex(interruptProviderId);
53162
53606
  }
53163
53607
  return;
53164
53608
  }
@@ -53219,8 +53663,9 @@ function createBridgeMessageHandler(deps) {
53219
53663
  return;
53220
53664
  }
53221
53665
  pendingTurnSessions.add(directorSessionId);
53222
- const turnProviderCapabilities = resolveProvider(process.env, getSelectedProvider()).capabilities();
53223
- const codexClient = await deps.acquireCodex();
53666
+ const turnSeat = await mintResolvedSeat(resolveProvider(process.env, getSelectedProvider()));
53667
+ const turnProviderCapabilities = turnSeat.capabilities;
53668
+ const codexClient = await deps.acquireCodex(turnSeat.id);
53224
53669
  let thread = deps.threads.get(directorSessionId);
53225
53670
  if (thread?.inFlight && msg.takeover === true) {
53226
53671
  const takenOver = thread;
@@ -53264,7 +53709,7 @@ function createBridgeMessageHandler(deps) {
53264
53709
  }
53265
53710
  const desiredModel = normalizeModel(msg.model);
53266
53711
  const desiredEffort = normalizeReasoningEffort(msg.reasoningEffort);
53267
- const resolvedProviderId = resolveProvider(process.env, getSelectedProvider()).id;
53712
+ const resolvedProviderId = turnSeat.id;
53268
53713
  try {
53269
53714
  const startParams = deps.buildStartParams(msg, desiredModel);
53270
53715
  if (!thread) {
@@ -53322,7 +53767,7 @@ function createBridgeMessageHandler(deps) {
53322
53767
  deps.setCodexAuth?.("expired");
53323
53768
  }
53324
53769
  pendingTurnSessions.delete(directorSessionId);
53325
- deps.releaseCodex();
53770
+ deps.releaseCodex(turnSeat.id);
53326
53771
  return;
53327
53772
  }
53328
53773
  if (thread.inFlight) {
@@ -53334,7 +53779,7 @@ function createBridgeMessageHandler(deps) {
53334
53779
  error: "Director session is busy."
53335
53780
  });
53336
53781
  pendingTurnSessions.delete(directorSessionId);
53337
- deps.releaseCodex();
53782
+ deps.releaseCodex(turnSeat.id);
53338
53783
  return;
53339
53784
  }
53340
53785
  thread.inFlight = true;
@@ -53587,7 +54032,7 @@ ${effectiveText}` : effectiveText;
53587
54032
  thread.lastUsedAt = Date.now();
53588
54033
  thread.lastUsedAtSuspendedMs = deps.suspendedMs?.() ?? 0;
53589
54034
  deps.persistThreads();
53590
- deps.releaseCodex();
54035
+ deps.releaseCodex(turnSeat.id);
53591
54036
  }
53592
54037
  });
53593
54038
  };
@@ -53801,20 +54246,20 @@ async function startDirectorBridge() {
53801
54246
  const hasStaticToken = !agentSecret && Boolean(envToken ?? readOperatorToken());
53802
54247
  const client = createMrrlinClient({ baseUrl: apiBaseUrl, fetch: authedFetch });
53803
54248
  const agentKeepalive = createAgentKeepalive({
53804
- acquire: () => resolveProvider(process.env, getSelectedProvider()).acquireWarm(),
53805
- release: () => resolveProvider(process.env, getSelectedProvider()).releaseWarm(),
53806
- idleMs: agentIdleMs(resolveProvider(process.env, getSelectedProvider()).id)
54249
+ acquire: (providerId) => resolveProvider(process.env, providerId).acquireWarm(),
54250
+ release: (providerId) => resolveProvider(process.env, providerId).releaseWarm(),
54251
+ idleMs: (providerId) => agentIdleMs(providerId)
53807
54252
  });
53808
54253
  const checkoutRegistry = new CheckoutRegistry(config2.stateDir);
53809
54254
  const staleSeconds = readEnvInt("MRRLIN_DIRECTOR_BRIDGE_STALE_SECONDS", DEFAULT_STALE_SECONDS);
53810
54255
  const consumerDeps = {
53811
54256
  client,
53812
- acquireCodex: () => agentKeepalive.acquire(),
54257
+ acquireCodex: (providerId) => agentKeepalive.acquire(providerId),
53813
54258
  // Read-only / chat path release: the shared keepalive. The executor path no
53814
54259
  // longer routes through here — it disposes its OWN per-run server via the
53815
54260
  // handle returned by acquireExecutorCodex (Fix A + G), so there is no shared
53816
54261
  // mutable `activeExecutorClient` slot to race a concurrent chat/bootstrap turn.
53817
- releaseCodex: () => agentKeepalive.release(),
54262
+ releaseCodex: (providerId) => agentKeepalive.release(providerId),
53818
54263
  run: wrappedTurnLock.run,
53819
54264
  staleSeconds,
53820
54265
  touchIntervalMs: Math.min(DEFAULT_TOUCH_INTERVAL_MS, Math.floor(staleSeconds * 1e3 / 2)),
@@ -53830,10 +54275,13 @@ async function startDirectorBridge() {
53830
54275
  // The returned handle's dispose() shuts down the spawned process (task 4.1: startExecutorServer
53831
54276
  // itself owns which provider-specific shutdown that is) — releasing the executor therefore
53832
54277
  // kills the process. driveRun owns this dispose per-run (Fix A + G).
53833
- acquireExecutorAgent: async (worktreePath) => {
54278
+ // Task 1.11: providerId is the caller's own pinned/minted seat (execution-consumer.ts's
54279
+ // resolvedProviderId), not resolved ambiently here — this run-scoped spawn has no business
54280
+ // re-reading a setting that could have moved since the run's own seat was decided.
54281
+ acquireExecutorAgent: async (worktreePath, providerId) => {
53834
54282
  const executor = await startExecutorServer({
53835
54283
  worktree: worktreePath,
53836
- provider: resolveProvider(process.env, getSelectedProvider()).id
54284
+ provider: providerId
53837
54285
  });
53838
54286
  return { handle: executor.handle, dispose: executor.dispose };
53839
54287
  },
@@ -53862,11 +54310,11 @@ async function startDirectorBridge() {
53862
54310
  runVerificationTurn: createRunVerificationTurn({
53863
54311
  // egressDomains is retained in the seam signature (verification-turn.ts still computes the
53864
54312
  // env host) but no longer constrains anything — the executor is unrestricted.
53865
- startSandbox: async (_egressDomains) => {
54313
+ startSandbox: async (_egressDomains, providerId) => {
53866
54314
  const cwd = import_node_fs16.default.mkdtempSync(import_node_path20.default.join(import_node_os11.default.tmpdir(), "mrrlin-verify-"));
53867
54315
  let executor;
53868
54316
  try {
53869
- executor = await startExecutorServer({ worktree: cwd, provider: resolveProvider(process.env, getSelectedProvider()).id });
54317
+ executor = await startExecutorServer({ worktree: cwd, provider: providerId });
53870
54318
  } catch (error51) {
53871
54319
  try {
53872
54320
  import_node_fs16.default.rmSync(cwd, { recursive: true, force: true });
@@ -54037,8 +54485,8 @@ async function startDirectorBridge() {
54037
54485
  const relayUrl = process.env.MRRLIN_RELAY_URL?.trim();
54038
54486
  const relayConfigured = Boolean(agentSecret && relayUrl);
54039
54487
  const sharedHandlerDeps = {
54040
- acquireCodex: () => agentKeepalive.acquire(),
54041
- releaseCodex: () => agentKeepalive.release(),
54488
+ acquireCodex: (providerId) => agentKeepalive.acquire(providerId),
54489
+ releaseCodex: (providerId) => agentKeepalive.release(providerId),
54042
54490
  run: wrappedTurnLock.run,
54043
54491
  suspendedMs: () => suspensionDetector.suspendedMs(),
54044
54492
  buildStartParams: (msg, desiredModel) => buildThreadStartParams(msg, config2, desiredModel),
@@ -58117,6 +58565,7 @@ async function runCodeGate(_target, deps) {
58117
58565
  heads,
58118
58566
  rounds: deps.rounds,
58119
58567
  maxHeadCalls: deps.maxHeadCalls,
58568
+ panelComposition: deps.panelComposition ?? { requested: heads.length, filled: 1, underpowered: heads.length > 1 },
58120
58569
  runHead: deps.runHead,
58121
58570
  runArbiter: deps.runArbiter,
58122
58571
  revise: async (prev, accepted) => {
@@ -58152,7 +58601,13 @@ async function runCodeGate(_target, deps) {
58152
58601
  return await deps.readReviewArtifact();
58153
58602
  }
58154
58603
  });
58155
- return { outcome: result.outcome, rounds: result.rounds };
58604
+ return {
58605
+ outcome: result.outcome,
58606
+ rounds: result.rounds,
58607
+ arbiterInfo: result.arbiterInfo,
58608
+ panelComposition: result.panelComposition,
58609
+ sufficiencyVerdict: result.sufficiencyVerdict
58610
+ };
58156
58611
  } catch (err) {
58157
58612
  if (err instanceof CodeGateUnfixableError) {
58158
58613
  return { outcome: "CONSENSUS_BLOCKED" };
@@ -58162,10 +58617,101 @@ async function runCodeGate(_target, deps) {
58162
58617
  }
58163
58618
 
58164
58619
  // src/consensus/multi-llm-head.ts
58620
+ var import_node_child_process17 = require("node:child_process");
58621
+
58622
+ // src/consensus/provider-limits.ts
58165
58623
  var import_node_child_process16 = require("node:child_process");
58624
+ var DEFAULT_STATUS_TIMEOUT_MS = 1e4;
58166
58625
  function errMessage4(err) {
58167
58626
  return err instanceof Error ? err.message : String(err);
58168
58627
  }
58628
+ function runExternalAgentsStatus(bin, args, timeoutMs, spawn4) {
58629
+ return new Promise((resolve) => {
58630
+ let settled = false;
58631
+ const timerRef = { id: void 0 };
58632
+ const finish = (r) => {
58633
+ if (settled) return;
58634
+ settled = true;
58635
+ if (timerRef.id) clearTimeout(timerRef.id);
58636
+ resolve(r);
58637
+ };
58638
+ const options = { stdio: ["ignore", "pipe", "pipe"], env: process.env, shell: false };
58639
+ let child;
58640
+ try {
58641
+ child = spawn4(bin, args, options);
58642
+ } catch (err) {
58643
+ finish({ stdout: "", error: `spawn ${bin} failed: ${errMessage4(err)}` });
58644
+ return;
58645
+ }
58646
+ let stdout = "";
58647
+ timerRef.id = setTimeout(() => {
58648
+ child.kill("SIGKILL");
58649
+ finish({ stdout, error: `external-agents status timed out after ${timeoutMs}ms` });
58650
+ }, timeoutMs);
58651
+ timerRef.id.unref?.();
58652
+ child.stdout?.on("data", (chunk) => {
58653
+ stdout += chunk.toString();
58654
+ });
58655
+ child.stderr?.on("data", () => {
58656
+ });
58657
+ child.on("error", (err) => finish({ stdout, error: `external-agents status error: ${errMessage4(err)}` }));
58658
+ child.on("close", (code) => {
58659
+ if (code !== 0 && !stdout.trim()) finish({ stdout, error: `external-agents status exited ${code}` });
58660
+ else finish({ stdout });
58661
+ });
58662
+ });
58663
+ }
58664
+ async function fetchRegistrySnapshot(deps) {
58665
+ const spawn4 = deps?.spawn ?? import_node_child_process16.spawn;
58666
+ const { command, prefixArgs } = resolveExternalAgentsCliInvocation(deps?.externalAgentsBin);
58667
+ const { stdout, error: error51 } = await runExternalAgentsStatus(
58668
+ command,
58669
+ [...prefixArgs, "status", "--json"],
58670
+ deps?.statusTimeoutMs ?? DEFAULT_STATUS_TIMEOUT_MS,
58671
+ spawn4
58672
+ );
58673
+ if (error51) return { ok: false, error: error51 };
58674
+ try {
58675
+ const parsed = JSON.parse(stdout);
58676
+ return { ok: true, entries: Array.isArray(parsed) ? parsed : [] };
58677
+ } catch (err) {
58678
+ return { ok: false, error: `unparseable status --json output: ${errMessage4(err)}` };
58679
+ }
58680
+ }
58681
+ function tightestPublishedContextWindow(entries) {
58682
+ const values = entries.map((e) => e.token_limits?.context_window).filter((v) => typeof v === "number" && v > 0);
58683
+ return values.length > 0 ? Math.min(...values) : null;
58684
+ }
58685
+ async function enforceProviderLimits(seat, agentId, tokenCount, deps) {
58686
+ const snapshot = await fetchRegistrySnapshot(deps);
58687
+ if (!snapshot.ok) {
58688
+ console.warn(
58689
+ JSON.stringify({ event: "provider_limits_check_skipped_fetch_failed", agentId, error: snapshot.error })
58690
+ );
58691
+ return { ok: true };
58692
+ }
58693
+ const entries = snapshot.entries;
58694
+ const entry = entries.find((e) => e.id === agentId);
58695
+ const ownContextWindow = entry?.token_limits?.context_window;
58696
+ const borrowedContextWindow = ownContextWindow == null ? tightestPublishedContextWindow(entries) : null;
58697
+ const effectiveContextWindow = ownContextWindow ?? borrowedContextWindow;
58698
+ if (effectiveContextWindow != null && tokenCount > effectiveContextWindow) {
58699
+ return { ok: false, reason: "over_context", borrowed: ownContextWindow == null };
58700
+ }
58701
+ const tpm = entry?.token_limits?.tpm;
58702
+ if (tpm != null && tokenCount > tpm) {
58703
+ return { ok: false, reason: "over_rate" };
58704
+ }
58705
+ return { ok: true };
58706
+ }
58707
+
58708
+ // src/consensus/multi-llm-head.ts
58709
+ function estimateTokenCount(text) {
58710
+ return Math.ceil(text.length / 4);
58711
+ }
58712
+ function errMessage5(err) {
58713
+ return err instanceof Error ? err.message : String(err);
58714
+ }
58169
58715
  function runExternalAgents(bin, args, label, timeoutMs, spawn4) {
58170
58716
  return new Promise((resolve) => {
58171
58717
  let settled = false;
@@ -58186,7 +58732,7 @@ function runExternalAgents(bin, args, label, timeoutMs, spawn4) {
58186
58732
  try {
58187
58733
  child = spawn4(bin, args, options);
58188
58734
  } catch (err) {
58189
- finish({ code: null, stdout: "", error: `spawn ${bin} failed: ${errMessage4(err)}` });
58735
+ finish({ code: null, stdout: "", error: `spawn ${bin} failed: ${errMessage5(err)}` });
58190
58736
  return;
58191
58737
  }
58192
58738
  let stdout = "";
@@ -58199,12 +58745,12 @@ function runExternalAgents(bin, args, label, timeoutMs, spawn4) {
58199
58745
  });
58200
58746
  child.stderr?.on("data", () => {
58201
58747
  });
58202
- child.on("error", (err) => finish({ code: null, stdout, error: `external-agents ${label} error: ${errMessage4(err)}` }));
58748
+ child.on("error", (err) => finish({ code: null, stdout, error: `external-agents ${label} error: ${errMessage5(err)}` }));
58203
58749
  child.on("close", (code) => finish({ code, stdout }));
58204
58750
  });
58205
58751
  }
58206
58752
  async function pickPanel(input, deps) {
58207
- const spawn4 = deps?.spawn ?? import_node_child_process16.spawn;
58753
+ const spawn4 = deps?.spawn ?? import_node_child_process17.spawn;
58208
58754
  const { command, prefixArgs } = resolveExternalAgentsCliInvocation(deps?.externalAgentsBin);
58209
58755
  const args = [
58210
58756
  "pick",
@@ -58225,7 +58771,7 @@ async function pickPanel(input, deps) {
58225
58771
  return { ok: true, agents };
58226
58772
  }
58227
58773
  async function dispatchHead(agentId, prompt, timeoutMs, deps) {
58228
- const spawn4 = deps?.spawn ?? import_node_child_process16.spawn;
58774
+ const spawn4 = deps?.spawn ?? import_node_child_process17.spawn;
58229
58775
  const { command, prefixArgs } = resolveExternalAgentsCliInvocation(deps?.externalAgentsBin);
58230
58776
  const args = ["dispatch", agentId, "--json", prompt];
58231
58777
  const r = await runExternalAgents(command, [...prefixArgs, ...args], "dispatch", timeoutMs, spawn4);
@@ -58237,7 +58783,7 @@ async function dispatchHead(agentId, prompt, timeoutMs, deps) {
58237
58783
  try {
58238
58784
  parsed = JSON.parse(jsonLine);
58239
58785
  } catch (err) {
58240
- return { ok: false, error: `dispatch ${agentId}: unparseable JSON (${errMessage4(err)})` };
58786
+ return { ok: false, error: `dispatch ${agentId}: unparseable JSON (${errMessage5(err)})` };
58241
58787
  }
58242
58788
  if (parsed.outcome !== "success") {
58243
58789
  return { ok: false, error: `dispatch ${agentId}: outcome=${parsed.outcome} (exit ${parsed.exit_code})` };
@@ -58248,31 +58794,66 @@ async function dispatchHead(agentId, prompt, timeoutMs, deps) {
58248
58794
  return { ok: true, text: parsed.text };
58249
58795
  }
58250
58796
  async function buildMultiHeadRunner(input, deps) {
58251
- const picked = await pickPanel(
58797
+ const pickOnce = () => pickPanel(
58252
58798
  { n: input.personas.length, excludeProvider: input.excludeProvider, tierPrefer: input.tierPrefer, timeoutMs: input.pickTimeoutMs },
58253
58799
  deps
58254
58800
  );
58801
+ const picked = await pickOnce();
58255
58802
  if (!picked.ok) return { ok: false, error: picked.error };
58256
58803
  if (picked.agents.length === 0) return { ok: false, error: "pick returned no agents" };
58257
- const agents = picked.agents;
58258
- const assignment = {};
58259
- input.personas.forEach((persona, i) => {
58260
- const agent = agents[i % agents.length];
58261
- if (agent) assignment[persona] = agent;
58262
- });
58263
- const runHead = async (persona, artifact, dismissed) => {
58264
- const agentId = assignment[persona];
58265
- if (!agentId) return { ok: false, error: `no agent assigned for persona ${persona}` };
58266
- const instructions = input.personaInstructions[persona] ?? "";
58267
- const dismissedBlock = dismissed.trim() ? `
58804
+ function buildFromAgents(agents) {
58805
+ const assignment = {};
58806
+ input.personas.forEach((persona, i) => {
58807
+ const agent = agents[i % agents.length];
58808
+ if (agent) assignment[persona] = agent;
58809
+ });
58810
+ const composition = {
58811
+ requested: input.personas.length,
58812
+ filled: agents.length,
58813
+ agentIds: agents,
58814
+ underpowered: agents.length < input.personas.length
58815
+ };
58816
+ const runHead = async (persona, artifact, dismissed) => {
58817
+ const agentId = assignment[persona];
58818
+ if (!agentId) return { ok: false, error: `no agent assigned for persona ${persona}` };
58819
+ const instructions = input.personaInstructions[persona] ?? "";
58820
+ const dismissedBlock = dismissed.trim() ? `
58268
58821
 
58269
58822
  ${dismissed.trim()}` : "";
58270
- const prompt = instructions.trim() ? `${instructions.trim()}
58823
+ const prompt = instructions.trim() ? `${instructions.trim()}
58271
58824
 
58272
58825
  ${artifact}${dismissedBlock}` : `${artifact}${dismissedBlock}`;
58273
- return dispatchHead(agentId, prompt, input.dispatchTimeoutMs, deps);
58826
+ if (input.seat) {
58827
+ const limits = await enforceProviderLimits(input.seat, agentId, estimateTokenCount(prompt), deps);
58828
+ if (!limits.ok) {
58829
+ const borrowed = limits.reason === "over_context" && limits.borrowed === true;
58830
+ console.warn(
58831
+ JSON.stringify({
58832
+ event: borrowed ? "head_skipped_borrowed_ceiling" : "head_skipped_over_budget",
58833
+ persona,
58834
+ agentId,
58835
+ reason: limits.reason
58836
+ })
58837
+ );
58838
+ return {
58839
+ ok: false,
58840
+ error: `${agentId} skipped (${limits.reason}${borrowed ? ", borrowed ceiling" : ""})`
58841
+ };
58842
+ }
58843
+ }
58844
+ console.log(JSON.stringify({ event: "pool_egress", persona, agentId, promptBytes: prompt.length }));
58845
+ return dispatchHead(agentId, prompt, input.dispatchTimeoutMs, deps);
58846
+ };
58847
+ return { agents, assignment, composition, runHead };
58848
+ }
58849
+ const built = buildFromAgents(picked.agents);
58850
+ const repick = async () => {
58851
+ const rePicked = await pickOnce();
58852
+ if (!rePicked.ok || rePicked.agents.length <= built.agents.length) return { ok: false };
58853
+ const rebuilt = buildFromAgents(rePicked.agents);
58854
+ return { ok: true, composition: rebuilt.composition, runHead: rebuilt.runHead };
58274
58855
  };
58275
- return { ok: true, runner: { agents, assignment, runHead } };
58856
+ return { ok: true, runner: { ...built, repick } };
58276
58857
  }
58277
58858
 
58278
58859
  // src/consensus/multi-llm-shadow.ts
@@ -58283,7 +58864,7 @@ function rollUpVerdict(verdicts) {
58283
58864
  return "APPROVE";
58284
58865
  }
58285
58866
  async function runShadowPanel(input, deps) {
58286
- const orchestratorSeat = resolveProvider(process.env, getSelectedProvider()).id;
58867
+ const orchestratorSeat = input.orchestratorSeat;
58287
58868
  const base = {
58288
58869
  ran: false,
58289
58870
  correlationId: input.correlationId,
@@ -58297,7 +58878,8 @@ async function runShadowPanel(input, deps) {
58297
58878
  excludeProvider: input.excludeProvider,
58298
58879
  tierPrefer: "strong",
58299
58880
  pickTimeoutMs: input.pickTimeoutMs,
58300
- dispatchTimeoutMs: input.dispatchTimeoutMs
58881
+ dispatchTimeoutMs: input.dispatchTimeoutMs,
58882
+ seat: input.seat
58301
58883
  },
58302
58884
  deps
58303
58885
  );
@@ -58345,7 +58927,7 @@ async function runShadowPanelSafe(input, deps, logger = (line) => console.error(
58345
58927
  correlationId: input.correlationId,
58346
58928
  productionVerdict: input.productionVerdict,
58347
58929
  reason: `shadow panel threw: ${err instanceof Error ? err.message : String(err)}`,
58348
- orchestratorSeat: resolveProvider(process.env, getSelectedProvider()).id
58930
+ orchestratorSeat: input.orchestratorSeat
58349
58931
  };
58350
58932
  }
58351
58933
  try {
@@ -58379,13 +58961,19 @@ async function maybeRunShadow(input, deps, logger) {
58379
58961
  personaInstructions[name] = "";
58380
58962
  }
58381
58963
  }
58964
+ const seat = await mintResolvedSeat(resolveProvider(process.env, input.persistedProvider));
58382
58965
  await runShadowPanelSafe(
58383
58966
  {
58384
58967
  heads,
58385
58968
  personaInstructions,
58386
58969
  artifact: input.artifact,
58387
58970
  productionVerdict,
58388
- excludeProvider: resolveProvider(process.env, input.persistedProvider).capabilities().poolProviderLabels.join(","),
58971
+ excludeProvider: seat.capabilities.poolProviderLabels.join(","),
58972
+ orchestratorSeat: seat.id,
58973
+ // task 1.21: the same seat, forwarded so buildMultiHeadRunner's per-head dispatch path
58974
+ // has one in scope too (see ShadowInput.seat's own comment for why this is separate
58975
+ // from orchestratorSeat above).
58976
+ seat,
58389
58977
  pickTimeoutMs: PICK_TIMEOUT_MS,
58390
58978
  dispatchTimeoutMs: DISPATCH_TIMEOUT_MS,
58391
58979
  correlationId: input.correlationId
@@ -58399,14 +58987,10 @@ async function maybeRunShadow(input, deps, logger) {
58399
58987
  }
58400
58988
 
58401
58989
  // src/consensus/multi-llm-activate.ts
58402
- var MULTI_LLM_ENV_FLAG = "MRRLIN_CONSENSUS_MULTI_LLM";
58403
58990
  var PICK_TIMEOUT_MS2 = 15e3;
58404
58991
  var DISPATCH_TIMEOUT_MS2 = 6e4;
58405
- function multiLlmActive(env = process.env) {
58406
- if (env["NODE_ENV"] === "production") return false;
58407
- const flag = env[MULTI_LLM_ENV_FLAG];
58408
- if (flag === "0" || flag === "false" || flag === "off") return false;
58409
- return Boolean(flag) || env["MRRLIN_STAGING"] === "1";
58992
+ function multiLlmActive(_env = process.env) {
58993
+ return true;
58410
58994
  }
58411
58995
  async function resolveActiveRunHead(input, deps) {
58412
58996
  if (!multiLlmActive(input.env ?? process.env)) return null;
@@ -58421,6 +59005,7 @@ async function resolveActiveRunHead(input, deps) {
58421
59005
  personaInstructions[name] = "";
58422
59006
  }
58423
59007
  }
59008
+ const seat = await mintResolvedSeat(resolveProvider(process.env, input.persistedProvider));
58424
59009
  const built = await buildMultiHeadRunner(
58425
59010
  {
58426
59011
  personas: heads,
@@ -58429,10 +59014,12 @@ async function resolveActiveRunHead(input, deps) {
58429
59014
  // Codex (["openai"]) and Claude (["anthropic"]) are single-element, so this is a
58430
59015
  // behavior-neutral join; excludeProvider itself is a single (possibly comma-joined,
58431
59016
  // per --exclude-providers) string, not an array.
58432
- excludeProvider: resolveProvider(process.env, input.persistedProvider).capabilities().poolProviderLabels.join(","),
59017
+ excludeProvider: seat.capabilities.poolProviderLabels.join(","),
58433
59018
  tierPrefer: "strong",
58434
59019
  pickTimeoutMs: PICK_TIMEOUT_MS2,
58435
- dispatchTimeoutMs: DISPATCH_TIMEOUT_MS2
59020
+ dispatchTimeoutMs: DISPATCH_TIMEOUT_MS2,
59021
+ // task 1.21: same seat that produced excludeProvider above, not a second mint.
59022
+ seat
58436
59023
  },
58437
59024
  deps
58438
59025
  );
@@ -58640,7 +59227,9 @@ async function runNativeSpecGate(ctx, input, builders = defaultNativeSpecGateBui
58640
59227
  specPageId: input.pageId,
58641
59228
  cwd: ctx.cwd,
58642
59229
  codexExecutable: ctx.codexExecutable,
58643
- extraHeadCount: input.extraHeads?.length ?? 0
59230
+ extraHeadCount: input.extraHeads?.length ?? 0,
59231
+ // model-tier-planning-and-arbiter task 1.21: the seat minted at the async-job claim.
59232
+ seat: ctx.seat
58644
59233
  });
58645
59234
  return builders.runSpecGate(
58646
59235
  { projectSlug: input.projectSlug, specPageId: input.pageId },
@@ -58883,7 +59472,9 @@ async function run(ctx, input, builders = defaultNativeSpecGateBuilders) {
58883
59472
  domain: shadowDomain,
58884
59473
  extraHeads: input.extraHeads,
58885
59474
  correlationId: `Specs/${input.pageId}`,
58886
- persistedProvider: getSelectedProvider()
59475
+ // task 1.21: ctx's own minted seat, not an ambient read — ctx already carries it by
59476
+ // the time `run` executes (async-job-consumer.ts mints before calling descriptor.run).
59477
+ persistedProvider: ctx.seat?.id ?? null
58887
59478
  });
58888
59479
  }
58889
59480
  } catch {
@@ -59079,7 +59670,9 @@ async function run2(ctx, input) {
59079
59670
  projectSlug: input.projectSlug,
59080
59671
  deliverablePageId: input.pageId,
59081
59672
  cwd: ctx.cwd,
59082
- codexExecutable: ctx.codexExecutable
59673
+ codexExecutable: ctx.codexExecutable,
59674
+ // model-tier-planning-and-arbiter task 1.21: the seat minted at the async-job claim.
59675
+ seat: ctx.seat
59083
59676
  });
59084
59677
  const result = await runDeliverableGate(
59085
59678
  { projectSlug: input.projectSlug, deliverablePageId: input.pageId },
@@ -59159,6 +59752,177 @@ var deliverableConsensusDescriptor = {
59159
59752
  wakeDirector: true
59160
59753
  };
59161
59754
 
59755
+ // src/intent-rubric.ts
59756
+ var INTENT_CRITERIA = `Fire only if at least one applies. There is deliberately NO criterion for "I would like more detail" \u2014 that is what produces the flood.
59757
+
59758
+ 1. Unresolved referent \u2014 names an object (site, page, product, metric, element) that does not resolve to exactly one thing.
59759
+ 2. No observable outcome \u2014 no acceptance criterion is derivable; it is not stated what will change in the world.
59760
+ 3. Two incompatible readings \u2014 two materially different end states both fit the text.
59761
+ 4. Missing decisive input \u2014 a value only the operator holds (a URL, access, a choice between vendors, a target number).`;
59762
+ var INTENT_MANUAL_PATH_SCOPE_NOTE = `Scope of criterion 1 on the manual path: the judge receives only the title and notes \u2014 no taxonomy, no existing task titles, no wiki. Criterion 1 is therefore evaluated at face value only: it fires when a referent is unresolvable on its own terms \u2014 a demonstrative with no antecedent ("here", "this page", "\u0442\u0443\u0442"), or an explicitly deferred one ("add foobar (ask Vlad)"). It does NOT fire merely because the judge cannot personally confirm that a named thing exists.`;
59763
+ var INTENT_DECISION_RULES_INTRO = `Each decision rule came from an observed misfire. Each is a required non-firing test case, named rule-1 through rule-5.`;
59764
+ var INTENT_DECISION_RULES = {
59765
+ "rule-1": `A delegated pick is not an unresolved referent. "Zapier" or equivalent, "another low-friction directory" hand the choice to the doer. Criterion 1 does not fire.`,
59766
+ "rule-2": `A decision is a valid outcome. "Define pause-or-monitor-only policy", "decide whether to ship one public repo" \u2014 the binary is the task's output, not a missing precondition. Criterion 2 does not fire.`,
59767
+ "rule-3": `Interchangeable implementation methods are not two readings. Three routes to one stated goal (CloudFront Function / S3 rule / Hugo frontmatter for one 301) do not fire criterion 3. Criterion 3 needs two different end states.`,
59768
+ "rule-4": `A shared execution resource is not a missing decisive input. Needing an account or credentials is common to every task of its kind; firing criterion 4 there fires on all of them.`,
59769
+ "rule-5": `A diagnosable symptom is not an unresolved referent. For a defect report, an unnamed element on a named route is a diagnostic step, not a blocking ambiguity. Criterion 1 does not fire.`
59770
+ };
59771
+ function capQuestions(questions) {
59772
+ return questions.slice(0, 3);
59773
+ }
59774
+
59775
+ // src/async-tools/intent-check.ts
59776
+ var INTENT_CHECK_TOOL_NAME = "intent_check";
59777
+ function intentCheckTimeoutMs() {
59778
+ const parsed = Number(process.env["MRRLIN_INTENT_CHECK_TIMEOUT_MS"] ?? 6e4);
59779
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 6e4;
59780
+ }
59781
+ function assembleJudgeInstruction() {
59782
+ const rulesBlock = [
59783
+ INTENT_DECISION_RULES_INTRO,
59784
+ INTENT_DECISION_RULES["rule-1"],
59785
+ INTENT_DECISION_RULES["rule-2"],
59786
+ INTENT_DECISION_RULES["rule-3"],
59787
+ INTENT_DECISION_RULES["rule-4"],
59788
+ INTENT_DECISION_RULES["rule-5"]
59789
+ ].join("\n");
59790
+ return [
59791
+ `Judge whether this task's intent is understood, using ONLY the four criteria and the five`,
59792
+ `decision rules below. Apply them at face value. Do not add tolerance rules of your own and do`,
59793
+ `not bias toward either verdict.`,
59794
+ ``,
59795
+ INTENT_CRITERIA,
59796
+ ``,
59797
+ INTENT_MANUAL_PATH_SCOPE_NOTE,
59798
+ ``,
59799
+ rulesBlock,
59800
+ ``,
59801
+ `Output exactly:`,
59802
+ `VERDICT: CLEAR | UNCLEAR`,
59803
+ `RESTATEMENT: <one sentence, English. Preserve every specific whose error would change execution`,
59804
+ ` (names, URLs, numbers, scope boundaries); drop everything that would not.>`,
59805
+ `QUESTIONS: <empty if CLEAR. Otherwise 1-3 lines, each prefixed "- ", scope-level only: what the`,
59806
+ ` work is, where it applies, or what counts as done. Never spec-level detail.>`
59807
+ ].join("\n");
59808
+ }
59809
+ function buildJudgePrompt(title, notes) {
59810
+ return `TITLE: ${title}
59811
+ NOTES: ${notes ?? "(none)"}`;
59812
+ }
59813
+ function parseJudgeOutput(text) {
59814
+ const verdictMatch = /VERDICT:\s*\b(CLEAR|UNCLEAR)\b/i.exec(text);
59815
+ if (!verdictMatch) return null;
59816
+ const verdict = (verdictMatch[1] ?? "").toUpperCase();
59817
+ const restatementMatch = /RESTATEMENT:\s*([\s\S]*?)(?=\n\s*(?:VERDICT|QUESTIONS):|$)/i.exec(text);
59818
+ const restatement = restatementMatch?.[1]?.trim();
59819
+ if (!restatement) return null;
59820
+ const questionsMatch = /QUESTIONS:\s*([\s\S]*)/i.exec(text);
59821
+ const questionsBlock = questionsMatch?.[1] ?? "";
59822
+ const questions = capQuestions(
59823
+ questionsBlock.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("-")).map((line) => line.replace(/^-\s*/, "").trim()).filter((line) => line.length > 0)
59824
+ );
59825
+ const result = mrrlinIntentVerdictSchema.safeParse({ verdict, restatement, questions });
59826
+ return result.success ? result.data : null;
59827
+ }
59828
+ async function runIntentCheck(ctx, input) {
59829
+ if (ctx.projectSlug) {
59830
+ let project;
59831
+ try {
59832
+ project = await ctx.client.getProject(ctx.projectSlug);
59833
+ } catch (err) {
59834
+ return {
59835
+ kind: "retry",
59836
+ failureReason: "provider_unavailable",
59837
+ error: `intent check: failed to read project settings: ${err instanceof Error ? err.message : String(err)}`,
59838
+ taskId: input.taskId
59839
+ };
59840
+ }
59841
+ if (project.intentJudgeEnabled === false) {
59842
+ const verdict2 = {
59843
+ verdict: "CLEAR",
59844
+ restatement: "(intent judge disabled for this project)",
59845
+ questions: []
59846
+ };
59847
+ return {
59848
+ kind: "done",
59849
+ resultJson: JSON.stringify(verdict2),
59850
+ finalReviewedHash: null,
59851
+ taskId: input.taskId,
59852
+ taskTitle: input.title
59853
+ };
59854
+ }
59855
+ }
59856
+ const exec = await runAgentExec({
59857
+ prompt: buildJudgePrompt(input.title, input.notes),
59858
+ developerInstructions: assembleJudgeInstruction(),
59859
+ cwd: ctx.cwd,
59860
+ timeoutMs: intentCheckTimeoutMs(),
59861
+ sandbox: "read-only",
59862
+ codexExecutable: ctx.codexExecutable,
59863
+ // model-tier-planning-and-arbiter task 2.6 (D1): the judge is pinned to the seat's strongest
59864
+ // model.
59865
+ model: ctx.seat?.models.strong
59866
+ });
59867
+ if (!exec.ok) {
59868
+ return {
59869
+ kind: "retry",
59870
+ failureReason: "provider_unavailable",
59871
+ error: `intent check failed: ${exec.error}`,
59872
+ taskId: input.taskId
59873
+ };
59874
+ }
59875
+ const verdict = parseJudgeOutput(exec.text);
59876
+ if (!verdict) {
59877
+ return {
59878
+ kind: "failed",
59879
+ failureReason: "zero_output",
59880
+ error: "intent judge produced unparseable output",
59881
+ taskId: input.taskId
59882
+ };
59883
+ }
59884
+ return {
59885
+ kind: "done",
59886
+ resultJson: JSON.stringify(verdict),
59887
+ finalReviewedHash: null,
59888
+ taskId: input.taskId,
59889
+ taskTitle: input.title
59890
+ };
59891
+ }
59892
+ function onComplete4(outcome, input) {
59893
+ if (outcome.kind !== "done") {
59894
+ return { kind: "info", taskId: input.taskId, suppressInbox: true, body: "" };
59895
+ }
59896
+ let verdict = null;
59897
+ try {
59898
+ verdict = mrrlinIntentVerdictSchema.parse(JSON.parse(outcome.resultJson));
59899
+ } catch {
59900
+ return { kind: "info", taskId: input.taskId, suppressInbox: true, body: "" };
59901
+ }
59902
+ if (verdict.verdict === "CLEAR") {
59903
+ return { kind: "info", taskId: input.taskId, suppressInbox: true, body: "" };
59904
+ }
59905
+ const body = [verdict.restatement, "", ...verdict.questions.map((q, i) => `${i + 1}. ${q}`)].join("\n");
59906
+ return {
59907
+ kind: "question",
59908
+ taskId: input.taskId,
59909
+ origin: "gate",
59910
+ questionOptions: null,
59911
+ body
59912
+ };
59913
+ }
59914
+ function createIntentCheckDescriptor() {
59915
+ return {
59916
+ toolName: INTENT_CHECK_TOOL_NAME,
59917
+ dedupKey: (input) => `intent-check:${input.taskId}`,
59918
+ run: runIntentCheck,
59919
+ onComplete: onComplete4,
59920
+ wakeDirector: true,
59921
+ appliesIntentVerdict: true
59922
+ };
59923
+ }
59924
+ var intentCheckDescriptor = createIntentCheckDescriptor();
59925
+
59162
59926
  // src/async-tools/enqueue.ts
59163
59927
  var import_node_crypto10 = __toESM(require("node:crypto"), 1);
59164
59928
  async function enqueueAsyncTool(client, descriptor2, input) {
@@ -59741,6 +60505,7 @@ var COMPACT_OPT_IN = {
59741
60505
  registerAsyncTool(consensusDescriptor);
59742
60506
  registerAsyncTool(deliverableConsensusDescriptor);
59743
60507
  registerAsyncTool(draftSpecDescriptor);
60508
+ registerAsyncTool(intentCheckDescriptor);
59744
60509
  var projectScopedSchema = external_exports.object({ projectSlug: mrrlinProjectSlugSchema });
59745
60510
  var taskScopedSchema = projectScopedSchema.extend({ taskId: mrrlinTaskIdSchema });
59746
60511
  var planScopedSchema = projectScopedSchema.extend({ planId: mrrlinPlanIdSchema });
@@ -61037,7 +61802,7 @@ function readZodObjectShape(schema) {
61037
61802
  if (candidate && typeof candidate === "object") return candidate;
61038
61803
  return null;
61039
61804
  }
61040
- function jsonSchemaPrimitiveType(schema) {
61805
+ function unwrapZodSchema(schema) {
61041
61806
  let current2 = schema;
61042
61807
  for (; ; ) {
61043
61808
  const def = current2._def;
@@ -61045,18 +61810,27 @@ function jsonSchemaPrimitiveType(schema) {
61045
61810
  current2 = def.innerType;
61046
61811
  continue;
61047
61812
  }
61813
+ if (def?.in) {
61814
+ current2 = def.in;
61815
+ continue;
61816
+ }
61048
61817
  if (def?.schema) {
61049
61818
  current2 = def.schema;
61050
61819
  continue;
61051
61820
  }
61052
61821
  break;
61053
61822
  }
61823
+ return current2;
61824
+ }
61825
+ function jsonSchemaPrimitiveType(schema) {
61826
+ const current2 = unwrapZodSchema(schema);
61054
61827
  if (current2 instanceof external_exports.ZodNumber) return "number";
61055
61828
  if (current2 instanceof external_exports.ZodBoolean) return "boolean";
61056
61829
  return "string";
61057
61830
  }
61058
61831
  function jsonSchemaProperty(schema) {
61059
- const property = { type: jsonSchemaPrimitiveType(schema) };
61832
+ const unwrapped = unwrapZodSchema(schema);
61833
+ const property = unwrapped instanceof external_exports.ZodArray ? { items: { type: jsonSchemaPrimitiveType(unwrapped.element) }, type: "array" } : { type: jsonSchemaPrimitiveType(schema) };
61060
61834
  let jsonSchema = {};
61061
61835
  try {
61062
61836
  jsonSchema = external_exports.toJSONSchema(schema);
@@ -61308,13 +62082,13 @@ async function uninstallPrompts(codexHome, names) {
61308
62082
 
61309
62083
  // src/bin.ts
61310
62084
  function pm2Runner(args) {
61311
- const r = (0, import_node_child_process17.spawnSync)("pm2", args, { encoding: "utf8" });
62085
+ const r = (0, import_node_child_process18.spawnSync)("pm2", args, { encoding: "utf8" });
61312
62086
  process.stderr.write(r.stdout ?? "");
61313
62087
  process.stderr.write(r.stderr ?? "");
61314
62088
  return { code: r.status ?? 1, stdout: r.stdout ?? "" };
61315
62089
  }
61316
62090
  function whichBin(bin) {
61317
- const r = (0, import_node_child_process17.spawnSync)("which", [bin], { encoding: "utf8" });
62091
+ const r = (0, import_node_child_process18.spawnSync)("which", [bin], { encoding: "utf8" });
61318
62092
  const out = (r.stdout ?? "").trim();
61319
62093
  return r.status === 0 && out ? out.split("\n")[0] ?? null : null;
61320
62094
  }
@@ -61499,13 +62273,13 @@ async function main() {
61499
62273
  }
61500
62274
  case "browser-serve": {
61501
62275
  const { command: command2, args, env } = browserServeArgs();
61502
- const r = (0, import_node_child_process17.spawnSync)(command2, args, { stdio: "inherit", env: { ...process.env, ...env } });
62276
+ const r = (0, import_node_child_process18.spawnSync)(command2, args, { stdio: "inherit", env: { ...process.env, ...env } });
61503
62277
  process.exitCode = r.status ?? 1;
61504
62278
  return;
61505
62279
  }
61506
62280
  case "external-agents-serve": {
61507
62281
  const { command: command2, args } = externalAgentsServeArgs();
61508
- const r = (0, import_node_child_process17.spawnSync)(command2, args, { stdio: "inherit", env: process.env });
62282
+ const r = (0, import_node_child_process18.spawnSync)(command2, args, { stdio: "inherit", env: process.env });
61509
62283
  process.exitCode = r.status ?? 1;
61510
62284
  return;
61511
62285
  }
@@ -61573,7 +62347,7 @@ async function main() {
61573
62347
  );
61574
62348
  }
61575
62349
  const realProviderRunner = (command2) => (args) => {
61576
- const r = (0, import_node_child_process17.spawnSync)(command2, args, { encoding: "utf8", timeout: 8e3 });
62350
+ const r = (0, import_node_child_process18.spawnSync)(command2, args, { encoding: "utf8", timeout: 8e3 });
61577
62351
  return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
61578
62352
  };
61579
62353
  for (const provider of ["codex", "claude"]) {