@ricsam/r5d-worker 0.0.60 → 0.0.62

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
@@ -2489,6 +2489,7 @@ async function startWorker(options) {
2489
2489
  requestId: options2.requestId,
2490
2490
  trigger,
2491
2491
  dirtyGeneration: snapshot2.dirtyGeneration,
2492
+ visibleBaselineHead: workspaceConvergence.visibleBaselineHead(),
2492
2493
  explicit: true,
2493
2494
  workInFlight: false,
2494
2495
  requestedAt: Date.now()
@@ -2525,6 +2526,7 @@ async function startWorker(options) {
2525
2526
  requestId,
2526
2527
  trigger,
2527
2528
  dirtyGeneration: snapshot.dirtyGeneration,
2529
+ visibleBaselineHead: workspaceConvergence.visibleBaselineHead(),
2528
2530
  explicit: options2.requestId !== void 0,
2529
2531
  workInFlight: false,
2530
2532
  requestedAt: Date.now()
@@ -2728,6 +2730,7 @@ async function startWorker(options) {
2728
2730
  const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
2729
2731
  const input = workspaceSyncInput(trigger, {
2730
2732
  ...overrides,
2733
+ ...shouldEnsureCheckouts && !trigger.canonicalCheckoutOnly ? { visibleBaselineHead: workspaceConvergence.visibleBaselineHead() } : {},
2731
2734
  ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2732
2735
  });
2733
2736
  synchronizeStartedAt = Date.now();
@@ -2845,7 +2848,8 @@ async function startWorker(options) {
2845
2848
  canonicalResolverCheckout: true,
2846
2849
  browserPortForwarding: true,
2847
2850
  globalWorkspaceSyncLeaseV1: true,
2848
- eventualWorkspaceSyncV1: true
2851
+ eventualWorkspaceSyncV1: true,
2852
+ workspaceIncidentCandidatePinV1: true
2849
2853
  },
2850
2854
  projectRoot: projectsRoot,
2851
2855
  artifactRoot,
@@ -2912,7 +2916,15 @@ async function startWorker(options) {
2912
2916
  hydrateVisible: firstBootstrap && !activeWorkspaceIncidentId
2913
2917
  });
2914
2918
  if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2915
- workspaceConvergence.setLocalHead(bootstrapped.localHead);
2919
+ if ((0, import_workspace_convergence.workspaceBootstrapEstablishesVisibleBaseline)({
2920
+ hydrated: bootstrapped.hydrated,
2921
+ firstBootstrap,
2922
+ incidentActive: Boolean(activeWorkspaceIncidentId),
2923
+ currentLocalHead: workspaceConvergence.snapshot().localHead,
2924
+ currentVisibleBaselineHead: workspaceConvergence.visibleBaselineHead()
2925
+ })) {
2926
+ workspaceConvergence.setLocalHead(bootstrapped.localHead);
2927
+ }
2916
2928
  workspaceConvergence.observeCanonicalHead(bootstrapped.canonicalHead);
2917
2929
  for (const checkout of currentCheckouts) {
2918
2930
  const manifest = manifestByProjectId.get(checkout.projectId);
@@ -2985,6 +2997,7 @@ async function startWorker(options) {
2985
2997
  quarantineRef: message.quarantineRef,
2986
2998
  confirmedLargeDiff: message.confirmedLargeDiff,
2987
2999
  confirmationReason: message.confirmationReason,
3000
+ expectedIncidentCandidateHead: message.expectedIncidentCandidateHead,
2988
3001
  resetToCanonical: message.resetToCanonical,
2989
3002
  skipVisibleMirror: message.skipVisibleMirror
2990
3003
  }
@@ -3063,6 +3076,7 @@ async function startWorker(options) {
3063
3076
  attemptId: message.attemptId,
3064
3077
  quarantineRef: message.quarantineRef,
3065
3078
  expectedCanonicalHead: message.expectedHead,
3079
+ visibleBaselineHead: publication.visibleBaselineHead,
3066
3080
  newVisibleCheckouts: [...createdCheckouts, ...pendingTargets]
3067
3081
  })
3068
3082
  );
@@ -3140,6 +3154,7 @@ async function startWorker(options) {
3140
3154
  published: true
3141
3155
  });
3142
3156
  } else if (message.status === "blocked" && workspaceIncidentOrdering.permitsBlockedResponse(message.incidentId)) {
3157
+ if (message.visibleBaselineHead) workspaceConvergence.setVisibleBaselineHead(message.visibleBaselineHead);
3143
3158
  workspaceConvergence.block();
3144
3159
  if (message.incidentId) activeWorkspaceIncidentId = message.incidentId;
3145
3160
  } else {
@@ -3191,7 +3206,11 @@ async function startWorker(options) {
3191
3206
  if (message.type === "workspace_incident_updated") {
3192
3207
  workspaceIncidentOrdering.observe(message);
3193
3208
  const previousIncidentId = activeWorkspaceIncidentId;
3194
- activeWorkspaceIncidentId = (0, import_workspace_incident_state.applyWorkspaceIncidentUpdate)(activeWorkspaceIncidentId, message);
3209
+ const nextIncidentId = (0, import_workspace_incident_state.applyWorkspaceIncidentUpdate)(activeWorkspaceIncidentId, message);
3210
+ if (message.incidentId && (previousIncidentId === message.incidentId || nextIncidentId === message.incidentId) && message.originWorkerLabel === label && message.visibleBaselineHead) {
3211
+ workspaceConvergence.setVisibleBaselineHead(message.visibleBaselineHead);
3212
+ }
3213
+ activeWorkspaceIncidentId = nextIncidentId;
3195
3214
  const releasedHead = (0, import_workspace_incident_state.releasePendingWorkspaceHead)(
3196
3215
  previousIncidentId,
3197
3216
  activeWorkspaceIncidentId,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.60",
3
+ "version": "0.0.62",
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";
@@ -2459,6 +2460,7 @@ async function startWorker(options) {
2459
2460
  requestId: options2.requestId,
2460
2461
  trigger,
2461
2462
  dirtyGeneration: snapshot2.dirtyGeneration,
2463
+ visibleBaselineHead: workspaceConvergence.visibleBaselineHead(),
2462
2464
  explicit: true,
2463
2465
  workInFlight: false,
2464
2466
  requestedAt: Date.now()
@@ -2495,6 +2497,7 @@ async function startWorker(options) {
2495
2497
  requestId,
2496
2498
  trigger,
2497
2499
  dirtyGeneration: snapshot.dirtyGeneration,
2500
+ visibleBaselineHead: workspaceConvergence.visibleBaselineHead(),
2498
2501
  explicit: options2.requestId !== void 0,
2499
2502
  workInFlight: false,
2500
2503
  requestedAt: Date.now()
@@ -2698,6 +2701,7 @@ async function startWorker(options) {
2698
2701
  const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
2699
2702
  const input = workspaceSyncInput(trigger, {
2700
2703
  ...overrides,
2704
+ ...shouldEnsureCheckouts && !trigger.canonicalCheckoutOnly ? { visibleBaselineHead: workspaceConvergence.visibleBaselineHead() } : {},
2701
2705
  ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2702
2706
  });
2703
2707
  synchronizeStartedAt = Date.now();
@@ -2815,7 +2819,8 @@ async function startWorker(options) {
2815
2819
  canonicalResolverCheckout: true,
2816
2820
  browserPortForwarding: true,
2817
2821
  globalWorkspaceSyncLeaseV1: true,
2818
- eventualWorkspaceSyncV1: true
2822
+ eventualWorkspaceSyncV1: true,
2823
+ workspaceIncidentCandidatePinV1: true
2819
2824
  },
2820
2825
  projectRoot: projectsRoot,
2821
2826
  artifactRoot,
@@ -2882,7 +2887,15 @@ async function startWorker(options) {
2882
2887
  hydrateVisible: firstBootstrap && !activeWorkspaceIncidentId
2883
2888
  });
2884
2889
  if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2885
- workspaceConvergence.setLocalHead(bootstrapped.localHead);
2890
+ if (workspaceBootstrapEstablishesVisibleBaseline({
2891
+ hydrated: bootstrapped.hydrated,
2892
+ firstBootstrap,
2893
+ incidentActive: Boolean(activeWorkspaceIncidentId),
2894
+ currentLocalHead: workspaceConvergence.snapshot().localHead,
2895
+ currentVisibleBaselineHead: workspaceConvergence.visibleBaselineHead()
2896
+ })) {
2897
+ workspaceConvergence.setLocalHead(bootstrapped.localHead);
2898
+ }
2886
2899
  workspaceConvergence.observeCanonicalHead(bootstrapped.canonicalHead);
2887
2900
  for (const checkout of currentCheckouts) {
2888
2901
  const manifest = manifestByProjectId.get(checkout.projectId);
@@ -2955,6 +2968,7 @@ async function startWorker(options) {
2955
2968
  quarantineRef: message.quarantineRef,
2956
2969
  confirmedLargeDiff: message.confirmedLargeDiff,
2957
2970
  confirmationReason: message.confirmationReason,
2971
+ expectedIncidentCandidateHead: message.expectedIncidentCandidateHead,
2958
2972
  resetToCanonical: message.resetToCanonical,
2959
2973
  skipVisibleMirror: message.skipVisibleMirror
2960
2974
  }
@@ -3033,6 +3047,7 @@ async function startWorker(options) {
3033
3047
  attemptId: message.attemptId,
3034
3048
  quarantineRef: message.quarantineRef,
3035
3049
  expectedCanonicalHead: message.expectedHead,
3050
+ visibleBaselineHead: publication.visibleBaselineHead,
3036
3051
  newVisibleCheckouts: [...createdCheckouts, ...pendingTargets]
3037
3052
  })
3038
3053
  );
@@ -3110,6 +3125,7 @@ async function startWorker(options) {
3110
3125
  published: true
3111
3126
  });
3112
3127
  } else if (message.status === "blocked" && workspaceIncidentOrdering.permitsBlockedResponse(message.incidentId)) {
3128
+ if (message.visibleBaselineHead) workspaceConvergence.setVisibleBaselineHead(message.visibleBaselineHead);
3113
3129
  workspaceConvergence.block();
3114
3130
  if (message.incidentId) activeWorkspaceIncidentId = message.incidentId;
3115
3131
  } else {
@@ -3161,7 +3177,11 @@ async function startWorker(options) {
3161
3177
  if (message.type === "workspace_incident_updated") {
3162
3178
  workspaceIncidentOrdering.observe(message);
3163
3179
  const previousIncidentId = activeWorkspaceIncidentId;
3164
- activeWorkspaceIncidentId = applyWorkspaceIncidentUpdate(activeWorkspaceIncidentId, message);
3180
+ const nextIncidentId = applyWorkspaceIncidentUpdate(activeWorkspaceIncidentId, message);
3181
+ if (message.incidentId && (previousIncidentId === message.incidentId || nextIncidentId === message.incidentId) && message.originWorkerLabel === label && message.visibleBaselineHead) {
3182
+ workspaceConvergence.setVisibleBaselineHead(message.visibleBaselineHead);
3183
+ }
3184
+ activeWorkspaceIncidentId = nextIncidentId;
3165
3185
  const releasedHead = releasePendingWorkspaceHead(
3166
3186
  previousIncidentId,
3167
3187
  activeWorkspaceIncidentId,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.60",
3
+ "version": "0.0.62",
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.60",
3
+ "version": "0.0.62",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",