@adhdev/daemon-core 0.9.82-rc.486 → 0.9.82-rc.488

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.
@@ -13,6 +13,20 @@ export declare function buildRefineJobHandle(self: DaemonCommandRouter, args: {
13
13
  retryOfJobId?: string;
14
14
  coordinatorDaemonId?: string;
15
15
  }): MeshRefineJobHandle;
16
+ /**
17
+ * Slim the terminal-stage refine result down to the fields a coordinator needs to
18
+ * decide next-step, dropping the heavy per-command / per-entry detail.
19
+ *
20
+ * The full `CommandRouterResult` (with `validationSummary.commandsRun[]` carrying
21
+ * per-command stdout/stderr, `rejectedCommands`, `suggestions`, `suggestedConfig`,
22
+ * the full `patchEquivalence`, and `submoduleReachability.entries[]`/`.unreachable[]`)
23
+ * routinely exceeds 70KB and overflows the coordinator token limit when it rides on a
24
+ * `mesh_wait_events` payload. The full detail is still persisted verbatim to the ledger
25
+ * (`appendRefineJobLedger`) and `terminalRefineJobs`, so slimming only the EVENT loses
26
+ * nothing — the coordinator can pull the full record on demand via
27
+ * `evidence.ledgerCommand` / `taskHistoryKind`.
28
+ */
29
+ export declare function slimRefineEventResult(result: Record<string, unknown>): Record<string, unknown>;
16
30
  export declare function queueRefineJobEvent(self: DaemonCommandRouter, event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void;
17
31
  export declare function appendRefineJobLedger(self: DaemonCommandRouter, kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void>;
18
32
  /**
@@ -67,6 +67,29 @@ export interface GitStatusOptions {
67
67
  */
68
68
  export declare const GIT_FETCH_THROTTLE_MS = 30000;
69
69
  export declare function getGitRepoStatus(workspace: string, options?: GitStatusOptions): Promise<GitRepoStatus>;
70
+ /** Coarse change-impact verdict produced from a changed-file list. */
71
+ export interface ChangedPackageClassification {
72
+ isDaemonAffecting: boolean;
73
+ affectedPackages: string[];
74
+ }
75
+ /**
76
+ * Ref-parameterized change-impact classification for a repo/worktree, reusing the
77
+ * exact daemon-vs-web bucketing that the stale-build detector uses — but over an
78
+ * arbitrary `fromRef..toRef` range (e.g. a refine base head → branch head) instead
79
+ * of the live daemon's build commit → HEAD, and WITHOUT any daemonBuildInfo caching.
80
+ *
81
+ * Policy is resolved the same way as getGitRepoStatus: an explicit
82
+ * `options.changeImpactConfig` wins; otherwise the repo's `.adhdev/change-impact.*`
83
+ * is auto-loaded; otherwise the built-in ADHDev default policy applies. The
84
+ * classification uses `git diff --name-only fromRef..toRef`.
85
+ *
86
+ * FAIL-OPEN on error: if the diff can't be collected (bad ref, not a repo), the
87
+ * caller should treat "no verdict" as "run everything" — so we throw rather than
88
+ * returning a misleading benign verdict. Callers wrap this in try/catch and leave
89
+ * changeImpact undefined on failure. Unclassified/new packages still default to
90
+ * isDaemonAffecting:true (never silently skipped).
91
+ */
92
+ export declare function classifyChangedPackages(repoPath: string, fromRef: string, toRef: string, options?: GitStatusOptions): Promise<ChangedPackageClassification>;
70
93
  interface ParsedPorcelainStatus {
71
94
  branch: string | null;
72
95
  /** Full HEAD object id from `# branch.oid`, or null when detached/unborn. */
package/dist/index.js 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 ? "cae0f42043ded25e4d3bed93d9f658035c09e007" : void 0) ?? "unknown";
418
- const commitShort = readInjected(true ? "cae0f420" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
419
- const version = readInjected(true ? "0.9.82-rc.486" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
420
- const builtAt = readInjected(true ? "2026-07-09T23:19:53.642Z" : void 0);
417
+ const commit = readInjected(true ? "a659fcbbe9641240de42b95804a16b8852c5b4eb" : void 0) ?? "unknown";
418
+ const commitShort = readInjected(true ? "a659fcbb" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
419
+ const version = readInjected(true ? "0.9.82-rc.488" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
420
+ const builtAt = readInjected(true ? "2026-07-10T03:46:52.780Z" : void 0);
421
421
  cached = builtAt ? { commit, commitShort, version, builtAt } : { commit, commitShort, version };
422
422
  return cached;
423
423
  }
@@ -789,30 +789,41 @@ function isNonRuntimeRootFile(file, policy) {
789
789
  }
790
790
  return false;
791
791
  }
792
+ function classifyChangedFileList(files, policy) {
793
+ if (files.length === 0) {
794
+ return { isDaemonAffecting: true, affectedPackages: [] };
795
+ }
796
+ const pkgs = /* @__PURE__ */ new Set();
797
+ let sawRuntimeAmbiguousNonPackage = false;
798
+ for (const file of files) {
799
+ const match = file.match(/(?:^|\/)packages\/([^/]+)\//);
800
+ if (!match) {
801
+ if (!isNonRuntimeRootFile(file, policy)) sawRuntimeAmbiguousNonPackage = true;
802
+ continue;
803
+ }
804
+ pkgs.add(match[1]);
805
+ }
806
+ const affectedPackages = [...pkgs].sort();
807
+ const allBenign = !sawRuntimeAmbiguousNonPackage && affectedPackages.every((p) => policy.webOnlyPackages.has(p) && !policy.daemonRuntimePackages.has(p));
808
+ return { isDaemonAffecting: !allBenign, affectedPackages };
809
+ }
792
810
  async function classifyDaemonBuildChange(repoPath, buildCommit, options, policy) {
793
811
  try {
794
812
  const diff = await runGit(repoPath, ["diff", "--name-only", `${buildCommit}..HEAD`], options);
795
813
  const files = diff.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
796
- if (files.length === 0) {
797
- return { isDaemonAffecting: true, affectedPackages: [] };
798
- }
799
- const pkgs = /* @__PURE__ */ new Set();
800
- let sawRuntimeAmbiguousNonPackage = false;
801
- for (const file of files) {
802
- const match = file.match(/(?:^|\/)packages\/([^/]+)\//);
803
- if (!match) {
804
- if (!isNonRuntimeRootFile(file, policy)) sawRuntimeAmbiguousNonPackage = true;
805
- continue;
806
- }
807
- pkgs.add(match[1]);
808
- }
809
- const affectedPackages = [...pkgs].sort();
810
- const allBenign = !sawRuntimeAmbiguousNonPackage && affectedPackages.every((p) => policy.webOnlyPackages.has(p) && !policy.daemonRuntimePackages.has(p));
811
- return { isDaemonAffecting: !allBenign, affectedPackages };
814
+ return classifyChangedFileList(files, policy);
812
815
  } catch {
813
816
  return { isDaemonAffecting: true, affectedPackages: [] };
814
817
  }
815
818
  }
819
+ async function classifyChangedPackages(repoPath, fromRef, toRef, options = {}) {
820
+ const repo = await resolveGitRepository(repoPath, options);
821
+ const { config } = resolveChangeImpactConfigForRepo(repo.repoRoot, options);
822
+ const policy = resolveChangeImpactPolicy(config);
823
+ const diff = await runGit(repoPath, ["diff", "--name-only", `${fromRef}..${toRef}`], options);
824
+ const files = diff.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
825
+ return classifyChangedFileList(files, policy);
826
+ }
816
827
  function resolveChangeImpactConfigForRepo(repoRoot, options) {
817
828
  if (options.changeImpactConfig === null) {
818
829
  return { config: null, sourceKey: "forced-default" };
@@ -6164,6 +6175,7 @@ function reclaimStrandedAssignedTask(meshId, taskId, opts) {
6164
6175
  delete entry.assignedSessionId;
6165
6176
  delete entry.assignedProviderType;
6166
6177
  delete entry.dispatchTimestamp;
6178
+ entry.dispatchNonce = (entry.dispatchNonce || 0) + 1;
6167
6179
  entry.strandedReclaimCount = reclaims;
6168
6180
  entry.updatedAt = now;
6169
6181
  endTaskDispatchInFlight(meshId, taskId);
@@ -7188,6 +7200,7 @@ var init_mesh_runtime_store = __esm({
7188
7200
  entry.assignedSessionId = sessionId;
7189
7201
  if (providerType) entry.assignedProviderType = providerType;
7190
7202
  entry.dispatchTimestamp = now;
7203
+ entry.dispatchNonce = (entry.dispatchNonce || 0) + 1;
7191
7204
  entry.updatedAt = now;
7192
7205
  this.db.prepare(`
7193
7206
  UPDATE mesh_queue SET
@@ -15646,6 +15659,10 @@ function tryAssignQueueTask(components, meshId, nodeId, sessionId, providerType)
15646
15659
  meshId,
15647
15660
  nodeId,
15648
15661
  taskId: task.id,
15662
+ // REDRIVE-DUP: carry the current dispatch nonce so the worker can echo it
15663
+ // back on generating_started; a reclaim bumps this row's nonce, making an
15664
+ // already-in-flight stale inject rejectable on arrival.
15665
+ ...typeof task.dispatchNonce === "number" ? { dispatchNonce: task.dispatchNonce } : {},
15649
15666
  ...localDaemonIdForDispatch ? { coordinatorDaemonId: localDaemonIdForDispatch } : {},
15650
15667
  ...sourceCoordinatorSessionId ? { coordinatorSessionId: sourceCoordinatorSessionId } : {}
15651
15668
  }
@@ -15704,6 +15721,8 @@ function tryAssignQueueTask(components, meshId, nodeId, sessionId, providerType)
15704
15721
  meshId,
15705
15722
  nodeId,
15706
15723
  taskId: task.id,
15724
+ // REDRIVE-DUP: carry the current dispatch nonce (see remote branch above).
15725
+ ...typeof task.dispatchNonce === "number" ? { dispatchNonce: task.dispatchNonce } : {},
15707
15726
  ...localCoordinatorDaemonId() ? { coordinatorDaemonId: localCoordinatorDaemonId() } : {},
15708
15727
  ...readNonEmptyString2(task.sourceCoordinatorSessionId) ? { coordinatorSessionId: readNonEmptyString2(task.sourceCoordinatorSessionId) } : {}
15709
15728
  }
@@ -19687,6 +19706,42 @@ function sourceWorkerAutoApproves(components, sessionId) {
19687
19706
  return false;
19688
19707
  }
19689
19708
  }
19709
+ function stopStaleMeshWorker(components, args) {
19710
+ const { meshId, sessionId, providerType } = args;
19711
+ const stopArgs = {
19712
+ targetSessionId: sessionId,
19713
+ ...providerType ? { cliType: providerType } : {},
19714
+ mode: "hard",
19715
+ reason: "stale_mesh_dispatch_reclaimed"
19716
+ };
19717
+ try {
19718
+ const isLocal = components.cliManager?.adapters?.has?.(sessionId) === true;
19719
+ if (isLocal) {
19720
+ if (!stopArgs.cliType) {
19721
+ const localType = components.cliManager?.adapters?.get?.(sessionId)?.cliType;
19722
+ if (localType) stopArgs.cliType = localType;
19723
+ }
19724
+ Promise.resolve(components.cliManager?.handleCliCommand?.("stop_cli", stopArgs)).catch((e) => LOG.warn("MeshQueue", `Local stop of stale worker ${sessionId} failed: ${e?.message || e}`));
19725
+ return;
19726
+ }
19727
+ let daemonId = args.daemonId;
19728
+ if (!daemonId && args.nodeId) {
19729
+ try {
19730
+ const mesh = getMeshWithCache(components, meshId);
19731
+ const node = mesh?.nodes?.find((n) => meshNodeIdMatches(n, args.nodeId));
19732
+ daemonId = node ? readMeshNodeDaemonId(node) || void 0 : void 0;
19733
+ } catch {
19734
+ }
19735
+ }
19736
+ if (daemonId && components.dispatchMeshCommand) {
19737
+ Promise.resolve(components.dispatchMeshCommand(daemonId, "stop_cli", stopArgs)).catch((e) => LOG.warn("MeshQueue", `Remote stop of stale worker ${sessionId} on daemon ${daemonId} failed: ${e?.message || e}`));
19738
+ } else {
19739
+ LOG.warn("MeshQueue", `Cannot stop stale worker ${sessionId}: no local adapter and no resolvable remote daemon id (node ${args.nodeId ?? "?"}). Ack already rejected \u2014 task will re-strand-and-fail if the worker completes.`);
19740
+ }
19741
+ } catch (e) {
19742
+ LOG.warn("MeshQueue", `stopStaleMeshWorker error for ${sessionId}: ${e?.message || e}`);
19743
+ }
19744
+ }
19690
19745
  function injectMeshSystemMessage(components, args) {
19691
19746
  const eventSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
19692
19747
  const eventNodeId = readNonEmptyString2(args.nodeId) || readNonEmptyString2(args.metadataEvent.meshNodeId);
@@ -19926,6 +19981,35 @@ function injectMeshSystemMessage(components, args) {
19926
19981
  }
19927
19982
  if (sessionId) {
19928
19983
  const startedTaskId = readNonEmptyString2(args.metadataEvent.taskId) || void 0;
19984
+ const startedNonce = typeof args.metadataEvent.dispatchNonce === "number" ? args.metadataEvent.dispatchNonce : void 0;
19985
+ if (startedTaskId && startedNonce !== void 0) {
19986
+ const currentRow = (() => {
19987
+ try {
19988
+ return MeshRuntimeStore.getInstance().findQueueEntryById(args.meshId, startedTaskId);
19989
+ } catch {
19990
+ return null;
19991
+ }
19992
+ })();
19993
+ const currentNonce = typeof currentRow?.dispatchNonce === "number" ? currentRow.dispatchNonce : void 0;
19994
+ if (currentNonce !== void 0 && startedNonce < currentNonce) {
19995
+ LOG.warn("MeshQueue", `Rejecting stale mesh dispatch: task ${startedTaskId} generating_started from session ${sessionId} (node ${nodeId ?? "?"}) carries dispatchNonce ${startedNonce} < current ${currentNonce} \u2014 the task was reclaimed and re-dispatched; stopping this worker to prevent duplicate execution.`);
19996
+ traceMeshEventDrop("stale_dispatch_nonce_rejected", {
19997
+ taskId: startedTaskId,
19998
+ sessionId,
19999
+ nodeId,
20000
+ meshId: args.meshId,
20001
+ event: "agent:generating_started"
20002
+ }, `nonce ${startedNonce} < ${currentNonce}`);
20003
+ stopStaleMeshWorker(components, {
20004
+ meshId: args.meshId,
20005
+ sessionId,
20006
+ nodeId,
20007
+ providerType: readNonEmptyString2(args.metadataEvent.providerType) || readNonEmptyString2(args.metadataEvent.cliType),
20008
+ daemonId: readNonEmptyString2(args.metadataEvent.sourceDaemonId) || readNonEmptyString2(args.metadataEvent.daemonId)
20009
+ });
20010
+ return { success: true, forwarded: 0, suppressed: true, staleDispatchRejected: true };
20011
+ }
20012
+ }
19929
20013
  if (startedTaskId) {
19930
20014
  updateDirectDispatchStatus(args.meshId, sessionId, "acked", startedTaskId);
19931
20015
  } else if (sessionHasActiveAssignment(args.meshId, sessionId)) {
@@ -20462,6 +20546,7 @@ var init_mesh_event_forwarding = __esm({
20462
20546
  init_dist();
20463
20547
  init_mesh_events_stale();
20464
20548
  init_mesh_task_inflight();
20549
+ init_mesh_node_identity();
20465
20550
  init_mesh_events_utils();
20466
20551
  init_mesh_event_classify();
20467
20552
  init_mesh_queue_assignment();
@@ -45966,6 +46051,10 @@ var CliProviderInstance = class _CliProviderInstance {
45966
46051
  // shares this daemon. See isMeshOwnedDelegateSession's post-detach gate.
45967
46052
  ...assignment.nodeId ? { meshNodeId: assignment.nodeId, meshLastNodeId: assignment.nodeId } : {},
45968
46053
  ...assignment.taskId ? { meshActiveTaskId: assignment.taskId } : {},
46054
+ // REDRIVE-DUP: task-level dispatch nonce, echoed on generating_started so the
46055
+ // coordinator can reject a stale (reclaimed) dispatch. Cleared with meshActiveTaskId
46056
+ // on detach so a subsequent unrelated turn never re-echoes a prior task's nonce.
46057
+ ...typeof assignment.dispatchNonce === "number" ? { meshActiveDispatchNonce: assignment.dispatchNonce } : {},
45969
46058
  ...assignment.coordinatorDaemonId ? { meshCoordinatorDaemonId: assignment.coordinatorDaemonId } : {},
45970
46059
  // Session-level routing anchor: the originating coordinator session, so this
45971
46060
  // worker's completion events route back to the exact session that dispatched it.
@@ -46001,15 +46090,17 @@ var CliProviderInstance = class _CliProviderInstance {
46001
46090
  if (!this.settings.meshNodeFor && !this.settings.meshActiveTaskId && !this.settings.meshNodeId) return;
46002
46091
  if (this.settings.launchedByCoordinator === true) {
46003
46092
  if (!this.settings.meshActiveTaskId) return;
46004
- const { meshActiveTaskId: meshActiveTaskId2, ...rest2 } = this.settings;
46093
+ const { meshActiveTaskId: meshActiveTaskId2, meshActiveDispatchNonce: meshActiveDispatchNonce2, ...rest2 } = this.settings;
46005
46094
  void meshActiveTaskId2;
46095
+ void meshActiveDispatchNonce2;
46006
46096
  this.settings = rest2;
46007
46097
  this.adapter.updateRuntimeSettings?.(this.settings);
46008
46098
  return;
46009
46099
  }
46010
- const { meshNodeFor, meshNodeId, meshActiveTaskId, ...rest } = this.settings;
46100
+ const { meshNodeFor, meshNodeId, meshActiveTaskId, meshActiveDispatchNonce, ...rest } = this.settings;
46011
46101
  void meshNodeFor;
46012
46102
  void meshActiveTaskId;
46103
+ void meshActiveDispatchNonce;
46013
46104
  const lastNodeId = typeof meshNodeId === "string" && meshNodeId.trim() ? meshNodeId.trim() : typeof rest.meshLastNodeId === "string" && rest.meshLastNodeId.trim() ? rest.meshLastNodeId.trim() : void 0;
46014
46105
  this.settings = lastNodeId ? { ...rest, meshLastNodeId: lastNodeId } : rest;
46015
46106
  this.adapter.updateRuntimeSettings?.(this.settings);
@@ -47498,6 +47589,9 @@ var CliProviderInstance = class _CliProviderInstance {
47498
47589
  const resolved = this.completingTurnTaskId();
47499
47590
  if (resolved) enrichedEvent.taskId = resolved;
47500
47591
  }
47592
+ if (enrichedEvent.dispatchNonce === void 0 && typeof this.settings.meshActiveDispatchNonce === "number") {
47593
+ enrichedEvent.dispatchNonce = this.settings.meshActiveDispatchNonce;
47594
+ }
47501
47595
  }
47502
47596
  if (this.context?.emitProviderEvent) {
47503
47597
  this.context.emitProviderEvent(enrichedEvent);
@@ -50384,6 +50478,10 @@ Run 'adhdev doctor' for detailed diagnostics.`
50384
50478
  meshId: meshContext.meshId,
50385
50479
  ...typeof meshContext.nodeId === "string" && meshContext.nodeId ? { nodeId: meshContext.nodeId } : {},
50386
50480
  ...typeof meshContext.taskId === "string" && meshContext.taskId ? { taskId: meshContext.taskId } : {},
50481
+ // REDRIVE-DUP: carry the dispatch nonce onto the worker session so
50482
+ // its generating_started event echoes it back for the coordinator's
50483
+ // stale-nonce guard.
50484
+ ...typeof meshContext.dispatchNonce === "number" ? { dispatchNonce: meshContext.dispatchNonce } : {},
50387
50485
  ...typeof meshContext.coordinatorDaemonId === "string" && meshContext.coordinatorDaemonId ? { coordinatorDaemonId: meshContext.coordinatorDaemonId } : {}
50388
50486
  });
50389
50487
  } catch {
@@ -58219,6 +58317,7 @@ function orderMeshRefineBatchNodes(changeAreas) {
58219
58317
 
58220
58318
  // src/commands/router-refine.ts
58221
58319
  init_repo_mesh_types();
58320
+ init_git_status();
58222
58321
  init_mesh_node_identity();
58223
58322
 
58224
58323
  // src/mesh/mesh-refine-gates.ts
@@ -59159,6 +59258,41 @@ async function runMeshRefineValidationGate(mesh, workspace, opts) {
59159
59258
  if (fs31.existsSync((0, import_path14.join)(cwd, "node_modules"))) return false;
59160
59259
  return ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb", "bun.lock"].some((lock) => fs31.existsSync((0, import_path14.join)(cwd, lock)));
59161
59260
  };
59261
+ const needsNodeModules = (candidate, cwd) => isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd);
59262
+ const isDaemonScopedCommand = (candidate) => {
59263
+ const haystack = [candidate.command, ...candidate.args || [], candidate.displayCommand || ""].join(" ").toLowerCase();
59264
+ if (candidate.category === "typecheck") return false;
59265
+ if (/\btypecheck\b/.test(haystack)) return false;
59266
+ if (/\bweb-core\b|\bweb-cloud\b|\bweb-standalone\b|\btest:web\b/.test(haystack)) return false;
59267
+ return /\bdaemon-core\b|\bdaemon-cloud\b|\btest:daemon\b|check-vendor-drift/.test(haystack);
59268
+ };
59269
+ const scopeUnaffectedDaemon = opts?.changeImpact?.isDaemonAffecting === false;
59270
+ const skippedDaemonCommands = [];
59271
+ const commandsToRun = [];
59272
+ for (const candidate of selection.commands) {
59273
+ if (scopeUnaffectedDaemon && isDaemonScopedCommand(candidate)) {
59274
+ skippedDaemonCommands.push(candidate.displayCommand);
59275
+ summary.commandsRun.push({
59276
+ command: candidate.command,
59277
+ args: candidate.args,
59278
+ displayCommand: candidate.displayCommand,
59279
+ category: candidate.category,
59280
+ source: candidate.source,
59281
+ passed: true,
59282
+ skipped: true,
59283
+ skipReason: "unaffected_daemon_scope"
59284
+ });
59285
+ continue;
59286
+ }
59287
+ commandsToRun.push(candidate);
59288
+ }
59289
+ if (opts?.changeImpact) {
59290
+ summary.changeImpact = {
59291
+ isDaemonAffecting: opts.changeImpact.isDaemonAffecting,
59292
+ affectedPackages: opts.changeImpact.affectedPackages,
59293
+ ...skippedDaemonCommands.length ? { skippedDaemonCommands } : {}
59294
+ };
59295
+ }
59162
59296
  if (runLegacyBootstrapCommands) {
59163
59297
  summary.bootstrap = { stage: "legacy" };
59164
59298
  for (const candidate of selection.bootstrapCommands) {
@@ -59193,23 +59327,22 @@ async function runMeshRefineValidationGate(mesh, workspace, opts) {
59193
59327
  }
59194
59328
  }
59195
59329
  }
59196
- for (const candidate of selection.commands) {
59330
+ let missingDepsBlocked = false;
59331
+ for (const candidate of commandsToRun) {
59197
59332
  const startedAt = Date.now();
59198
59333
  const cwd = candidate.cwd ? (0, import_path14.resolve)(workspace, candidate.cwd) : workspace;
59199
59334
  const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
59200
59335
  const bootstrapProvidedDependencies = summary.bootstrap?.stage === "cached" || summary.bootstrap?.stage === "ran" || summary.bootstrap?.stage === "legacy";
59201
- if (!bootstrapProvidedDependencies && isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd)) {
59336
+ if (!bootstrapProvidedDependencies && needsNodeModules(candidate, cwd)) {
59202
59337
  summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, {
59203
- stderr: "Dependencies appear to be missing: package.json and a lockfile are present, but node_modules is absent. Configure validation.bootstrapCommands in repo mesh/refine config if Refinery should install/bootstrap before validation."
59338
+ stderr: "Dependencies appear to be missing: package.json and a lockfile are present, but node_modules is absent. Configure validation.bootstrapCommands (or .adhdev/worktree_bootstrap.json) in repo mesh/refine config if Refinery should install/bootstrap before validation."
59204
59339
  }, false, {
59205
59340
  exitCode: null,
59206
59341
  skipped: true,
59207
59342
  failureKind: "missing_dependencies"
59208
59343
  }));
59209
- summary.status = "failed";
59210
- summary.failureKind = "missing_dependencies";
59211
- summary.failureCode = "missing_dependencies";
59212
- return summary;
59344
+ missingDepsBlocked = true;
59345
+ continue;
59213
59346
  }
59214
59347
  const resolvedCommand = resolveWin32Executable(candidate.command);
59215
59348
  const spawn5 = buildWin32ExecFileSpawn(resolvedCommand, candidate.args);
@@ -59245,6 +59378,12 @@ async function runMeshRefineValidationGate(mesh, workspace, opts) {
59245
59378
  return summary;
59246
59379
  }
59247
59380
  }
59381
+ if (missingDepsBlocked) {
59382
+ summary.status = "failed";
59383
+ summary.failureKind = "missing_dependencies";
59384
+ summary.failureCode = "missing_dependencies";
59385
+ return summary;
59386
+ }
59248
59387
  summary.status = "passed";
59249
59388
  return summary;
59250
59389
  }
@@ -59277,7 +59416,50 @@ function buildRefineJobHandle(self, args) {
59277
59416
  }
59278
59417
  };
59279
59418
  }
59419
+ function slimRefineEventResult(result) {
59420
+ const slim = {};
59421
+ for (const key2 of [
59422
+ "success",
59423
+ "code",
59424
+ "error",
59425
+ "convergenceStatus",
59426
+ "blockedReason",
59427
+ "branch",
59428
+ "into",
59429
+ "terminalKind",
59430
+ "nextStep",
59431
+ "finalBranchConvergenceState"
59432
+ ]) {
59433
+ if (result[key2] !== void 0) slim[key2] = result[key2];
59434
+ }
59435
+ if (Array.isArray(result.unreachableSubmoduleCommits)) {
59436
+ slim.unreachableSubmoduleCommits = result.unreachableSubmoduleCommits.map((e) => ({ path: e?.path, autoPublishAllowed: e?.autoPublishAllowed }));
59437
+ }
59438
+ if (result.validationSummary && typeof result.validationSummary === "object") {
59439
+ const vs = result.validationSummary;
59440
+ slim.validationSummary = {
59441
+ status: vs.status,
59442
+ failureCode: vs.failureCode,
59443
+ configSource: vs.configSource,
59444
+ configSourceType: vs.configSourceType,
59445
+ commandsRunCount: Array.isArray(vs.commandsRun) ? vs.commandsRun.length : void 0
59446
+ };
59447
+ }
59448
+ if (result.patchEquivalence && typeof result.patchEquivalence === "object") {
59449
+ const pe = result.patchEquivalence;
59450
+ slim.patchEquivalence = { status: pe.status, equivalent: pe.equivalent };
59451
+ }
59452
+ if (result.submoduleReachability && typeof result.submoduleReachability === "object") {
59453
+ const sr = result.submoduleReachability;
59454
+ slim.submoduleReachability = {
59455
+ checked: Array.isArray(sr.entries) ? sr.entries.length : void 0,
59456
+ unreachable: Array.isArray(sr.unreachable) ? sr.unreachable.length : void 0
59457
+ };
59458
+ }
59459
+ return slim;
59460
+ }
59280
59461
  function queueRefineJobEvent(self, event, handle, result) {
59462
+ const slimResult = result ? slimRefineEventResult(result) : void 0;
59281
59463
  const metadataEvent = {
59282
59464
  source: "refine_mesh_node_async_job",
59283
59465
  jobId: handle.jobId,
@@ -59290,7 +59472,7 @@ function queueRefineJobEvent(self, event, handle, result) {
59290
59472
  startedAt: handle.startedAt,
59291
59473
  completedAt: handle.completedAt,
59292
59474
  retryOfJobId: handle.retryOfJobId,
59293
- ...result ? { result } : {}
59475
+ ...slimResult ? { result: slimResult } : {}
59294
59476
  };
59295
59477
  const eventPayload = {
59296
59478
  event,
@@ -59317,7 +59499,7 @@ function queueRefineJobEvent(self, event, handle, result) {
59317
59499
  startedAt: handle.startedAt,
59318
59500
  completedAt: handle.completedAt,
59319
59501
  retryOfJobId: handle.retryOfJobId,
59320
- ...result ? { result } : {}
59502
+ ...slimResult ? { result: slimResult } : {}
59321
59503
  }
59322
59504
  );
59323
59505
  if (forwarded?.success === true) return;
@@ -59452,7 +59634,20 @@ async function refineResolveRefsStage(self, meshId, nodeId, args, refineStages)
59452
59634
  const { stdout: branchHeadStdout } = await execFileAsync4("git", ["rev-parse", branch], { cwd: node.workspace, encoding: "utf8" });
59453
59635
  const baseHead = baseHeadRaw;
59454
59636
  const branchHead = branchHeadStdout.trim();
59455
- recordMeshRefineStage(refineStages, "resolve_refs", "passed", resolveStarted, { branch, baseBranch, baseHead, branchHead, ...fetchWarning ? { fetchWarning } : {} });
59637
+ let changeImpact;
59638
+ try {
59639
+ changeImpact = await classifyChangedPackages(node.workspace, baseHead, branchHead);
59640
+ } catch {
59641
+ changeImpact = void 0;
59642
+ }
59643
+ recordMeshRefineStage(refineStages, "resolve_refs", "passed", resolveStarted, {
59644
+ branch,
59645
+ baseBranch,
59646
+ baseHead,
59647
+ branchHead,
59648
+ ...changeImpact ? { changeImpact } : {},
59649
+ ...fetchWarning ? { fetchWarning } : {}
59650
+ });
59456
59651
  return {
59457
59652
  kind: "continue",
59458
59653
  ctx: {
@@ -59469,6 +59664,7 @@ async function refineResolveRefsStage(self, meshId, nodeId, args, refineStages)
59469
59664
  baseBranch,
59470
59665
  baseHead,
59471
59666
  branchHead,
59667
+ changeImpact,
59472
59668
  validationSummary: void 0,
59473
59669
  patchEquivalence: void 0,
59474
59670
  submoduleReachability: void 0
@@ -59479,6 +59675,9 @@ async function refineValidationStage(self, ctx) {
59479
59675
  const { mesh, node, branch, baseBranch, refineStages } = ctx;
59480
59676
  const validationStarted = Date.now();
59481
59677
  const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace, {
59678
+ // (a) Scope the validation command set by coarse change-impact (resolved
59679
+ // in resolve_refs). Undefined → gate runs the full command set (fail-open).
59680
+ changeImpact: ctx.changeImpact,
59482
59681
  // M2-2: consume the node's persisted bootstrap state; persist re-runs.
59483
59682
  persistedBootstrapState: node.worktreeBootstrap,
59484
59683
  onBootstrapStateChange: (state) => {
@@ -59498,7 +59697,7 @@ async function refineValidationStage(self, ctx) {
59498
59697
  if (validationSummary.status === "failed") {
59499
59698
  const firstFailedCmd = Array.isArray(validationSummary.commandsRun) ? validationSummary.commandsRun.find((c) => c.success === false) : void 0;
59500
59699
  const buildValidationFailedError = () => {
59501
- const base = validationSummary.failureCode === "missing_dependencies" ? "Refinery validation dependencies are missing; merge/refine was not attempted. Configure validation.bootstrapCommands if Refinery should bootstrap dependencies before validation." : 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.";
59700
+ 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.";
59502
59701
  if (!firstFailedCmd) return base;
59503
59702
  const cmdName = typeof firstFailedCmd.displayCommand === "string" ? firstFailedCmd.displayCommand : typeof firstFailedCmd.command === "string" ? [firstFailedCmd.command, ...Array.isArray(firstFailedCmd.args) ? firstFailedCmd.args : []].join(" ").trim() : typeof firstFailedCmd.cmd === "string" ? firstFailedCmd.cmd : "";
59504
59703
  const rawOutput = [firstFailedCmd.stdout, firstFailedCmd.stderr, firstFailedCmd.output].filter((s2) => typeof s2 === "string" && s2.length > 0).join("\n");