@ricsam/r5d-worker 0.0.61 → 0.0.63

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/cjs/main.cjs CHANGED
@@ -1032,6 +1032,19 @@ function checkoutVisibleBranch(input) {
1032
1032
  }
1033
1033
  runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName], { cwd: input.branchPath });
1034
1034
  }
1035
+ function reconcileRewrittenCanonicalHistory(input) {
1036
+ const remoteRef = `refs/remotes/origin/${input.branchName}`;
1037
+ if (!tryGit(["rev-parse", "--verify", "HEAD"], { cwd: input.branchPath })) return;
1038
+ if (!tryGit(["show-ref", "--verify", "--quiet", remoteRef], { cwd: input.branchPath })) return;
1039
+ if (tryGit(["merge-base", "HEAD", remoteRef], { cwd: input.branchPath })) return;
1040
+ if (hasWorktreeChanges(input.branchPath)) return;
1041
+ const localTree = runGit(["rev-parse", "HEAD^{tree}"], { cwd: input.branchPath });
1042
+ const remoteTree = runGit(["rev-parse", `${remoteRef}^{tree}`], { cwd: input.branchPath });
1043
+ if (localTree !== remoteTree) return;
1044
+ const previousHead = runGit(["rev-parse", "HEAD"], { cwd: input.branchPath });
1045
+ runGit(["update-ref", `refs/r5d/pre-canonical-checkout/${previousHead}`, previousHead], { cwd: input.branchPath });
1046
+ runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, remoteRef], { cwd: input.branchPath });
1047
+ }
1035
1048
  function migrateExistingCheckoutToRequiredRemote(input) {
1036
1049
  if (originRemoteUrl(input.branchPath) === input.remoteUrl) return;
1037
1050
  const dirtyWorkingTree = hasWorktreeChanges(input.branchPath);
@@ -1139,6 +1152,9 @@ function ensureVisibleGitCheckout(input) {
1139
1152
  if (input.requireRemoteBranch && !tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], { cwd: branchPath })) {
1140
1153
  throw new Error(`Existing checkout for ${input.branchName} does not contain its canonical remote branch`);
1141
1154
  }
1155
+ if (input.requireRemoteBranch) {
1156
+ reconcileRewrittenCanonicalHistory({ branchPath, branchName: input.branchName });
1157
+ }
1142
1158
  if (input.requiredBaseCommit) {
1143
1159
  assertCheckoutDerivedFromRequiredBase({
1144
1160
  branchPath,
@@ -2489,6 +2505,7 @@ async function startWorker(options) {
2489
2505
  requestId: options2.requestId,
2490
2506
  trigger,
2491
2507
  dirtyGeneration: snapshot2.dirtyGeneration,
2508
+ visibleBaselineHead: workspaceConvergence.visibleBaselineHead(),
2492
2509
  explicit: true,
2493
2510
  workInFlight: false,
2494
2511
  requestedAt: Date.now()
@@ -2525,6 +2542,7 @@ async function startWorker(options) {
2525
2542
  requestId,
2526
2543
  trigger,
2527
2544
  dirtyGeneration: snapshot.dirtyGeneration,
2545
+ visibleBaselineHead: workspaceConvergence.visibleBaselineHead(),
2528
2546
  explicit: options2.requestId !== void 0,
2529
2547
  workInFlight: false,
2530
2548
  requestedAt: Date.now()
@@ -2728,6 +2746,7 @@ async function startWorker(options) {
2728
2746
  const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
2729
2747
  const input = workspaceSyncInput(trigger, {
2730
2748
  ...overrides,
2749
+ ...shouldEnsureCheckouts && !trigger.canonicalCheckoutOnly ? { visibleBaselineHead: workspaceConvergence.visibleBaselineHead() } : {},
2731
2750
  ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2732
2751
  });
2733
2752
  synchronizeStartedAt = Date.now();
@@ -2845,7 +2864,8 @@ async function startWorker(options) {
2845
2864
  canonicalResolverCheckout: true,
2846
2865
  browserPortForwarding: true,
2847
2866
  globalWorkspaceSyncLeaseV1: true,
2848
- eventualWorkspaceSyncV1: true
2867
+ eventualWorkspaceSyncV1: true,
2868
+ workspaceIncidentCandidatePinV1: true
2849
2869
  },
2850
2870
  projectRoot: projectsRoot,
2851
2871
  artifactRoot,
@@ -2912,7 +2932,15 @@ async function startWorker(options) {
2912
2932
  hydrateVisible: firstBootstrap && !activeWorkspaceIncidentId
2913
2933
  });
2914
2934
  if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2915
- workspaceConvergence.setLocalHead(bootstrapped.localHead);
2935
+ if ((0, import_workspace_convergence.workspaceBootstrapEstablishesVisibleBaseline)({
2936
+ hydrated: bootstrapped.hydrated,
2937
+ firstBootstrap,
2938
+ incidentActive: Boolean(activeWorkspaceIncidentId),
2939
+ currentLocalHead: workspaceConvergence.snapshot().localHead,
2940
+ currentVisibleBaselineHead: workspaceConvergence.visibleBaselineHead()
2941
+ })) {
2942
+ workspaceConvergence.setLocalHead(bootstrapped.localHead);
2943
+ }
2916
2944
  workspaceConvergence.observeCanonicalHead(bootstrapped.canonicalHead);
2917
2945
  for (const checkout of currentCheckouts) {
2918
2946
  const manifest = manifestByProjectId.get(checkout.projectId);
@@ -2985,6 +3013,7 @@ async function startWorker(options) {
2985
3013
  quarantineRef: message.quarantineRef,
2986
3014
  confirmedLargeDiff: message.confirmedLargeDiff,
2987
3015
  confirmationReason: message.confirmationReason,
3016
+ expectedIncidentCandidateHead: message.expectedIncidentCandidateHead,
2988
3017
  resetToCanonical: message.resetToCanonical,
2989
3018
  skipVisibleMirror: message.skipVisibleMirror
2990
3019
  }
@@ -3063,6 +3092,7 @@ async function startWorker(options) {
3063
3092
  attemptId: message.attemptId,
3064
3093
  quarantineRef: message.quarantineRef,
3065
3094
  expectedCanonicalHead: message.expectedHead,
3095
+ visibleBaselineHead: publication.visibleBaselineHead,
3066
3096
  newVisibleCheckouts: [...createdCheckouts, ...pendingTargets]
3067
3097
  })
3068
3098
  );
@@ -3140,6 +3170,7 @@ async function startWorker(options) {
3140
3170
  published: true
3141
3171
  });
3142
3172
  } else if (message.status === "blocked" && workspaceIncidentOrdering.permitsBlockedResponse(message.incidentId)) {
3173
+ if (message.visibleBaselineHead) workspaceConvergence.setVisibleBaselineHead(message.visibleBaselineHead);
3143
3174
  workspaceConvergence.block();
3144
3175
  if (message.incidentId) activeWorkspaceIncidentId = message.incidentId;
3145
3176
  } else {
@@ -3191,7 +3222,11 @@ async function startWorker(options) {
3191
3222
  if (message.type === "workspace_incident_updated") {
3192
3223
  workspaceIncidentOrdering.observe(message);
3193
3224
  const previousIncidentId = activeWorkspaceIncidentId;
3194
- activeWorkspaceIncidentId = (0, import_workspace_incident_state.applyWorkspaceIncidentUpdate)(activeWorkspaceIncidentId, message);
3225
+ const nextIncidentId = (0, import_workspace_incident_state.applyWorkspaceIncidentUpdate)(activeWorkspaceIncidentId, message);
3226
+ if (message.incidentId && (previousIncidentId === message.incidentId || nextIncidentId === message.incidentId) && message.originWorkerLabel === label && message.visibleBaselineHead) {
3227
+ workspaceConvergence.setVisibleBaselineHead(message.visibleBaselineHead);
3228
+ }
3229
+ activeWorkspaceIncidentId = nextIncidentId;
3195
3230
  const releasedHead = (0, import_workspace_incident_state.releasePendingWorkspaceHead)(
3196
3231
  previousIncidentId,
3197
3232
  activeWorkspaceIncidentId,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.61",
3
+ "version": "0.0.63",
4
4
  "type": "commonjs"
5
5
  }
@@ -36,6 +36,7 @@ __export(workspace_convergence_exports, {
36
36
  processWorkspaceConvergenceState: () => processWorkspaceConvergenceState,
37
37
  readWorkspaceConvergenceState: () => readWorkspaceConvergenceState,
38
38
  waitForWorkspacePublicationOnShutdown: () => waitForWorkspacePublicationOnShutdown,
39
+ workspaceBootstrapEstablishesVisibleBaseline: () => workspaceBootstrapEstablishesVisibleBaseline,
39
40
  workspacePublicationIncidentAction: () => workspacePublicationIncidentAction,
40
41
  workspacePublicationTelemetry: () => workspacePublicationTelemetry
41
42
  });
@@ -113,6 +114,7 @@ class ExplicitWorkspacePublicationQueue {
113
114
  }
114
115
  const EMPTY_STATE = {
115
116
  localHead: null,
117
+ visibleBaselineHead: null,
116
118
  desiredCanonicalHead: null,
117
119
  dirtyGeneration: 0,
118
120
  publishedGeneration: 0
@@ -126,11 +128,12 @@ function validGeneration(value) {
126
128
  function readWorkspaceConvergenceState(statePath) {
127
129
  try {
128
130
  const parsed = JSON.parse(import_node_fs.default.readFileSync(statePath, "utf8"));
129
- if (!validHead(parsed.localHead) || !validHead(parsed.desiredCanonicalHead) || !validGeneration(parsed.dirtyGeneration) || !validGeneration(parsed.publishedGeneration) || parsed.publishedGeneration > parsed.dirtyGeneration) {
131
+ if (!validHead(parsed.localHead) || !(parsed.visibleBaselineHead === void 0 || validHead(parsed.visibleBaselineHead)) || !validHead(parsed.desiredCanonicalHead) || !validGeneration(parsed.dirtyGeneration) || !validGeneration(parsed.publishedGeneration) || parsed.publishedGeneration > parsed.dirtyGeneration) {
130
132
  return { ...EMPTY_STATE };
131
133
  }
132
134
  return {
133
135
  localHead: parsed.localHead,
136
+ visibleBaselineHead: parsed.visibleBaselineHead === void 0 ? parsed.localHead : parsed.visibleBaselineHead,
134
137
  desiredCanonicalHead: parsed.desiredCanonicalHead,
135
138
  dirtyGeneration: parsed.dirtyGeneration,
136
139
  publishedGeneration: parsed.publishedGeneration
@@ -149,11 +152,26 @@ class WorkspaceConvergenceState {
149
152
  state;
150
153
  publicationState;
151
154
  snapshot() {
152
- return { ...this.state, publicationState: this.publicationState };
155
+ return {
156
+ localHead: this.state.localHead,
157
+ desiredCanonicalHead: this.state.desiredCanonicalHead,
158
+ dirtyGeneration: this.state.dirtyGeneration,
159
+ publishedGeneration: this.state.publishedGeneration,
160
+ publicationState: this.publicationState
161
+ };
162
+ }
163
+ visibleBaselineHead() {
164
+ return this.state.visibleBaselineHead;
153
165
  }
154
166
  setLocalHead(localHead) {
155
167
  if (!validHead(localHead)) throw new Error("Invalid local workspace head");
156
168
  this.state.localHead = localHead;
169
+ this.state.visibleBaselineHead = localHead;
170
+ this.persist();
171
+ }
172
+ setVisibleBaselineHead(visibleBaselineHead) {
173
+ if (!validHead(visibleBaselineHead)) throw new Error("Invalid visible workspace baseline head");
174
+ this.state.visibleBaselineHead = visibleBaselineHead;
157
175
  this.persist();
158
176
  }
159
177
  observeCanonicalHead(canonicalHead) {
@@ -207,6 +225,7 @@ class WorkspaceConvergenceState {
207
225
  if (input.published) {
208
226
  this.state.publishedGeneration = Math.max(this.state.publishedGeneration, input.generation);
209
227
  this.state.localHead = input.canonicalHead;
228
+ this.state.visibleBaselineHead = input.canonicalHead;
210
229
  this.state.desiredCanonicalHead = input.canonicalHead;
211
230
  }
212
231
  this.publicationState = this.state.dirtyGeneration > this.state.publishedGeneration ? "scheduled" : "idle";
@@ -229,6 +248,10 @@ function processWorkspaceConvergenceState(statePath) {
229
248
  PROCESS_WORKSPACE_CONVERGENCE_STATES.set(key, created);
230
249
  return created;
231
250
  }
251
+ function workspaceBootstrapEstablishesVisibleBaseline(input) {
252
+ if (input.hydrated) return true;
253
+ return input.firstBootstrap && !input.incidentActive && input.currentLocalHead === null && input.currentVisibleBaselineHead === null;
254
+ }
232
255
  async function waitForWorkspacePublicationOnShutdown(input) {
233
256
  input.forcePublication();
234
257
  const deadline = Date.now() + (input.timeoutMs ?? 1e4);
@@ -251,6 +274,7 @@ async function waitForWorkspacePublicationOnShutdown(input) {
251
274
  processWorkspaceConvergenceState,
252
275
  readWorkspaceConvergenceState,
253
276
  waitForWorkspacePublicationOnShutdown,
277
+ workspaceBootstrapEstablishesVisibleBaseline,
254
278
  workspacePublicationIncidentAction,
255
279
  workspacePublicationTelemetry
256
280
  });
@@ -1195,6 +1195,48 @@ function mirrorVisibleWorkspaceToShadow(input, excludedCheckouts = /* @__PURE__
1195
1195
  projection.opaqueRoots = [...new Set(projection.opaqueRoots)].sort();
1196
1196
  return projection;
1197
1197
  }
1198
+ function visibleWorkspaceMatchesProjection(input, projection) {
1199
+ const preimages = projection.visiblePreimages;
1200
+ if (!preimages) return false;
1201
+ const workspacePlansRoot = localWorkspacePlansPath(input.plansRoot);
1202
+ const workspacePlansPreimage = preimages.get(workspacePlansRoot);
1203
+ if (!workspacePlansPreimage || !visibleRegularRootMatchesCopiedPreimage(
1204
+ input,
1205
+ workspacePlansRoot,
1206
+ [...workspacePlansPreimage.keys()].sort(),
1207
+ workspacePlansPreimage,
1208
+ (filePath) => filePath.endsWith(".plan.md")
1209
+ )) {
1210
+ return false;
1211
+ }
1212
+ for (const manifest of input.projects) {
1213
+ for (const branchName of manifest.branches) {
1214
+ const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, branchName);
1215
+ const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
1216
+ const projectPreimage = preimages.get(visibleRoot);
1217
+ if (!projectPreimage) return false;
1218
+ const gitlinks = [...projectPreimage].filter((entry) => entry[1].kind === "gitlink").map(([filePath, entry]) => ({ filePath, objectId: entry.objectId })).sort((left, right) => left.filePath.localeCompare(right.filePath));
1219
+ const ordinaryFiles = [...projectPreimage].filter(([, entry]) => entry.kind === "blob").map(([filePath]) => filePath).sort();
1220
+ const rootPrefix = `${relativeRoot}/`;
1221
+ const opaqueRoots = projection.opaqueRoots.filter((root) => root.startsWith(rootPrefix)).map((root) => root.slice(rootPrefix.length));
1222
+ if (!visibleProjectMatchesCopiedPreimage(input, visibleRoot, ordinaryFiles, gitlinks, projectPreimage, opaqueRoots)) {
1223
+ return false;
1224
+ }
1225
+ const plansRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
1226
+ const plansPreimage = preimages.get(plansRoot);
1227
+ if (!plansPreimage || !visibleRegularRootMatchesCopiedPreimage(
1228
+ input,
1229
+ plansRoot,
1230
+ [...plansPreimage.keys()].sort(),
1231
+ plansPreimage,
1232
+ (filePath) => filePath.endsWith(".plan.md")
1233
+ )) {
1234
+ return false;
1235
+ }
1236
+ }
1237
+ }
1238
+ return true;
1239
+ }
1198
1240
  function reconcileNewVisibleCheckouts(input) {
1199
1241
  const projection = { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map() };
1200
1242
  for (const target of input.newVisibleCheckouts ?? []) {
@@ -1584,6 +1626,54 @@ function resetUncommittedShadowSnapshot(input) {
1584
1626
  }
1585
1627
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean interrupted workspace snapshot");
1586
1628
  }
1629
+ function restoreShadowToVisibleBaseline(input, canonicalHead) {
1630
+ if (input.skipVisibleMirror || input.trigger.canonicalCheckoutOnly) return void 0;
1631
+ if (input.visibleBaselineHead === void 0) {
1632
+ if (revParse(input, "HEAD") !== canonicalHead) {
1633
+ throw new Error("visible_workspace_baseline_missing: shadow HEAD differs from canonical main");
1634
+ }
1635
+ return void 0;
1636
+ }
1637
+ const baselineHead = input.visibleBaselineHead;
1638
+ const currentHead = revParse(input, "HEAD");
1639
+ if (!baselineHead) {
1640
+ if (canonicalHead) {
1641
+ throw new Error("Visible workspace has an unborn baseline after canonical main was created");
1642
+ }
1643
+ if (!currentHead) return null;
1644
+ resetUncommittedShadowSnapshot(input);
1645
+ const symbolicHead = runGit(input, input.shadowRoot, ["symbolic-ref", "-q", "HEAD"], "resolve unborn workspace branch");
1646
+ runGit(input, input.shadowRoot, ["update-ref", "-d", symbolicHead], "restore unborn visible workspace baseline");
1647
+ runGit(input, input.shadowRoot, ["read-tree", "--empty"], "clear unborn visible workspace index");
1648
+ runGit(input, input.shadowRoot, ["clean", "-fdx"], "clean unborn visible workspace baseline");
1649
+ return null;
1650
+ }
1651
+ if (currentHead === baselineHead) return baselineHead;
1652
+ if (revParse(input, `${baselineHead}^{commit}`) !== baselineHead) {
1653
+ throw new Error(`Visible workspace baseline ${baselineHead} is not available in the shadow repository`);
1654
+ }
1655
+ resetUncommittedShadowSnapshot(input);
1656
+ runGit(input, input.shadowRoot, ["reset", "--hard", baselineHead], "restore visible workspace publication baseline");
1657
+ runGit(input, input.shadowRoot, ["clean", "-fd"], "clean restored visible workspace publication baseline");
1658
+ return baselineHead;
1659
+ }
1660
+ function restoreCleanShadowToExpectedIncidentCandidate(input) {
1661
+ const expectedHead = input.expectedIncidentCandidateHead;
1662
+ if (expectedHead === void 0) return;
1663
+ if (input.trigger.type !== "remediation" || input.trigger.canonicalCheckoutOnly || input.trigger.processRunId !== void 0 || input.trigger.toolCallId !== void 0 || input.skipVisibleMirror !== true || input.confirmedLargeDiff === true || input.resetToCanonical === true) {
1664
+ throw new Error("An expected incident candidate may only be used for stable remediation preparation");
1665
+ }
1666
+ const status = gitStatus(input);
1667
+ if (status) {
1668
+ throw new Error("incident_candidate_shadow_dirty: refusing to replace a dirty remediation shadow checkout");
1669
+ }
1670
+ if (revParse(input, `${expectedHead}^{commit}`) !== expectedHead) {
1671
+ throw new Error(`incident_candidate_unavailable: elected workspace candidate ${expectedHead} is not available`);
1672
+ }
1673
+ if (revParse(input, "HEAD") !== expectedHead) {
1674
+ runGit(input, input.shadowRoot, ["reset", "--hard", expectedHead], "restore elected workspace incident candidate");
1675
+ }
1676
+ }
1587
1677
  function activeWorkspaceRebase(input) {
1588
1678
  for (const name of ["rebase-merge", "rebase-apply"]) {
1589
1679
  const resolved = runGitResult(input, input.shadowRoot, ["rev-parse", "--git-path", name]);
@@ -1776,7 +1866,7 @@ function uploadWorkspaceCandidate(input, candidateHead) {
1776
1866
  throw new Error("A server-issued workspace candidate ref is required before uploading a canonical workspace candidate");
1777
1867
  }
1778
1868
  assertWorkspaceQuarantineRef(quarantineRef);
1779
- return runGitResult(input, input.shadowRoot, ["push", "origin", `HEAD:${quarantineRef}`]);
1869
+ return runGitResult(input, input.shadowRoot, ["push", "origin", `${candidateHead}:${quarantineRef}`]);
1780
1870
  }
1781
1871
  function resetShadowToRemote(input, opaqueWorkspaceRoots = []) {
1782
1872
  runGit(input, input.shadowRoot, ["fetch", "origin", "--prune"], "fetch canonical workspace before reset");
@@ -1785,7 +1875,9 @@ function resetShadowToRemote(input, opaqueWorkspaceRoots = []) {
1785
1875
  runGitResult(input, input.shadowRoot, ["rebase", "--abort"]);
1786
1876
  runGit(input, input.shadowRoot, ["reset", "--hard", `origin/${WORKSPACE_BRANCH}`], "reset workspace to canonical state");
1787
1877
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean reset workspace");
1788
- if (!input.trigger.canonicalCheckoutOnly) mirrorShadowWorkspaceToVisible(input, opaqueWorkspaceRoots);
1878
+ if (!input.trigger.canonicalCheckoutOnly && !input.skipVisibleMirror) {
1879
+ mirrorShadowWorkspaceToVisible(input, opaqueWorkspaceRoots);
1880
+ }
1789
1881
  return remoteHead;
1790
1882
  }
1791
1883
  function baseResult(input, startingHead) {
@@ -1879,6 +1971,39 @@ function preserveWorkspaceConflictCandidate(input, observed, options) {
1879
1971
  };
1880
1972
  }
1881
1973
  }
1974
+ function preserveInProgressWorkspaceRebaseConflict(input, observed, options) {
1975
+ try {
1976
+ assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, options.candidateHead);
1977
+ const upload = uploadWorkspaceCandidate(input, options.candidateHead);
1978
+ if (upload.exitCode !== 0) {
1979
+ throw new Error(upload.stderr || upload.stdout || "workspace rebase conflict candidate upload failed");
1980
+ }
1981
+ const conflictPaths = [...new Set(options.conflictPaths)].sort();
1982
+ return {
1983
+ ...observed,
1984
+ outcome: "conflict_blocked",
1985
+ expectedHead: options.expectedHead,
1986
+ candidateHead: options.candidateHead,
1987
+ quarantineRef: input.quarantineRef,
1988
+ rebaseCount: options.rebaseCount,
1989
+ affectedProjects: affectedProjects(conflictPaths),
1990
+ affectedPaths: reportedPaths(conflictPaths),
1991
+ error: options.error,
1992
+ gitStatus: gitStatus(input)
1993
+ };
1994
+ } catch (error) {
1995
+ runGitResult(input, input.shadowRoot, ["rebase", "--abort"]);
1996
+ return {
1997
+ ...observed,
1998
+ outcome: "failed",
1999
+ rebaseCount: options.rebaseCount,
2000
+ affectedProjects: affectedProjects(options.conflictPaths),
2001
+ affectedPaths: reportedPaths(options.conflictPaths),
2002
+ error: `Workspace synchronization found a rebase conflict but could not preserve its durable candidate: ${error instanceof Error ? error.message : String(error)}`,
2003
+ gitStatus: gitStatus(input)
2004
+ };
2005
+ }
2006
+ }
1882
2007
  function preserveLargeDiffCandidate(input, observed, error) {
1883
2008
  try {
1884
2009
  if (stagedPaths(input).length > 0) {
@@ -1929,6 +2054,10 @@ async function synchronizeWorkspace(rawInput) {
1929
2054
  error: `canonical_head_advanced: expected ${input.expectedCanonicalHead ?? "unborn"}, fetched ${remoteHeadAtStart ?? "unborn"}`
1930
2055
  };
1931
2056
  }
2057
+ const inProgressRemediation = remediationRebaseInProgressResult(input, result);
2058
+ if (inProgressRemediation) return inProgressRemediation;
2059
+ restoreCleanShadowToExpectedIncidentCandidate(input);
2060
+ const visibleBaselineHead = restoreShadowToVisibleBaseline(input, remoteHeadAtStart);
1932
2061
  const inboundConflictsAtStart = input.skipVisibleMirror && !isCanonicalRemediationSync && remoteHeadAtStart ? visibleHydrationConflictPaths(input, hydrationPreimages, `origin/${WORKSPACE_BRANCH}`) : [];
1933
2062
  if (input.resetToCanonical) {
1934
2063
  if (input.trigger.canonicalCheckoutOnly) {
@@ -1943,8 +2072,6 @@ async function synchronizeWorkspace(rawInput) {
1943
2072
  gitStatus: gitStatus(input)
1944
2073
  };
1945
2074
  }
1946
- const inProgressRemediation = remediationRebaseInProgressResult(input, result);
1947
- if (inProgressRemediation) return inProgressRemediation;
1948
2075
  if (!input.skipVisibleMirror) resetUncommittedShadowSnapshot(input);
1949
2076
  restoreUnscopedCanonicalCheckoutSubtrees(input);
1950
2077
  if (!input.skipVisibleMirror) {
@@ -1954,6 +2081,10 @@ async function synchronizeWorkspace(rawInput) {
1954
2081
  mergeGitlinkProjection(mirroredGitlinkProjection, reconcileNewVisibleCheckouts(input));
1955
2082
  mirroredGitlinkProjection.opaqueRoots = [...new Set(mirroredGitlinkProjection.opaqueRoots)].sort();
1956
2083
  hydrationPreimages = mirroredGitlinkProjection.visiblePreimages ?? hydrationPreimages;
2084
+ if (input.trigger.type === "remediation" && !visibleWorkspaceMatchesProjection(input, mirroredGitlinkProjection)) {
2085
+ resetUncommittedShadowSnapshot(input);
2086
+ throw new Error("visible_workspace_unstable: visible files changed while preparing the remediation snapshot");
2087
+ }
1957
2088
  }
1958
2089
  const stagePathspecs = mirroredGitlinkProjection.opaqueRoots.flatMap((root) => [`:(exclude,literal)${root}`]);
1959
2090
  runGit(input, input.shadowRoot, ["add", "-A", "--", ".", ...stagePathspecs], "stage workspace changes");
@@ -1962,7 +2093,8 @@ async function synchronizeWorkspace(rawInput) {
1962
2093
  if (redundantInboundCandidate) return redundantInboundCandidate;
1963
2094
  const sampledCanonicalCheckoutTreeHash = sampleCanonicalCheckoutTree(input);
1964
2095
  const stagedWorkingPaths = stagedPaths(input);
1965
- const baseRevision = candidateBaseRevision(input);
2096
+ const preservesDivergentVisibleSnapshot = input.confirmedLargeDiff === true || input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
2097
+ const baseRevision = preservesDivergentVisibleSnapshot ? remoteHeadAtStart : visibleBaselineHead !== void 0 ? visibleBaselineHead : candidateBaseRevision(input);
1966
2098
  const paths = stagedPaths(input, baseRevision);
1967
2099
  const diffSizeBytes = await stagedDiffSizeBytes(input, baseRevision);
1968
2100
  const statusBeforeCommit = reportedStatus(gitStatus(input));
@@ -1975,16 +2107,12 @@ async function synchronizeWorkspace(rawInput) {
1975
2107
  ...sampledCanonicalCheckoutTreeHash ? { sampledCanonicalCheckoutTreeHash } : {}
1976
2108
  };
1977
2109
  const destructiveReductions = destructiveCheckoutReductions(input, baseRevision);
2110
+ let blockedDiffError;
1978
2111
  if (destructiveReductions.length > 0 && !input.confirmedLargeDiff) {
1979
2112
  const summary = destructiveReductions.map(({ root, trackedBefore, trackedAfter }) => `${root} (${trackedBefore} -> ${trackedAfter} tracked files)`).join(", ");
1980
- return preserveLargeDiffCandidate(input, observed, `Workspace safety guard blocked a destructive checkout reduction: ${summary}`);
1981
- }
1982
- if (paths.length > 0 && diffSizeBytes > MAX_WORKSPACE_SYNC_DIFF_BYTES && !input.confirmedLargeDiff && !input.trigger.canonicalCheckoutOnly) {
1983
- return preserveLargeDiffCandidate(
1984
- input,
1985
- observed,
1986
- `Workspace safety guard blocked a ${diffSizeBytes}-byte diff above the ${MAX_WORKSPACE_SYNC_DIFF_BYTES}-byte automatic publication limit.`
1987
- );
2113
+ blockedDiffError = `Workspace safety guard blocked a destructive checkout reduction: ${summary}`;
2114
+ } else if (paths.length > 0 && diffSizeBytes > MAX_WORKSPACE_SYNC_DIFF_BYTES && !input.confirmedLargeDiff && !input.trigger.canonicalCheckoutOnly) {
2115
+ blockedDiffError = `Workspace safety guard blocked a ${diffSizeBytes}-byte diff above the ${MAX_WORKSPACE_SYNC_DIFF_BYTES}-byte automatic publication limit.`;
1988
2116
  }
1989
2117
  if (stagedWorkingPaths.length > 0) {
1990
2118
  runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit workspace changes");
@@ -1992,8 +2120,11 @@ async function synchronizeWorkspace(rawInput) {
1992
2120
  let candidateHead = revParse(input, "HEAD");
1993
2121
  const candidateHeadBeforeRebase = candidateHead;
1994
2122
  if (candidateHead) assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
1995
- const hasRemoteMain = Boolean(revParse(input, `origin/${WORKSPACE_BRANCH}`));
1996
- const hasUnpushedCommit = candidateHead ? !hasRemoteMain || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", candidateHead, `origin/${WORKSPACE_BRANCH}`]) : false;
2123
+ const remoteHeadBeforeRebase = revParse(input, `origin/${WORKSPACE_BRANCH}`);
2124
+ const hasRemoteMain = Boolean(remoteHeadBeforeRebase);
2125
+ const hasCapturedVisibleDelta = visibleBaselineHead === void 0 || paths.length > 0;
2126
+ const mustDropVisibleBaselinePrefix = !preservesDivergentVisibleSnapshot && visibleBaselineHead !== void 0 && visibleBaselineHead !== remoteHeadBeforeRebase && stagedWorkingPaths.length > 0;
2127
+ const hasUnpushedCommit = candidateHead && hasCapturedVisibleDelta ? mustDropVisibleBaselinePrefix || !hasRemoteMain || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", candidateHead, `origin/${WORKSPACE_BRANCH}`]) : false;
1997
2128
  if (hasUnpushedCommit && inboundConflictsAtStart.length > 0) {
1998
2129
  const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...inboundConflictsAtStart])].sort();
1999
2130
  return preserveWorkspaceConflictCandidate(input, observed, {
@@ -2067,13 +2198,36 @@ async function synchronizeWorkspace(rawInput) {
2067
2198
  };
2068
2199
  }
2069
2200
  let rebaseCount = 0;
2070
- const expectedHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
2071
- if (candidateHead && expectedHead && !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", expectedHead, candidateHead])) {
2072
- const rebase = runGitResult(input, input.shadowRoot, ["rebase", `origin/${WORKSPACE_BRANCH}`]);
2201
+ const expectedHead = remoteHeadBeforeRebase;
2202
+ if (mustDropVisibleBaselinePrefix && !expectedHead) {
2203
+ return {
2204
+ ...observed,
2205
+ outcome: "failed",
2206
+ candidateHead: candidateHead ?? void 0,
2207
+ error: "Cannot normalize a visible workspace tail onto an unborn canonical workspace",
2208
+ gitStatus: gitStatus(input)
2209
+ };
2210
+ }
2211
+ if (candidateHead && expectedHead && (mustDropVisibleBaselinePrefix || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", expectedHead, candidateHead]))) {
2212
+ const rebase = runGitResult(
2213
+ input,
2214
+ input.shadowRoot,
2215
+ mustDropVisibleBaselinePrefix && visibleBaselineHead ? ["rebase", "--onto", `origin/${WORKSPACE_BRANCH}`, visibleBaselineHead] : ["rebase", `origin/${WORKSPACE_BRANCH}`]
2216
+ );
2073
2217
  rebaseCount += 1;
2074
2218
  if (rebase.exitCode !== 0) {
2075
2219
  const conflicted = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]);
2076
2220
  const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort();
2221
+ if (mustDropVisibleBaselinePrefix) {
2222
+ const originalCandidateHead = revParse(input, "ORIG_HEAD") ?? candidateHead;
2223
+ return preserveInProgressWorkspaceRebaseConflict(input, observed, {
2224
+ candidateHead: originalCandidateHead,
2225
+ expectedHead,
2226
+ rebaseCount,
2227
+ conflictPaths,
2228
+ error: `Workspace tail conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation in the in-progress rebase.`
2229
+ });
2230
+ }
2077
2231
  runGit(input, input.shadowRoot, ["rebase", "--abort"], "preserve workspace candidate after rebase conflict");
2078
2232
  return preserveWorkspaceConflictCandidate(input, observed, {
2079
2233
  baseRevision: candidateHead,
@@ -2119,6 +2273,9 @@ async function synchronizeWorkspace(rawInput) {
2119
2273
  error: "Workspace changed while its candidate was reconciling; visible changes and the old-base candidate were preserved."
2120
2274
  });
2121
2275
  }
2276
+ if (blockedDiffError) {
2277
+ return preserveLargeDiffCandidate(input, { ...observed, rebaseCount }, blockedDiffError);
2278
+ }
2122
2279
  const upload = uploadWorkspaceCandidate(input, candidateHead);
2123
2280
  if (upload.exitCode !== 0) {
2124
2281
  return {
package/dist/mjs/main.mjs CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  WorkspaceManifestRevisionFence,
23
23
  processWorkspaceConvergenceState,
24
24
  waitForWorkspacePublicationOnShutdown,
25
+ workspaceBootstrapEstablishesVisibleBaseline,
25
26
  workspacePublicationIncidentAction,
26
27
  workspacePublicationTelemetry
27
28
  } from "./workspace-convergence.mjs";
@@ -1002,6 +1003,19 @@ function checkoutVisibleBranch(input) {
1002
1003
  }
1003
1004
  runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName], { cwd: input.branchPath });
1004
1005
  }
1006
+ function reconcileRewrittenCanonicalHistory(input) {
1007
+ const remoteRef = `refs/remotes/origin/${input.branchName}`;
1008
+ if (!tryGit(["rev-parse", "--verify", "HEAD"], { cwd: input.branchPath })) return;
1009
+ if (!tryGit(["show-ref", "--verify", "--quiet", remoteRef], { cwd: input.branchPath })) return;
1010
+ if (tryGit(["merge-base", "HEAD", remoteRef], { cwd: input.branchPath })) return;
1011
+ if (hasWorktreeChanges(input.branchPath)) return;
1012
+ const localTree = runGit(["rev-parse", "HEAD^{tree}"], { cwd: input.branchPath });
1013
+ const remoteTree = runGit(["rev-parse", `${remoteRef}^{tree}`], { cwd: input.branchPath });
1014
+ if (localTree !== remoteTree) return;
1015
+ const previousHead = runGit(["rev-parse", "HEAD"], { cwd: input.branchPath });
1016
+ runGit(["update-ref", `refs/r5d/pre-canonical-checkout/${previousHead}`, previousHead], { cwd: input.branchPath });
1017
+ runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, remoteRef], { cwd: input.branchPath });
1018
+ }
1005
1019
  function migrateExistingCheckoutToRequiredRemote(input) {
1006
1020
  if (originRemoteUrl(input.branchPath) === input.remoteUrl) return;
1007
1021
  const dirtyWorkingTree = hasWorktreeChanges(input.branchPath);
@@ -1109,6 +1123,9 @@ function ensureVisibleGitCheckout(input) {
1109
1123
  if (input.requireRemoteBranch && !tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], { cwd: branchPath })) {
1110
1124
  throw new Error(`Existing checkout for ${input.branchName} does not contain its canonical remote branch`);
1111
1125
  }
1126
+ if (input.requireRemoteBranch) {
1127
+ reconcileRewrittenCanonicalHistory({ branchPath, branchName: input.branchName });
1128
+ }
1112
1129
  if (input.requiredBaseCommit) {
1113
1130
  assertCheckoutDerivedFromRequiredBase({
1114
1131
  branchPath,
@@ -2459,6 +2476,7 @@ async function startWorker(options) {
2459
2476
  requestId: options2.requestId,
2460
2477
  trigger,
2461
2478
  dirtyGeneration: snapshot2.dirtyGeneration,
2479
+ visibleBaselineHead: workspaceConvergence.visibleBaselineHead(),
2462
2480
  explicit: true,
2463
2481
  workInFlight: false,
2464
2482
  requestedAt: Date.now()
@@ -2495,6 +2513,7 @@ async function startWorker(options) {
2495
2513
  requestId,
2496
2514
  trigger,
2497
2515
  dirtyGeneration: snapshot.dirtyGeneration,
2516
+ visibleBaselineHead: workspaceConvergence.visibleBaselineHead(),
2498
2517
  explicit: options2.requestId !== void 0,
2499
2518
  workInFlight: false,
2500
2519
  requestedAt: Date.now()
@@ -2698,6 +2717,7 @@ async function startWorker(options) {
2698
2717
  const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
2699
2718
  const input = workspaceSyncInput(trigger, {
2700
2719
  ...overrides,
2720
+ ...shouldEnsureCheckouts && !trigger.canonicalCheckoutOnly ? { visibleBaselineHead: workspaceConvergence.visibleBaselineHead() } : {},
2701
2721
  ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2702
2722
  });
2703
2723
  synchronizeStartedAt = Date.now();
@@ -2815,7 +2835,8 @@ async function startWorker(options) {
2815
2835
  canonicalResolverCheckout: true,
2816
2836
  browserPortForwarding: true,
2817
2837
  globalWorkspaceSyncLeaseV1: true,
2818
- eventualWorkspaceSyncV1: true
2838
+ eventualWorkspaceSyncV1: true,
2839
+ workspaceIncidentCandidatePinV1: true
2819
2840
  },
2820
2841
  projectRoot: projectsRoot,
2821
2842
  artifactRoot,
@@ -2882,7 +2903,15 @@ async function startWorker(options) {
2882
2903
  hydrateVisible: firstBootstrap && !activeWorkspaceIncidentId
2883
2904
  });
2884
2905
  if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2885
- workspaceConvergence.setLocalHead(bootstrapped.localHead);
2906
+ if (workspaceBootstrapEstablishesVisibleBaseline({
2907
+ hydrated: bootstrapped.hydrated,
2908
+ firstBootstrap,
2909
+ incidentActive: Boolean(activeWorkspaceIncidentId),
2910
+ currentLocalHead: workspaceConvergence.snapshot().localHead,
2911
+ currentVisibleBaselineHead: workspaceConvergence.visibleBaselineHead()
2912
+ })) {
2913
+ workspaceConvergence.setLocalHead(bootstrapped.localHead);
2914
+ }
2886
2915
  workspaceConvergence.observeCanonicalHead(bootstrapped.canonicalHead);
2887
2916
  for (const checkout of currentCheckouts) {
2888
2917
  const manifest = manifestByProjectId.get(checkout.projectId);
@@ -2955,6 +2984,7 @@ async function startWorker(options) {
2955
2984
  quarantineRef: message.quarantineRef,
2956
2985
  confirmedLargeDiff: message.confirmedLargeDiff,
2957
2986
  confirmationReason: message.confirmationReason,
2987
+ expectedIncidentCandidateHead: message.expectedIncidentCandidateHead,
2958
2988
  resetToCanonical: message.resetToCanonical,
2959
2989
  skipVisibleMirror: message.skipVisibleMirror
2960
2990
  }
@@ -3033,6 +3063,7 @@ async function startWorker(options) {
3033
3063
  attemptId: message.attemptId,
3034
3064
  quarantineRef: message.quarantineRef,
3035
3065
  expectedCanonicalHead: message.expectedHead,
3066
+ visibleBaselineHead: publication.visibleBaselineHead,
3036
3067
  newVisibleCheckouts: [...createdCheckouts, ...pendingTargets]
3037
3068
  })
3038
3069
  );
@@ -3110,6 +3141,7 @@ async function startWorker(options) {
3110
3141
  published: true
3111
3142
  });
3112
3143
  } else if (message.status === "blocked" && workspaceIncidentOrdering.permitsBlockedResponse(message.incidentId)) {
3144
+ if (message.visibleBaselineHead) workspaceConvergence.setVisibleBaselineHead(message.visibleBaselineHead);
3113
3145
  workspaceConvergence.block();
3114
3146
  if (message.incidentId) activeWorkspaceIncidentId = message.incidentId;
3115
3147
  } else {
@@ -3161,7 +3193,11 @@ async function startWorker(options) {
3161
3193
  if (message.type === "workspace_incident_updated") {
3162
3194
  workspaceIncidentOrdering.observe(message);
3163
3195
  const previousIncidentId = activeWorkspaceIncidentId;
3164
- activeWorkspaceIncidentId = applyWorkspaceIncidentUpdate(activeWorkspaceIncidentId, message);
3196
+ const nextIncidentId = applyWorkspaceIncidentUpdate(activeWorkspaceIncidentId, message);
3197
+ if (message.incidentId && (previousIncidentId === message.incidentId || nextIncidentId === message.incidentId) && message.originWorkerLabel === label && message.visibleBaselineHead) {
3198
+ workspaceConvergence.setVisibleBaselineHead(message.visibleBaselineHead);
3199
+ }
3200
+ activeWorkspaceIncidentId = nextIncidentId;
3165
3201
  const releasedHead = releasePendingWorkspaceHead(
3166
3202
  previousIncidentId,
3167
3203
  activeWorkspaceIncidentId,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.61",
3
+ "version": "0.0.63",
4
4
  "type": "module"
5
5
  }
@@ -71,6 +71,7 @@ class ExplicitWorkspacePublicationQueue {
71
71
  }
72
72
  const EMPTY_STATE = {
73
73
  localHead: null,
74
+ visibleBaselineHead: null,
74
75
  desiredCanonicalHead: null,
75
76
  dirtyGeneration: 0,
76
77
  publishedGeneration: 0
@@ -84,11 +85,12 @@ function validGeneration(value) {
84
85
  function readWorkspaceConvergenceState(statePath) {
85
86
  try {
86
87
  const parsed = JSON.parse(fs.readFileSync(statePath, "utf8"));
87
- if (!validHead(parsed.localHead) || !validHead(parsed.desiredCanonicalHead) || !validGeneration(parsed.dirtyGeneration) || !validGeneration(parsed.publishedGeneration) || parsed.publishedGeneration > parsed.dirtyGeneration) {
88
+ if (!validHead(parsed.localHead) || !(parsed.visibleBaselineHead === void 0 || validHead(parsed.visibleBaselineHead)) || !validHead(parsed.desiredCanonicalHead) || !validGeneration(parsed.dirtyGeneration) || !validGeneration(parsed.publishedGeneration) || parsed.publishedGeneration > parsed.dirtyGeneration) {
88
89
  return { ...EMPTY_STATE };
89
90
  }
90
91
  return {
91
92
  localHead: parsed.localHead,
93
+ visibleBaselineHead: parsed.visibleBaselineHead === void 0 ? parsed.localHead : parsed.visibleBaselineHead,
92
94
  desiredCanonicalHead: parsed.desiredCanonicalHead,
93
95
  dirtyGeneration: parsed.dirtyGeneration,
94
96
  publishedGeneration: parsed.publishedGeneration
@@ -107,11 +109,26 @@ class WorkspaceConvergenceState {
107
109
  state;
108
110
  publicationState;
109
111
  snapshot() {
110
- return { ...this.state, publicationState: this.publicationState };
112
+ return {
113
+ localHead: this.state.localHead,
114
+ desiredCanonicalHead: this.state.desiredCanonicalHead,
115
+ dirtyGeneration: this.state.dirtyGeneration,
116
+ publishedGeneration: this.state.publishedGeneration,
117
+ publicationState: this.publicationState
118
+ };
119
+ }
120
+ visibleBaselineHead() {
121
+ return this.state.visibleBaselineHead;
111
122
  }
112
123
  setLocalHead(localHead) {
113
124
  if (!validHead(localHead)) throw new Error("Invalid local workspace head");
114
125
  this.state.localHead = localHead;
126
+ this.state.visibleBaselineHead = localHead;
127
+ this.persist();
128
+ }
129
+ setVisibleBaselineHead(visibleBaselineHead) {
130
+ if (!validHead(visibleBaselineHead)) throw new Error("Invalid visible workspace baseline head");
131
+ this.state.visibleBaselineHead = visibleBaselineHead;
115
132
  this.persist();
116
133
  }
117
134
  observeCanonicalHead(canonicalHead) {
@@ -165,6 +182,7 @@ class WorkspaceConvergenceState {
165
182
  if (input.published) {
166
183
  this.state.publishedGeneration = Math.max(this.state.publishedGeneration, input.generation);
167
184
  this.state.localHead = input.canonicalHead;
185
+ this.state.visibleBaselineHead = input.canonicalHead;
168
186
  this.state.desiredCanonicalHead = input.canonicalHead;
169
187
  }
170
188
  this.publicationState = this.state.dirtyGeneration > this.state.publishedGeneration ? "scheduled" : "idle";
@@ -187,6 +205,10 @@ function processWorkspaceConvergenceState(statePath) {
187
205
  PROCESS_WORKSPACE_CONVERGENCE_STATES.set(key, created);
188
206
  return created;
189
207
  }
208
+ function workspaceBootstrapEstablishesVisibleBaseline(input) {
209
+ if (input.hydrated) return true;
210
+ return input.firstBootstrap && !input.incidentActive && input.currentLocalHead === null && input.currentVisibleBaselineHead === null;
211
+ }
190
212
  async function waitForWorkspacePublicationOnShutdown(input) {
191
213
  input.forcePublication();
192
214
  const deadline = Date.now() + (input.timeoutMs ?? 1e4);
@@ -208,6 +230,7 @@ export {
208
230
  processWorkspaceConvergenceState,
209
231
  readWorkspaceConvergenceState,
210
232
  waitForWorkspacePublicationOnShutdown,
233
+ workspaceBootstrapEstablishesVisibleBaseline,
211
234
  workspacePublicationIncidentAction,
212
235
  workspacePublicationTelemetry
213
236
  };
@@ -1144,6 +1144,48 @@ function mirrorVisibleWorkspaceToShadow(input, excludedCheckouts = /* @__PURE__
1144
1144
  projection.opaqueRoots = [...new Set(projection.opaqueRoots)].sort();
1145
1145
  return projection;
1146
1146
  }
1147
+ function visibleWorkspaceMatchesProjection(input, projection) {
1148
+ const preimages = projection.visiblePreimages;
1149
+ if (!preimages) return false;
1150
+ const workspacePlansRoot = localWorkspacePlansPath(input.plansRoot);
1151
+ const workspacePlansPreimage = preimages.get(workspacePlansRoot);
1152
+ if (!workspacePlansPreimage || !visibleRegularRootMatchesCopiedPreimage(
1153
+ input,
1154
+ workspacePlansRoot,
1155
+ [...workspacePlansPreimage.keys()].sort(),
1156
+ workspacePlansPreimage,
1157
+ (filePath) => filePath.endsWith(".plan.md")
1158
+ )) {
1159
+ return false;
1160
+ }
1161
+ for (const manifest of input.projects) {
1162
+ for (const branchName of manifest.branches) {
1163
+ const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, branchName);
1164
+ const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
1165
+ const projectPreimage = preimages.get(visibleRoot);
1166
+ if (!projectPreimage) return false;
1167
+ const gitlinks = [...projectPreimage].filter((entry) => entry[1].kind === "gitlink").map(([filePath, entry]) => ({ filePath, objectId: entry.objectId })).sort((left, right) => left.filePath.localeCompare(right.filePath));
1168
+ const ordinaryFiles = [...projectPreimage].filter(([, entry]) => entry.kind === "blob").map(([filePath]) => filePath).sort();
1169
+ const rootPrefix = `${relativeRoot}/`;
1170
+ const opaqueRoots = projection.opaqueRoots.filter((root) => root.startsWith(rootPrefix)).map((root) => root.slice(rootPrefix.length));
1171
+ if (!visibleProjectMatchesCopiedPreimage(input, visibleRoot, ordinaryFiles, gitlinks, projectPreimage, opaqueRoots)) {
1172
+ return false;
1173
+ }
1174
+ const plansRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
1175
+ const plansPreimage = preimages.get(plansRoot);
1176
+ if (!plansPreimage || !visibleRegularRootMatchesCopiedPreimage(
1177
+ input,
1178
+ plansRoot,
1179
+ [...plansPreimage.keys()].sort(),
1180
+ plansPreimage,
1181
+ (filePath) => filePath.endsWith(".plan.md")
1182
+ )) {
1183
+ return false;
1184
+ }
1185
+ }
1186
+ }
1187
+ return true;
1188
+ }
1147
1189
  function reconcileNewVisibleCheckouts(input) {
1148
1190
  const projection = { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map() };
1149
1191
  for (const target of input.newVisibleCheckouts ?? []) {
@@ -1533,6 +1575,54 @@ function resetUncommittedShadowSnapshot(input) {
1533
1575
  }
1534
1576
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean interrupted workspace snapshot");
1535
1577
  }
1578
+ function restoreShadowToVisibleBaseline(input, canonicalHead) {
1579
+ if (input.skipVisibleMirror || input.trigger.canonicalCheckoutOnly) return void 0;
1580
+ if (input.visibleBaselineHead === void 0) {
1581
+ if (revParse(input, "HEAD") !== canonicalHead) {
1582
+ throw new Error("visible_workspace_baseline_missing: shadow HEAD differs from canonical main");
1583
+ }
1584
+ return void 0;
1585
+ }
1586
+ const baselineHead = input.visibleBaselineHead;
1587
+ const currentHead = revParse(input, "HEAD");
1588
+ if (!baselineHead) {
1589
+ if (canonicalHead) {
1590
+ throw new Error("Visible workspace has an unborn baseline after canonical main was created");
1591
+ }
1592
+ if (!currentHead) return null;
1593
+ resetUncommittedShadowSnapshot(input);
1594
+ const symbolicHead = runGit(input, input.shadowRoot, ["symbolic-ref", "-q", "HEAD"], "resolve unborn workspace branch");
1595
+ runGit(input, input.shadowRoot, ["update-ref", "-d", symbolicHead], "restore unborn visible workspace baseline");
1596
+ runGit(input, input.shadowRoot, ["read-tree", "--empty"], "clear unborn visible workspace index");
1597
+ runGit(input, input.shadowRoot, ["clean", "-fdx"], "clean unborn visible workspace baseline");
1598
+ return null;
1599
+ }
1600
+ if (currentHead === baselineHead) return baselineHead;
1601
+ if (revParse(input, `${baselineHead}^{commit}`) !== baselineHead) {
1602
+ throw new Error(`Visible workspace baseline ${baselineHead} is not available in the shadow repository`);
1603
+ }
1604
+ resetUncommittedShadowSnapshot(input);
1605
+ runGit(input, input.shadowRoot, ["reset", "--hard", baselineHead], "restore visible workspace publication baseline");
1606
+ runGit(input, input.shadowRoot, ["clean", "-fd"], "clean restored visible workspace publication baseline");
1607
+ return baselineHead;
1608
+ }
1609
+ function restoreCleanShadowToExpectedIncidentCandidate(input) {
1610
+ const expectedHead = input.expectedIncidentCandidateHead;
1611
+ if (expectedHead === void 0) return;
1612
+ if (input.trigger.type !== "remediation" || input.trigger.canonicalCheckoutOnly || input.trigger.processRunId !== void 0 || input.trigger.toolCallId !== void 0 || input.skipVisibleMirror !== true || input.confirmedLargeDiff === true || input.resetToCanonical === true) {
1613
+ throw new Error("An expected incident candidate may only be used for stable remediation preparation");
1614
+ }
1615
+ const status = gitStatus(input);
1616
+ if (status) {
1617
+ throw new Error("incident_candidate_shadow_dirty: refusing to replace a dirty remediation shadow checkout");
1618
+ }
1619
+ if (revParse(input, `${expectedHead}^{commit}`) !== expectedHead) {
1620
+ throw new Error(`incident_candidate_unavailable: elected workspace candidate ${expectedHead} is not available`);
1621
+ }
1622
+ if (revParse(input, "HEAD") !== expectedHead) {
1623
+ runGit(input, input.shadowRoot, ["reset", "--hard", expectedHead], "restore elected workspace incident candidate");
1624
+ }
1625
+ }
1536
1626
  function activeWorkspaceRebase(input) {
1537
1627
  for (const name of ["rebase-merge", "rebase-apply"]) {
1538
1628
  const resolved = runGitResult(input, input.shadowRoot, ["rev-parse", "--git-path", name]);
@@ -1725,7 +1815,7 @@ function uploadWorkspaceCandidate(input, candidateHead) {
1725
1815
  throw new Error("A server-issued workspace candidate ref is required before uploading a canonical workspace candidate");
1726
1816
  }
1727
1817
  assertWorkspaceQuarantineRef(quarantineRef);
1728
- return runGitResult(input, input.shadowRoot, ["push", "origin", `HEAD:${quarantineRef}`]);
1818
+ return runGitResult(input, input.shadowRoot, ["push", "origin", `${candidateHead}:${quarantineRef}`]);
1729
1819
  }
1730
1820
  function resetShadowToRemote(input, opaqueWorkspaceRoots = []) {
1731
1821
  runGit(input, input.shadowRoot, ["fetch", "origin", "--prune"], "fetch canonical workspace before reset");
@@ -1734,7 +1824,9 @@ function resetShadowToRemote(input, opaqueWorkspaceRoots = []) {
1734
1824
  runGitResult(input, input.shadowRoot, ["rebase", "--abort"]);
1735
1825
  runGit(input, input.shadowRoot, ["reset", "--hard", `origin/${WORKSPACE_BRANCH}`], "reset workspace to canonical state");
1736
1826
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean reset workspace");
1737
- if (!input.trigger.canonicalCheckoutOnly) mirrorShadowWorkspaceToVisible(input, opaqueWorkspaceRoots);
1827
+ if (!input.trigger.canonicalCheckoutOnly && !input.skipVisibleMirror) {
1828
+ mirrorShadowWorkspaceToVisible(input, opaqueWorkspaceRoots);
1829
+ }
1738
1830
  return remoteHead;
1739
1831
  }
1740
1832
  function baseResult(input, startingHead) {
@@ -1828,6 +1920,39 @@ function preserveWorkspaceConflictCandidate(input, observed, options) {
1828
1920
  };
1829
1921
  }
1830
1922
  }
1923
+ function preserveInProgressWorkspaceRebaseConflict(input, observed, options) {
1924
+ try {
1925
+ assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, options.candidateHead);
1926
+ const upload = uploadWorkspaceCandidate(input, options.candidateHead);
1927
+ if (upload.exitCode !== 0) {
1928
+ throw new Error(upload.stderr || upload.stdout || "workspace rebase conflict candidate upload failed");
1929
+ }
1930
+ const conflictPaths = [...new Set(options.conflictPaths)].sort();
1931
+ return {
1932
+ ...observed,
1933
+ outcome: "conflict_blocked",
1934
+ expectedHead: options.expectedHead,
1935
+ candidateHead: options.candidateHead,
1936
+ quarantineRef: input.quarantineRef,
1937
+ rebaseCount: options.rebaseCount,
1938
+ affectedProjects: affectedProjects(conflictPaths),
1939
+ affectedPaths: reportedPaths(conflictPaths),
1940
+ error: options.error,
1941
+ gitStatus: gitStatus(input)
1942
+ };
1943
+ } catch (error) {
1944
+ runGitResult(input, input.shadowRoot, ["rebase", "--abort"]);
1945
+ return {
1946
+ ...observed,
1947
+ outcome: "failed",
1948
+ rebaseCount: options.rebaseCount,
1949
+ affectedProjects: affectedProjects(options.conflictPaths),
1950
+ affectedPaths: reportedPaths(options.conflictPaths),
1951
+ error: `Workspace synchronization found a rebase conflict but could not preserve its durable candidate: ${error instanceof Error ? error.message : String(error)}`,
1952
+ gitStatus: gitStatus(input)
1953
+ };
1954
+ }
1955
+ }
1831
1956
  function preserveLargeDiffCandidate(input, observed, error) {
1832
1957
  try {
1833
1958
  if (stagedPaths(input).length > 0) {
@@ -1878,6 +2003,10 @@ async function synchronizeWorkspace(rawInput) {
1878
2003
  error: `canonical_head_advanced: expected ${input.expectedCanonicalHead ?? "unborn"}, fetched ${remoteHeadAtStart ?? "unborn"}`
1879
2004
  };
1880
2005
  }
2006
+ const inProgressRemediation = remediationRebaseInProgressResult(input, result);
2007
+ if (inProgressRemediation) return inProgressRemediation;
2008
+ restoreCleanShadowToExpectedIncidentCandidate(input);
2009
+ const visibleBaselineHead = restoreShadowToVisibleBaseline(input, remoteHeadAtStart);
1881
2010
  const inboundConflictsAtStart = input.skipVisibleMirror && !isCanonicalRemediationSync && remoteHeadAtStart ? visibleHydrationConflictPaths(input, hydrationPreimages, `origin/${WORKSPACE_BRANCH}`) : [];
1882
2011
  if (input.resetToCanonical) {
1883
2012
  if (input.trigger.canonicalCheckoutOnly) {
@@ -1892,8 +2021,6 @@ async function synchronizeWorkspace(rawInput) {
1892
2021
  gitStatus: gitStatus(input)
1893
2022
  };
1894
2023
  }
1895
- const inProgressRemediation = remediationRebaseInProgressResult(input, result);
1896
- if (inProgressRemediation) return inProgressRemediation;
1897
2024
  if (!input.skipVisibleMirror) resetUncommittedShadowSnapshot(input);
1898
2025
  restoreUnscopedCanonicalCheckoutSubtrees(input);
1899
2026
  if (!input.skipVisibleMirror) {
@@ -1903,6 +2030,10 @@ async function synchronizeWorkspace(rawInput) {
1903
2030
  mergeGitlinkProjection(mirroredGitlinkProjection, reconcileNewVisibleCheckouts(input));
1904
2031
  mirroredGitlinkProjection.opaqueRoots = [...new Set(mirroredGitlinkProjection.opaqueRoots)].sort();
1905
2032
  hydrationPreimages = mirroredGitlinkProjection.visiblePreimages ?? hydrationPreimages;
2033
+ if (input.trigger.type === "remediation" && !visibleWorkspaceMatchesProjection(input, mirroredGitlinkProjection)) {
2034
+ resetUncommittedShadowSnapshot(input);
2035
+ throw new Error("visible_workspace_unstable: visible files changed while preparing the remediation snapshot");
2036
+ }
1906
2037
  }
1907
2038
  const stagePathspecs = mirroredGitlinkProjection.opaqueRoots.flatMap((root) => [`:(exclude,literal)${root}`]);
1908
2039
  runGit(input, input.shadowRoot, ["add", "-A", "--", ".", ...stagePathspecs], "stage workspace changes");
@@ -1911,7 +2042,8 @@ async function synchronizeWorkspace(rawInput) {
1911
2042
  if (redundantInboundCandidate) return redundantInboundCandidate;
1912
2043
  const sampledCanonicalCheckoutTreeHash = sampleCanonicalCheckoutTree(input);
1913
2044
  const stagedWorkingPaths = stagedPaths(input);
1914
- const baseRevision = candidateBaseRevision(input);
2045
+ const preservesDivergentVisibleSnapshot = input.confirmedLargeDiff === true || input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
2046
+ const baseRevision = preservesDivergentVisibleSnapshot ? remoteHeadAtStart : visibleBaselineHead !== void 0 ? visibleBaselineHead : candidateBaseRevision(input);
1915
2047
  const paths = stagedPaths(input, baseRevision);
1916
2048
  const diffSizeBytes = await stagedDiffSizeBytes(input, baseRevision);
1917
2049
  const statusBeforeCommit = reportedStatus(gitStatus(input));
@@ -1924,16 +2056,12 @@ async function synchronizeWorkspace(rawInput) {
1924
2056
  ...sampledCanonicalCheckoutTreeHash ? { sampledCanonicalCheckoutTreeHash } : {}
1925
2057
  };
1926
2058
  const destructiveReductions = destructiveCheckoutReductions(input, baseRevision);
2059
+ let blockedDiffError;
1927
2060
  if (destructiveReductions.length > 0 && !input.confirmedLargeDiff) {
1928
2061
  const summary = destructiveReductions.map(({ root, trackedBefore, trackedAfter }) => `${root} (${trackedBefore} -> ${trackedAfter} tracked files)`).join(", ");
1929
- return preserveLargeDiffCandidate(input, observed, `Workspace safety guard blocked a destructive checkout reduction: ${summary}`);
1930
- }
1931
- if (paths.length > 0 && diffSizeBytes > MAX_WORKSPACE_SYNC_DIFF_BYTES && !input.confirmedLargeDiff && !input.trigger.canonicalCheckoutOnly) {
1932
- return preserveLargeDiffCandidate(
1933
- input,
1934
- observed,
1935
- `Workspace safety guard blocked a ${diffSizeBytes}-byte diff above the ${MAX_WORKSPACE_SYNC_DIFF_BYTES}-byte automatic publication limit.`
1936
- );
2062
+ blockedDiffError = `Workspace safety guard blocked a destructive checkout reduction: ${summary}`;
2063
+ } else if (paths.length > 0 && diffSizeBytes > MAX_WORKSPACE_SYNC_DIFF_BYTES && !input.confirmedLargeDiff && !input.trigger.canonicalCheckoutOnly) {
2064
+ blockedDiffError = `Workspace safety guard blocked a ${diffSizeBytes}-byte diff above the ${MAX_WORKSPACE_SYNC_DIFF_BYTES}-byte automatic publication limit.`;
1937
2065
  }
1938
2066
  if (stagedWorkingPaths.length > 0) {
1939
2067
  runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit workspace changes");
@@ -1941,8 +2069,11 @@ async function synchronizeWorkspace(rawInput) {
1941
2069
  let candidateHead = revParse(input, "HEAD");
1942
2070
  const candidateHeadBeforeRebase = candidateHead;
1943
2071
  if (candidateHead) assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
1944
- const hasRemoteMain = Boolean(revParse(input, `origin/${WORKSPACE_BRANCH}`));
1945
- const hasUnpushedCommit = candidateHead ? !hasRemoteMain || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", candidateHead, `origin/${WORKSPACE_BRANCH}`]) : false;
2072
+ const remoteHeadBeforeRebase = revParse(input, `origin/${WORKSPACE_BRANCH}`);
2073
+ const hasRemoteMain = Boolean(remoteHeadBeforeRebase);
2074
+ const hasCapturedVisibleDelta = visibleBaselineHead === void 0 || paths.length > 0;
2075
+ const mustDropVisibleBaselinePrefix = !preservesDivergentVisibleSnapshot && visibleBaselineHead !== void 0 && visibleBaselineHead !== remoteHeadBeforeRebase && stagedWorkingPaths.length > 0;
2076
+ const hasUnpushedCommit = candidateHead && hasCapturedVisibleDelta ? mustDropVisibleBaselinePrefix || !hasRemoteMain || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", candidateHead, `origin/${WORKSPACE_BRANCH}`]) : false;
1946
2077
  if (hasUnpushedCommit && inboundConflictsAtStart.length > 0) {
1947
2078
  const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...inboundConflictsAtStart])].sort();
1948
2079
  return preserveWorkspaceConflictCandidate(input, observed, {
@@ -2016,13 +2147,36 @@ async function synchronizeWorkspace(rawInput) {
2016
2147
  };
2017
2148
  }
2018
2149
  let rebaseCount = 0;
2019
- const expectedHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
2020
- if (candidateHead && expectedHead && !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", expectedHead, candidateHead])) {
2021
- const rebase = runGitResult(input, input.shadowRoot, ["rebase", `origin/${WORKSPACE_BRANCH}`]);
2150
+ const expectedHead = remoteHeadBeforeRebase;
2151
+ if (mustDropVisibleBaselinePrefix && !expectedHead) {
2152
+ return {
2153
+ ...observed,
2154
+ outcome: "failed",
2155
+ candidateHead: candidateHead ?? void 0,
2156
+ error: "Cannot normalize a visible workspace tail onto an unborn canonical workspace",
2157
+ gitStatus: gitStatus(input)
2158
+ };
2159
+ }
2160
+ if (candidateHead && expectedHead && (mustDropVisibleBaselinePrefix || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", expectedHead, candidateHead]))) {
2161
+ const rebase = runGitResult(
2162
+ input,
2163
+ input.shadowRoot,
2164
+ mustDropVisibleBaselinePrefix && visibleBaselineHead ? ["rebase", "--onto", `origin/${WORKSPACE_BRANCH}`, visibleBaselineHead] : ["rebase", `origin/${WORKSPACE_BRANCH}`]
2165
+ );
2022
2166
  rebaseCount += 1;
2023
2167
  if (rebase.exitCode !== 0) {
2024
2168
  const conflicted = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]);
2025
2169
  const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort();
2170
+ if (mustDropVisibleBaselinePrefix) {
2171
+ const originalCandidateHead = revParse(input, "ORIG_HEAD") ?? candidateHead;
2172
+ return preserveInProgressWorkspaceRebaseConflict(input, observed, {
2173
+ candidateHead: originalCandidateHead,
2174
+ expectedHead,
2175
+ rebaseCount,
2176
+ conflictPaths,
2177
+ error: `Workspace tail conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation in the in-progress rebase.`
2178
+ });
2179
+ }
2026
2180
  runGit(input, input.shadowRoot, ["rebase", "--abort"], "preserve workspace candidate after rebase conflict");
2027
2181
  return preserveWorkspaceConflictCandidate(input, observed, {
2028
2182
  baseRevision: candidateHead,
@@ -2068,6 +2222,9 @@ async function synchronizeWorkspace(rawInput) {
2068
2222
  error: "Workspace changed while its candidate was reconciling; visible changes and the old-base candidate were preserved."
2069
2223
  });
2070
2224
  }
2225
+ if (blockedDiffError) {
2226
+ return preserveLargeDiffCandidate(input, { ...observed, rebaseCount }, blockedDiffError);
2227
+ }
2071
2228
  const upload = uploadWorkspaceCandidate(input, candidateHead);
2072
2229
  if (upload.exitCode !== 0) {
2073
2230
  return {
@@ -46,7 +46,9 @@ export declare class ExplicitWorkspacePublicationQueue<TTrigger> {
46
46
  enqueue(publication: ExplicitWorkspacePublication<TTrigger>, pendingRequestId?: string): void;
47
47
  next(): ExplicitWorkspacePublication<TTrigger> | undefined;
48
48
  }
49
- type PersistedWorkspaceConvergenceState = Omit<WorkspaceConvergenceSnapshot, "publicationState">;
49
+ type PersistedWorkspaceConvergenceState = Omit<WorkspaceConvergenceSnapshot, "publicationState"> & {
50
+ visibleBaselineHead: string | null;
51
+ };
50
52
  export declare function readWorkspaceConvergenceState(statePath: string): PersistedWorkspaceConvergenceState;
51
53
  export declare class WorkspaceConvergenceState {
52
54
  private readonly statePath;
@@ -54,7 +56,9 @@ export declare class WorkspaceConvergenceState {
54
56
  private publicationState;
55
57
  constructor(statePath: string);
56
58
  snapshot(): WorkspaceConvergenceSnapshot;
59
+ visibleBaselineHead(): string | null;
57
60
  setLocalHead(localHead: string | null): void;
61
+ setVisibleBaselineHead(visibleBaselineHead: string | null): void;
58
62
  observeCanonicalHead(canonicalHead: string | null): boolean;
59
63
  markDirty(): number;
60
64
  schedule(): void;
@@ -73,6 +77,13 @@ export declare class WorkspaceConvergenceState {
73
77
  private persist;
74
78
  }
75
79
  export declare function processWorkspaceConvergenceState(statePath: string): WorkspaceConvergenceState;
80
+ export declare function workspaceBootstrapEstablishesVisibleBaseline(input: {
81
+ hydrated: boolean;
82
+ firstBootstrap: boolean;
83
+ incidentActive: boolean;
84
+ currentLocalHead: string | null;
85
+ currentVisibleBaselineHead: string | null;
86
+ }): boolean;
76
87
  export declare function waitForWorkspacePublicationOnShutdown(input: {
77
88
  snapshot: () => WorkspaceConvergenceSnapshot;
78
89
  forcePublication: () => void;
@@ -78,6 +78,15 @@ export type WorkspaceSyncInput = {
78
78
  quarantineRef?: string;
79
79
  /** Exact canonical head fenced by publication authority. */
80
80
  expectedCanonicalHead?: string | null;
81
+ /** Elected incident candidate pinned only while preparing remediation. */
82
+ expectedIncidentCandidateHead?: string;
83
+ /**
84
+ * Commit whose tree the visible workspace most recently reflected.
85
+ * Exact remediation may advance the disposable shadow checkout without
86
+ * hydrating visible files. A later visible snapshot must therefore rebuild
87
+ * its delta from this durable baseline before rebasing onto canonical main.
88
+ */
89
+ visibleBaselineHead?: string | null;
81
90
  confirmedLargeDiff?: boolean;
82
91
  confirmationReason?: string;
83
92
  resetToCanonical?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.61",
3
+ "version": "0.0.63",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",