@mrrlin-dev/mcp 0.3.54 → 0.3.56

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 +730 -264
  2. package/package.json +2 -2
package/dist/bin.cjs CHANGED
@@ -21153,11 +21153,11 @@ ${block}`;
21153
21153
  }
21154
21154
 
21155
21155
  // src/director-bridge.ts
21156
- var import_node_fs14 = __toESM(require("node:fs"), 1);
21156
+ var import_node_fs15 = __toESM(require("node:fs"), 1);
21157
21157
  var import_node_http = __toESM(require("node:http"), 1);
21158
21158
  var import_node_os9 = __toESM(require("node:os"), 1);
21159
21159
  var import_node_path18 = __toESM(require("node:path"), 1);
21160
- var import_node_child_process10 = require("node:child_process");
21160
+ var import_node_child_process11 = require("node:child_process");
21161
21161
  var import_node_crypto8 = __toESM(require("node:crypto"), 1);
21162
21162
 
21163
21163
  // src/turn-replay-buffer.ts
@@ -21289,11 +21289,25 @@ function createSuspensionDetector(opts) {
21289
21289
  const t = now();
21290
21290
  const gap = t - last;
21291
21291
  last = t;
21292
- if (gap >= gapMs) suspended += gap - tickMs;
21292
+ if (gap >= gapMs) {
21293
+ suspended += gap - tickMs;
21294
+ }
21293
21295
  }, tickMs);
21294
21296
  timer.unref?.();
21297
+ const settle = () => {
21298
+ const t = now();
21299
+ const gap = t - last;
21300
+ if (gap >= gapMs) {
21301
+ suspended += gap - tickMs;
21302
+ last = t;
21303
+ }
21304
+ };
21295
21305
  return {
21296
- suspendedMs: () => suspended,
21306
+ suspendedMs: () => {
21307
+ settle();
21308
+ return suspended;
21309
+ },
21310
+ settle,
21297
21311
  dispose: () => clearI(timer)
21298
21312
  };
21299
21313
  }
@@ -21368,20 +21382,22 @@ function transition(state, event) {
21368
21382
 
21369
21383
  // src/bridge-tombstone.ts
21370
21384
  var DEFAULT_TOMBSTONE_TTL_MS = 36e5;
21371
- function writeTombstone(tombstones, directorSessionId, buf, now) {
21385
+ function writeTombstone(tombstones, directorSessionId, buf, now, suspendedMsNow) {
21372
21386
  tombstones.set(directorSessionId, {
21373
21387
  turnId: buf.turnId,
21374
21388
  lastSeq: buf.lastSeq,
21375
- expiredAt: now
21389
+ expiredAt: now,
21390
+ expiredSuspendedMs: suspendedMsNow
21376
21391
  });
21377
21392
  }
21378
21393
  function lookupTombstone(tombstones, directorSessionId) {
21379
21394
  return tombstones.get(directorSessionId) ?? null;
21380
21395
  }
21381
- function sweepExpiredTombstones(tombstones, now, ttlMs = DEFAULT_TOMBSTONE_TTL_MS) {
21396
+ function sweepExpiredTombstones(tombstones, now, ttlMs = DEFAULT_TOMBSTONE_TTL_MS, suspendedMsNow) {
21382
21397
  let removed = 0;
21383
21398
  for (const [sid, t] of tombstones) {
21384
- if (now - t.expiredAt > ttlMs) {
21399
+ const suspendedSinceWrite = Math.max(0, suspendedMsNow - t.expiredSuspendedMs);
21400
+ if (now - t.expiredAt - suspendedSinceWrite > ttlMs) {
21385
21401
  tombstones.delete(sid);
21386
21402
  removed += 1;
21387
21403
  }
@@ -36479,6 +36495,7 @@ var openApiOperationIds = {
36479
36495
  requeueAsyncJob: "requeueAsyncJob",
36480
36496
  createGithubBranch: "createGithubBranch",
36481
36497
  createGithubPullRequest: "createGithubPullRequest",
36498
+ getGithubPullRequest: "getGithubPullRequest",
36482
36499
  getGithubWorkflowRunLatest: "getGithubWorkflowRunLatest",
36483
36500
  getGithubLatestDeployment: "getGithubLatestDeployment",
36484
36501
  mintGithubPushToken: "mintGithubPushToken",
@@ -36506,6 +36523,8 @@ var openApiOperationIds = {
36506
36523
  getProject: "getProject",
36507
36524
  getProjectSnapshot: "getProjectSnapshot",
36508
36525
  getTask: "getTask",
36526
+ archiveTask: "archiveTask",
36527
+ unarchiveTask: "unarchiveTask",
36509
36528
  getWikiPage: "getWikiPage",
36510
36529
  handleGithubInstallCallback: "handleGithubInstallCallback",
36511
36530
  listExecutionArtifacts: "listExecutionArtifacts",
@@ -36538,7 +36557,9 @@ var openApiOperationIds = {
36538
36557
  markSpecReady: "markSpecReady",
36539
36558
  resetDirectorThread: "resetDirectorThread",
36540
36559
  claimExecutionRun: "claimExecutionRun",
36560
+ reclaimExecutionRunLease: "reclaimExecutionRunLease",
36541
36561
  requeueExecutionRun: "requeueExecutionRun",
36562
+ restartExecutionRun: "restartExecutionRun",
36542
36563
  resolveHandoff: "resolveHandoff",
36543
36564
  touchExecutionRun: "touchExecutionRun",
36544
36565
  issueAgentCredential: "issueAgentCredential",
@@ -36556,6 +36577,7 @@ var openApiOperationIds = {
36556
36577
  listArtifactFiles: "listArtifactFiles",
36557
36578
  getOperatorSettings: "getOperatorSettings",
36558
36579
  updateOperatorSettings: "updateOperatorSettings",
36580
+ recordOperatorPresence: "recordOperatorPresence",
36559
36581
  getAgentPool: "getAgentPool",
36560
36582
  putAgentPool: "putAgentPool",
36561
36583
  getConsensusConfig: "getConsensusConfig",
@@ -36600,6 +36622,8 @@ var mrrlinOperatorSettingsSchema = external_exports.object({
36600
36622
  // to it (the web active-project selection is otherwise only a browser cookie). Null = not
36601
36623
  // selected yet.
36602
36624
  activeProjectSlug: external_exports.string().min(1).nullable(),
36625
+ // The last time an authenticated BROWSER operator session was observed. Null means never seen.
36626
+ lastPresenceAt: external_exports.string().datetime().nullable(),
36603
36627
  createdAt: external_exports.string().datetime(),
36604
36628
  updatedAt: external_exports.string().datetime()
36605
36629
  });
@@ -36611,6 +36635,7 @@ var mrrlinOperatorSettingsUpdateSchema = external_exports.object({
36611
36635
  message: "OperatorSettingsUpdate requires at least one field"
36612
36636
  });
36613
36637
  var operatorSettingsResponseSchema = external_exports.object({ data: mrrlinOperatorSettingsSchema });
36638
+ var operatorPresenceResponseSchema = external_exports.object({ data: external_exports.object({ lastPresenceAt: external_exports.string().datetime() }) });
36614
36639
  var mrrlinConsensusConfigSchema = external_exports.object({ consensusEnabled: external_exports.boolean() });
36615
36640
  var consensusConfigResponseSchema = external_exports.object({ data: mrrlinConsensusConfigSchema });
36616
36641
  var agentPoolSnapshotSchema = external_exports.object({
@@ -36748,7 +36773,13 @@ var mrrlinTaskDomainSchema = external_exports.enum(["code", "seo", "marketing",
36748
36773
  var mrrlinSpecApprovedSchema = external_exports.enum(["pending", "approved", "skipped", "skipped_legacy"]);
36749
36774
  var mrrlinTaskSourceSchema = external_exports.enum(["fixture-import", "api"]);
36750
36775
  var mrrlinPlanStatusSchema = external_exports.enum(["active", "paused", "done", "archived"]);
36751
- var mrrlinTaskEventTypeSchema = external_exports.enum(["autonomy_set", "director_thread_reset", "note"]);
36776
+ var mrrlinTaskEventTypeSchema = external_exports.enum([
36777
+ "autonomy_set",
36778
+ "director_thread_reset",
36779
+ "note",
36780
+ "archived",
36781
+ "restored"
36782
+ ]);
36752
36783
  var mrrlinTaskEventCreateKindSchema = external_exports.enum(["note"]);
36753
36784
  var githubRepositorySelectionSchema = external_exports.enum(["all", "selected"]);
36754
36785
  var mrrlinTaskIdSchema = external_exports.string().regex(/^GT-\d+$/);
@@ -36812,6 +36843,14 @@ var mrrlinExecutionRunStatusSchema = external_exports.enum([
36812
36843
  "handoff_queued",
36813
36844
  "cancelled"
36814
36845
  ]);
36846
+ var mrrlinRunBlockingReasonCodeSchema = external_exports.enum([
36847
+ "local_checkout_missing",
36848
+ "task_blocked",
36849
+ "default_branch_unresolved",
36850
+ "execution_turn_timeout",
36851
+ "deliverable_gate_exhausted",
36852
+ "unknown"
36853
+ ]);
36815
36854
  var mrrlinExecutionArtifactKindSchema = external_exports.enum([
36816
36855
  "plan",
36817
36856
  "checkpoint",
@@ -36826,7 +36865,8 @@ var mrrlinExecutionArtifactKindSchema = external_exports.enum([
36826
36865
  "verification",
36827
36866
  "produced_url",
36828
36867
  "resume_spec",
36829
- "deliverable"
36868
+ "deliverable",
36869
+ "pull_request_state"
36830
36870
  ]);
36831
36871
  var mrrlinDeployAttemptPayloadSchema = external_exports.object({
36832
36872
  env: external_exports.enum(["dev", "prod"]),
@@ -36862,6 +36902,9 @@ var taxonomyCategorySchema = external_exports.string().min(1).regex(/^[a-z][a-z0
36862
36902
  var taxonomySubcategorySchema = external_exports.string().min(1).regex(/^[a-z][a-z0-9-]*$/);
36863
36903
  var mrrlinTaskAssigneeSchema = external_exports.union([external_exports.string().regex(/^@/), external_exports.string().email()]).nullable();
36864
36904
  var mrrlinTaskSchema = external_exports.object({
36905
+ // Output-optional (not defaulted-required) so existing task fixtures/mocks across the
36906
+ // codebase need no churn; DB rows always resolve it to `string | null` via taskRowToTask.
36907
+ archivedAt: external_exports.string().datetime().nullable().optional(),
36865
36908
  assignee: mrrlinTaskAssigneeSchema,
36866
36909
  autoDeploy: mrrlinAutoDeploySchema,
36867
36910
  // ADR 0020. OUTPUT-optional (not defaulted-required) so hardcoded task fixtures/mocks across the
@@ -36976,6 +37019,27 @@ var mrrlinGithubPullRequestSchema = external_exports.object({
36976
37019
  title: external_exports.string().min(1),
36977
37020
  url: external_exports.string().url()
36978
37021
  });
37022
+ var mrrlinGithubPullRequestCheckStateSchema = external_exports.enum([
37023
+ "passing",
37024
+ "failing",
37025
+ "running",
37026
+ "none",
37027
+ "unknown"
37028
+ ]);
37029
+ var mrrlinGithubPullRequestCheckSourceSchema = external_exports.enum(["check_run", "commit_status"]);
37030
+ var mrrlinGithubPullRequestObservedSchema = external_exports.object({
37031
+ checks: mrrlinGithubPullRequestCheckStateSchema,
37032
+ failingChecks: external_exports.array(external_exports.object({
37033
+ name: external_exports.string().min(1),
37034
+ source: mrrlinGithubPullRequestCheckSourceSchema
37035
+ })),
37036
+ headSha: external_exports.string().min(1),
37037
+ merged: external_exports.boolean(),
37038
+ mergedAt: external_exports.string().datetime().nullable(),
37039
+ number: external_exports.number().int().positive(),
37040
+ state: external_exports.string().min(1),
37041
+ url: external_exports.string().url()
37042
+ });
36979
37043
  var mrrlinGithubWorkflowSchema = external_exports.object({
36980
37044
  id: external_exports.union([external_exports.number().int().positive(), external_exports.string().min(1)]),
36981
37045
  name: external_exports.string().min(1),
@@ -37109,6 +37173,7 @@ var mrrlinTaskUpdateSchema = external_exports.object({
37109
37173
  }
37110
37174
  });
37111
37175
  var mrrlinTaskListFiltersSchema = external_exports.object({
37176
+ archived: external_exports.enum(["true", "false", "all"]).optional(),
37112
37177
  assignee: external_exports.string().startsWith("@").optional(),
37113
37178
  autoDeploy: mrrlinAutoDeploySchema.optional(),
37114
37179
  autonomyLevel: mrrlinAutonomyLevelSchema.optional(),
@@ -37271,6 +37336,9 @@ var githubCreatePullRequestRequestSchema = external_exports.object({
37271
37336
  var githubCreatePullRequestResponseSchema = external_exports.object({
37272
37337
  data: mrrlinGithubPullRequestSchema
37273
37338
  });
37339
+ var githubPullRequestNumberParamSchema = external_exports.object({
37340
+ pullNumber: external_exports.coerce.number().int().positive()
37341
+ });
37274
37342
  var githubWorkflowIdParamSchema = external_exports.object({
37275
37343
  workflowId: external_exports.string().min(1)
37276
37344
  });
@@ -37284,6 +37352,9 @@ var githubWorkflowDispatchResponseSchema = external_exports.object({
37284
37352
  var githubWorkflowListResponseSchema = external_exports.object({
37285
37353
  data: external_exports.array(mrrlinGithubWorkflowSchema)
37286
37354
  });
37355
+ var githubPullRequestResponseSchema = external_exports.object({
37356
+ data: mrrlinGithubPullRequestObservedSchema
37357
+ });
37287
37358
  var githubWorkflowRunQuerySchema = external_exports.object({
37288
37359
  workflowId: external_exports.string().min(1),
37289
37360
  branch: external_exports.string().min(1),
@@ -37384,6 +37455,7 @@ var mrrlinExecutionRunSchema = external_exports.object({
37384
37455
  actor: external_exports.string().min(1),
37385
37456
  baseSha: external_exports.string().min(1).nullable(),
37386
37457
  branch: external_exports.string().min(1).nullable(),
37458
+ blockingReason: mrrlinRunBlockingReasonCodeSchema.nullable(),
37387
37459
  checkpointCursor: external_exports.string().min(1).nullable(),
37388
37460
  claimedAt: external_exports.string().datetime().nullable(),
37389
37461
  codeExecution: external_exports.boolean(),
@@ -37398,6 +37470,7 @@ var mrrlinExecutionRunSchema = external_exports.object({
37398
37470
  lastActor: external_exports.string().min(1).nullable(),
37399
37471
  leaseId: external_exports.string().min(1).nullable(),
37400
37472
  prerequisites: external_exports.array(mrrlinRunPrerequisiteSchema),
37473
+ prState: external_exports.enum(["open", "merged", "closed"]).optional(),
37401
37474
  prodEnvUrl: external_exports.string().min(1).nullable(),
37402
37475
  prodRunUrl: external_exports.string().min(1).nullable(),
37403
37476
  prodWorkflowId: external_exports.number().int().positive().nullable(),
@@ -37427,6 +37500,7 @@ var mrrlinExecutionRunCreateSchema = external_exports.object({
37427
37500
  var mrrlinExecutionRunUpdateSchema = external_exports.object({
37428
37501
  baseSha: external_exports.string().min(1).nullable().optional(),
37429
37502
  branch: external_exports.string().min(1).nullable().optional(),
37503
+ blockingReason: mrrlinRunBlockingReasonCodeSchema.nullable().optional(),
37430
37504
  checkpointCursor: external_exports.string().min(1).nullable().optional(),
37431
37505
  devEnvUrl: external_exports.string().min(1).nullable().optional(),
37432
37506
  devRunUrl: external_exports.string().min(1).nullable().optional(),
@@ -37453,6 +37527,7 @@ var mrrlinResumeSpecPayloadSchema = external_exports.object({
37453
37527
  class: external_exports.literal("tool_assisted"),
37454
37528
  question: external_exports.string().min(1),
37455
37529
  prerequisites: external_exports.array(external_exports.object({ mcp: external_exports.string().min(1), reason: external_exports.string().min(1) })).default([]),
37530
+ reasonCode: mrrlinRunBlockingReasonCodeSchema.optional(),
37456
37531
  steps: external_exports.array(external_exports.string().min(1)).min(1),
37457
37532
  runId: mrrlinExecutionRunIdSchema,
37458
37533
  artifactId: mrrlinExecutionArtifactIdSchema,
@@ -37482,7 +37557,13 @@ var mrrlinResolveHandoffSchema = external_exports.object({
37482
37557
  }
37483
37558
  });
37484
37559
  var resolveHandoffResponseSchema = external_exports.object({ data: mrrlinExecutionRunSchema });
37485
- var createExecutionRunHandoffSchema = external_exports.object({ reason: external_exports.string().min(1) });
37560
+ var createExecutionRunHandoffSchema = external_exports.object({
37561
+ reason: external_exports.string().min(1),
37562
+ reasonCode: mrrlinRunBlockingReasonCodeSchema.optional()
37563
+ });
37564
+ var mrrlinRestartExecutionRunSchema = external_exports.object({
37565
+ restartedBy: external_exports.string().min(1)
37566
+ });
37486
37567
  var mrrlinExecutionRunTouchSchema = external_exports.object({
37487
37568
  leaseId: external_exports.string().min(1)
37488
37569
  });
@@ -37829,7 +37910,11 @@ var schedulerPendingTasksResponseSchema = external_exports.object({
37829
37910
  // disclosure that some exist, so a misconfigured `github.operator` (e.g. it doesn't match the
37830
37911
  // person actually running the project) is visible as a large number instead of the autopilot
37831
37912
  // silently doing nothing.
37832
- excludedByOwnershipCount: external_exports.number().int().min(0)
37913
+ excludedByOwnershipCount: external_exports.number().int().min(0),
37914
+ // Additive: true when the operator-web-presence gate is the reason nothing new is starting
37915
+ // (autonomyPaused takes precedence when both apply). Computed by the same shared helper the
37916
+ // tick uses, so this page can never claim a task is starting when the tick would decline it.
37917
+ presenceGated: external_exports.boolean()
37833
37918
  })
37834
37919
  });
37835
37920
  var wakeDirectorRequestSchema = external_exports.object({
@@ -38343,6 +38428,10 @@ function createMrrlinClient(config2) {
38343
38428
  const body = await request(`${projectPath(projectSlug)}/github/workflow-runs/latest?${params.toString()}`);
38344
38429
  return githubWorkflowRunResponseSchema.parse(body).data;
38345
38430
  },
38431
+ async getGithubPullRequest(projectSlug, pullNumber) {
38432
+ const body = await request(`${projectPath(projectSlug)}/github/pull-requests/${encodeURIComponent(String(pullNumber))}`);
38433
+ return githubPullRequestResponseSchema.parse(body).data;
38434
+ },
38346
38435
  async getGithubLatestDeployment(projectSlug, query) {
38347
38436
  const params = new URLSearchParams({ environment: query.environment });
38348
38437
  const body = await request(`${projectPath(projectSlug)}/github/deployments/latest?${params.toString()}`);
@@ -38386,6 +38475,7 @@ function createMrrlinClient(config2) {
38386
38475
  },
38387
38476
  async listTasks(projectSlug, filters = {}) {
38388
38477
  const path19 = appendQuery(`${projectPath(projectSlug)}/tasks`, {
38478
+ archived: filters.archived,
38389
38479
  assignee: filters.assignee,
38390
38480
  autonomyLevel: filters.autonomyLevel,
38391
38481
  blockedBy: filters.blockedBy,
@@ -38443,6 +38533,18 @@ function createMrrlinClient(config2) {
38443
38533
  const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/director-thread/reset`, { method: "POST" });
38444
38534
  return taskResponseSchema.parse(body).data;
38445
38535
  },
38536
+ async archiveTask(projectSlug, taskId) {
38537
+ const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/archive`, {
38538
+ method: "POST"
38539
+ });
38540
+ return taskResponseSchema.parse(body).data;
38541
+ },
38542
+ async unarchiveTask(projectSlug, taskId) {
38543
+ const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/unarchive`, {
38544
+ method: "POST"
38545
+ });
38546
+ return taskResponseSchema.parse(body).data;
38547
+ },
38446
38548
  async updateWikiPage(projectSlug, pageId, patch) {
38447
38549
  const body = await request(`${projectPath(projectSlug)}/wiki/${encodeURIComponent(pageId)}`, {
38448
38550
  body: JSON.stringify(patch),
@@ -38663,7 +38765,7 @@ function createMrrlinClient(config2) {
38663
38765
  async requeueExecutionRun(projectSlug, runId, staleSeconds) {
38664
38766
  try {
38665
38767
  const body = await request(`${projectPath(projectSlug)}/execution-runs/${encodeURIComponent(runId)}/requeue`, {
38666
- body: JSON.stringify({ staleSeconds }),
38768
+ body: JSON.stringify(mrrlinExecutionRunRequeueSchema.parse({ staleSeconds })),
38667
38769
  headers: { "content-type": "application/json" },
38668
38770
  method: "POST"
38669
38771
  });
@@ -38674,6 +38776,28 @@ function createMrrlinClient(config2) {
38674
38776
  throw error51;
38675
38777
  }
38676
38778
  },
38779
+ async reclaimExecutionRunLease(projectSlug, runId, staleSeconds) {
38780
+ try {
38781
+ const body = await request(`${projectPath(projectSlug)}/execution-runs/${encodeURIComponent(runId)}/reclaim-lease`, {
38782
+ body: JSON.stringify(mrrlinExecutionRunRequeueSchema.parse({ staleSeconds })),
38783
+ headers: { "content-type": "application/json" },
38784
+ method: "POST"
38785
+ });
38786
+ return executionRunResponseSchema.parse(body).data;
38787
+ } catch (error51) {
38788
+ if (error51 instanceof MrrlinApiError && error51.status === 409 && error51.code === "RUN_LEASE_NOT_STALE")
38789
+ return null;
38790
+ throw error51;
38791
+ }
38792
+ },
38793
+ async restartExecutionRun(projectSlug, runId, input) {
38794
+ const body = await request(`${projectPath(projectSlug)}/execution-runs/${encodeURIComponent(runId)}/restart`, {
38795
+ body: JSON.stringify(mrrlinRestartExecutionRunSchema.parse(input)),
38796
+ headers: { "content-type": "application/json" },
38797
+ method: "POST"
38798
+ });
38799
+ return executionRunResponseSchema.parse(body).data;
38800
+ },
38677
38801
  async touchExecutionRun(projectSlug, runId, leaseId) {
38678
38802
  try {
38679
38803
  const body = await request(`${projectPath(projectSlug)}/execution-runs/${encodeURIComponent(runId)}/touch`, {
@@ -38808,6 +38932,10 @@ function createMrrlinClient(config2) {
38808
38932
  });
38809
38933
  return operatorSettingsResponseSchema.parse(body).data;
38810
38934
  },
38935
+ async recordOperatorPresence() {
38936
+ const body = await request(`/operator/presence`, { method: "POST" });
38937
+ return operatorPresenceResponseSchema.parse(body).data;
38938
+ },
38811
38939
  async getOperatorAgentPool() {
38812
38940
  const body = await request(`/operator/agent-pool`);
38813
38941
  return agentPoolResponseSchema.parse(body).data;
@@ -39322,7 +39450,7 @@ function startCodexProbe(deps = {}) {
39322
39450
  }
39323
39451
 
39324
39452
  // src/_generated/version.ts
39325
- var PKG_VERSION = "0.3.54";
39453
+ var PKG_VERSION = "0.3.56";
39326
39454
 
39327
39455
  // src/api-base-url-migrate.ts
39328
39456
  var import_node_fs2 = __toESM(require("node:fs"), 1);
@@ -42149,6 +42277,7 @@ ${lines2.join("\n")}`;
42149
42277
 
42150
42278
  // src/execution-consumer.ts
42151
42279
  var DEFAULT_STALE_SECONDS = 300;
42280
+ var DEFAULT_MAX_LEASE_RECLAIMS = 3;
42152
42281
  var DEFAULT_TOUCH_INTERVAL_MS = 6e4;
42153
42282
  var DEFAULT_CLAIM_INTERVAL_SECONDS = 30;
42154
42283
  var DEFAULT_EXEC_TURN_IDLE_TIMEOUT_SECONDS = 600;
@@ -42239,8 +42368,8 @@ function buildCompletionTurnPrompt(taskId, currentStatus) {
42239
42368
  "If the work is NOT complete \u2014 be honest, do not close incomplete work:",
42240
42369
  "1. Call update_task with the status that matches why it is not complete, and explain what remains",
42241
42370
  " in the notes:",
42242
- " - if the code work itself is done and only a PR or merge remains, use status=awaiting_verification;",
42243
- " - if you are blocked and need an operator decision before the work can continue, use status=needs_human.",
42371
+ " - if the code work itself is done and only an open PR still awaits operator merge or follow-up, use status=awaiting_verification;",
42372
+ " - if you are blocked and need an operator decision before the work can continue, use status=handoff_needed.",
42244
42373
  "2. Append a handoff_judgement artifact explaining what the operator needs to do.",
42245
42374
  "",
42246
42375
  "You must pick one of these paths \u2014 do not leave the task in_progress."
@@ -42264,9 +42393,9 @@ var BASE_INSTRUCTIONS_PREFIX = [
42264
42393
  "- For a CODE task the run opens a draft PR for you after your turn; opening that PR is NOT",
42265
42394
  " completion. Do NOT set closed_done for code work \u2014 leave the task open; the run parks at",
42266
42395
  " `pull_request` (waiting for merge) and the operator merges. Closure happens after merge.",
42267
- "- Pick the right open status when you stop a CODE task: if the code work itself is done and only a",
42268
- " PR or merge remains, leave the task at `awaiting_verification`; if you are blocked and a human",
42269
- " must decide before the work can continue, leave it at `needs_human`.",
42396
+ "- Pick the right open status when you stop a CODE task: if the code work itself is done and only an",
42397
+ " already-open PR still awaits operator merge or follow-up, leave the task at `awaiting_verification`;",
42398
+ " if you are blocked and a human must decide before the work can continue, leave it at `handoff_needed`.",
42270
42399
  "",
42271
42400
  "File artifacts:",
42272
42401
  "- When your output is a file (markdown report, HTML, screenshot), upload it with",
@@ -42512,14 +42641,19 @@ async function assertRunCompletionReady(deps, projectSlug, taskId, runId, allowe
42512
42641
  task
42513
42642
  };
42514
42643
  }
42515
- const artifacts = await deps.client.listExecutionArtifacts(projectSlug, runId);
42516
- const hasEvidence = artifacts.some(
42517
- (artifact) => COMPLETION_EVIDENCE_KINDS.has(artifact.kind) && !isBridgeGeneratedArtifact(artifact)
42644
+ const runs = await deps.client.listExecutionRuns(projectSlug, { taskId });
42645
+ const runIds = new Set(runs.map((run3) => run3.id));
42646
+ runIds.add(runId);
42647
+ const artifactsNested = await Promise.all(
42648
+ [...runIds].map(async (candidateRunId) => deps.client.listExecutionArtifacts(projectSlug, candidateRunId))
42649
+ );
42650
+ const hasEvidence = artifactsNested.flat().some(
42651
+ (artifact) => COMPLETION_EVIDENCE_KINDS.has(artifact.kind) && !isBridgeGeneratedArtifact(artifact) && runIds.has(artifact.runId)
42518
42652
  );
42519
42653
  if (!hasEvidence) {
42520
42654
  return {
42521
42655
  ok: false,
42522
- reason: `Codex turn completed and task ${task.id} closed, but run ${runId} has no durable completion evidence artifact.`,
42656
+ reason: `Codex turn completed and task ${task.id} closed, but task lineage for run ${runId} has no durable completion evidence artifact.`,
42523
42657
  task
42524
42658
  };
42525
42659
  }
@@ -42662,7 +42796,8 @@ async function driveRun(deps, projectSlug, runId) {
42662
42796
  task,
42663
42797
  runId,
42664
42798
  leaseId,
42665
- `task_blocked: run ${runId} targets ${task.id} but blocker tasks could not be loaded: ${missingBlockers.join(", ")}`
42799
+ `task_blocked: run ${runId} targets ${task.id} but blocker tasks could not be loaded: ${missingBlockers.join(", ")}`,
42800
+ "task_blocked"
42666
42801
  );
42667
42802
  return;
42668
42803
  }
@@ -42674,7 +42809,8 @@ async function driveRun(deps, projectSlug, runId) {
42674
42809
  task,
42675
42810
  runId,
42676
42811
  leaseId,
42677
- `task_blocked: run ${runId} targets ${task.id} but active blockers remain open: ${activeBlockers.join(", ")}`
42812
+ `task_blocked: run ${runId} targets ${task.id} but active blockers remain open: ${activeBlockers.join(", ")}`,
42813
+ "task_blocked"
42678
42814
  );
42679
42815
  return;
42680
42816
  }
@@ -42711,7 +42847,7 @@ async function driveRun(deps, projectSlug, runId) {
42711
42847
  2. If you find a likely match, tell the operator where you think it is and ask them to confirm \u2014 e.g. "For code changes I need this project's repo checked out here. I think it's at <path> \u2014 should I register that one, or do you want to point me at a different path?"
42712
42848
  3. When the operator confirms a path, call register_local_checkout({ projectSlug: "${projectSlug}", path: "<absolute path>", repo: "<this project's GitHub repo>" }).
42713
42849
  4. If no candidate is found, ask the operator for the absolute path to the checkout (or to clone the repo first), then register it. Once a checkout is registered, the run can be re-queued.`;
42714
- await failRun(deps, projectSlug, task, runId, leaseId, reason);
42850
+ await failRun(deps, projectSlug, task, runId, leaseId, reason, "local_checkout_missing");
42715
42851
  return;
42716
42852
  }
42717
42853
  try {
@@ -42745,7 +42881,15 @@ async function driveRun(deps, projectSlug, runId) {
42745
42881
  } catch (error51) {
42746
42882
  const detail = error51 instanceof Error ? error51.message : String(error51);
42747
42883
  const reason = `worktree_setup_failed: run ${runId} could not provision a code-execution worktree for project ${projectSlug}: ${detail}. (A missing default branch surfaces here as default_branch_unresolved; the operator must resolve the repo state before re-queuing.)`;
42748
- await failRun(deps, projectSlug, task, runId, leaseId, reason);
42884
+ await failRun(
42885
+ deps,
42886
+ projectSlug,
42887
+ task,
42888
+ runId,
42889
+ leaseId,
42890
+ reason,
42891
+ detail.includes("default_branch_unresolved") ? "default_branch_unresolved" : void 0
42892
+ );
42749
42893
  return;
42750
42894
  }
42751
42895
  } else {
@@ -42784,7 +42928,15 @@ async function driveRun(deps, projectSlug, runId) {
42784
42928
  if (!resumed) {
42785
42929
  const primingBudgetMs = remainingTurnBudgetMs();
42786
42930
  if (primingBudgetMs <= 0) {
42787
- await failRun(deps, projectSlug, task, runId, leaseId, executionTurnBudgetMessage(execRunMaxMs()));
42931
+ await failRun(
42932
+ deps,
42933
+ projectSlug,
42934
+ task,
42935
+ runId,
42936
+ leaseId,
42937
+ executionTurnBudgetMessage(execRunMaxMs()),
42938
+ "execution_turn_timeout"
42939
+ );
42788
42940
  return;
42789
42941
  }
42790
42942
  const primed = await driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, bundle, primingBudgetMs, "priming");
@@ -42799,7 +42951,15 @@ async function driveRun(deps, projectSlug, runId) {
42799
42951
  Continue executing task ${task.id}. Make the next concrete increment of progress and record artifacts/judgements via MCP tools.` : `Continue executing task ${task.id}. Make the next concrete increment of progress and record artifacts/judgements via MCP tools.`;
42800
42952
  const workBudgetMs = remainingTurnBudgetMs();
42801
42953
  if (workBudgetMs <= 0) {
42802
- await failRun(deps, projectSlug, task, runId, leaseId, executionTurnBudgetMessage(execRunMaxMs()));
42954
+ await failRun(
42955
+ deps,
42956
+ projectSlug,
42957
+ task,
42958
+ runId,
42959
+ leaseId,
42960
+ executionTurnBudgetMessage(execRunMaxMs()),
42961
+ "execution_turn_timeout"
42962
+ );
42803
42963
  return;
42804
42964
  }
42805
42965
  const outcome = await driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, executionPrompt, workBudgetMs, "work");
@@ -42966,6 +43126,13 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
42966
43126
  pullRequestUrl: pr.url,
42967
43127
  status: "dev_deploy"
42968
43128
  });
43129
+ await reconcileTaskAwaitingMerge(
43130
+ deps,
43131
+ projectSlug,
43132
+ readiness.task,
43133
+ runId,
43134
+ `PR opened (${pr.url}); deploy gate is running, but the operator still owns the merge decision.`
43135
+ );
42969
43136
  } else {
42970
43137
  await deps.client.updateExecutionRun(projectSlug, runId, {
42971
43138
  leaseId,
@@ -43221,7 +43388,7 @@ async function resetWedgedThread(deps, projectSlug, taskId, interruptFailed) {
43221
43388
  await deps.client.resetDirectorThread(projectSlug, taskId).catch(() => {
43222
43389
  });
43223
43390
  }
43224
- async function failRun(deps, projectSlug, task, runId, leaseId, reason) {
43391
+ async function failRun(deps, projectSlug, task, runId, leaseId, reason, reasonCode) {
43225
43392
  let currentRun;
43226
43393
  try {
43227
43394
  currentRun = await deps.client.getExecutionRun(projectSlug, runId);
@@ -43280,13 +43447,21 @@ async function failRun(deps, projectSlug, task, runId, leaseId, reason) {
43280
43447
  }
43281
43448
  }
43282
43449
  try {
43283
- await deps.client.createExecutionRunHandoff(projectSlug, runId, { reason });
43450
+ await deps.client.createExecutionRunHandoff(projectSlug, runId, {
43451
+ reason,
43452
+ ...reasonCode ? { reasonCode } : {}
43453
+ });
43284
43454
  } catch {
43285
43455
  await notifyOperatorHandoff(deps, projectSlug, task, runId, reason);
43286
43456
  }
43287
43457
  }
43288
43458
  async function reconcileTaskAwaitingMerge(deps, projectSlug, task, runId, reason) {
43289
43459
  if (task.status === "handoff_needed") return;
43460
+ const run3 = await deps.client.getExecutionRun(projectSlug, runId).catch(() => null);
43461
+ if (run3?.prState === "merged") {
43462
+ console.log(JSON.stringify({ event: "pr_merge_already_observed", projectSlug, runId, taskId: task.id }));
43463
+ return;
43464
+ }
43290
43465
  await deps.client.appendExecutionArtifact(projectSlug, runId, {
43291
43466
  id: newArtifactId(),
43292
43467
  kind: "handoff_judgement",
@@ -43388,6 +43563,27 @@ async function runBackstopSweep(deps, drive = (slug, id) => driveRun(deps, slug,
43388
43563
  pending = [];
43389
43564
  }
43390
43565
  for (const run3 of pending) {
43566
+ const updatedAtMs = run3.updatedAt ? Date.parse(run3.updatedAt) : Number.NaN;
43567
+ const staleBeforeMs = Date.now() - deps.staleSeconds * 1e3;
43568
+ if (run3.leaseId) {
43569
+ if (!Number.isFinite(updatedAtMs) || updatedAtMs >= staleBeforeMs) continue;
43570
+ try {
43571
+ const reclaimed = await deps.client.reclaimExecutionRunLease(slug, run3.id, deps.staleSeconds);
43572
+ if (!reclaimed) continue;
43573
+ } catch (error51) {
43574
+ if (error51 instanceof MrrlinApiError && error51.status === 404) continue;
43575
+ if (error51 instanceof MrrlinApiError && error51.status === 409 && error51.code === "RUN_LEASE_RECLAIM_EXHAUSTED") {
43576
+ await deps.client.createInboxItem(slug, {
43577
+ id: `II-${run3.id}-lease-stuck`,
43578
+ kind: "handoff",
43579
+ taskId: run3.taskId,
43580
+ body: `Execution run ${run3.id} exceeded its stale-lease reclaim budget (${maxLeaseReclaims()} attempts) and still is not making progress.`
43581
+ }).catch(() => {
43582
+ });
43583
+ }
43584
+ continue;
43585
+ }
43586
+ }
43391
43587
  await drive(slug, run3.id);
43392
43588
  }
43393
43589
  if (deps.deployGate) {
@@ -43456,6 +43652,9 @@ function claimIntervalMs() {
43456
43652
  const jitter = base * 0.25 * (Math.random() * 2 - 1);
43457
43653
  return Math.round((base + jitter) * 1e3);
43458
43654
  }
43655
+ function maxLeaseReclaims() {
43656
+ return readPositiveIntEnv("MRRLIN_MAX_LEASE_RECLAIMS", DEFAULT_MAX_LEASE_RECLAIMS);
43657
+ }
43459
43658
 
43460
43659
  // src/async-job-consumer.ts
43461
43660
  var import_node_crypto6 = __toESM(require("node:crypto"), 1);
@@ -47913,6 +48112,103 @@ var CheckoutRegistry = class {
47913
48112
  }
47914
48113
  };
47915
48114
 
48115
+ // src/checkout-handlers.ts
48116
+ var import_node_child_process10 = require("node:child_process");
48117
+ var import_node_fs12 = require("node:fs");
48118
+
48119
+ // src/git-remote-match.ts
48120
+ function normalizeRemote(url2) {
48121
+ const trimmed = url2.trim().replace(/\.git$/i, "");
48122
+ const scp = /^[^@]+@[^:]+:(.+)$/.exec(trimmed);
48123
+ const pathPart = scp ? scp[1] : (() => {
48124
+ try {
48125
+ return new URL(trimmed).pathname.replace(/^\/+/, "");
48126
+ } catch {
48127
+ return null;
48128
+ }
48129
+ })();
48130
+ if (!pathPart) return null;
48131
+ const segs = pathPart.split("/").filter(Boolean);
48132
+ if (segs.length < 2) return null;
48133
+ return `${segs[segs.length - 2]}/${segs[segs.length - 1]}`.toLowerCase();
48134
+ }
48135
+ function remoteMatchesRepo(remoteUrl, repoFullName) {
48136
+ const a = normalizeRemote(remoteUrl);
48137
+ return a !== null && a === repoFullName.trim().toLowerCase();
48138
+ }
48139
+
48140
+ // src/checkout-handlers.ts
48141
+ function validateCheckoutPath(checkoutPath, repo) {
48142
+ const trimmedPath = checkoutPath.trim();
48143
+ if (!trimmedPath.startsWith("/")) {
48144
+ return {
48145
+ ok: false,
48146
+ code: "not_absolute",
48147
+ detail: `Path must be absolute, got: ${trimmedPath}`
48148
+ };
48149
+ }
48150
+ let resolvedPath;
48151
+ try {
48152
+ resolvedPath = (0, import_node_fs12.realpathSync)(trimmedPath);
48153
+ } catch {
48154
+ return {
48155
+ ok: false,
48156
+ code: "not_found",
48157
+ detail: `Path does not exist or is not accessible: ${trimmedPath}`
48158
+ };
48159
+ }
48160
+ let remoteUrl;
48161
+ try {
48162
+ remoteUrl = (0, import_node_child_process10.execFileSync)("git", ["-C", resolvedPath, "remote", "get-url", "origin"], {
48163
+ encoding: "utf8",
48164
+ stdio: ["ignore", "pipe", "ignore"],
48165
+ timeout: 15e3,
48166
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
48167
+ }).trim();
48168
+ } catch {
48169
+ return {
48170
+ ok: false,
48171
+ code: "not_a_git_repo",
48172
+ detail: `Path is not a git repository or has no origin remote: ${resolvedPath}`
48173
+ };
48174
+ }
48175
+ if (!remoteMatchesRepo(remoteUrl, repo)) {
48176
+ return {
48177
+ ok: false,
48178
+ code: "remote_mismatch",
48179
+ detail: `Origin remote ${remoteUrl} does not match repo ${repo}.`
48180
+ };
48181
+ }
48182
+ return { ok: true, resolvedPath };
48183
+ }
48184
+ function readCheckoutState(registry3, projectSlug, machineLabel) {
48185
+ const entry = registry3.get(projectSlug);
48186
+ if (!entry) return null;
48187
+ return checkoutStateData(projectSlug, entry, machineLabel);
48188
+ }
48189
+ function registerCheckout(registry3, input, machineLabel, now = (/* @__PURE__ */ new Date()).toISOString()) {
48190
+ const validated = validateCheckoutPath(input.path, input.repo);
48191
+ if (!validated.ok) return validated;
48192
+ registry3.confirm(input.projectSlug, validated.resolvedPath, now);
48193
+ return {
48194
+ ok: true,
48195
+ data: {
48196
+ projectSlug: input.projectSlug,
48197
+ path: validated.resolvedPath,
48198
+ confirmedAt: now,
48199
+ machineLabel
48200
+ }
48201
+ };
48202
+ }
48203
+ function checkoutStateData(projectSlug, entry, machineLabel) {
48204
+ return {
48205
+ projectSlug,
48206
+ path: entry.path,
48207
+ confirmedAt: entry.confirmedAt,
48208
+ machineLabel
48209
+ };
48210
+ }
48211
+
47916
48212
  // src/executor-server.ts
47917
48213
  async function startExecutorServer(opts) {
47918
48214
  const manager = new CodexServerManager();
@@ -48120,7 +48416,7 @@ function extractVerificationSection(markdown) {
48120
48416
  }
48121
48417
 
48122
48418
  // src/bridge-log.ts
48123
- var import_node_fs12 = require("node:fs");
48419
+ var import_node_fs13 = require("node:fs");
48124
48420
  var import_node_path16 = require("node:path");
48125
48421
 
48126
48422
  // src/redact.ts
@@ -48208,7 +48504,7 @@ function createBridgeLogger(stateDir, env = process.env) {
48208
48504
  if (!resolveBridgeLogEnabled(env)) return NOOP_LOGGER;
48209
48505
  const logsDir = (0, import_node_path16.join)(stateDir, "logs");
48210
48506
  try {
48211
- (0, import_node_fs12.mkdirSync)(logsDir, { recursive: true });
48507
+ (0, import_node_fs13.mkdirSync)(logsDir, { recursive: true });
48212
48508
  } catch {
48213
48509
  return NOOP_LOGGER;
48214
48510
  }
@@ -48225,7 +48521,7 @@ function createBridgeLogger(stateDir, env = process.env) {
48225
48521
  ...rec.ms !== void 0 ? { ms: rec.ms } : {},
48226
48522
  payload: redactMessage(rec.payload, { includePrompts })
48227
48523
  }) + "\n";
48228
- (0, import_node_fs12.appendFileSync)(fileFor(), line);
48524
+ (0, import_node_fs13.appendFileSync)(fileFor(), line);
48229
48525
  } catch {
48230
48526
  }
48231
48527
  };
@@ -48238,7 +48534,7 @@ function createBridgeLogger(stateDir, env = process.env) {
48238
48534
  }
48239
48535
 
48240
48536
  // src/external-agents-scrub.ts
48241
- var import_node_fs13 = __toESM(require("node:fs"), 1);
48537
+ var import_node_fs14 = __toESM(require("node:fs"), 1);
48242
48538
  var import_node_os8 = __toESM(require("node:os"), 1);
48243
48539
  var import_node_path17 = __toESM(require("node:path"), 1);
48244
48540
  var REDACTION = "[REDACTED]";
@@ -48249,7 +48545,7 @@ function readKeysEnv() {
48249
48545
  const filePath = KEYS_ENV_PATH();
48250
48546
  let mtimeMs = 0;
48251
48547
  try {
48252
- mtimeMs = import_node_fs13.default.statSync(filePath).mtimeMs;
48548
+ mtimeMs = import_node_fs14.default.statSync(filePath).mtimeMs;
48253
48549
  } catch {
48254
48550
  mtimeMs = 0;
48255
48551
  }
@@ -48258,7 +48554,7 @@ function readKeysEnv() {
48258
48554
  }
48259
48555
  const map2 = /* @__PURE__ */ new Map();
48260
48556
  try {
48261
- const content = import_node_fs13.default.readFileSync(filePath, "utf8");
48557
+ const content = import_node_fs14.default.readFileSync(filePath, "utf8");
48262
48558
  for (const rawLine of content.split(/\r?\n/)) {
48263
48559
  const line = rawLine.trim();
48264
48560
  if (line === "" || line.startsWith("#")) continue;
@@ -48650,7 +48946,7 @@ function addBridgeModel(target, model) {
48650
48946
  }
48651
48947
  function readTextIfExists(filePath) {
48652
48948
  try {
48653
- return import_node_fs14.default.readFileSync(filePath, "utf8");
48949
+ return import_node_fs15.default.readFileSync(filePath, "utf8");
48654
48950
  } catch {
48655
48951
  return null;
48656
48952
  }
@@ -48729,7 +49025,7 @@ function readCodexFallbackModels() {
48729
49025
  }
48730
49026
  const automationsDir = import_node_path18.default.join(codexHome, "automations");
48731
49027
  try {
48732
- for (const entry of import_node_fs14.default.readdirSync(automationsDir, { withFileTypes: true })) {
49028
+ for (const entry of import_node_fs15.default.readdirSync(automationsDir, { withFileTypes: true })) {
48733
49029
  if (!entry.isDirectory()) continue;
48734
49030
  for (const slug of readKnownGptSlugsFromTomlFile(import_node_path18.default.join(automationsDir, entry.name, "automation.toml"))) {
48735
49031
  addBridgeModel(
@@ -48753,7 +49049,7 @@ function readCodexFallbackModels() {
48753
49049
  function neutralCheckoutCwd(stateDir) {
48754
49050
  const dir = import_node_path18.default.join(stateDir, "no-checkout");
48755
49051
  try {
48756
- import_node_fs14.default.mkdirSync(dir, { recursive: true });
49052
+ import_node_fs15.default.mkdirSync(dir, { recursive: true });
48757
49053
  } catch {
48758
49054
  }
48759
49055
  return dir;
@@ -48772,6 +49068,21 @@ function buildThreadStartParams(msg, config2, desiredModel) {
48772
49068
  "transcript. Just call the tools and reply with the outcome \u2014 start your reply with the result.",
48773
49069
  "When creating or updating tasks, always provide `autonomyLevel` and `autonomyReason`.",
48774
49070
  "",
49071
+ "Tool-use contract \u2014 typed MCP tools are the ONLY channel for Mrrlin data:",
49072
+ "- Every read or write of Mrrlin data \u2014 tasks, plans, wiki pages, inbox items, execution runs,",
49073
+ " projects \u2014 must go through the corresponding typed MCP tool. Never use `commandExecution`,",
49074
+ " `curl`, `python`, or any direct HTTP call to the Mrrlin API.",
49075
+ "- A single `create_task` call creates a task completely; it accepts `title`, `notes`, `category`,",
49076
+ " `subcategory`, `impact`, `autonomyLevel`, `planId`, `parentTaskId`, and more in one call.",
49077
+ " Do not create then patch, and do not re-read the task afterwards unless the operator asked",
49078
+ " for confirmation.",
49079
+ "- `commandExecution` is for the operator's own machine and repositories \u2014 running builds, tests,",
49080
+ " `git`, inspecting local files \u2014 and for public third-party web checks. It is not a transport",
49081
+ " for this product's API.",
49082
+ "- If no typed tool exists for a needed operation, state that to the operator instead of reaching",
49083
+ " for the API by hand.",
49084
+ "- Do not spend turns re-reading state you just wrote.",
49085
+ "",
48775
49086
  "Turn lifecycle \u2014 what you can and cannot do between turns:",
48776
49087
  "- Your turn ends the moment you stop producing output. You CANNOT start a new turn yourself:",
48777
49088
  " no timer and no scheduler wakes you up. Only a new operator message starts the next turn.",
@@ -48845,9 +49156,10 @@ function buildThreadStartParams(msg, config2, desiredModel) {
48845
49156
  ' Never answer "nothing found" from plans alone.',
48846
49157
  '- Only report "nothing found" after the q-variants AND a full-list scan are empty, and then',
48847
49158
  " state exactly what you searched.",
48848
- "- For public HTTP/HTML verification through `commandExecution`, prefer built-in tooling such",
48849
- " as `curl`, `python3` stdlib `urllib.request`, and `html.parser`; do not assume third-party",
48850
- " Python packages such as `requests` or `bs4` are installed.",
49159
+ "- For public/third-party HTTP/HTML verification through `commandExecution`, prefer built-in",
49160
+ " tooling such as `curl`, `python3` stdlib `urllib.request`, and `html.parser`; do not assume",
49161
+ " third-party Python packages such as `requests` or `bs4` are installed. This permission covers",
49162
+ " third-party sites ONLY \u2014 never call the Mrrlin API from `commandExecution`.",
48851
49163
  "",
48852
49164
  "Handoff contract \u2014 durable operator action MUST NOT live only in chat:",
48853
49165
  "- If you conclude that a specific task needs human input, account access, manual evidence,",
@@ -49058,7 +49370,7 @@ function resolveDefaultBranch(checkoutPath) {
49058
49370
  if (!checkoutPath) return "main";
49059
49371
  const runGit = (args) => {
49060
49372
  try {
49061
- return (0, import_node_child_process10.execFileSync)("git", ["-C", checkoutPath, ...args], {
49373
+ return (0, import_node_child_process11.execFileSync)("git", ["-C", checkoutPath, ...args], {
49062
49374
  encoding: "utf8",
49063
49375
  stdio: ["ignore", "pipe", "ignore"],
49064
49376
  timeout: 15e3,
@@ -49090,7 +49402,7 @@ async function runOrphanWorktreeSweep(client, opts) {
49090
49402
  const cutoff = Date.now() - worktreeRetentionMs();
49091
49403
  const isReapable = (worktreePath) => {
49092
49404
  try {
49093
- return import_node_fs14.default.statSync(worktreePath).mtimeMs < cutoff;
49405
+ return import_node_fs15.default.statSync(worktreePath).mtimeMs < cutoff;
49094
49406
  } catch {
49095
49407
  return false;
49096
49408
  }
@@ -49394,7 +49706,9 @@ function createBridgeMessageHandler(deps) {
49394
49706
  logResumeRejected(req.directorSessionId, reason);
49395
49707
  return;
49396
49708
  }
49397
- const buf = t?.replay ?? null;
49709
+ const parked = t?.supersededReplay ?? null;
49710
+ const buf = req.acceptSuperseded === true && parked && parked.turnId === req.turnId ? parked : t?.replay ?? null;
49711
+ const servingSuperseded = buf !== null && buf === parked;
49398
49712
  if (!buf) {
49399
49713
  sendMsg({ type: "resume", status: "no-buffer", reason: "wrong-session" });
49400
49714
  logResumeRejected(req.directorSessionId, "wrong-session");
@@ -49425,15 +49739,17 @@ function createBridgeMessageHandler(deps) {
49425
49739
  buf.state = result.next;
49426
49740
  incrementBindEpoch(buf);
49427
49741
  buf.replaying = true;
49428
- deps.bindSessionSink?.(req.directorSessionId, send);
49429
- boundSessions.add(req.directorSessionId);
49430
- if (buf.triggerDetach) {
49431
- const detach = buf.triggerDetach;
49432
- const epochAtBind = buf.bindEpoch;
49433
- socket.once("close", () => {
49434
- if (buf.bindEpoch !== epochAtBind) return;
49435
- detach();
49436
- });
49742
+ if (!servingSuperseded) {
49743
+ deps.bindSessionSink?.(req.directorSessionId, send);
49744
+ boundSessions.add(req.directorSessionId);
49745
+ if (buf.triggerDetach) {
49746
+ const detach = buf.triggerDetach;
49747
+ const epochAtBind = buf.bindEpoch;
49748
+ socket.once("close", () => {
49749
+ if (buf.bindEpoch !== epochAtBind) return;
49750
+ detach();
49751
+ });
49752
+ }
49437
49753
  }
49438
49754
  const willStreamLive = result.next === "live";
49439
49755
  sendMsg({
@@ -49510,7 +49826,8 @@ function createBridgeMessageHandler(deps) {
49510
49826
  }
49511
49827
  const msg = parsed;
49512
49828
  const sid = sessionIdOf(msg);
49513
- if (sid && deps.bindSessionSink && !boundSessions.has(sid)) {
49829
+ const skipEagerBindForSupersededResume = msg.type === "resume" && msg.acceptSuperseded === true;
49830
+ if (sid && deps.bindSessionSink && !boundSessions.has(sid) && !skipEagerBindForSupersededResume) {
49514
49831
  deps.bindSessionSink(sid, send);
49515
49832
  boundSessions.add(sid);
49516
49833
  }
@@ -49532,6 +49849,57 @@ function createBridgeMessageHandler(deps) {
49532
49849
  sendForSpan({ type: "pairing-qr", ...result });
49533
49850
  return;
49534
49851
  }
49852
+ if (msg.type === "checkout-state") {
49853
+ if (!deps.checkoutRegistry) {
49854
+ sendForSpan({ type: "checkout-state", ok: false, error: { code: "bridge_offline", detail: "checkout registry is not configured on this bridge" } });
49855
+ return;
49856
+ }
49857
+ const projectSlug = typeof msg.projectSlug === "string" ? msg.projectSlug.trim() : "";
49858
+ if (!projectSlug) {
49859
+ sendForSpan({ type: "checkout-state", ok: false, error: { code: "invalid_input", fields: { projectSlug: "required" } } });
49860
+ return;
49861
+ }
49862
+ const state = readCheckoutState(deps.checkoutRegistry, projectSlug, import_node_os9.default.hostname());
49863
+ if (!state) {
49864
+ sendForSpan({ type: "checkout-state", ok: false, error: { code: "checkout_missing" } });
49865
+ return;
49866
+ }
49867
+ sendForSpan({ type: "checkout-state", ok: true, data: state });
49868
+ return;
49869
+ }
49870
+ if (msg.type === "checkout-register") {
49871
+ if (!deps.checkoutRegistry) {
49872
+ sendForSpan({ type: "checkout-register", ok: false, error: { code: "bridge_offline", detail: "checkout registry is not configured on this bridge" } });
49873
+ return;
49874
+ }
49875
+ const projectSlug = typeof msg.projectSlug === "string" ? msg.projectSlug.trim() : "";
49876
+ const repo = typeof msg.repo === "string" ? msg.repo.trim() : "";
49877
+ if (!projectSlug || !repo) {
49878
+ sendForSpan({
49879
+ type: "checkout-register",
49880
+ ok: false,
49881
+ error: {
49882
+ code: "invalid_input",
49883
+ fields: {
49884
+ ...!projectSlug ? { projectSlug: "required" } : {},
49885
+ ...!repo ? { repo: "required" } : {}
49886
+ }
49887
+ }
49888
+ });
49889
+ return;
49890
+ }
49891
+ const result = registerCheckout(
49892
+ deps.checkoutRegistry,
49893
+ { projectSlug, path: typeof msg.path === "string" ? msg.path : "", repo },
49894
+ import_node_os9.default.hostname()
49895
+ );
49896
+ if (!result.ok) {
49897
+ sendForSpan({ type: "checkout-register", ok: false, error: { code: result.code, detail: result.detail } });
49898
+ return;
49899
+ }
49900
+ sendForSpan({ type: "checkout-register", ok: true, data: result.data });
49901
+ return;
49902
+ }
49535
49903
  if (msg.type === "agents-state") {
49536
49904
  if (!deps.runExternalAgentsCli) {
49537
49905
  sendForSpan({ type: "agents-state", ok: false, error: { code: "bridge_offline", detail: "external-agents is not configured on this bridge" } });
@@ -49841,6 +50209,10 @@ function createBridgeMessageHandler(deps) {
49841
50209
  recordInterrupted(oldBuf, Date.now(), deps.suspendedMs?.() ?? 0);
49842
50210
  }
49843
50211
  }
50212
+ const droppedParked = takenOver.supersededReplay;
50213
+ if (oldBuf) {
50214
+ takenOver.supersededReplay = oldBuf;
50215
+ }
49844
50216
  takenOver.cancel?.();
49845
50217
  void withTimeout2(
49846
50218
  codexClient.turn.interrupt({ threadId: takenOver.threadId }),
@@ -49858,7 +50230,13 @@ function createBridgeMessageHandler(deps) {
49858
50230
  spanId,
49859
50231
  sessionId: directorSessionId,
49860
50232
  type: "<turn-takeover>",
49861
- payload: { sessionId: directorSessionId, clean: cleared, oldTurnId: oldBuf?.turnId ?? null }
50233
+ payload: {
50234
+ sessionId: directorSessionId,
50235
+ clean: cleared,
50236
+ oldTurnId: oldBuf?.turnId ?? null,
50237
+ parkedTurnId: oldBuf?.turnId ?? null,
50238
+ droppedParkedTurnId: droppedParked?.turnId ?? null
50239
+ }
49862
50240
  });
49863
50241
  }
49864
50242
  const desiredModel = normalizeModel(msg.model);
@@ -49870,10 +50248,12 @@ function createBridgeMessageHandler(deps) {
49870
50248
  threadId: "",
49871
50249
  mapper: new CodexBridgeEventMapper(),
49872
50250
  lastUsedAt: Date.now(),
50251
+ lastUsedAtSuspendedMs: deps.suspendedMs?.() ?? 0,
49873
50252
  inFlight: false,
49874
50253
  model: desiredModel,
49875
50254
  reasoningEffort: desiredEffort,
49876
- replay: null
50255
+ replay: null,
50256
+ supersededReplay: null
49877
50257
  };
49878
50258
  await acquireThread(codexClient, {
49879
50259
  threadId: null,
@@ -49939,6 +50319,7 @@ function createBridgeMessageHandler(deps) {
49939
50319
  ...deps.ackLingerMs !== void 0 ? { ackLingerMs: deps.ackLingerMs } : {}
49940
50320
  });
49941
50321
  thread.lastUsedAt = Date.now();
50322
+ thread.lastUsedAtSuspendedMs = deps.suspendedMs?.() ?? 0;
49942
50323
  const threadId = thread.threadId;
49943
50324
  const currentReplayBuf = thread.replay;
49944
50325
  sendForSpan({
@@ -50157,6 +50538,7 @@ ${effectiveText}` : effectiveText;
50157
50538
  thread.inFlight = false;
50158
50539
  }
50159
50540
  thread.lastUsedAt = Date.now();
50541
+ thread.lastUsedAtSuspendedMs = deps.suspendedMs?.() ?? 0;
50160
50542
  deps.persistThreads();
50161
50543
  deps.releaseCodex();
50162
50544
  }
@@ -50182,6 +50564,56 @@ function createBridgeShutdown(deps) {
50182
50564
  deps.exit(0);
50183
50565
  };
50184
50566
  }
50567
+ function shouldReapIdleThread(thread, now, suspendedMsNow, idleThreadTtlMs) {
50568
+ const suspendedSinceUse = Math.max(0, suspendedMsNow - thread.lastUsedAtSuspendedMs);
50569
+ return now - thread.lastUsedAt - suspendedSinceUse > idleThreadTtlMs;
50570
+ }
50571
+ function sweepIdleThreads(threads, opts) {
50572
+ const suspendedNow = opts.suspendedMs();
50573
+ const reaped = [];
50574
+ for (const [directorSessionId, thread] of threads.entries()) {
50575
+ if (thread.inFlight) continue;
50576
+ if (thread.replay && !shouldGc(thread.replay, opts.now, suspendedNow)) continue;
50577
+ if (thread.supersededReplay && !shouldGc(thread.supersededReplay, opts.now, suspendedNow)) continue;
50578
+ if (!shouldReapIdleThread(thread, opts.now, suspendedNow, opts.idleThreadTtlMs)) continue;
50579
+ threads.delete(directorSessionId);
50580
+ reaped.push(directorSessionId);
50581
+ }
50582
+ return reaped;
50583
+ }
50584
+ function sweepReplayBuffers(threads, tombstones, opts) {
50585
+ const suspendedNow = opts.suspendedMs();
50586
+ let retainedBuffers = 0;
50587
+ let retainedBytes = 0;
50588
+ for (const [directorSessionId, thread] of threads) {
50589
+ if (thread.replay && shouldGc(thread.replay, opts.now, suspendedNow)) {
50590
+ const turnId = thread.replay.turnId;
50591
+ const lastSeq = thread.replay.lastSeq;
50592
+ const expiredUnacked = thread.replay.ackedAt === null;
50593
+ writeTombstone(tombstones, directorSessionId, thread.replay, opts.now, suspendedNow);
50594
+ opts.onTombstone?.({ directorSessionId, turnId, lastSeq, expiredUnacked });
50595
+ thread.replay = null;
50596
+ } else if (thread.replay) {
50597
+ retainedBuffers += 1;
50598
+ retainedBytes += thread.replay.bytesUsed;
50599
+ }
50600
+ if (thread.supersededReplay && shouldGc(thread.supersededReplay, opts.now, suspendedNow)) {
50601
+ const turnId = thread.supersededReplay.turnId;
50602
+ thread.supersededReplay = null;
50603
+ opts.onSupersededDropped?.({ directorSessionId, turnId });
50604
+ } else if (thread.supersededReplay) {
50605
+ retainedBuffers += 1;
50606
+ retainedBytes += thread.supersededReplay.bytesUsed;
50607
+ }
50608
+ }
50609
+ const tombstonesRemoved = sweepExpiredTombstones(
50610
+ tombstones,
50611
+ opts.now,
50612
+ opts.tombstoneTtlMs ?? DEFAULT_TOMBSTONE_TTL_MS,
50613
+ suspendedNow
50614
+ );
50615
+ return { retainedBuffers, retainedBytes, tombstonesRemoved };
50616
+ }
50185
50617
  function parseConfig() {
50186
50618
  const host = (process.env.MRRLIN_DIRECTOR_BRIDGE_HOST ?? "127.0.0.1").trim() || "127.0.0.1";
50187
50619
  const port = readEnvInt("MRRLIN_DIRECTOR_BRIDGE_PORT", 8791);
@@ -50195,7 +50627,7 @@ function parseConfig() {
50195
50627
  }
50196
50628
  function scanForOtherBridges() {
50197
50629
  try {
50198
- const result = (0, import_node_child_process10.spawnSync)("pgrep", ["-f", "mrrlin-mcp director-bridge"], {
50630
+ const result = (0, import_node_child_process11.spawnSync)("pgrep", ["-f", "mrrlin-mcp director-bridge"], {
50199
50631
  encoding: "utf8"
50200
50632
  });
50201
50633
  if (result.status !== 0) return [];
@@ -50375,13 +50807,13 @@ async function startDirectorBridge() {
50375
50807
  // egressDomains is retained in the seam signature (verification-turn.ts still computes the
50376
50808
  // env host) but no longer constrains anything — the executor is unrestricted.
50377
50809
  startSandbox: async (_egressDomains) => {
50378
- const cwd = import_node_fs14.default.mkdtempSync(import_node_path18.default.join(import_node_os9.default.tmpdir(), "mrrlin-verify-"));
50810
+ const cwd = import_node_fs15.default.mkdtempSync(import_node_path18.default.join(import_node_os9.default.tmpdir(), "mrrlin-verify-"));
50379
50811
  let handle;
50380
50812
  try {
50381
50813
  handle = await startExecutorServer({ worktree: cwd });
50382
50814
  } catch (error51) {
50383
50815
  try {
50384
- import_node_fs14.default.rmSync(cwd, { recursive: true, force: true });
50816
+ import_node_fs15.default.rmSync(cwd, { recursive: true, force: true });
50385
50817
  } catch {
50386
50818
  }
50387
50819
  throw error51;
@@ -50392,7 +50824,7 @@ async function startDirectorBridge() {
50392
50824
  dispose: () => {
50393
50825
  handle.dispose();
50394
50826
  try {
50395
- import_node_fs14.default.rmSync(cwd, { recursive: true, force: true });
50827
+ import_node_fs15.default.rmSync(cwd, { recursive: true, force: true });
50396
50828
  } catch {
50397
50829
  }
50398
50830
  }
@@ -50504,10 +50936,12 @@ async function startDirectorBridge() {
50504
50936
  threadId: entry.threadId,
50505
50937
  mapper: new CodexBridgeEventMapper(),
50506
50938
  lastUsedAt: entry.lastUsedAt,
50939
+ lastUsedAtSuspendedMs: suspensionDetector.suspendedMs(),
50507
50940
  inFlight: false,
50508
50941
  model: entry.model,
50509
50942
  reasoningEffort: normalizeReasoningEffort(entry.reasoningEffort),
50510
- replay: null
50943
+ replay: null,
50944
+ supersededReplay: null
50511
50945
  });
50512
50946
  }
50513
50947
  const persistThreads = () => {
@@ -50532,17 +50966,12 @@ async function startDirectorBridge() {
50532
50966
  }
50533
50967
  };
50534
50968
  const cleanupTimer = setInterval(() => {
50535
- const now = Date.now();
50536
- let changed = false;
50537
- for (const [directorSessionId, thread] of threads.entries()) {
50538
- if (thread.inFlight) continue;
50539
- if (thread.replay && !shouldGc(thread.replay, now, suspensionDetector.suspendedMs())) continue;
50540
- if (now - thread.lastUsedAt > config2.idleThreadTtlMs) {
50541
- threads.delete(directorSessionId);
50542
- changed = true;
50543
- }
50544
- }
50545
- if (changed) persistThreads();
50969
+ const reaped = sweepIdleThreads(threads, {
50970
+ now: Date.now(),
50971
+ suspendedMs: () => suspensionDetector.suspendedMs(),
50972
+ idleThreadTtlMs: config2.idleThreadTtlMs
50973
+ });
50974
+ if (reaped.length > 0) persistThreads();
50546
50975
  }, 6e4);
50547
50976
  cleanupTimer.unref?.();
50548
50977
  let remoteRelay = null;
@@ -50603,7 +51032,7 @@ async function startDirectorBridge() {
50603
51032
  const bridgeLogger = createBridgeLogger(config2.stateDir, process.env);
50604
51033
  let bridgeBinShaShort = "unknown00000";
50605
51034
  try {
50606
- bridgeBinShaShort = import_node_crypto8.default.createHash("sha256").update(import_node_fs14.default.readFileSync(process.argv[1] ?? "")).digest("hex").slice(0, 12);
51035
+ bridgeBinShaShort = import_node_crypto8.default.createHash("sha256").update(import_node_fs15.default.readFileSync(process.argv[1] ?? "")).digest("hex").slice(0, 12);
50607
51036
  } catch {
50608
51037
  }
50609
51038
  attachBridgeWss(server, {
@@ -50737,28 +51166,27 @@ async function startDirectorBridge() {
50737
51166
  })
50738
51167
  );
50739
51168
  enqueueRun(async () => {
50740
- const now = Date.now();
50741
- const suspendedNow = suspensionDetector.suspendedMs();
50742
- let retainedBuffers = 0;
50743
- let retainedBytes = 0;
50744
- for (const [sid, t] of threads) {
50745
- if (t.replay && shouldGc(t.replay, now, suspendedNow)) {
50746
- const turnId = t.replay.turnId;
50747
- const lastSeq = t.replay.lastSeq;
50748
- const expiredUnacked = t.replay.ackedAt === null;
50749
- writeTombstone(tombstones, sid, t.replay, now);
51169
+ const { retainedBuffers, retainedBytes, tombstonesRemoved } = sweepReplayBuffers(threads, tombstones, {
51170
+ now: Date.now(),
51171
+ suspendedMs: () => suspensionDetector.suspendedMs(),
51172
+ tombstoneTtlMs: DEFAULT_TOMBSTONE_TTL_MS,
51173
+ onTombstone: ({ directorSessionId, turnId, lastSeq, expiredUnacked }) => {
50750
51174
  bridgeLogger.logOut({
50751
51175
  spanId: "gc",
50752
- sessionId: sid,
51176
+ sessionId: directorSessionId,
50753
51177
  type: expiredUnacked ? "<result-expired>" : "<tombstone-written>",
50754
- payload: { sessionId: sid, turnId, lastSeq }
51178
+ payload: { sessionId: directorSessionId, turnId, lastSeq }
51179
+ });
51180
+ },
51181
+ onSupersededDropped: ({ directorSessionId, turnId }) => {
51182
+ bridgeLogger.logOut({
51183
+ spanId: "gc",
51184
+ sessionId: directorSessionId,
51185
+ type: "<superseded-dropped>",
51186
+ payload: { sessionId: directorSessionId, turnId }
50755
51187
  });
50756
- t.replay = null;
50757
- } else if (t.replay) {
50758
- retainedBuffers += 1;
50759
- retainedBytes += t.replay.bytesUsed;
50760
51188
  }
50761
- }
51189
+ });
50762
51190
  if (retainedBuffers > 0) {
50763
51191
  bridgeLogger.logOut({
50764
51192
  spanId: "gc",
@@ -50767,13 +51195,12 @@ async function startDirectorBridge() {
50767
51195
  payload: { retainedBuffers, retainedBytes }
50768
51196
  });
50769
51197
  }
50770
- const removed = sweepExpiredTombstones(tombstones, now);
50771
- if (removed > 0) {
51198
+ if (tombstonesRemoved > 0) {
50772
51199
  bridgeLogger.logOut({
50773
51200
  spanId: "gc",
50774
51201
  sessionId: null,
50775
51202
  type: "<tombstone-expired>",
50776
- payload: { removed }
51203
+ payload: { removed: tombstonesRemoved }
50777
51204
  });
50778
51205
  }
50779
51206
  });
@@ -50997,13 +51424,13 @@ function readOrCreateBridgeToken(stateDir) {
50997
51424
  if (explicit) return explicit;
50998
51425
  const tokenPath = import_node_path18.default.join(stateDir, "token.txt");
50999
51426
  try {
51000
- const existing = import_node_fs14.default.readFileSync(tokenPath, "utf8").trim();
51427
+ const existing = import_node_fs15.default.readFileSync(tokenPath, "utf8").trim();
51001
51428
  if (existing) return existing;
51002
51429
  } catch {
51003
51430
  }
51004
- import_node_fs14.default.mkdirSync(stateDir, { recursive: true, mode: 448 });
51431
+ import_node_fs15.default.mkdirSync(stateDir, { recursive: true, mode: 448 });
51005
51432
  const token = import_node_crypto8.default.randomBytes(32).toString("base64url");
51006
- import_node_fs14.default.writeFileSync(tokenPath, `${token}
51433
+ import_node_fs15.default.writeFileSync(tokenPath, `${token}
51007
51434
  `, { mode: 384 });
51008
51435
  return token;
51009
51436
  }
@@ -54387,10 +54814,8 @@ var StdioServerTransport = class {
54387
54814
  // src/tools.ts
54388
54815
  var import_node_crypto11 = require("node:crypto");
54389
54816
  var import_promises3 = require("node:fs/promises");
54390
- var import_node_fs15 = require("node:fs");
54391
54817
  var import_node_path20 = __toESM(require("node:path"), 1);
54392
54818
  var import_node_os10 = __toESM(require("node:os"), 1);
54393
- var import_node_child_process12 = require("node:child_process");
54394
54819
 
54395
54820
  // ../../packages/wiki/dist/index.js
54396
54821
  var import_promises2 = __toESM(require("node:fs/promises"), 1);
@@ -54649,15 +55074,35 @@ function firstTaskIdToken(text) {
54649
55074
  }
54650
55075
 
54651
55076
  // src/consensus/spec-gate.ts
55077
+ function mapAcceptedIssueToClarification(issue2) {
55078
+ const text = issue2.text.toLowerCase();
55079
+ let kind;
55080
+ if (issue2.category === "scope") {
55081
+ const directionCues = ["only", "or also", "narrower", "broader", "include", "exclude", "versus", "vs"];
55082
+ kind = directionCues.some((cue) => text.includes(cue)) ? "scope_direction" : "missing_requirement";
55083
+ } else {
55084
+ const acceptanceCues = ["acceptance", "done", "success", "criteria", "satisfy"];
55085
+ kind = acceptanceCues.some((cue) => text.includes(cue)) ? "acceptance_criteria_detail" : "default_choice";
55086
+ }
55087
+ return {
55088
+ kind,
55089
+ diagnostic: false,
55090
+ rawQuestion: issue2.text,
55091
+ whyBlocking: `reviewer raised a ${issue2.category} issue`,
55092
+ category: issue2.category
55093
+ };
55094
+ }
54652
55095
  async function runSpecGate(_target, deps, opts) {
54653
55096
  if (!await deps.consensusEnabled()) return { outcome: "SKIPPED" };
54654
55097
  const intent = await deps.readIntent();
54655
55098
  if (intent.taskId !== null && (intent.seed === null || intent.seed.trim() === "")) {
54656
55099
  return {
54657
55100
  outcome: "CONSENSUS_BLOCKED",
54658
- clarifications: [
55101
+ operatorClarifications: [
54659
55102
  {
54660
- question: "What are the requirements for this task? There is no operator request on record, so the spec cannot be validated against your intent \u2014 please state what this task must do.",
55103
+ kind: "missing_requirement",
55104
+ diagnostic: false,
55105
+ rawQuestion: "What are the requirements for this task? There is no operator request on record, so the spec cannot be validated against your intent \u2014 please state what this task must do.",
54661
55106
  whyBlocking: "operator_seed_request is empty",
54662
55107
  category: "scope"
54663
55108
  }
@@ -54671,9 +55116,11 @@ async function runSpecGate(_target, deps, opts) {
54671
55116
  if (normalizeDomain(intent.domain) !== "code" && parseAcceptanceCriteria(spec.markdownBody) === null) {
54672
55117
  return {
54673
55118
  outcome: "CONSENSUS_BLOCKED",
54674
- clarifications: [
55119
+ operatorClarifications: [
54675
55120
  {
54676
- question: "This spec has no machine-usable acceptance criteria. Add an `## Acceptance criteria` section written as a markdown checklist (`- [ ] \u2026` items) describing exactly what the finished deliverable must satisfy \u2014 the review of the produced work is scored against it.",
55121
+ kind: "acceptance_criteria_detail",
55122
+ diagnostic: false,
55123
+ rawQuestion: "This spec has no machine-usable acceptance criteria. Add an `## Acceptance criteria` section written as a markdown checklist (`- [ ] \u2026` items) describing exactly what the finished deliverable must satisfy \u2014 the review of the produced work is scored against it.",
54677
55124
  whyBlocking: "non-code spec is missing a parseable `## Acceptance criteria` checklist",
54678
55125
  category: "scope"
54679
55126
  }
@@ -54723,9 +55170,11 @@ async function runSpecGate(_target, deps, opts) {
54723
55170
  );
54724
55171
  return {
54725
55172
  outcome: "CONSENSUS_BLOCKED",
54726
- clarifications: [
55173
+ diagnostics: [
54727
55174
  {
54728
- question: `SPEC_IDENTITY_MISMATCH: this spec page gates ${intent.taskId}, but its heading identifies a different task \u2014 rewrite the spec heading and content for ${intent.taskId}, or re-link the task.`,
55175
+ kind: "spec_identity_mismatch",
55176
+ diagnostic: true,
55177
+ rawQuestion: `SPEC_IDENTITY_MISMATCH: this spec page gates ${intent.taskId}, but its heading identifies a different task \u2014 rewrite the spec heading and content for ${intent.taskId}, or re-link the task.`,
54729
55178
  whyBlocking: "converged spec content belongs to a different task than the one it gates",
54730
55179
  category: "scope"
54731
55180
  }
@@ -54745,12 +55194,8 @@ async function runSpecGate(_target, deps, opts) {
54745
55194
  }
54746
55195
  if (result.outcome === "CONSENSUS_BLOCKED") {
54747
55196
  const accepted = result.history.at(-1)?.accepted ?? [];
54748
- const clarifications = accepted.filter((issue2) => issue2.category === "ambiguity" || issue2.category === "scope").map((issue2) => ({
54749
- question: issue2.text,
54750
- whyBlocking: `reviewer raised a ${issue2.category} issue`,
54751
- category: issue2.category
54752
- }));
54753
- return clarifications.length > 0 ? { outcome: "CONSENSUS_BLOCKED", clarifications } : { outcome: "CONSENSUS_BLOCKED" };
55197
+ const operatorClarifications = accepted.filter((issue2) => issue2.category === "ambiguity" || issue2.category === "scope").map((issue2) => mapAcceptedIssueToClarification(issue2));
55198
+ return operatorClarifications.length > 0 ? { outcome: "CONSENSUS_BLOCKED", operatorClarifications } : { outcome: "CONSENSUS_BLOCKED" };
54754
55199
  }
54755
55200
  return { outcome: result.outcome };
54756
55201
  }
@@ -54828,7 +55273,7 @@ async function runCodeGate(_target, deps) {
54828
55273
  }
54829
55274
 
54830
55275
  // src/consensus/multi-llm-head.ts
54831
- var import_node_child_process11 = require("node:child_process");
55276
+ var import_node_child_process12 = require("node:child_process");
54832
55277
  function errMessage3(err) {
54833
55278
  return err instanceof Error ? err.message : String(err);
54834
55279
  }
@@ -54870,7 +55315,7 @@ function runExternalAgents(bin, args, label, timeoutMs, spawn3) {
54870
55315
  });
54871
55316
  }
54872
55317
  async function pickPanel(input, deps) {
54873
- const spawn3 = deps?.spawn ?? import_node_child_process11.spawn;
55318
+ const spawn3 = deps?.spawn ?? import_node_child_process12.spawn;
54874
55319
  const { command, prefixArgs } = resolveExternalAgentsCliInvocation(deps?.externalAgentsBin);
54875
55320
  const args = [
54876
55321
  "pick",
@@ -54891,7 +55336,7 @@ async function pickPanel(input, deps) {
54891
55336
  return { ok: true, agents };
54892
55337
  }
54893
55338
  async function dispatchHead(agentId, prompt, timeoutMs, deps) {
54894
- const spawn3 = deps?.spawn ?? import_node_child_process11.spawn;
55339
+ const spawn3 = deps?.spawn ?? import_node_child_process12.spawn;
54895
55340
  const { command, prefixArgs } = resolveExternalAgentsCliInvocation(deps?.externalAgentsBin);
54896
55341
  const args = ["dispatch", agentId, "--json", prompt];
54897
55342
  const r = await runExternalAgents(command, [...prefixArgs, ...args], "dispatch", timeoutMs, spawn3);
@@ -55175,6 +55620,22 @@ function buildGateQuestionOptions(question, category) {
55175
55620
  }
55176
55621
  ];
55177
55622
  }
55623
+ var HUMANIZE_TEMPLATE_BY_KIND = {
55624
+ scope_direction: "The spec can be read in two different ways. Which direction should it follow?",
55625
+ default_choice: "One important detail is still ambiguous. Which interpretation should the spec use?",
55626
+ missing_requirement: "A required part of the task is still missing from the spec. What should it say?",
55627
+ acceptance_criteria_detail: "The direction is clear, but the spec still needs a sharper success check. What should count as done?"
55628
+ };
55629
+ var HUMANIZE_CONTEXT_CAP = 200;
55630
+ function humanizeClarificationBody(clarification) {
55631
+ const prefix = HUMANIZE_TEMPLATE_BY_KIND[clarification.kind];
55632
+ let context = clarification.rawQuestion.replace(/\b(?:parseable|SPEC_IDENTITY_MISMATCH|machine-usable)\b/gi, "").replace(/\s+/g, " ").trim();
55633
+ if (context.length > HUMANIZE_CONTEXT_CAP) {
55634
+ const cutIdx = context.lastIndexOf(" ", HUMANIZE_CONTEXT_CAP);
55635
+ context = context.slice(0, cutIdx > 0 ? cutIdx : HUMANIZE_CONTEXT_CAP).trim() + "\u2026";
55636
+ }
55637
+ return context ? `${prefix} ${context}` : prefix;
55638
+ }
55178
55639
  var defaultNativeSpecGateBuilders = { buildSpecGateDeps, runSpecGate };
55179
55640
  async function runNativeSpecGate(ctx, input, builders = defaultNativeSpecGateBuilders) {
55180
55641
  const deps = builders.buildSpecGateDeps({
@@ -55215,6 +55676,100 @@ async function cachedResult(client, input) {
55215
55676
  }
55216
55677
  return null;
55217
55678
  }
55679
+ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
55680
+ const operatorClarifications = result.operatorClarifications ?? [];
55681
+ const diagnostics = result.diagnostics ?? [];
55682
+ if (operatorClarifications.length > 0) {
55683
+ try {
55684
+ const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
55685
+ if (page) {
55686
+ const domain2 = await resolveSpecDomain(ctx.client, input.projectSlug, {
55687
+ id: page.id,
55688
+ taskId: page.taskId ?? null
55689
+ });
55690
+ await ctx.client.recordSpecConsensusReview(input.projectSlug, input.pageId, {
55691
+ contentHash: domainScopedReviewHash(page.contentHash, domain2),
55692
+ outcome: "CONSENSUS_BLOCKED_CLARIFICATION",
55693
+ rounds: 0
55694
+ });
55695
+ }
55696
+ } catch (error52) {
55697
+ process.stderr.write(
55698
+ `[mrrlin-mcp consensus] clarification verdict write failed for ${input.projectSlug}/${input.pageId}: ${error52 instanceof Error ? error52.message : String(error52)}
55699
+ `
55700
+ );
55701
+ }
55702
+ }
55703
+ let filedQuestionCount = 0;
55704
+ if (operatorClarifications.length > 0) {
55705
+ const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
55706
+ const taskId = page?.taskId ?? null;
55707
+ if (!resolvedTaskId && taskId) resolvedTaskId = taskId;
55708
+ let planId = null;
55709
+ if (!taskId) {
55710
+ const plans = await ctx.client.listPlans(input.projectSlug);
55711
+ planId = plans.find((p) => p.specWikiPageId === input.pageId)?.id ?? null;
55712
+ }
55713
+ if (taskId) {
55714
+ const scopeId = taskId;
55715
+ const groupId = operatorClarifications.length > 1 ? `II-${scopeId}-gate-group-${(0, import_node_crypto9.createHash)("sha256").update(operatorClarifications.map((c) => c.rawQuestion).sort().join("\n")).digest("hex").slice(0, 10)}` : void 0;
55716
+ for (const clarification of operatorClarifications) {
55717
+ const id = `II-${scopeId}-gate-${(0, import_node_crypto9.createHash)("sha256").update(clarification.rawQuestion).digest("hex").slice(0, 10)}`;
55718
+ try {
55719
+ const questionOptions = buildGateQuestionOptions(clarification.rawQuestion, clarification.category);
55720
+ await ctx.client.createInboxItem(input.projectSlug, {
55721
+ id,
55722
+ kind: "question",
55723
+ taskId: scopeId,
55724
+ body: humanizeClarificationBody(clarification),
55725
+ origin: "gate",
55726
+ questionOptions,
55727
+ ...groupId ? { groupId } : {}
55728
+ });
55729
+ } catch {
55730
+ }
55731
+ }
55732
+ filedQuestionCount = operatorClarifications.length;
55733
+ } else if (planId) {
55734
+ const scopeId = planId;
55735
+ const groupId = operatorClarifications.length > 1 ? `II-${scopeId}-gate-group-${(0, import_node_crypto9.createHash)("sha256").update(operatorClarifications.map((c) => c.rawQuestion).sort().join("\n")).digest("hex").slice(0, 10)}` : void 0;
55736
+ for (const clarification of operatorClarifications) {
55737
+ const id = `II-${scopeId}-gate-${(0, import_node_crypto9.createHash)("sha256").update(clarification.rawQuestion).digest("hex").slice(0, 10)}`;
55738
+ try {
55739
+ const questionOptions = buildGateQuestionOptions(clarification.rawQuestion, clarification.category);
55740
+ await ctx.client.createInboxItem(input.projectSlug, {
55741
+ id,
55742
+ kind: "question",
55743
+ planId: scopeId,
55744
+ body: humanizeClarificationBody(clarification),
55745
+ origin: "gate",
55746
+ questionOptions,
55747
+ ...groupId ? { groupId } : {}
55748
+ });
55749
+ } catch {
55750
+ }
55751
+ }
55752
+ filedQuestionCount = operatorClarifications.length;
55753
+ }
55754
+ }
55755
+ let error51;
55756
+ if (filedQuestionCount > 0) {
55757
+ error51 = `Consensus blocked on ${filedQuestionCount} clarification(s); filed as operator question(s).`;
55758
+ } else if (diagnostics.length > 0 && operatorClarifications.length === 0) {
55759
+ error51 = "Consensus blocked: the spec content does not match its linked task. This requires fixing the spec/task relationship directly, not an inbox answer.";
55760
+ } else if (operatorClarifications.length > 0) {
55761
+ error51 = `Consensus blocked on ${operatorClarifications.length} clarification(s), but the spec page is not linked to a task or plan, so none could be filed as questions.`;
55762
+ } else {
55763
+ error51 = "Consensus blocked: reviewers did not converge within budget.";
55764
+ }
55765
+ return {
55766
+ kind: "failed",
55767
+ failureReason: "consensus_blocked",
55768
+ error: error51,
55769
+ filedQuestionCount,
55770
+ ...resolvedTaskId ? { taskId: resolvedTaskId } : {}
55771
+ };
55772
+ }
55218
55773
  async function run(ctx, input) {
55219
55774
  const result = await runNativeSpecGate(ctx, input);
55220
55775
  if (process.env[SHADOW_ENV_FLAG]) {
@@ -55243,95 +55798,7 @@ async function run(ctx, input) {
55243
55798
  } catch {
55244
55799
  }
55245
55800
  if (result.outcome === "CONSENSUS_BLOCKED") {
55246
- const clarifications = result.clarifications ?? [];
55247
- if (clarifications.length > 0) {
55248
- try {
55249
- const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
55250
- if (page) {
55251
- const domain2 = await resolveSpecDomain(ctx.client, input.projectSlug, {
55252
- id: page.id,
55253
- taskId: page.taskId ?? null
55254
- });
55255
- await ctx.client.recordSpecConsensusReview(input.projectSlug, input.pageId, {
55256
- contentHash: domainScopedReviewHash(page.contentHash, domain2),
55257
- outcome: "CONSENSUS_BLOCKED_CLARIFICATION",
55258
- rounds: 0
55259
- });
55260
- }
55261
- } catch (error52) {
55262
- process.stderr.write(
55263
- `[mrrlin-mcp consensus] clarification verdict write failed for ${input.projectSlug}/${input.pageId}: ${error52 instanceof Error ? error52.message : String(error52)}
55264
- `
55265
- );
55266
- }
55267
- }
55268
- let filedQuestionCount = 0;
55269
- if (clarifications.length > 0) {
55270
- const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
55271
- const taskId = page?.taskId ?? null;
55272
- if (!resolvedTaskId && taskId) resolvedTaskId = taskId;
55273
- let planId = null;
55274
- if (!taskId) {
55275
- const plans = await ctx.client.listPlans(input.projectSlug);
55276
- planId = plans.find((p) => p.specWikiPageId === input.pageId)?.id ?? null;
55277
- }
55278
- if (taskId) {
55279
- const scopeId = taskId;
55280
- const groupId = clarifications.length > 1 ? `II-${scopeId}-gate-group-${(0, import_node_crypto9.createHash)("sha256").update(clarifications.map((c) => c.question).sort().join("\n")).digest("hex").slice(0, 10)}` : void 0;
55281
- for (const clarification of clarifications) {
55282
- const id = `II-${scopeId}-gate-${(0, import_node_crypto9.createHash)("sha256").update(clarification.question).digest("hex").slice(0, 10)}`;
55283
- try {
55284
- const questionOptions = buildGateQuestionOptions(clarification.question, clarification.category);
55285
- await ctx.client.createInboxItem(input.projectSlug, {
55286
- id,
55287
- kind: "question",
55288
- taskId: scopeId,
55289
- body: clarification.question,
55290
- origin: "gate",
55291
- questionOptions,
55292
- ...groupId ? { groupId } : {}
55293
- });
55294
- } catch {
55295
- }
55296
- }
55297
- filedQuestionCount = clarifications.length;
55298
- } else if (planId) {
55299
- const scopeId = planId;
55300
- const groupId = clarifications.length > 1 ? `II-${scopeId}-gate-group-${(0, import_node_crypto9.createHash)("sha256").update(clarifications.map((c) => c.question).sort().join("\n")).digest("hex").slice(0, 10)}` : void 0;
55301
- for (const clarification of clarifications) {
55302
- const id = `II-${scopeId}-gate-${(0, import_node_crypto9.createHash)("sha256").update(clarification.question).digest("hex").slice(0, 10)}`;
55303
- try {
55304
- const questionOptions = buildGateQuestionOptions(clarification.question, clarification.category);
55305
- await ctx.client.createInboxItem(input.projectSlug, {
55306
- id,
55307
- kind: "question",
55308
- planId: scopeId,
55309
- body: clarification.question,
55310
- origin: "gate",
55311
- questionOptions,
55312
- ...groupId ? { groupId } : {}
55313
- });
55314
- } catch {
55315
- }
55316
- }
55317
- filedQuestionCount = clarifications.length;
55318
- }
55319
- }
55320
- let error51;
55321
- if (filedQuestionCount > 0) {
55322
- error51 = `Consensus blocked on ${filedQuestionCount} clarification(s); filed as operator question(s).`;
55323
- } else if (clarifications.length > 0) {
55324
- error51 = `Consensus blocked on ${clarifications.length} clarification(s), but the spec page is not linked to a task or plan, so none could be filed as questions.`;
55325
- } else {
55326
- error51 = "Consensus blocked: reviewers did not converge within budget.";
55327
- }
55328
- return {
55329
- kind: "failed",
55330
- failureReason: "consensus_blocked",
55331
- error: error51,
55332
- filedQuestionCount,
55333
- ...resolvedTaskId ? { taskId: resolvedTaskId } : {}
55334
- };
55801
+ return handleConsensusBlocked(result, ctx, input, resolvedTaskId);
55335
55802
  }
55336
55803
  switch (result.outcome) {
55337
55804
  case "CONVERGED":
@@ -55527,27 +55994,6 @@ async function enqueueAsyncTool(client, descriptor2, input) {
55527
55994
  }
55528
55995
  }
55529
55996
 
55530
- // src/git-remote-match.ts
55531
- function normalizeRemote(url2) {
55532
- const trimmed = url2.trim().replace(/\.git$/i, "");
55533
- const scp = /^[^@]+@[^:]+:(.+)$/.exec(trimmed);
55534
- const pathPart = scp ? scp[1] : (() => {
55535
- try {
55536
- return new URL(trimmed).pathname.replace(/^\/+/, "");
55537
- } catch {
55538
- return null;
55539
- }
55540
- })();
55541
- if (!pathPart) return null;
55542
- const segs = pathPart.split("/").filter(Boolean);
55543
- if (segs.length < 2) return null;
55544
- return `${segs[segs.length - 2]}/${segs[segs.length - 1]}`.toLowerCase();
55545
- }
55546
- function remoteMatchesRepo(remoteUrl, repoFullName) {
55547
- const a = normalizeRemote(remoteUrl);
55548
- return a !== null && a === repoFullName.trim().toLowerCase();
55549
- }
55550
-
55551
55997
  // src/tool-names.ts
55552
55998
  var mcpToolNames = {
55553
55999
  appendExecutionArtifact: "append_execution_artifact",
@@ -55601,12 +56047,16 @@ var mcpToolNames = {
55601
56047
  updatePlan: "update_plan",
55602
56048
  updateTask: "update_task",
55603
56049
  resetDirectorThread: "reset_director_thread",
56050
+ archiveTask: "archive_task",
56051
+ unarchiveTask: "unarchive_task",
55604
56052
  updateWikiPage: "update_wiki_page",
55605
56053
  markSpecReady: "mark_spec_ready",
55606
56054
  runSpecConsensus: "run_spec_consensus",
55607
56055
  runCodeReviewerGate: "run_code_reviewer_gate",
55608
56056
  uploadArtifact: "upload_artifact",
55609
56057
  listArtifactFiles: "list_artifact_files",
56058
+ reclaimExecutionRunLease: "reclaim_execution_run_lease",
56059
+ restartExecutionRun: "restart_execution_run",
55610
56060
  resolveHandoff: "resolve_handoff",
55611
56061
  recordProducedUrl: "record_produced_url",
55612
56062
  registerLocalCheckout: "register_local_checkout",
@@ -56116,6 +56566,8 @@ var getTaskInputSchema = taskScopedSchema;
56116
56566
  var createTaskInputSchema = projectScopedSchema.merge(mrrlinTaskCreateSchema);
56117
56567
  var updateTaskInputSchema = taskScopedSchema.merge(mrrlinTaskUpdateSchema);
56118
56568
  var resetDirectorThreadInputSchema = taskScopedSchema;
56569
+ var archiveTaskInputSchema = taskScopedSchema;
56570
+ var unarchiveTaskInputSchema = taskScopedSchema;
56119
56571
  var listPlansInputSchema = projectScopedSchema;
56120
56572
  var createPlanInputSchema = projectScopedSchema.merge(mrrlinPlanCreateSchema);
56121
56573
  var updatePlanInputSchema = planScopedSchema.merge(mrrlinPlanUpdateSchema);
@@ -56203,6 +56655,8 @@ var listArtifactFilesInputSchema = projectScopedSchema.merge(
56203
56655
  runId: mrrlinExecutionRunIdSchema.optional()
56204
56656
  })
56205
56657
  );
56658
+ var reclaimExecutionRunLeaseInputSchema = projectScopedSchema.extend({ runId: mrrlinExecutionRunIdSchema }).merge(mrrlinExecutionRunRequeueSchema);
56659
+ var restartExecutionRunInputSchema = projectScopedSchema.extend({ runId: mrrlinExecutionRunIdSchema }).merge(mrrlinRestartExecutionRunSchema);
56206
56660
  var listInboxItemsInputSchema = projectScopedSchema.merge(mrrlinInboxItemListFiltersSchema);
56207
56661
  var createInboxItemInputSchema = projectScopedSchema.merge(mrrlinInboxItemCreateSchema);
56208
56662
  var decideInboxItemInputSchema = projectScopedSchema.extend({ itemId: mrrlinInboxItemIdSchema }).merge(mrrlinInboxItemDecideSchema);
@@ -56268,6 +56722,8 @@ var mcpToolInputSchemas = {
56268
56722
  [mcpToolNames.updatePlan]: updatePlanInputSchema,
56269
56723
  [mcpToolNames.updateTask]: updateTaskInputSchema,
56270
56724
  [mcpToolNames.resetDirectorThread]: resetDirectorThreadInputSchema,
56725
+ [mcpToolNames.archiveTask]: archiveTaskInputSchema,
56726
+ [mcpToolNames.unarchiveTask]: unarchiveTaskInputSchema,
56271
56727
  [mcpToolNames.updateWikiPage]: updateWikiPageInputSchema,
56272
56728
  [mcpToolNames.markSpecReady]: markSpecReadyInputSchema,
56273
56729
  [mcpToolNames.runSpecConsensus]: runSpecConsensusInputSchema,
@@ -56277,6 +56733,8 @@ var mcpToolInputSchemas = {
56277
56733
  [mcpToolNames.decideInboxItem]: decideInboxItemInputSchema,
56278
56734
  [mcpToolNames.uploadArtifact]: uploadArtifactInputSchema,
56279
56735
  [mcpToolNames.listArtifactFiles]: listArtifactFilesInputSchema,
56736
+ [mcpToolNames.reclaimExecutionRunLease]: reclaimExecutionRunLeaseInputSchema,
56737
+ [mcpToolNames.restartExecutionRun]: restartExecutionRunInputSchema,
56280
56738
  [mcpToolNames.resolveHandoff]: resolveHandoffInputSchema,
56281
56739
  [mcpToolNames.recordProducedUrl]: recordProducedUrlInputSchema,
56282
56740
  [mcpToolNames.registerLocalCheckout]: registerLocalCheckoutInputSchema,
@@ -56331,6 +56789,8 @@ var mcpToolOperationIds = {
56331
56789
  [mcpToolNames.updatePlan]: openApiOperationIds.updatePlan,
56332
56790
  [mcpToolNames.updateTask]: openApiOperationIds.updateTask,
56333
56791
  [mcpToolNames.resetDirectorThread]: openApiOperationIds.resetDirectorThread,
56792
+ [mcpToolNames.archiveTask]: openApiOperationIds.archiveTask,
56793
+ [mcpToolNames.unarchiveTask]: openApiOperationIds.unarchiveTask,
56334
56794
  [mcpToolNames.updateWikiPage]: openApiOperationIds.updateWikiPage,
56335
56795
  [mcpToolNames.markSpecReady]: openApiOperationIds.markSpecReady,
56336
56796
  [mcpToolNames.listInboxItems]: openApiOperationIds.listInboxItems,
@@ -56338,6 +56798,8 @@ var mcpToolOperationIds = {
56338
56798
  [mcpToolNames.decideInboxItem]: openApiOperationIds.decideInboxItem,
56339
56799
  [mcpToolNames.uploadArtifact]: openApiOperationIds.createArtifactFile,
56340
56800
  [mcpToolNames.listArtifactFiles]: openApiOperationIds.listArtifactFiles,
56801
+ [mcpToolNames.reclaimExecutionRunLease]: openApiOperationIds.reclaimExecutionRunLease,
56802
+ [mcpToolNames.restartExecutionRun]: openApiOperationIds.restartExecutionRun,
56341
56803
  [mcpToolNames.resolveHandoff]: openApiOperationIds.resolveHandoff
56342
56804
  };
56343
56805
  var mcpToolDescriptions = {
@@ -56378,6 +56840,8 @@ var mcpToolDescriptions = {
56378
56840
  [mcpToolNames.updatePlan]: "Update a plan/epic.",
56379
56841
  [mcpToolNames.updateTask]: "Update a task. Honors DB invariants: closed_no_result requires linked Evidence wiki pages. Set directorThreadId here to persist the worker's resumable provider thread handle.",
56380
56842
  [mcpToolNames.resetDirectorThread]: "Clear a task's persisted Director execution thread handle so the next execution run starts a fresh provider thread. Records a director_thread_reset task event.",
56843
+ [mcpToolNames.archiveTask]: "Archive a task: hides it from default reads, removes it from plan progress and scheduler consideration, cancels its non-terminal execution runs, and closes its open inbox items. Reversible \u2014 the project owner can restore it with unarchive_task. Records an 'archived' task event.",
56844
+ [mcpToolNames.unarchiveTask]: "Restore an archived task to its original status and visibility. Owner-only \u2014 refused with NOT_PROJECT_OWNER for any other caller. Does not resurrect the execution runs or inbox items that were cancelled/closed when the task was archived. Records a 'restored' task event.",
56381
56845
  [mcpToolNames.updateWikiPage]: "Update a markdown Wiki page.",
56382
56846
  [mcpToolNames.markSpecReady]: "Mark a wiki page (folder=Specs) as ready (or not) for execution-run gating. The Director sets this true after validating the spec; any edit to the page's markdown auto-resets it to false.",
56383
56847
  [mcpToolNames.runSpecConsensus]: 'Enqueue the consensus review gate over a Specs/{id} page. Returns IMMEDIATELY with one of: already_converged (a CONVERGED review already exists for the current content \u2014 proceed straight to create_execution_run, do NOT call mark_spec_ready, it is already set), already_reviewed (a terminal CONSENSUS_BLOCKED for this content), already_running (a consensus job is in flight), or queued (a new job was launched). The review runs in the background; its result/blocker arrives in the inbox (info on CONVERGED, handoff on failure) and is surfaced into the open chat. Do NOT call again for the same spec and do NOT poll. On queued/already_running, tell the user it was launched and end the turn. Pass extraHeads: ["researcher"] for specs referencing external libraries/APIs, or ["debugger"] for root-cause/investigation specs to add opt-in specialist reviewers on top of the fixed four. For a NON-code task (domain seo/marketing/content, ADR 0020): the low-risk lanes `seo`, `marketing`, and `content` proceed to `create_execution_run` after consensus, using the same flow as code tasks. Report the spec\'s review outcome to the operator.',
@@ -56399,6 +56863,8 @@ var mcpToolDescriptions = {
56399
56863
  [mcpToolNames.decideInboxItem]: "Approve, reject, or acknowledge an operator-inbox item. Rejection requires a reason.",
56400
56864
  [mcpToolNames.uploadArtifact]: "Upload a local file (md/txt/html/csv/json/pdf/png/jpg/webp/gif, <=5MB) as a project artifact. Returns a stable markdown link to embed in chat/spec/handoff text. Use class=temp for one-off hand-offs, class=durable for anything referenced by specs or evidence. Pass runId to record the file in that run's artifact history; pass taskId to surface it on the task's Artifacts list.",
56401
56865
  [mcpToolNames.listArtifactFiles]: "List uploaded artifact files for the project (optionally filtered by taskId/runId), newest first.",
56866
+ [mcpToolNames.reclaimExecutionRunLease]: "Clear a stale pending execution-run lease when the lease has aged past staleSeconds. Returns the reclaimed run on success, null when the lease is not stale yet, and surfaces a structured conflict when reclaim retries are exhausted.",
56867
+ [mcpToolNames.restartExecutionRun]: "Restart a parked execution run (handoff_requested or handoff_queued) back to pending so the queue can resume it.",
56402
56868
  [mcpToolNames.resolveHandoff]: "Resolve a tool_assisted handoff for an execution run (outcome success|failure). Success returns the task to the board; failure opens a new handoff cycle. Idempotent.",
56403
56869
  [mcpToolNames.registerLocalCheckout]: "Register the operator-local git checkout path for `projectSlug` so execution-run workers run code on this machine instead of the cloud. Validates the path is a git repo whose `origin` remote matches `repo`, then writes the (slug -> absolute path) entry to the operator-local checkout registry. Call this after locating the checkout on disk (e.g. via filesystem search) and confirming the choice with the user in chat \u2014 they should NOT have to paste the path into Settings. Returns `{ projectSlug, path, confirmedAt }`. Idempotent: re-registering overwrites the prior entry.",
56404
56870
  [mcpToolNames.retrieveContext]: "Rehydrate a mrrlin:// handle emitted by the compact-context layer into the full canonical Mrrlin record (task/wiki page/project snapshot/execution run/artifacts). Use whenever you need the full source \u2014 irreversible decisions (closing tasks, dispatching deploys, writing specs) MUST call this first; never act on a compact envelope alone. Returns `{ kind, data }`. Invalid or unsupported handle \u2192 structured `HANDLE_INVALID` error."
@@ -56808,6 +57274,20 @@ function createMrrlinTools(options) {
56808
57274
  "Unable to reset director thread.",
56809
57275
  async (c, { projectSlug, taskId }) => await c.resetDirectorThread(projectSlug, taskId)
56810
57276
  ),
57277
+ [mcpToolNames.archiveTask]: makeTool(
57278
+ client,
57279
+ mcpToolNames.archiveTask,
57280
+ "TASK_ARCHIVE_FAILED",
57281
+ "Unable to archive task.",
57282
+ async (c, { projectSlug, taskId }) => await c.archiveTask(projectSlug, taskId)
57283
+ ),
57284
+ [mcpToolNames.unarchiveTask]: makeTool(
57285
+ client,
57286
+ mcpToolNames.unarchiveTask,
57287
+ "TASK_UNARCHIVE_FAILED",
57288
+ "Unable to unarchive task.",
57289
+ async (c, { projectSlug, taskId }) => await c.unarchiveTask(projectSlug, taskId)
57290
+ ),
56811
57291
  [mcpToolNames.updateWikiPage]: makeTool(
56812
57292
  client,
56813
57293
  mcpToolNames.updateWikiPage,
@@ -57142,6 +57622,20 @@ function createMrrlinTools(options) {
57142
57622
  "Unable to list artifact files.",
57143
57623
  async (c, { projectSlug, taskId, runId }) => await c.listArtifactFiles(projectSlug, { runId, taskId })
57144
57624
  ),
57625
+ [mcpToolNames.reclaimExecutionRunLease]: makeTool(
57626
+ client,
57627
+ mcpToolNames.reclaimExecutionRunLease,
57628
+ "EXECUTION_RUN_RECLAIM_FAILED",
57629
+ "Unable to reclaim the execution run lease.",
57630
+ async (c, { projectSlug, runId, staleSeconds }) => await c.reclaimExecutionRunLease(projectSlug, runId, staleSeconds)
57631
+ ),
57632
+ [mcpToolNames.restartExecutionRun]: makeTool(
57633
+ client,
57634
+ mcpToolNames.restartExecutionRun,
57635
+ "EXECUTION_RUN_RESTART_FAILED",
57636
+ "Unable to restart the execution run.",
57637
+ async (c, { projectSlug, runId, ...input }) => await c.restartExecutionRun(projectSlug, runId, input)
57638
+ ),
57145
57639
  [mcpToolNames.resolveHandoff]: makeTool(
57146
57640
  client,
57147
57641
  mcpToolNames.resolveHandoff,
@@ -57155,47 +57649,19 @@ function createMrrlinTools(options) {
57155
57649
  "LOCAL_CHECKOUT_REGISTER_FAILED",
57156
57650
  "Unable to register local checkout.",
57157
57651
  async (_c, { projectSlug, path: checkoutPath, repo }) => {
57158
- const trimmedPath = checkoutPath.trim();
57159
- if (!import_node_path20.default.isAbsolute(trimmedPath)) {
57160
- throw new McpToolCodedError(
57161
- "LOCAL_CHECKOUT_PATH_NOT_ABSOLUTE",
57162
- `Path must be absolute, got: ${trimmedPath}`
57163
- );
57164
- }
57165
- let resolvedPath;
57166
- try {
57167
- resolvedPath = (0, import_node_fs15.realpathSync)(trimmedPath);
57168
- } catch {
57169
- throw new McpToolCodedError(
57170
- "LOCAL_CHECKOUT_PATH_NOT_FOUND",
57171
- `Path does not exist or is not accessible: ${trimmedPath}`
57172
- );
57173
- }
57174
- let remoteUrl;
57175
- try {
57176
- remoteUrl = (0, import_node_child_process12.execFileSync)("git", ["-C", resolvedPath, "remote", "get-url", "origin"], {
57177
- encoding: "utf8",
57178
- stdio: ["ignore", "pipe", "ignore"],
57179
- timeout: 15e3,
57180
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
57181
- }).trim();
57182
- } catch {
57183
- throw new McpToolCodedError(
57184
- "LOCAL_CHECKOUT_NOT_A_GIT_REPO",
57185
- `Path is not a git repository or has no origin remote: ${resolvedPath}`
57186
- );
57187
- }
57188
- if (!remoteMatchesRepo(remoteUrl, repo)) {
57189
- throw new McpToolCodedError(
57190
- "LOCAL_CHECKOUT_REMOTE_MISMATCH",
57191
- `Origin remote ${remoteUrl} does not match repo ${repo}.`
57192
- );
57193
- }
57194
57652
  const stateDir = process.env.CODEX_HOME ? import_node_path20.default.join(process.env.CODEX_HOME, "mrrlin", "director-bridge") : import_node_path20.default.join(import_node_os10.default.homedir(), ".mrrlin", "director-bridge");
57195
57653
  const registry3 = new CheckoutRegistry(stateDir);
57196
- const confirmedAt = (/* @__PURE__ */ new Date()).toISOString();
57197
- registry3.confirm(projectSlug, resolvedPath, confirmedAt);
57198
- return { projectSlug, path: resolvedPath, confirmedAt };
57654
+ const result = registerCheckout(registry3, { projectSlug, path: checkoutPath, repo }, import_node_os10.default.hostname());
57655
+ if (!result.ok) {
57656
+ const codeMap = {
57657
+ not_absolute: "LOCAL_CHECKOUT_PATH_NOT_ABSOLUTE",
57658
+ not_found: "LOCAL_CHECKOUT_PATH_NOT_FOUND",
57659
+ not_a_git_repo: "LOCAL_CHECKOUT_NOT_A_GIT_REPO",
57660
+ remote_mismatch: "LOCAL_CHECKOUT_REMOTE_MISMATCH"
57661
+ };
57662
+ throw new McpToolCodedError(codeMap[result.code], result.detail);
57663
+ }
57664
+ return result.data;
57199
57665
  }
57200
57666
  ),
57201
57667
  [mcpToolNames.retrieveContext]: makeTool(