@ricsam/r5d-worker 0.0.57 → 0.0.59

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.
@@ -5,8 +5,8 @@ import { managedBranchPath } from "./managed-paths.mjs";
5
5
  import { WorkspaceMutationGate } from "./workspace-mutation-gate.mjs";
6
6
  const WORKSPACE_BRANCH = "main";
7
7
  const WORKSPACE_INTENT_REF_PREFIX = "refs/r5d/workspace-intents/";
8
+ const WORKSPACE_PUBLICATION_REF_PREFIX = "refs/r5d/workspace-publications/";
8
9
  const MAX_WORKSPACE_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
9
- const WORKSPACE_PERIODIC_SCAN_INTERVAL_MS = 5e3;
10
10
  const MAX_REPORTED_WORKSPACE_PATHS = 2e3;
11
11
  const MAX_REPORTED_WORKSPACE_STATUS_BYTES = 256 * 1024;
12
12
  const MAX_MIRROR_COMPARISON_CACHE_ENTRIES = 2e5;
@@ -911,7 +911,7 @@ function hydrateProjectFromShadowOptimistically(input, relativeRoot, visibleRoot
911
911
  expectedCheckoutSnapshot = checkoutGitSnapshot(visibleRoot);
912
912
  return expectedCheckoutSnapshot !== null;
913
913
  };
914
- let currentGitlinks = new Map(checkoutGitlinks(visibleRoot).map((entry) => [entry.filePath, entry]));
914
+ const currentGitlinks = new Map(checkoutGitlinks(visibleRoot).map((entry) => [entry.filePath, entry]));
915
915
  const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
916
916
  (filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
917
917
  );
@@ -1533,6 +1533,33 @@ function resetUncommittedShadowSnapshot(input) {
1533
1533
  }
1534
1534
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean interrupted workspace snapshot");
1535
1535
  }
1536
+ function activeWorkspaceRebase(input) {
1537
+ for (const name of ["rebase-merge", "rebase-apply"]) {
1538
+ const resolved = runGitResult(input, input.shadowRoot, ["rev-parse", "--git-path", name]);
1539
+ if (resolved.exitCode !== 0 || !resolved.stdout) continue;
1540
+ const gitPath = path.isAbsolute(resolved.stdout) ? resolved.stdout : path.resolve(input.shadowRoot, resolved.stdout);
1541
+ if (fs.existsSync(gitPath)) return name;
1542
+ }
1543
+ return null;
1544
+ }
1545
+ function remediationRebaseInProgressResult(input, result) {
1546
+ if (input.trigger.type !== "remediation" && input.trigger.type !== "remediation_confirm") return null;
1547
+ const rebaseKind = activeWorkspaceRebase(input);
1548
+ if (!rebaseKind) return null;
1549
+ const conflictPaths = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]).stdout.split("\0").filter(Boolean).sort();
1550
+ const status = reportedStatus(gitStatus(input));
1551
+ return {
1552
+ ...result,
1553
+ outcome: "conflict_blocked",
1554
+ expectedHead: revParse(input, `origin/${WORKSPACE_BRANCH}`),
1555
+ candidateHead: revParse(input, "ORIG_HEAD") ?? revParse(input, "HEAD") ?? void 0,
1556
+ rebaseCount: 1,
1557
+ gitStatus: status,
1558
+ affectedProjects: affectedProjects(conflictPaths),
1559
+ affectedPaths: reportedPaths(conflictPaths),
1560
+ error: `Workspace conflict remediation has an in-progress ${rebaseKind}; the index and worktree were preserved until the resolver completes or aborts it.`
1561
+ };
1562
+ }
1536
1563
  function fastForwardCleanShadowForNewCheckouts(input) {
1537
1564
  if (!input.newVisibleCheckouts?.length) return null;
1538
1565
  runGit(input, input.shadowRoot, ["add", "-A"], "stage existing workspace changes before checkout hydration");
@@ -1678,9 +1705,12 @@ function commitMessage(input) {
1678
1705
  });
1679
1706
  }
1680
1707
  function assertWorkspaceQuarantineRef(quarantineRef) {
1681
- const suffix = quarantineRef.slice(WORKSPACE_INTENT_REF_PREFIX.length);
1682
- if (!quarantineRef.startsWith(WORKSPACE_INTENT_REF_PREFIX) || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(suffix)) {
1683
- throw new Error(`Workspace candidate ref must be below ${WORKSPACE_INTENT_REF_PREFIX}`);
1708
+ const prefix = quarantineRef.startsWith(WORKSPACE_PUBLICATION_REF_PREFIX) ? WORKSPACE_PUBLICATION_REF_PREFIX : WORKSPACE_INTENT_REF_PREFIX;
1709
+ const suffix = quarantineRef.slice(prefix.length);
1710
+ if (!quarantineRef.startsWith(prefix) || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(suffix)) {
1711
+ throw new Error(
1712
+ `Workspace candidate ref must be below ${WORKSPACE_PUBLICATION_REF_PREFIX} or ${WORKSPACE_INTENT_REF_PREFIX}`
1713
+ );
1684
1714
  }
1685
1715
  const result = Bun.spawnSync(["git", "check-ref-format", quarantineRef], {
1686
1716
  stdout: "pipe",
@@ -1752,17 +1782,105 @@ function convergeRedundantInboundCandidate(input, result) {
1752
1782
  affectedPaths: []
1753
1783
  };
1754
1784
  }
1785
+ function preserveWorkspaceConflictCandidate(input, observed, options) {
1786
+ try {
1787
+ if (!options.baseRevision) throw new Error("the preserved workspace does not have an old-base commit");
1788
+ runGit(input, input.shadowRoot, ["reset", "--hard", options.baseRevision], "restore old-base workspace conflict candidate");
1789
+ runGit(input, input.shadowRoot, ["clean", "-fd"], "clean old-base workspace conflict candidate");
1790
+ if (!options.preserveShadowCandidate) {
1791
+ const projection = mirrorVisibleWorkspaceToShadow(input);
1792
+ restoreUnscopedCanonicalCheckoutSubtrees(input);
1793
+ const stagePathspecs = projection.opaqueRoots.flatMap((root) => [`:(exclude,literal)${root}`]);
1794
+ runGit(input, input.shadowRoot, ["add", "-A", "--", ".", ...stagePathspecs], "stage preserved workspace conflict candidate");
1795
+ forceStageCanonicalCheckoutFiles(input, projection.entries, projection.opaqueRoots);
1796
+ if (stagedPaths(input).length > 0) {
1797
+ runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit preserved workspace conflict candidate");
1798
+ }
1799
+ }
1800
+ const candidateHead = revParse(input, "HEAD");
1801
+ if (!candidateHead) throw new Error("the preserved workspace conflict candidate does not have a commit");
1802
+ assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
1803
+ const upload = uploadWorkspaceCandidate(input, candidateHead);
1804
+ if (upload.exitCode !== 0) {
1805
+ throw new Error(upload.stderr || upload.stdout || "workspace conflict candidate upload failed");
1806
+ }
1807
+ const conflictPaths = [...new Set(options.conflictPaths)].sort();
1808
+ return {
1809
+ ...observed,
1810
+ outcome: "conflict_blocked",
1811
+ expectedHead: options.expectedHead,
1812
+ candidateHead,
1813
+ quarantineRef: input.quarantineRef,
1814
+ ...options.publishedHead ? { publishedHead: options.publishedHead } : {},
1815
+ rebaseCount: options.rebaseCount ?? 0,
1816
+ affectedProjects: affectedProjects(conflictPaths),
1817
+ affectedPaths: reportedPaths(conflictPaths),
1818
+ error: options.error,
1819
+ gitStatus: gitStatus(input)
1820
+ };
1821
+ } catch (error) {
1822
+ return {
1823
+ ...observed,
1824
+ outcome: "failed",
1825
+ rebaseCount: options.rebaseCount ?? 0,
1826
+ affectedProjects: affectedProjects(options.conflictPaths),
1827
+ affectedPaths: reportedPaths(options.conflictPaths),
1828
+ error: `Workspace synchronization found a conflict but could not preserve a durable candidate: ${error instanceof Error ? error.message : String(error)}`,
1829
+ gitStatus: gitStatus(input)
1830
+ };
1831
+ }
1832
+ }
1833
+ function preserveLargeDiffCandidate(input, observed, error) {
1834
+ try {
1835
+ if (stagedPaths(input).length > 0) {
1836
+ runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit blocked workspace candidate");
1837
+ }
1838
+ const candidateHead = revParse(input, "HEAD");
1839
+ if (!candidateHead) throw new Error("the blocked workspace candidate does not have a commit");
1840
+ assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
1841
+ const upload = uploadWorkspaceCandidate(input, candidateHead);
1842
+ if (upload.exitCode !== 0) {
1843
+ throw new Error(upload.stderr || upload.stdout || "blocked workspace candidate upload failed");
1844
+ }
1845
+ return {
1846
+ ...observed,
1847
+ outcome: "large_diff_blocked",
1848
+ expectedHead: revParse(input, `origin/${WORKSPACE_BRANCH}`),
1849
+ candidateHead,
1850
+ quarantineRef: input.quarantineRef,
1851
+ error,
1852
+ gitStatus: gitStatus(input)
1853
+ };
1854
+ } catch (candidateError) {
1855
+ return {
1856
+ ...observed,
1857
+ outcome: "failed",
1858
+ error: `Workspace safety guard blocked publication but could not preserve a durable candidate: ${candidateError instanceof Error ? candidateError.message : String(candidateError)}`,
1859
+ gitStatus: gitStatus(input)
1860
+ };
1861
+ }
1862
+ }
1755
1863
  async function synchronizeWorkspace(rawInput) {
1756
1864
  let input = { ...rawInput, attemptId: rawInput.attemptId ?? crypto.randomUUID() };
1757
1865
  try {
1758
1866
  input = { ...input, projects: workspaceProjectsForSync(input.projects, input.trigger) };
1867
+ const isCanonicalRemediationSync = input.skipVisibleMirror === true && (input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm");
1759
1868
  let mirroredGitlinkProjection = { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map() };
1760
1869
  let fastForwardedFromHead = null;
1761
1870
  const shadowWasCreated = ensureShadowWorkspace(input);
1762
1871
  let hydrationPreimages = input.skipVisibleMirror ? hydrationPreimagesFromShadowRevision(input, shadowWasCreated ? null : revParse(input, "HEAD")) : /* @__PURE__ */ new Map();
1763
1872
  const remoteHeadAtStart = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1764
1873
  const result = baseResult(input, remoteHeadAtStart);
1765
- const inboundConflictsAtStart = input.skipVisibleMirror && remoteHeadAtStart ? visibleHydrationConflictPaths(input, hydrationPreimages, `origin/${WORKSPACE_BRANCH}`) : [];
1874
+ if (input.expectedCanonicalHead !== void 0 && remoteHeadAtStart !== input.expectedCanonicalHead) {
1875
+ return {
1876
+ ...result,
1877
+ outcome: "failed",
1878
+ expectedHead: input.expectedCanonicalHead,
1879
+ publishedHead: remoteHeadAtStart ?? void 0,
1880
+ error: `canonical_head_advanced: expected ${input.expectedCanonicalHead ?? "unborn"}, fetched ${remoteHeadAtStart ?? "unborn"}`
1881
+ };
1882
+ }
1883
+ const inboundConflictsAtStart = input.skipVisibleMirror && !isCanonicalRemediationSync && remoteHeadAtStart ? visibleHydrationConflictPaths(input, hydrationPreimages, `origin/${WORKSPACE_BRANCH}`) : [];
1766
1884
  if (input.resetToCanonical) {
1767
1885
  if (input.trigger.canonicalCheckoutOnly) {
1768
1886
  throw new Error("A canonical-checkout-only synchronization cannot reset the authoritative resolver checkout");
@@ -1776,6 +1894,8 @@ async function synchronizeWorkspace(rawInput) {
1776
1894
  gitStatus: gitStatus(input)
1777
1895
  };
1778
1896
  }
1897
+ const inProgressRemediation = remediationRebaseInProgressResult(input, result);
1898
+ if (inProgressRemediation) return inProgressRemediation;
1779
1899
  if (!input.skipVisibleMirror) resetUncommittedShadowSnapshot(input);
1780
1900
  restoreUnscopedCanonicalCheckoutSubtrees(input);
1781
1901
  if (!input.skipVisibleMirror) {
@@ -1808,18 +1928,18 @@ async function synchronizeWorkspace(rawInput) {
1808
1928
  const destructiveReductions = destructiveCheckoutReductions(input, baseRevision);
1809
1929
  if (destructiveReductions.length > 0 && !input.confirmedLargeDiff) {
1810
1930
  const summary = destructiveReductions.map(({ root, trackedBefore, trackedAfter }) => `${root} (${trackedBefore} -> ${trackedAfter} tracked files)`).join(", ");
1811
- return {
1812
- ...observed,
1813
- outcome: "large_diff_blocked",
1814
- error: `Workspace safety guard blocked a destructive checkout reduction: ${summary}`
1815
- };
1931
+ return preserveLargeDiffCandidate(
1932
+ input,
1933
+ observed,
1934
+ `Workspace safety guard blocked a destructive checkout reduction: ${summary}`
1935
+ );
1816
1936
  }
1817
1937
  if (paths.length > 0 && diffSizeBytes > MAX_WORKSPACE_SYNC_DIFF_BYTES && !input.confirmedLargeDiff && !input.trigger.canonicalCheckoutOnly) {
1818
- return {
1819
- ...observed,
1820
- outcome: "large_diff_blocked",
1821
- error: `Workspace safety guard blocked a ${diffSizeBytes}-byte diff above the ${MAX_WORKSPACE_SYNC_DIFF_BYTES}-byte automatic publication limit.`
1822
- };
1938
+ return preserveLargeDiffCandidate(
1939
+ input,
1940
+ observed,
1941
+ `Workspace safety guard blocked a ${diffSizeBytes}-byte diff above the ${MAX_WORKSPACE_SYNC_DIFF_BYTES}-byte automatic publication limit.`
1942
+ );
1823
1943
  }
1824
1944
  if (stagedWorkingPaths.length > 0) {
1825
1945
  runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit workspace changes");
@@ -1831,15 +1951,12 @@ async function synchronizeWorkspace(rawInput) {
1831
1951
  const hasUnpushedCommit = candidateHead ? !hasRemoteMain || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", candidateHead, `origin/${WORKSPACE_BRANCH}`]) : false;
1832
1952
  if (hasUnpushedCommit && inboundConflictsAtStart.length > 0) {
1833
1953
  const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...inboundConflictsAtStart])].sort();
1834
- return {
1835
- ...observed,
1836
- outcome: "failed",
1837
- candidateHead: candidateHead ?? void 0,
1838
- affectedProjects: affectedProjects(conflictPaths),
1839
- affectedPaths: reportedPaths(conflictPaths),
1840
- error: "Inbound workspace update overlaps local visible changes; the old-base candidate was preserved for reconciliation.",
1841
- gitStatus: gitStatus(input)
1842
- };
1954
+ return preserveWorkspaceConflictCandidate(input, observed, {
1955
+ baseRevision: candidateHead,
1956
+ expectedHead: revParse(input, `origin/${WORKSPACE_BRANCH}`),
1957
+ conflictPaths,
1958
+ error: "Inbound workspace update overlaps local visible changes; the old-base candidate was preserved for reconciliation."
1959
+ });
1843
1960
  }
1844
1961
  if (!hasUnpushedCommit) {
1845
1962
  const localHead = revParse(input, "HEAD");
@@ -1848,22 +1965,21 @@ async function synchronizeWorkspace(rawInput) {
1848
1965
  runGit(input, input.shadowRoot, ["reset", "--hard", `origin/${WORKSPACE_BRANCH}`], "update workspace from canonical state");
1849
1966
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean updated workspace");
1850
1967
  assertPublishedCanonicalCheckoutTree(input, currentRemoteHead, sampledCanonicalCheckoutTreeHash);
1851
- mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
1852
- const inboundConflicts = input.trigger.canonicalCheckoutOnly ? [] : localHead ? hydrationConflictPaths(input, hydrationPreimages, localHead, currentRemoteHead) : visibleHydrationConflictPaths(input, hydrationPreimages, currentRemoteHead);
1968
+ if (!isCanonicalRemediationSync) {
1969
+ mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
1970
+ }
1971
+ const inboundConflicts = input.trigger.canonicalCheckoutOnly || isCanonicalRemediationSync ? [] : localHead ? hydrationConflictPaths(input, hydrationPreimages, localHead, currentRemoteHead) : visibleHydrationConflictPaths(input, hydrationPreimages, currentRemoteHead);
1853
1972
  if (inboundConflicts.length > 0 && localHead) {
1854
1973
  runGit(input, input.shadowRoot, ["reset", "--hard", localHead], "preserve old workspace base after inbound conflict");
1855
1974
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean preserved old workspace base");
1856
1975
  const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...inboundConflicts])].sort();
1857
- return {
1858
- ...observed,
1859
- outcome: "failed",
1860
- candidateHead: candidateHead ?? void 0,
1976
+ return preserveWorkspaceConflictCandidate(input, observed, {
1977
+ baseRevision: localHead,
1978
+ expectedHead: currentRemoteHead,
1861
1979
  publishedHead: currentRemoteHead,
1862
- affectedProjects: affectedProjects(conflictPaths),
1863
- affectedPaths: reportedPaths(conflictPaths),
1864
- error: "Inbound workspace update overlaps local visible changes; disjoint updates were hydrated and the old base was preserved for reconciliation.",
1865
- gitStatus: gitStatus(input)
1866
- };
1980
+ conflictPaths,
1981
+ error: "Inbound workspace update overlaps local visible changes; disjoint updates were hydrated and the old base was preserved for reconciliation."
1982
+ });
1867
1983
  }
1868
1984
  return {
1869
1985
  ...observed,
@@ -1876,8 +1992,10 @@ async function synchronizeWorkspace(rawInput) {
1876
1992
  const authoritativeHead = currentRemoteHead ?? localHead;
1877
1993
  if (authoritativeHead) {
1878
1994
  assertPublishedCanonicalCheckoutTree(input, authoritativeHead, sampledCanonicalCheckoutTreeHash);
1879
- mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
1880
- const hydrationConflicts2 = input.trigger.canonicalCheckoutOnly ? [] : fastForwardedFromHead ? hydrationConflictPaths(input, hydrationPreimages, fastForwardedFromHead, authoritativeHead) : visibleHydrationConflictPaths(input, hydrationPreimages, authoritativeHead);
1995
+ if (!isCanonicalRemediationSync) {
1996
+ mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
1997
+ }
1998
+ const hydrationConflicts2 = input.trigger.canonicalCheckoutOnly || isCanonicalRemediationSync ? [] : fastForwardedFromHead ? hydrationConflictPaths(input, hydrationPreimages, fastForwardedFromHead, authoritativeHead) : visibleHydrationConflictPaths(input, hydrationPreimages, authoritativeHead);
1881
1999
  if (hydrationConflicts2.length > 0 && fastForwardedFromHead) {
1882
2000
  runGit(
1883
2001
  input,
@@ -1886,16 +2004,13 @@ async function synchronizeWorkspace(rawInput) {
1886
2004
  "preserve old workspace base after new-checkout fast-forward conflict"
1887
2005
  );
1888
2006
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean preserved new-checkout workspace base");
1889
- return {
1890
- ...observed,
1891
- outcome: "failed",
1892
- candidateHead: candidateHead ?? void 0,
2007
+ return preserveWorkspaceConflictCandidate(input, observed, {
2008
+ baseRevision: fastForwardedFromHead,
2009
+ expectedHead: currentRemoteHead,
1893
2010
  publishedHead: currentRemoteHead ?? void 0,
1894
- affectedProjects: affectedProjects(hydrationConflicts2),
1895
- affectedPaths: reportedPaths(hydrationConflicts2),
1896
- error: "Workspace fast-forward for a new checkout overlaps local visible changes; compatible updates were hydrated and the old base was preserved for reconciliation.",
1897
- gitStatus: gitStatus(input)
1898
- };
2011
+ conflictPaths: hydrationConflicts2,
2012
+ error: "Workspace fast-forward for a new checkout overlaps local visible changes; compatible updates were hydrated and the old base was preserved for reconciliation."
2013
+ });
1899
2014
  }
1900
2015
  }
1901
2016
  return {
@@ -1915,17 +2030,14 @@ async function synchronizeWorkspace(rawInput) {
1915
2030
  const conflicted = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]);
1916
2031
  const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort();
1917
2032
  runGit(input, input.shadowRoot, ["rebase", "--abort"], "preserve workspace candidate after rebase conflict");
1918
- return {
1919
- ...observed,
1920
- outcome: "failed",
2033
+ return preserveWorkspaceConflictCandidate(input, observed, {
2034
+ baseRevision: candidateHead,
1921
2035
  expectedHead,
1922
- candidateHead: candidateHead ?? void 0,
1923
2036
  rebaseCount,
1924
- affectedProjects: affectedProjects(conflictPaths),
1925
- affectedPaths: reportedPaths(conflictPaths),
1926
- error: `Workspace candidate conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation.`,
1927
- gitStatus: gitStatus(input)
1928
- };
2037
+ preserveShadowCandidate: isCanonicalRemediationSync,
2038
+ conflictPaths,
2039
+ error: `Workspace candidate conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation.`
2040
+ });
1929
2041
  }
1930
2042
  candidateHead = revParse(input, "HEAD");
1931
2043
  assertPublishedCanonicalCheckoutTree(input, "HEAD", sampledCanonicalCheckoutTreeHash);
@@ -1941,8 +2053,10 @@ async function synchronizeWorkspace(rawInput) {
1941
2053
  };
1942
2054
  }
1943
2055
  assertPublishedCanonicalCheckoutTree(input, candidateHead, sampledCanonicalCheckoutTreeHash);
1944
- mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
1945
- const hydrationConflicts = input.trigger.canonicalCheckoutOnly ? [] : candidateHeadBeforeRebase ? hydrationConflictPaths(input, hydrationPreimages, candidateHeadBeforeRebase, "HEAD") : visibleHydrationConflictPaths(input, hydrationPreimages, "HEAD");
2056
+ if (!isCanonicalRemediationSync) {
2057
+ mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
2058
+ }
2059
+ const hydrationConflicts = input.trigger.canonicalCheckoutOnly || isCanonicalRemediationSync ? [] : candidateHeadBeforeRebase ? hydrationConflictPaths(input, hydrationPreimages, candidateHeadBeforeRebase, "HEAD") : visibleHydrationConflictPaths(input, hydrationPreimages, "HEAD");
1946
2060
  if (hydrationConflicts.length > 0 && candidateHeadBeforeRebase) {
1947
2061
  runGit(
1948
2062
  input,
@@ -1952,17 +2066,13 @@ async function synchronizeWorkspace(rawInput) {
1952
2066
  );
1953
2067
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean old-base workspace candidate");
1954
2068
  const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...hydrationConflicts])].sort();
1955
- return {
1956
- ...observed,
1957
- outcome: "failed",
2069
+ return preserveWorkspaceConflictCandidate(input, observed, {
2070
+ baseRevision: candidateHeadBeforeRebase,
1958
2071
  expectedHead,
1959
- candidateHead,
1960
2072
  rebaseCount,
1961
- affectedProjects: affectedProjects(conflictPaths),
1962
- affectedPaths: reportedPaths(conflictPaths),
1963
- error: "Workspace changed while its candidate was reconciling; visible changes and the old-base candidate were preserved.",
1964
- gitStatus: gitStatus(input)
1965
- };
2073
+ conflictPaths,
2074
+ error: "Workspace changed while its candidate was reconciling; visible changes and the old-base candidate were preserved."
2075
+ });
1966
2076
  }
1967
2077
  const upload = uploadWorkspaceCandidate(input, candidateHead);
1968
2078
  if (upload.exitCode !== 0) {
@@ -1994,6 +2104,79 @@ async function synchronizeWorkspace(rawInput) {
1994
2104
  };
1995
2105
  }
1996
2106
  }
2107
+ async function convergeWorkspaceHead(rawInput, options) {
2108
+ const input = {
2109
+ ...rawInput,
2110
+ projects: workspaceProjectsForSync(rawInput.projects, rawInput.trigger)
2111
+ };
2112
+ if (input.trigger.canonicalCheckoutOnly) {
2113
+ throw new Error("Ordinary inbound convergence cannot target a canonical resolver checkout");
2114
+ }
2115
+ ensureShadowWorkspace(input);
2116
+ const fetchedLocalHead = revParse(input, "HEAD");
2117
+ const fetchedCanonicalHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
2118
+ if (!options.hydrateVisible) {
2119
+ return {
2120
+ localHead: fetchedLocalHead,
2121
+ canonicalHead: fetchedCanonicalHead,
2122
+ localChanges: false,
2123
+ hydrated: false,
2124
+ hydrationConflicts: []
2125
+ };
2126
+ }
2127
+ resetUncommittedShadowSnapshot(input);
2128
+ restoreUnscopedCanonicalCheckoutSubtrees(input);
2129
+ const localHead = fetchedLocalHead;
2130
+ const canonicalHead = fetchedCanonicalHead;
2131
+ const projection = mirrorVisibleWorkspaceToShadow(input);
2132
+ runGit(
2133
+ input,
2134
+ input.shadowRoot,
2135
+ ["add", "-A", "--", ".", ...projection.opaqueRoots.map((root) => `:(exclude,literal)${root}`)],
2136
+ "stage inbound workspace observation"
2137
+ );
2138
+ forceStageCanonicalCheckoutFiles(input, projection.entries, projection.opaqueRoots);
2139
+ const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write inbound workspace observation tree");
2140
+ const headTree = localHead ? revParse(input, `${localHead}^{tree}`) : null;
2141
+ const localChanges = stagedTree !== headTree;
2142
+ if (!options.hydrateVisible || localChanges || !canonicalHead || localHead === canonicalHead) {
2143
+ resetUncommittedShadowSnapshot(input);
2144
+ return { localHead, canonicalHead, localChanges, hydrated: false, hydrationConflicts: [] };
2145
+ }
2146
+ const preimages = projection.visiblePreimages ?? hydrationPreimagesFromShadowRevision(input, localHead);
2147
+ const hydrationConflicts = localHead ? hydrationConflictPaths(input, preimages, localHead, canonicalHead) : [];
2148
+ if (hydrationConflicts.length > 0) {
2149
+ resetUncommittedShadowSnapshot(input);
2150
+ return { localHead, canonicalHead, localChanges: true, hydrated: false, hydrationConflicts };
2151
+ }
2152
+ runGit(input, input.shadowRoot, ["reset", "--hard", canonicalHead], "fast-forward workspace to available canonical head");
2153
+ runGit(input, input.shadowRoot, ["clean", "-fd"], "clean fast-forwarded workspace");
2154
+ mirrorShadowWorkspaceToVisible(input, projection.opaqueRoots, preimages);
2155
+ return { localHead: canonicalHead, canonicalHead, localChanges: false, hydrated: true, hydrationConflicts: [] };
2156
+ }
2157
+ function calculateWorkspaceLocalDiffFingerprint(rawInput) {
2158
+ const input = {
2159
+ ...rawInput,
2160
+ projects: workspaceProjectsForSync(rawInput.projects, rawInput.trigger)
2161
+ };
2162
+ if (!fs.existsSync(path.join(input.shadowRoot, ".git"))) {
2163
+ throw new Error("Cannot inspect local workspace changes before first bootstrap");
2164
+ }
2165
+ resetUncommittedShadowSnapshot(input);
2166
+ restoreUnscopedCanonicalCheckoutSubtrees(input);
2167
+ const projection = mirrorVisibleWorkspaceToShadow(input);
2168
+ runGit(
2169
+ input,
2170
+ input.shadowRoot,
2171
+ ["add", "-A", "--", ".", ...projection.opaqueRoots.map((root) => `:(exclude,literal)${root}`)],
2172
+ "stage local workspace fingerprint"
2173
+ );
2174
+ forceStageCanonicalCheckoutFiles(input, projection.entries, projection.opaqueRoots);
2175
+ const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write local workspace fingerprint tree");
2176
+ const headTree = revParse(input, "HEAD^{tree}");
2177
+ resetUncommittedShadowSnapshot(input);
2178
+ return stagedTree === headTree ? createHash("sha256").update("").digest("hex") : createHash("sha256").update(stagedTree).digest("hex");
2179
+ }
1997
2180
  function calculateWorkspaceDiffFingerprint(rawInput) {
1998
2181
  const input = {
1999
2182
  ...rawInput,
@@ -2047,6 +2230,9 @@ class WorkspaceSyncSingleFlight {
2047
2230
  fingerprintPrepared(prepare) {
2048
2231
  return this.enqueue(() => calculateWorkspaceDiffFingerprint(prepare()));
2049
2232
  }
2233
+ runExclusive(operation) {
2234
+ return this.enqueue(operation);
2235
+ }
2050
2236
  runMutation(operation) {
2051
2237
  return this.mutationGate.runMutation(operation);
2052
2238
  }
@@ -2062,11 +2248,13 @@ export {
2062
2248
  WORKSPACE_BRANCH,
2063
2249
  WORKSPACE_INTENT_REF_PREFIX,
2064
2250
  WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
2065
- WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
2251
+ WORKSPACE_PUBLICATION_REF_PREFIX,
2066
2252
  WorkspaceSyncSingleFlight,
2067
2253
  assertCanonicalCheckoutIndexMatchesWorktree,
2068
2254
  assertWorkspaceQuarantineRef,
2069
2255
  calculateWorkspaceDiffFingerprint,
2256
+ calculateWorkspaceLocalDiffFingerprint,
2257
+ convergeWorkspaceHead,
2070
2258
  encodeWorkspaceBranch,
2071
2259
  mirrorShadowWorkspaceToVisible,
2072
2260
  mirrorVisibleWorkspaceToShadow,
@@ -0,0 +1,102 @@
1
+ export declare const WORKSPACE_CHECKPOINT_QUIET_MS = 5000;
2
+ export declare const WORKSPACE_IDLE_SAFETY_SCAN_MS = 60000;
3
+ export type WorkspaceCheckpointState = "idle" | "scheduled" | "preparing" | "submitting" | "blocked";
4
+ export type WorkspaceConvergenceSnapshot = {
5
+ localHead: string | null;
6
+ desiredCanonicalHead: string | null;
7
+ dirtyGeneration: number;
8
+ publishedGeneration: number;
9
+ checkpointState: WorkspaceCheckpointState;
10
+ };
11
+ export type ExplicitWorkspaceCheckpoint<TTrigger> = {
12
+ requestId: string;
13
+ trigger: TTrigger;
14
+ };
15
+ export type WorkspaceManifestRevisionToken = {
16
+ sequence: number;
17
+ revision: string;
18
+ };
19
+ export type WorkspaceWriterLease = () => void;
20
+ /**
21
+ * Makes checkpoint admission atomic with command startup. Once a checkpoint
22
+ * lease is admitted, later writers wait; once a writer lease is admitted, a
23
+ * checkpoint must defer. Writer acquisition increments synchronously before
24
+ * its promise resolves, covering asynchronous environment setup and spawn.
25
+ */
26
+ export declare class WorkspaceFilesystemWriterGate {
27
+ private activeWriters;
28
+ private checkpointActive;
29
+ private readonly waitingWriters;
30
+ private readonly waitingCheckpoints;
31
+ acquireWriter(): Promise<WorkspaceWriterLease>;
32
+ tryAcquireCheckpoint(): WorkspaceWriterLease | null;
33
+ acquireCheckpoint(): Promise<WorkspaceWriterLease>;
34
+ private checkpointRelease;
35
+ private writerRelease;
36
+ private drain;
37
+ }
38
+ export declare function processWorkspaceFilesystemWriterGate(): WorkspaceFilesystemWriterGate;
39
+ export declare class WorkspaceManifestRevisionFence {
40
+ private sequence;
41
+ private requestedRevision;
42
+ private completedRevision;
43
+ begin(revision: string): WorkspaceManifestRevisionToken;
44
+ isCurrent(token: WorkspaceManifestRevisionToken): boolean;
45
+ complete(token: WorkspaceManifestRevisionToken): boolean;
46
+ readyRevision(): string | null;
47
+ }
48
+ export declare function workspaceCheckpointTelemetry(input: {
49
+ requestedAt: number;
50
+ prepareStartedAt: number;
51
+ synchronizeStartedAt: number;
52
+ finishedAt: number;
53
+ }): {
54
+ totalMs: number;
55
+ queueMs: number;
56
+ prepareMs: number;
57
+ synchronizeMs: number;
58
+ };
59
+ export declare function workspaceCheckpointIncidentAction(requestId: string | undefined): "defer_automatic" | "request_terminal_authority";
60
+ export declare class ExplicitWorkspaceCheckpointQueue<TTrigger> {
61
+ private scheduled;
62
+ private readonly queued;
63
+ schedule(checkpoint: ExplicitWorkspaceCheckpoint<TTrigger>, pendingRequestId?: string): boolean;
64
+ hasScheduled(): boolean;
65
+ consumeScheduled(requestId: string): void;
66
+ enqueue(checkpoint: ExplicitWorkspaceCheckpoint<TTrigger>, pendingRequestId?: string): void;
67
+ next(): ExplicitWorkspaceCheckpoint<TTrigger> | undefined;
68
+ }
69
+ type PersistedWorkspaceConvergenceState = Omit<WorkspaceConvergenceSnapshot, "checkpointState">;
70
+ export declare function readWorkspaceConvergenceState(statePath: string): PersistedWorkspaceConvergenceState;
71
+ export declare class WorkspaceConvergenceState {
72
+ private readonly statePath;
73
+ private state;
74
+ private checkpointState;
75
+ constructor(statePath: string);
76
+ snapshot(): WorkspaceConvergenceSnapshot;
77
+ setLocalHead(localHead: string | null): void;
78
+ observeCanonicalHead(canonicalHead: string | null): boolean;
79
+ markDirty(): number;
80
+ schedule(): void;
81
+ beginPreparing(): void;
82
+ beginSubmitting(): void;
83
+ defer(): void;
84
+ block(): void;
85
+ releaseBlock(): void;
86
+ observeIncidentState(blocked: boolean): void;
87
+ resetTransientCheckpointState(): void;
88
+ complete(input: {
89
+ generation: number;
90
+ canonicalHead: string | null;
91
+ published: boolean;
92
+ }): void;
93
+ private persist;
94
+ }
95
+ export declare function processWorkspaceConvergenceState(statePath: string): WorkspaceConvergenceState;
96
+ export declare function waitForWorkspaceCheckpointOnShutdown(input: {
97
+ snapshot: () => WorkspaceConvergenceSnapshot;
98
+ forceCheckpoint: () => void;
99
+ timeoutMs?: number;
100
+ pollMs?: number;
101
+ }): Promise<"settled" | "blocked" | "timed_out">;
102
+ export {};
@@ -2,7 +2,21 @@ export type WorkspaceIncidentUpdate = {
2
2
  incidentId: string | null;
3
3
  status: "remediating" | "waiting_for_worker" | "resolved" | "confirmed" | "reset" | null;
4
4
  };
5
+ /**
6
+ * WebSocket writes are ordered, but independent server handlers can decide an
7
+ * incident state under the user lock and write their messages after releasing
8
+ * it. Remember terminal incident IDs so a delayed checkpoint response cannot
9
+ * resurrect a barrier that this connection has already observed as terminal.
10
+ */
11
+ export declare class WorkspaceIncidentOrderingFence {
12
+ private readonly historyLimit;
13
+ private readonly terminalIncidentIds;
14
+ private readonly terminalIncidentOrder;
15
+ constructor(historyLimit?: number);
16
+ observe(update: WorkspaceIncidentUpdate): void;
17
+ permitsBlockedResponse(incidentId: string | null | undefined): boolean;
18
+ }
5
19
  export declare function applyWorkspaceIncidentUpdate(currentIncidentId: string | null, update: WorkspaceIncidentUpdate): string | null;
6
20
  export declare function releasePendingWorkspaceHead(previousIncidentId: string | null, currentIncidentId: string | null, terminalIncidentId?: string | null): {
7
- syncHead: string | null;
21
+ fetchAuthoritativeHead: boolean;
8
22
  };
@@ -0,0 +1,27 @@
1
+ import type { WorkspaceFilesystemWriterGate, WorkspaceWriterLease } from "./workspace-convergence";
2
+ export type WorkspaceManifestCheckout = {
3
+ projectId: string;
4
+ projectPath: string;
5
+ checkoutPathSegments: [namespace: string, project: string];
6
+ branches: string[];
7
+ };
8
+ export type WorkspaceManifestAdmission = {
9
+ firstBootstrap: boolean;
10
+ missingCheckouts: Array<{
11
+ projectId: string;
12
+ branchName: string;
13
+ }>;
14
+ migratedProjectIds: string[];
15
+ requiresVisibleLease: boolean;
16
+ };
17
+ /**
18
+ * Inspects only the paths that make manifest acknowledgement mutate existing
19
+ * visible state. Missing checkouts are reported as target-specific pending
20
+ * work and hydrated opportunistically; they must not delay unrelated targets.
21
+ */
22
+ export declare function inspectWorkspaceManifestAdmission(input: {
23
+ projectsRoot: string;
24
+ workspaceShadowRoot: string;
25
+ projects: WorkspaceManifestCheckout[];
26
+ }): WorkspaceManifestAdmission;
27
+ export declare function acquireWorkspaceManifestVisibleLease(gate: WorkspaceFilesystemWriterGate, admission: WorkspaceManifestAdmission): Promise<WorkspaceWriterLease | undefined>;