@adhdev/daemon-core 0.9.82-rc.89 → 0.9.82-rc.90

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.
@@ -21,6 +21,11 @@ export interface RepoMeshRefineConfig {
21
21
  allowAutoPublishSubmoduleMainCommits?: boolean;
22
22
  validation?: {
23
23
  required?: boolean;
24
+ /**
25
+ * Optional dependency/bootstrap commands that Refinery runs before
26
+ * validation commands. Refinery never infers installs on its own.
27
+ */
28
+ bootstrapCommands?: RepoMeshRefineValidationCommandConfig[];
24
29
  commands?: RepoMeshRefineValidationCommandConfig[];
25
30
  };
26
31
  }
@@ -44,6 +49,7 @@ export interface MeshRefineConfigLoadResult {
44
49
  export interface MeshRefineValidationPlan {
45
50
  source: string;
46
51
  sourceType: MeshRefineConfigLoadResult['sourceType'];
52
+ bootstrapCommands: MeshRefineValidationCommandPlan[];
47
53
  commands: MeshRefineValidationCommandPlan[];
48
54
  rejectedCommands: Array<Record<string, unknown>>;
49
55
  suggestions: RepoMeshRefineValidationCommandConfig[];
@@ -113,6 +119,44 @@ export declare const MESH_REFINE_CONFIG_SCHEMA: {
113
119
  };
114
120
  };
115
121
  };
122
+ readonly bootstrapCommands: {
123
+ readonly type: "array";
124
+ readonly maxItems: 4;
125
+ readonly items: {
126
+ readonly type: "object";
127
+ readonly additionalProperties: false;
128
+ readonly required: readonly ["command"];
129
+ readonly properties: {
130
+ readonly command: {
131
+ readonly type: "string";
132
+ readonly minLength: 1;
133
+ };
134
+ readonly args: {
135
+ readonly type: "array";
136
+ readonly items: {
137
+ readonly type: "string";
138
+ };
139
+ };
140
+ readonly category: {
141
+ readonly enum: readonly ["typecheck", "test", "lint", "build", "custom"];
142
+ };
143
+ readonly cwd: {
144
+ readonly type: "string";
145
+ };
146
+ readonly timeoutMs: {
147
+ readonly type: "number";
148
+ readonly minimum: 1000;
149
+ readonly maximum: 600000;
150
+ };
151
+ readonly env: {
152
+ readonly type: "object";
153
+ readonly additionalProperties: {
154
+ readonly type: "string";
155
+ };
156
+ };
157
+ };
158
+ };
159
+ };
116
160
  };
117
161
  };
118
162
  };
@@ -120,6 +164,7 @@ export declare const MESH_REFINE_CONFIG_SCHEMA: {
120
164
  export declare function validateMeshRefineConfig(config: unknown, source?: string): {
121
165
  valid: boolean;
122
166
  errors: string[];
167
+ bootstrapCommands: MeshRefineValidationCommandPlan[];
123
168
  commands: MeshRefineValidationCommandPlan[];
124
169
  rejectedCommands: Array<Record<string, unknown>>;
125
170
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.89",
3
+ "version": "0.9.82-rc.90",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -38,7 +38,7 @@ import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../l
38
38
  import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
39
39
  import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
40
40
  import { buildSessionEntries } from '../status/builders.js';
41
- import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
41
+ import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
42
42
  import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
43
43
  import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
44
44
  import {
@@ -1070,8 +1070,11 @@ type MeshRefineValidationSummary = {
1070
1070
  status: MeshRefineValidationStatus;
1071
1071
  required: true;
1072
1072
  commandsRun: Array<Record<string, unknown>>;
1073
+ bootstrapCommandsRun: Array<Record<string, unknown>>;
1073
1074
  rejectedCommands: Array<Record<string, unknown>>;
1074
1075
  skippedReason?: string;
1076
+ failureKind?: string;
1077
+ failureCode?: string;
1075
1078
  timeoutMs: number;
1076
1079
  outputLimitBytes: number;
1077
1080
  configSource?: string;
@@ -1107,6 +1110,8 @@ type MeshRefineSubmoduleReachabilityEntry = {
1107
1110
  autoPublishSucceeded?: boolean;
1108
1111
  autoPublishVerified?: boolean;
1109
1112
  autoPublishRefspec?: string;
1113
+ autoPublishSkippedReason?: string;
1114
+ importedFromWorktree?: boolean;
1110
1115
  checkedLocal?: boolean;
1111
1116
  localReachable?: boolean;
1112
1117
  remote?: string;
@@ -1284,7 +1289,7 @@ async function runMeshRefinePatchEquivalenceGate(
1284
1289
  async function runMeshRefineSubmoduleReachabilityGate(
1285
1290
  repoRoot: string,
1286
1291
  mergedTree: string,
1287
- options: { allowAutoPublishSubmoduleMainCommits?: boolean; autoPublishPolicySource?: string } = {},
1292
+ options: { allowAutoPublishSubmoduleMainCommits?: boolean; autoPublishPolicySource?: string; worktreeRoot?: string } = {},
1288
1293
  ): Promise<MeshRefineSubmoduleReachabilitySummary> {
1289
1294
  const startedAt = Date.now();
1290
1295
  const entries: MeshRefineSubmoduleReachabilityEntry[] = [];
@@ -1317,6 +1322,17 @@ async function runMeshRefineSubmoduleReachabilityGate(
1317
1322
  });
1318
1323
  return { stdout: String(stdout || ''), stderr: String(stderr || ''), refspec };
1319
1324
  };
1325
+ const importCommitFromWorktreeSubmodule = async (submodulePath: string, worktreeSubmodulePath: string, commit: string): Promise<boolean> => {
1326
+ if (!fs.existsSync(worktreeSubmodulePath)) return false;
1327
+ try {
1328
+ await runGit(worktreeSubmodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
1329
+ } catch {
1330
+ return false;
1331
+ }
1332
+ await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', worktreeSubmodulePath, commit]);
1333
+ await runGit(submodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
1334
+ return true;
1335
+ };
1320
1336
 
1321
1337
  const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
1322
1338
  const gitlinks = treeOutput
@@ -1339,6 +1355,11 @@ async function runMeshRefineSubmoduleReachabilityGate(
1339
1355
  if (!fs.existsSync(submodulePath)) {
1340
1356
  entry.error = `Submodule checkout missing at ${gitlink.path}`;
1341
1357
  entry.publishRequired = true;
1358
+ if (options.allowAutoPublishSubmoduleMainCommits === true) {
1359
+ entry.autoPublishAllowed = true;
1360
+ entry.autoPublishAttempted = false;
1361
+ entry.autoPublishSkippedReason = `submodule checkout missing at ${gitlink.path}; cannot perform non-force push to origin/main`;
1362
+ }
1342
1363
  entries.push(entry);
1343
1364
  continue;
1344
1365
  }
@@ -1349,6 +1370,21 @@ async function runMeshRefineSubmoduleReachabilityGate(
1349
1370
  entry.localReachable = true;
1350
1371
  } catch {
1351
1372
  entry.localReachable = false;
1373
+ if (options.allowAutoPublishSubmoduleMainCommits === true && options.worktreeRoot) {
1374
+ try {
1375
+ const imported = await importCommitFromWorktreeSubmodule(
1376
+ submodulePath,
1377
+ pathResolve(options.worktreeRoot, gitlink.path),
1378
+ gitlink.commit,
1379
+ );
1380
+ if (imported) {
1381
+ entry.localReachable = true;
1382
+ entry.importedFromWorktree = true;
1383
+ }
1384
+ } catch (importError: any) {
1385
+ entry.autoPublishSkippedReason = `candidate commit was not present in the source checkout and could not be imported from worktree submodule: ${truncateValidationOutput(importError?.stderr || importError?.message || String(importError))}`;
1386
+ }
1387
+ }
1352
1388
  // Probe the submodule remote before allowing cleanup/completion.
1353
1389
  }
1354
1390
 
@@ -1362,6 +1398,11 @@ async function runMeshRefineSubmoduleReachabilityGate(
1362
1398
  } catch {
1363
1399
  entry.error = 'Submodule remote reachability check failed: no configured origin remote';
1364
1400
  entry.publishRequired = true;
1401
+ if (options.allowAutoPublishSubmoduleMainCommits === true) {
1402
+ entry.autoPublishAllowed = true;
1403
+ entry.autoPublishAttempted = false;
1404
+ entry.autoPublishSkippedReason = 'submodule origin remote is not configured; cannot perform non-force push to origin/main';
1405
+ }
1365
1406
  entries.push(entry);
1366
1407
  continue;
1367
1408
  }
@@ -1401,6 +1442,11 @@ async function runMeshRefineSubmoduleReachabilityGate(
1401
1442
  const publishDetails = truncateValidationOutput(publishError?.stderr || publishError?.message || String(publishError));
1402
1443
  entry.error = `Submodule auto-publish to origin/main failed or could not be verified: ${publishDetails}`;
1403
1444
  }
1445
+ } else if (options.allowAutoPublishSubmoduleMainCommits === true) {
1446
+ entry.autoPublishAllowed = true;
1447
+ entry.autoPublishAttempted = false;
1448
+ entry.autoPublishSkippedReason = entry.autoPublishSkippedReason
1449
+ || 'candidate commit is not reachable in the source checkout or worktree submodule, so Refinery cannot push it to origin/main';
1404
1450
  }
1405
1451
  }
1406
1452
  } catch (e: any) {
@@ -1444,16 +1490,18 @@ async function runMeshRefineSubmoduleReachabilityGate(
1444
1490
 
1445
1491
  function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
1446
1492
  const plan = resolveMeshRefineValidationPlan(mesh, workspace);
1493
+ const mapCommand = (command: MeshRefineValidationCommandPlan) => ({
1494
+ displayCommand: command.displayCommand,
1495
+ category: command.category,
1496
+ source: command.source,
1497
+ cwd: command.cwd,
1498
+ timeoutMs: command.timeoutMs,
1499
+ });
1447
1500
  return {
1448
1501
  source: plan.source,
1449
1502
  sourceType: plan.sourceType,
1450
- commands: plan.commands.map(command => ({
1451
- displayCommand: command.displayCommand,
1452
- category: command.category,
1453
- source: command.source,
1454
- cwd: command.cwd,
1455
- timeoutMs: command.timeoutMs,
1456
- })),
1503
+ bootstrapCommands: plan.bootstrapCommands.map(mapCommand),
1504
+ commands: plan.commands.map(mapCommand),
1457
1505
  unavailableReason: plan.unavailableReason,
1458
1506
  rejectedCommands: plan.rejectedCommands,
1459
1507
  suggestions: plan.suggestions,
@@ -1473,6 +1521,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
1473
1521
  status: 'skipped',
1474
1522
  required: true,
1475
1523
  commandsRun: [],
1524
+ bootstrapCommandsRun: [],
1476
1525
  rejectedCommands: selection.rejectedCommands,
1477
1526
  skippedReason: undefined,
1478
1527
  timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
@@ -1488,7 +1537,32 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
1488
1537
  return summary;
1489
1538
  }
1490
1539
 
1491
- for (const candidate of selection.commands) {
1540
+ const commandRecord = (candidate: MeshRefineValidationCommand, cwd: string, startedAt: number, result: any, passed: boolean, extras: Record<string, unknown> = {}) => ({
1541
+ command: candidate.command,
1542
+ args: candidate.args,
1543
+ displayCommand: candidate.displayCommand,
1544
+ category: candidate.category,
1545
+ source: candidate.source,
1546
+ cwd,
1547
+ passed,
1548
+ durationMs: Date.now() - startedAt,
1549
+ stdout: truncateValidationOutput(result?.stdout),
1550
+ stderr: truncateValidationOutput(result?.stderr || result?.message),
1551
+ ...extras,
1552
+ });
1553
+ const isPackageManagerValidation = (candidate: MeshRefineValidationCommand): boolean => {
1554
+ const command = pathBasename(candidate.command).replace(/\.(?:cmd|exe)$/i, '');
1555
+ return ['npm', 'pnpm', 'yarn', 'bun'].includes(command)
1556
+ && candidate.args.some(arg => arg === 'run' || arg === 'test' || arg === 'exec');
1557
+ };
1558
+ const dependenciesLikelyMissing = (cwd: string): boolean => {
1559
+ if (!fs.existsSync(pathJoin(cwd, 'package.json'))) return false;
1560
+ if (fs.existsSync(pathJoin(cwd, 'node_modules'))) return false;
1561
+ return ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb', 'bun.lock']
1562
+ .some(lock => fs.existsSync(pathJoin(cwd, lock)));
1563
+ };
1564
+
1565
+ for (const candidate of selection.bootstrapCommands) {
1492
1566
  const startedAt = Date.now();
1493
1567
  const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
1494
1568
  const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
@@ -1500,36 +1574,61 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
1500
1574
  maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
1501
1575
  env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
1502
1576
  });
1503
- summary.commandsRun.push({
1504
- command: candidate.command,
1505
- args: candidate.args,
1506
- displayCommand: candidate.displayCommand,
1507
- category: candidate.category,
1508
- source: candidate.source,
1577
+ summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
1578
+ } catch (error: any) {
1579
+ summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
1580
+ exitCode: typeof error?.code === 'number' ? error.code : null,
1581
+ signal: typeof error?.signal === 'string' ? error.signal : null,
1582
+ timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
1583
+ failureKind: 'dependency_bootstrap_failed',
1584
+ }));
1585
+ summary.status = 'failed';
1586
+ summary.failureKind = 'dependency_bootstrap_failed';
1587
+ summary.failureCode = 'dependency_bootstrap_failed';
1588
+ return summary;
1589
+ }
1590
+ }
1591
+
1592
+ for (const candidate of selection.commands) {
1593
+ const startedAt = Date.now();
1594
+ const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
1595
+ const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
1596
+ if (selection.bootstrapCommands.length === 0 && isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd)) {
1597
+ summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, {
1598
+ 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.',
1599
+ }, false, {
1600
+ exitCode: null,
1601
+ skipped: true,
1602
+ failureKind: 'missing_dependencies',
1603
+ }));
1604
+ summary.status = 'failed';
1605
+ summary.failureKind = 'missing_dependencies';
1606
+ summary.failureCode = 'missing_dependencies';
1607
+ return summary;
1608
+ }
1609
+ try {
1610
+ const result = await execFileAsync(candidate.command, candidate.args, {
1509
1611
  cwd,
1510
- passed: true,
1511
- exitCode: 0,
1512
- durationMs: Date.now() - startedAt,
1513
- stdout: truncateValidationOutput(result.stdout),
1514
- stderr: truncateValidationOutput(result.stderr),
1612
+ encoding: 'utf8',
1613
+ timeout,
1614
+ maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
1615
+ env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
1515
1616
  });
1617
+ summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
1516
1618
  } catch (error: any) {
1517
- summary.commandsRun.push({
1518
- command: candidate.command,
1519
- args: candidate.args,
1520
- displayCommand: candidate.displayCommand,
1521
- category: candidate.category,
1522
- source: candidate.source,
1523
- cwd,
1524
- passed: false,
1619
+ const stderr = truncateValidationOutput(error?.stderr || error?.message);
1620
+ const missingDependencyFailure = /Cannot find module|MODULE_NOT_FOUND|node_modules|command not found|not found/i.test(stderr);
1621
+ summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
1525
1622
  exitCode: typeof error?.code === 'number' ? error.code : null,
1526
1623
  signal: typeof error?.signal === 'string' ? error.signal : null,
1527
1624
  timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
1528
- durationMs: Date.now() - startedAt,
1529
- stdout: truncateValidationOutput(error?.stdout),
1530
- stderr: truncateValidationOutput(error?.stderr || error?.message),
1531
- });
1625
+ ...(missingDependencyFailure ? { failureKind: 'missing_dependencies' } : {}),
1626
+ }));
1532
1627
  summary.status = 'failed';
1628
+ if (missingDependencyFailure) {
1629
+ summary.failureKind = 'missing_dependencies';
1630
+ summary.failureCode = 'missing_dependencies';
1631
+ }
1533
1632
  return summary;
1534
1633
  }
1535
1634
  }
@@ -2751,9 +2850,13 @@ export class DaemonCommandRouter {
2751
2850
  if (validationSummary.status === 'failed') {
2752
2851
  return {
2753
2852
  success: false,
2754
- code: 'validation_failed',
2853
+ code: validationSummary.failureCode || 'validation_failed',
2755
2854
  convergenceStatus: 'blocked_review',
2756
- error: 'Refinery validation gate failed; merge/refine was not attempted.',
2855
+ error: validationSummary.failureCode === 'missing_dependencies'
2856
+ ? 'Refinery validation dependencies are missing; merge/refine was not attempted. Configure validation.bootstrapCommands if Refinery should bootstrap dependencies before validation.'
2857
+ : validationSummary.failureCode === 'dependency_bootstrap_failed'
2858
+ ? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
2859
+ : 'Refinery validation gate failed; merge/refine was not attempted.',
2757
2860
  branch,
2758
2861
  into: baseBranch,
2759
2862
  validationSummary,
@@ -2825,6 +2928,7 @@ export class DaemonCommandRouter {
2825
2928
  const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
2826
2929
  allowAutoPublishSubmoduleMainCommits: autoPublishSubmoduleMainCommits.enabled,
2827
2930
  autoPublishPolicySource: autoPublishSubmoduleMainCommits.source,
2931
+ worktreeRoot: node.workspace,
2828
2932
  });
2829
2933
  recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
2830
2934
  checked: submoduleReachability.checked,
@@ -2844,6 +2948,16 @@ export class DaemonCommandRouter {
2844
2948
  remoteMainReachable: entry.remoteMainReachable,
2845
2949
  error: entry.error,
2846
2950
  })),
2951
+ autoPublishSkipped: submoduleReachability.entries
2952
+ .filter(entry => entry.autoPublishAllowed === true && entry.autoPublishAttempted !== true)
2953
+ .map(entry => ({
2954
+ path: entry.path,
2955
+ commit: entry.commit,
2956
+ remote: entry.remote,
2957
+ remoteUrl: entry.remoteUrl,
2958
+ remoteMainBranch: entry.remoteMainBranch,
2959
+ reason: entry.autoPublishSkippedReason || entry.error || 'auto-publish was allowed but no publish attempt was possible',
2960
+ })),
2847
2961
  unreachable: submoduleReachability.unreachable.map(entry => ({
2848
2962
  path: entry.path,
2849
2963
  commit: entry.commit,
@@ -2851,9 +2965,10 @@ export class DaemonCommandRouter {
2851
2965
  autoPublishAllowed: entry.autoPublishAllowed,
2852
2966
  autoPublishAttempted: entry.autoPublishAttempted,
2853
2967
  autoPublishSucceeded: entry.autoPublishSucceeded,
2854
- autoPublishVerified: entry.autoPublishVerified,
2855
- autoPublishRefspec: entry.autoPublishRefspec,
2856
- remote: entry.remote,
2968
+ autoPublishVerified: entry.autoPublishVerified,
2969
+ autoPublishRefspec: entry.autoPublishRefspec,
2970
+ autoPublishSkippedReason: entry.autoPublishSkippedReason,
2971
+ remote: entry.remote,
2857
2972
  remoteUrl: entry.remoteUrl,
2858
2973
  remoteReachable: entry.remoteReachable,
2859
2974
  remoteMainBranch: entry.remoteMainBranch,
@@ -2891,6 +3006,7 @@ export class DaemonCommandRouter {
2891
3006
  autoPublishSucceeded: entry.autoPublishSucceeded,
2892
3007
  autoPublishVerified: entry.autoPublishVerified,
2893
3008
  autoPublishRefspec: entry.autoPublishRefspec,
3009
+ autoPublishSkippedReason: entry.autoPublishSkippedReason,
2894
3010
  error: entry.error,
2895
3011
  })),
2896
3012
  branch,
@@ -4897,8 +5013,9 @@ export class DaemonCommandRouter {
4897
5013
  const meshHost = resolveMeshHostStatus(mesh);
4898
5014
 
4899
5015
  const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
5016
+ const pendingCoordinatorEventCount = getPendingMeshCoordinatorEvents(meshId).length;
4900
5017
  const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
4901
- if (!refreshRequested) {
5018
+ if (!refreshRequested && pendingCoordinatorEventCount === 0) {
4902
5019
  const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
4903
5020
  if (cachedStatus) {
4904
5021
  logRepoMeshStatusDebug('return_cached', {
@@ -4912,6 +5029,8 @@ export class DaemonCommandRouter {
4912
5029
  }
4913
5030
  const refreshReason = refreshRequested
4914
5031
  ? 'explicit_refresh'
5032
+ : pendingCoordinatorEventCount > 0
5033
+ ? 'pending_coordinator_events'
4915
5034
  : hadAggregateCache
4916
5035
  ? 'stale_pending_cache_refresh'
4917
5036
  : 'cold_cache_miss';
@@ -4954,8 +5073,14 @@ export class DaemonCommandRouter {
4954
5073
  const effectiveDirectTruth = passivePeerTruthNotAttempted
4955
5074
  ? { ...directTruth, unavailableNodeIds: [] as string[] }
4956
5075
  : directTruth;
5076
+ const unavailableDirectTruthNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
5077
+ const unavailableNodesAreOnlyRemovedWorktrees = unavailableDirectTruthNodeIds.size > 0
5078
+ && Array.isArray(mesh.nodes)
5079
+ && mesh.nodes
5080
+ .filter((node: any) => unavailableDirectTruthNodeIds.has(String(node.id || node.nodeId || '')))
5081
+ .every((node: any) => node?.isLocalWorktree === true);
4957
5082
  const directTruthSatisfied = !requireDirectPeerTruth
4958
- || effectiveDirectTruth.directEvidenceCount > 0;
5083
+ || (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
4959
5084
  if (requireDirectPeerTruth && !directTruthSatisfied) {
4960
5085
  const failureResult = {
4961
5086
  success: false,
@@ -5216,6 +5341,7 @@ export class DaemonCommandRouter {
5216
5341
  nodeStatuses.push(status);
5217
5342
  }
5218
5343
 
5344
+ const pendingCoordinatorEvents = drainPendingMeshCoordinatorEvents(meshId);
5219
5345
  const statusResult = {
5220
5346
  success: true,
5221
5347
  meshId: mesh.id,
@@ -5257,8 +5383,13 @@ export class DaemonCommandRouter {
5257
5383
  nodes: nodeStatuses,
5258
5384
  queue: { tasks: queue, summary: queueSummary },
5259
5385
  ledger: { entries: ledgerEntries, summary: ledgerSummary },
5386
+ ...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
5260
5387
  };
5261
- const rememberedStatus = this.rememberAggregateMeshStatus(meshId, statusResult, refreshReason);
5388
+ const { pendingCoordinatorEvents: _pendingCoordinatorEvents, ...cacheableStatusResult } = statusResult as any;
5389
+ const rememberedStatus = this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
5390
+ const returnedStatus = pendingCoordinatorEvents.length > 0
5391
+ ? { ...rememberedStatus, pendingCoordinatorEvents }
5392
+ : rememberedStatus;
5262
5393
  logRepoMeshStatusDebug('return_live', {
5263
5394
  meshId,
5264
5395
  command: 'mesh_status',
@@ -5266,9 +5397,9 @@ export class DaemonCommandRouter {
5266
5397
  refreshReason,
5267
5398
  meshSource: meshRecord.source,
5268
5399
  directTruth,
5269
- summary: summarizeRepoMeshStatusDebug(rememberedStatus),
5400
+ summary: summarizeRepoMeshStatusDebug(returnedStatus),
5270
5401
  });
5271
- return rememberedStatus;
5402
+ return returnedStatus;
5272
5403
  } catch (e: any) {
5273
5404
  return { success: false, error: e.message };
5274
5405
  }
@@ -27,6 +27,11 @@ export interface RepoMeshRefineConfig {
27
27
  allowAutoPublishSubmoduleMainCommits?: boolean;
28
28
  validation?: {
29
29
  required?: boolean;
30
+ /**
31
+ * Optional dependency/bootstrap commands that Refinery runs before
32
+ * validation commands. Refinery never infers installs on its own.
33
+ */
34
+ bootstrapCommands?: RepoMeshRefineValidationCommandConfig[];
30
35
  commands?: RepoMeshRefineValidationCommandConfig[];
31
36
  };
32
37
  }
@@ -53,6 +58,7 @@ export interface MeshRefineConfigLoadResult {
53
58
  export interface MeshRefineValidationPlan {
54
59
  source: string;
55
60
  sourceType: MeshRefineConfigLoadResult['sourceType'];
61
+ bootstrapCommands: MeshRefineValidationCommandPlan[];
56
62
  commands: MeshRefineValidationCommandPlan[];
57
63
  rejectedCommands: Array<Record<string, unknown>>;
58
64
  suggestions: RepoMeshRefineValidationCommandConfig[];
@@ -108,6 +114,23 @@ export const MESH_REFINE_CONFIG_SCHEMA = {
108
114
  },
109
115
  },
110
116
  },
117
+ bootstrapCommands: {
118
+ type: 'array',
119
+ maxItems: 4,
120
+ items: {
121
+ type: 'object',
122
+ additionalProperties: false,
123
+ required: ['command'],
124
+ properties: {
125
+ command: { type: 'string', minLength: 1 },
126
+ args: { type: 'array', items: { type: 'string' } },
127
+ category: { enum: [...MESH_REFINE_VALIDATION_CATEGORIES, 'custom'] },
128
+ cwd: { type: 'string' },
129
+ timeoutMs: { type: 'number', minimum: 1000, maximum: 600000 },
130
+ env: { type: 'object', additionalProperties: { type: 'string' } },
131
+ },
132
+ },
133
+ },
111
134
  },
112
135
  },
113
136
  },
@@ -184,12 +207,13 @@ function normalizeCommandConfig(entry: unknown, source: string): { command?: Mes
184
207
  };
185
208
  }
186
209
 
187
- export function validateMeshRefineConfig(config: unknown, source = 'inline'): { valid: boolean; errors: string[]; commands: MeshRefineValidationCommandPlan[]; rejectedCommands: Array<Record<string, unknown>> } {
210
+ export function validateMeshRefineConfig(config: unknown, source = 'inline'): { valid: boolean; errors: string[]; bootstrapCommands: MeshRefineValidationCommandPlan[]; commands: MeshRefineValidationCommandPlan[]; rejectedCommands: Array<Record<string, unknown>> } {
188
211
  const errors: string[] = [];
212
+ const bootstrapCommands: MeshRefineValidationCommandPlan[] = [];
189
213
  const commands: MeshRefineValidationCommandPlan[] = [];
190
214
  const rejectedCommands: Array<Record<string, unknown>> = [];
191
215
 
192
- if (!isRecord(config)) return { valid: false, errors: ['config must be an object'], commands, rejectedCommands };
216
+ if (!isRecord(config)) return { valid: false, errors: ['config must be an object'], bootstrapCommands, commands, rejectedCommands };
193
217
  if (config.version !== 1) errors.push('version must be 1');
194
218
  if (config.allowAutoPublishSubmoduleMainCommits !== undefined && typeof config.allowAutoPublishSubmoduleMainCommits !== 'boolean') {
195
219
  errors.push('allowAutoPublishSubmoduleMainCommits must be a boolean when provided');
@@ -197,7 +221,16 @@ export function validateMeshRefineConfig(config: unknown, source = 'inline'): {
197
221
  const validation = config.validation;
198
222
  if (validation !== undefined && !isRecord(validation)) errors.push('validation must be an object');
199
223
  const rawCommands = isRecord(validation) ? validation.commands : undefined;
224
+ const rawBootstrapCommands = isRecord(validation) ? validation.bootstrapCommands : undefined;
200
225
  if (rawCommands !== undefined && !Array.isArray(rawCommands)) errors.push('validation.commands must be an array');
226
+ if (rawBootstrapCommands !== undefined && !Array.isArray(rawBootstrapCommands)) errors.push('validation.bootstrapCommands must be an array');
227
+ if (Array.isArray(rawBootstrapCommands)) {
228
+ rawBootstrapCommands.forEach((entry, index) => {
229
+ const normalized = normalizeCommandConfig(entry, `${source}:validation.bootstrapCommands[${index}]`);
230
+ if (normalized.command) bootstrapCommands.push(normalized.command);
231
+ if (normalized.rejected) rejectedCommands.push(normalized.rejected);
232
+ });
233
+ }
201
234
  if (Array.isArray(rawCommands)) {
202
235
  rawCommands.forEach((entry, index) => {
203
236
  const normalized = normalizeCommandConfig(entry, `${source}:validation.commands[${index}]`);
@@ -206,7 +239,7 @@ export function validateMeshRefineConfig(config: unknown, source = 'inline'): {
206
239
  });
207
240
  }
208
241
  if (rejectedCommands.length) errors.push('one or more validation commands are invalid');
209
- return { valid: errors.length === 0, errors, commands, rejectedCommands };
242
+ return { valid: errors.length === 0, errors, bootstrapCommands, commands, rejectedCommands };
210
243
  }
211
244
 
212
245
  function parseConfigText(path: string, text: string): unknown {
@@ -300,6 +333,7 @@ export function resolveMeshRefineValidationPlan(mesh: any, workspace: string): M
300
333
  return {
301
334
  source: loaded.source,
302
335
  sourceType: loaded.sourceType,
336
+ bootstrapCommands: [],
303
337
  commands: [],
304
338
  rejectedCommands: loaded.error ? [{ source: loaded.source, reason: loaded.error }] : [],
305
339
  suggestions: suggestion.suggestions,
@@ -312,6 +346,7 @@ export function resolveMeshRefineValidationPlan(mesh: any, workspace: string): M
312
346
  return {
313
347
  source: loaded.path || loaded.source,
314
348
  sourceType: loaded.sourceType,
349
+ bootstrapCommands: validation.bootstrapCommands,
315
350
  commands: validation.commands,
316
351
  rejectedCommands: validation.rejectedCommands,
317
352
  suggestions: suggestion.suggestions,