@adhdev/daemon-core 0.9.82-rc.522 → 0.9.82-rc.524

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/index.mjs CHANGED
@@ -414,10 +414,10 @@ function readInjected(value) {
414
414
  }
415
415
  function getDaemonBuildInfo() {
416
416
  if (cached) return cached;
417
- const commit = readInjected(true ? "d3a44d29416fef77ac6cd36f4798637571260dfb" : void 0) ?? "unknown";
418
- const commitShort = readInjected(true ? "d3a44d29" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
419
- const version = readInjected(true ? "0.9.82-rc.522" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
420
- const builtAt = readInjected(true ? "2026-07-14T03:03:55.067Z" : void 0);
417
+ const commit = readInjected(true ? "91ed5e0752d70b62ebcc2cddcea4d502756cff0a" : void 0) ?? "unknown";
418
+ const commitShort = readInjected(true ? "91ed5e07" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
419
+ const version = readInjected(true ? "0.9.82-rc.524" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
420
+ const builtAt = readInjected(true ? "2026-07-14T07:23:02.573Z" : void 0);
421
421
  cached = builtAt ? { commit, commitShort, version, builtAt } : { commit, commitShort, version };
422
422
  return cached;
423
423
  }
@@ -790,6 +790,9 @@ function isNonRuntimeRootFile(file, policy) {
790
790
  }
791
791
  return false;
792
792
  }
793
+ function deriveChangeArea(isDaemonAffecting, affectedPackages) {
794
+ return isDaemonAffecting ? "daemon" : affectedPackages.length > 0 ? "web" : "none";
795
+ }
793
796
  function classifyChangedFileList(files, policy) {
794
797
  if (files.length === 0) {
795
798
  return { isDaemonAffecting: true, affectedPackages: [], ambiguousNonPackageFiles: [] };
@@ -812,9 +815,10 @@ async function classifyDaemonBuildChange(repoPath, buildCommit, options, policy)
812
815
  try {
813
816
  const diff = await runGit(repoPath, ["diff", "--name-only", `${buildCommit}..HEAD`], options);
814
817
  const files = diff.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
815
- return classifyChangedFileList(files, policy);
818
+ const { isDaemonAffecting, affectedPackages } = classifyChangedFileList(files, policy);
819
+ return { isDaemonAffecting, affectedPackages, changeArea: deriveChangeArea(isDaemonAffecting, affectedPackages) };
816
820
  } catch {
817
- return { isDaemonAffecting: true, affectedPackages: [] };
821
+ return { isDaemonAffecting: true, affectedPackages: [], changeArea: "daemon" };
818
822
  }
819
823
  }
820
824
  async function classifyChangedPackages(repoPath, fromRef, toRef, options = {}) {
@@ -827,7 +831,7 @@ async function classifyChangedPackages(repoPath, fromRef, toRef, options = {}) {
827
831
  return refineVerdictThroughSubmodules(repoPath, fromRef, toRef, options, policy, rootVerdict);
828
832
  }
829
833
  async function refineVerdictThroughSubmodules(repoPath, fromRef, toRef, options, policy, rootVerdict) {
830
- const strip = ({ isDaemonAffecting, affectedPackages }) => ({ isDaemonAffecting, affectedPackages });
834
+ const strip = ({ isDaemonAffecting, affectedPackages: affectedPackages2 }) => ({ isDaemonAffecting, affectedPackages: affectedPackages2, changeArea: deriveChangeArea(isDaemonAffecting, affectedPackages2) });
831
835
  const ambiguous = rootVerdict.ambiguousNonPackageFiles;
832
836
  if (ambiguous.length === 0) return strip(rootVerdict);
833
837
  let submodulePaths;
@@ -859,9 +863,11 @@ async function refineVerdictThroughSubmodules(repoPath, fromRef, toRef, options,
859
863
  return strip(rootVerdict);
860
864
  }
861
865
  if (subVerdict.isDaemonAffecting) {
866
+ const affectedPackages2 = [.../* @__PURE__ */ new Set([...rootVerdict.affectedPackages, ...subVerdict.affectedPackages])].sort();
862
867
  return {
863
868
  isDaemonAffecting: true,
864
- affectedPackages: [.../* @__PURE__ */ new Set([...rootVerdict.affectedPackages, ...subVerdict.affectedPackages])].sort()
869
+ affectedPackages: affectedPackages2,
870
+ changeArea: deriveChangeArea(true, affectedPackages2)
865
871
  };
866
872
  }
867
873
  submoduleAffectedPackages.push(...subVerdict.affectedPackages);
@@ -869,9 +875,11 @@ async function refineVerdictThroughSubmodules(repoPath, fromRef, toRef, options,
869
875
  const rootPackagesBenign = rootVerdict.affectedPackages.every(
870
876
  (p) => policy.webOnlyPackages.has(p) && !policy.daemonRuntimePackages.has(p)
871
877
  );
878
+ const affectedPackages = [.../* @__PURE__ */ new Set([...rootVerdict.affectedPackages, ...submoduleAffectedPackages])].sort();
872
879
  return {
873
880
  isDaemonAffecting: !rootPackagesBenign,
874
- affectedPackages: [.../* @__PURE__ */ new Set([...rootVerdict.affectedPackages, ...submoduleAffectedPackages])].sort()
881
+ affectedPackages,
882
+ changeArea: deriveChangeArea(!rootPackagesBenign, affectedPackages)
875
883
  };
876
884
  }
877
885
  async function listSubmodulePaths(repoPath, options) {
@@ -10749,6 +10757,14 @@ function normalizeMeshCommandConfig(entry, source) {
10749
10757
  if (entry.env !== void 0 && (!isMeshConfigRecord(entry.env) || !Object.values(entry.env).every((value) => typeof value === "string"))) {
10750
10758
  return { rejected: { source, command: commandText, reason: "env must be an object of string values" } };
10751
10759
  }
10760
+ let scopes;
10761
+ if (entry.scopes !== void 0) {
10762
+ if (!Array.isArray(entry.scopes) || !entry.scopes.every((s2) => MESH_REFINE_VALIDATION_SCOPES.includes(s2))) {
10763
+ return { rejected: { source, command: commandText, reason: `scopes must be an array of ${MESH_REFINE_VALIDATION_SCOPES.join(" | ")}` } };
10764
+ }
10765
+ const deduped = [...new Set(entry.scopes)];
10766
+ scopes = deduped.length ? deduped : void 0;
10767
+ }
10752
10768
  return {
10753
10769
  command: {
10754
10770
  command,
@@ -10759,7 +10775,8 @@ function normalizeMeshCommandConfig(entry, source) {
10759
10775
  ...typeof entry.cwd === "string" && entry.cwd.trim() ? { cwd: entry.cwd.trim() } : {},
10760
10776
  ...typeof entry.timeoutMs === "number" ? { timeoutMs: entry.timeoutMs } : {},
10761
10777
  ...typeof entry.outputLimitBytes === "number" ? { outputLimitBytes: entry.outputLimitBytes } : {},
10762
- ...isMeshConfigRecord(entry.env) ? { env: entry.env } : {}
10778
+ ...isMeshConfigRecord(entry.env) ? { env: entry.env } : {},
10779
+ ...scopes ? { scopes } : {}
10763
10780
  }
10764
10781
  };
10765
10782
  }
@@ -10916,11 +10933,12 @@ function resolveMeshRefineValidationPlan(mesh, workspace) {
10916
10933
  unavailableReason: validation.commands.length ? void 0 : "validation_unavailable: repo mesh/refine config has no validation.commands"
10917
10934
  };
10918
10935
  }
10919
- var MESH_REFINE_VALIDATION_CATEGORIES, MESH_REFINE_CONFIG_LOCATIONS, MESH_REFINE_CONFIG_SCHEMA, SHELL_METACHAR_RE, SAFE_TOKEN_RE, SAFE_WIN32_EXEC_TOKEN_RE, isRecord2;
10936
+ var MESH_REFINE_VALIDATION_CATEGORIES, MESH_REFINE_VALIDATION_SCOPES, MESH_REFINE_CONFIG_LOCATIONS, MESH_REFINE_CONFIG_SCHEMA, SHELL_METACHAR_RE, SAFE_TOKEN_RE, SAFE_WIN32_EXEC_TOKEN_RE, isRecord2;
10920
10937
  var init_refine_config = __esm({
10921
10938
  "src/mesh/refine-config.ts"() {
10922
10939
  "use strict";
10923
10940
  MESH_REFINE_VALIDATION_CATEGORIES = ["typecheck", "test", "lint", "build"];
10941
+ MESH_REFINE_VALIDATION_SCOPES = ["none", "web", "daemon"];
10924
10942
  MESH_REFINE_CONFIG_LOCATIONS = [
10925
10943
  ".adhdev/refine.json",
10926
10944
  ".adhdev/refine.yaml",
@@ -10970,7 +10988,12 @@ var init_refine_config = __esm({
10970
10988
  cwd: { type: "string" },
10971
10989
  timeoutMs: { type: "number", minimum: 1e3, maximum: 6e5 },
10972
10990
  outputLimitBytes: { type: "number", minimum: 1024, maximum: 1048576 },
10973
- env: { type: "object", additionalProperties: { type: "string" } }
10991
+ env: { type: "object", additionalProperties: { type: "string" } },
10992
+ scopes: {
10993
+ type: "array",
10994
+ items: { enum: [...MESH_REFINE_VALIDATION_SCOPES] },
10995
+ description: "DOCS-ROOT: change-impact scopes this command runs in ('none'=docs-only, 'web', 'daemon'). Omitted/empty \u2192 runs in every area."
10996
+ }
10974
10997
  }
10975
10998
  }
10976
10999
  },
@@ -10989,7 +11012,11 @@ var init_refine_config = __esm({
10989
11012
  cwd: { type: "string" },
10990
11013
  timeoutMs: { type: "number", minimum: 1e3, maximum: 6e5 },
10991
11014
  outputLimitBytes: { type: "number", minimum: 1024, maximum: 1048576 },
10992
- env: { type: "object", additionalProperties: { type: "string" } }
11015
+ env: { type: "object", additionalProperties: { type: "string" } },
11016
+ scopes: {
11017
+ type: "array",
11018
+ items: { enum: [...MESH_REFINE_VALIDATION_SCOPES] }
11019
+ }
10993
11020
  }
10994
11021
  }
10995
11022
  }
@@ -17219,6 +17246,56 @@ async function runContinuousAutoFastForwardScan(components, mesh) {
17219
17246
  await delegateRemoteAutoFastForward(components, { meshId, nodeId, node, daemonId, workspace, policy, trigger: "reconcile_auto" });
17220
17247
  }
17221
17248
  }
17249
+ async function runPendingCoordinatorCatchupScan(components, mesh) {
17250
+ const meshId = readNonEmptyString2(mesh?.id);
17251
+ if (!meshId) return;
17252
+ const localIds = expandDaemonIdForms([
17253
+ readNonEmptyString2(components.statusInstanceId),
17254
+ readNonEmptyString2(loadConfig().machineId)
17255
+ ]);
17256
+ let markers = [];
17257
+ try {
17258
+ markers = drainPendingMeshCoordinatorEvents(
17259
+ meshId,
17260
+ localIds.length > 0 ? localIds : void 0,
17261
+ { onlyEvents: /* @__PURE__ */ new Set(["coordinator_catchup"]) }
17262
+ );
17263
+ } catch (e) {
17264
+ LOG.warn("MeshReconcile", `Coordinator-catchup drain failed for mesh ${meshId}: ${e?.message || e}`);
17265
+ return;
17266
+ }
17267
+ if (markers.length === 0) return;
17268
+ const nodes = Array.isArray(mesh?.nodes) ? mesh.nodes : [];
17269
+ for (const marker of markers) {
17270
+ const meta = marker.metadataEvent || {};
17271
+ const nodeId = readNonEmptyString2(marker.nodeId) || readNonEmptyString2(meta.nodeId);
17272
+ const workspace = readNonEmptyString2(marker.workspace) || readNonEmptyString2(meta.workspace);
17273
+ const baseBranch = readNonEmptyString2(meta.baseBranch);
17274
+ if (!workspace) continue;
17275
+ if (nodeId && nodeHasActiveMeshWork(components, meshId, nodeId)) {
17276
+ try {
17277
+ queuePendingMeshCoordinatorEvent(marker);
17278
+ } catch {
17279
+ }
17280
+ continue;
17281
+ }
17282
+ try {
17283
+ const ff = await fastForwardMeshNode({
17284
+ meshId,
17285
+ ...nodeId ? { nodeId } : {},
17286
+ workspace,
17287
+ ...baseBranch ? { branch: baseBranch } : {},
17288
+ mode: "merge",
17289
+ execute: true,
17290
+ trigger: "refine_post_push_catchup",
17291
+ allowAutoPublishSubmoduleMainCommits: mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true
17292
+ });
17293
+ LOG.info("MeshReconcile", `Coordinator catch-up ff for ${meshId}/${nodeId || workspace}: ${ff.code} (executed=${ff.executed})`);
17294
+ } catch (e) {
17295
+ LOG.warn("MeshReconcile", `Coordinator catch-up ff failed for ${meshId}/${nodeId || workspace}: ${e?.message || e}`);
17296
+ }
17297
+ }
17298
+ }
17222
17299
  function runIdleMaintenanceThenAssignQueue(components, args) {
17223
17300
  setImmediate(() => {
17224
17301
  maybeAutoFastForwardIdleNode(components, args).finally(() => {
@@ -22214,6 +22291,15 @@ async function runMeshReconcileTick(components) {
22214
22291
  }
22215
22292
  }
22216
22293
  }
22294
+ for (const mesh of listMeshes()) {
22295
+ const selfIds = resolveCoordinatorSelfIds(mesh, drainDaemonIds);
22296
+ if (!daemonHostsMesh(mesh, selfIds)) continue;
22297
+ try {
22298
+ await runPendingCoordinatorCatchupScan(components, mesh);
22299
+ } catch (e) {
22300
+ LOG.warn("MeshReconcile", `Coordinator catch-up scan failed for mesh ${mesh.id}: ${e?.message || e}`);
22301
+ }
22302
+ }
22217
22303
  if (dispatchMeshCommand) {
22218
22304
  for (const mesh of listMeshes()) {
22219
22305
  const selfIds = resolveCoordinatorSelfIds(mesh, drainDaemonIds);
@@ -58891,6 +58977,8 @@ init_logger();
58891
58977
  init_debug_trace();
58892
58978
  init_dist();
58893
58979
  init_mesh_events();
58980
+ init_mesh_reconcile_identity();
58981
+ init_mesh_fast_forward();
58894
58982
  import { execFileSync as execFileSync8 } from "child_process";
58895
58983
 
58896
58984
  // src/mesh/mesh-refine-batch.ts
@@ -59821,7 +59909,10 @@ function buildMeshRefineValidationPlan(mesh, workspace) {
59821
59909
  category: command.category,
59822
59910
  source: command.source,
59823
59911
  cwd: command.cwd,
59824
- timeoutMs: command.timeoutMs
59912
+ timeoutMs: command.timeoutMs,
59913
+ // DOCS-ROOT: surface the change-impact scopes so `mesh_refine_config` shows which
59914
+ // area(s) each command runs in (absent → every area).
59915
+ ...command.scopes ? { scopes: command.scopes } : {}
59825
59916
  });
59826
59917
  return {
59827
59918
  source: plan.source,
@@ -59934,9 +60025,33 @@ async function runMeshRefineValidationGate(mesh, workspace, opts) {
59934
60025
  return /\bdaemon-core\b|\bdaemon-cloud\b|\btest:daemon\b|check-vendor-drift/.test(haystack);
59935
60026
  };
59936
60027
  const scopeUnaffectedDaemon = opts?.changeImpact?.isDaemonAffecting === false;
60028
+ const changeArea = opts?.changeImpact?.changeArea;
60029
+ const commandRunsInArea = (candidate) => {
60030
+ if (!changeArea) return true;
60031
+ const scopes = candidate.scopes;
60032
+ if (scopes && scopes.length) return scopes.includes(changeArea);
60033
+ return changeArea !== "none";
60034
+ };
59937
60035
  const skippedDaemonCommands = [];
60036
+ const skippedScopeCommands = [];
59938
60037
  const commandsToRun = [];
59939
60038
  for (const candidate of selection.commands) {
60039
+ if (!commandRunsInArea(candidate)) {
60040
+ skippedScopeCommands.push(candidate.displayCommand);
60041
+ summary.commandsRun.push({
60042
+ command: candidate.command,
60043
+ args: candidate.args,
60044
+ displayCommand: candidate.displayCommand,
60045
+ category: candidate.category,
60046
+ source: candidate.source,
60047
+ passed: true,
60048
+ skipped: true,
60049
+ skipReason: "unaffected_change_scope",
60050
+ changeArea,
60051
+ ...candidate.scopes ? { scopes: candidate.scopes } : {}
60052
+ });
60053
+ continue;
60054
+ }
59940
60055
  if (scopeUnaffectedDaemon && isDaemonScopedCommand(candidate)) {
59941
60056
  skippedDaemonCommands.push(candidate.displayCommand);
59942
60057
  summary.commandsRun.push({
@@ -59957,7 +60072,9 @@ async function runMeshRefineValidationGate(mesh, workspace, opts) {
59957
60072
  summary.changeImpact = {
59958
60073
  isDaemonAffecting: opts.changeImpact.isDaemonAffecting,
59959
60074
  affectedPackages: opts.changeImpact.affectedPackages,
59960
- ...skippedDaemonCommands.length ? { skippedDaemonCommands } : {}
60075
+ ...changeArea ? { changeArea } : {},
60076
+ ...skippedDaemonCommands.length ? { skippedDaemonCommands } : {},
60077
+ ...skippedScopeCommands.length ? { skippedScopeCommands } : {}
59961
60078
  };
59962
60079
  }
59963
60080
  if (runLegacyBootstrapCommands) {
@@ -60083,6 +60200,24 @@ function buildRefineJobHandle(self, args) {
60083
60200
  }
60084
60201
  };
60085
60202
  }
60203
+ function extractValidationFailureDiagnostics(validationSummary) {
60204
+ if (!validationSummary || typeof validationSummary !== "object") return void 0;
60205
+ const commandsRun = Array.isArray(validationSummary.commandsRun) ? validationSummary.commandsRun : [];
60206
+ const failed = commandsRun.find((c) => c.passed === false);
60207
+ const summaryFailureKind = validationSummary.failureKind;
60208
+ if (!failed) {
60209
+ return summaryFailureKind !== void 0 ? { failureKind: summaryFailureKind } : void 0;
60210
+ }
60211
+ const firstFailedCommand = typeof failed.displayCommand === "string" ? failed.displayCommand : typeof failed.command === "string" ? [failed.command, ...Array.isArray(failed.args) ? failed.args : []].join(" ").trim() : void 0;
60212
+ const rawOutput = [failed.stderr, failed.stdout, failed.output].filter((s2) => typeof s2 === "string" && s2.length > 0).join("\n");
60213
+ const outputTail = rawOutput.length > 600 ? rawOutput.slice(-600) : rawOutput;
60214
+ return {
60215
+ ...firstFailedCommand ? { firstFailedCommand } : {},
60216
+ ...failed.exitCode !== void 0 ? { exitCode: failed.exitCode } : {},
60217
+ ...failed.failureKind !== void 0 ? { failureKind: failed.failureKind } : summaryFailureKind !== void 0 ? { failureKind: summaryFailureKind } : {},
60218
+ ...outputTail ? { outputTail } : {}
60219
+ };
60220
+ }
60086
60221
  function slimRefineEventResult(result) {
60087
60222
  const slim = {};
60088
60223
  for (const key2 of [
@@ -60095,7 +60230,12 @@ function slimRefineEventResult(result) {
60095
60230
  "into",
60096
60231
  "terminalKind",
60097
60232
  "nextStep",
60098
- "finalBranchConvergenceState"
60233
+ "finalBranchConvergenceState",
60234
+ // QW4: merge conflict paths; QW5: cleanup branch-ref / residue warnings.
60235
+ "conflictPaths",
60236
+ "branchRefWarning",
60237
+ "residueWarning",
60238
+ "branchRefDeleted"
60099
60239
  ]) {
60100
60240
  if (result[key2] !== void 0) slim[key2] = result[key2];
60101
60241
  }
@@ -60104,12 +60244,15 @@ function slimRefineEventResult(result) {
60104
60244
  }
60105
60245
  if (result.validationSummary && typeof result.validationSummary === "object") {
60106
60246
  const vs = result.validationSummary;
60247
+ const diagnostics = vs.status === "failed" ? extractValidationFailureDiagnostics(vs) : void 0;
60107
60248
  slim.validationSummary = {
60108
60249
  status: vs.status,
60109
60250
  failureCode: vs.failureCode,
60251
+ failureKind: vs.failureKind,
60110
60252
  configSource: vs.configSource,
60111
60253
  configSourceType: vs.configSourceType,
60112
- commandsRunCount: Array.isArray(vs.commandsRun) ? vs.commandsRun.length : void 0
60254
+ commandsRunCount: Array.isArray(vs.commandsRun) ? vs.commandsRun.length : void 0,
60255
+ ...diagnostics ? { failure: diagnostics } : {}
60113
60256
  };
60114
60257
  }
60115
60258
  if (result.patchEquivalence && typeof result.patchEquivalence === "object") {
@@ -60250,6 +60393,8 @@ async function executeMeshRefineNodeSynchronously(self, meshId, nodeId, args) {
60250
60393
  const resolved = await refineResolveRefsStage(self, meshId, nodeId, args, refineStages);
60251
60394
  if (resolved.kind === "terminal") return resolved.result;
60252
60395
  const ctx = resolved.ctx;
60396
+ const syncBase = await refineSyncBaseStage(self, ctx);
60397
+ if (syncBase.kind === "terminal") return syncBase.result;
60253
60398
  const validation = await refineValidationStage(self, ctx);
60254
60399
  if (validation.kind === "terminal") return validation.result;
60255
60400
  const patchEquivalence = await refinePatchEquivalenceStage(self, ctx);
@@ -60338,6 +60483,152 @@ async function refineResolveRefsStage(self, meshId, nodeId, args, refineStages)
60338
60483
  }
60339
60484
  };
60340
60485
  }
60486
+ async function computeBranchBaseDivergence(execFileAsync4, cwd, baseHead, branchHead) {
60487
+ let mergeBase;
60488
+ try {
60489
+ const { stdout } = await execFileAsync4("git", ["merge-base", baseHead, branchHead], { cwd, encoding: "utf8" });
60490
+ mergeBase = stdout.trim() || void 0;
60491
+ } catch {
60492
+ }
60493
+ let ahead = 0;
60494
+ let behind = 0;
60495
+ try {
60496
+ const { stdout } = await execFileAsync4("git", ["rev-list", "--left-right", "--count", `${baseHead}...${branchHead}`], { cwd, encoding: "utf8" });
60497
+ const [left, right] = stdout.trim().split(/\s+/).map((n) => Number.parseInt(n, 10));
60498
+ behind = Number.isFinite(left) ? left : 0;
60499
+ ahead = Number.isFinite(right) ? right : 0;
60500
+ } catch {
60501
+ }
60502
+ return {
60503
+ mergeBase,
60504
+ ahead,
60505
+ behind,
60506
+ diverged: ahead > 0 && behind > 0,
60507
+ // Strictly behind = base is a descendant of branch (branch is an ancestor of base):
60508
+ // behind>0 with ahead===0.
60509
+ isStrictlyBehind: behind > 0 && ahead === 0
60510
+ };
60511
+ }
60512
+ async function refineSyncBaseStage(self, ctx) {
60513
+ const { repoRoot, baseHead, node, branch, baseBranch, refineStages, execFileAsync: execFileAsync4 } = ctx;
60514
+ let branchHead = ctx.branchHead;
60515
+ const syncStarted = Date.now();
60516
+ const divergence = await computeBranchBaseDivergence(execFileAsync4, node.workspace, baseHead, branchHead);
60517
+ if (divergence.behind === 0) {
60518
+ recordMeshRefineStage(refineStages, "sync_base", "passed", syncStarted, {
60519
+ ahead: divergence.ahead,
60520
+ behind: divergence.behind,
60521
+ rebased: false,
60522
+ reason: "branch_up_to_date_with_base"
60523
+ });
60524
+ return { kind: "continue", ctx };
60525
+ }
60526
+ try {
60527
+ const preRebasePe = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
60528
+ const alreadyMerged = !preRebasePe.actualPatchId && !!preRebasePe.expectedPatchId;
60529
+ const submoduleConflict = preRebasePe.actionableHint?.kind === "submodule_conflict";
60530
+ if (alreadyMerged || submoduleConflict) {
60531
+ recordMeshRefineStage(refineStages, "sync_base", "passed", syncStarted, {
60532
+ ahead: divergence.ahead,
60533
+ behind: divergence.behind,
60534
+ rebased: false,
60535
+ reason: alreadyMerged ? "already_merged_via_other_path_skip_rebase" : "submodule_conflict_defer_to_patch_equivalence"
60536
+ });
60537
+ return { kind: "continue", ctx };
60538
+ }
60539
+ } catch {
60540
+ }
60541
+ const rebaseStarted = Date.now();
60542
+ try {
60543
+ execFileSync8("git", ["rebase", baseHead], { cwd: node.workspace, stdio: ["ignore", "pipe", "pipe"] });
60544
+ } catch (rebaseErr) {
60545
+ try {
60546
+ execFileSync8("git", ["rebase", "--abort"], { cwd: node.workspace, stdio: "ignore" });
60547
+ } catch {
60548
+ }
60549
+ let submoduleHintPatchEquivalence;
60550
+ try {
60551
+ submoduleHintPatchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, ctx.branchHead);
60552
+ } catch {
60553
+ }
60554
+ const submoduleConflict = submoduleHintPatchEquivalence?.actionableHint?.kind === "submodule_conflict";
60555
+ recordMeshRefineStage(refineStages, "sync_base", "failed", syncStarted, {
60556
+ ahead: divergence.ahead,
60557
+ behind: divergence.behind,
60558
+ diverged: divergence.diverged,
60559
+ error: rebaseErr?.message || String(rebaseErr),
60560
+ ...submoduleConflict ? { submoduleConflict: true } : {}
60561
+ });
60562
+ if (submoduleConflict && submoduleHintPatchEquivalence) {
60563
+ recordMeshRefineStage(refineStages, "patch_equivalence", "failed", rebaseStarted, {
60564
+ equivalent: submoduleHintPatchEquivalence.equivalent,
60565
+ expectedPatchId: submoduleHintPatchEquivalence.expectedPatchId,
60566
+ actualPatchId: submoduleHintPatchEquivalence.actualPatchId,
60567
+ error: submoduleHintPatchEquivalence.error,
60568
+ actionableHint: submoduleHintPatchEquivalence.actionableHint
60569
+ });
60570
+ return { kind: "terminal", result: {
60571
+ success: false,
60572
+ code: "patch_equivalence_failed",
60573
+ convergenceStatus: "blocked_review",
60574
+ error: "Refinery patch-equivalence preflight failed (submodule gitlink conflict); merge/refine was not attempted.",
60575
+ branch,
60576
+ into: baseBranch,
60577
+ patchEquivalence: submoduleHintPatchEquivalence,
60578
+ refineStages,
60579
+ finalBranchConvergenceState: {
60580
+ branch,
60581
+ baseBranch,
60582
+ merged: false,
60583
+ removed: false,
60584
+ patchEquivalence: "failed",
60585
+ status: "blocked_review"
60586
+ }
60587
+ } };
60588
+ }
60589
+ recordMeshRefineStage(refineStages, "patch_equivalence_after_auto_rebase", "failed", rebaseStarted, {
60590
+ error: rebaseErr?.message || String(rebaseErr)
60591
+ });
60592
+ return { kind: "terminal", result: {
60593
+ success: false,
60594
+ code: "needs_rebase_with_conflicts",
60595
+ convergenceStatus: "blocked_review",
60596
+ error: divergence.diverged ? `Branch has diverged from ${baseBranch} (ahead ${divergence.ahead}, behind ${divergence.behind}) and auto-rebase onto the fetched base hit conflicts; resolve conflicts manually and retry.` : `Branch is behind ${baseBranch} and auto-rebase failed due to conflicts; resolve conflicts manually and retry.`,
60597
+ branch,
60598
+ into: baseBranch,
60599
+ refineStages,
60600
+ finalBranchConvergenceState: {
60601
+ branch,
60602
+ baseBranch,
60603
+ merged: false,
60604
+ removed: false,
60605
+ status: "blocked_review"
60606
+ }
60607
+ } };
60608
+ }
60609
+ const { stdout: rebasedHeadStdout } = await execFileAsync4("git", ["rev-parse", "HEAD"], { cwd: node.workspace, encoding: "utf8" });
60610
+ branchHead = rebasedHeadStdout.trim();
60611
+ ctx.branchHead = branchHead;
60612
+ let changeImpact = ctx.changeImpact;
60613
+ try {
60614
+ changeImpact = await classifyChangedPackages(node.workspace, baseHead, branchHead);
60615
+ ctx.changeImpact = changeImpact;
60616
+ } catch {
60617
+ }
60618
+ recordMeshRefineStage(refineStages, "sync_base", "passed", syncStarted, {
60619
+ ahead: divergence.ahead,
60620
+ behind: divergence.behind,
60621
+ diverged: divergence.diverged,
60622
+ rebased: true,
60623
+ rebasedBranchHead: branchHead,
60624
+ ...changeImpact ? { changeImpact } : {}
60625
+ });
60626
+ recordMeshRefineStage(refineStages, "patch_equivalence_after_auto_rebase", "passed", rebaseStarted, {
60627
+ rebasedBranchHead: branchHead,
60628
+ rebasedOnto: baseHead
60629
+ });
60630
+ return { kind: "continue", ctx };
60631
+ }
60341
60632
  async function refineValidationStage(self, ctx) {
60342
60633
  const { mesh, node, branch, baseBranch, refineStages } = ctx;
60343
60634
  const validationStarted = Date.now();
@@ -60362,7 +60653,7 @@ async function refineValidationStage(self, ctx) {
60362
60653
  { validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length }
60363
60654
  );
60364
60655
  if (validationSummary.status === "failed") {
60365
- const firstFailedCmd = Array.isArray(validationSummary.commandsRun) ? validationSummary.commandsRun.find((c) => c.success === false) : void 0;
60656
+ const firstFailedCmd = Array.isArray(validationSummary.commandsRun) ? validationSummary.commandsRun.find((c) => c.passed === false) : void 0;
60366
60657
  const buildValidationFailedError = () => {
60367
60658
  const base = validationSummary.failureCode === "missing_dependencies" ? "Refinery validation dependencies are missing for a change-affected package; merge/refine was not attempted. To make this self-service, either (1) configure .adhdev/worktree_bootstrap.json (or validation.bootstrapCommands in .adhdev/refine.json) so Refinery installs deps before validation, or (2) converge the branch via the documented manual fast-forward-only bypass (rebase onto the fetched base, verify strict ancestry, then push ff-only) instead of the refine gate." : validationSummary.failureCode === "dependency_bootstrap_failed" ? "Refinery dependency/bootstrap command failed; merge/refine was not attempted." : validationSummary.failureCode === "spawn_resolution_failed" ? validationSummary.spawnResolutionError || "Refinery validation command could not be spawned (executable not found); merge/refine was not attempted." : "Refinery validation gate failed; merge/refine was not attempted.";
60368
60659
  if (!firstFailedCmd) return base;
@@ -60418,10 +60709,10 @@ ${tail}` : ""
60418
60709
  return { kind: "continue", ctx };
60419
60710
  }
60420
60711
  async function refinePatchEquivalenceStage(self, ctx) {
60421
- const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, validationSummary, refineStages, execFileAsync: execFileAsync4 } = ctx;
60422
- let branchHead = ctx.branchHead;
60712
+ const { meshId, nodeId, args, repoRoot, baseHead, branch, baseBranch, validationSummary, refineStages } = ctx;
60713
+ const branchHead = ctx.branchHead;
60423
60714
  const patchEquivalenceStarted = Date.now();
60424
- let patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
60715
+ const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
60425
60716
  recordMeshRefineStage(refineStages, "patch_equivalence", patchEquivalence.status, patchEquivalenceStarted, {
60426
60717
  equivalent: patchEquivalence.equivalent,
60427
60718
  expectedPatchId: patchEquivalence.expectedPatchId,
@@ -60430,90 +60721,8 @@ async function refinePatchEquivalenceStage(self, ctx) {
60430
60721
  actionableHint: patchEquivalence.actionableHint
60431
60722
  });
60432
60723
  if (!patchEquivalence.equivalent) {
60433
- let didAutoRebase = false;
60434
- let isBehindBase = false;
60435
- try {
60436
- execFileSync8("git", ["merge-base", "--is-ancestor", branchHead, baseHead], {
60437
- cwd: node.workspace,
60438
- stdio: "ignore"
60439
- });
60440
- isBehindBase = true;
60441
- } catch {
60442
- }
60443
- if (isBehindBase) {
60444
- const autoRebaseStarted = Date.now();
60445
- try {
60446
- execFileSync8("git", ["rebase", baseHead], {
60447
- cwd: node.workspace,
60448
- stdio: ["ignore", "pipe", "pipe"]
60449
- });
60450
- const { stdout: rebasedHeadStdout } = await execFileAsync4("git", ["rev-parse", "HEAD"], { cwd: node.workspace, encoding: "utf8" });
60451
- branchHead = rebasedHeadStdout.trim();
60452
- const rebasedPatchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
60453
- recordMeshRefineStage(refineStages, "patch_equivalence_after_auto_rebase", rebasedPatchEquivalence.status, autoRebaseStarted, {
60454
- equivalent: rebasedPatchEquivalence.equivalent,
60455
- expectedPatchId: rebasedPatchEquivalence.expectedPatchId,
60456
- actualPatchId: rebasedPatchEquivalence.actualPatchId,
60457
- error: rebasedPatchEquivalence.error,
60458
- rebasedBranchHead: branchHead
60459
- });
60460
- if (rebasedPatchEquivalence.equivalent) {
60461
- patchEquivalence = rebasedPatchEquivalence;
60462
- didAutoRebase = true;
60463
- } else {
60464
- return { kind: "terminal", result: {
60465
- success: false,
60466
- code: "needs_rebase",
60467
- convergenceStatus: "blocked_review",
60468
- error: "Branch was rebased onto base but patch equivalence still failed; manual intervention required.",
60469
- branch,
60470
- into: baseBranch,
60471
- validationSummary,
60472
- patchEquivalence: rebasedPatchEquivalence,
60473
- refineStages,
60474
- finalBranchConvergenceState: {
60475
- branch,
60476
- baseBranch,
60477
- merged: false,
60478
- removed: false,
60479
- validation: "passed",
60480
- patchEquivalence: "failed",
60481
- status: "blocked_review"
60482
- }
60483
- } };
60484
- }
60485
- } catch (rebaseErr) {
60486
- try {
60487
- execFileSync8("git", ["rebase", "--abort"], { cwd: node.workspace, stdio: "ignore" });
60488
- } catch {
60489
- }
60490
- recordMeshRefineStage(refineStages, "patch_equivalence_after_auto_rebase", "failed", autoRebaseStarted, {
60491
- error: rebaseErr?.message || String(rebaseErr)
60492
- });
60493
- return { kind: "terminal", result: {
60494
- success: false,
60495
- code: "needs_rebase_with_conflicts",
60496
- convergenceStatus: "blocked_review",
60497
- error: "Branch is behind base and auto-rebase failed due to conflicts; resolve conflicts manually and retry.",
60498
- branch,
60499
- into: baseBranch,
60500
- validationSummary,
60501
- patchEquivalence,
60502
- refineStages,
60503
- finalBranchConvergenceState: {
60504
- branch,
60505
- baseBranch,
60506
- merged: false,
60507
- removed: false,
60508
- validation: "passed",
60509
- patchEquivalence: "failed",
60510
- status: "blocked_review"
60511
- }
60512
- } };
60513
- }
60514
- }
60515
60724
  const alreadyMergedViaOtherPath = !patchEquivalence.actualPatchId && !!patchEquivalence.expectedPatchId;
60516
- if (!didAutoRebase && !alreadyMergedViaOtherPath) {
60725
+ if (!alreadyMergedViaOtherPath) {
60517
60726
  return { kind: "terminal", result: {
60518
60727
  success: false,
60519
60728
  code: "patch_equivalence_failed",
@@ -60535,7 +60744,7 @@ async function refinePatchEquivalenceStage(self, ctx) {
60535
60744
  }
60536
60745
  } };
60537
60746
  }
60538
- if (!didAutoRebase && alreadyMergedViaOtherPath) {
60747
+ {
60539
60748
  recordMeshRefineStage(refineStages, "merge", "skipped", Date.now(), {
60540
60749
  reason: "already_merged_via_other_path",
60541
60750
  note: "actualPatchId is empty; branch content is already present in base via a different commit path"
@@ -60743,8 +60952,145 @@ ${hintLines.join("\n")}` : "",
60743
60952
  }
60744
60953
  return { kind: "continue", ctx };
60745
60954
  }
60955
+ async function requestCoordinatorLocalCatchup(self, params) {
60956
+ const { meshId, ctx, mesh, baseBranch, repoRoot } = params;
60957
+ const coordinatorDaemonId = typeof ctx.args?.coordinatorDaemonId === "string" && ctx.args.coordinatorDaemonId.trim() ? ctx.args.coordinatorDaemonId.trim() : self.deps.statusInstanceId || void 0;
60958
+ if (!coordinatorDaemonId) return void 0;
60959
+ if (!Array.isArray(mesh?.nodes)) return void 0;
60960
+ const coordinatorBaseNode = mesh.nodes.find((n) => !n?.isLocalWorktree && daemonIdListIncludes([coordinatorDaemonId], readStringValue(n?.daemonId)));
60961
+ if (!coordinatorBaseNode) return void 0;
60962
+ const coordinatorWorkspace = readStringValue(coordinatorBaseNode.repoRoot) || readStringValue(coordinatorBaseNode.workspace);
60963
+ if (!coordinatorWorkspace) return void 0;
60964
+ const drainIds = [self.deps.statusInstanceId].filter((v) => typeof v === "string" && v.length > 0);
60965
+ const selfIds = resolveCoordinatorSelfIds(mesh, drainIds);
60966
+ const coordinatorIsSelf = daemonIdListIncludes(selfIds, readStringValue(coordinatorBaseNode.daemonId));
60967
+ if (coordinatorIsSelf) {
60968
+ try {
60969
+ const ff = await fastForwardMeshNode({
60970
+ meshId,
60971
+ nodeId: readStringValue(coordinatorBaseNode.id),
60972
+ workspace: coordinatorWorkspace,
60973
+ branch: baseBranch,
60974
+ mode: "merge",
60975
+ execute: true,
60976
+ trigger: "refine_post_push_catchup",
60977
+ allowAutoPublishSubmoduleMainCommits: mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true
60978
+ });
60979
+ return {
60980
+ mode: "local_fast_forward",
60981
+ coordinatorWorkspace,
60982
+ sameAsRepoRoot: coordinatorWorkspace === repoRoot,
60983
+ code: ff.code,
60984
+ executed: ff.executed,
60985
+ success: ff.success,
60986
+ ...ff.blockingReasons?.length ? { blockingReasons: ff.blockingReasons } : {}
60987
+ };
60988
+ } catch (e) {
60989
+ return { mode: "local_fast_forward", coordinatorWorkspace, error: e?.message || String(e) };
60990
+ }
60991
+ }
60992
+ try {
60993
+ queuePendingMeshCoordinatorEvent({
60994
+ event: "coordinator_catchup",
60995
+ meshId,
60996
+ nodeLabel: readStringValue(coordinatorBaseNode.id) || "coordinator-base",
60997
+ nodeId: readStringValue(coordinatorBaseNode.id),
60998
+ workspace: coordinatorWorkspace,
60999
+ metadataEvent: {
61000
+ source: "refine_post_push_coordinator_catchup",
61001
+ operation: "coordinator_catchup",
61002
+ baseBranch,
61003
+ coordinatorDaemonId,
61004
+ reason: "post_push_base_advanced"
61005
+ },
61006
+ queuedAt: Date.now(),
61007
+ targetCoordinatorDaemonId: coordinatorDaemonId
61008
+ });
61009
+ return { mode: "pending_marker_queued", coordinatorDaemonId, coordinatorWorkspace, baseBranch };
61010
+ } catch (e) {
61011
+ return { mode: "pending_marker_queued", coordinatorDaemonId, error: e?.message || String(e) };
61012
+ }
61013
+ }
60746
61014
  async function refineMergeAndFinalizeStage(self, ctx) {
60747
61015
  const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, sourceNode, validationSummary, patchEquivalence, submoduleReachability, mesh, refineStages, execFileAsync: execFileAsync4 } = ctx;
61016
+ const leaseKey = `${repoRoot}::${baseBranch}`;
61017
+ const leaseHolder = buildRefineJobKey(self, meshId, nodeId);
61018
+ if (self.refineBaseLeases.has(leaseKey) && self.refineBaseLeases.get(leaseKey) !== leaseHolder) {
61019
+ recordMeshRefineStage(refineStages, "base_lease", "skipped", Date.now(), {
61020
+ leaseKey,
61021
+ heldBy: self.refineBaseLeases.get(leaseKey),
61022
+ retryable: true
61023
+ });
61024
+ return { kind: "terminal", result: {
61025
+ success: false,
61026
+ code: "base_locked",
61027
+ convergenceStatus: "blocked_review",
61028
+ retryable: true,
61029
+ error: `Another refine holds the base lease for ${baseBranch} in this repo; retry after it completes.`,
61030
+ branch,
61031
+ into: baseBranch,
61032
+ validationSummary,
61033
+ patchEquivalence,
61034
+ submoduleReachability,
61035
+ refineStages,
61036
+ finalBranchConvergenceState: {
61037
+ branch,
61038
+ baseBranch,
61039
+ merged: false,
61040
+ removed: false,
61041
+ status: "blocked_review"
61042
+ }
61043
+ } };
61044
+ }
61045
+ self.refineBaseLeases.set(leaseKey, leaseHolder);
61046
+ try {
61047
+ return await runRefineMergeAndFinalizeLocked(self, ctx);
61048
+ } finally {
61049
+ if (self.refineBaseLeases.get(leaseKey) === leaseHolder) self.refineBaseLeases.delete(leaseKey);
61050
+ }
61051
+ }
61052
+ async function runRefineMergeAndFinalizeLocked(self, ctx) {
61053
+ const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, sourceNode, validationSummary, patchEquivalence, submoduleReachability, mesh, refineStages, execFileAsync: execFileAsync4 } = ctx;
61054
+ const casStarted = Date.now();
61055
+ let baseMoved = false;
61056
+ let liveBaseHead;
61057
+ try {
61058
+ await execFileAsync4("git", ["fetch", "origin", baseBranch], { cwd: repoRoot, encoding: "utf8" });
61059
+ const { stdout } = await execFileAsync4("git", ["rev-parse", `origin/${baseBranch}`], { cwd: repoRoot, encoding: "utf8" });
61060
+ liveBaseHead = stdout.trim();
61061
+ baseMoved = !!liveBaseHead && liveBaseHead !== baseHead;
61062
+ } catch {
61063
+ }
61064
+ if (baseMoved) {
61065
+ recordMeshRefineStage(refineStages, "base_cas", "failed", casStarted, {
61066
+ pinnedBaseHead: baseHead,
61067
+ liveBaseHead,
61068
+ retryable: true
61069
+ });
61070
+ return { kind: "terminal", result: {
61071
+ success: false,
61072
+ code: "base_moved",
61073
+ convergenceStatus: "blocked_review",
61074
+ retryable: true,
61075
+ error: `Base ${baseBranch} advanced from ${baseHead.slice(0, 7)} to ${(liveBaseHead || "").slice(0, 7)} after this node was validated; re-run refine to rebase onto and re-validate the new base.`,
61076
+ branch,
61077
+ into: baseBranch,
61078
+ pinnedBaseHead: baseHead,
61079
+ liveBaseHead,
61080
+ validationSummary,
61081
+ patchEquivalence,
61082
+ submoduleReachability,
61083
+ refineStages,
61084
+ finalBranchConvergenceState: {
61085
+ branch,
61086
+ baseBranch,
61087
+ merged: false,
61088
+ removed: false,
61089
+ status: "blocked_review"
61090
+ }
61091
+ } };
61092
+ }
61093
+ recordMeshRefineStage(refineStages, "base_cas", "passed", casStarted, { pinnedBaseHead: baseHead });
60748
61094
  let mergeResult;
60749
61095
  const mergeStarted = Date.now();
60750
61096
  try {
@@ -60756,16 +61102,33 @@ async function refineMergeAndFinalizeStage(self, ctx) {
60756
61102
  };
60757
61103
  recordMeshRefineStage(refineStages, "merge", "passed", mergeStarted, mergeResult);
60758
61104
  } catch (e) {
61105
+ const mergeOutput = `${e?.stdout || ""}
61106
+ ${e?.stderr || ""}`;
61107
+ const conflictPaths = [...mergeOutput.matchAll(/Merge conflict in (.+)/g)].map((m) => m[1].trim()).filter(Boolean);
61108
+ try {
61109
+ await execFileAsync4("git", ["merge", "--abort"], { cwd: repoRoot, encoding: "utf8" });
61110
+ } catch {
61111
+ }
60759
61112
  recordMeshRefineStage(refineStages, "merge", "failed", mergeStarted, {
60760
61113
  error: e?.message || String(e),
60761
61114
  stdout: truncateValidationOutput(e?.stdout),
60762
- stderr: truncateValidationOutput(e?.stderr)
61115
+ stderr: truncateValidationOutput(e?.stderr),
61116
+ ...conflictPaths.length ? { conflictPaths } : {}
60763
61117
  });
60764
61118
  return { kind: "terminal", result: {
60765
61119
  success: false,
60766
- error: `Merge failed (conflicts?): ${e.message}`,
61120
+ code: "merge_failed",
61121
+ convergenceStatus: "not_mergeable",
61122
+ error: conflictPaths.length ? `Merge failed \u2014 conflicts in ${conflictPaths.length} path(s): ${conflictPaths.join(", ")}. The branch cannot fast-forward-merge onto ${baseBranch}; resolve conflicts (rebase the branch onto the fetched base) and retry.` : `Merge failed (conflicts?): ${e?.message || String(e)}`,
61123
+ branch,
61124
+ into: baseBranch,
61125
+ ...conflictPaths.length ? { conflictPaths } : {},
60767
61126
  validationSummary,
60768
61127
  patchEquivalence,
61128
+ mergeResult: {
61129
+ stdout: truncateValidationOutput(e?.stdout),
61130
+ stderr: truncateValidationOutput(e?.stderr)
61131
+ },
60769
61132
  refineStages,
60770
61133
  finalBranchConvergenceState: {
60771
61134
  branch,
@@ -60821,6 +61184,93 @@ async function refineMergeAndFinalizeStage(self, ctx) {
60821
61184
  }
60822
61185
  } };
60823
61186
  }
61187
+ const requireApprovalForPush = mesh?.policy?.requireApprovalForPush ?? DEFAULT_MESH_POLICY.requireApprovalForPush;
61188
+ let pushResult;
61189
+ if (!requireApprovalForPush) {
61190
+ const pushStarted = Date.now();
61191
+ try {
61192
+ await execFileAsync4("git", ["push", "origin", baseBranch], { cwd: repoRoot, encoding: "utf8" });
61193
+ pushResult = { pushed: true, remote: "origin", branch: baseBranch, durationMs: Date.now() - pushStarted };
61194
+ recordMeshRefineStage(refineStages, "push", "passed", pushStarted, pushResult);
61195
+ } catch (e) {
61196
+ pushResult = {
61197
+ pushed: false,
61198
+ remote: "origin",
61199
+ branch: baseBranch,
61200
+ error: e?.message || String(e),
61201
+ stderr: e?.stderr,
61202
+ durationMs: Date.now() - pushStarted
61203
+ };
61204
+ recordMeshRefineStage(refineStages, "push", "failed", pushStarted, pushResult);
61205
+ return { kind: "terminal", result: {
61206
+ success: false,
61207
+ code: "push_failed",
61208
+ convergenceStatus: "blocked_review",
61209
+ retryable: true,
61210
+ merged: true,
61211
+ mergedLocal: true,
61212
+ pushed: false,
61213
+ error: `Refinery merged '${branch}' into local ${baseBranch} but the push to origin failed; the worktree and branch ref were preserved (NOT cleaned up) so the push can be retried. Run: git -C ${repoRoot} push origin ${baseBranch}`,
61214
+ branch,
61215
+ into: baseBranch,
61216
+ pushResult,
61217
+ pushCommand: `git push origin ${baseBranch}`,
61218
+ validationSummary,
61219
+ patchEquivalence,
61220
+ submoduleReachability,
61221
+ submoduleAlignment,
61222
+ mergeResult,
61223
+ refineStages,
61224
+ finalBranchConvergenceState: {
61225
+ branch: baseBranch,
61226
+ mergedBranch: branch,
61227
+ baseBranch,
61228
+ merged: true,
61229
+ pushed: false,
61230
+ removed: false,
61231
+ validation: "passed",
61232
+ patchEquivalence: "passed",
61233
+ submoduleAlignment: submoduleAlignment.status,
61234
+ status: "merged_push_failed",
61235
+ nextStep: `Retry the push (git -C ${repoRoot} push origin ${baseBranch}); then the worktree can be cleaned up.`
61236
+ }
61237
+ } };
61238
+ }
61239
+ } else {
61240
+ recordMeshRefineStage(refineStages, "push", "skipped", Date.now(), {
61241
+ reason: "require_approval_for_push"
61242
+ });
61243
+ return { kind: "terminal", result: {
61244
+ success: true,
61245
+ merged: true,
61246
+ mergedLocal: true,
61247
+ pushed: false,
61248
+ branch,
61249
+ into: baseBranch,
61250
+ validationSummary,
61251
+ patchEquivalence,
61252
+ submoduleReachability,
61253
+ submoduleAlignment,
61254
+ mergeResult,
61255
+ refineStages,
61256
+ pushReady: true,
61257
+ pushCommand: `git push origin ${baseBranch}`,
61258
+ pushNote: "requireApprovalForPush is enabled \u2014 the merge landed on the local base but was NOT pushed and the worktree was NOT cleaned up. Run the push (or approve it), then re-run refine/cleanup to remove the worktree.",
61259
+ finalBranchConvergenceState: {
61260
+ branch: baseBranch,
61261
+ mergedBranch: branch,
61262
+ baseBranch,
61263
+ merged: true,
61264
+ pushed: false,
61265
+ removed: false,
61266
+ validation: "passed",
61267
+ patchEquivalence: "passed",
61268
+ submoduleAlignment: submoduleAlignment.status,
61269
+ status: "merged_local_pending_push",
61270
+ nextStep: `Approve and run the push (git -C ${repoRoot} push origin ${baseBranch}); the worktree is retained until then.`
61271
+ }
61272
+ } };
61273
+ }
60824
61274
  const cleanupStarted = Date.now();
60825
61275
  const refineSessionCleanupMode = self.normalizeMeshSessionCleanupMode(
60826
61276
  mesh?.policy?.sessionCleanupOnNodeRemove
@@ -60848,10 +61298,10 @@ async function refineMergeAndFinalizeStage(self, ctx) {
60848
61298
  sessionCleanupMode: refineSessionCleanupMode,
60849
61299
  ...refineSessionIds && refineSessionIds.length > 0 ? { sessionIds: refineSessionIds } : {},
60850
61300
  inlineMesh: args?.inlineMesh,
60851
- // REFINE-CLEANUP: refine reaches cleanup only AFTER a verified merge
60852
- // convergence, so any residual worktree dirtiness here is incidental
60853
- // (e.g. a bootstrap lockfile rewrite) — never unmerged work. `force`
60854
- // sets requireClean=false so a plain-dirty worktree no longer aborts
61301
+ // REFINE-CLEANUP: refine reaches cleanup only AFTER a verified merge AND a
61302
+ // successful push (DS1), so any residual worktree dirtiness here is
61303
+ // incidental (e.g. a bootstrap lockfile rewrite) — never unmerged work.
61304
+ // `force` sets requireClean=false so a plain-dirty worktree no longer aborts
60855
61305
  // removal with merged_cleanup_failed. Branch-ref deletion still keys off
60856
61306
  // mergeConvergence (NOT the force flag), so no merged work can be lost.
60857
61307
  force: true
@@ -60868,7 +61318,7 @@ async function refineMergeAndFinalizeStage(self, ctx) {
60868
61318
  appendLedgerEntry2(meshId, {
60869
61319
  kind: "node_removed",
60870
61320
  nodeId,
60871
- payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence, submoduleReachability, submoduleAlignment }
61321
+ payload: { refined: true, mergedBranch: branch, into: baseBranch, pushed: true, validationSummary, patchEquivalence, submoduleReachability, submoduleAlignment }
60872
61322
  });
60873
61323
  recordMeshRefineStage(refineStages, "ledger", "passed", ledgerStarted);
60874
61324
  } catch (e) {
@@ -60880,21 +61330,24 @@ async function refineMergeAndFinalizeStage(self, ctx) {
60880
61330
  mergedBranch: branch,
60881
61331
  baseBranch,
60882
61332
  merged: true,
61333
+ pushed: true,
60883
61334
  removed: removeResult?.success !== false,
60884
61335
  validation: "passed",
60885
61336
  patchEquivalence: "passed",
60886
61337
  submoduleAlignment: submoduleAlignment.status,
60887
- status: removeResult?.success === false ? "merged_cleanup_failed" : "merged"
61338
+ status: removeResult?.success === false ? "merged_cleanup_failed" : "merged_pushed"
60888
61339
  };
60889
61340
  if (removeResult?.success === false) {
60890
61341
  return { kind: "terminal", result: {
60891
61342
  success: false,
60892
61343
  code: "cleanup_failed",
60893
- error: "Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.",
61344
+ error: "Refinery merge + push completed but worktree cleanup failed; the change is on origin \u2014 manual worktree cleanup/retry is required.",
60894
61345
  merged: true,
61346
+ pushed: true,
60895
61347
  branch,
60896
61348
  into: baseBranch,
60897
61349
  removeResult,
61350
+ pushResult,
60898
61351
  validationSummary,
60899
61352
  patchEquivalence,
60900
61353
  submoduleReachability,
@@ -60905,33 +61358,35 @@ async function refineMergeAndFinalizeStage(self, ctx) {
60905
61358
  finalBranchConvergenceState
60906
61359
  } };
60907
61360
  }
60908
- const requireApprovalForPush = mesh?.policy?.requireApprovalForPush ?? DEFAULT_MESH_POLICY.requireApprovalForPush;
60909
- let pushResult;
60910
- if (!requireApprovalForPush) {
60911
- const pushStarted = Date.now();
60912
- try {
60913
- await execFileAsync4("git", ["push", "origin", baseBranch], { cwd: repoRoot, encoding: "utf8" });
60914
- pushResult = { pushed: true, remote: "origin", branch: baseBranch, durationMs: Date.now() - pushStarted };
60915
- recordMeshRefineStage(refineStages, "push", "passed", pushStarted, pushResult);
60916
- finalBranchConvergenceState.status = "merged_pushed";
60917
- } catch (e) {
60918
- pushResult = {
60919
- pushed: false,
60920
- remote: "origin",
60921
- branch: baseBranch,
60922
- error: e?.message || String(e),
60923
- stderr: e?.stderr,
60924
- durationMs: Date.now() - pushStarted
60925
- };
60926
- recordMeshRefineStage(refineStages, "push", "failed", pushStarted, pushResult);
61361
+ let coordinatorCatchup;
61362
+ try {
61363
+ coordinatorCatchup = await requestCoordinatorLocalCatchup(self, {
61364
+ meshId,
61365
+ ctx,
61366
+ mesh,
61367
+ baseBranch,
61368
+ repoRoot
61369
+ });
61370
+ if (coordinatorCatchup) {
61371
+ recordMeshRefineStage(refineStages, "coordinator_catchup", "passed", Date.now(), coordinatorCatchup);
60927
61372
  }
61373
+ } catch {
60928
61374
  }
61375
+ const cleanupBranchRefWarning = typeof removeResult?.branchRefWarning === "string" ? removeResult.branchRefWarning : void 0;
61376
+ const cleanupResidueWarning = typeof removeResult?.residueWarning === "string" ? removeResult.residueWarning : void 0;
61377
+ const cleanupBranchRefDeleted = typeof removeResult?.worktreeCleanup?.branchRefDeleted === "boolean" ? removeResult.worktreeCleanup.branchRefDeleted : void 0;
60929
61378
  return { kind: "terminal", result: {
60930
61379
  success: true,
60931
61380
  merged: true,
61381
+ pushed: true,
60932
61382
  branch,
60933
61383
  into: baseBranch,
60934
61384
  removeResult,
61385
+ pushResult,
61386
+ ...coordinatorCatchup ? { coordinatorCatchup } : {},
61387
+ ...cleanupBranchRefWarning ? { branchRefWarning: cleanupBranchRefWarning } : {},
61388
+ ...cleanupResidueWarning ? { residueWarning: cleanupResidueWarning } : {},
61389
+ ...cleanupBranchRefDeleted !== void 0 ? { branchRefDeleted: cleanupBranchRefDeleted } : {},
60935
61390
  validationSummary,
60936
61391
  patchEquivalence,
60937
61392
  submoduleReachability,
@@ -60939,13 +61394,7 @@ async function refineMergeAndFinalizeStage(self, ctx) {
60939
61394
  mergeResult,
60940
61395
  refineStages,
60941
61396
  ...ledgerError ? { ledgerError } : {},
60942
- finalBranchConvergenceState,
60943
- // Push outcome or readiness info for coordinator.
60944
- ...pushResult ? { pushResult } : {
60945
- pushReady: true,
60946
- pushCommand: `git push origin ${baseBranch}`,
60947
- pushNote: "requireApprovalForPush is enabled \u2014 run the push command or obtain user approval before pushing."
60948
- }
61397
+ finalBranchConvergenceState
60949
61398
  } };
60950
61399
  }
60951
61400
  async function batchRefineMeshNodes(self, meshId, requestedNodeIds, args) {
@@ -61100,29 +61549,34 @@ async function batchRefineMeshNodes(self, meshId, requestedNodeIds, args) {
61100
61549
  }
61101
61550
  return runMeshRefineBatchConvergence(self, meshId, orderedNodes, ordering, args);
61102
61551
  }
61552
+ var RETRYABLE_BASE_MOVEMENT_CODES = /* @__PURE__ */ new Set(["base_moved", "base_locked"]);
61553
+ function classifyBatchNodeConvergence(result) {
61554
+ const code = typeof result.code === "string" ? result.code : "";
61555
+ const stage = Array.isArray(result.refineStages) ? result.refineStages.filter((s2) => s2.status === "failed").map((s2) => s2.stage).filter(Boolean).pop() : void 0;
61556
+ let convergence;
61557
+ if (code === "already_merged" && result.alreadyMergedViaOtherPath) {
61558
+ convergence = "skipped_patch_equivalent";
61559
+ } else if (result.success === true) {
61560
+ convergence = "merged_to_main";
61561
+ } else if (code === "merge_failed" || stage === "merge") {
61562
+ convergence = "not_mergeable";
61563
+ } else {
61564
+ convergence = "blocked_review";
61565
+ }
61566
+ const retryable = convergence === "blocked_review" && (result.retryable === true || RETRYABLE_BASE_MOVEMENT_CODES.has(code));
61567
+ return { convergence, code, retryable, ...stage ? { stage } : {} };
61568
+ }
61103
61569
  async function runMeshRefineBatchConvergence(self, meshId, orderedNodes, ordering, args) {
61104
- const results = [];
61105
- for (const node of orderedNodes) {
61570
+ const refineOne = async (node) => {
61106
61571
  let result;
61107
61572
  try {
61108
61573
  result = await executeMeshRefineNodeSynchronously(self, meshId, node.id, args);
61109
61574
  } catch (e) {
61110
61575
  result = { success: false, error: e?.message || String(e) };
61111
61576
  }
61112
- const code = typeof result.code === "string" ? result.code : "";
61113
- let convergence;
61114
- if (code === "already_merged" && result.alreadyMergedViaOtherPath) {
61115
- convergence = "skipped_patch_equivalent";
61116
- } else if (result.success === true) {
61117
- convergence = "merged_to_main";
61118
- } else if (code === "merge_failed") {
61119
- convergence = "not_mergeable";
61120
- } else {
61121
- convergence = "blocked_review";
61122
- }
61577
+ const { convergence, code, stage, retryable } = classifyBatchNodeConvergence(result);
61123
61578
  const fbcs = result.finalBranchConvergenceState && typeof result.finalBranchConvergenceState === "object" ? result.finalBranchConvergenceState : void 0;
61124
- const stage = Array.isArray(result.refineStages) ? result.refineStages.filter((s2) => s2.status === "failed").map((s2) => s2.stage).filter(Boolean).pop() : void 0;
61125
- results.push({
61579
+ return {
61126
61580
  nodeId: node.id,
61127
61581
  workspace: node.workspace,
61128
61582
  convergence,
@@ -61130,14 +61584,30 @@ async function runMeshRefineBatchConvergence(self, meshId, orderedNodes, orderin
61130
61584
  ...typeof result.blockedReason === "string" ? { reason: result.blockedReason } : {},
61131
61585
  ...stage ? { stage } : {},
61132
61586
  ...typeof result.error === "string" ? { error: result.error } : {},
61587
+ ...retryable ? { retryable: true } : {},
61133
61588
  ...fbcs ? { finalBranchConvergenceState: fbcs } : {}
61134
- });
61589
+ };
61590
+ };
61591
+ const results = [];
61592
+ const retryQueue = [];
61593
+ for (const node of orderedNodes) {
61594
+ const outcome = await refineOne(node);
61595
+ results.push(outcome);
61596
+ if (outcome.retryable) retryQueue.push(node);
61597
+ }
61598
+ for (const node of retryQueue) {
61599
+ const idx = results.findIndex((r) => r.nodeId === node.id);
61600
+ const retried = await refineOne(node);
61601
+ retried.retried = true;
61602
+ if (idx >= 0) results[idx] = retried;
61603
+ else results.push(retried);
61135
61604
  }
61136
61605
  const summary = {
61137
61606
  merged: results.filter((r) => r.convergence === "merged_to_main").length,
61138
61607
  skipped: results.filter((r) => r.convergence === "skipped_patch_equivalent").length,
61139
61608
  blocked: results.filter((r) => r.convergence === "blocked_review").length,
61140
- notMergeable: results.filter((r) => r.convergence === "not_mergeable").length
61609
+ notMergeable: results.filter((r) => r.convergence === "not_mergeable").length,
61610
+ ...retryQueue.length ? { retried: retryQueue.length } : {}
61141
61611
  };
61142
61612
  const allConverged = summary.blocked === 0 && summary.notMergeable === 0;
61143
61613
  return {
@@ -61350,7 +61820,7 @@ async function finishMeshRefineJob(self, handle, args) {
61350
61820
  }
61351
61821
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
61352
61822
  const refineCode = typeof result.code === "string" ? result.code : "";
61353
- const refineTerminalKind = result.success === true ? "completed" : refineCode === "blocked_review" ? "blocked_review" : refineCode === "validation_failed" || refineCode === "validation_dependencies_missing" ? "validation_failed" : refineCode === "submodule_reachability_failed" ? "submodule_reachability_failed" : refineCode === "merge_failed" || refineCode === "patch_equivalence_failed" || refineCode === "needs_rebase" || refineCode === "needs_rebase_with_conflicts" ? "merge_failed" : refineCode === "cleanup_failed" ? "cleanup_failed" : "merge_failed";
61823
+ const refineTerminalKind = result.success === true ? "completed" : refineCode === "blocked_review" ? "blocked_review" : refineCode === "validation_failed" || refineCode === "validation_dependencies_missing" || refineCode === "missing_dependencies" || refineCode === "dependency_bootstrap_failed" || refineCode === "spawn_resolution_failed" || refineCode === "validation_unavailable" ? "validation_failed" : refineCode === "submodule_reachability_failed" ? "submodule_reachability_failed" : refineCode === "merge_failed" || refineCode === "patch_equivalence_failed" || refineCode === "needs_rebase" || refineCode === "needs_rebase_with_conflicts" ? "merge_failed" : refineCode === "cleanup_failed" ? "cleanup_failed" : "merge_failed";
61354
61824
  const isTerminalSuccess = refineTerminalKind === "completed";
61355
61825
  const blockerContext = isTerminalSuccess ? void 0 : (() => {
61356
61826
  const code = typeof result.code === "string" ? result.code : refineTerminalKind;
@@ -61381,9 +61851,12 @@ async function finishMeshRefineJob(self, handle, args) {
61381
61851
  }
61382
61852
  if (stage === "validation" && result.validationSummary) {
61383
61853
  const vs = result.validationSummary;
61854
+ const diagnostics = extractValidationFailureDiagnostics(vs);
61384
61855
  ctx.details = {
61385
61856
  failureCode: vs.failureCode,
61386
- commandsRun: Array.isArray(vs.commandsRun) ? vs.commandsRun.length : void 0
61857
+ failureKind: vs.failureKind,
61858
+ commandsRun: Array.isArray(vs.commandsRun) ? vs.commandsRun.length : void 0,
61859
+ ...diagnostics ? { failure: diagnostics } : {}
61387
61860
  };
61388
61861
  }
61389
61862
  return ctx;
@@ -62418,6 +62891,15 @@ var DaemonCommandRouter = class {
62418
62891
  runningRefineBatchJobs = /* @__PURE__ */ new Map();
62419
62892
  /** Terminal async batch Refinery jobs preserve the last batch outcome for late readers. */
62420
62893
  terminalRefineBatchJobs = /* @__PURE__ */ new Map();
62894
+ /**
62895
+ * DS2: in-process refinement leases keyed by `${repoRoot}::${baseBranch}`. Serialize
62896
+ * the base-mutating window (candidate-SHA pin → merge → push) of concurrent single-node
62897
+ * refines that target the SAME base branch in the same repo, so two refines cannot both
62898
+ * validate against one baseHead and then race their merges (the base-movement race). The
62899
+ * batch path is already sequential, so this only matters for overlapping single-node
62900
+ * async jobs. Value = the meshId:nodeId job key holding the lease (for diagnostics).
62901
+ */
62902
+ refineBaseLeases = /* @__PURE__ */ new Map();
62421
62903
  constructor(deps) {
62422
62904
  this.deps = deps;
62423
62905
  }