@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/cli.js CHANGED
@@ -2,10 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { createRequire } from "module";
5
-
6
- // src/server.ts
7
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { drainAsyncJobs, drainPendingFinalizeQueue } from "@remixhq/core/collab";
9
6
 
10
7
  // src/domain/apiClient.ts
11
8
  import { createApiClient as createCoreApiClient, resolveConfig as resolveConfig2 } from "@remixhq/core";
@@ -42,6 +39,10 @@ async function createApiClient() {
42
39
  });
43
40
  }
44
41
 
42
+ // src/server.ts
43
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
44
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
45
+
45
46
  // src/observability/logger.ts
46
47
  function createLogger() {
47
48
  return {
@@ -355,14 +356,6 @@ function assertConfirm(confirm, operation) {
355
356
  if (confirm) return;
356
357
  throw createPolicyError(`${operation} requires explicit confirmation.`, "Pass confirm=true to run this tool.");
357
358
  }
358
- function assertDiffWithinLimit(policy, diff) {
359
- const sizeBytes = Buffer.byteLength(diff, "utf8");
360
- if (sizeBytes <= policy.maxDiffBytes) return;
361
- throw createPolicyError(
362
- "Diff exceeds the configured maximum size for Remix MCP.",
363
- `Configured limit=${policy.maxDiffBytes} bytes actual=${sizeBytes} bytes.`
364
- );
365
- }
366
359
 
367
360
  // src/bootstrap/context.ts
368
361
  function createServerContext(params) {
@@ -510,8 +503,6 @@ var finalizeTurnInputSchema = {
510
503
  ...commonRequestFieldsSchema,
511
504
  prompt: z2.string().trim().min(1),
512
505
  assistantResponse: z2.string().trim().min(1),
513
- diffSource: z2.enum(["worktree", "external"]).optional(),
514
- externalDiff: z2.string().optional(),
515
506
  sync: z2.boolean().optional(),
516
507
  allowBranchMismatch: z2.boolean().optional(),
517
508
  idempotencyKey: z2.string().trim().min(1).optional()
@@ -519,11 +510,17 @@ var finalizeTurnInputSchema = {
519
510
  var previewInputSchema = {
520
511
  ...commonRequestFieldsSchema
521
512
  };
513
+ var drainFinalizeQueueInputSchema = {
514
+ ...commonRequestFieldsSchema
515
+ };
522
516
  var applyInputSchema = {
523
517
  ...commonRequestFieldsSchema,
524
518
  confirm: z2.boolean(),
525
519
  allowBranchMismatch: z2.boolean().optional()
526
520
  };
521
+ var reAnchorInputSchema = {
522
+ ...applyInputSchema
523
+ };
527
524
  var requestMergeInputSchema = {
528
525
  ...commonRequestFieldsSchema
529
526
  };
@@ -626,7 +623,7 @@ var statusDataSchema = z2.object({
626
623
  status: genericRecordSchema,
627
624
  riskLevel: z2.enum(["low", "medium", "high"])
628
625
  });
629
- var initDataSchema = z2.object({
626
+ var initSyncDataSchema = z2.object({
630
627
  reused: z2.boolean(),
631
628
  projectId: z2.string(),
632
629
  appId: z2.string(),
@@ -635,8 +632,26 @@ var initDataSchema = z2.object({
635
632
  bindingPath: z2.string(),
636
633
  repoRoot: z2.string(),
637
634
  bindingMode: z2.enum(["legacy", "lane", "explicit_root"]).optional(),
638
- createdCanonicalFamily: z2.boolean().optional()
635
+ createdCanonicalFamily: z2.boolean().optional(),
636
+ baselineStatus: z2.enum(["seeded", "existing", "requires_re_anchor", "requires_sync"]).optional()
639
637
  });
638
+ var initQueuedDataSchema = z2.object({
639
+ queued: z2.literal(true),
640
+ status: z2.literal("ready_to_record"),
641
+ doNotRetry: z2.literal(true),
642
+ jobId: z2.string(),
643
+ projectId: z2.string(),
644
+ appId: z2.string(),
645
+ dashboardUrl: z2.string(),
646
+ upstreamAppId: z2.string(),
647
+ bindingPath: z2.string(),
648
+ repoRoot: z2.string(),
649
+ bindingMode: z2.enum(["legacy", "lane", "explicit_root"]),
650
+ createdCanonicalFamily: z2.boolean(),
651
+ remoteUrl: z2.string().nullable(),
652
+ defaultBranch: z2.string().nullable()
653
+ });
654
+ var initDataSchema = initSyncDataSchema;
640
655
  var listDataSchema = z2.object({
641
656
  apps: genericArraySchema,
642
657
  pagination: paginationSchema
@@ -658,19 +673,26 @@ var checkoutDataSchema = z2.object({
658
673
  repoRoot: z2.string()
659
674
  });
660
675
  var addDataSchema = z2.object({
661
- changeStep: genericRecordSchema,
662
- autoSync: genericRecordSchema
676
+ changeStep: genericRecordSchema
663
677
  });
664
678
  var recordTurnDataSchema = genericRecordSchema;
665
679
  var finalizeTurnDataSchema = z2.object({
666
680
  mode: z2.enum(["changed_turn", "no_diff_turn"]),
667
681
  idempotencyKey: z2.string().min(1),
682
+ queued: z2.boolean(),
683
+ jobId: z2.string().nullable(),
684
+ repoState: z2.string().nullable(),
668
685
  changeStep: genericRecordSchema.nullable(),
669
686
  collabTurn: genericRecordSchema.nullable(),
670
687
  autoSync: genericRecordSchema.nullable(),
671
688
  warnings: z2.array(z2.string())
672
689
  });
690
+ var drainFinalizeQueueDataSchema = z2.object({
691
+ processed: z2.number().int().nonnegative(),
692
+ results: z2.array(genericRecordSchema)
693
+ });
673
694
  var syncDataSchema = genericRecordSchema;
695
+ var reAnchorDataSchema = genericRecordSchema;
674
696
  var requestMergeDataSchema = genericRecordSchema;
675
697
  var mergeRequestQueueDataSchema = z2.object({
676
698
  queue: mergeRequestQueueSchema,
@@ -745,7 +767,9 @@ var checkoutSuccessSchema = makeSuccessSchema(checkoutDataSchema);
745
767
  var addSuccessSchema = makeSuccessSchema(addDataSchema);
746
768
  var recordTurnSuccessSchema = makeSuccessSchema(recordTurnDataSchema);
747
769
  var finalizeTurnSuccessSchema = makeSuccessSchema(finalizeTurnDataSchema);
770
+ var drainFinalizeQueueSuccessSchema = makeSuccessSchema(drainFinalizeQueueDataSchema);
748
771
  var syncSuccessSchema = makeSuccessSchema(syncDataSchema);
772
+ var reAnchorSuccessSchema = makeSuccessSchema(reAnchorDataSchema);
749
773
  var requestMergeSuccessSchema = makeSuccessSchema(requestMergeDataSchema);
750
774
  var mergeRequestQueueSuccessSchema = makeSuccessSchema(mergeRequestQueueDataSchema);
751
775
  var viewMergeRequestSuccessSchema = makeSuccessSchema(viewMergeRequestDataSchema);
@@ -763,17 +787,18 @@ var accessDebugSuccessSchema = makeSuccessSchema(accessDebugDataSchema);
763
787
  var updateMemberRoleSuccessSchema = makeSuccessSchema(updateMemberRoleDataSchema);
764
788
 
765
789
  // src/domain/coreAdapter.ts
790
+ import { spawn } from "child_process";
766
791
  import {
767
792
  collabList as coreCollabList,
768
793
  collabListMembers as coreCollabListMembers,
769
794
  collabUpdateMemberRole as coreCollabUpdateMemberRole,
770
- collabAdd as coreCollabAdd,
795
+ drainPendingFinalizeQueue as coreDrainPendingFinalizeQueue,
771
796
  collabFinalizeTurn as coreCollabFinalizeTurn,
772
- collabRecordTurn as coreCollabRecordTurn,
773
797
  collabApprove as coreCollabApprove,
774
798
  collabCheckout as coreCollabCheckout,
775
799
  collabListMergeRequests as coreCollabListMergeRequests,
776
800
  collabInit as coreCollabInit,
801
+ collabReAnchor as coreCollabReAnchor,
777
802
  collabInvite as coreCollabInvite,
778
803
  collabReconcile as coreCollabReconcile,
779
804
  collabReject as coreCollabReject,
@@ -784,11 +809,13 @@ import {
784
809
  collabSyncUpstream as coreCollabSyncUpstream,
785
810
  collabView as coreCollabView
786
811
  } from "@remixhq/core/collab";
787
- import { findGitRoot, getHeadCommitHash, listUntrackedFiles } from "@remixhq/core/repo";
812
+ import { findGitRoot } from "@remixhq/core/repo";
788
813
  function getRiskLevel(status) {
789
814
  if (status.recommendedAction === "reconcile") return "high";
790
- if (status.recommendedAction === "choose_family") return "medium";
791
- if (status.recommendedAction === "sync" || status.remote.incomingOpenMergeRequestCount) return "medium";
815
+ if (status.recommendedAction === "choose_family" || status.recommendedAction === "await_finalize") return "medium";
816
+ if (status.recommendedAction === "pull" || status.recommendedAction === "re_anchor" || status.remote.incomingOpenMergeRequestCount) {
817
+ return "medium";
818
+ }
792
819
  if (status.repo.branchMismatch || !status.repo.isGitRepo || !status.binding.isBound || !status.repo.worktree.isClean) return "medium";
793
820
  return "low";
794
821
  }
@@ -801,10 +828,22 @@ function getRecommendedNextActions(status) {
801
828
  switch (status.recommendedAction) {
802
829
  case "init":
803
830
  return ["Run remix_collab_init to bind the repository to Remix before using any Remix collaboration mutation flow."];
804
- case "sync":
805
- 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."];
831
+ case "pull":
832
+ 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."];
833
+ case "re_anchor":
834
+ return [
835
+ "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."
836
+ ];
837
+ case "record":
838
+ return [
839
+ "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."
840
+ ];
806
841
  case "reconcile":
807
- 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."];
842
+ 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."];
843
+ case "await_finalize":
844
+ return [
845
+ "Run remix_collab_drain_finalize_queue before merge-related or recovery flows. finalize_turn is queued only until the local finalize queue is drained."
846
+ ];
808
847
  case "review_queue":
809
848
  return ["Run remix_collab_review_queue to inspect reviewable merge requests instead of using local git merge flows."];
810
849
  case "choose_family":
@@ -838,6 +877,21 @@ function truncateText(value, maxChars) {
838
877
  originalChars: value.length
839
878
  };
840
879
  }
880
+ function spawnFinalizeQueueDrainer() {
881
+ const entrypoint = process.argv[1];
882
+ if (!entrypoint) return false;
883
+ const child = spawn(process.execPath, [...process.execArgv, entrypoint, "--drain-finalize-queue"], {
884
+ detached: true,
885
+ stdio: "ignore",
886
+ env: process.env
887
+ });
888
+ child.unref();
889
+ return true;
890
+ }
891
+ async function drainBeforeMutation(api) {
892
+ const results = await coreDrainPendingFinalizeQueue({ api });
893
+ return results.flatMap((result) => collectResultWarnings(result));
894
+ }
841
895
  async function getStatus(params) {
842
896
  const api = params.includeRemote ? await createCollabApiClient() : null;
843
897
  const status = await coreCollabStatus({
@@ -863,15 +917,26 @@ async function initCollab(params) {
863
917
  api,
864
918
  cwd: params.cwd,
865
919
  appName: params.appName ?? null,
866
- forceNew: params.forceNew ?? false
920
+ forceNew: params.forceNew ?? false,
921
+ asyncSubmit: false
867
922
  });
923
+ if ("queued" in result && result.queued) {
924
+ throw new Error(
925
+ "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)."
926
+ );
927
+ }
928
+ const syncResult = result;
868
929
  return {
869
- data: result,
930
+ data: syncResult,
870
931
  warnings: collectResultWarnings(result),
871
- recommendedNextActions: ["Run remix_collab_status to inspect sync, reconcile, and merge-request readiness before mutating bound-repo state."],
932
+ recommendedNextActions: syncResult.baselineStatus === "requires_re_anchor" ? [
933
+ "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."
934
+ ] : syncResult.baselineStatus === "requires_sync" ? [
935
+ "Run remix_collab_sync_preview, then remix_collab_sync_apply to pull the server delta and create the first local baseline for this checkout."
936
+ ] : ["Run remix_collab_status to inspect sync, reconcile, and merge-request readiness before mutating bound-repo state."],
872
937
  logContext: {
873
- repoRoot: result.repoRoot,
874
- appId: result.appId
938
+ repoRoot: syncResult.repoRoot,
939
+ appId: syncResult.appId
875
940
  }
876
941
  };
877
942
  }
@@ -938,23 +1003,46 @@ async function finalizeCollabTurn(params) {
938
1003
  cwd: params.cwd,
939
1004
  prompt: params.prompt,
940
1005
  assistantResponse: params.assistantResponse,
941
- diff: params.externalDiff ?? null,
942
- diffSource: params.diffSource,
943
1006
  sync: params.sync,
944
1007
  allowBranchMismatch: params.allowBranchMismatch ?? false,
945
1008
  idempotencyKey: params.idempotencyKey ?? null,
946
- actor: params.agent
1009
+ actor: params.agent,
1010
+ awaitingUsageDeadlineMs: params.awaitingUsageDeadlineMs ?? null
947
1011
  });
1012
+ const hasAwaitingDeadline = typeof params.awaitingUsageDeadlineMs === "number" && params.awaitingUsageDeadlineMs > 0;
1013
+ if (result.queued && !hasAwaitingDeadline) {
1014
+ if (!spawnFinalizeQueueDrainer()) {
1015
+ await coreDrainPendingFinalizeQueue({ api });
1016
+ }
1017
+ }
948
1018
  return {
949
1019
  data: result,
950
1020
  warnings: result.warnings,
951
- recommendedNextActions: [],
1021
+ recommendedNextActions: result.queued ? ["Run remix_collab_drain_finalize_queue before merge-related flows if you need this queued turn recorded immediately."] : [],
952
1022
  logContext: {
953
1023
  repoRoot,
954
1024
  appId: result.changeStep?.appId ?? result.collabTurn?.appId ?? null
955
1025
  }
956
1026
  };
957
1027
  }
1028
+ async function drainFinalizeQueue(params) {
1029
+ const api = await createCollabApiClient();
1030
+ const repoRoot = await findGitRoot(params.cwd);
1031
+ const results = await coreDrainPendingFinalizeQueue({ api });
1032
+ const warnings = results.flatMap((result) => collectResultWarnings(result));
1033
+ return {
1034
+ data: {
1035
+ processed: results.length,
1036
+ results
1037
+ },
1038
+ warnings,
1039
+ recommendedNextActions: [],
1040
+ logContext: {
1041
+ repoRoot,
1042
+ appId: null
1043
+ }
1044
+ };
1045
+ }
958
1046
  async function syncCollab(params) {
959
1047
  const api = await createCollabApiClient();
960
1048
  const result = await coreCollabSync({
@@ -966,21 +1054,45 @@ async function syncCollab(params) {
966
1054
  return {
967
1055
  data: result,
968
1056
  warnings: collectResultWarnings(result),
969
- 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."] : [],
1057
+ 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" ? [
1058
+ "Direct pull is unavailable because Remix cannot diff from the last acknowledged server head.",
1059
+ "Run remix_collab_reconcile_preview next to inspect recovery options before applying any recovery flow."
1060
+ ] : [] : [],
970
1061
  logContext: {
971
1062
  repoRoot: result.repoRoot
972
1063
  }
973
1064
  };
974
1065
  }
1066
+ async function reAnchor(params) {
1067
+ const api = await createCollabApiClient();
1068
+ const result = await coreCollabReAnchor({
1069
+ api,
1070
+ cwd: params.cwd,
1071
+ dryRun: params.dryRun,
1072
+ allowBranchMismatch: params.allowBranchMismatch ?? false
1073
+ });
1074
+ return {
1075
+ data: result,
1076
+ warnings: collectWarnings(result.warnings),
1077
+ recommendedNextActions: params.dryRun ? [
1078
+ "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."
1079
+ ] : [],
1080
+ logContext: {
1081
+ repoRoot: result.repoRoot,
1082
+ appId: result.currentAppId
1083
+ }
1084
+ };
1085
+ }
975
1086
  async function requestMerge(params) {
976
1087
  const api = await createCollabApiClient();
1088
+ const drainWarnings = await drainBeforeMutation(api);
977
1089
  const result = await coreCollabRequestMerge({
978
1090
  api,
979
1091
  cwd: params.cwd
980
1092
  });
981
1093
  return {
982
1094
  data: result,
983
- warnings: [],
1095
+ warnings: drainWarnings,
984
1096
  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.`] : [],
985
1097
  logContext: {
986
1098
  mrId: typeof result.id === "string" ? result.id : null
@@ -1136,6 +1248,7 @@ async function syncUpstream(params) {
1136
1248
  }
1137
1249
  async function reconcile(params) {
1138
1250
  const api = await createCollabApiClient();
1251
+ const drainWarnings = params.dryRun ? [] : await drainBeforeMutation(api);
1139
1252
  const result = await coreCollabReconcile({
1140
1253
  api,
1141
1254
  cwd: params.cwd,
@@ -1144,9 +1257,9 @@ async function reconcile(params) {
1144
1257
  });
1145
1258
  return {
1146
1259
  data: result,
1147
- warnings: collectWarnings(result.warnings),
1148
- 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."] : [],
1149
- risks: params.dryRun ? ["Reconcile apply rewrites local history and creates a backup branch."] : [],
1260
+ warnings: [...drainWarnings, ...collectWarnings(result.warnings)],
1261
+ 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."] : [],
1262
+ risks: params.dryRun ? ["Reconcile may upload local history to recover the server lane onto the latest agreed state before future recording continues."] : [],
1150
1263
  logContext: {
1151
1264
  repoRoot: result.repoRoot ?? null
1152
1265
  }
@@ -1693,6 +1806,50 @@ async function accessDebug(params) {
1693
1806
  };
1694
1807
  }
1695
1808
 
1809
+ // src/tools/collab/autoSpawnHistoryImport.ts
1810
+ import { spawn as spawn2 } from "child_process";
1811
+ import { existsSync, mkdirSync, openSync } from "fs";
1812
+ import path2 from "path";
1813
+ var MARKER_REL_PATH = path2.join(".remix", ".history-imported");
1814
+ var LOG_REL_PATH = path2.join(".remix", "history-import.log");
1815
+ function shouldAutoSpawnHistoryImport(repoRoot) {
1816
+ try {
1817
+ return !existsSync(path2.join(repoRoot, MARKER_REL_PATH));
1818
+ } catch {
1819
+ return false;
1820
+ }
1821
+ }
1822
+ function spawnHistoryImportDetached(repoRoot) {
1823
+ const remixDir = path2.join(repoRoot, ".remix");
1824
+ try {
1825
+ mkdirSync(remixDir, { recursive: true });
1826
+ } catch {
1827
+ }
1828
+ const logPath = path2.join(repoRoot, LOG_REL_PATH);
1829
+ const out = openSync(logPath, "a");
1830
+ const err = openSync(logPath, "a");
1831
+ const child = spawn2(
1832
+ "remix",
1833
+ [
1834
+ "history",
1835
+ "import",
1836
+ "--repo",
1837
+ repoRoot,
1838
+ // Include prompt text for parity with the CLI auto-spawn path:
1839
+ // first-time UX is a lot worse if the dashboard renders every
1840
+ // historical row as "(prompt not uploaded)".
1841
+ "--include-prompt-text"
1842
+ ],
1843
+ {
1844
+ detached: true,
1845
+ stdio: ["ignore", out, err],
1846
+ env: { ...process.env, REMIX_HISTORY_AUTO_SPAWN: "1" }
1847
+ }
1848
+ );
1849
+ child.unref();
1850
+ return { pid: child.pid, logPath };
1851
+ }
1852
+
1696
1853
  // src/tools/collab/register.ts
1697
1854
  function getAnnotations(access, options) {
1698
1855
  if (access === "read") {
@@ -1722,9 +1879,6 @@ function buildSuccessEnvelope(tool, requestId, result) {
1722
1879
  };
1723
1880
  }
1724
1881
  function deriveErrorRisks(tool, normalized) {
1725
- if (isFinalizeTurnLocalSyncFailure(tool, normalized)) {
1726
- return ["The change step succeeded remotely, but the local repository may need manual recovery or a follow-up sync."];
1727
- }
1728
1882
  if (normalized.code === "DESTRUCTIVE_OPERATION_BLOCKED") {
1729
1883
  return ["A policy guard blocked a potentially destructive or state-mutating operation."];
1730
1884
  }
@@ -1736,16 +1890,9 @@ function deriveErrorRisks(tool, normalized) {
1736
1890
  }
1737
1891
  return [];
1738
1892
  }
1739
- function isFinalizeTurnLocalSyncFailure(tool, normalized) {
1740
- return tool === "remix_collab_finalize_turn" && normalized.message === "Change step succeeded remotely, but automatic local sync failed.";
1741
- }
1742
1893
  function buildErrorEnvelope(tool, requestId, error) {
1743
1894
  const normalized = normalizeToolError(error);
1744
- const recommendedNextActions = isFinalizeTurnLocalSyncFailure(tool, normalized) ? [
1745
- "Run `remix_collab_status` to confirm the bound repo state before attempting recovery.",
1746
- "Run `remix_collab_sync_preview` next, then `remix_collab_sync_apply` with `confirm=true` if the preview looks correct.",
1747
- "Inspect `error.hint` for any preserved diff backup path before retrying local recovery, and do not rerun `remix_collab_finalize_turn` immediately."
1748
- ] : 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."] : [];
1895
+ 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."] : [];
1749
1896
  return {
1750
1897
  schemaVersion: SCHEMA_VERSION,
1751
1898
  ok: false,
@@ -1824,18 +1971,37 @@ function registerCollabTools(server, context) {
1824
1971
  });
1825
1972
  registerTool(server, context, {
1826
1973
  name: "remix_collab_init",
1827
- description: "Import the current repository into Remix and write the local binding file.",
1974
+ 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.",
1828
1975
  access: "remote_write",
1829
1976
  inputSchema: initInputSchema,
1830
1977
  outputSchema: initSuccessSchema,
1831
1978
  run: async (args) => {
1832
1979
  const input = z3.object(initInputSchema).parse(args);
1833
1980
  const cwd = resolvePolicyCwd(context.policy, input.cwd);
1834
- return initCollab({
1981
+ const result = await initCollab({
1835
1982
  cwd,
1836
1983
  appName: input.appName,
1837
1984
  forceNew: input.forceNew
1838
1985
  });
1986
+ try {
1987
+ const repoRoot = result && typeof result === "object" && "data" in result && result.data && typeof result.data.repoRoot === "string" ? result.data.repoRoot : null;
1988
+ if (repoRoot && shouldAutoSpawnHistoryImport(repoRoot)) {
1989
+ const spawned = spawnHistoryImportDetached(repoRoot);
1990
+ context.logger.log({
1991
+ level: "info",
1992
+ message: `history_import_auto_spawned pid=${spawned.pid ?? "?"} log=${spawned.logPath}`,
1993
+ tool: "remix_collab_init",
1994
+ repoRoot
1995
+ });
1996
+ }
1997
+ } catch (spawnError) {
1998
+ context.logger.log({
1999
+ level: "error",
2000
+ message: `history_import_auto_spawn_failed: ${spawnError instanceof Error ? spawnError.message : String(spawnError)}`,
2001
+ tool: "remix_collab_init"
2002
+ });
2003
+ }
2004
+ return result;
1839
2005
  }
1840
2006
  });
1841
2007
  registerTool(server, context, {
@@ -1891,7 +2057,7 @@ function registerCollabTools(server, context) {
1891
2057
  });
1892
2058
  registerTool(server, context, {
1893
2059
  name: "remix_collab_finalize_turn",
1894
- 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.",
2060
+ 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.",
1895
2061
  access: "local_write",
1896
2062
  inputSchema: finalizeTurnInputSchema,
1897
2063
  outputSchema: finalizeTurnSuccessSchema,
@@ -1899,25 +2065,34 @@ function registerCollabTools(server, context) {
1899
2065
  run: async (args) => {
1900
2066
  const input = z3.object(finalizeTurnInputSchema).parse(args);
1901
2067
  const cwd = resolvePolicyCwd(context.policy, input.cwd);
1902
- if ((input.diffSource ?? "worktree") === "external" || typeof input.externalDiff === "string") {
1903
- assertDiffWithinLimit(context.policy, input.externalDiff ?? "");
1904
- }
1905
2068
  return finalizeCollabTurn({
1906
2069
  cwd,
1907
2070
  prompt: input.prompt,
1908
2071
  assistantResponse: input.assistantResponse,
1909
- diffSource: input.diffSource,
1910
- externalDiff: input.externalDiff,
1911
2072
  sync: input.sync,
1912
2073
  allowBranchMismatch: input.allowBranchMismatch ?? false,
1913
2074
  idempotencyKey: input.idempotencyKey,
1914
- agent: context.agentMetadata
2075
+ agent: context.agentMetadata,
2076
+ awaitingUsageDeadlineMs: 3e4
1915
2077
  });
1916
2078
  }
1917
2079
  });
2080
+ registerTool(server, context, {
2081
+ name: "remix_collab_drain_finalize_queue",
2082
+ 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.",
2083
+ access: "local_write",
2084
+ inputSchema: drainFinalizeQueueInputSchema,
2085
+ outputSchema: drainFinalizeQueueSuccessSchema,
2086
+ annotations: getAnnotations("local_write", { idempotent: true }),
2087
+ run: async (args) => {
2088
+ const input = z3.object(drainFinalizeQueueInputSchema).parse(args);
2089
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
2090
+ return drainFinalizeQueue({ cwd });
2091
+ }
2092
+ });
1918
2093
  registerTool(server, context, {
1919
2094
  name: "remix_collab_sync_preview",
1920
- 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.",
2095
+ 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.",
1921
2096
  access: "read",
1922
2097
  inputSchema: previewInputSchema,
1923
2098
  outputSchema: syncSuccessSchema,
@@ -1929,7 +2104,7 @@ function registerCollabTools(server, context) {
1929
2104
  });
1930
2105
  registerTool(server, context, {
1931
2106
  name: "remix_collab_sync_apply",
1932
- 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.",
2107
+ 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.",
1933
2108
  access: "local_write",
1934
2109
  inputSchema: applyInputSchema,
1935
2110
  outputSchema: syncSuccessSchema,
@@ -1940,6 +2115,31 @@ function registerCollabTools(server, context) {
1940
2115
  return syncCollab({ cwd, dryRun: false, allowBranchMismatch: input.allowBranchMismatch ?? false });
1941
2116
  }
1942
2117
  });
2118
+ registerTool(server, context, {
2119
+ name: "remix_collab_re_anchor_preview",
2120
+ 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.",
2121
+ access: "read",
2122
+ inputSchema: previewInputSchema,
2123
+ outputSchema: reAnchorSuccessSchema,
2124
+ run: async (args) => {
2125
+ const input = z3.object(previewInputSchema).parse(args);
2126
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
2127
+ return reAnchor({ cwd, dryRun: true });
2128
+ }
2129
+ });
2130
+ registerTool(server, context, {
2131
+ name: "remix_collab_re_anchor_apply",
2132
+ 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`.",
2133
+ access: "local_write",
2134
+ inputSchema: reAnchorInputSchema,
2135
+ outputSchema: reAnchorSuccessSchema,
2136
+ run: async (args) => {
2137
+ const input = z3.object(reAnchorInputSchema).parse(args);
2138
+ assertConfirm(input.confirm, "remix_collab_re_anchor_apply");
2139
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
2140
+ return reAnchor({ cwd, dryRun: false, allowBranchMismatch: input.allowBranchMismatch ?? false });
2141
+ }
2142
+ });
1943
2143
  registerTool(server, context, {
1944
2144
  name: "remix_collab_request_merge",
1945
2145
  description: "Open a prompt-backed Remix merge request from the current bound repository to its upstream app instead of merging locally with raw git.",
@@ -2080,7 +2280,7 @@ function registerCollabTools(server, context) {
2080
2280
  });
2081
2281
  registerTool(server, context, {
2082
2282
  name: "remix_collab_reconcile_preview",
2083
- description: "Preview reconcile readiness when the local repository cannot be fast-forward synced. Use this before any explicit Remix history-repair workflow.",
2283
+ description: "Preview the explicit Remix recovery flow when local and server state diverged beyond a simple pull.",
2084
2284
  access: "read",
2085
2285
  inputSchema: previewInputSchema,
2086
2286
  outputSchema: reconcileSuccessSchema,
@@ -2092,7 +2292,7 @@ function registerCollabTools(server, context) {
2092
2292
  });
2093
2293
  registerTool(server, context, {
2094
2294
  name: "remix_collab_reconcile_apply",
2095
- 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.",
2295
+ description: "Run the explicit Remix recovery flow for diverged local/server state.",
2096
2296
  access: "local_write",
2097
2297
  inputSchema: applyInputSchema,
2098
2298
  outputSchema: reconcileSuccessSchema,
@@ -3217,6 +3417,19 @@ import { z as z9 } from "zod";
3217
3417
  // src/contracts/ops.ts
3218
3418
  import { z as z8 } from "zod";
3219
3419
  var genericRecordSchema4 = z8.record(z8.string(), z8.unknown());
3420
+ var appJobKindSchema = z8.enum([
3421
+ "fork",
3422
+ "edit",
3423
+ "bundle",
3424
+ "import_github",
3425
+ "import_upload",
3426
+ "merge",
3427
+ "revert",
3428
+ "change_step",
3429
+ "reconcile",
3430
+ "change_step_replay"
3431
+ ]);
3432
+ var appJobStatusSchema = z8.enum(["pending", "enqueued", "processing", "succeeded", "failed", "cancelled"]);
3220
3433
  var appScopedInputSchema = {
3221
3434
  ...commonRequestFieldsSchema,
3222
3435
  appId: z8.string().trim().min(1).optional()
@@ -3226,6 +3439,13 @@ var editQueueInputSchema = {
3226
3439
  limit: z8.number().int().positive().max(100).optional(),
3227
3440
  offset: z8.number().int().nonnegative().optional()
3228
3441
  };
3442
+ var appJobQueueInputSchema = {
3443
+ ...appScopedInputSchema,
3444
+ limit: z8.number().int().positive().max(100).optional(),
3445
+ offset: z8.number().int().nonnegative().optional(),
3446
+ kind: z8.array(appJobKindSchema).min(1).optional(),
3447
+ status: z8.array(appJobStatusSchema).min(1).optional()
3448
+ };
3229
3449
  var bundleInputSchema = {
3230
3450
  ...appScopedInputSchema,
3231
3451
  bundleId: z8.string().trim().min(1)
@@ -3258,6 +3478,7 @@ var agentRunEventsInputSchema = {
3258
3478
  };
3259
3479
  var appOverviewSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3260
3480
  var editQueueSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3481
+ var appJobQueueSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3261
3482
  var bundleSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3262
3483
  var timelineSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3263
3484
  var agentRunsSuccessSchema = makeSuccessSchema(genericRecordSchema4);
@@ -3291,7 +3512,7 @@ async function getAppOverview(params) {
3291
3512
  data,
3292
3513
  warnings: [],
3293
3514
  recommendedNextActions: [
3294
- "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."
3515
+ "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."
3295
3516
  ],
3296
3517
  logContext: target
3297
3518
  };
@@ -3314,6 +3535,42 @@ async function getEditQueue(params) {
3314
3535
  logContext: target
3315
3536
  };
3316
3537
  }
3538
+ async function listAppJobQueue(params) {
3539
+ const api = await createApiClient();
3540
+ const target = await resolveAppTarget(api, params);
3541
+ const data = unwrapResponseObject(
3542
+ await api.listAppJobQueue(target.appId, {
3543
+ limit: params.limit,
3544
+ offset: params.offset,
3545
+ kind: params.kind,
3546
+ status: params.status
3547
+ }),
3548
+ "app job queue"
3549
+ );
3550
+ const pageInfo = typeof data.pageInfo === "object" && data.pageInfo ? data.pageInfo : null;
3551
+ const items = Array.isArray(data.items) ? data.items : [];
3552
+ const activeBlockingKinds = /* @__PURE__ */ new Set(["merge", "change_step", "change_step_replay", "reconcile", "revert"]);
3553
+ const hasBlockingJobs = items.some((item) => {
3554
+ if (!item || typeof item !== "object") return false;
3555
+ const record = item;
3556
+ return typeof record.kind === "string" && activeBlockingKinds.has(record.kind) && (typeof record.status !== "string" || ["pending", "enqueued", "processing"].includes(record.status));
3557
+ });
3558
+ const recommendedNextActions = [];
3559
+ if (hasBlockingJobs) {
3560
+ recommendedNextActions.push(
3561
+ "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."
3562
+ );
3563
+ }
3564
+ if (pageInfo?.hasMore === true && typeof pageInfo.limit === "number" && typeof pageInfo.offset === "number") {
3565
+ recommendedNextActions.push(`Pass offset=${pageInfo.offset + pageInfo.limit} to load the next app job queue page.`);
3566
+ }
3567
+ return {
3568
+ data,
3569
+ warnings: [],
3570
+ recommendedNextActions,
3571
+ logContext: target
3572
+ };
3573
+ }
3317
3574
  async function getBundle(params) {
3318
3575
  const api = await createApiClient();
3319
3576
  const target = await resolveAppTarget(api, params);
@@ -3540,6 +3797,25 @@ function registerOpsTools(server, context) {
3540
3797
  });
3541
3798
  }
3542
3799
  });
3800
+ registerTool4(server, context, {
3801
+ name: "remix_ops_list_app_job_queue",
3802
+ 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.",
3803
+ access: "read",
3804
+ inputSchema: appJobQueueInputSchema,
3805
+ outputSchema: appJobQueueSuccessSchema,
3806
+ run: async (args) => {
3807
+ const input = z9.object(appJobQueueInputSchema).parse(args);
3808
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
3809
+ return listAppJobQueue({
3810
+ appId: input.appId,
3811
+ cwd,
3812
+ limit: input.limit,
3813
+ offset: input.offset,
3814
+ kind: input.kind,
3815
+ status: input.status
3816
+ });
3817
+ }
3818
+ });
3543
3819
  registerTool4(server, context, {
3544
3820
  name: "remix_ops_get_bundle",
3545
3821
  description: "Inspect one app bundle by id without downloading the artifact payload.",
@@ -3670,6 +3946,16 @@ async function startStdioServer(params) {
3670
3946
  var require2 = createRequire(import.meta.url);
3671
3947
  var { version } = require2("../package.json");
3672
3948
  async function main() {
3949
+ if (process.argv.includes("--drain-finalize-queue")) {
3950
+ const api = await createCollabApiClient();
3951
+ await drainPendingFinalizeQueue({ api });
3952
+ return;
3953
+ }
3954
+ if (process.argv.includes("--drain-async-jobs")) {
3955
+ const api = await createCollabApiClient();
3956
+ await drainAsyncJobs({ api });
3957
+ return;
3958
+ }
3673
3959
  await startStdioServer({ version });
3674
3960
  }
3675
3961
  main().catch((error) => {