@remixhq/mcp 0.1.12 → 0.1.14

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.
package/dist/server.js CHANGED
@@ -352,14 +352,6 @@ function assertConfirm(confirm, operation) {
352
352
  if (confirm) return;
353
353
  throw createPolicyError(`${operation} requires explicit confirmation.`, "Pass confirm=true to run this tool.");
354
354
  }
355
- function assertDiffWithinLimit(policy, diff) {
356
- const sizeBytes = Buffer.byteLength(diff, "utf8");
357
- if (sizeBytes <= policy.maxDiffBytes) return;
358
- throw createPolicyError(
359
- "Diff exceeds the configured maximum size for Remix MCP.",
360
- `Configured limit=${policy.maxDiffBytes} bytes actual=${sizeBytes} bytes.`
361
- );
362
- }
363
355
 
364
356
  // src/bootstrap/context.ts
365
357
  function createServerContext(params) {
@@ -507,8 +499,6 @@ var finalizeTurnInputSchema = {
507
499
  ...commonRequestFieldsSchema,
508
500
  prompt: z2.string().trim().min(1),
509
501
  assistantResponse: z2.string().trim().min(1),
510
- diffSource: z2.enum(["worktree", "external"]).optional(),
511
- externalDiff: z2.string().optional(),
512
502
  sync: z2.boolean().optional(),
513
503
  allowBranchMismatch: z2.boolean().optional(),
514
504
  idempotencyKey: z2.string().trim().min(1).optional()
@@ -516,11 +506,17 @@ var finalizeTurnInputSchema = {
516
506
  var previewInputSchema = {
517
507
  ...commonRequestFieldsSchema
518
508
  };
509
+ var drainFinalizeQueueInputSchema = {
510
+ ...commonRequestFieldsSchema
511
+ };
519
512
  var applyInputSchema = {
520
513
  ...commonRequestFieldsSchema,
521
514
  confirm: z2.boolean(),
522
515
  allowBranchMismatch: z2.boolean().optional()
523
516
  };
517
+ var reAnchorInputSchema = {
518
+ ...applyInputSchema
519
+ };
524
520
  var requestMergeInputSchema = {
525
521
  ...commonRequestFieldsSchema
526
522
  };
@@ -623,7 +619,7 @@ var statusDataSchema = z2.object({
623
619
  status: genericRecordSchema,
624
620
  riskLevel: z2.enum(["low", "medium", "high"])
625
621
  });
626
- var initDataSchema = z2.object({
622
+ var initSyncDataSchema = z2.object({
627
623
  reused: z2.boolean(),
628
624
  projectId: z2.string(),
629
625
  appId: z2.string(),
@@ -632,8 +628,26 @@ var initDataSchema = z2.object({
632
628
  bindingPath: z2.string(),
633
629
  repoRoot: z2.string(),
634
630
  bindingMode: z2.enum(["legacy", "lane", "explicit_root"]).optional(),
635
- createdCanonicalFamily: z2.boolean().optional()
631
+ createdCanonicalFamily: z2.boolean().optional(),
632
+ baselineStatus: z2.enum(["seeded", "existing", "requires_re_anchor", "requires_sync"]).optional()
633
+ });
634
+ var initQueuedDataSchema = z2.object({
635
+ queued: z2.literal(true),
636
+ status: z2.literal("ready_to_record"),
637
+ doNotRetry: z2.literal(true),
638
+ jobId: z2.string(),
639
+ projectId: z2.string(),
640
+ appId: z2.string(),
641
+ dashboardUrl: z2.string(),
642
+ upstreamAppId: z2.string(),
643
+ bindingPath: z2.string(),
644
+ repoRoot: z2.string(),
645
+ bindingMode: z2.enum(["legacy", "lane", "explicit_root"]),
646
+ createdCanonicalFamily: z2.boolean(),
647
+ remoteUrl: z2.string().nullable(),
648
+ defaultBranch: z2.string().nullable()
636
649
  });
650
+ var initDataSchema = initSyncDataSchema;
637
651
  var listDataSchema = z2.object({
638
652
  apps: genericArraySchema,
639
653
  pagination: paginationSchema
@@ -655,19 +669,26 @@ var checkoutDataSchema = z2.object({
655
669
  repoRoot: z2.string()
656
670
  });
657
671
  var addDataSchema = z2.object({
658
- changeStep: genericRecordSchema,
659
- autoSync: genericRecordSchema
672
+ changeStep: genericRecordSchema
660
673
  });
661
674
  var recordTurnDataSchema = genericRecordSchema;
662
675
  var finalizeTurnDataSchema = z2.object({
663
676
  mode: z2.enum(["changed_turn", "no_diff_turn"]),
664
677
  idempotencyKey: z2.string().min(1),
678
+ queued: z2.boolean(),
679
+ jobId: z2.string().nullable(),
680
+ repoState: z2.string().nullable(),
665
681
  changeStep: genericRecordSchema.nullable(),
666
682
  collabTurn: genericRecordSchema.nullable(),
667
683
  autoSync: genericRecordSchema.nullable(),
668
684
  warnings: z2.array(z2.string())
669
685
  });
686
+ var drainFinalizeQueueDataSchema = z2.object({
687
+ processed: z2.number().int().nonnegative(),
688
+ results: z2.array(genericRecordSchema)
689
+ });
670
690
  var syncDataSchema = genericRecordSchema;
691
+ var reAnchorDataSchema = genericRecordSchema;
671
692
  var requestMergeDataSchema = genericRecordSchema;
672
693
  var mergeRequestQueueDataSchema = z2.object({
673
694
  queue: mergeRequestQueueSchema,
@@ -742,7 +763,9 @@ var checkoutSuccessSchema = makeSuccessSchema(checkoutDataSchema);
742
763
  var addSuccessSchema = makeSuccessSchema(addDataSchema);
743
764
  var recordTurnSuccessSchema = makeSuccessSchema(recordTurnDataSchema);
744
765
  var finalizeTurnSuccessSchema = makeSuccessSchema(finalizeTurnDataSchema);
766
+ var drainFinalizeQueueSuccessSchema = makeSuccessSchema(drainFinalizeQueueDataSchema);
745
767
  var syncSuccessSchema = makeSuccessSchema(syncDataSchema);
768
+ var reAnchorSuccessSchema = makeSuccessSchema(reAnchorDataSchema);
746
769
  var requestMergeSuccessSchema = makeSuccessSchema(requestMergeDataSchema);
747
770
  var mergeRequestQueueSuccessSchema = makeSuccessSchema(mergeRequestQueueDataSchema);
748
771
  var viewMergeRequestSuccessSchema = makeSuccessSchema(viewMergeRequestDataSchema);
@@ -760,17 +783,18 @@ var accessDebugSuccessSchema = makeSuccessSchema(accessDebugDataSchema);
760
783
  var updateMemberRoleSuccessSchema = makeSuccessSchema(updateMemberRoleDataSchema);
761
784
 
762
785
  // src/domain/coreAdapter.ts
786
+ import { spawn } from "child_process";
763
787
  import {
764
788
  collabList as coreCollabList,
765
789
  collabListMembers as coreCollabListMembers,
766
790
  collabUpdateMemberRole as coreCollabUpdateMemberRole,
767
- collabAdd as coreCollabAdd,
791
+ drainPendingFinalizeQueue as coreDrainPendingFinalizeQueue,
768
792
  collabFinalizeTurn as coreCollabFinalizeTurn,
769
- collabRecordTurn as coreCollabRecordTurn,
770
793
  collabApprove as coreCollabApprove,
771
794
  collabCheckout as coreCollabCheckout,
772
795
  collabListMergeRequests as coreCollabListMergeRequests,
773
796
  collabInit as coreCollabInit,
797
+ collabReAnchor as coreCollabReAnchor,
774
798
  collabInvite as coreCollabInvite,
775
799
  collabReconcile as coreCollabReconcile,
776
800
  collabReject as coreCollabReject,
@@ -781,11 +805,13 @@ import {
781
805
  collabSyncUpstream as coreCollabSyncUpstream,
782
806
  collabView as coreCollabView
783
807
  } from "@remixhq/core/collab";
784
- import { findGitRoot, getHeadCommitHash, listUntrackedFiles } from "@remixhq/core/repo";
808
+ import { findGitRoot } from "@remixhq/core/repo";
785
809
  function getRiskLevel(status) {
786
810
  if (status.recommendedAction === "reconcile") return "high";
787
- if (status.recommendedAction === "choose_family") return "medium";
788
- if (status.recommendedAction === "sync" || status.remote.incomingOpenMergeRequestCount) return "medium";
811
+ if (status.recommendedAction === "choose_family" || status.recommendedAction === "await_finalize") return "medium";
812
+ if (status.recommendedAction === "pull" || status.recommendedAction === "re_anchor" || status.remote.incomingOpenMergeRequestCount) {
813
+ return "medium";
814
+ }
789
815
  if (status.repo.branchMismatch || !status.repo.isGitRepo || !status.binding.isBound || !status.repo.worktree.isClean) return "medium";
790
816
  return "low";
791
817
  }
@@ -798,10 +824,22 @@ function getRecommendedNextActions(status) {
798
824
  switch (status.recommendedAction) {
799
825
  case "init":
800
826
  return ["Run remix_collab_init to bind the repository to Remix before using any Remix collaboration mutation flow."];
801
- case "sync":
802
- return ["Run remix_collab_sync_preview, then remix_collab_sync_apply if the preview is acceptable. Use this instead of raw git pull or rebase for bound-repo alignment."];
827
+ case "pull":
828
+ return ["Run remix_collab_sync_preview, then remix_collab_sync_apply if the preview is acceptable. This pulls the server delta into the local working tree without rewriting local git history."];
829
+ case "re_anchor":
830
+ return [
831
+ "Run remix_collab_re_anchor_preview, then remix_collab_re_anchor_apply. This seeds a local Remix baseline. It is required because no local baseline exists for this lane yet (fresh clone, deleted .remix/ state, or first init didn't seed) \u2014 not because of any specific git operation. After it succeeds, normal recording (remix_collab_finalize_turn) becomes available."
832
+ ];
833
+ case "record":
834
+ return [
835
+ "Run remix_collab_finalize_turn to capture the local boundary delta. This is the catch-all for any local content change since the last recorded turn, regardless of whether the change came from agent edits, manual user edits, git commit, git pull, git merge, git rebase, or git reset."
836
+ ];
803
837
  case "reconcile":
804
- return ["Run remix_collab_reconcile_preview before attempting remix_collab_reconcile_apply. Reconcile is the explicit Remix recovery path when fast-forward sync is no longer possible."];
838
+ return ["Run remix_collab_reconcile_preview before attempting remix_collab_reconcile_apply. Reconcile applies only when both the local workspace and the server lane changed since the last agreed baseline."];
839
+ case "await_finalize":
840
+ return [
841
+ "Run remix_collab_drain_finalize_queue before merge-related or recovery flows. finalize_turn is queued only until the local finalize queue is drained."
842
+ ];
805
843
  case "review_queue":
806
844
  return ["Run remix_collab_review_queue to inspect reviewable merge requests instead of using local git merge flows."];
807
845
  case "choose_family":
@@ -835,6 +873,21 @@ function truncateText(value, maxChars) {
835
873
  originalChars: value.length
836
874
  };
837
875
  }
876
+ function spawnFinalizeQueueDrainer() {
877
+ const entrypoint = process.argv[1];
878
+ if (!entrypoint) return false;
879
+ const child = spawn(process.execPath, [...process.execArgv, entrypoint, "--drain-finalize-queue"], {
880
+ detached: true,
881
+ stdio: "ignore",
882
+ env: process.env
883
+ });
884
+ child.unref();
885
+ return true;
886
+ }
887
+ async function drainBeforeMutation(api) {
888
+ const results = await coreDrainPendingFinalizeQueue({ api });
889
+ return results.flatMap((result) => collectResultWarnings(result));
890
+ }
838
891
  async function getStatus(params) {
839
892
  const api = params.includeRemote ? await createCollabApiClient() : null;
840
893
  const status = await coreCollabStatus({
@@ -860,15 +913,26 @@ async function initCollab(params) {
860
913
  api,
861
914
  cwd: params.cwd,
862
915
  appName: params.appName ?? null,
863
- forceNew: params.forceNew ?? false
916
+ forceNew: params.forceNew ?? false,
917
+ asyncSubmit: false
864
918
  });
919
+ if ("queued" in result && result.queued) {
920
+ throw new Error(
921
+ "Unexpected queued init result on the MCP path. Async init is currently disabled for agent callers; if you intended to re-enable it, also restore the queued shape in initDataSchema (see remix-mcp/src/contracts/collab.ts)."
922
+ );
923
+ }
924
+ const syncResult = result;
865
925
  return {
866
- data: result,
926
+ data: syncResult,
867
927
  warnings: collectResultWarnings(result),
868
- recommendedNextActions: ["Run remix_collab_status to inspect sync, reconcile, and merge-request readiness before mutating bound-repo state."],
928
+ recommendedNextActions: syncResult.baselineStatus === "requires_re_anchor" ? [
929
+ "This checkout has no local Remix baseline yet. Run remix_collab_re_anchor_preview, then remix_collab_re_anchor_apply to seed one. After it succeeds, normal recording (remix_collab_finalize_turn) becomes available."
930
+ ] : syncResult.baselineStatus === "requires_sync" ? [
931
+ "Run remix_collab_sync_preview, then remix_collab_sync_apply to pull the server delta and create the first local baseline for this checkout."
932
+ ] : ["Run remix_collab_status to inspect sync, reconcile, and merge-request readiness before mutating bound-repo state."],
869
933
  logContext: {
870
- repoRoot: result.repoRoot,
871
- appId: result.appId
934
+ repoRoot: syncResult.repoRoot,
935
+ appId: syncResult.appId
872
936
  }
873
937
  };
874
938
  }
@@ -935,23 +999,46 @@ async function finalizeCollabTurn(params) {
935
999
  cwd: params.cwd,
936
1000
  prompt: params.prompt,
937
1001
  assistantResponse: params.assistantResponse,
938
- diff: params.externalDiff ?? null,
939
- diffSource: params.diffSource,
940
1002
  sync: params.sync,
941
1003
  allowBranchMismatch: params.allowBranchMismatch ?? false,
942
1004
  idempotencyKey: params.idempotencyKey ?? null,
943
- actor: params.agent
1005
+ actor: params.agent,
1006
+ awaitingUsageDeadlineMs: params.awaitingUsageDeadlineMs ?? null
944
1007
  });
1008
+ const hasAwaitingDeadline = typeof params.awaitingUsageDeadlineMs === "number" && params.awaitingUsageDeadlineMs > 0;
1009
+ if (result.queued && !hasAwaitingDeadline) {
1010
+ if (!spawnFinalizeQueueDrainer()) {
1011
+ await coreDrainPendingFinalizeQueue({ api });
1012
+ }
1013
+ }
945
1014
  return {
946
1015
  data: result,
947
1016
  warnings: result.warnings,
948
- recommendedNextActions: [],
1017
+ recommendedNextActions: result.queued ? ["Run remix_collab_drain_finalize_queue before merge-related flows if you need this queued turn recorded immediately."] : [],
949
1018
  logContext: {
950
1019
  repoRoot,
951
1020
  appId: result.changeStep?.appId ?? result.collabTurn?.appId ?? null
952
1021
  }
953
1022
  };
954
1023
  }
1024
+ async function drainFinalizeQueue(params) {
1025
+ const api = await createCollabApiClient();
1026
+ const repoRoot = await findGitRoot(params.cwd);
1027
+ const results = await coreDrainPendingFinalizeQueue({ api });
1028
+ const warnings = results.flatMap((result) => collectResultWarnings(result));
1029
+ return {
1030
+ data: {
1031
+ processed: results.length,
1032
+ results
1033
+ },
1034
+ warnings,
1035
+ recommendedNextActions: [],
1036
+ logContext: {
1037
+ repoRoot,
1038
+ appId: null
1039
+ }
1040
+ };
1041
+ }
955
1042
  async function syncCollab(params) {
956
1043
  const api = await createCollabApiClient();
957
1044
  const result = await coreCollabSync({
@@ -963,21 +1050,45 @@ async function syncCollab(params) {
963
1050
  return {
964
1051
  data: result,
965
1052
  warnings: collectResultWarnings(result),
966
- recommendedNextActions: params.dryRun ? ["Run remix_collab_sync_apply with confirm=true to apply this fast-forward update instead of using raw git pull or rebase."] : [],
1053
+ recommendedNextActions: params.dryRun ? result.status === "delta_ready" ? ["Run remix_collab_sync_apply with confirm=true to apply this server delta into the local working tree."] : result.status === "base_unknown" ? [
1054
+ "Direct pull is unavailable because Remix cannot diff from the last acknowledged server head.",
1055
+ "Run remix_collab_reconcile_preview next to inspect recovery options before applying any recovery flow."
1056
+ ] : [] : [],
967
1057
  logContext: {
968
1058
  repoRoot: result.repoRoot
969
1059
  }
970
1060
  };
971
1061
  }
1062
+ async function reAnchor(params) {
1063
+ const api = await createCollabApiClient();
1064
+ const result = await coreCollabReAnchor({
1065
+ api,
1066
+ cwd: params.cwd,
1067
+ dryRun: params.dryRun,
1068
+ allowBranchMismatch: params.allowBranchMismatch ?? false
1069
+ });
1070
+ return {
1071
+ data: result,
1072
+ warnings: collectWarnings(result.warnings),
1073
+ recommendedNextActions: params.dryRun ? [
1074
+ "Run remix_collab_re_anchor_apply with confirm=true to seed a local Remix baseline for this checkout. Re-anchor is for missing-baseline cases only and does not replace remix_collab_finalize_turn for ordinary local content changes."
1075
+ ] : [],
1076
+ logContext: {
1077
+ repoRoot: result.repoRoot,
1078
+ appId: result.currentAppId
1079
+ }
1080
+ };
1081
+ }
972
1082
  async function requestMerge(params) {
973
1083
  const api = await createCollabApiClient();
1084
+ const drainWarnings = await drainBeforeMutation(api);
974
1085
  const result = await coreCollabRequestMerge({
975
1086
  api,
976
1087
  cwd: params.cwd
977
1088
  });
978
1089
  return {
979
1090
  data: result,
980
- warnings: [],
1091
+ warnings: drainWarnings,
981
1092
  recommendedNextActions: result.id ? [`Run remix_collab_view_merge_request with mrId=${String(result.id)} to inspect the request before deciding whether to approve or reject it.`] : [],
982
1093
  logContext: {
983
1094
  mrId: typeof result.id === "string" ? result.id : null
@@ -1133,6 +1244,7 @@ async function syncUpstream(params) {
1133
1244
  }
1134
1245
  async function reconcile(params) {
1135
1246
  const api = await createCollabApiClient();
1247
+ const drainWarnings = params.dryRun ? [] : await drainBeforeMutation(api);
1136
1248
  const result = await coreCollabReconcile({
1137
1249
  api,
1138
1250
  cwd: params.cwd,
@@ -1141,9 +1253,9 @@ async function reconcile(params) {
1141
1253
  });
1142
1254
  return {
1143
1255
  data: result,
1144
- warnings: collectWarnings(result.warnings),
1145
- recommendedNextActions: params.dryRun ? ["Run remix_collab_reconcile_apply with confirm=true only if the preview is acceptable. Do not replace this with raw git history-rewrite commands."] : [],
1146
- risks: params.dryRun ? ["Reconcile apply rewrites local history and creates a backup branch."] : [],
1256
+ warnings: [...drainWarnings, ...collectWarnings(result.warnings)],
1257
+ recommendedNextActions: params.dryRun ? ["Run remix_collab_reconcile_apply with confirm=true only if the preview is acceptable. This is the explicit Remix recovery flow for diverged state."] : [],
1258
+ risks: params.dryRun ? ["Reconcile may upload local history to recover the server lane onto the latest agreed state before future recording continues."] : [],
1147
1259
  logContext: {
1148
1260
  repoRoot: result.repoRoot ?? null
1149
1261
  }
@@ -1690,6 +1802,50 @@ async function accessDebug(params) {
1690
1802
  };
1691
1803
  }
1692
1804
 
1805
+ // src/tools/collab/autoSpawnHistoryImport.ts
1806
+ import { spawn as spawn2 } from "child_process";
1807
+ import { existsSync, mkdirSync, openSync } from "fs";
1808
+ import path2 from "path";
1809
+ var MARKER_REL_PATH = path2.join(".remix", ".history-imported");
1810
+ var LOG_REL_PATH = path2.join(".remix", "history-import.log");
1811
+ function shouldAutoSpawnHistoryImport(repoRoot) {
1812
+ try {
1813
+ return !existsSync(path2.join(repoRoot, MARKER_REL_PATH));
1814
+ } catch {
1815
+ return false;
1816
+ }
1817
+ }
1818
+ function spawnHistoryImportDetached(repoRoot) {
1819
+ const remixDir = path2.join(repoRoot, ".remix");
1820
+ try {
1821
+ mkdirSync(remixDir, { recursive: true });
1822
+ } catch {
1823
+ }
1824
+ const logPath = path2.join(repoRoot, LOG_REL_PATH);
1825
+ const out = openSync(logPath, "a");
1826
+ const err = openSync(logPath, "a");
1827
+ const child = spawn2(
1828
+ "remix",
1829
+ [
1830
+ "history",
1831
+ "import",
1832
+ "--repo",
1833
+ repoRoot,
1834
+ // Include prompt text for parity with the CLI auto-spawn path:
1835
+ // first-time UX is a lot worse if the dashboard renders every
1836
+ // historical row as "(prompt not uploaded)".
1837
+ "--include-prompt-text"
1838
+ ],
1839
+ {
1840
+ detached: true,
1841
+ stdio: ["ignore", out, err],
1842
+ env: { ...process.env, REMIX_HISTORY_AUTO_SPAWN: "1" }
1843
+ }
1844
+ );
1845
+ child.unref();
1846
+ return { pid: child.pid, logPath };
1847
+ }
1848
+
1693
1849
  // src/tools/collab/register.ts
1694
1850
  function getAnnotations(access, options) {
1695
1851
  if (access === "read") {
@@ -1719,9 +1875,6 @@ function buildSuccessEnvelope(tool, requestId, result) {
1719
1875
  };
1720
1876
  }
1721
1877
  function deriveErrorRisks(tool, normalized) {
1722
- if (isFinalizeTurnLocalSyncFailure(tool, normalized)) {
1723
- return ["The change step succeeded remotely, but the local repository may need manual recovery or a follow-up sync."];
1724
- }
1725
1878
  if (normalized.code === "DESTRUCTIVE_OPERATION_BLOCKED") {
1726
1879
  return ["A policy guard blocked a potentially destructive or state-mutating operation."];
1727
1880
  }
@@ -1733,16 +1886,9 @@ function deriveErrorRisks(tool, normalized) {
1733
1886
  }
1734
1887
  return [];
1735
1888
  }
1736
- function isFinalizeTurnLocalSyncFailure(tool, normalized) {
1737
- return tool === "remix_collab_finalize_turn" && normalized.message === "Change step succeeded remotely, but automatic local sync failed.";
1738
- }
1739
1889
  function buildErrorEnvelope(tool, requestId, error) {
1740
1890
  const normalized = normalizeToolError(error);
1741
- const recommendedNextActions = isFinalizeTurnLocalSyncFailure(tool, normalized) ? [
1742
- "Run `remix_collab_status` to confirm the bound repo state before attempting recovery.",
1743
- "Run `remix_collab_sync_preview` next, then `remix_collab_sync_apply` with `confirm=true` if the preview looks correct.",
1744
- "Inspect `error.hint` for any preserved diff backup path before retrying local recovery, and do not rerun `remix_collab_finalize_turn` immediately."
1745
- ] : normalized.code === "AUTH_REQUIRED" ? ["Set COMERGE_ACCESS_TOKEN, then retry the tool call."] : normalized.code === "REPO_LOCK_TIMEOUT" ? ["Wait for the active Remix mutation to finish, then retry the tool call."] : normalized.code === "REPO_STATE_CHANGED_DURING_OPERATION" ? ["Review local repository changes, then rerun the tool once the worktree is stable."] : normalized.code === "PREFERRED_BRANCH_MISMATCH" ? ["Switch to the repository's preferred Remix branch, or rerun with allowBranchMismatch=true if intentional."] : [];
1891
+ const recommendedNextActions = normalized.code === "AUTH_REQUIRED" ? ["Set COMERGE_ACCESS_TOKEN, then retry the tool call."] : normalized.code === "REPO_LOCK_TIMEOUT" ? ["Wait for the active Remix mutation to finish, then retry the tool call."] : normalized.code === "REPO_STATE_CHANGED_DURING_OPERATION" ? ["Review local repository changes, then rerun the tool once the worktree is stable."] : normalized.code === "PREFERRED_BRANCH_MISMATCH" ? ["Switch to the repository's preferred Remix branch, or rerun with allowBranchMismatch=true if intentional."] : [];
1746
1892
  return {
1747
1893
  schemaVersion: SCHEMA_VERSION,
1748
1894
  ok: false,
@@ -1821,18 +1967,37 @@ function registerCollabTools(server, context) {
1821
1967
  });
1822
1968
  registerTool(server, context, {
1823
1969
  name: "remix_collab_init",
1824
- description: "Import the current repository into Remix and write the local binding file.",
1970
+ description: "Import the current repository into Remix and write the local binding file. Synchronous: by the time this tool resolves, the local binding file AND the local Remix baseline are both on disk, so the very next call to remix_collab_finalize_turn will succeed. Brand-new init on the default branch typically takes ~10s; non-default-branch init can take 30-90s while the server provisions a feature lane. The result includes `reused: boolean` (false for a brand-new app, true if a binding already existed) plus the canonical app/project identifiers and the dashboard URL. Use forceNew=true only when intentionally creating a new canonical family from scratch in a previously-bound repo; do NOT use forceNew as a retry mechanism for a failed init \u2014 it creates orphan backend apps and triggers canonical-family ambiguity errors on subsequent inits in this directory.",
1825
1971
  access: "remote_write",
1826
1972
  inputSchema: initInputSchema,
1827
1973
  outputSchema: initSuccessSchema,
1828
1974
  run: async (args) => {
1829
1975
  const input = z3.object(initInputSchema).parse(args);
1830
1976
  const cwd = resolvePolicyCwd(context.policy, input.cwd);
1831
- return initCollab({
1977
+ const result = await initCollab({
1832
1978
  cwd,
1833
1979
  appName: input.appName,
1834
1980
  forceNew: input.forceNew
1835
1981
  });
1982
+ try {
1983
+ const repoRoot = result && typeof result === "object" && "data" in result && result.data && typeof result.data.repoRoot === "string" ? result.data.repoRoot : null;
1984
+ if (repoRoot && shouldAutoSpawnHistoryImport(repoRoot)) {
1985
+ const spawned = spawnHistoryImportDetached(repoRoot);
1986
+ context.logger.log({
1987
+ level: "info",
1988
+ message: `history_import_auto_spawned pid=${spawned.pid ?? "?"} log=${spawned.logPath}`,
1989
+ tool: "remix_collab_init",
1990
+ repoRoot
1991
+ });
1992
+ }
1993
+ } catch (spawnError) {
1994
+ context.logger.log({
1995
+ level: "error",
1996
+ message: `history_import_auto_spawn_failed: ${spawnError instanceof Error ? spawnError.message : String(spawnError)}`,
1997
+ tool: "remix_collab_init"
1998
+ });
1999
+ }
2000
+ return result;
1836
2001
  }
1837
2002
  });
1838
2003
  registerTool(server, context, {
@@ -1888,7 +2053,7 @@ function registerCollabTools(server, context) {
1888
2053
  });
1889
2054
  registerTool(server, context, {
1890
2055
  name: "remix_collab_finalize_turn",
1891
- description: "Primary turn recorder for the current bound repository. Call this exactly once before the final response; it records a changed turn when the worktree has a diff, records a no-diff turn when it does not, and can accept an explicit external diff when needed.",
2056
+ description: "Primary turn recorder for the current bound repository. Required: call this exactly once before the final response on every turn that touched a Remix-bound repo. Captures the current boundary locally and queues remote processing. Queued only: no remote change step exists yet until the finalize queue drains. Runtime exception: if this turn's context contains a [Remix runtime status] system note from the Remix UserPromptSubmit hook stating that the Stop hook will record this turn automatically, do NOT call this tool \u2014 the runtime is recording it on your behalf and a manual call would create a duplicate record. The runtime note is the only sanctioned override; trivial prompts, error states, and ambiguity all still require this call.",
1892
2057
  access: "local_write",
1893
2058
  inputSchema: finalizeTurnInputSchema,
1894
2059
  outputSchema: finalizeTurnSuccessSchema,
@@ -1896,25 +2061,34 @@ function registerCollabTools(server, context) {
1896
2061
  run: async (args) => {
1897
2062
  const input = z3.object(finalizeTurnInputSchema).parse(args);
1898
2063
  const cwd = resolvePolicyCwd(context.policy, input.cwd);
1899
- if ((input.diffSource ?? "worktree") === "external" || typeof input.externalDiff === "string") {
1900
- assertDiffWithinLimit(context.policy, input.externalDiff ?? "");
1901
- }
1902
2064
  return finalizeCollabTurn({
1903
2065
  cwd,
1904
2066
  prompt: input.prompt,
1905
2067
  assistantResponse: input.assistantResponse,
1906
- diffSource: input.diffSource,
1907
- externalDiff: input.externalDiff,
1908
2068
  sync: input.sync,
1909
2069
  allowBranchMismatch: input.allowBranchMismatch ?? false,
1910
2070
  idempotencyKey: input.idempotencyKey,
1911
- agent: context.agentMetadata
2071
+ agent: context.agentMetadata,
2072
+ awaitingUsageDeadlineMs: 3e4
1912
2073
  });
1913
2074
  }
1914
2075
  });
2076
+ registerTool(server, context, {
2077
+ name: "remix_collab_drain_finalize_queue",
2078
+ description: "Drain the local finalize queue and record queued finalize_turn jobs immediately. NOT required as a precondition for `remix_collab_request_merge` or `remix_collab_reconcile_apply` \u2014 those tools drain the queue internally before they run. Useful only for explicit recovery flows (e.g. status reports `await_finalize` and you want to flush before re-checking). Runtime exception: if this turn's context contains a [Remix runtime status] system note from the Remix UserPromptSubmit hook, the runtime drains the queue automatically in the background; do NOT call this tool unless an explicit recovery flow requires it.",
2079
+ access: "local_write",
2080
+ inputSchema: drainFinalizeQueueInputSchema,
2081
+ outputSchema: drainFinalizeQueueSuccessSchema,
2082
+ annotations: getAnnotations("local_write", { idempotent: true }),
2083
+ run: async (args) => {
2084
+ const input = z3.object(drainFinalizeQueueInputSchema).parse(args);
2085
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
2086
+ return drainFinalizeQueue({ cwd });
2087
+ }
2088
+ });
1915
2089
  registerTool(server, context, {
1916
2090
  name: "remix_collab_sync_preview",
1917
- description: "Preview whether the current bound repository can be fast-forward synced to the Remix app state. Use this instead of raw git pull or rebase for bound-repo alignment.",
2091
+ description: "Preview whether the current bound repository can pull the server delta into the working tree. Use this instead of raw git pull or rebase for bound-repo alignment.",
1918
2092
  access: "read",
1919
2093
  inputSchema: previewInputSchema,
1920
2094
  outputSchema: syncSuccessSchema,
@@ -1926,7 +2100,7 @@ function registerCollabTools(server, context) {
1926
2100
  });
1927
2101
  registerTool(server, context, {
1928
2102
  name: "remix_collab_sync_apply",
1929
- description: "Fast-forward sync the current bound repository to the Remix app state. This is the Remix-native replacement for raw git pull or rebase in a bound repo.",
2103
+ description: "Pull the server delta into the local working tree without moving local git history. This is the Remix-native replacement for raw git pull or rebase in a bound repo.",
1930
2104
  access: "local_write",
1931
2105
  inputSchema: applyInputSchema,
1932
2106
  outputSchema: syncSuccessSchema,
@@ -1937,6 +2111,31 @@ function registerCollabTools(server, context) {
1937
2111
  return syncCollab({ cwd, dryRun: false, allowBranchMismatch: input.allowBranchMismatch ?? false });
1938
2112
  }
1939
2113
  });
2114
+ registerTool(server, context, {
2115
+ name: "remix_collab_re_anchor_preview",
2116
+ description: "Preview whether this checkout needs a fresh local Remix baseline. Use only when status reports `re_anchor` (no local baseline exists for this lane yet \u2014 fresh clone, deleted `.remix/` state, or first init didn't seed). Re-anchor does not replace `remix_collab_finalize_turn`; ordinary local content changes (including merges, pulls, and rebases) are recorded by `finalize-turn`, not by re-anchor.",
2117
+ access: "read",
2118
+ inputSchema: previewInputSchema,
2119
+ outputSchema: reAnchorSuccessSchema,
2120
+ run: async (args) => {
2121
+ const input = z3.object(previewInputSchema).parse(args);
2122
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
2123
+ return reAnchor({ cwd, dryRun: true });
2124
+ }
2125
+ });
2126
+ registerTool(server, context, {
2127
+ name: "remix_collab_re_anchor_apply",
2128
+ description: "Establish a local Remix baseline for the current checkout against the existing app head, without rewriting the local checkout afterward. Required only when status reports `re_anchor` (missing local baseline). It does not replace `remix_collab_finalize_turn` \u2014 local commits, pulls, merges, and rebases must still be recorded with `finalize-turn`.",
2129
+ access: "local_write",
2130
+ inputSchema: reAnchorInputSchema,
2131
+ outputSchema: reAnchorSuccessSchema,
2132
+ run: async (args) => {
2133
+ const input = z3.object(reAnchorInputSchema).parse(args);
2134
+ assertConfirm(input.confirm, "remix_collab_re_anchor_apply");
2135
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
2136
+ return reAnchor({ cwd, dryRun: false, allowBranchMismatch: input.allowBranchMismatch ?? false });
2137
+ }
2138
+ });
1940
2139
  registerTool(server, context, {
1941
2140
  name: "remix_collab_request_merge",
1942
2141
  description: "Open a prompt-backed Remix merge request from the current bound repository to its upstream app instead of merging locally with raw git.",
@@ -2077,7 +2276,7 @@ function registerCollabTools(server, context) {
2077
2276
  });
2078
2277
  registerTool(server, context, {
2079
2278
  name: "remix_collab_reconcile_preview",
2080
- description: "Preview reconcile readiness when the local repository cannot be fast-forward synced. Use this before any explicit Remix history-repair workflow.",
2279
+ description: "Preview the explicit Remix recovery flow when local and server state diverged beyond a simple pull.",
2081
2280
  access: "read",
2082
2281
  inputSchema: previewInputSchema,
2083
2282
  outputSchema: reconcileSuccessSchema,
@@ -2089,7 +2288,7 @@ function registerCollabTools(server, context) {
2089
2288
  });
2090
2289
  registerTool(server, context, {
2091
2290
  name: "remix_collab_reconcile_apply",
2092
- description: "Reconcile divergent local history against the bound Remix app and update the local checkout. This is the explicit Remix recovery path for divergent history.",
2291
+ description: "Run the explicit Remix recovery flow for diverged local/server state.",
2093
2292
  access: "local_write",
2094
2293
  inputSchema: applyInputSchema,
2095
2294
  outputSchema: reconcileSuccessSchema,
@@ -3214,6 +3413,19 @@ import { z as z9 } from "zod";
3214
3413
  // src/contracts/ops.ts
3215
3414
  import { z as z8 } from "zod";
3216
3415
  var genericRecordSchema4 = z8.record(z8.string(), z8.unknown());
3416
+ var appJobKindSchema = z8.enum([
3417
+ "fork",
3418
+ "edit",
3419
+ "bundle",
3420
+ "import_github",
3421
+ "import_upload",
3422
+ "merge",
3423
+ "revert",
3424
+ "change_step",
3425
+ "reconcile",
3426
+ "change_step_replay"
3427
+ ]);
3428
+ var appJobStatusSchema = z8.enum(["pending", "enqueued", "processing", "succeeded", "failed", "cancelled"]);
3217
3429
  var appScopedInputSchema = {
3218
3430
  ...commonRequestFieldsSchema,
3219
3431
  appId: z8.string().trim().min(1).optional()
@@ -3223,6 +3435,13 @@ var editQueueInputSchema = {
3223
3435
  limit: z8.number().int().positive().max(100).optional(),
3224
3436
  offset: z8.number().int().nonnegative().optional()
3225
3437
  };
3438
+ var appJobQueueInputSchema = {
3439
+ ...appScopedInputSchema,
3440
+ limit: z8.number().int().positive().max(100).optional(),
3441
+ offset: z8.number().int().nonnegative().optional(),
3442
+ kind: z8.array(appJobKindSchema).min(1).optional(),
3443
+ status: z8.array(appJobStatusSchema).min(1).optional()
3444
+ };
3226
3445
  var bundleInputSchema = {
3227
3446
  ...appScopedInputSchema,
3228
3447
  bundleId: z8.string().trim().min(1)
@@ -3255,6 +3474,7 @@ var agentRunEventsInputSchema = {
3255
3474
  };
3256
3475
  var appOverviewSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3257
3476
  var editQueueSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3477
+ var appJobQueueSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3258
3478
  var bundleSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3259
3479
  var timelineSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3260
3480
  var agentRunsSuccessSchema = makeSuccessSchema(genericRecordSchema4);
@@ -3288,7 +3508,7 @@ async function getAppOverview(params) {
3288
3508
  data,
3289
3509
  warnings: [],
3290
3510
  recommendedNextActions: [
3291
- "Use `remix_ops_list_timeline`, `remix_ops_list_agent_runs`, `remix_ops_get_edit_queue`, or `remix_ops_get_sandbox_status` for the next operational drill-down on this app."
3511
+ "Use `remix_ops_list_timeline`, `remix_ops_list_agent_runs`, `remix_ops_get_edit_queue`, `remix_ops_list_app_job_queue`, or `remix_ops_get_sandbox_status` for the next operational drill-down on this app."
3292
3512
  ],
3293
3513
  logContext: target
3294
3514
  };
@@ -3311,6 +3531,42 @@ async function getEditQueue(params) {
3311
3531
  logContext: target
3312
3532
  };
3313
3533
  }
3534
+ async function listAppJobQueue(params) {
3535
+ const api = await createApiClient();
3536
+ const target = await resolveAppTarget(api, params);
3537
+ const data = unwrapResponseObject(
3538
+ await api.listAppJobQueue(target.appId, {
3539
+ limit: params.limit,
3540
+ offset: params.offset,
3541
+ kind: params.kind,
3542
+ status: params.status
3543
+ }),
3544
+ "app job queue"
3545
+ );
3546
+ const pageInfo = typeof data.pageInfo === "object" && data.pageInfo ? data.pageInfo : null;
3547
+ const items = Array.isArray(data.items) ? data.items : [];
3548
+ const activeBlockingKinds = /* @__PURE__ */ new Set(["merge", "change_step", "change_step_replay", "reconcile", "revert"]);
3549
+ const hasBlockingJobs = items.some((item) => {
3550
+ if (!item || typeof item !== "object") return false;
3551
+ const record = item;
3552
+ return typeof record.kind === "string" && activeBlockingKinds.has(record.kind) && (typeof record.status !== "string" || ["pending", "enqueued", "processing"].includes(record.status));
3553
+ });
3554
+ const recommendedNextActions = [];
3555
+ if (hasBlockingJobs) {
3556
+ recommendedNextActions.push(
3557
+ "Inspect the returned active workflow jobs before concluding that merge, replay, reconcile, or revert work is stuck. This surface reflects backend app-scoped queue state, not local finalize state."
3558
+ );
3559
+ }
3560
+ if (pageInfo?.hasMore === true && typeof pageInfo.limit === "number" && typeof pageInfo.offset === "number") {
3561
+ recommendedNextActions.push(`Pass offset=${pageInfo.offset + pageInfo.limit} to load the next app job queue page.`);
3562
+ }
3563
+ return {
3564
+ data,
3565
+ warnings: [],
3566
+ recommendedNextActions,
3567
+ logContext: target
3568
+ };
3569
+ }
3314
3570
  async function getBundle(params) {
3315
3571
  const api = await createApiClient();
3316
3572
  const target = await resolveAppTarget(api, params);
@@ -3537,6 +3793,25 @@ function registerOpsTools(server, context) {
3537
3793
  });
3538
3794
  }
3539
3795
  });
3796
+ registerTool4(server, context, {
3797
+ name: "remix_ops_list_app_job_queue",
3798
+ description: "Inspect bounded app-scoped workflow queue jobs for one app, including active backend work such as merge, replay, reconcile, revert, bundle, or import processing.",
3799
+ access: "read",
3800
+ inputSchema: appJobQueueInputSchema,
3801
+ outputSchema: appJobQueueSuccessSchema,
3802
+ run: async (args) => {
3803
+ const input = z9.object(appJobQueueInputSchema).parse(args);
3804
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
3805
+ return listAppJobQueue({
3806
+ appId: input.appId,
3807
+ cwd,
3808
+ limit: input.limit,
3809
+ offset: input.offset,
3810
+ kind: input.kind,
3811
+ status: input.status
3812
+ });
3813
+ }
3814
+ });
3540
3815
  registerTool4(server, context, {
3541
3816
  name: "remix_ops_get_bundle",
3542
3817
  description: "Inspect one app bundle by id without downloading the artifact payload.",