@remixhq/core 0.1.17 → 0.1.18

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/collab.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  writeCollabBinding,
9
9
  writeCollabBindingSnapshot,
10
10
  writeJsonAtomic
11
- } from "./chunk-YCFLOHJV.js";
11
+ } from "./chunk-DBVN42RF.js";
12
12
  import {
13
13
  applyUnifiedDiffToWorktree,
14
14
  assertRepoSnapshotUnchanged,
@@ -30,13 +30,13 @@ import {
30
30
  requireCurrentBranch,
31
31
  setRemoteOriginUrl,
32
32
  summarizeUnifiedDiff
33
- } from "./chunk-WT6VRLXU.js";
33
+ } from "./chunk-S4ECO35X.js";
34
34
  import {
35
35
  REMIX_ERROR_CODES
36
36
  } from "./chunk-GC2MOT3U.js";
37
37
  import {
38
38
  RemixError
39
- } from "./chunk-YZ34ICNN.js";
39
+ } from "./chunk-7XJGOKEO.js";
40
40
 
41
41
  // src/application/collab/appDeltaCache.ts
42
42
  var APP_DELTA_CACHE_TTL_MS = 5e3;
@@ -47,6 +47,8 @@ function buildAppDeltaCacheKey(appId, payload) {
47
47
  appId,
48
48
  payload.baseHeadHash,
49
49
  payload.targetHeadHash ?? "",
50
+ payload.baseRevisionId ?? "",
51
+ payload.targetRevisionId ?? "",
50
52
  payload.localSnapshotHash ?? "",
51
53
  payload.repoFingerprint ?? "",
52
54
  payload.remoteUrl ?? "",
@@ -189,9 +191,6 @@ function getAsyncJobDir(jobId) {
189
191
  function getAsyncJobFilePath(jobId) {
190
192
  return path.join(getAsyncJobDir(jobId), "job.json");
191
193
  }
192
- function getAsyncJobBundlePath(jobId) {
193
- return path.join(getAsyncJobDir(jobId), "bundle.bundle");
194
- }
195
194
  function getLogsRoot() {
196
195
  return path.join(getCollabStateRoot(), "logs");
197
196
  }
@@ -493,11 +492,11 @@ async function readLocalBaseline(params) {
493
492
  const raw = await fs2.readFile(getBaselinePath(params), "utf8");
494
493
  const parsed = JSON.parse(raw);
495
494
  if (!parsed || typeof parsed !== "object") return null;
496
- if (parsed.schemaVersion !== 1 || typeof parsed.key !== "string" || typeof parsed.repoRoot !== "string") {
495
+ if (![1, 2].includes(Number(parsed.schemaVersion)) || typeof parsed.key !== "string" || typeof parsed.repoRoot !== "string") {
497
496
  return null;
498
497
  }
499
498
  return {
500
- schemaVersion: 1,
499
+ schemaVersion: Number(parsed.schemaVersion) === 2 ? 2 : 1,
501
500
  key: parsed.key,
502
501
  repoRoot: parsed.repoRoot,
503
502
  repoFingerprint: parsed.repoFingerprint ?? null,
@@ -506,6 +505,8 @@ async function readLocalBaseline(params) {
506
505
  branchName: parsed.branchName ?? null,
507
506
  lastSnapshotId: parsed.lastSnapshotId ?? null,
508
507
  lastSnapshotHash: parsed.lastSnapshotHash ?? null,
508
+ lastServerRevisionId: parsed.lastServerRevisionId ?? null,
509
+ lastServerTreeHash: parsed.lastServerTreeHash ?? null,
509
510
  lastServerHeadHash: parsed.lastServerHeadHash ?? null,
510
511
  lastSeenLocalCommitHash: parsed.lastSeenLocalCommitHash ?? null,
511
512
  updatedAt: String(parsed.updatedAt ?? "")
@@ -517,7 +518,7 @@ async function readLocalBaseline(params) {
517
518
  async function writeLocalBaseline(baseline) {
518
519
  const key = buildLaneStateKey(baseline);
519
520
  const normalized = {
520
- schemaVersion: 1,
521
+ schemaVersion: 2,
521
522
  key,
522
523
  repoRoot: baseline.repoRoot,
523
524
  repoFingerprint: baseline.repoFingerprint ?? null,
@@ -526,6 +527,8 @@ async function writeLocalBaseline(baseline) {
526
527
  branchName: baseline.branchName ?? null,
527
528
  lastSnapshotId: baseline.lastSnapshotId ?? null,
528
529
  lastSnapshotHash: baseline.lastSnapshotHash ?? null,
530
+ lastServerRevisionId: baseline.lastServerRevisionId ?? null,
531
+ lastServerTreeHash: baseline.lastServerTreeHash ?? null,
529
532
  lastServerHeadHash: baseline.lastServerHeadHash ?? null,
530
533
  lastSeenLocalCommitHash: baseline.lastSeenLocalCommitHash ?? null,
531
534
  updatedAt: baseline.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
@@ -912,6 +915,7 @@ function normalizeJob2(input) {
912
915
  prompt: input.prompt,
913
916
  assistantResponse: input.assistantResponse,
914
917
  baselineSnapshotId: input.baselineSnapshotId ?? null,
918
+ baselineServerRevisionId: input.baselineServerRevisionId ?? null,
915
919
  baselineServerHeadHash: input.baselineServerHeadHash ?? null,
916
920
  currentSnapshotId: input.currentSnapshotId,
917
921
  capturedAt: input.capturedAt ?? now,
@@ -946,6 +950,7 @@ async function readPendingFinalizeJob(jobId) {
946
950
  prompt: String(parsed.prompt ?? ""),
947
951
  assistantResponse: String(parsed.assistantResponse ?? ""),
948
952
  baselineSnapshotId: parsed.baselineSnapshotId ?? null,
953
+ baselineServerRevisionId: parsed.baselineServerRevisionId ?? null,
949
954
  baselineServerHeadHash: parsed.baselineServerHeadHash ?? null,
950
955
  currentSnapshotId: String(parsed.currentSnapshotId ?? ""),
951
956
  capturedAt: parsed.capturedAt,
@@ -1701,6 +1706,8 @@ function buildBaseState() {
1701
1706
  branchName: null,
1702
1707
  localCommitHash: null,
1703
1708
  currentSnapshotHash: null,
1709
+ currentServerRevisionId: null,
1710
+ currentServerTreeHash: null,
1704
1711
  currentServerHeadHash: null,
1705
1712
  currentServerHeadCommitId: null,
1706
1713
  worktreeClean: false,
@@ -1734,6 +1741,8 @@ function buildBaseState() {
1734
1741
  baseline: {
1735
1742
  lastSnapshotId: null,
1736
1743
  lastSnapshotHash: null,
1744
+ lastServerRevisionId: null,
1745
+ lastServerTreeHash: null,
1737
1746
  lastServerHeadHash: null,
1738
1747
  lastSeenLocalCommitHash: null
1739
1748
  }
@@ -1860,6 +1869,8 @@ async function collabDetectRepoState(params) {
1860
1869
  summarizeAsyncJobs({ repoRoot, branchName: binding.branchName ?? null })
1861
1870
  ]);
1862
1871
  const appHead = unwrapResponseObject(headResp, "app head");
1872
+ detected.currentServerRevisionId = appHead.headRevisionId ?? null;
1873
+ detected.currentServerTreeHash = appHead.treeHash ?? null;
1863
1874
  detected.currentServerHeadHash = appHead.headCommitHash;
1864
1875
  detected.currentServerHeadCommitId = appHead.headCommitId;
1865
1876
  detected.currentSnapshotHash = inspection.snapshotHash;
@@ -1868,6 +1879,8 @@ async function collabDetectRepoState(params) {
1868
1879
  detected.baseline = {
1869
1880
  lastSnapshotId: baseline?.lastSnapshotId ?? null,
1870
1881
  lastSnapshotHash: baseline?.lastSnapshotHash ?? null,
1882
+ lastServerRevisionId: baseline?.lastServerRevisionId ?? null,
1883
+ lastServerTreeHash: baseline?.lastServerTreeHash ?? null,
1871
1884
  lastServerHeadHash: baseline?.lastServerHeadHash ?? null,
1872
1885
  lastSeenLocalCommitHash: baseline?.lastSeenLocalCommitHash ?? null
1873
1886
  };
@@ -1877,6 +1890,7 @@ async function collabDetectRepoState(params) {
1877
1890
  const bootstrapResp = await params.api.getAppDelta(binding.currentAppId, {
1878
1891
  baseHeadHash: localCommitHash,
1879
1892
  targetHeadHash: appHead.headCommitHash,
1893
+ targetRevisionId: appHead.headRevisionId,
1880
1894
  repoFingerprint: binding.repoFingerprint ?? void 0,
1881
1895
  remoteUrl: binding.remoteUrl ?? void 0,
1882
1896
  defaultBranch: binding.defaultBranch ?? void 0
@@ -1899,7 +1913,7 @@ async function collabDetectRepoState(params) {
1899
1913
  }
1900
1914
  }
1901
1915
  detected.repoState = "external_local_base_changed";
1902
- detected.hint = "No local Remix baseline exists for this lane yet. Run `remix collab re-anchor` to anchor this checkout.";
1916
+ detected.hint = "No local Remix revision baseline exists for this lane yet. Run `remix collab init` or sync this lane to seed the baseline.";
1903
1917
  return detected;
1904
1918
  }
1905
1919
  const localHeadMovedSinceBaseline = Boolean(baseline.lastSeenLocalCommitHash) && localCommitHash !== baseline.lastSeenLocalCommitHash;
@@ -1918,7 +1932,30 @@ async function collabDetectRepoState(params) {
1918
1932
  return detected;
1919
1933
  }
1920
1934
  const localChanged = inspection.snapshotHash !== baseline.lastSnapshotHash;
1921
- const serverChanged = appHead.headCommitHash !== baseline.lastServerHeadHash;
1935
+ const serverHeadChanged = appHead.headCommitHash !== baseline.lastServerHeadHash;
1936
+ const revisionChanged = Boolean(
1937
+ baseline.lastServerRevisionId && (appHead.headRevisionId ?? null) !== baseline.lastServerRevisionId
1938
+ );
1939
+ const equivalentRevisionDrift = revisionChanged && !serverHeadChanged;
1940
+ if (equivalentRevisionDrift) {
1941
+ await writeLocalBaseline({
1942
+ repoRoot,
1943
+ repoFingerprint: binding.repoFingerprint,
1944
+ laneId: binding.laneId,
1945
+ currentAppId: binding.currentAppId,
1946
+ branchName: binding.branchName,
1947
+ lastSnapshotId: baseline.lastSnapshotId,
1948
+ lastSnapshotHash: baseline.lastSnapshotHash,
1949
+ lastServerRevisionId: appHead.headRevisionId ?? null,
1950
+ lastServerTreeHash: appHead.treeHash ?? baseline.lastServerTreeHash ?? null,
1951
+ lastServerHeadHash: appHead.headCommitHash,
1952
+ lastSeenLocalCommitHash: baseline.lastSeenLocalCommitHash
1953
+ });
1954
+ detected.baseline.lastServerRevisionId = appHead.headRevisionId ?? null;
1955
+ detected.baseline.lastServerTreeHash = appHead.treeHash ?? baseline.lastServerTreeHash ?? null;
1956
+ detected.baseline.lastServerHeadHash = appHead.headCommitHash;
1957
+ }
1958
+ const serverChanged = serverHeadChanged;
1922
1959
  if (!localChanged && !serverChanged) {
1923
1960
  detected.repoState = "idle";
1924
1961
  return detected;
@@ -2347,6 +2384,7 @@ function buildWorkspaceMetadata(params) {
2347
2384
  recordingMode: "boundary_delta",
2348
2385
  baselineSnapshotId: params.baselineSnapshotId,
2349
2386
  currentSnapshotId: params.currentSnapshotId,
2387
+ baselineServerRevisionId: params.baselineServerRevisionId ?? null,
2350
2388
  baselineServerHeadHash: params.baselineServerHeadHash,
2351
2389
  currentSnapshotHash: params.currentSnapshotHash,
2352
2390
  localCommitHash: params.localCommitHash,
@@ -2425,12 +2463,12 @@ async function processClaimedPendingFinalizeJobInner(params) {
2425
2463
  throw buildFinalizeCliError({
2426
2464
  message: "Local baseline is missing for this queued finalize job.",
2427
2465
  exitCode: 2,
2428
- hint: "Run `remix collab re-anchor` to anchor the repository again.",
2466
+ hint: "Run `remix collab init` to seed this checkout's revision baseline.",
2429
2467
  disposition: "terminal",
2430
2468
  reason: "baseline_missing"
2431
2469
  });
2432
2470
  }
2433
- const baselineDrifted = baseline.lastSnapshotId !== job.baselineSnapshotId || baseline.lastServerHeadHash !== job.baselineServerHeadHash;
2471
+ const baselineDrifted = baseline.lastSnapshotId !== job.baselineSnapshotId || (job.baselineServerRevisionId ? baseline.lastServerRevisionId !== job.baselineServerRevisionId : false) || baseline.lastServerHeadHash !== job.baselineServerHeadHash;
2434
2472
  const appHead = unwrapResponseObject(appHeadResp, "app head");
2435
2473
  const remoteUrl = readMetadataString(job, "remoteUrl");
2436
2474
  const defaultBranch = readMetadataString(job, "defaultBranch");
@@ -2453,12 +2491,13 @@ async function processClaimedPendingFinalizeJobInner(params) {
2453
2491
  throw buildFinalizeCliError({
2454
2492
  message: "Finalize queue baseline drifted before this job was processed.",
2455
2493
  exitCode: 1,
2456
- hint: "Process queued finalize jobs in capture order, or re-anchor the repository before retrying.",
2494
+ hint: "Process queued finalize jobs in capture order, or run `remix collab init` to refresh the revision baseline before retrying.",
2457
2495
  disposition: "terminal",
2458
2496
  reason: "baseline_drifted"
2459
2497
  });
2460
2498
  }
2461
- if (appHead.headCommitHash !== job.baselineServerHeadHash) {
2499
+ const serverStillAtBaseline = job.baselineServerRevisionId ? appHead.headRevisionId === job.baselineServerRevisionId : appHead.headCommitHash === job.baselineServerHeadHash;
2500
+ if (!serverStillAtBaseline) {
2462
2501
  throw buildFinalizeCliError({
2463
2502
  message: "Server lane changed before a no-diff turn could be recorded.",
2464
2503
  exitCode: 2,
@@ -2480,6 +2519,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
2480
2519
  defaultBranch,
2481
2520
  baselineSnapshotId: job.baselineSnapshotId,
2482
2521
  currentSnapshotId: job.currentSnapshotId,
2522
+ baselineServerRevisionId: job.baselineServerRevisionId,
2483
2523
  baselineServerHeadHash: job.baselineServerHeadHash,
2484
2524
  currentSnapshotHash: snapshot.snapshotHash,
2485
2525
  localCommitHash: snapshot.localCommitHash,
@@ -2500,6 +2540,8 @@ async function processClaimedPendingFinalizeJobInner(params) {
2500
2540
  branchName: job.branchName,
2501
2541
  lastSnapshotId: snapshot.id,
2502
2542
  lastSnapshotHash: snapshot.snapshotHash,
2543
+ lastServerRevisionId: appHead.headRevisionId ?? null,
2544
+ lastServerTreeHash: appHead.treeHash ?? null,
2503
2545
  lastServerHeadHash: appHead.headCommitHash,
2504
2546
  lastSeenLocalCommitHash: snapshot.localCommitHash
2505
2547
  });
@@ -2520,14 +2562,14 @@ async function processClaimedPendingFinalizeJobInner(params) {
2520
2562
  };
2521
2563
  }
2522
2564
  const localBaselineAdvanced = baseline.lastSnapshotId !== job.baselineSnapshotId;
2523
- const serverHeadAdvanced = appHead.headCommitHash !== job.baselineServerHeadHash;
2565
+ const serverHeadAdvanced = job.baselineServerRevisionId ? appHead.headRevisionId !== job.baselineServerRevisionId : appHead.headCommitHash !== job.baselineServerHeadHash;
2524
2566
  if (baselineDrifted) {
2525
2567
  const consistentAdvance = localBaselineAdvanced && serverHeadAdvanced;
2526
2568
  if (!consistentAdvance) {
2527
2569
  throw buildFinalizeCliError({
2528
2570
  message: `Finalize queue baseline advanced inconsistently before this job was processed (localBaselineAdvanced=${localBaselineAdvanced}, serverHeadAdvanced=${serverHeadAdvanced}, jobBaselineSnapshotId=${job.baselineSnapshotId ?? "null"}, liveBaselineSnapshotId=${baseline.lastSnapshotId ?? "null"}, jobBaselineServerHeadHash=${job.baselineServerHeadHash ?? "null"}, liveBaselineServerHeadHash=${baseline.lastServerHeadHash ?? "null"}, currentAppHeadHash=${appHead.headCommitHash}). This indicates local Remix state diverged from the backend in a way that should not be reachable in normal operation; please report this as a bug.`,
2529
2571
  exitCode: 1,
2530
- hint: "Run `remix collab status` to inspect, then `remix collab re-anchor` only if the lane has no valid baseline.",
2572
+ hint: "Run `remix collab status` to inspect, then sync or reconcile before retrying.",
2531
2573
  disposition: "terminal",
2532
2574
  reason: "baseline_drifted"
2533
2575
  });
@@ -2535,6 +2577,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
2535
2577
  }
2536
2578
  let submissionDiff = diffResult.diff;
2537
2579
  let submissionBaseHeadHash = job.baselineServerHeadHash;
2580
+ let submissionBaseRevisionId = job.baselineServerRevisionId;
2538
2581
  let replayedFromBaseHash = null;
2539
2582
  if (!submissionBaseHeadHash) {
2540
2583
  throw buildFinalizeCliError({
@@ -2552,7 +2595,9 @@ async function processClaimedPendingFinalizeJobInner(params) {
2552
2595
  assistantResponse: job.assistantResponse,
2553
2596
  diff: diffResult.diff,
2554
2597
  baseCommitHash: submissionBaseHeadHash,
2598
+ baseRevisionId: job.baselineServerRevisionId,
2555
2599
  targetHeadCommitHash: appHead.headCommitHash,
2600
+ targetRevisionId: appHead.headRevisionId,
2556
2601
  expectedPaths: diffResult.changedPaths,
2557
2602
  actor,
2558
2603
  workspaceMetadata: buildWorkspaceMetadata({
@@ -2562,6 +2607,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
2562
2607
  defaultBranch,
2563
2608
  baselineSnapshotId: job.baselineSnapshotId,
2564
2609
  currentSnapshotId: job.currentSnapshotId,
2610
+ baselineServerRevisionId: job.baselineServerRevisionId,
2565
2611
  baselineServerHeadHash: job.baselineServerHeadHash,
2566
2612
  currentSnapshotHash: snapshot.snapshotHash,
2567
2613
  localCommitHash: snapshot.localCommitHash,
@@ -2587,6 +2633,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
2587
2633
  submissionDiff = replayDiff.diff;
2588
2634
  replayedFromBaseHash = submissionBaseHeadHash;
2589
2635
  submissionBaseHeadHash = appHead.headCommitHash;
2636
+ submissionBaseRevisionId = appHead.headRevisionId;
2590
2637
  } catch (error) {
2591
2638
  if (error instanceof RemixError && error.finalizeDisposition === void 0) {
2592
2639
  const detail = error.hint ? `${error.message} (${error.hint})` : error.message;
@@ -2608,6 +2655,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
2608
2655
  assistantResponse: job.assistantResponse,
2609
2656
  diff: submissionDiff,
2610
2657
  baseCommitHash: submissionBaseHeadHash,
2658
+ baseRevisionId: submissionBaseRevisionId,
2611
2659
  headCommitHash: submissionBaseHeadHash,
2612
2660
  changedFilesCount: diffResult.stats.changedFilesCount,
2613
2661
  insertions: diffResult.stats.insertions,
@@ -2620,6 +2668,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
2620
2668
  defaultBranch,
2621
2669
  baselineSnapshotId: job.baselineSnapshotId,
2622
2670
  currentSnapshotId: job.currentSnapshotId,
2671
+ baselineServerRevisionId: job.baselineServerRevisionId,
2623
2672
  baselineServerHeadHash: job.baselineServerHeadHash,
2624
2673
  currentSnapshotHash: snapshot.snapshotHash,
2625
2674
  localCommitHash: snapshot.localCommitHash,
@@ -2641,11 +2690,28 @@ async function processClaimedPendingFinalizeJobInner(params) {
2641
2690
  throw buildFinalizeCliError({
2642
2691
  message: "Backend returned a succeeded change step without a head commit hash.",
2643
2692
  exitCode: 1,
2644
- hint: "This is a backend invariant violation; retry will not help. Re-anchor and try again.",
2693
+ hint: "This is a backend invariant violation; retry will not help. Run `remix collab status` before trying again.",
2645
2694
  disposition: "terminal",
2646
2695
  reason: "missing_head_commit_hash"
2647
2696
  });
2648
2697
  }
2698
+ let nextServerRevisionId = typeof changeStep.resultRevisionId === "string" ? changeStep.resultRevisionId.trim() : "";
2699
+ let nextServerTreeHash = null;
2700
+ if (!nextServerRevisionId) {
2701
+ const freshHeadResp = await params.api.getAppHead(job.currentAppId);
2702
+ const freshHead = unwrapResponseObject(freshHeadResp, "app head");
2703
+ if (freshHead.headCommitHash !== nextServerHeadHash || !freshHead.headRevisionId) {
2704
+ throw buildFinalizeCliError({
2705
+ message: "Backend returned a succeeded change step without a matching result revision.",
2706
+ exitCode: 1,
2707
+ hint: "The local baseline was not advanced because the post-step revision could not be verified. Restart the backend/CLI and retry after checking `remix collab status`.",
2708
+ disposition: "terminal",
2709
+ reason: "missing_result_revision_id"
2710
+ });
2711
+ }
2712
+ nextServerRevisionId = freshHead.headRevisionId;
2713
+ nextServerTreeHash = freshHead.treeHash ?? null;
2714
+ }
2649
2715
  await writeLocalBaseline({
2650
2716
  repoRoot: job.repoRoot,
2651
2717
  repoFingerprint: job.repoFingerprint,
@@ -2654,6 +2720,8 @@ async function processClaimedPendingFinalizeJobInner(params) {
2654
2720
  branchName: job.branchName,
2655
2721
  lastSnapshotId: snapshot.id,
2656
2722
  lastSnapshotHash: snapshot.snapshotHash,
2723
+ lastServerRevisionId: nextServerRevisionId,
2724
+ lastServerTreeHash: nextServerTreeHash,
2657
2725
  lastServerHeadHash: nextServerHeadHash,
2658
2726
  lastSeenLocalCommitHash: snapshot.localCommitHash
2659
2727
  });
@@ -2702,6 +2770,7 @@ async function enqueueCapturedFinalizeTurn(params) {
2702
2770
  prompt: params.prompt,
2703
2771
  assistantResponse: params.assistantResponse,
2704
2772
  baselineSnapshotId: params.baselineSnapshotId,
2773
+ baselineServerRevisionId: params.baselineServerRevisionId ?? null,
2705
2774
  baselineServerHeadHash: params.baselineServerHeadHash,
2706
2775
  currentSnapshotId: params.currentSnapshotId,
2707
2776
  idempotencyKey: params.idempotencyKey,
@@ -2802,17 +2871,6 @@ async function collabFinalizeTurn(params) {
2802
2871
  });
2803
2872
  }
2804
2873
  }
2805
- const pendingReAnchor = await findPendingAsyncJob({
2806
- repoRoot,
2807
- branchName: binding.branchName ?? null,
2808
- kind: "re_anchor"
2809
- });
2810
- if (pendingReAnchor) {
2811
- throw new RemixError("Cannot finalize a turn while a re-anchor is still processing.", {
2812
- exitCode: 2,
2813
- hint: `Re-anchor job ${pendingReAnchor.id} is still in the background queue. Run \`remix collab status\` to check progress.`
2814
- });
2815
- }
2816
2874
  const detected = await collabDetectRepoState({
2817
2875
  api: params.api,
2818
2876
  cwd: repoRoot,
@@ -2853,9 +2911,16 @@ async function collabFinalizeTurn(params) {
2853
2911
  hint: detected.hint
2854
2912
  });
2855
2913
  }
2914
+ if (detected.repoState === "both_changed") {
2915
+ throw new RemixError("Local and server changes must be reconciled before finalizing this turn.", {
2916
+ code: "reconcile_required",
2917
+ exitCode: 2,
2918
+ hint: detected.hint || "Run `remix collab reconcile --dry-run` to inspect recovery options before retrying."
2919
+ });
2920
+ }
2856
2921
  if (detected.repoState === "external_local_base_changed") {
2857
- throw new RemixError("The local checkout must be re-anchored before finalizing this turn.", {
2858
- code: "re_anchor_required",
2922
+ throw new RemixError("The local checkout is missing a Remix revision baseline for this lane.", {
2923
+ code: "baseline_missing",
2859
2924
  exitCode: 2,
2860
2925
  hint: detected.hint
2861
2926
  });
@@ -2867,8 +2932,9 @@ async function collabFinalizeTurn(params) {
2867
2932
  });
2868
2933
  if (!baseline) {
2869
2934
  throw new RemixError("Local Remix baseline is missing for this lane.", {
2935
+ code: "baseline_missing",
2870
2936
  exitCode: 2,
2871
- hint: "Run `remix collab re-anchor` to create a fresh baseline."
2937
+ hint: "Run `remix collab init` or sync this lane to create a fresh revision baseline."
2872
2938
  });
2873
2939
  }
2874
2940
  const snapshot = await captureLocalSnapshot({
@@ -2879,10 +2945,11 @@ async function collabFinalizeTurn(params) {
2879
2945
  });
2880
2946
  const mode = snapshot.snapshotHash === baseline.lastSnapshotHash ? "no_diff_turn" : "changed_turn";
2881
2947
  const idempotencyKey = params.idempotencyKey?.trim() || buildDeterministicIdempotencyKey({
2882
- kind: "collab_finalize_turn_boundary_v1",
2948
+ kind: "collab_finalize_turn_boundary_v2",
2883
2949
  appId: binding.currentAppId,
2884
2950
  laneId: binding.laneId,
2885
2951
  baselineSnapshotId: baseline.lastSnapshotId,
2952
+ baselineServerRevisionId: baseline.lastServerRevisionId,
2886
2953
  baselineServerHeadHash: baseline.lastServerHeadHash,
2887
2954
  currentSnapshotId: snapshot.id,
2888
2955
  currentSnapshotHash: snapshot.snapshotHash,
@@ -2902,6 +2969,7 @@ async function collabFinalizeTurn(params) {
2902
2969
  prompt,
2903
2970
  assistantResponse,
2904
2971
  baselineSnapshotId: baseline.lastSnapshotId,
2972
+ baselineServerRevisionId: baseline.lastServerRevisionId,
2905
2973
  baselineServerHeadHash: baseline.lastServerHeadHash,
2906
2974
  currentSnapshotId: snapshot.id,
2907
2975
  idempotencyKey,
@@ -2950,9 +3018,10 @@ var FINALIZE_PREFLIGHT_FAILURE_CODES = [
2950
3018
  // Server has commits we don't. Fix: `remix collab sync` (safe to
2951
3019
  // auto-run for fast-forward; non-FF refused by the command itself).
2952
3020
  "pull_required",
2953
- // Local base hash doesn't match the recorded baseline (force-push,
2954
- // hard reset, rebase). Fix: `remix collab re-anchor`.
2955
- "re_anchor_required"
3021
+ // Both local and server changed. Fix: inspect and apply reconcile.
3022
+ "reconcile_required",
3023
+ // Local revision baseline is missing. Fix: `remix collab init` or sync.
3024
+ "baseline_missing"
2956
3025
  ];
2957
3026
  var CODE_SET = new Set(FINALIZE_PREFLIGHT_FAILURE_CODES);
2958
3027
  function isFinalizePreflightFailureCode(value) {
@@ -3011,7 +3080,7 @@ async function collabRecordingPreflight(params) {
3011
3080
  if (detected.status === "branch_mismatch") return { status: "branch_mismatch", ...base };
3012
3081
  if (detected.repoState === "server_only_changed") return { status: "pull_required", ...base };
3013
3082
  if (detected.repoState === "both_changed") return { status: "reconcile_required", ...base };
3014
- if (detected.repoState === "external_local_base_changed") return { status: "re_anchor_required", ...base };
3083
+ if (detected.repoState === "external_local_base_changed") return { status: "baseline_missing", ...base };
3015
3084
  return { status: "ready", ...base };
3016
3085
  }
3017
3086
 
@@ -3238,7 +3307,7 @@ async function ensureWorkspaceMatchesBaseline(params) {
3238
3307
  if (!baseline?.lastSnapshotHash || !baseline.lastServerHeadHash) {
3239
3308
  throw new RemixError("Local Remix baseline is missing for this lane.", {
3240
3309
  exitCode: 2,
3241
- hint: "Run `remix collab re-anchor` to create a fresh baseline before applying server changes."
3310
+ hint: "Run `remix collab init` or sync from a checkout with a valid revision baseline before applying server changes."
3242
3311
  });
3243
3312
  }
3244
3313
  const inspection = await inspectLocalSnapshot({
@@ -3312,11 +3381,12 @@ async function collabSync(params) {
3312
3381
  const repoSnapshot = await captureRepoSnapshot(repoRoot, { includeWorkspaceDiffHash: true });
3313
3382
  const bootstrapFromLocalHead = !detected.baseline.lastSnapshotHash || !detected.baseline.lastServerHeadHash;
3314
3383
  let baselineServerHeadHash;
3384
+ let baselineServerRevisionId = null;
3315
3385
  if (bootstrapFromLocalHead) {
3316
3386
  if (!headCommitHash) {
3317
3387
  throw new RemixError("Failed to resolve local HEAD commit for the initial sync bootstrap.", {
3318
3388
  exitCode: 1,
3319
- hint: "Retry after Git HEAD is available, or run `remix collab re-anchor` if this checkout has no local Remix baseline yet."
3389
+ hint: "Retry after Git HEAD is available, or run `remix collab init` to seed this checkout's revision baseline."
3320
3390
  });
3321
3391
  }
3322
3392
  baselineServerHeadHash = headCommitHash;
@@ -3331,13 +3401,15 @@ async function collabSync(params) {
3331
3401
  if (!baseline.lastServerHeadHash) {
3332
3402
  throw new RemixError("Local Remix baseline is missing the last acknowledged server head.", {
3333
3403
  exitCode: 2,
3334
- hint: "Run `remix collab re-anchor` to create a fresh baseline before pulling server changes."
3404
+ hint: "Run `remix collab init` or sync from a checkout with a valid revision baseline before pulling server changes."
3335
3405
  });
3336
3406
  }
3337
3407
  baselineServerHeadHash = baseline.lastServerHeadHash;
3408
+ baselineServerRevisionId = baseline.lastServerRevisionId;
3338
3409
  }
3339
3410
  const deltaResp = await params.api.getAppDelta(binding.currentAppId, {
3340
3411
  baseHeadHash: baselineServerHeadHash,
3412
+ baseRevisionId: baselineServerRevisionId,
3341
3413
  repoFingerprint: binding.repoFingerprint ?? void 0,
3342
3414
  remoteUrl: binding.remoteUrl ?? void 0,
3343
3415
  defaultBranch: binding.defaultBranch ?? void 0
@@ -3361,13 +3433,54 @@ async function collabSync(params) {
3361
3433
  applied: false,
3362
3434
  dryRun: params.dryRun
3363
3435
  };
3364
- if (params.dryRun || delta.status === "up_to_date") {
3436
+ if (params.dryRun) {
3365
3437
  return previewResult;
3366
3438
  }
3439
+ if (delta.status === "up_to_date") {
3440
+ if (!bootstrapFromLocalHead) {
3441
+ return previewResult;
3442
+ }
3443
+ return withRepoMutationLock(
3444
+ {
3445
+ cwd: repoRoot,
3446
+ operation: "collabSync"
3447
+ },
3448
+ async ({ repoRoot: lockedRepoRoot, warnings }) => {
3449
+ await assertRepoSnapshotUnchanged(lockedRepoRoot, repoSnapshot, {
3450
+ operation: "`remix collab sync`",
3451
+ recoveryHint: "The repository changed before the first local Remix baseline could be created. Review the local changes and rerun `remix collab sync`."
3452
+ });
3453
+ const snapshot = await captureLocalSnapshot({
3454
+ repoRoot: lockedRepoRoot,
3455
+ repoFingerprint: binding.repoFingerprint,
3456
+ laneId: binding.laneId,
3457
+ branchName: binding.branchName
3458
+ });
3459
+ await writeLocalBaseline({
3460
+ repoRoot: lockedRepoRoot,
3461
+ repoFingerprint: binding.repoFingerprint,
3462
+ laneId: binding.laneId,
3463
+ currentAppId: binding.currentAppId,
3464
+ branchName: binding.branchName,
3465
+ lastSnapshotId: snapshot.id,
3466
+ lastSnapshotHash: snapshot.snapshotHash,
3467
+ lastServerRevisionId: delta.targetRevisionId ?? null,
3468
+ lastServerTreeHash: delta.targetTreeHash ?? null,
3469
+ lastServerHeadHash: delta.targetHeadHash,
3470
+ lastSeenLocalCommitHash: snapshot.localCommitHash
3471
+ });
3472
+ return {
3473
+ ...previewResult,
3474
+ localCommitHash: snapshot.localCommitHash,
3475
+ ...warnings.length > 0 ? { warnings } : {}
3476
+ };
3477
+ }
3478
+ );
3479
+ }
3367
3480
  if (delta.status === "base_unknown") {
3368
3481
  throw new RemixError("Direct pull is unavailable because Remix can no longer diff from the last acknowledged server head.", {
3369
3482
  exitCode: 2,
3370
- hint: "Run `remix collab reconcile --dry-run` to inspect recovery options before retrying. If this checkout has no local Remix baseline yet for this lane, `remix collab re-anchor` may be required."
3483
+ hint: "Run `remix collab reconcile --dry-run` to inspect recovery options before retrying. If this checkout has no local Remix baseline yet for this lane, run `remix collab init` to seed one."
3371
3484
  });
3372
3485
  }
3373
3486
  if (delta.status !== "delta_ready") {
@@ -3411,6 +3524,8 @@ async function collabSync(params) {
3411
3524
  branchName: binding.branchName,
3412
3525
  lastSnapshotId: snapshot.id,
3413
3526
  lastSnapshotHash: snapshot.snapshotHash,
3527
+ lastServerRevisionId: delta.targetRevisionId ?? null,
3528
+ lastServerTreeHash: delta.targetTreeHash ?? null,
3414
3529
  lastServerHeadHash: delta.targetHeadHash,
3415
3530
  lastSeenLocalCommitHash: snapshot.localCommitHash
3416
3531
  });
@@ -3705,6 +3820,8 @@ async function collabCheckout(params) {
3705
3820
  branchName: branchNameForBaseline,
3706
3821
  lastSnapshotId: snapshot.id,
3707
3822
  lastSnapshotHash: snapshot.snapshotHash,
3823
+ lastServerRevisionId: appHead.headRevisionId ?? null,
3824
+ lastServerTreeHash: appHead.treeHash ?? null,
3708
3825
  lastServerHeadHash: appHead.headCommitHash,
3709
3826
  lastSeenLocalCommitHash: snapshot.localCommitHash
3710
3827
  });
@@ -4107,6 +4224,8 @@ async function trySeedEquivalentBranchBaseline(params) {
4107
4224
  branchName: params.branchName,
4108
4225
  lastSnapshotId: snapshot.id,
4109
4226
  lastSnapshotHash: snapshot.snapshotHash,
4227
+ lastServerRevisionId: params.appHeadRevisionId,
4228
+ lastServerTreeHash: params.appTreeHash,
4110
4229
  lastServerHeadHash: params.appHeadHash,
4111
4230
  lastSeenLocalCommitHash: snapshot.localCommitHash
4112
4231
  });
@@ -4118,11 +4237,11 @@ async function resolveInitBaselineStatus(params) {
4118
4237
  laneId: params.laneId,
4119
4238
  repoRoot: params.repoRoot
4120
4239
  });
4121
- if (baseline?.lastSnapshotHash && baseline.lastServerHeadHash) {
4240
+ if (baseline?.lastSnapshotHash && (baseline.lastServerRevisionId || baseline.lastServerHeadHash)) {
4122
4241
  return "existing";
4123
4242
  }
4124
4243
  const localHeadCommitHash = await getHeadCommitHash(params.repoRoot);
4125
- if (!localHeadCommitHash) return "requires_re_anchor";
4244
+ if (!localHeadCommitHash) return "baseline_missing";
4126
4245
  const appHead = unwrapResponseObject(
4127
4246
  await params.api.getAppHead(params.currentAppId),
4128
4247
  "app head"
@@ -4149,6 +4268,7 @@ async function resolveInitBaselineStatus(params) {
4149
4268
  const deltaResp = await params.api.getAppDelta(params.currentAppId, {
4150
4269
  baseHeadHash: localHeadCommitHash,
4151
4270
  targetHeadHash: appHead.headCommitHash,
4271
+ targetRevisionId: appHead.headRevisionId,
4152
4272
  localSnapshotHash,
4153
4273
  repoFingerprint: params.repoFingerprint,
4154
4274
  remoteUrl: params.remoteUrl ?? void 0,
@@ -4178,7 +4298,9 @@ async function resolveInitBaselineStatus(params) {
4178
4298
  upstreamAppId: params.upstreamAppId ?? null,
4179
4299
  branchName: params.branchName,
4180
4300
  defaultBranch: params.defaultBranch,
4181
- appHeadHash: appHead.headCommitHash
4301
+ appHeadHash: appHead.headCommitHash,
4302
+ appHeadRevisionId: appHead.headRevisionId ?? null,
4303
+ appTreeHash: appHead.treeHash ?? null
4182
4304
  });
4183
4305
  if (equivalentBaseline) {
4184
4306
  return equivalentBaseline;
@@ -4186,7 +4308,7 @@ async function resolveInitBaselineStatus(params) {
4186
4308
  }
4187
4309
  } catch {
4188
4310
  }
4189
- return "requires_re_anchor";
4311
+ return "baseline_missing";
4190
4312
  }
4191
4313
  async function seedImportedInitBaseline(params) {
4192
4314
  const appHead = unwrapResponseObject(
@@ -4207,6 +4329,8 @@ async function seedImportedInitBaseline(params) {
4207
4329
  branchName: params.branchName,
4208
4330
  lastSnapshotId: snapshot.id,
4209
4331
  lastSnapshotHash: snapshot.snapshotHash,
4332
+ lastServerRevisionId: appHead.headRevisionId ?? null,
4333
+ lastServerTreeHash: appHead.treeHash ?? null,
4210
4334
  lastServerHeadHash: appHead.headCommitHash,
4211
4335
  lastSeenLocalCommitHash: snapshot.localCommitHash
4212
4336
  });
@@ -4220,7 +4344,6 @@ async function collabInit(params) {
4220
4344
  },
4221
4345
  async ({ repoRoot, warnings }) => {
4222
4346
  await ensureGitInfoExcludeEntries(repoRoot, [".remix/"]);
4223
- await ensureCleanWorktree(repoRoot, "`remix collab init`");
4224
4347
  if (params.path?.trim()) {
4225
4348
  throw new RemixError("`remix collab init --path` is not supported.", {
4226
4349
  exitCode: 2,
@@ -4228,6 +4351,10 @@ async function collabInit(params) {
4228
4351
  });
4229
4352
  }
4230
4353
  const localBindingState = await readCollabBindingState(repoRoot, { persist: true });
4354
+ const hasExistingBinding = localBindingState != null && Object.keys(localBindingState.branchBindings ?? {}).length > 0;
4355
+ if (params.forceNew || !hasExistingBinding) {
4356
+ await ensureCleanWorktree(repoRoot, "`remix collab init`");
4357
+ }
4231
4358
  const persistedRemoteUrl = normalizeGitRemote(localBindingState?.remoteUrl ?? null);
4232
4359
  const currentBranch = await getCurrentBranch(repoRoot);
4233
4360
  const defaultBranch = localBindingState?.defaultBranch ?? await getDefaultBranch(repoRoot) ?? currentBranch;
@@ -5081,10 +5208,11 @@ async function collabList(params) {
5081
5208
  };
5082
5209
  }
5083
5210
 
5084
- // src/application/collab/collabReAnchor.ts
5085
- import { randomUUID as randomUUID5 } from "crypto";
5211
+ // src/application/collab/collabReconcile.ts
5086
5212
  import fs11 from "fs/promises";
5213
+ import os5 from "os";
5087
5214
  import path10 from "path";
5215
+ import { execa as execa3 } from "execa";
5088
5216
 
5089
5217
  // src/application/collab/pendingFinalize.ts
5090
5218
  function hasPendingFinalize(summary) {
@@ -5094,258 +5222,7 @@ function buildPendingFinalizeHint() {
5094
5222
  return "Drain or await the local finalize queue first, then retry after the queued Remix turn finishes recording remotely.";
5095
5223
  }
5096
5224
 
5097
- // src/application/collab/collabReAnchor.ts
5098
- async function collabReAnchor(params) {
5099
- const repoRoot = await findGitRoot(params.cwd);
5100
- const binding = await ensureActiveLaneBinding({
5101
- repoRoot,
5102
- api: params.api,
5103
- operation: "`remix collab re-anchor`"
5104
- });
5105
- if (!binding) {
5106
- throw new RemixError("Repository is not bound to Remix.", {
5107
- exitCode: 2,
5108
- hint: "Run `remix collab init` first."
5109
- });
5110
- }
5111
- const detected = await collabDetectRepoState({
5112
- api: params.api,
5113
- cwd: repoRoot,
5114
- allowBranchMismatch: params.allowBranchMismatch
5115
- });
5116
- if (detected.status === "metadata_conflict" || detected.status === "branch_mismatch") {
5117
- throw new RemixError("Repository must be realigned before seeding a fresh local Remix baseline.", {
5118
- exitCode: 2,
5119
- hint: detected.hint
5120
- });
5121
- }
5122
- if (detected.status !== "ready" || !detected.binding) {
5123
- throw new RemixError(detected.hint || "Repository is not ready for re-anchor.", {
5124
- exitCode: 2,
5125
- hint: detected.hint
5126
- });
5127
- }
5128
- if (detected.repoState === "server_only_changed") {
5129
- throw new RemixError("This checkout is already on a server-known base and only needs a local pull.", {
5130
- exitCode: 2,
5131
- hint: "Run `remix collab sync` instead of `remix collab re-anchor`."
5132
- });
5133
- }
5134
- if (detected.repoState === "both_changed") {
5135
- throw new RemixError("Both the local workspace and the server lane changed since the last agreed baseline.", {
5136
- exitCode: 2,
5137
- hint: "Run `remix collab reconcile` to replay the local boundary onto the newer server head."
5138
- });
5139
- }
5140
- if (detected.repoState === "local_only_changed") {
5141
- if (hasPendingFinalize(detected.pendingFinalize)) {
5142
- throw new RemixError("Re-anchor is not needed while queued Remix turn recording is still processing.", {
5143
- exitCode: 2,
5144
- hint: buildPendingFinalizeHint()
5145
- });
5146
- }
5147
- throw new RemixError("Re-anchor is not the right command for local content changes.", {
5148
- exitCode: 2,
5149
- hint: "Remix is source-blind: any local content change since the last recorded turn \u2014 including manual commits, pulls, merges, and rebases \u2014 is recorded with `remix collab finalize-turn`. Use `remix collab re-anchor` only when no local Remix baseline exists yet for this lane (status reports `re_anchor`)."
5150
- });
5151
- }
5152
- if (detected.repoState === "idle") {
5153
- throw new RemixError("This checkout is already aligned with Remix.", {
5154
- exitCode: 2,
5155
- hint: "No re-anchor step is needed. Re-anchor only applies when no local Remix baseline exists yet for this lane."
5156
- });
5157
- }
5158
- await ensureCleanWorktree(repoRoot, "`remix collab re-anchor`");
5159
- const branch = await requireCurrentBranch(repoRoot);
5160
- const headCommitHash = await getHeadCommitHash(repoRoot);
5161
- if (!headCommitHash) {
5162
- throw new RemixError("Failed to resolve local HEAD commit.", { exitCode: 1 });
5163
- }
5164
- if (params.asyncSubmit && !params.dryRun) {
5165
- const pending = await findPendingAsyncJob({
5166
- repoRoot,
5167
- branchName: binding.branchName ?? branch,
5168
- kind: "re_anchor"
5169
- });
5170
- if (pending) {
5171
- return {
5172
- status: "queued",
5173
- queued: true,
5174
- jobId: pending.id,
5175
- repoRoot,
5176
- branch,
5177
- currentAppId: binding.currentAppId,
5178
- dryRun: false,
5179
- applied: false
5180
- };
5181
- }
5182
- }
5183
- const preflightResp = await params.api.preflightAppReconcile(binding.currentAppId, {
5184
- localHeadCommitHash: headCommitHash,
5185
- repoFingerprint: binding.repoFingerprint ?? void 0,
5186
- remoteUrl: binding.remoteUrl ?? void 0,
5187
- defaultBranch: binding.defaultBranch ?? void 0
5188
- });
5189
- const preflight = unwrapResponseObject(preflightResp, "reconcile preflight");
5190
- if (preflight.status === "metadata_conflict") {
5191
- throw new RemixError("Local repository metadata conflicts with the bound Remix app.", {
5192
- exitCode: 2,
5193
- hint: preflight.warnings.join("\n") || "Run the command from the correct bound repository."
5194
- });
5195
- }
5196
- const preview = {
5197
- status: preflight.status === "up_to_date" ? "reanchored" : "re_anchor_required",
5198
- repoRoot,
5199
- branch,
5200
- currentAppId: binding.currentAppId,
5201
- localHeadCommitHash: headCommitHash,
5202
- targetHeadCommitHash: preflight.targetHeadCommitHash,
5203
- targetHeadCommitId: preflight.targetHeadCommitId,
5204
- warnings: preflight.warnings,
5205
- applied: false,
5206
- dryRun: params.dryRun === true
5207
- };
5208
- if (params.dryRun) {
5209
- return preview;
5210
- }
5211
- let anchoredServerHeadHash = preflight.targetHeadCommitHash;
5212
- if (params.asyncSubmit && preflight.status === "ready_to_reconcile") {
5213
- const failed = await findFailedAsyncJob({
5214
- repoRoot,
5215
- branchName: binding.branchName ?? branch,
5216
- kind: "re_anchor"
5217
- });
5218
- if (failed) {
5219
- await deleteAsyncJob(failed.id);
5220
- }
5221
- const { bundlePath: tmpBundlePath, headCommitHash: bundledHeadCommitHash } = await createGitBundle(
5222
- repoRoot,
5223
- "re-anchor.bundle"
5224
- );
5225
- const tmpBundleDir = path10.dirname(tmpBundlePath);
5226
- try {
5227
- const jobId = randomUUID5();
5228
- const durableBundlePath = getAsyncJobBundlePath(jobId);
5229
- await fs11.mkdir(getAsyncJobDir(jobId), { recursive: true });
5230
- try {
5231
- await fs11.rename(tmpBundlePath, durableBundlePath);
5232
- } catch (error) {
5233
- if (error?.code !== "EXDEV") throw error;
5234
- await fs11.copyFile(tmpBundlePath, durableBundlePath);
5235
- await fs11.unlink(tmpBundlePath).catch(() => void 0);
5236
- }
5237
- const bundleSha = await sha256FileHex(durableBundlePath);
5238
- const job = await enqueueAsyncJob({
5239
- id: jobId,
5240
- kind: "re_anchor",
5241
- status: "queued",
5242
- repoRoot,
5243
- repoFingerprint: binding.repoFingerprint,
5244
- branchName: binding.branchName ?? branch,
5245
- laneId: binding.laneId,
5246
- retryCount: 0,
5247
- error: null,
5248
- idempotencyKey: null,
5249
- payload: {
5250
- bundlePath: durableBundlePath,
5251
- bundleSha256: bundleSha,
5252
- localHeadCommitHash: bundledHeadCommitHash,
5253
- targetHeadCommitHash: preflight.targetHeadCommitHash,
5254
- appId: binding.currentAppId
5255
- }
5256
- });
5257
- await logDrainerEvent(job.id, "submitted", { kind: "re_anchor" });
5258
- return {
5259
- status: "queued",
5260
- queued: true,
5261
- jobId: job.id,
5262
- repoRoot,
5263
- branch,
5264
- currentAppId: binding.currentAppId,
5265
- localHeadCommitHash: bundledHeadCommitHash,
5266
- targetHeadCommitHash: preflight.targetHeadCommitHash,
5267
- warnings: preflight.warnings,
5268
- dryRun: false,
5269
- applied: false
5270
- };
5271
- } finally {
5272
- await fs11.rm(tmpBundleDir, { recursive: true, force: true }).catch(() => void 0);
5273
- }
5274
- }
5275
- if (preflight.status === "ready_to_reconcile") {
5276
- const { bundlePath, headCommitHash: bundledHeadCommitHash } = await createGitBundle(repoRoot, "re-anchor.bundle");
5277
- const bundleTempDir = path10.dirname(bundlePath);
5278
- try {
5279
- const bundleStat = await fs11.stat(bundlePath);
5280
- const checksumSha256 = await sha256FileHex(bundlePath);
5281
- const presignResp = await params.api.presignImportUploadFirstParty({
5282
- file: {
5283
- name: path10.basename(bundlePath),
5284
- mimeType: "application/x-git-bundle",
5285
- size: bundleStat.size,
5286
- checksumSha256
5287
- }
5288
- });
5289
- const uploadTarget = unwrapResponseObject(presignResp, "import upload target");
5290
- await uploadPresigned({
5291
- uploadUrl: String(uploadTarget.uploadUrl),
5292
- filePath: bundlePath,
5293
- headers: uploadTarget.headers ?? {}
5294
- });
5295
- const startResp = await params.api.startAppReconcile(binding.currentAppId, {
5296
- uploadId: String(uploadTarget.uploadId),
5297
- localHeadCommitHash: bundledHeadCommitHash,
5298
- repoFingerprint: binding.repoFingerprint ?? void 0,
5299
- remoteUrl: binding.remoteUrl ?? void 0,
5300
- defaultBranch: binding.defaultBranch ?? void 0,
5301
- idempotencyKey: buildDeterministicIdempotencyKey({
5302
- kind: "collab_re_anchor_v1",
5303
- appId: binding.currentAppId,
5304
- localHeadCommitHash: bundledHeadCommitHash,
5305
- targetHeadCommitHash: preflight.targetHeadCommitHash
5306
- })
5307
- });
5308
- const started = unwrapResponseObject(startResp, "reconcile");
5309
- const reconcile = await pollReconcile(params.api, binding.currentAppId, started.id);
5310
- anchoredServerHeadHash = reconcile.reconciledHeadCommitHash ?? reconcile.targetHeadCommitHash ?? preflight.targetHeadCommitHash;
5311
- } finally {
5312
- await fs11.rm(bundleTempDir, { recursive: true, force: true });
5313
- }
5314
- }
5315
- const snapshot = await captureLocalSnapshot({
5316
- repoRoot,
5317
- repoFingerprint: binding.repoFingerprint,
5318
- laneId: binding.laneId,
5319
- branchName: binding.branchName
5320
- });
5321
- await writeLocalBaseline({
5322
- repoRoot,
5323
- repoFingerprint: binding.repoFingerprint,
5324
- laneId: binding.laneId,
5325
- currentAppId: binding.currentAppId,
5326
- branchName: binding.branchName,
5327
- lastSnapshotId: snapshot.id,
5328
- lastSnapshotHash: snapshot.snapshotHash,
5329
- lastServerHeadHash: anchoredServerHeadHash,
5330
- lastSeenLocalCommitHash: snapshot.localCommitHash
5331
- });
5332
- return {
5333
- ...preview,
5334
- status: "reanchored",
5335
- targetHeadCommitHash: anchoredServerHeadHash,
5336
- applied: true,
5337
- dryRun: false
5338
- };
5339
- }
5340
- async function collabReAnchorSubmit(params) {
5341
- return collabReAnchor({ ...params, asyncSubmit: true });
5342
- }
5343
-
5344
5225
  // src/application/collab/collabReconcile.ts
5345
- import fs12 from "fs/promises";
5346
- import os5 from "os";
5347
- import path11 from "path";
5348
- import { execa as execa3 } from "execa";
5349
5226
  async function reconcileBothChanged(params) {
5350
5227
  const repoRoot = await findGitRoot(params.cwd);
5351
5228
  const binding = await ensureActiveLaneBinding({
@@ -5368,7 +5245,7 @@ async function reconcileBothChanged(params) {
5368
5245
  if (!baseline?.lastSnapshotId || !baseline.lastServerHeadHash) {
5369
5246
  throw new RemixError("Local Remix baseline is missing for this lane.", {
5370
5247
  exitCode: 2,
5371
- hint: "Run `remix collab re-anchor` to create a fresh baseline first."
5248
+ hint: "Run `remix collab init` or sync from a checkout with a valid revision baseline first."
5372
5249
  });
5373
5250
  }
5374
5251
  const currentSnapshot = await captureLocalSnapshot({
@@ -5391,6 +5268,7 @@ async function reconcileBothChanged(params) {
5391
5268
  params.api.getAppHead(binding.currentAppId),
5392
5269
  params.api.getAppDelta(binding.currentAppId, {
5393
5270
  baseHeadHash: baseline.lastServerHeadHash,
5271
+ baseRevisionId: baseline.lastServerRevisionId,
5394
5272
  repoFingerprint: binding.repoFingerprint ?? void 0,
5395
5273
  remoteUrl: binding.remoteUrl ?? void 0,
5396
5274
  defaultBranch: binding.defaultBranch ?? void 0
@@ -5408,7 +5286,7 @@ async function reconcileBothChanged(params) {
5408
5286
  if (delta.status === "base_unknown") {
5409
5287
  throw new RemixError("Reconcile cannot pull the newer server state from the last acknowledged baseline.", {
5410
5288
  exitCode: 2,
5411
- hint: "Run `remix collab re-anchor` to re-anchor this checkout before retrying."
5289
+ hint: "Run `remix collab init` to seed a fresh revision baseline for this checkout before retrying."
5412
5290
  });
5413
5291
  }
5414
5292
  if (delta.status !== "delta_ready" && delta.status !== "up_to_date") {
@@ -5439,7 +5317,9 @@ async function reconcileBothChanged(params) {
5439
5317
  assistantResponse: "Replay the local boundary delta onto the latest server head without recording a new change step.",
5440
5318
  diff: diffResult.diff,
5441
5319
  baseCommitHash: baseline.lastServerHeadHash,
5320
+ baseRevisionId: baseline.lastServerRevisionId,
5442
5321
  targetHeadCommitHash: appHead.headCommitHash,
5322
+ targetRevisionId: appHead.headRevisionId,
5443
5323
  expectedPaths: diffResult.changedPaths,
5444
5324
  workspaceMetadata: {
5445
5325
  recordingMode: "boundary_delta",
@@ -5447,6 +5327,7 @@ async function reconcileBothChanged(params) {
5447
5327
  branch,
5448
5328
  baselineSnapshotId: baseline.lastSnapshotId,
5449
5329
  currentSnapshotId: currentSnapshot.id,
5330
+ baselineServerRevisionId: baseline.lastServerRevisionId,
5450
5331
  baselineServerHeadHash: baseline.lastServerHeadHash,
5451
5332
  currentSnapshotHash: currentSnapshot.snapshotHash,
5452
5333
  localCommitHash: currentSnapshot.localCommitHash,
@@ -5465,12 +5346,12 @@ async function reconcileBothChanged(params) {
5465
5346
  const replay = await pollChangeStepReplay(params.api, binding.currentAppId, String(replayStart.id));
5466
5347
  const replayDiffResp = await params.api.getChangeStepReplayDiff(binding.currentAppId, replay.id);
5467
5348
  const replayDiff = unwrapResponseObject(replayDiffResp, "change step replay diff");
5468
- const tempRoot = await fs12.mkdtemp(path11.join(os5.tmpdir(), "remix-reconcile-"));
5349
+ const tempRoot = await fs11.mkdtemp(path10.join(os5.tmpdir(), "remix-reconcile-"));
5469
5350
  let serverHeadSnapshot = null;
5470
5351
  let mergedSnapshot = null;
5471
5352
  try {
5472
- const tempRepoRoot = path11.join(tempRoot, "repo");
5473
- await fs12.mkdir(tempRepoRoot, { recursive: true });
5353
+ const tempRepoRoot = path10.join(tempRoot, "repo");
5354
+ await fs11.mkdir(tempRepoRoot, { recursive: true });
5474
5355
  await execa3("git", ["init"], { cwd: tempRepoRoot, stderr: "ignore" });
5475
5356
  await materializeLocalSnapshot(baseline.lastSnapshotId, tempRepoRoot);
5476
5357
  if (delta.status === "delta_ready" && delta.diff.trim()) {
@@ -5492,7 +5373,7 @@ async function reconcileBothChanged(params) {
5492
5373
  branchName: binding.branchName
5493
5374
  });
5494
5375
  } finally {
5495
- await fs12.rm(tempRoot, { recursive: true, force: true }).catch(() => void 0);
5376
+ await fs11.rm(tempRoot, { recursive: true, force: true }).catch(() => void 0);
5496
5377
  }
5497
5378
  if (!serverHeadSnapshot || !mergedSnapshot) {
5498
5379
  throw new RemixError("Failed to materialize the reconciled local workspace.", { exitCode: 1 });
@@ -5529,6 +5410,8 @@ async function reconcileBothChanged(params) {
5529
5410
  branchName: binding.branchName,
5530
5411
  lastSnapshotId: serverHeadSnapshot.id,
5531
5412
  lastSnapshotHash: serverHeadSnapshot.snapshotHash,
5413
+ lastServerRevisionId: appHead.headRevisionId ?? null,
5414
+ lastServerTreeHash: appHead.treeHash ?? null,
5532
5415
  lastServerHeadHash: appHead.headCommitHash,
5533
5416
  lastSeenLocalCommitHash: restoredSnapshot.localCommitHash
5534
5417
  });
@@ -5564,7 +5447,10 @@ async function collabReconcile(params) {
5564
5447
  return reconcileBothChanged(params);
5565
5448
  }
5566
5449
  if (detected.repoState === "external_local_base_changed") {
5567
- return collabReAnchor(params);
5450
+ throw new RemixError("This checkout needs a local Remix revision baseline before reconciliation.", {
5451
+ exitCode: 2,
5452
+ hint: detected.hint || "Run `remix collab init` or `remix collab sync` to seed the baseline."
5453
+ });
5568
5454
  }
5569
5455
  if (detected.repoState === "local_only_changed") {
5570
5456
  if (hasPendingFinalize(detected.pendingFinalize)) {
@@ -5675,6 +5561,8 @@ async function collabRemix(params) {
5675
5561
  branchName: branchNameForBaseline,
5676
5562
  lastSnapshotId: snapshot.id,
5677
5563
  lastSnapshotHash: snapshot.snapshotHash,
5564
+ lastServerRevisionId: appHead.headRevisionId ?? null,
5565
+ lastServerTreeHash: appHead.treeHash ?? null,
5678
5566
  lastServerHeadHash: appHead.headCommitHash,
5679
5567
  lastSeenLocalCommitHash: snapshot.localCommitHash
5680
5568
  });
@@ -5799,11 +5687,15 @@ function createBaseStatus() {
5799
5687
  baseline: {
5800
5688
  lastSnapshotId: null,
5801
5689
  lastSnapshotHash: null,
5690
+ lastServerRevisionId: null,
5691
+ lastServerTreeHash: null,
5802
5692
  lastServerHeadHash: null,
5803
5693
  lastSeenLocalCommitHash: null
5804
5694
  },
5805
5695
  current: {
5806
5696
  snapshotHash: null,
5697
+ serverRevisionId: null,
5698
+ serverTreeHash: null,
5807
5699
  serverHeadHash: null,
5808
5700
  serverHeadCommitId: null,
5809
5701
  localCommitHash: null
@@ -5890,6 +5782,8 @@ async function collabStatus(params) {
5890
5782
  status.alignment.baseline = detected.baseline;
5891
5783
  status.alignment.current = {
5892
5784
  snapshotHash: detected.currentSnapshotHash,
5785
+ serverRevisionId: detected.currentServerRevisionId,
5786
+ serverTreeHash: detected.currentServerTreeHash,
5893
5787
  serverHeadHash: detected.currentServerHeadHash,
5894
5788
  serverHeadCommitId: detected.currentServerHeadCommitId,
5895
5789
  localCommitHash: detected.localCommitHash
@@ -5953,7 +5847,7 @@ async function collabStatus(params) {
5953
5847
  status.reconcile.canApply = !status.repo.branchMismatch;
5954
5848
  status.recommendedAction = "reconcile";
5955
5849
  } else if (detected.repoState === "external_local_base_changed") {
5956
- status.recommendedAction = "re_anchor";
5850
+ status.recommendedAction = "init";
5957
5851
  addBlockedReason(status.sync, "baseline_missing");
5958
5852
  addBlockedReason(status.reconcile, "baseline_missing");
5959
5853
  } else if (detected.repoState === "local_only_changed") {
@@ -6117,8 +6011,8 @@ async function collabView(params) {
6117
6011
  }
6118
6012
 
6119
6013
  // src/application/collab/collabAsyncProcessing.ts
6120
- import fs13 from "fs/promises";
6121
- import path12 from "path";
6014
+ import fs12 from "fs/promises";
6015
+ import path11 from "path";
6122
6016
  var MAX_TRANSIENT_RETRIES = 5;
6123
6017
  var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
6124
6018
  "ECONNREFUSED",
@@ -6214,10 +6108,10 @@ async function processInitJob(job, api) {
6214
6108
  try {
6215
6109
  await updateAsyncJob(job.id, { status: "submitting", error: null });
6216
6110
  await logDrainerEvent(job.id, "claimed", { kind: "init" });
6217
- const bundleStat = await fs13.stat(job.payload.bundlePath);
6111
+ const bundleStat = await fs12.stat(job.payload.bundlePath);
6218
6112
  const presignResp = await api.presignImportUploadFirstParty({
6219
6113
  file: {
6220
- name: path12.basename(job.payload.bundlePath),
6114
+ name: path11.basename(job.payload.bundlePath),
6221
6115
  mimeType: "application/x-git-bundle",
6222
6116
  size: bundleStat.size,
6223
6117
  checksumSha256: job.payload.bundleSha256
@@ -6234,7 +6128,7 @@ async function processInitJob(job, api) {
6234
6128
  await updateAsyncJob(job.id, { status: "server_processing" });
6235
6129
  const importResp = await api.importFromUploadFirstParty({
6236
6130
  uploadId: String(presign.uploadId),
6237
- appName: job.payload.appName?.trim() || path12.basename(job.repoRoot),
6131
+ appName: job.payload.appName?.trim() || path11.basename(job.repoRoot),
6238
6132
  platform: "generic",
6239
6133
  isPublic: false,
6240
6134
  branch: job.payload.defaultBranch && job.branchName && job.branchName !== job.payload.defaultBranch ? job.payload.defaultBranch : job.branchName ?? void 0,
@@ -6428,7 +6322,7 @@ async function processInitPostJob(job, api) {
6428
6322
  if (outcome.status === "failed") {
6429
6323
  const bindingPath = getCollabBindingPath(job.repoRoot);
6430
6324
  try {
6431
- await fs13.unlink(bindingPath);
6325
+ await fs12.unlink(bindingPath);
6432
6326
  await logDrainerEvent(job.id, "binding_cleared", {
6433
6327
  kind: "init_post",
6434
6328
  appId: job.payload.appId,
@@ -6469,10 +6363,10 @@ async function processReAnchorJob(job, api) {
6469
6363
  }
6470
6364
  let anchoredServerHeadHash = preflight.targetHeadCommitHash;
6471
6365
  if (preflight.status === "ready_to_reconcile") {
6472
- const bundleStat = await fs13.stat(job.payload.bundlePath);
6366
+ const bundleStat = await fs12.stat(job.payload.bundlePath);
6473
6367
  const presignResp = await api.presignImportUploadFirstParty({
6474
6368
  file: {
6475
- name: path12.basename(job.payload.bundlePath),
6369
+ name: path11.basename(job.payload.bundlePath),
6476
6370
  mimeType: "application/x-git-bundle",
6477
6371
  size: bundleStat.size,
6478
6372
  checksumSha256: job.payload.bundleSha256
@@ -6576,9 +6470,9 @@ async function collabReAnchorProcess(jobId, opts) {
6576
6470
  }
6577
6471
  async function acquireDrainerPidLock() {
6578
6472
  const pidPath = getDrainerPidPath();
6579
- await fs13.mkdir(path12.dirname(pidPath), { recursive: true });
6473
+ await fs12.mkdir(path11.dirname(pidPath), { recursive: true });
6580
6474
  try {
6581
- const existing = await fs13.readFile(pidPath, "utf8").catch(() => "");
6475
+ const existing = await fs12.readFile(pidPath, "utf8").catch(() => "");
6582
6476
  const existingPid = parseInt(existing.trim(), 10);
6583
6477
  if (Number.isFinite(existingPid) && existingPid > 0 && existingPid !== process.pid) {
6584
6478
  try {
@@ -6588,13 +6482,13 @@ async function acquireDrainerPidLock() {
6588
6482
  if (error?.code !== "ESRCH") return null;
6589
6483
  }
6590
6484
  }
6591
- await fs13.writeFile(pidPath, String(process.pid), "utf8");
6485
+ await fs12.writeFile(pidPath, String(process.pid), "utf8");
6592
6486
  return {
6593
6487
  release: async () => {
6594
6488
  try {
6595
- const current = (await fs13.readFile(pidPath, "utf8")).trim();
6489
+ const current = (await fs12.readFile(pidPath, "utf8")).trim();
6596
6490
  if (current === String(process.pid)) {
6597
- await fs13.unlink(pidPath).catch(() => void 0);
6491
+ await fs12.unlink(pidPath).catch(() => void 0);
6598
6492
  }
6599
6493
  } catch {
6600
6494
  }
@@ -6645,9 +6539,6 @@ export {
6645
6539
  collabList,
6646
6540
  collabListMembers,
6647
6541
  collabListMergeRequests,
6648
- collabReAnchor,
6649
- collabReAnchorProcess,
6650
- collabReAnchorSubmit,
6651
6542
  collabReconcile,
6652
6543
  collabRecordingPreflight,
6653
6544
  collabReject,