@remixhq/core 0.1.16 → 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;
@@ -4744,7 +4871,7 @@ async function collabInit(params) {
4744
4871
  operation: "`remix collab init`",
4745
4872
  recoveryHint: "The repository changed before the Remix binding was written. Review the local changes and rerun `remix collab init`."
4746
4873
  });
4747
- const bindingMode2 = params.forceNew && (!defaultBranch || branchName === defaultBranch) ? "explicit_root" : "lane";
4874
+ const bindingMode2 = defaultBranch && branchName !== defaultBranch ? "lane" : "explicit_root";
4748
4875
  let bindingPath2;
4749
4876
  if (params.forceNew && defaultBranch && canonicalLane2) {
4750
4877
  const canonicalBinding = branchBindingFromLane(canonicalLane2, "explicit_root", {
@@ -4786,7 +4913,15 @@ async function collabInit(params) {
4786
4913
  defaultBranch: canonicalLane2.defaultBranch ?? defaultBranch,
4787
4914
  laneId: canonicalLane2.laneId ?? null,
4788
4915
  branchName: defaultBranch,
4789
- bindingMode: params.forceNew ? "explicit_root" : "lane"
4916
+ // This branch is reached only when the CURRENT branch is
4917
+ // not the default branch — so the binding being written
4918
+ // here is for the DEFAULT branch (the canonical/main app).
4919
+ // It must always be `explicit_root` regardless of
4920
+ // `forceNew`; the previous `forceNew ? ... : "lane"`
4921
+ // produced a `lane`-marked default-branch binding for
4922
+ // every plain init, which silently disabled the
4923
+ // history-import auto-spawn.
4924
+ bindingMode: "explicit_root"
4790
4925
  });
4791
4926
  }
4792
4927
  bindingPath2 = await writeCollabBinding(repoRoot, {
@@ -4938,7 +5073,7 @@ async function collabInit(params) {
4938
5073
  operation: "`remix collab init`",
4939
5074
  recoveryHint: "The repository changed before the Remix binding was written. Review the local changes and rerun `remix collab init`."
4940
5075
  });
4941
- const bindingMode = params.forceNew && (!defaultBranch || branchName === defaultBranch) ? "explicit_root" : "lane";
5076
+ const bindingMode = defaultBranch && branchName !== defaultBranch ? "lane" : "explicit_root";
4942
5077
  let bindingPath;
4943
5078
  if (params.forceNew && defaultBranch && canonicalLane) {
4944
5079
  const canonicalBinding = branchBindingFromLane(canonicalLane, "explicit_root", {
@@ -4980,7 +5115,12 @@ async function collabInit(params) {
4980
5115
  defaultBranch: canonicalLane.defaultBranch ?? defaultBranch,
4981
5116
  laneId: canonicalLane.laneId ?? null,
4982
5117
  branchName: defaultBranch,
4983
- bindingMode: params.forceNew ? "explicit_root" : "lane"
5118
+ // Same reasoning as the queued-path default-branch write
5119
+ // above: this is the canonical/main-app binding for the
5120
+ // default branch and must be `explicit_root`, otherwise the
5121
+ // history-import auto-spawn (gated on explicit_root) will
5122
+ // silently no-op for every plain init.
5123
+ bindingMode: "explicit_root"
4984
5124
  });
4985
5125
  }
4986
5126
  bindingPath = await writeCollabBinding(repoRoot, {
@@ -5068,10 +5208,11 @@ async function collabList(params) {
5068
5208
  };
5069
5209
  }
5070
5210
 
5071
- // src/application/collab/collabReAnchor.ts
5072
- import { randomUUID as randomUUID5 } from "crypto";
5211
+ // src/application/collab/collabReconcile.ts
5073
5212
  import fs11 from "fs/promises";
5213
+ import os5 from "os";
5074
5214
  import path10 from "path";
5215
+ import { execa as execa3 } from "execa";
5075
5216
 
5076
5217
  // src/application/collab/pendingFinalize.ts
5077
5218
  function hasPendingFinalize(summary) {
@@ -5081,258 +5222,7 @@ function buildPendingFinalizeHint() {
5081
5222
  return "Drain or await the local finalize queue first, then retry after the queued Remix turn finishes recording remotely.";
5082
5223
  }
5083
5224
 
5084
- // src/application/collab/collabReAnchor.ts
5085
- async function collabReAnchor(params) {
5086
- const repoRoot = await findGitRoot(params.cwd);
5087
- const binding = await ensureActiveLaneBinding({
5088
- repoRoot,
5089
- api: params.api,
5090
- operation: "`remix collab re-anchor`"
5091
- });
5092
- if (!binding) {
5093
- throw new RemixError("Repository is not bound to Remix.", {
5094
- exitCode: 2,
5095
- hint: "Run `remix collab init` first."
5096
- });
5097
- }
5098
- const detected = await collabDetectRepoState({
5099
- api: params.api,
5100
- cwd: repoRoot,
5101
- allowBranchMismatch: params.allowBranchMismatch
5102
- });
5103
- if (detected.status === "metadata_conflict" || detected.status === "branch_mismatch") {
5104
- throw new RemixError("Repository must be realigned before seeding a fresh local Remix baseline.", {
5105
- exitCode: 2,
5106
- hint: detected.hint
5107
- });
5108
- }
5109
- if (detected.status !== "ready" || !detected.binding) {
5110
- throw new RemixError(detected.hint || "Repository is not ready for re-anchor.", {
5111
- exitCode: 2,
5112
- hint: detected.hint
5113
- });
5114
- }
5115
- if (detected.repoState === "server_only_changed") {
5116
- throw new RemixError("This checkout is already on a server-known base and only needs a local pull.", {
5117
- exitCode: 2,
5118
- hint: "Run `remix collab sync` instead of `remix collab re-anchor`."
5119
- });
5120
- }
5121
- if (detected.repoState === "both_changed") {
5122
- throw new RemixError("Both the local workspace and the server lane changed since the last agreed baseline.", {
5123
- exitCode: 2,
5124
- hint: "Run `remix collab reconcile` to replay the local boundary onto the newer server head."
5125
- });
5126
- }
5127
- if (detected.repoState === "local_only_changed") {
5128
- if (hasPendingFinalize(detected.pendingFinalize)) {
5129
- throw new RemixError("Re-anchor is not needed while queued Remix turn recording is still processing.", {
5130
- exitCode: 2,
5131
- hint: buildPendingFinalizeHint()
5132
- });
5133
- }
5134
- throw new RemixError("Re-anchor is not the right command for local content changes.", {
5135
- exitCode: 2,
5136
- 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`)."
5137
- });
5138
- }
5139
- if (detected.repoState === "idle") {
5140
- throw new RemixError("This checkout is already aligned with Remix.", {
5141
- exitCode: 2,
5142
- hint: "No re-anchor step is needed. Re-anchor only applies when no local Remix baseline exists yet for this lane."
5143
- });
5144
- }
5145
- await ensureCleanWorktree(repoRoot, "`remix collab re-anchor`");
5146
- const branch = await requireCurrentBranch(repoRoot);
5147
- const headCommitHash = await getHeadCommitHash(repoRoot);
5148
- if (!headCommitHash) {
5149
- throw new RemixError("Failed to resolve local HEAD commit.", { exitCode: 1 });
5150
- }
5151
- if (params.asyncSubmit && !params.dryRun) {
5152
- const pending = await findPendingAsyncJob({
5153
- repoRoot,
5154
- branchName: binding.branchName ?? branch,
5155
- kind: "re_anchor"
5156
- });
5157
- if (pending) {
5158
- return {
5159
- status: "queued",
5160
- queued: true,
5161
- jobId: pending.id,
5162
- repoRoot,
5163
- branch,
5164
- currentAppId: binding.currentAppId,
5165
- dryRun: false,
5166
- applied: false
5167
- };
5168
- }
5169
- }
5170
- const preflightResp = await params.api.preflightAppReconcile(binding.currentAppId, {
5171
- localHeadCommitHash: headCommitHash,
5172
- repoFingerprint: binding.repoFingerprint ?? void 0,
5173
- remoteUrl: binding.remoteUrl ?? void 0,
5174
- defaultBranch: binding.defaultBranch ?? void 0
5175
- });
5176
- const preflight = unwrapResponseObject(preflightResp, "reconcile preflight");
5177
- if (preflight.status === "metadata_conflict") {
5178
- throw new RemixError("Local repository metadata conflicts with the bound Remix app.", {
5179
- exitCode: 2,
5180
- hint: preflight.warnings.join("\n") || "Run the command from the correct bound repository."
5181
- });
5182
- }
5183
- const preview = {
5184
- status: preflight.status === "up_to_date" ? "reanchored" : "re_anchor_required",
5185
- repoRoot,
5186
- branch,
5187
- currentAppId: binding.currentAppId,
5188
- localHeadCommitHash: headCommitHash,
5189
- targetHeadCommitHash: preflight.targetHeadCommitHash,
5190
- targetHeadCommitId: preflight.targetHeadCommitId,
5191
- warnings: preflight.warnings,
5192
- applied: false,
5193
- dryRun: params.dryRun === true
5194
- };
5195
- if (params.dryRun) {
5196
- return preview;
5197
- }
5198
- let anchoredServerHeadHash = preflight.targetHeadCommitHash;
5199
- if (params.asyncSubmit && preflight.status === "ready_to_reconcile") {
5200
- const failed = await findFailedAsyncJob({
5201
- repoRoot,
5202
- branchName: binding.branchName ?? branch,
5203
- kind: "re_anchor"
5204
- });
5205
- if (failed) {
5206
- await deleteAsyncJob(failed.id);
5207
- }
5208
- const { bundlePath: tmpBundlePath, headCommitHash: bundledHeadCommitHash } = await createGitBundle(
5209
- repoRoot,
5210
- "re-anchor.bundle"
5211
- );
5212
- const tmpBundleDir = path10.dirname(tmpBundlePath);
5213
- try {
5214
- const jobId = randomUUID5();
5215
- const durableBundlePath = getAsyncJobBundlePath(jobId);
5216
- await fs11.mkdir(getAsyncJobDir(jobId), { recursive: true });
5217
- try {
5218
- await fs11.rename(tmpBundlePath, durableBundlePath);
5219
- } catch (error) {
5220
- if (error?.code !== "EXDEV") throw error;
5221
- await fs11.copyFile(tmpBundlePath, durableBundlePath);
5222
- await fs11.unlink(tmpBundlePath).catch(() => void 0);
5223
- }
5224
- const bundleSha = await sha256FileHex(durableBundlePath);
5225
- const job = await enqueueAsyncJob({
5226
- id: jobId,
5227
- kind: "re_anchor",
5228
- status: "queued",
5229
- repoRoot,
5230
- repoFingerprint: binding.repoFingerprint,
5231
- branchName: binding.branchName ?? branch,
5232
- laneId: binding.laneId,
5233
- retryCount: 0,
5234
- error: null,
5235
- idempotencyKey: null,
5236
- payload: {
5237
- bundlePath: durableBundlePath,
5238
- bundleSha256: bundleSha,
5239
- localHeadCommitHash: bundledHeadCommitHash,
5240
- targetHeadCommitHash: preflight.targetHeadCommitHash,
5241
- appId: binding.currentAppId
5242
- }
5243
- });
5244
- await logDrainerEvent(job.id, "submitted", { kind: "re_anchor" });
5245
- return {
5246
- status: "queued",
5247
- queued: true,
5248
- jobId: job.id,
5249
- repoRoot,
5250
- branch,
5251
- currentAppId: binding.currentAppId,
5252
- localHeadCommitHash: bundledHeadCommitHash,
5253
- targetHeadCommitHash: preflight.targetHeadCommitHash,
5254
- warnings: preflight.warnings,
5255
- dryRun: false,
5256
- applied: false
5257
- };
5258
- } finally {
5259
- await fs11.rm(tmpBundleDir, { recursive: true, force: true }).catch(() => void 0);
5260
- }
5261
- }
5262
- if (preflight.status === "ready_to_reconcile") {
5263
- const { bundlePath, headCommitHash: bundledHeadCommitHash } = await createGitBundle(repoRoot, "re-anchor.bundle");
5264
- const bundleTempDir = path10.dirname(bundlePath);
5265
- try {
5266
- const bundleStat = await fs11.stat(bundlePath);
5267
- const checksumSha256 = await sha256FileHex(bundlePath);
5268
- const presignResp = await params.api.presignImportUploadFirstParty({
5269
- file: {
5270
- name: path10.basename(bundlePath),
5271
- mimeType: "application/x-git-bundle",
5272
- size: bundleStat.size,
5273
- checksumSha256
5274
- }
5275
- });
5276
- const uploadTarget = unwrapResponseObject(presignResp, "import upload target");
5277
- await uploadPresigned({
5278
- uploadUrl: String(uploadTarget.uploadUrl),
5279
- filePath: bundlePath,
5280
- headers: uploadTarget.headers ?? {}
5281
- });
5282
- const startResp = await params.api.startAppReconcile(binding.currentAppId, {
5283
- uploadId: String(uploadTarget.uploadId),
5284
- localHeadCommitHash: bundledHeadCommitHash,
5285
- repoFingerprint: binding.repoFingerprint ?? void 0,
5286
- remoteUrl: binding.remoteUrl ?? void 0,
5287
- defaultBranch: binding.defaultBranch ?? void 0,
5288
- idempotencyKey: buildDeterministicIdempotencyKey({
5289
- kind: "collab_re_anchor_v1",
5290
- appId: binding.currentAppId,
5291
- localHeadCommitHash: bundledHeadCommitHash,
5292
- targetHeadCommitHash: preflight.targetHeadCommitHash
5293
- })
5294
- });
5295
- const started = unwrapResponseObject(startResp, "reconcile");
5296
- const reconcile = await pollReconcile(params.api, binding.currentAppId, started.id);
5297
- anchoredServerHeadHash = reconcile.reconciledHeadCommitHash ?? reconcile.targetHeadCommitHash ?? preflight.targetHeadCommitHash;
5298
- } finally {
5299
- await fs11.rm(bundleTempDir, { recursive: true, force: true });
5300
- }
5301
- }
5302
- const snapshot = await captureLocalSnapshot({
5303
- repoRoot,
5304
- repoFingerprint: binding.repoFingerprint,
5305
- laneId: binding.laneId,
5306
- branchName: binding.branchName
5307
- });
5308
- await writeLocalBaseline({
5309
- repoRoot,
5310
- repoFingerprint: binding.repoFingerprint,
5311
- laneId: binding.laneId,
5312
- currentAppId: binding.currentAppId,
5313
- branchName: binding.branchName,
5314
- lastSnapshotId: snapshot.id,
5315
- lastSnapshotHash: snapshot.snapshotHash,
5316
- lastServerHeadHash: anchoredServerHeadHash,
5317
- lastSeenLocalCommitHash: snapshot.localCommitHash
5318
- });
5319
- return {
5320
- ...preview,
5321
- status: "reanchored",
5322
- targetHeadCommitHash: anchoredServerHeadHash,
5323
- applied: true,
5324
- dryRun: false
5325
- };
5326
- }
5327
- async function collabReAnchorSubmit(params) {
5328
- return collabReAnchor({ ...params, asyncSubmit: true });
5329
- }
5330
-
5331
5225
  // src/application/collab/collabReconcile.ts
5332
- import fs12 from "fs/promises";
5333
- import os5 from "os";
5334
- import path11 from "path";
5335
- import { execa as execa3 } from "execa";
5336
5226
  async function reconcileBothChanged(params) {
5337
5227
  const repoRoot = await findGitRoot(params.cwd);
5338
5228
  const binding = await ensureActiveLaneBinding({
@@ -5355,7 +5245,7 @@ async function reconcileBothChanged(params) {
5355
5245
  if (!baseline?.lastSnapshotId || !baseline.lastServerHeadHash) {
5356
5246
  throw new RemixError("Local Remix baseline is missing for this lane.", {
5357
5247
  exitCode: 2,
5358
- 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."
5359
5249
  });
5360
5250
  }
5361
5251
  const currentSnapshot = await captureLocalSnapshot({
@@ -5378,6 +5268,7 @@ async function reconcileBothChanged(params) {
5378
5268
  params.api.getAppHead(binding.currentAppId),
5379
5269
  params.api.getAppDelta(binding.currentAppId, {
5380
5270
  baseHeadHash: baseline.lastServerHeadHash,
5271
+ baseRevisionId: baseline.lastServerRevisionId,
5381
5272
  repoFingerprint: binding.repoFingerprint ?? void 0,
5382
5273
  remoteUrl: binding.remoteUrl ?? void 0,
5383
5274
  defaultBranch: binding.defaultBranch ?? void 0
@@ -5395,7 +5286,7 @@ async function reconcileBothChanged(params) {
5395
5286
  if (delta.status === "base_unknown") {
5396
5287
  throw new RemixError("Reconcile cannot pull the newer server state from the last acknowledged baseline.", {
5397
5288
  exitCode: 2,
5398
- 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."
5399
5290
  });
5400
5291
  }
5401
5292
  if (delta.status !== "delta_ready" && delta.status !== "up_to_date") {
@@ -5426,7 +5317,9 @@ async function reconcileBothChanged(params) {
5426
5317
  assistantResponse: "Replay the local boundary delta onto the latest server head without recording a new change step.",
5427
5318
  diff: diffResult.diff,
5428
5319
  baseCommitHash: baseline.lastServerHeadHash,
5320
+ baseRevisionId: baseline.lastServerRevisionId,
5429
5321
  targetHeadCommitHash: appHead.headCommitHash,
5322
+ targetRevisionId: appHead.headRevisionId,
5430
5323
  expectedPaths: diffResult.changedPaths,
5431
5324
  workspaceMetadata: {
5432
5325
  recordingMode: "boundary_delta",
@@ -5434,6 +5327,7 @@ async function reconcileBothChanged(params) {
5434
5327
  branch,
5435
5328
  baselineSnapshotId: baseline.lastSnapshotId,
5436
5329
  currentSnapshotId: currentSnapshot.id,
5330
+ baselineServerRevisionId: baseline.lastServerRevisionId,
5437
5331
  baselineServerHeadHash: baseline.lastServerHeadHash,
5438
5332
  currentSnapshotHash: currentSnapshot.snapshotHash,
5439
5333
  localCommitHash: currentSnapshot.localCommitHash,
@@ -5452,12 +5346,12 @@ async function reconcileBothChanged(params) {
5452
5346
  const replay = await pollChangeStepReplay(params.api, binding.currentAppId, String(replayStart.id));
5453
5347
  const replayDiffResp = await params.api.getChangeStepReplayDiff(binding.currentAppId, replay.id);
5454
5348
  const replayDiff = unwrapResponseObject(replayDiffResp, "change step replay diff");
5455
- const tempRoot = await fs12.mkdtemp(path11.join(os5.tmpdir(), "remix-reconcile-"));
5349
+ const tempRoot = await fs11.mkdtemp(path10.join(os5.tmpdir(), "remix-reconcile-"));
5456
5350
  let serverHeadSnapshot = null;
5457
5351
  let mergedSnapshot = null;
5458
5352
  try {
5459
- const tempRepoRoot = path11.join(tempRoot, "repo");
5460
- await fs12.mkdir(tempRepoRoot, { recursive: true });
5353
+ const tempRepoRoot = path10.join(tempRoot, "repo");
5354
+ await fs11.mkdir(tempRepoRoot, { recursive: true });
5461
5355
  await execa3("git", ["init"], { cwd: tempRepoRoot, stderr: "ignore" });
5462
5356
  await materializeLocalSnapshot(baseline.lastSnapshotId, tempRepoRoot);
5463
5357
  if (delta.status === "delta_ready" && delta.diff.trim()) {
@@ -5479,7 +5373,7 @@ async function reconcileBothChanged(params) {
5479
5373
  branchName: binding.branchName
5480
5374
  });
5481
5375
  } finally {
5482
- await fs12.rm(tempRoot, { recursive: true, force: true }).catch(() => void 0);
5376
+ await fs11.rm(tempRoot, { recursive: true, force: true }).catch(() => void 0);
5483
5377
  }
5484
5378
  if (!serverHeadSnapshot || !mergedSnapshot) {
5485
5379
  throw new RemixError("Failed to materialize the reconciled local workspace.", { exitCode: 1 });
@@ -5516,6 +5410,8 @@ async function reconcileBothChanged(params) {
5516
5410
  branchName: binding.branchName,
5517
5411
  lastSnapshotId: serverHeadSnapshot.id,
5518
5412
  lastSnapshotHash: serverHeadSnapshot.snapshotHash,
5413
+ lastServerRevisionId: appHead.headRevisionId ?? null,
5414
+ lastServerTreeHash: appHead.treeHash ?? null,
5519
5415
  lastServerHeadHash: appHead.headCommitHash,
5520
5416
  lastSeenLocalCommitHash: restoredSnapshot.localCommitHash
5521
5417
  });
@@ -5551,7 +5447,10 @@ async function collabReconcile(params) {
5551
5447
  return reconcileBothChanged(params);
5552
5448
  }
5553
5449
  if (detected.repoState === "external_local_base_changed") {
5554
- 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
+ });
5555
5454
  }
5556
5455
  if (detected.repoState === "local_only_changed") {
5557
5456
  if (hasPendingFinalize(detected.pendingFinalize)) {
@@ -5662,6 +5561,8 @@ async function collabRemix(params) {
5662
5561
  branchName: branchNameForBaseline,
5663
5562
  lastSnapshotId: snapshot.id,
5664
5563
  lastSnapshotHash: snapshot.snapshotHash,
5564
+ lastServerRevisionId: appHead.headRevisionId ?? null,
5565
+ lastServerTreeHash: appHead.treeHash ?? null,
5665
5566
  lastServerHeadHash: appHead.headCommitHash,
5666
5567
  lastSeenLocalCommitHash: snapshot.localCommitHash
5667
5568
  });
@@ -5786,11 +5687,15 @@ function createBaseStatus() {
5786
5687
  baseline: {
5787
5688
  lastSnapshotId: null,
5788
5689
  lastSnapshotHash: null,
5690
+ lastServerRevisionId: null,
5691
+ lastServerTreeHash: null,
5789
5692
  lastServerHeadHash: null,
5790
5693
  lastSeenLocalCommitHash: null
5791
5694
  },
5792
5695
  current: {
5793
5696
  snapshotHash: null,
5697
+ serverRevisionId: null,
5698
+ serverTreeHash: null,
5794
5699
  serverHeadHash: null,
5795
5700
  serverHeadCommitId: null,
5796
5701
  localCommitHash: null
@@ -5877,6 +5782,8 @@ async function collabStatus(params) {
5877
5782
  status.alignment.baseline = detected.baseline;
5878
5783
  status.alignment.current = {
5879
5784
  snapshotHash: detected.currentSnapshotHash,
5785
+ serverRevisionId: detected.currentServerRevisionId,
5786
+ serverTreeHash: detected.currentServerTreeHash,
5880
5787
  serverHeadHash: detected.currentServerHeadHash,
5881
5788
  serverHeadCommitId: detected.currentServerHeadCommitId,
5882
5789
  localCommitHash: detected.localCommitHash
@@ -5940,7 +5847,7 @@ async function collabStatus(params) {
5940
5847
  status.reconcile.canApply = !status.repo.branchMismatch;
5941
5848
  status.recommendedAction = "reconcile";
5942
5849
  } else if (detected.repoState === "external_local_base_changed") {
5943
- status.recommendedAction = "re_anchor";
5850
+ status.recommendedAction = "init";
5944
5851
  addBlockedReason(status.sync, "baseline_missing");
5945
5852
  addBlockedReason(status.reconcile, "baseline_missing");
5946
5853
  } else if (detected.repoState === "local_only_changed") {
@@ -6104,8 +6011,8 @@ async function collabView(params) {
6104
6011
  }
6105
6012
 
6106
6013
  // src/application/collab/collabAsyncProcessing.ts
6107
- import fs13 from "fs/promises";
6108
- import path12 from "path";
6014
+ import fs12 from "fs/promises";
6015
+ import path11 from "path";
6109
6016
  var MAX_TRANSIENT_RETRIES = 5;
6110
6017
  var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
6111
6018
  "ECONNREFUSED",
@@ -6201,10 +6108,10 @@ async function processInitJob(job, api) {
6201
6108
  try {
6202
6109
  await updateAsyncJob(job.id, { status: "submitting", error: null });
6203
6110
  await logDrainerEvent(job.id, "claimed", { kind: "init" });
6204
- const bundleStat = await fs13.stat(job.payload.bundlePath);
6111
+ const bundleStat = await fs12.stat(job.payload.bundlePath);
6205
6112
  const presignResp = await api.presignImportUploadFirstParty({
6206
6113
  file: {
6207
- name: path12.basename(job.payload.bundlePath),
6114
+ name: path11.basename(job.payload.bundlePath),
6208
6115
  mimeType: "application/x-git-bundle",
6209
6116
  size: bundleStat.size,
6210
6117
  checksumSha256: job.payload.bundleSha256
@@ -6221,7 +6128,7 @@ async function processInitJob(job, api) {
6221
6128
  await updateAsyncJob(job.id, { status: "server_processing" });
6222
6129
  const importResp = await api.importFromUploadFirstParty({
6223
6130
  uploadId: String(presign.uploadId),
6224
- appName: job.payload.appName?.trim() || path12.basename(job.repoRoot),
6131
+ appName: job.payload.appName?.trim() || path11.basename(job.repoRoot),
6225
6132
  platform: "generic",
6226
6133
  isPublic: false,
6227
6134
  branch: job.payload.defaultBranch && job.branchName && job.branchName !== job.payload.defaultBranch ? job.payload.defaultBranch : job.branchName ?? void 0,
@@ -6296,7 +6203,7 @@ async function processInitJob(job, api) {
6296
6203
  boundProjectId = String(readyApp.projectId ?? boundProjectId);
6297
6204
  boundThreadId = readyApp.threadId ? String(readyApp.threadId) : boundThreadId;
6298
6205
  }
6299
- const bindingMode = job.payload.forceNew && (!defaultBranch || branchName === defaultBranch) ? "explicit_root" : "lane";
6206
+ const bindingMode = defaultBranch && branchName !== defaultBranch ? "lane" : "explicit_root";
6300
6207
  if (job.payload.forceNew && defaultBranch && canonicalLane) {
6301
6208
  const canonicalBinding = branchBindingFromLane(canonicalLane, "explicit_root", {
6302
6209
  projectId: canonicalLane.projectId ?? boundProjectId,
@@ -6335,7 +6242,12 @@ async function processInitJob(job, api) {
6335
6242
  defaultBranch: canonicalLane.defaultBranch ?? defaultBranch,
6336
6243
  laneId: canonicalLane.laneId ?? null,
6337
6244
  branchName: defaultBranch,
6338
- bindingMode: job.payload.forceNew ? "explicit_root" : "lane"
6245
+ // This branch is reached only when the current branch is NOT
6246
+ // the default branch — so the binding being written here is
6247
+ // for the DEFAULT branch (the canonical/main app). It must
6248
+ // always be `explicit_root` so the history-import auto-spawn
6249
+ // can fire on first-ever inits (see autoSpawnHistoryImport.ts).
6250
+ bindingMode: "explicit_root"
6339
6251
  });
6340
6252
  }
6341
6253
  await writeCollabBinding(repoRoot, {
@@ -6410,7 +6322,7 @@ async function processInitPostJob(job, api) {
6410
6322
  if (outcome.status === "failed") {
6411
6323
  const bindingPath = getCollabBindingPath(job.repoRoot);
6412
6324
  try {
6413
- await fs13.unlink(bindingPath);
6325
+ await fs12.unlink(bindingPath);
6414
6326
  await logDrainerEvent(job.id, "binding_cleared", {
6415
6327
  kind: "init_post",
6416
6328
  appId: job.payload.appId,
@@ -6451,10 +6363,10 @@ async function processReAnchorJob(job, api) {
6451
6363
  }
6452
6364
  let anchoredServerHeadHash = preflight.targetHeadCommitHash;
6453
6365
  if (preflight.status === "ready_to_reconcile") {
6454
- const bundleStat = await fs13.stat(job.payload.bundlePath);
6366
+ const bundleStat = await fs12.stat(job.payload.bundlePath);
6455
6367
  const presignResp = await api.presignImportUploadFirstParty({
6456
6368
  file: {
6457
- name: path12.basename(job.payload.bundlePath),
6369
+ name: path11.basename(job.payload.bundlePath),
6458
6370
  mimeType: "application/x-git-bundle",
6459
6371
  size: bundleStat.size,
6460
6372
  checksumSha256: job.payload.bundleSha256
@@ -6558,9 +6470,9 @@ async function collabReAnchorProcess(jobId, opts) {
6558
6470
  }
6559
6471
  async function acquireDrainerPidLock() {
6560
6472
  const pidPath = getDrainerPidPath();
6561
- await fs13.mkdir(path12.dirname(pidPath), { recursive: true });
6473
+ await fs12.mkdir(path11.dirname(pidPath), { recursive: true });
6562
6474
  try {
6563
- const existing = await fs13.readFile(pidPath, "utf8").catch(() => "");
6475
+ const existing = await fs12.readFile(pidPath, "utf8").catch(() => "");
6564
6476
  const existingPid = parseInt(existing.trim(), 10);
6565
6477
  if (Number.isFinite(existingPid) && existingPid > 0 && existingPid !== process.pid) {
6566
6478
  try {
@@ -6570,13 +6482,13 @@ async function acquireDrainerPidLock() {
6570
6482
  if (error?.code !== "ESRCH") return null;
6571
6483
  }
6572
6484
  }
6573
- await fs13.writeFile(pidPath, String(process.pid), "utf8");
6485
+ await fs12.writeFile(pidPath, String(process.pid), "utf8");
6574
6486
  return {
6575
6487
  release: async () => {
6576
6488
  try {
6577
- const current = (await fs13.readFile(pidPath, "utf8")).trim();
6489
+ const current = (await fs12.readFile(pidPath, "utf8")).trim();
6578
6490
  if (current === String(process.pid)) {
6579
- await fs13.unlink(pidPath).catch(() => void 0);
6491
+ await fs12.unlink(pidPath).catch(() => void 0);
6580
6492
  }
6581
6493
  } catch {
6582
6494
  }
@@ -6627,9 +6539,6 @@ export {
6627
6539
  collabList,
6628
6540
  collabListMembers,
6629
6541
  collabListMergeRequests,
6630
- collabReAnchor,
6631
- collabReAnchorProcess,
6632
- collabReAnchorSubmit,
6633
6542
  collabReconcile,
6634
6543
  collabRecordingPreflight,
6635
6544
  collabReject,