@remixhq/mcp 0.1.12 → 0.1.13

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 { 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
  };
@@ -635,7 +632,8 @@ 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
  });
640
638
  var listDataSchema = z2.object({
641
639
  apps: genericArraySchema,
@@ -658,19 +656,26 @@ var checkoutDataSchema = z2.object({
658
656
  repoRoot: z2.string()
659
657
  });
660
658
  var addDataSchema = z2.object({
661
- changeStep: genericRecordSchema,
662
- autoSync: genericRecordSchema
659
+ changeStep: genericRecordSchema
663
660
  });
664
661
  var recordTurnDataSchema = genericRecordSchema;
665
662
  var finalizeTurnDataSchema = z2.object({
666
663
  mode: z2.enum(["changed_turn", "no_diff_turn"]),
667
664
  idempotencyKey: z2.string().min(1),
665
+ queued: z2.boolean(),
666
+ jobId: z2.string().nullable(),
667
+ repoState: z2.string().nullable(),
668
668
  changeStep: genericRecordSchema.nullable(),
669
669
  collabTurn: genericRecordSchema.nullable(),
670
670
  autoSync: genericRecordSchema.nullable(),
671
671
  warnings: z2.array(z2.string())
672
672
  });
673
+ var drainFinalizeQueueDataSchema = z2.object({
674
+ processed: z2.number().int().nonnegative(),
675
+ results: z2.array(genericRecordSchema)
676
+ });
673
677
  var syncDataSchema = genericRecordSchema;
678
+ var reAnchorDataSchema = genericRecordSchema;
674
679
  var requestMergeDataSchema = genericRecordSchema;
675
680
  var mergeRequestQueueDataSchema = z2.object({
676
681
  queue: mergeRequestQueueSchema,
@@ -745,7 +750,9 @@ var checkoutSuccessSchema = makeSuccessSchema(checkoutDataSchema);
745
750
  var addSuccessSchema = makeSuccessSchema(addDataSchema);
746
751
  var recordTurnSuccessSchema = makeSuccessSchema(recordTurnDataSchema);
747
752
  var finalizeTurnSuccessSchema = makeSuccessSchema(finalizeTurnDataSchema);
753
+ var drainFinalizeQueueSuccessSchema = makeSuccessSchema(drainFinalizeQueueDataSchema);
748
754
  var syncSuccessSchema = makeSuccessSchema(syncDataSchema);
755
+ var reAnchorSuccessSchema = makeSuccessSchema(reAnchorDataSchema);
749
756
  var requestMergeSuccessSchema = makeSuccessSchema(requestMergeDataSchema);
750
757
  var mergeRequestQueueSuccessSchema = makeSuccessSchema(mergeRequestQueueDataSchema);
751
758
  var viewMergeRequestSuccessSchema = makeSuccessSchema(viewMergeRequestDataSchema);
@@ -763,17 +770,18 @@ var accessDebugSuccessSchema = makeSuccessSchema(accessDebugDataSchema);
763
770
  var updateMemberRoleSuccessSchema = makeSuccessSchema(updateMemberRoleDataSchema);
764
771
 
765
772
  // src/domain/coreAdapter.ts
773
+ import { spawn } from "child_process";
766
774
  import {
767
775
  collabList as coreCollabList,
768
776
  collabListMembers as coreCollabListMembers,
769
777
  collabUpdateMemberRole as coreCollabUpdateMemberRole,
770
- collabAdd as coreCollabAdd,
778
+ drainPendingFinalizeQueue as coreDrainPendingFinalizeQueue,
771
779
  collabFinalizeTurn as coreCollabFinalizeTurn,
772
- collabRecordTurn as coreCollabRecordTurn,
773
780
  collabApprove as coreCollabApprove,
774
781
  collabCheckout as coreCollabCheckout,
775
782
  collabListMergeRequests as coreCollabListMergeRequests,
776
783
  collabInit as coreCollabInit,
784
+ collabReAnchor as coreCollabReAnchor,
777
785
  collabInvite as coreCollabInvite,
778
786
  collabReconcile as coreCollabReconcile,
779
787
  collabReject as coreCollabReject,
@@ -784,11 +792,13 @@ import {
784
792
  collabSyncUpstream as coreCollabSyncUpstream,
785
793
  collabView as coreCollabView
786
794
  } from "@remixhq/core/collab";
787
- import { findGitRoot, getHeadCommitHash, listUntrackedFiles } from "@remixhq/core/repo";
795
+ import { findGitRoot } from "@remixhq/core/repo";
788
796
  function getRiskLevel(status) {
789
797
  if (status.recommendedAction === "reconcile") return "high";
790
- if (status.recommendedAction === "choose_family") return "medium";
791
- if (status.recommendedAction === "sync" || status.remote.incomingOpenMergeRequestCount) return "medium";
798
+ if (status.recommendedAction === "choose_family" || status.recommendedAction === "await_finalize") return "medium";
799
+ if (status.recommendedAction === "pull" || status.recommendedAction === "re_anchor" || status.remote.incomingOpenMergeRequestCount) {
800
+ return "medium";
801
+ }
792
802
  if (status.repo.branchMismatch || !status.repo.isGitRepo || !status.binding.isBound || !status.repo.worktree.isClean) return "medium";
793
803
  return "low";
794
804
  }
@@ -801,10 +811,16 @@ function getRecommendedNextActions(status) {
801
811
  switch (status.recommendedAction) {
802
812
  case "init":
803
813
  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."];
814
+ case "pull":
815
+ 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."];
816
+ case "re_anchor":
817
+ return ["Run remix_collab_re_anchor_preview, then remix_collab_re_anchor_apply to re-anchor the lane after manual Git/GitHub history movement."];
806
818
  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."];
819
+ return ["Run remix_collab_reconcile_preview before attempting remix_collab_reconcile_apply. Reconcile now routes through the explicit Remix recovery/re-anchor flow for diverged state."];
820
+ case "await_finalize":
821
+ return [
822
+ "Run remix_collab_drain_finalize_queue before merge-related or recovery flows. finalize_turn is queued only until the local finalize queue is drained."
823
+ ];
808
824
  case "review_queue":
809
825
  return ["Run remix_collab_review_queue to inspect reviewable merge requests instead of using local git merge flows."];
810
826
  case "choose_family":
@@ -838,6 +854,17 @@ function truncateText(value, maxChars) {
838
854
  originalChars: value.length
839
855
  };
840
856
  }
857
+ function spawnFinalizeQueueDrainer() {
858
+ const entrypoint = process.argv[1];
859
+ if (!entrypoint) return false;
860
+ const child = spawn(process.execPath, [...process.execArgv, entrypoint, "--drain-finalize-queue"], {
861
+ detached: true,
862
+ stdio: "ignore",
863
+ env: process.env
864
+ });
865
+ child.unref();
866
+ return true;
867
+ }
841
868
  async function getStatus(params) {
842
869
  const api = params.includeRemote ? await createCollabApiClient() : null;
843
870
  const status = await coreCollabStatus({
@@ -868,7 +895,11 @@ async function initCollab(params) {
868
895
  return {
869
896
  data: result,
870
897
  warnings: collectResultWarnings(result),
871
- recommendedNextActions: ["Run remix_collab_status to inspect sync, reconcile, and merge-request readiness before mutating bound-repo state."],
898
+ recommendedNextActions: result.baselineStatus === "requires_re_anchor" ? [
899
+ "Run remix_collab_re_anchor_preview, then remix_collab_re_anchor_apply to anchor this checkout before recording turns."
900
+ ] : result.baselineStatus === "requires_sync" ? [
901
+ "Run remix_collab_sync_preview, then remix_collab_sync_apply to pull the server delta and create the first local baseline for this checkout."
902
+ ] : ["Run remix_collab_status to inspect sync, reconcile, and merge-request readiness before mutating bound-repo state."],
872
903
  logContext: {
873
904
  repoRoot: result.repoRoot,
874
905
  appId: result.appId
@@ -938,23 +969,44 @@ async function finalizeCollabTurn(params) {
938
969
  cwd: params.cwd,
939
970
  prompt: params.prompt,
940
971
  assistantResponse: params.assistantResponse,
941
- diff: params.externalDiff ?? null,
942
- diffSource: params.diffSource,
943
972
  sync: params.sync,
944
973
  allowBranchMismatch: params.allowBranchMismatch ?? false,
945
974
  idempotencyKey: params.idempotencyKey ?? null,
946
975
  actor: params.agent
947
976
  });
977
+ if (result.queued) {
978
+ if (!spawnFinalizeQueueDrainer()) {
979
+ await coreDrainPendingFinalizeQueue({ api });
980
+ }
981
+ }
948
982
  return {
949
983
  data: result,
950
984
  warnings: result.warnings,
951
- recommendedNextActions: [],
985
+ recommendedNextActions: result.queued ? ["Run remix_collab_drain_finalize_queue before merge-related flows if you need this queued turn recorded immediately."] : [],
952
986
  logContext: {
953
987
  repoRoot,
954
988
  appId: result.changeStep?.appId ?? result.collabTurn?.appId ?? null
955
989
  }
956
990
  };
957
991
  }
992
+ async function drainFinalizeQueue(params) {
993
+ const api = await createCollabApiClient();
994
+ const repoRoot = await findGitRoot(params.cwd);
995
+ const results = await coreDrainPendingFinalizeQueue({ api });
996
+ const warnings = results.flatMap((result) => collectResultWarnings(result));
997
+ return {
998
+ data: {
999
+ processed: results.length,
1000
+ results
1001
+ },
1002
+ warnings,
1003
+ recommendedNextActions: [],
1004
+ logContext: {
1005
+ repoRoot,
1006
+ appId: null
1007
+ }
1008
+ };
1009
+ }
958
1010
  async function syncCollab(params) {
959
1011
  const api = await createCollabApiClient();
960
1012
  const result = await coreCollabSync({
@@ -966,12 +1018,33 @@ async function syncCollab(params) {
966
1018
  return {
967
1019
  data: result,
968
1020
  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."] : [],
1021
+ 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" ? [
1022
+ "Direct pull is unavailable because Remix cannot diff from the last acknowledged server head.",
1023
+ "Run remix_collab_reconcile_preview next to inspect recovery options before applying any recovery flow."
1024
+ ] : [] : [],
970
1025
  logContext: {
971
1026
  repoRoot: result.repoRoot
972
1027
  }
973
1028
  };
974
1029
  }
1030
+ async function reAnchor(params) {
1031
+ const api = await createCollabApiClient();
1032
+ const result = await coreCollabReAnchor({
1033
+ api,
1034
+ cwd: params.cwd,
1035
+ dryRun: params.dryRun,
1036
+ allowBranchMismatch: params.allowBranchMismatch ?? false
1037
+ });
1038
+ return {
1039
+ data: result,
1040
+ warnings: collectWarnings(result.warnings),
1041
+ recommendedNextActions: params.dryRun ? ["Run remix_collab_re_anchor_apply with confirm=true to re-anchor this lane after manual Git/GitHub history movement."] : [],
1042
+ logContext: {
1043
+ repoRoot: result.repoRoot,
1044
+ appId: result.currentAppId
1045
+ }
1046
+ };
1047
+ }
975
1048
  async function requestMerge(params) {
976
1049
  const api = await createCollabApiClient();
977
1050
  const result = await coreCollabRequestMerge({
@@ -1145,8 +1218,8 @@ async function reconcile(params) {
1145
1218
  return {
1146
1219
  data: result,
1147
1220
  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."] : [],
1221
+ 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."] : [],
1222
+ risks: params.dryRun ? ["Reconcile may upload local history to re-anchor the server lane before future recording continues."] : [],
1150
1223
  logContext: {
1151
1224
  repoRoot: result.repoRoot ?? null
1152
1225
  }
@@ -1722,9 +1795,6 @@ function buildSuccessEnvelope(tool, requestId, result) {
1722
1795
  };
1723
1796
  }
1724
1797
  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
1798
  if (normalized.code === "DESTRUCTIVE_OPERATION_BLOCKED") {
1729
1799
  return ["A policy guard blocked a potentially destructive or state-mutating operation."];
1730
1800
  }
@@ -1736,16 +1806,9 @@ function deriveErrorRisks(tool, normalized) {
1736
1806
  }
1737
1807
  return [];
1738
1808
  }
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
1809
  function buildErrorEnvelope(tool, requestId, error) {
1743
1810
  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."] : [];
1811
+ 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
1812
  return {
1750
1813
  schemaVersion: SCHEMA_VERSION,
1751
1814
  ok: false,
@@ -1891,7 +1954,7 @@ function registerCollabTools(server, context) {
1891
1954
  });
1892
1955
  registerTool(server, context, {
1893
1956
  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.",
1957
+ description: "Primary turn recorder for the current bound repository. Call this exactly once before the final response; it captures the current boundary locally and queues remote processing. Queued only: no remote change step exists yet until the finalize queue is drained.",
1895
1958
  access: "local_write",
1896
1959
  inputSchema: finalizeTurnInputSchema,
1897
1960
  outputSchema: finalizeTurnSuccessSchema,
@@ -1899,15 +1962,10 @@ function registerCollabTools(server, context) {
1899
1962
  run: async (args) => {
1900
1963
  const input = z3.object(finalizeTurnInputSchema).parse(args);
1901
1964
  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
1965
  return finalizeCollabTurn({
1906
1966
  cwd,
1907
1967
  prompt: input.prompt,
1908
1968
  assistantResponse: input.assistantResponse,
1909
- diffSource: input.diffSource,
1910
- externalDiff: input.externalDiff,
1911
1969
  sync: input.sync,
1912
1970
  allowBranchMismatch: input.allowBranchMismatch ?? false,
1913
1971
  idempotencyKey: input.idempotencyKey,
@@ -1915,9 +1973,22 @@ function registerCollabTools(server, context) {
1915
1973
  });
1916
1974
  }
1917
1975
  });
1976
+ registerTool(server, context, {
1977
+ name: "remix_collab_drain_finalize_queue",
1978
+ description: "Drain the local finalize queue and record queued finalize_turn jobs immediately. Use this before request-merge, reconcile, or other flows that require the remote change step to exist already.",
1979
+ access: "local_write",
1980
+ inputSchema: drainFinalizeQueueInputSchema,
1981
+ outputSchema: drainFinalizeQueueSuccessSchema,
1982
+ annotations: getAnnotations("local_write", { idempotent: true }),
1983
+ run: async (args) => {
1984
+ const input = z3.object(drainFinalizeQueueInputSchema).parse(args);
1985
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1986
+ return drainFinalizeQueue({ cwd });
1987
+ }
1988
+ });
1918
1989
  registerTool(server, context, {
1919
1990
  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.",
1991
+ 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
1992
  access: "read",
1922
1993
  inputSchema: previewInputSchema,
1923
1994
  outputSchema: syncSuccessSchema,
@@ -1929,7 +2000,7 @@ function registerCollabTools(server, context) {
1929
2000
  });
1930
2001
  registerTool(server, context, {
1931
2002
  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.",
2003
+ 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
2004
  access: "local_write",
1934
2005
  inputSchema: applyInputSchema,
1935
2006
  outputSchema: syncSuccessSchema,
@@ -1940,6 +2011,31 @@ function registerCollabTools(server, context) {
1940
2011
  return syncCollab({ cwd, dryRun: false, allowBranchMismatch: input.allowBranchMismatch ?? false });
1941
2012
  }
1942
2013
  });
2014
+ registerTool(server, context, {
2015
+ name: "remix_collab_re_anchor_preview",
2016
+ description: "Preview whether the current checkout needs an explicit re-anchor to adopt or recover external Git/GitHub history.",
2017
+ access: "read",
2018
+ inputSchema: previewInputSchema,
2019
+ outputSchema: reAnchorSuccessSchema,
2020
+ run: async (args) => {
2021
+ const input = z3.object(previewInputSchema).parse(args);
2022
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
2023
+ return reAnchor({ cwd, dryRun: true });
2024
+ }
2025
+ });
2026
+ registerTool(server, context, {
2027
+ name: "remix_collab_re_anchor_apply",
2028
+ description: "Explicitly re-anchor the current lane to adopted or recovered external Git/GitHub history, without rewriting the local checkout afterward.",
2029
+ access: "local_write",
2030
+ inputSchema: reAnchorInputSchema,
2031
+ outputSchema: reAnchorSuccessSchema,
2032
+ run: async (args) => {
2033
+ const input = z3.object(reAnchorInputSchema).parse(args);
2034
+ assertConfirm(input.confirm, "remix_collab_re_anchor_apply");
2035
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
2036
+ return reAnchor({ cwd, dryRun: false, allowBranchMismatch: input.allowBranchMismatch ?? false });
2037
+ }
2038
+ });
1943
2039
  registerTool(server, context, {
1944
2040
  name: "remix_collab_request_merge",
1945
2041
  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 +2176,7 @@ function registerCollabTools(server, context) {
2080
2176
  });
2081
2177
  registerTool(server, context, {
2082
2178
  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.",
2179
+ description: "Preview the explicit Remix recovery flow when local and server state diverged beyond a simple pull.",
2084
2180
  access: "read",
2085
2181
  inputSchema: previewInputSchema,
2086
2182
  outputSchema: reconcileSuccessSchema,
@@ -2092,7 +2188,7 @@ function registerCollabTools(server, context) {
2092
2188
  });
2093
2189
  registerTool(server, context, {
2094
2190
  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.",
2191
+ description: "Run the explicit Remix recovery flow for diverged local/server state.",
2096
2192
  access: "local_write",
2097
2193
  inputSchema: applyInputSchema,
2098
2194
  outputSchema: reconcileSuccessSchema,
@@ -3217,6 +3313,19 @@ import { z as z9 } from "zod";
3217
3313
  // src/contracts/ops.ts
3218
3314
  import { z as z8 } from "zod";
3219
3315
  var genericRecordSchema4 = z8.record(z8.string(), z8.unknown());
3316
+ var appJobKindSchema = z8.enum([
3317
+ "fork",
3318
+ "edit",
3319
+ "bundle",
3320
+ "import_github",
3321
+ "import_upload",
3322
+ "merge",
3323
+ "revert",
3324
+ "change_step",
3325
+ "reconcile",
3326
+ "change_step_replay"
3327
+ ]);
3328
+ var appJobStatusSchema = z8.enum(["pending", "enqueued", "processing", "succeeded", "failed", "cancelled"]);
3220
3329
  var appScopedInputSchema = {
3221
3330
  ...commonRequestFieldsSchema,
3222
3331
  appId: z8.string().trim().min(1).optional()
@@ -3226,6 +3335,13 @@ var editQueueInputSchema = {
3226
3335
  limit: z8.number().int().positive().max(100).optional(),
3227
3336
  offset: z8.number().int().nonnegative().optional()
3228
3337
  };
3338
+ var appJobQueueInputSchema = {
3339
+ ...appScopedInputSchema,
3340
+ limit: z8.number().int().positive().max(100).optional(),
3341
+ offset: z8.number().int().nonnegative().optional(),
3342
+ kind: z8.array(appJobKindSchema).min(1).optional(),
3343
+ status: z8.array(appJobStatusSchema).min(1).optional()
3344
+ };
3229
3345
  var bundleInputSchema = {
3230
3346
  ...appScopedInputSchema,
3231
3347
  bundleId: z8.string().trim().min(1)
@@ -3258,6 +3374,7 @@ var agentRunEventsInputSchema = {
3258
3374
  };
3259
3375
  var appOverviewSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3260
3376
  var editQueueSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3377
+ var appJobQueueSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3261
3378
  var bundleSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3262
3379
  var timelineSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3263
3380
  var agentRunsSuccessSchema = makeSuccessSchema(genericRecordSchema4);
@@ -3291,7 +3408,7 @@ async function getAppOverview(params) {
3291
3408
  data,
3292
3409
  warnings: [],
3293
3410
  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."
3411
+ "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
3412
  ],
3296
3413
  logContext: target
3297
3414
  };
@@ -3314,6 +3431,42 @@ async function getEditQueue(params) {
3314
3431
  logContext: target
3315
3432
  };
3316
3433
  }
3434
+ async function listAppJobQueue(params) {
3435
+ const api = await createApiClient();
3436
+ const target = await resolveAppTarget(api, params);
3437
+ const data = unwrapResponseObject(
3438
+ await api.listAppJobQueue(target.appId, {
3439
+ limit: params.limit,
3440
+ offset: params.offset,
3441
+ kind: params.kind,
3442
+ status: params.status
3443
+ }),
3444
+ "app job queue"
3445
+ );
3446
+ const pageInfo = typeof data.pageInfo === "object" && data.pageInfo ? data.pageInfo : null;
3447
+ const items = Array.isArray(data.items) ? data.items : [];
3448
+ const activeBlockingKinds = /* @__PURE__ */ new Set(["merge", "change_step", "change_step_replay", "reconcile", "revert"]);
3449
+ const hasBlockingJobs = items.some((item) => {
3450
+ if (!item || typeof item !== "object") return false;
3451
+ const record = item;
3452
+ return typeof record.kind === "string" && activeBlockingKinds.has(record.kind) && (typeof record.status !== "string" || ["pending", "enqueued", "processing"].includes(record.status));
3453
+ });
3454
+ const recommendedNextActions = [];
3455
+ if (hasBlockingJobs) {
3456
+ recommendedNextActions.push(
3457
+ "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."
3458
+ );
3459
+ }
3460
+ if (pageInfo?.hasMore === true && typeof pageInfo.limit === "number" && typeof pageInfo.offset === "number") {
3461
+ recommendedNextActions.push(`Pass offset=${pageInfo.offset + pageInfo.limit} to load the next app job queue page.`);
3462
+ }
3463
+ return {
3464
+ data,
3465
+ warnings: [],
3466
+ recommendedNextActions,
3467
+ logContext: target
3468
+ };
3469
+ }
3317
3470
  async function getBundle(params) {
3318
3471
  const api = await createApiClient();
3319
3472
  const target = await resolveAppTarget(api, params);
@@ -3540,6 +3693,25 @@ function registerOpsTools(server, context) {
3540
3693
  });
3541
3694
  }
3542
3695
  });
3696
+ registerTool4(server, context, {
3697
+ name: "remix_ops_list_app_job_queue",
3698
+ 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.",
3699
+ access: "read",
3700
+ inputSchema: appJobQueueInputSchema,
3701
+ outputSchema: appJobQueueSuccessSchema,
3702
+ run: async (args) => {
3703
+ const input = z9.object(appJobQueueInputSchema).parse(args);
3704
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
3705
+ return listAppJobQueue({
3706
+ appId: input.appId,
3707
+ cwd,
3708
+ limit: input.limit,
3709
+ offset: input.offset,
3710
+ kind: input.kind,
3711
+ status: input.status
3712
+ });
3713
+ }
3714
+ });
3543
3715
  registerTool4(server, context, {
3544
3716
  name: "remix_ops_get_bundle",
3545
3717
  description: "Inspect one app bundle by id without downloading the artifact payload.",
@@ -3670,6 +3842,11 @@ async function startStdioServer(params) {
3670
3842
  var require2 = createRequire(import.meta.url);
3671
3843
  var { version } = require2("../package.json");
3672
3844
  async function main() {
3845
+ if (process.argv.includes("--drain-finalize-queue")) {
3846
+ const api = await createCollabApiClient();
3847
+ await drainPendingFinalizeQueue({ api });
3848
+ return;
3849
+ }
3673
3850
  await startStdioServer({ version });
3674
3851
  }
3675
3852
  main().catch((error) => {