@adhdev/daemon-core 0.9.82-rc.365 → 0.9.82-rc.367

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.
@@ -16,6 +16,8 @@ import { lowFamilyRegistry } from './low-family/index.js';
16
16
  import { medFamilyRegistry } from './med-family/index.js';
17
17
  import { launchIde } from './med-family/ide.js';
18
18
  import type { MedFamilyContext } from './med-family/index.js';
19
+ import { highFamilyRegistry } from './high-family/index.js';
20
+ import type { HighFamilyContext } from './high-family/index.js';
19
21
  import { DaemonCliManager } from './cli-manager.js';
20
22
  import { supportsExplicitSessionResume } from './cli-manager.js';
21
23
  import type { HostedCliRuntimeDescriptor } from './cli-manager.js';
@@ -45,6 +47,7 @@ import {
45
47
  normalizeMeshNodeId,
46
48
  meshNodeIdMatches,
47
49
  daemonIdsEquivalent,
50
+ meshWorkspacesEquivalent,
48
51
  } from '@adhdev/mesh-shared';
49
52
  import { SessionRegistry } from '../sessions/registry.js';
50
53
  import { LOG } from '../logging/logger.js';
@@ -52,17 +55,12 @@ import { logCommand } from '../logging/command-log.js';
52
55
  import type { CommandLogEntry } from '../logging/command-log.js';
53
56
  import * as yaml from 'js-yaml';
54
57
  import { createInteractionId, recordDebugTrace } from '../logging/debug-trace.js';
55
- import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
56
- import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
57
- import { registerMeshCoordinator } from '../mesh/coordinator-registry.js';
58
- import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, type PendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
59
- import { getRecentUnroutableDeliveries } from '../mesh/mesh-routing.js';
58
+ import { getSessionHostSurfaceKind } from '../session-host/runtime-surface.js';
59
+ import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
60
60
  import { buildMeshWorkerRelayStamp } from '../mesh/mesh-events-utils.js';
61
- import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
61
+ import { buildMeshHostRequiredFailure, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
62
62
  import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
63
63
  import { analyzeMeshRefineNodeChangeArea, orderMeshRefineBatchNodes } from '../mesh/mesh-refine-batch.js';
64
- import { buildPreviewFreshness } from '../mesh/preview-freshness.js';
65
- import { buildMeshAsyncRefineJobs } from '../mesh/mesh-refine-status.js';
66
64
  import {
67
65
  MESH_REFINE_CONFIG_LOCATIONS,
68
66
  MESH_REFINE_CONFIG_SCHEMA,
@@ -88,11 +86,10 @@ import { homedir, hostname as osHostname } from 'os';
88
86
  import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
89
87
  import * as fs from 'fs';
90
88
  import { execFileSync } from 'node:child_process';
91
- import { normalizeInteractivePromptResponse } from '../providers/types/interactive-prompt.js';
92
89
  import { workingDirBasename } from '../providers/working-dir.js';
93
90
  import { resolveWin32Executable } from '../cli-adapters/resolve-executable.js';
94
91
 
95
- function readProviderPriorityFromPolicy(policy: unknown): string[] {
92
+ export function readProviderPriorityFromPolicy(policy: unknown): string[] {
96
93
  const record = policy && typeof policy === 'object' && !Array.isArray(policy)
97
94
  ? policy as Record<string, unknown>
98
95
  : {};
@@ -153,7 +150,7 @@ function readNumberValue(...values: unknown[]): number | undefined {
153
150
  return undefined;
154
151
  }
155
152
 
156
- function readBooleanValue(...values: unknown[]): boolean | undefined {
153
+ export function readBooleanValue(...values: unknown[]): boolean | undefined {
157
154
  for (const value of values) {
158
155
  if (typeof value === 'boolean') return value;
159
156
  }
@@ -163,7 +160,7 @@ function readBooleanValue(...values: unknown[]): boolean | undefined {
163
160
  // summarizeRepoMeshDebugGit was a hand-synced copy of the cloud git-shape
164
161
  // summarizer; both now call shared summarizeGitShape (@adhdev/mesh-shared).
165
162
 
166
- function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
163
+ export function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
167
164
  const nodes = Array.isArray(status?.nodes) ? status.nodes : [];
168
165
  return {
169
166
  success: status?.success,
@@ -195,7 +192,7 @@ function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
195
192
  };
196
193
  }
197
194
 
198
- function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
195
+ export function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
199
196
  try {
200
197
  LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${JSON.stringify({ event, ...fields })}`);
201
198
  } catch {
@@ -257,7 +254,7 @@ function readMeshNodeDaemonId(node: Record<string, unknown>): string | undefined
257
254
  );
258
255
  }
259
256
 
260
- function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
257
+ export function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
261
258
  return readStringValue(
262
259
  node.hostname,
263
260
  node.host,
@@ -297,7 +294,7 @@ function compactMeshIdentityEvidence(value: string | undefined): string | undefi
297
294
  return value.length > 24 ? `${value.slice(0, 12)}…${value.slice(-8)}` : value;
298
295
  }
299
296
 
300
- function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
297
+ export function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
301
298
  localMachineId?: string;
302
299
  localDaemonId?: string;
303
300
  coordinatorHostname?: string;
@@ -357,7 +354,7 @@ function normalizeInlineMeshGitStatus(
357
354
  return sharedNormalizeGitStatus(status, readObjectRecord(node), options) as Record<string, unknown> | undefined;
358
355
  }
359
356
 
360
- function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
357
+ export function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
361
358
  return sharedPickBestTransitGitStatus(readObjectRecord(node), { lastCheckedAt: Date.now() }) as Record<string, unknown> | undefined;
362
359
  }
363
360
 
@@ -370,7 +367,7 @@ function shouldRefreshStalePendingAggregate(snapshot: any, options?: { requireDi
370
367
  });
371
368
  }
372
369
 
373
- function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
370
+ export function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
374
371
  const source = readStringValue(connection.source);
375
372
  const transport = readStringValue(connection.transport);
376
373
  return {
@@ -385,7 +382,7 @@ function buildLivePeerGitConnection(connection: Record<string, unknown>, timesta
385
382
  };
386
383
  }
387
384
 
388
- function recordInlineMeshDirectGitTruth(
385
+ export function recordInlineMeshDirectGitTruth(
389
386
  node: any,
390
387
  git: Record<string, unknown>,
391
388
  source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
@@ -462,7 +459,7 @@ function stampNodeReporterPlatform(node: any, platform: string | null, arch: str
462
459
  * Fire-and-forget (same pattern as the worktreeBootstrap writer) — a persistence
463
460
  * failure must never block the status response.
464
461
  */
465
- function persistNodeReporterPlatform(
462
+ export function persistNodeReporterPlatform(
466
463
  meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config',
467
464
  mesh: any,
468
465
  nodeId: string | undefined,
@@ -736,7 +733,7 @@ function isInlineMeshAutoFastForwardEligible(git: Record<string, unknown> | null
736
733
  return dirty !== true && countGitWorktreeChanges(git) === 0;
737
734
  }
738
735
 
739
- function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
736
+ export function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
740
737
  if (!git || readBooleanValue(git.isGitRepo) === false) return 'degraded';
741
738
  const branch = readStringValue(git.branch);
742
739
  if (!branch) return 'degraded';
@@ -875,7 +872,7 @@ function buildInlineMeshBranchConvergence(args: {
875
872
  };
876
873
  }
877
874
 
878
- function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
875
+ export function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
879
876
  const git = readObjectRecord(status.git);
880
877
  if (Object.keys(git).length === 0 && !status.gitProbePending) return;
881
878
  const uncommittedChanges = countGitWorktreeChanges(git);
@@ -890,7 +887,7 @@ function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<s
890
887
  }
891
888
  }
892
889
 
893
- function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
890
+ export function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
894
891
  const followUps = nodes
895
892
  .filter(node => {
896
893
  if (readObjectRecord(node.branchConvergence).needsConvergence !== true) return false;
@@ -1076,7 +1073,7 @@ function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknow
1076
1073
  }
1077
1074
  }
1078
1075
 
1079
- function finalizeMeshNodeStatus(args: {
1076
+ export function finalizeMeshNodeStatus(args: {
1080
1077
  status: Record<string, unknown>;
1081
1078
  node: any;
1082
1079
  daemonId?: string;
@@ -1132,8 +1129,23 @@ function readMeshTimeoutEnvMs(name: string, defaultMs: number): number {
1132
1129
  // round-trip to slow (often TURN-relayed) peers, so such a node was permanently
1133
1130
  // marked unavailable and blocked the whole mesh graph. Default raised to 25s
1134
1131
  // (still under the P2P REQUEST_TIMEOUT of 30s) and made env-overridable.
1135
- const MESH_DIRECT_PROBE_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_TIMEOUT_MS', 25_000);
1136
- const MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS', 25_000);
1132
+ export const MESH_DIRECT_PROBE_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_TIMEOUT_MS', 25_000);
1133
+ export const MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS', 25_000);
1134
+ // Cold-open warmup budget for the FIRST direct-peer probe to a peer whose mesh
1135
+ // DataChannel is not open yet. A fresh cross-machine, TURN-relayed handshake
1136
+ // (ICE gather + TURN allocation + DTLS across two residential networks) routinely
1137
+ // needs many seconds. Charging that warmup against the response deadline
1138
+ // (MESH_DIRECT_PROBE_TIMEOUT_MS) made the very first git_status to a cold peer
1139
+ // false-timeout, after which the warm retry — reusing the now-open channel —
1140
+ // succeeded: the classic cold-open signature. This budget bounds ONLY the
1141
+ // "channel not open yet" phase; once the channel opens the response deadline
1142
+ // governs the round trip. A genuine connect failure still rejects immediately —
1143
+ // the mesh manager fails the peer the instant its PeerConnection state goes
1144
+ // terminal, and isMeshConnectionDefinitivelyDown pre-gates an already-dead peer —
1145
+ // so this never masks a real failure for the whole window; it only grants a
1146
+ // still-handshaking peer the time it legitimately needs. Matches the daemon-cloud
1147
+ // DaemonMeshManager CONNECT_TIMEOUT_MS (45s). Env-overridable for very slow links.
1148
+ export const MESH_DIRECT_PROBE_CONNECT_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_CONNECT_TIMEOUT_MS', 45_000);
1137
1149
  // How long a successful per-peer git_status probe stays fresh enough to be
1138
1150
  // reused instead of issuing another blocking `refreshUpstream:true` fan-out.
1139
1151
  // A slow (TURN-relayed) peer's probe can take 9-23s, and the dashboard's
@@ -1202,17 +1214,120 @@ export class MeshGitProbeCache {
1202
1214
  }
1203
1215
  }
1204
1216
 
1217
+ /**
1218
+ * Await `work` under a warmup-aware deadline so a cold-open DataChannel handshake
1219
+ * is NOT charged against the command response budget — the root cause of the
1220
+ * "first mesh probe to a cold peer false-times-out, the warm retry succeeds"
1221
+ * signature. Two budgets, switched by the live peer connection state:
1222
+ *
1223
+ * - While `isConnected()` returns false the peer's channel is still opening; the
1224
+ * cold-open `connectTimeoutMs` budget applies. This phase is deliberately
1225
+ * generous because a TURN-relayed cross-machine handshake legitimately needs
1226
+ * many seconds — but a genuine connect *failure* is surfaced by `work`
1227
+ * rejecting on its own (the mesh manager fails the peer the instant its
1228
+ * PeerConnection state goes terminal), so a real failure is never masked for
1229
+ * the whole window.
1230
+ * - The first time `isConnected()` returns true the channel is warm; from that
1231
+ * instant the tight `responseTimeoutMs` governs how long the handler may take.
1232
+ * Warm-channel callers therefore see behavior identical to the old single
1233
+ * `Promise.race(work, responseTimeoutMs)`.
1234
+ *
1235
+ * Rejects with `Error('timeout')` when either budget is exhausted, mirroring the
1236
+ * previous single-race contract. Pure except for timers + the injected
1237
+ * `isConnected` probe, so it is unit-testable under fake timers without any real
1238
+ * WebRTC. When no connection getter is wired `isConnected` should be `() => true`
1239
+ * (the caller's choice) so the response deadline governs from t0 — the legacy
1240
+ * single-budget behavior, never a combined connect+response window.
1241
+ */
1242
+ export function awaitWithWarmupDeadline<T>(
1243
+ work: Promise<T>,
1244
+ opts: {
1245
+ isConnected: () => boolean;
1246
+ connectTimeoutMs: number;
1247
+ responseTimeoutMs: number;
1248
+ pollIntervalMs?: number;
1249
+ },
1250
+ ): Promise<T> {
1251
+ const pollMs = Math.max(1, Math.min(opts.pollIntervalMs ?? 200, opts.connectTimeoutMs));
1252
+ return new Promise<T>((resolve, reject) => {
1253
+ let done = false;
1254
+ let poll: ReturnType<typeof setInterval> | undefined;
1255
+ let responseTimer: ReturnType<typeof setTimeout> | undefined;
1256
+ const startedAt = Date.now();
1257
+ const cleanup = () => {
1258
+ if (poll) { clearInterval(poll); poll = undefined; }
1259
+ if (responseTimer) { clearTimeout(responseTimer); responseTimer = undefined; }
1260
+ };
1261
+ const settle = (fn: () => void) => {
1262
+ if (done) return;
1263
+ done = true;
1264
+ cleanup();
1265
+ fn();
1266
+ };
1267
+ // Arm the response deadline exactly once, the moment the channel is warm.
1268
+ const armResponse = () => {
1269
+ if (responseTimer || done) return;
1270
+ responseTimer = setTimeout(
1271
+ () => settle(() => reject(new Error('timeout'))),
1272
+ opts.responseTimeoutMs,
1273
+ );
1274
+ if (typeof responseTimer.unref === 'function') responseTimer.unref();
1275
+ };
1276
+ const onPoll = () => {
1277
+ if (done) return;
1278
+ if (opts.isConnected()) {
1279
+ if (poll) { clearInterval(poll); poll = undefined; }
1280
+ armResponse();
1281
+ return;
1282
+ }
1283
+ if (Date.now() - startedAt >= opts.connectTimeoutMs) {
1284
+ settle(() => reject(new Error('timeout')));
1285
+ }
1286
+ };
1287
+ if (opts.isConnected()) {
1288
+ // Already warm (e.g. a retry over an open channel) — skip the warmup
1289
+ // phase entirely and let the response deadline govern from t0.
1290
+ armResponse();
1291
+ } else {
1292
+ poll = setInterval(onPoll, pollMs);
1293
+ if (typeof poll.unref === 'function') poll.unref();
1294
+ }
1295
+ work.then(
1296
+ (val) => settle(() => resolve(val)),
1297
+ (err) => settle(() => reject(err)),
1298
+ );
1299
+ });
1300
+ }
1301
+
1205
1302
  async function probeRemoteMeshGitStatus(args: {
1206
1303
  dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
1207
1304
  daemonId: string;
1208
1305
  workspace: string;
1209
- timeoutMs: number;
1306
+ // Response deadline — applies only once the peer's DataChannel is open (warm).
1307
+ responseTimeoutMs: number;
1308
+ // Cold-open warmup budget — applies only while the channel is still opening.
1309
+ connectTimeoutMs: number;
1310
+ // Live peer connection snapshot getter; lets the deadline tell "still warming
1311
+ // up" apart from "warm but slow". Absent → behave as if always warm (the
1312
+ // response deadline governs from t0, i.e. the legacy single-budget behavior).
1313
+ getConnection?: (daemonId: string) => Record<string, unknown> | null;
1210
1314
  }): Promise<Record<string, unknown> | null> {
1211
1315
  if (!args.dispatchMeshCommand) return null;
1212
- const remoteResult = await Promise.race([
1213
- args.dispatchMeshCommand(args.daemonId, 'git_status', { workspace: args.workspace, refreshUpstream: true }),
1214
- new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), args.timeoutMs)),
1215
- ]) as any;
1316
+ // Fire the dispatch first — this is what drives the mesh manager to ensure /
1317
+ // open the peer connection. The warmup-aware deadline then charges the
1318
+ // cold-open handshake to the connect budget and only the warm round trip to
1319
+ // the response budget, so the first probe to a cold peer is no longer
1320
+ // false-timed-out before its channel has even opened.
1321
+ const dispatch = args.dispatchMeshCommand(args.daemonId, 'git_status', { workspace: args.workspace, refreshUpstream: true });
1322
+ const getConnection = args.getConnection;
1323
+ const isConnected = getConnection
1324
+ ? () => readMeshConnectionState(getConnection(args.daemonId)) === 'connected'
1325
+ : () => true;
1326
+ const remoteResult = await awaitWithWarmupDeadline(dispatch, {
1327
+ isConnected,
1328
+ connectTimeoutMs: args.connectTimeoutMs,
1329
+ responseTimeoutMs: args.responseTimeoutMs,
1330
+ }) as any;
1216
1331
  const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
1217
1332
  if (!remoteGit || typeof remoteGit !== 'object' || typeof remoteGit.isGitRepo !== 'boolean') return null;
1218
1333
  // The member daemon stamps its own platform/arch onto the git_status result
@@ -1266,13 +1381,15 @@ function isMeshConnectionDefinitivelyDown(
1266
1381
  * attempt; a non-`connected` state short-circuits the retry loop (the very first
1267
1382
  * attempt always runs so a missing connection getter still gets one try).
1268
1383
  */
1269
- async function probeRemoteMeshGitStatusWithRetry(args: {
1384
+ export async function probeRemoteMeshGitStatusWithRetry(args: {
1270
1385
  dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
1271
1386
  daemonId: string;
1272
1387
  workspace: string;
1273
1388
  timeoutMs: number;
1274
1389
  /** Per-attempt timeout for retries (attempts > 0); defaults to timeoutMs. */
1275
1390
  retryTimeoutMs?: number;
1391
+ /** Cold-open warmup budget per attempt; defaults to MESH_DIRECT_PROBE_CONNECT_TIMEOUT_MS. */
1392
+ connectTimeoutMs?: number;
1276
1393
  getConnection?: (daemonId: string) => Record<string, unknown> | null;
1277
1394
  onConnection?: (connection: Record<string, unknown>) => void;
1278
1395
  }): Promise<Record<string, unknown> | null> {
@@ -1304,7 +1421,9 @@ async function probeRemoteMeshGitStatusWithRetry(args: {
1304
1421
  dispatchMeshCommand: args.dispatchMeshCommand,
1305
1422
  daemonId: args.daemonId,
1306
1423
  workspace: args.workspace,
1307
- timeoutMs: attempt === 0 ? args.timeoutMs : (args.retryTimeoutMs ?? args.timeoutMs),
1424
+ responseTimeoutMs: attempt === 0 ? args.timeoutMs : (args.retryTimeoutMs ?? args.timeoutMs),
1425
+ connectTimeoutMs: args.connectTimeoutMs ?? MESH_DIRECT_PROBE_CONNECT_TIMEOUT_MS,
1426
+ getConnection: args.getConnection,
1308
1427
  });
1309
1428
  if (remoteGit) return remoteGit;
1310
1429
  } catch {
@@ -1451,6 +1570,7 @@ export async function hydrateInlineMeshDirectTruth(args: {
1451
1570
  workspace,
1452
1571
  timeoutMs: MESH_DIRECT_PROBE_TIMEOUT_MS,
1453
1572
  retryTimeoutMs: MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS,
1573
+ connectTimeoutMs: MESH_DIRECT_PROBE_CONNECT_TIMEOUT_MS,
1454
1574
  getConnection: args.getMeshPeerConnectionStatus,
1455
1575
  });
1456
1576
  const remoteGit = args.probeCache
@@ -1483,7 +1603,7 @@ export async function hydrateInlineMeshDirectTruth(args: {
1483
1603
  };
1484
1604
  }
1485
1605
 
1486
- function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
1606
+ export function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
1487
1607
  const meta = readObjectRecord(record?.meta);
1488
1608
  const isSelfCoordinator = Boolean(readStringValue(meta.meshCoordinatorFor));
1489
1609
  const chatStatus = readStringValue(record?.chatStatus, record?.activeChat?.status, meta.chatStatus, meta.sessionStatus);
@@ -1516,14 +1636,17 @@ function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: s
1516
1636
  if (!recordNodeId || recordNodeId !== nodeId) return false;
1517
1637
  if (nodeIsMissingLocalWorktree) return false;
1518
1638
  const recordWorkspace = readStringValue(record?.workspace);
1519
- if (nodeWorkspace && recordWorkspace && recordWorkspace !== nodeWorkspace) return false;
1639
+ // Normalized compare (shared WTCLAIM rule): a base node and a co-located worktree
1640
+ // clone differ ONLY by workspace root, so a separator/case-skewed exact compare
1641
+ // could wrongly keep a sibling worktree's session attached to this node.
1642
+ if (nodeWorkspace && recordWorkspace && !meshWorkspacesEquivalent(recordWorkspace, nodeWorkspace)) return false;
1520
1643
  const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
1521
1644
  return !recordMeshId || recordMeshId === meshId;
1522
1645
  }
1523
1646
 
1524
1647
  function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
1525
1648
  const recordWorkspace = readStringValue(record?.workspace);
1526
- if (!recordWorkspace || !workspace || recordWorkspace !== workspace) return false;
1649
+ if (!recordWorkspace || !workspace || !meshWorkspacesEquivalent(recordWorkspace, workspace)) return false;
1527
1650
 
1528
1651
  const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
1529
1652
  if (recordMeshId) return recordMeshId === meshId;
@@ -1531,7 +1654,7 @@ function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, work
1531
1654
  return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
1532
1655
  }
1533
1656
 
1534
- function readLiveMeshNodeWorkspace(args: {
1657
+ export function readLiveMeshNodeWorkspace(args: {
1535
1658
  meshId: string;
1536
1659
  nodeId: string;
1537
1660
  liveSessionRecords: any[];
@@ -1558,7 +1681,7 @@ function readLiveMeshNodeWorkspace(args: {
1558
1681
  return '';
1559
1682
  }
1560
1683
 
1561
- function collectLiveMeshSessionRecords(args: {
1684
+ export function collectLiveMeshSessionRecords(args: {
1562
1685
  meshId: string;
1563
1686
  node: any;
1564
1687
  nodeId: string;
@@ -1589,7 +1712,7 @@ function collectLiveMeshSessionRecords(args: {
1589
1712
  return matches;
1590
1713
  }
1591
1714
 
1592
- function buildHistoricalMeshSessions(args: {
1715
+ export function buildHistoricalMeshSessions(args: {
1593
1716
  meshId: string;
1594
1717
  nodes: any[];
1595
1718
  liveSessionRecords: any[];
@@ -1635,7 +1758,7 @@ function buildHistoricalMeshSessions(args: {
1635
1758
  };
1636
1759
  }
1637
1760
 
1638
- function applyCachedInlineMeshNodeStatus(
1761
+ export function applyCachedInlineMeshNodeStatus(
1639
1762
  status: Record<string, unknown>,
1640
1763
  node: any,
1641
1764
  options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
@@ -1669,7 +1792,7 @@ function applyCachedInlineMeshNodeStatus(
1669
1792
  return activeSessions.length > 0 || !!machineStatus || !!lastSeenAt || !!updatedAt;
1670
1793
  }
1671
1794
 
1672
- async function resolveProviderTypeFromPriority(args: {
1795
+ export async function resolveProviderTypeFromPriority(args: {
1673
1796
  nodeId: string;
1674
1797
  providerPriority: string[];
1675
1798
  providerLoader: ProviderLoader;
@@ -1699,7 +1822,7 @@ async function resolveProviderTypeFromPriority(args: {
1699
1822
 
1700
1823
  return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join('; ')}` };
1701
1824
  }
1702
- type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
1825
+ export type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
1703
1826
  type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
1704
1827
  type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
1705
1828
 
@@ -1849,7 +1972,7 @@ type MeshRefineSubmoduleReachabilitySummary = {
1849
1972
 
1850
1973
  type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
1851
1974
 
1852
- type MeshRefineJobHandle = {
1975
+ export type MeshRefineJobHandle = {
1853
1976
  success: true;
1854
1977
  async: true;
1855
1978
  status: MeshRefineAsyncJobStatus;
@@ -1982,6 +2105,51 @@ function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReach
1982
2105
  return `Ask the user for explicit approval to push/publish the unreachable submodule commit(s) (${refs}) to the configured submodule remote main branch, then rerun mesh_refine_node. Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.`;
1983
2106
  }
1984
2107
 
2108
+ /**
2109
+ * Async git exec helper used across the synchronous-refine stage pipeline. Bound
2110
+ * once in the orchestrator and threaded through RefineContext so every stage runs
2111
+ * git the same way (execFile + promisify, utf8). Returns the child's stdout/stderr.
2112
+ */
2113
+ type RefineExecFileAsync = (file: string, args: string[], options: { cwd: string; encoding: 'utf8' }) => Promise<{ stdout: string; stderr: string }>;
2114
+
2115
+ /**
2116
+ * Accumulated state shared by the synchronous-refine stages. The orchestrator
2117
+ * (executeMeshRefineNodeSynchronously) seeds this in the resolve_refs stage and
2118
+ * each later stage reads / extends it. `branchHead` and `patchEquivalence` are the
2119
+ * only fields a stage mutates after creation (auto-rebase updates both), so they
2120
+ * are carried on the mutable context rather than re-threaded through return types.
2121
+ */
2122
+ interface RefineContext {
2123
+ meshId: string;
2124
+ nodeId: string;
2125
+ args: any;
2126
+ refineStages: Array<Record<string, unknown>>;
2127
+ execFileAsync: RefineExecFileAsync;
2128
+ mesh: any;
2129
+ node: any;
2130
+ sourceNode: any;
2131
+ repoRoot: string;
2132
+ branch: string;
2133
+ baseBranch: string;
2134
+ baseHead: string;
2135
+ branchHead: string;
2136
+ validationSummary: Awaited<ReturnType<typeof runMeshRefineValidationGate>>;
2137
+ patchEquivalence: Awaited<ReturnType<typeof runMeshRefinePatchEquivalenceGate>>;
2138
+ submoduleReachability: Awaited<ReturnType<typeof runMeshRefineSubmoduleReachabilityGate>>;
2139
+ }
2140
+
2141
+ /**
2142
+ * Stage outcome for the synchronous-refine pipeline. A stage either produces a
2143
+ * terminal CommandRouterResult (an early-exit gate failure, or a successful
2144
+ * already-merged short-circuit), in which case the orchestrator returns it
2145
+ * immediately, or it returns `continue` with the (possibly extended) context for
2146
+ * the next stage. This makes the orchestrator a flat sequence of stage calls
2147
+ * while preserving the original body's exact early-return control flow.
2148
+ */
2149
+ type RefineStageOutcome =
2150
+ | { kind: 'terminal'; result: CommandRouterResult }
2151
+ | { kind: 'continue'; ctx: RefineContext };
2152
+
1985
2153
  function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
1986
2154
  if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
1987
2155
  process.stderr.write(
@@ -3285,18 +3453,18 @@ function loadYamlModule(): { load: (input: string) => any; dump: (input: any, op
3285
3453
  return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
3286
3454
  }
3287
3455
 
3288
- function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
3456
+ export function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
3289
3457
  return format === 'hermes_config_yaml' ? 'mcp_servers' : 'mcpServers';
3290
3458
  }
3291
3459
 
3292
- function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
3460
+ export function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
3293
3461
  if (!text.trim()) return {};
3294
3462
  if (format === 'claude_mcp_json') return JSON.parse(text);
3295
3463
  const parsed = loadYamlModule().load(text);
3296
3464
  return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
3297
3465
  }
3298
3466
 
3299
- function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
3467
+ export function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
3300
3468
  if (format === 'claude_mcp_json') return JSON.stringify(config, null, 2);
3301
3469
  return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
3302
3470
  }
@@ -3306,7 +3474,7 @@ function resolveHermesUserHome(): string {
3306
3474
  return explicitHome || pathJoin(homedir(), '.hermes');
3307
3475
  }
3308
3476
 
3309
- function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } {
3477
+ export function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } {
3310
3478
  const sourceHome = resolveHermesUserHome();
3311
3479
  const sourceConfigPath = pathJoin(sourceHome, 'config.yaml');
3312
3480
  if (!fs.existsSync(sourceConfigPath)) return { config: {}, sourceHome, sourceConfigPath };
@@ -3317,7 +3485,7 @@ function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Re
3317
3485
  return { config: baseConfig, sourceHome, sourceConfigPath };
3318
3486
  }
3319
3487
 
3320
- function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
3488
+ export function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
3321
3489
  const {
3322
3490
  model: _model,
3323
3491
  provider: _provider,
@@ -3346,7 +3514,7 @@ function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string,
3346
3514
  return sanitized;
3347
3515
  }
3348
3516
 
3349
- function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
3517
+ export function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
3350
3518
  if (pathResolve(sourceHome) === pathResolve(targetHome)) return;
3351
3519
  for (const fileName of ['.env', 'auth.json']) {
3352
3520
  const sourcePath = pathJoin(sourceHome, fileName);
@@ -3897,6 +4065,30 @@ export class DaemonCommandRouter {
3897
4065
  return ctx;
3898
4066
  }
3899
4067
 
4068
+ /**
4069
+ * Build the HighFamilyContext handed to RF-ROUTER HIGH family handlers. Binds
4070
+ * the router-private collaborators those handlers need (mesh resolution, the
4071
+ * aggregate-status memory cache + its bound read/write helpers, the
4072
+ * running-refine-job table, inline-mesh + git-probe caches, and the router's
4073
+ * own `execute` for the get_mesh_review_inbox mesh_status re-entry). HIGH
4074
+ * handlers reach more router-owned state than MED, but the binding shape is
4075
+ * the same: bound methods + direct field references, none reachable from
4076
+ * `deps`.
4077
+ */
4078
+ private buildHighFamilyContext(): HighFamilyContext {
4079
+ return {
4080
+ deps: this.deps,
4081
+ getMeshForCommand: this.getMeshForCommand.bind(this),
4082
+ getCachedAggregateMeshStatus: this.getCachedAggregateMeshStatus.bind(this),
4083
+ rememberAggregateMeshStatus: this.rememberAggregateMeshStatus.bind(this),
4084
+ execute: this.execute.bind(this),
4085
+ aggregateMeshStatusCache: this.aggregateMeshStatusCache,
4086
+ runningRefineJobs: this.runningRefineJobs,
4087
+ inlineMeshCache: this.inlineMeshCache,
4088
+ meshGitProbeCache: this.meshGitProbeCache,
4089
+ };
4090
+ }
4091
+
3900
4092
 
3901
4093
  private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
3902
4094
  const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
@@ -4801,33 +4993,77 @@ export class DaemonCommandRouter {
4801
4993
  }
4802
4994
  }
4803
4995
 
4996
+ /**
4997
+ * Synchronous refinery for a single worktree node — the gate pipeline that
4998
+ * validates, preflights (patch-equivalence / submodule-reachability /
4999
+ * no-op), merges, aligns submodules, cleans up the worktree node and
5000
+ * (optionally) pushes. The body is a flat sequence of stage methods; each
5001
+ * stage either returns a terminal CommandRouterResult (gate failure or a
5002
+ * successful already-merged short-circuit) or `continue` with the extended
5003
+ * context. Behavior — stage order, every early-exit, and every result shape —
5004
+ * is identical to the previous single inlined body.
5005
+ */
4804
5006
  private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
4805
5007
  const refineStages: Array<Record<string, unknown>> = [];
4806
5008
  try {
5009
+ const resolved = await this.refineResolveRefsStage(meshId, nodeId, args, refineStages);
5010
+ if (resolved.kind === 'terminal') return resolved.result;
5011
+ const ctx = resolved.ctx;
5012
+
5013
+ const validation = await this.refineValidationStage(ctx);
5014
+ if (validation.kind === 'terminal') return validation.result;
5015
+
5016
+ const patchEquivalence = await this.refinePatchEquivalenceStage(ctx);
5017
+ if (patchEquivalence.kind === 'terminal') return patchEquivalence.result;
5018
+
5019
+ const submoduleReachability = await this.refineSubmoduleReachabilityStage(ctx);
5020
+ if (submoduleReachability.kind === 'terminal') return submoduleReachability.result;
5021
+
5022
+ const effectiveDiff = await this.refineEffectiveDiffStage(ctx);
5023
+ if (effectiveDiff.kind === 'terminal') return effectiveDiff.result;
5024
+
5025
+ const merge = await this.refineMergeAndFinalizeStage(ctx);
5026
+ return (merge as { kind: 'terminal'; result: CommandRouterResult }).result;
5027
+ } catch (e: any) {
5028
+ return { success: false, error: e.message, refineStages };
5029
+ }
5030
+ }
5031
+
5032
+ /**
5033
+ * resolve_refs stage: resolve the mesh / worktree node / source node /
5034
+ * repoRoot, then the worktree branch, base branch, fetched base head and
5035
+ * branch head. Seeds the RefineContext consumed by every later stage.
5036
+ */
5037
+ private async refineResolveRefsStage(
5038
+ meshId: string,
5039
+ nodeId: string,
5040
+ args: any,
5041
+ refineStages: Array<Record<string, unknown>>,
5042
+ ): Promise<RefineStageOutcome> {
4807
5043
  // preferInline: same as startMeshRefineJob — inline-cache-only clone nodes must resolve.
4808
5044
  const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
4809
5045
  const mesh = meshRecord?.mesh;
4810
5046
  const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
4811
- if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
5047
+ if (!node) return { kind: 'terminal', result: { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages } };
4812
5048
 
4813
5049
  if (!node.isLocalWorktree || !node.workspace) {
4814
- return { success: false, error: `Refinery requires a local worktree node`, refineStages };
5050
+ return { kind: 'terminal', result: { success: false, error: `Refinery requires a local worktree node`, refineStages } };
4815
5051
  }
4816
5052
 
4817
5053
  const sourceNode = node.clonedFromNodeId
4818
5054
  ? mesh?.nodes.find((n: any) => meshNodeIdMatches(n, node.clonedFromNodeId))
4819
5055
  : mesh?.nodes.find((n: any) => !n.isLocalWorktree);
4820
5056
  const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
4821
- if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
5057
+ if (!repoRoot) return { kind: 'terminal', result: { success: false, error: 'Source node repoRoot not found', refineStages } };
4822
5058
 
4823
5059
  const { execFile } = await import('node:child_process');
4824
5060
  const { promisify } = await import('node:util');
4825
- const execFileAsync = promisify(execFile);
5061
+ const execFileAsync = promisify(execFile) as unknown as RefineExecFileAsync;
4826
5062
 
4827
5063
  const resolveStarted = Date.now();
4828
5064
  const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
4829
5065
  const branch = branchStdout.trim();
4830
- if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
5066
+ if (!branch) return { kind: 'terminal', result: { success: false, error: 'Could not determine branch of the worktree node', refineStages } };
4831
5067
 
4832
5068
  const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
4833
5069
  const baseBranch = baseBranchStdout.trim();
@@ -4854,9 +5090,39 @@ export class DaemonCommandRouter {
4854
5090
 
4855
5091
  const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
4856
5092
  const baseHead = baseHeadRaw;
4857
- let branchHead = branchHeadStdout.trim();
5093
+ const branchHead = branchHeadStdout.trim();
4858
5094
  recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead, ...(fetchWarning ? { fetchWarning } : {}) });
4859
5095
 
5096
+ return {
5097
+ kind: 'continue',
5098
+ ctx: {
5099
+ meshId,
5100
+ nodeId,
5101
+ args,
5102
+ refineStages,
5103
+ execFileAsync,
5104
+ mesh,
5105
+ node,
5106
+ sourceNode,
5107
+ repoRoot,
5108
+ branch,
5109
+ baseBranch,
5110
+ baseHead,
5111
+ branchHead,
5112
+ validationSummary: undefined as any,
5113
+ patchEquivalence: undefined as any,
5114
+ submoduleReachability: undefined as any,
5115
+ },
5116
+ };
5117
+ }
5118
+
5119
+ /**
5120
+ * validation stage: run the refinery validation gate (typecheck / test /
5121
+ * lint / build per node config) and block on failure or when no allowlisted
5122
+ * command was available. On pass, stores the summary on the context.
5123
+ */
5124
+ private async refineValidationStage(ctx: RefineContext): Promise<RefineStageOutcome> {
5125
+ const { mesh, node, branch, baseBranch, refineStages } = ctx;
4860
5126
  const validationStarted = Date.now();
4861
5127
  const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace, {
4862
5128
  // M2-2: consume the node's persisted bootstrap state; persist re-runs.
@@ -4868,6 +5134,7 @@ export class DaemonCommandRouter {
4868
5134
  .catch(() => { /* persistence is best-effort */ });
4869
5135
  },
4870
5136
  });
5137
+ ctx.validationSummary = validationSummary;
4871
5138
  recordMeshRefineStage(
4872
5139
  refineStages,
4873
5140
  'validation',
@@ -4903,7 +5170,7 @@ export class DaemonCommandRouter {
4903
5170
  tail ? `Output (tail):\n${tail}` : '',
4904
5171
  ].filter(Boolean).join('\n');
4905
5172
  };
4906
- return {
5173
+ return { kind: 'terminal', result: {
4907
5174
  success: false,
4908
5175
  code: validationSummary.failureCode || 'validation_failed',
4909
5176
  convergenceStatus: 'blocked_review',
@@ -4920,10 +5187,10 @@ export class DaemonCommandRouter {
4920
5187
  validation: 'failed',
4921
5188
  status: 'blocked_review',
4922
5189
  },
4923
- };
5190
+ } };
4924
5191
  }
4925
5192
  if (validationSummary.status === 'skipped') {
4926
- return {
5193
+ return { kind: 'terminal', result: {
4927
5194
  success: false,
4928
5195
  code: 'validation_unavailable',
4929
5196
  convergenceStatus: 'blocked_review',
@@ -4940,9 +5207,22 @@ export class DaemonCommandRouter {
4940
5207
  validation: 'unavailable',
4941
5208
  status: 'blocked_review',
4942
5209
  },
4943
- };
5210
+ } };
4944
5211
  }
4945
5212
 
5213
+ return { kind: 'continue', ctx };
5214
+ }
5215
+
5216
+ /**
5217
+ * patch_equivalence stage: preflight that the worktree branch's cumulative
5218
+ * patch is equivalent to base+branch. On a "behind base" branch, auto-rebase
5219
+ * once and re-check; on an empty merge-tree with real branch changes, treat as
5220
+ * already-merged-via-another-path and short-circuit to cleanup. Mutates the
5221
+ * context's branchHead (after rebase) and patchEquivalence (rebased gate).
5222
+ */
5223
+ private async refinePatchEquivalenceStage(ctx: RefineContext): Promise<RefineStageOutcome> {
5224
+ const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, validationSummary, refineStages, execFileAsync } = ctx;
5225
+ let branchHead = ctx.branchHead;
4946
5226
  const patchEquivalenceStarted = Date.now();
4947
5227
  let patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
4948
5228
  recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
@@ -4985,7 +5265,7 @@ export class DaemonCommandRouter {
4985
5265
  patchEquivalence = rebasedPatchEquivalence;
4986
5266
  didAutoRebase = true;
4987
5267
  } else {
4988
- return {
5268
+ return { kind: 'terminal', result: {
4989
5269
  success: false,
4990
5270
  code: 'needs_rebase',
4991
5271
  convergenceStatus: 'blocked_review',
@@ -5004,14 +5284,14 @@ export class DaemonCommandRouter {
5004
5284
  patchEquivalence: 'failed',
5005
5285
  status: 'blocked_review',
5006
5286
  },
5007
- };
5287
+ } };
5008
5288
  }
5009
5289
  } catch (rebaseErr: any) {
5010
5290
  try { execFileSync('git', ['rebase', '--abort'], { cwd: node.workspace, stdio: 'ignore' }); } catch { /* ignore */ }
5011
5291
  recordMeshRefineStage(refineStages, 'patch_equivalence_after_auto_rebase', 'failed', autoRebaseStarted, {
5012
5292
  error: rebaseErr?.message || String(rebaseErr),
5013
5293
  });
5014
- return {
5294
+ return { kind: 'terminal', result: {
5015
5295
  success: false,
5016
5296
  code: 'needs_rebase_with_conflicts',
5017
5297
  convergenceStatus: 'blocked_review',
@@ -5030,7 +5310,7 @@ export class DaemonCommandRouter {
5030
5310
  patchEquivalence: 'failed',
5031
5311
  status: 'blocked_review',
5032
5312
  },
5033
- };
5313
+ } };
5034
5314
  }
5035
5315
  }
5036
5316
 
@@ -5046,7 +5326,7 @@ export class DaemonCommandRouter {
5046
5326
  // is a degenerate worktree case, not an "already merged" scenario.
5047
5327
  const alreadyMergedViaOtherPath = !patchEquivalence.actualPatchId && !!patchEquivalence.expectedPatchId;
5048
5328
  if (!didAutoRebase && !alreadyMergedViaOtherPath) {
5049
- return {
5329
+ return { kind: 'terminal', result: {
5050
5330
  success: false,
5051
5331
  code: 'patch_equivalence_failed',
5052
5332
  convergenceStatus: 'blocked_review',
@@ -5065,7 +5345,7 @@ export class DaemonCommandRouter {
5065
5345
  patchEquivalence: 'failed',
5066
5346
  status: 'blocked_review',
5067
5347
  },
5068
- };
5348
+ } };
5069
5349
  }
5070
5350
 
5071
5351
  if (!didAutoRebase && alreadyMergedViaOtherPath) {
@@ -5094,7 +5374,7 @@ export class DaemonCommandRouter {
5094
5374
  payload: { alreadyMergedViaOtherPath: true, branch, into: baseBranch, validationSummary, patchEquivalence },
5095
5375
  });
5096
5376
  } catch { /* ledger append is best-effort */ }
5097
- return {
5377
+ return { kind: 'terminal', result: {
5098
5378
  success: removeResult?.success !== false,
5099
5379
  code: 'already_merged',
5100
5380
  merged: false,
@@ -5116,10 +5396,23 @@ export class DaemonCommandRouter {
5116
5396
  patchEquivalence: 'already_merged',
5117
5397
  status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged_to_main',
5118
5398
  },
5119
- };
5399
+ } };
5120
5400
  }
5121
5401
  }
5122
5402
 
5403
+ ctx.branchHead = branchHead;
5404
+ ctx.patchEquivalence = patchEquivalence;
5405
+ return { kind: 'continue', ctx };
5406
+ }
5407
+
5408
+ /**
5409
+ * submodule_reachability stage: verify every submodule gitlink commit that
5410
+ * would land via the merge is reachable from its configured remote main
5411
+ * branch (optionally auto-publishing when policy allows). Blocks the merge
5412
+ * when any commit is unreachable. Stores the result on the context.
5413
+ */
5414
+ private async refineSubmoduleReachabilityStage(ctx: RefineContext): Promise<RefineStageOutcome> {
5415
+ const { mesh, node, repoRoot, branch, baseBranch, branchHead, validationSummary, patchEquivalence, refineStages } = ctx;
5123
5416
  const submoduleReachabilityStarted = Date.now();
5124
5417
  const autoPublishSubmoduleMainCommits = resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace);
5125
5418
  const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
@@ -5176,7 +5469,7 @@ export class DaemonCommandRouter {
5176
5469
  });
5177
5470
  if (submoduleReachability.status === 'failed') {
5178
5471
  const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
5179
- return {
5472
+ return { kind: 'terminal', result: {
5180
5473
  success: false,
5181
5474
  code: 'submodule_reachability_failed',
5182
5475
  convergenceStatus: 'blocked_review',
@@ -5224,9 +5517,21 @@ export class DaemonCommandRouter {
5224
5517
  reason: 'submodule_publish_required',
5225
5518
  nextStep,
5226
5519
  },
5227
- };
5520
+ } };
5228
5521
  }
5229
5522
 
5523
+ ctx.submoduleReachability = submoduleReachability;
5524
+ return { kind: 'continue', ctx };
5525
+ }
5526
+
5527
+ /**
5528
+ * effective_diff stage (no-op guard): block a silent no-op merge where the
5529
+ * branch produces no effective root-tree diff against base — typically a
5530
+ * submodule that has commits but whose root-level gitlink (pointer) bump was
5531
+ * never committed, so the merge would land nothing real on main.
5532
+ */
5533
+ private async refineEffectiveDiffStage(ctx: RefineContext): Promise<RefineStageOutcome> {
5534
+ const { repoRoot, baseHead, branchHead, branch, baseBranch, validationSummary, patchEquivalence, refineStages } = ctx;
5230
5535
  // No-op guard: block a silent no-op merge where the root tree is identical to base.
5231
5536
  // This catches the trap where a submodule has commits but the root branch never
5232
5537
  // committed the gitlink (oss-pointer) bump — merging would report success while the
@@ -5248,7 +5553,7 @@ export class DaemonCommandRouter {
5248
5553
  hintLines.length ? `Submodules with uncommitted pointer bumps:\n${hintLines.join('\n')}` : '',
5249
5554
  `Fix: commit the submodule pointer bump on '${branch}' (git add <submodule-path> && git commit), then re-run refine.`,
5250
5555
  ].filter(Boolean).join('\n');
5251
- return {
5556
+ return { kind: 'terminal', result: {
5252
5557
  success: false,
5253
5558
  code: 'no_effective_diff',
5254
5559
  convergenceStatus: 'blocked_review',
@@ -5271,9 +5576,20 @@ export class DaemonCommandRouter {
5271
5576
  reason: 'no_effective_diff',
5272
5577
  ...(effectiveDiff.submoduleHints?.length ? { submoduleHints: effectiveDiff.submoduleHints } : {}),
5273
5578
  },
5274
- };
5579
+ } };
5275
5580
  }
5276
5581
 
5582
+ return { kind: 'continue', ctx };
5583
+ }
5584
+
5585
+ /**
5586
+ * merge + finalize stage: perform the --no-ff merge, align submodule
5587
+ * checkouts after merge, clean up (remove) the worktree node per policy,
5588
+ * append the refinery ledger entry, and (unless approval is required) push the
5589
+ * base branch. Always terminal — produces the final CommandRouterResult.
5590
+ */
5591
+ private async refineMergeAndFinalizeStage(ctx: RefineContext): Promise<RefineStageOutcome> {
5592
+ const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, sourceNode, validationSummary, patchEquivalence, submoduleReachability, mesh, refineStages, execFileAsync } = ctx;
5277
5593
  let mergeResult: Record<string, unknown> | undefined;
5278
5594
  const mergeStarted = Date.now();
5279
5595
  try {
@@ -5290,7 +5606,7 @@ export class DaemonCommandRouter {
5290
5606
  stdout: truncateValidationOutput(e?.stdout),
5291
5607
  stderr: truncateValidationOutput(e?.stderr),
5292
5608
  });
5293
- return {
5609
+ return { kind: 'terminal', result: {
5294
5610
  success: false,
5295
5611
  error: `Merge failed (conflicts?): ${e.message}`,
5296
5612
  validationSummary,
@@ -5305,7 +5621,7 @@ export class DaemonCommandRouter {
5305
5621
  patchEquivalence: 'passed',
5306
5622
  status: 'not_mergeable',
5307
5623
  },
5308
- };
5624
+ } };
5309
5625
  }
5310
5626
 
5311
5627
  const submoduleAlignmentStarted = Date.now();
@@ -5325,7 +5641,7 @@ export class DaemonCommandRouter {
5325
5641
  });
5326
5642
  }
5327
5643
  if (submoduleAlignment.status === 'failed') {
5328
- return {
5644
+ return { kind: 'terminal', result: {
5329
5645
  success: false,
5330
5646
  code: 'post_merge_submodule_alignment_failed',
5331
5647
  error: 'Refinery merge completed but post-merge submodule checkout alignment failed; run the reported git submodule update command and re-check base workspace status.',
@@ -5351,7 +5667,7 @@ export class DaemonCommandRouter {
5351
5667
  status: 'post_merge_alignment_failed',
5352
5668
  nextStep: submoduleAlignment.command || 'Run git submodule update --init --recursive for the reported path(s), then re-check base workspace status.',
5353
5669
  },
5354
- };
5670
+ } };
5355
5671
  }
5356
5672
 
5357
5673
  const cleanupStarted = Date.now();
@@ -5431,7 +5747,7 @@ export class DaemonCommandRouter {
5431
5747
  };
5432
5748
 
5433
5749
  if (removeResult?.success === false) {
5434
- return {
5750
+ return { kind: 'terminal', result: {
5435
5751
  success: false,
5436
5752
  code: 'cleanup_failed',
5437
5753
  error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
@@ -5447,7 +5763,7 @@ export class DaemonCommandRouter {
5447
5763
  refineStages,
5448
5764
  ...(ledgerError ? { ledgerError } : {}),
5449
5765
  finalBranchConvergenceState,
5450
- };
5766
+ } };
5451
5767
  }
5452
5768
 
5453
5769
  // Push logic: after a successful merge, either auto-push or surface push info
@@ -5474,7 +5790,7 @@ export class DaemonCommandRouter {
5474
5790
  }
5475
5791
  }
5476
5792
 
5477
- return {
5793
+ return { kind: 'terminal', result: {
5478
5794
  success: true,
5479
5795
  merged: true,
5480
5796
  branch,
@@ -5496,10 +5812,7 @@ export class DaemonCommandRouter {
5496
5812
  pushCommand: `git push origin ${baseBranch}`,
5497
5813
  pushNote: 'requireApprovalForPush is enabled — run the push command or obtain user approval before pushing.',
5498
5814
  }),
5499
- };
5500
- } catch (e: any) {
5501
- return { success: false, error: e.message, refineStages };
5502
- }
5815
+ } };
5503
5816
  }
5504
5817
 
5505
5818
  /**
@@ -6231,1179 +6544,19 @@ export class DaemonCommandRouter {
6231
6544
  return await medFamilyHandler(this.buildMedFamilyContext(), args);
6232
6545
  }
6233
6546
 
6234
- switch (cmd) {
6235
- // ─── CLI / ACP commands ───
6236
- case 'mesh_forward_event': {
6237
- return handleMeshForwardEvent({ instanceManager: this.deps.instanceManager } as any, args as Record<string, unknown>);
6238
- }
6239
-
6240
- case 'get_pending_mesh_events': {
6241
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6242
- // (B3) Respect coordinatorDaemonId when the caller declares it
6243
- // so unicast events route to the right coordinator instead of
6244
- // being silently consumed by the first drainer.
6245
- const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
6246
- ? args.coordinatorDaemonId.trim()
6247
- : undefined;
6248
- const events = drainPendingMeshCoordinatorEvents(meshId || undefined, coordinatorDaemonId);
6249
- return { success: true, events };
6250
- }
6251
-
6252
- case 'interactive_prompt_response': {
6253
- const sessionId = typeof args?.targetSessionId === 'string' && args.targetSessionId.trim()
6254
- ? args.targetSessionId.trim()
6255
- : typeof args?.sessionId === 'string' && args.sessionId.trim()
6256
- ? args.sessionId.trim()
6257
- : '';
6258
- if (!sessionId) return { success: false, error: 'targetSessionId required' };
6259
- const response = normalizeInteractivePromptResponse(args?.response ?? args);
6260
- const instance = this.deps.instanceManager.getInstance(sessionId);
6261
- if (!instance) return { success: false, error: `No running instance for session ${sessionId}` };
6262
- this.deps.instanceManager.sendEvent(sessionId, 'interactive_prompt_response', response);
6263
- return { success: true };
6264
- }
6265
-
6266
- // ─── Mesh Coordinator Launch ───
6267
- case 'launch_mesh_coordinator': {
6268
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6269
- let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
6270
- // Optional per-launch system-prompt addition. Dashboard or API
6271
- // callers (e.g. when spawning a mesh-node-specific coordinator)
6272
- // can pass extra context that gets appended to the rendered
6273
- // default prompt under the "## Additional Context" section.
6274
- // Going through buildCoordinatorSystemPrompt's userInstruction
6275
- // means user-level override files (~/.adhdev/coordinator-prompts)
6276
- // and this per-launch addition compose cleanly: an override
6277
- // wins outright, but if there's no override, the default
6278
- // prompt + the optional append.md file + this extra context
6279
- // all stack in declared order.
6280
- const extraSystemPrompt = typeof args?.extraSystemPrompt === 'string'
6281
- ? args.extraSystemPrompt.trim()
6282
- : '';
6283
- if (!meshId) return { success: false, error: 'meshId required' };
6284
-
6285
- try {
6286
- const { buildCoordinatorSystemPrompt } = await import('../mesh/coordinator-prompt.js');
6287
- const { buildMissionPromptSection } = await import('../mesh/mesh-missions.js');
6288
- // M3-3: inject the active mission summary into the coordinator prompt.
6289
- // Best-effort — a store failure must not block coordinator launch.
6290
- const buildMissionSectionBestEffort = (id: string): string => {
6291
- try { return buildMissionPromptSection(id); } catch { return ''; }
6292
- };
6293
-
6294
- // Support inline mesh data from cloud (bypasses local meshes.json lookup)
6295
- let mesh: any;
6296
- if (args?.inlineMesh && typeof args.inlineMesh === 'object') {
6297
- mesh = args.inlineMesh;
6298
- // Cache cloud mesh so the MCP server can retrieve it via get_mesh
6299
- this.inlineMeshCache.set(meshId, mesh);
6300
- } else {
6301
- const { getMesh } = await import('../config/mesh-config.js');
6302
- mesh = getMesh(meshId);
6303
- }
6304
- if (!mesh) return { success: false, error: 'Mesh not found' };
6305
- const meshHost = resolveMeshHostStatus(mesh);
6306
- if (!meshHost.canOwnCoordinator) {
6307
- return {
6308
- success: false,
6309
- ...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
6310
- meshId,
6311
- cliType,
6312
- };
6313
- }
6314
- if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
6315
-
6316
- const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
6317
- ? args.coordinatorNodeId.trim()
6318
- : '';
6319
- const preferredCoordinatorNodeId = requestedCoordinatorNodeId
6320
- || (typeof mesh.coordinator?.preferredNodeId === 'string' ? mesh.coordinator.preferredNodeId.trim() : '');
6321
- const coordinatorNode = preferredCoordinatorNodeId
6322
- ? mesh.nodes.find((node: any) => node?.id === preferredCoordinatorNodeId || node?.nodeId === preferredCoordinatorNodeId)
6323
- : mesh.nodes[0];
6324
- if (!coordinatorNode) {
6325
- return {
6326
- success: false,
6327
- code: 'mesh_coordinator_node_not_found',
6328
- error: `Coordinator node ${preferredCoordinatorNodeId} was not found in mesh`,
6329
- meshId,
6330
- cliType,
6331
- };
6332
- }
6333
- const sessionHostRecords = this.deps.sessionHostControl?.listSessions
6334
- ? await this.deps.sessionHostControl.listSessions().catch(() => [])
6335
- : [];
6336
- const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
6337
- const workspace = readLiveMeshNodeWorkspace({
6338
- meshId,
6339
- nodeId: String(normalizeMeshNodeId(coordinatorNode) || preferredCoordinatorNodeId || ''),
6340
- liveSessionRecords: liveMeshSessions,
6341
- allowCoordinatorSession: true,
6342
- }) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
6343
- if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
6344
- if (!cliType) {
6345
- const resolved = await resolveProviderTypeFromPriority({
6346
- nodeId: String(normalizeMeshNodeId(coordinatorNode) || preferredCoordinatorNodeId || 'coordinator'),
6347
- providerPriority: readProviderPriorityFromPolicy(coordinatorNode.policy),
6348
- providerLoader: this.deps.providerLoader,
6349
- onStatusChange: this.deps.onStatusChange,
6350
- });
6351
- if (!resolved.providerType) {
6352
- return {
6353
- success: false,
6354
- code: 'mesh_coordinator_provider_priority_unusable',
6355
- error: resolved.error || 'No usable provider found from node providerPriority',
6356
- meshId,
6357
- cliType,
6358
- workspace,
6359
- };
6360
- }
6361
- cliType = resolved.providerType;
6362
- }
6363
- const providerMeta = this.deps.providerLoader.resolve?.(cliType) || this.deps.providerLoader.getMeta(cliType);
6364
- const coordinatorSetup = resolveMeshCoordinatorSetup({
6365
- provider: providerMeta,
6366
- cliType,
6367
- meshId,
6368
- workspace,
6369
- });
6370
-
6371
- if (coordinatorSetup.kind === 'unsupported') {
6372
- return {
6373
- success: false,
6374
- code: 'mesh_coordinator_unsupported',
6375
- error: coordinatorSetup.reason,
6376
- meshId,
6377
- cliType,
6378
- workspace,
6379
- };
6380
- }
6381
-
6382
- if (coordinatorSetup.kind === 'manual') {
6383
- return {
6384
- success: false,
6385
- code: 'mesh_coordinator_manual_mcp_setup_required',
6386
- error: coordinatorSetup.instructions,
6387
- meshId,
6388
- cliType,
6389
- workspace,
6390
- meshCoordinatorSetup: coordinatorSetup,
6391
- };
6392
- }
6393
-
6394
- // ─── CLI-command MCP registration (Codex, Gemini CLI) ───────────
6395
- if (coordinatorSetup.kind === 'cli_command') {
6396
- // Build coordinator prompt first — fail closed on errors.
6397
- let cliCmdSystemPrompt = '';
6398
- try {
6399
- cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id) });
6400
- } catch (error: any) {
6401
- const message = error?.message || String(error);
6402
- LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
6403
- return {
6404
- success: false,
6405
- code: 'mesh_coordinator_prompt_failed',
6406
- error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
6407
- meshId, cliType, workspace,
6408
- };
6409
- }
6410
-
6411
- // Run the provider's MCP registration command under a
6412
- // PTY. Some providers (agy, future bubbletea CLIs)
6413
- // refuse to run without /dev/tty, so pipe-only
6414
- // execFileSync silently no-ops the registration and
6415
- // the coordinator ends up without any mcp tools. With
6416
- // a real PTY the registration goes through and the
6417
- // exit code tells us whether it actually persisted.
6418
- let mcpRegistrationOk = false;
6419
- let mcpRegistrationFailure: {
6420
- command: string;
6421
- output: string;
6422
- exitCode: number | null;
6423
- signal: number | null;
6424
- timedOut: boolean;
6425
- } | null = null;
6426
- try {
6427
- const { buildMeshCoordinatorRegistrationPlan, execUnderPty } = await import('./mesh-coordinator.js');
6428
- const registrationPlan = buildMeshCoordinatorRegistrationPlan(
6429
- cliType,
6430
- coordinatorSetup.serverName,
6431
- coordinatorSetup.command,
6432
- );
6433
- for (const step of registrationPlan) {
6434
- const renderedCommand = [step.command, ...step.args].join(' ');
6435
- LOG.info('MeshCoordinator', `Running MCP ${step.label} (pty): ${renderedCommand}`);
6436
- const ptyResult = await execUnderPty(step.command, step.args, { cwd: workspace, timeoutMs: 20_000 });
6437
- if (ptyResult.exitCode === 0 && !ptyResult.timedOut) {
6438
- if (step.required) mcpRegistrationOk = true;
6439
- continue;
6440
- }
6441
- LOG.warn('MeshCoordinator', `MCP ${step.label} failed exit=${ptyResult.exitCode} signal=${ptyResult.signal} timedOut=${ptyResult.timedOut} — output:\n${ptyResult.output.slice(-2000)}`);
6442
- if (step.required) {
6443
- mcpRegistrationFailure = {
6444
- command: renderedCommand,
6445
- output: ptyResult.output.slice(-2000),
6446
- exitCode: ptyResult.exitCode,
6447
- signal: ptyResult.signal,
6448
- timedOut: ptyResult.timedOut,
6449
- };
6450
- break;
6451
- }
6452
- }
6453
- } catch (error: any) {
6454
- LOG.warn('MeshCoordinator', `MCP registration command failed: ${error?.message || error}`);
6455
- mcpRegistrationFailure = {
6456
- command: coordinatorSetup.command,
6457
- output: error?.message || String(error),
6458
- exitCode: null,
6459
- signal: null,
6460
- timedOut: false,
6461
- };
6462
- }
6463
-
6464
- if (!mcpRegistrationOk) {
6465
- return {
6466
- success: false,
6467
- code: 'mesh_coordinator_mcp_registration_failed',
6468
- error: `Could not register ${coordinatorSetup.serverName}; coordinator session was not launched`,
6469
- meshId,
6470
- cliType,
6471
- workspace,
6472
- registration: mcpRegistrationFailure,
6473
- };
6474
- }
6475
-
6476
- // Codex gives repo-local .mcp.json precedence over its
6477
- // global `codex mcp add` registration. Refresh an
6478
- // existing ADHDev entry so a stale workspace command
6479
- // cannot shadow the registration we just verified.
6480
- if (cliType === 'codex-cli') {
6481
- const repoMcpConfigPath = pathJoin(workspace, '.mcp.json');
6482
- if (fs.existsSync(repoMcpConfigPath)) {
6483
- try {
6484
- const repoMcpConfig = parseMeshCoordinatorMcpConfig(
6485
- fs.readFileSync(repoMcpConfigPath, 'utf-8'),
6486
- 'claude_mcp_json',
6487
- );
6488
- const existingServers = repoMcpConfig.mcpServers;
6489
- if (
6490
- existingServers
6491
- && typeof existingServers === 'object'
6492
- && !Array.isArray(existingServers)
6493
- && existingServers[coordinatorSetup.serverName]
6494
- ) {
6495
- fs.writeFileSync(repoMcpConfigPath, serializeMeshCoordinatorMcpConfig({
6496
- ...repoMcpConfig,
6497
- mcpServers: {
6498
- ...existingServers,
6499
- [coordinatorSetup.serverName]: coordinatorSetup.mcpServer,
6500
- },
6501
- }, 'claude_mcp_json'), 'utf-8');
6502
- LOG.info('MeshCoordinator', `Refreshed repo-local ${repoMcpConfigPath} entry for ${coordinatorSetup.serverName}`);
6503
- }
6504
- } catch (error: any) {
6505
- return {
6506
- success: false,
6507
- code: 'mesh_coordinator_config_write_failed',
6508
- error: `Could not refresh repo-local MCP config: ${error?.message || error}`,
6509
- meshId,
6510
- cliType,
6511
- workspace,
6512
- };
6513
- }
6514
- }
6515
- }
6516
-
6517
- // Inject system prompt declaratively from provider.v1.json.
6518
- const cliCmdArgs: string[] = [];
6519
- const cliCmdEnv: Record<string, string> = {};
6520
- let cliCmdContextFilePath: string | undefined;
6521
- if (cliCmdSystemPrompt) {
6522
- const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
6523
- const effect = applyMeshCoordinatorSystemPromptInjection(
6524
- cliCmdSystemPrompt,
6525
- providerMeta?.meshCoordinator?.systemPromptInjection,
6526
- { cliArgs: cliCmdArgs, launchEnv: cliCmdEnv, workspace, cliType },
6527
- );
6528
- cliCmdContextFilePath = effect.contextFilePath;
6529
- }
6530
-
6531
- const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
6532
- cliType,
6533
- dir: workspace,
6534
- cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
6535
- env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
6536
- settings: { meshCoordinatorFor: meshId },
6537
- });
6538
-
6539
- // R48 inject-then-remove. Spawn was just kicked off above; agy and
6540
- // gemini-cli read AGENTS.md / GEMINI.md exactly once at startup and
6541
- // cache it for the rest of the session, so we can safely strip
6542
- // the wrapper from disk shortly after launch. That keeps any
6543
- // worker session launched into the same workspace later from
6544
- // picking up our wrapper block.
6545
- if (cliCmdLaunch?.success && cliCmdContextFilePath) {
6546
- const stripPath = cliCmdContextFilePath;
6547
- setTimeout(() => {
6548
- void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
6549
- stripCoordinatorWrapperFile(stripPath);
6550
- LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (cli_command)`);
6551
- }).catch(() => { /* best-effort */ });
6552
- }, 5000);
6553
- }
6554
-
6555
- if (!cliCmdLaunch?.success) {
6556
- return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
6557
- }
6558
-
6559
- LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
6560
- const cliCmdSessionId = cliCmdLaunch.sessionId || cliCmdLaunch.id;
6561
- if (cliCmdSessionId) {
6562
- const cliCmdInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
6563
- registerMeshCoordinator({
6564
- meshId,
6565
- sessionId: cliCmdSessionId,
6566
- workspace,
6567
- startedAt: Date.now(),
6568
- cliType,
6569
- systemPrompt: cliCmdSystemPrompt || undefined,
6570
- extraSystemPrompt: extraSystemPrompt || undefined,
6571
- injection: cliCmdInjectionDecl ? {
6572
- mode: cliCmdInjectionDecl.mode,
6573
- target: 'flag' in cliCmdInjectionDecl ? cliCmdInjectionDecl.flag
6574
- : 'name' in cliCmdInjectionDecl ? cliCmdInjectionDecl.name
6575
- : 'path' in cliCmdInjectionDecl ? cliCmdInjectionDecl.path
6576
- : undefined,
6577
- } : undefined,
6578
- });
6579
- }
6580
- try {
6581
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
6582
- appendLedgerEntry(meshId, {
6583
- kind: 'coordinator_started',
6584
- sessionId: cliCmdSessionId,
6585
- providerType: cliType,
6586
- payload: { workspace },
6587
- });
6588
- } catch { /* best-effort */ }
6589
-
6590
- return {
6591
- success: true,
6592
- meshId,
6593
- cliType,
6594
- workspace,
6595
- sessionId: cliCmdSessionId,
6596
- mcpRegistered: mcpRegistrationOk,
6597
- };
6598
- }
6599
-
6600
- const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
6601
- if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
6602
- return {
6603
- success: false,
6604
- code: 'mesh_coordinator_unsupported',
6605
- error: `Unsupported auto-import MCP config format: ${String(coordinatorSetup.configFormat)}`,
6606
- meshId,
6607
- cliType,
6608
- workspace,
6609
- };
6610
- }
6611
-
6612
- // Build the coordinator prompt before mutating workspace config or launching.
6613
- // Prompt generation failures are configuration/data-shape errors; fail closed so
6614
- // broken mesh state is visible instead of silently launching with weaker rules.
6615
- let systemPrompt = '';
6616
- try {
6617
- systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id) });
6618
- } catch (error: any) {
6619
- const message = error?.message || String(error);
6620
- LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
6621
- return {
6622
- success: false,
6623
- code: 'mesh_coordinator_prompt_failed',
6624
- error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
6625
- meshId,
6626
- cliType,
6627
- workspace,
6628
- };
6629
- }
6630
-
6631
- // 1. Write provider-declared MCP config for CLIs that auto-import it.
6632
- const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
6633
- const { dirname } = await import('path');
6634
- const mcpConfigPath = coordinatorSetup.configPath;
6635
- const hermesManualFallback = cliType === 'hermes-cli' && configFormat === 'hermes_config_yaml'
6636
- ? createHermesManualMeshCoordinatorSetup(meshId, workspace)
6637
- : null;
6638
- let hermesBaseConfig: { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } | null = null;
6639
- if (hermesManualFallback) {
6640
- try {
6641
- hermesBaseConfig = loadHermesCoordinatorBaseConfig(mcpConfigPath);
6642
- } catch (error: any) {
6643
- const message = `Failed to parse Hermes base config for automatic coordinator setup: ${error?.message || error}`;
6644
- LOG.error('MeshCoordinator', message);
6645
- return { success: false, code: 'mesh_coordinator_config_parse_failed', error: message, meshId, cliType, workspace };
6646
- }
6647
- }
6648
- const returnManualFallback = (message: string) => ({
6649
- success: false,
6650
- code: 'mesh_coordinator_manual_mcp_setup_required',
6651
- error: message,
6652
- meshId,
6653
- cliType,
6654
- workspace,
6655
- meshCoordinatorSetup: hermesManualFallback,
6656
- });
6657
-
6658
- // Merge ADHDev mesh server into existing config.
6659
- // Pass full mesh data as env var so the MCP server can bootstrap
6660
- // without depending on meshes.json or a running daemon.
6661
- const mcpServerEntry: Record<string, any> = {
6662
- command: coordinatorSetup.mcpServer.command,
6663
- args: coordinatorSetup.mcpServer.args,
6664
- };
6665
- if (args?.inlineMesh) {
6666
- const modeArgIndex = coordinatorSetup.mcpServer.args.findIndex((value: string) => value === '--mode');
6667
- const mcpTransport = modeArgIndex >= 0 ? coordinatorSetup.mcpServer.args[modeArgIndex + 1] : 'ipc';
6668
- mcpServerEntry.env = {
6669
- ADHDEV_INLINE_MESH: JSON.stringify(mesh),
6670
- ADHDEV_MCP_TRANSPORT: mcpTransport === 'local' ? 'local' : 'ipc',
6671
- };
6672
- }
6673
-
6674
- try {
6675
- mkdirSync(dirname(mcpConfigPath), { recursive: true });
6676
- } catch (error: any) {
6677
- const message = `Could not prepare MCP config path for automatic setup: ${error?.message || error}`;
6678
- LOG.error('MeshCoordinator', message);
6679
- if (hermesManualFallback) return returnManualFallback(message);
6680
- return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
6681
- }
6682
-
6683
- // Backup existing MCP config if present.
6684
- const hadExistingMcpConfig = existsSync(mcpConfigPath);
6685
- let existingMcpConfig: Record<string, any> = hermesBaseConfig?.config || {};
6686
- if (hermesBaseConfig) {
6687
- copyHermesCoordinatorCredentialFiles(hermesBaseConfig.sourceHome, dirname(mcpConfigPath));
6688
- }
6689
- if (hadExistingMcpConfig) {
6690
- try {
6691
- const parsedExistingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
6692
- const existingCoordinatorConfig = hermesManualFallback
6693
- ? stripHermesCoordinatorTempModelProviderOverrides(parsedExistingMcpConfig)
6694
- : parsedExistingMcpConfig;
6695
- existingMcpConfig = { ...existingMcpConfig, ...existingCoordinatorConfig };
6696
- copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
6697
- } catch (error: any) {
6698
- LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
6699
- return {
6700
- success: false,
6701
- code: 'mesh_coordinator_config_parse_failed',
6702
- error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
6703
- };
6704
- }
6705
- }
6706
-
6707
- const mcpServersKey = getMcpServersKey(configFormat);
6708
- const existingServers = existingMcpConfig[mcpServersKey];
6709
- const mcpConfig = {
6710
- ...existingMcpConfig,
6711
- [mcpServersKey]: {
6712
- ...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
6713
- [coordinatorSetup.serverName]: mcpServerEntry,
6714
- },
6715
- };
6716
- try {
6717
- writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
6718
- } catch (error: any) {
6719
- const message = `Could not write MCP config for automatic setup: ${error?.message || error}`;
6720
- LOG.error('MeshCoordinator', message);
6721
- if (hermesManualFallback) return returnManualFallback(message);
6722
- return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
6723
- }
6724
- LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
6725
-
6726
- const cliArgs: string[] = [];
6727
- const launchEnv: Record<string, string> = {};
6728
- if (configFormat === 'hermes_config_yaml') {
6729
- launchEnv.HERMES_HOME = dirname(mcpConfigPath);
6730
- launchEnv.HERMES_IGNORE_USER_CONFIG = '';
6731
- }
6732
- let autoImportContextFilePath: string | undefined;
6733
- if (systemPrompt) {
6734
- const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
6735
- const effect = applyMeshCoordinatorSystemPromptInjection(
6736
- systemPrompt,
6737
- providerMeta?.meshCoordinator?.systemPromptInjection,
6738
- { cliArgs, launchEnv, workspace, cliType },
6739
- );
6740
- autoImportContextFilePath = effect.contextFilePath;
6741
- }
6742
- if (cliType === 'claude-cli') {
6743
- cliArgs.push('--mcp-config', coordinatorSetup.configPath);
6744
- }
6745
-
6746
- // 3. Launch CLI session via existing cliManager.
6747
- // Provider-specific prompt injection remains fail-closed: Claude gets
6748
- // explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
6749
- const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
6750
- cliType,
6751
- dir: workspace,
6752
- cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
6753
- env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
6754
- settings: {
6755
- meshCoordinatorFor: meshId
6756
- }
6757
- });
6758
-
6759
- // R48 inject-then-remove. See the cli_command branch for context;
6760
- // same idea: strip the wrapper from disk ~5s after launch so the
6761
- // user's AGENTS.md / GEMINI.md is untouched the moment any
6762
- // worker session opens up in the same workspace.
6763
- if (launchResult?.success && autoImportContextFilePath) {
6764
- const stripPath = autoImportContextFilePath;
6765
- setTimeout(() => {
6766
- void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
6767
- stripCoordinatorWrapperFile(stripPath);
6768
- LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (auto_import)`);
6769
- }).catch(() => { /* best-effort */ });
6770
- }, 5000);
6771
- }
6772
-
6773
- if (!launchResult?.success) {
6774
- return { success: false, error: launchResult?.error || 'Failed to launch CLI session' };
6775
- }
6776
-
6777
- LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
6778
- const launchSessionId = launchResult.sessionId || launchResult.id;
6779
- if (launchSessionId) {
6780
- const autoImportInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
6781
- registerMeshCoordinator({
6782
- meshId,
6783
- sessionId: launchSessionId,
6784
- workspace,
6785
- startedAt: Date.now(),
6786
- cliType,
6787
- systemPrompt: systemPrompt || undefined,
6788
- extraSystemPrompt: extraSystemPrompt || undefined,
6789
- mcpConfigPath,
6790
- injection: autoImportInjectionDecl ? {
6791
- mode: autoImportInjectionDecl.mode,
6792
- target: 'flag' in autoImportInjectionDecl ? autoImportInjectionDecl.flag
6793
- : 'name' in autoImportInjectionDecl ? autoImportInjectionDecl.name
6794
- : 'path' in autoImportInjectionDecl ? autoImportInjectionDecl.path
6795
- : undefined,
6796
- } : undefined,
6797
- });
6798
- }
6799
-
6800
- // Record coordinator launch in task ledger
6801
- try {
6802
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
6803
- appendLedgerEntry(meshId, {
6804
- kind: 'coordinator_started',
6805
- sessionId: launchSessionId,
6806
- providerType: cliType,
6807
- payload: { workspace },
6808
- });
6809
- } catch { /* ledger append is best-effort */ }
6810
-
6811
- return {
6812
- success: true,
6813
- meshId,
6814
- cliType,
6815
- workspace,
6816
- sessionId: launchSessionId,
6817
- mcpConfigWritten: true,
6818
- };
6819
- } catch (e: any) {
6820
- LOG.error('MeshCoordinator', `Failed: ${e.message}`);
6821
- return { success: false, error: e.message };
6822
- }
6823
- }
6824
-
6825
- case 'mesh_status': {
6826
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6827
- if (!meshId) return { success: false, error: 'meshId required' };
6828
- try {
6829
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
6830
- const mesh = meshRecord?.mesh;
6831
- if (!mesh) return { success: false, error: 'Mesh not found' };
6832
- const meshHost = resolveMeshHostStatus(mesh);
6833
-
6834
- const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
6835
- // Compact (default) elides each mission's full goal text from the
6836
- // payload — coordinators polling node health don't need every
6837
- // mission's multi-hundred-char goal repeated. verbose=true (or the
6838
- // explicit compact=false) restores full goals. Verbose bypasses the
6839
- // shared (compact) aggregate cache so a verbose call never poisons
6840
- // the compact cache and vice versa.
6841
- const verboseMissions = args?.verbose === true || args?.compact === false;
6842
- // See (B3) below: scope the peek to this daemon when the
6843
- // caller doesn't tell us, otherwise scoped events look
6844
- // missing and we falsely return a stale cache.
6845
- const peekScope = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
6846
- ? args.coordinatorDaemonId.trim()
6847
- : (this.deps.statusInstanceId || undefined);
6848
- const pendingCoordinatorEventCount = getPendingMeshCoordinatorEvents(meshId, peekScope).length;
6849
- const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
6850
- if (!refreshRequested && !verboseMissions && pendingCoordinatorEventCount === 0) {
6851
- const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
6852
- if (cachedStatus) {
6853
- logRepoMeshStatusDebug('return_cached', {
6854
- meshId,
6855
- command: 'mesh_status',
6856
- refreshRequested,
6857
- summary: summarizeRepoMeshStatusDebug(cachedStatus),
6858
- });
6859
- return cachedStatus;
6860
- }
6861
- }
6862
- const refreshReason = refreshRequested
6863
- ? 'explicit_refresh'
6864
- : pendingCoordinatorEventCount > 0
6865
- ? 'pending_coordinator_events'
6866
- : hadAggregateCache
6867
- ? 'stale_pending_cache_refresh'
6868
- : 'cold_cache_miss';
6869
-
6870
- const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
6871
- const queue = getQueue(meshId);
6872
- const queueSummary = getMeshQueueStats(meshId);
6873
-
6874
- const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
6875
- const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
6876
- const asyncRefineLedgerEntries = readLedgerEntries(meshId, { tail: 100 });
6877
- const ledgerSummary = getLedgerSummary(meshId);
6878
- const sessionHostRecords = this.deps.sessionHostControl?.listSessions
6879
- ? await this.deps.sessionHostControl.listSessions().catch(() => [])
6880
- : [];
6881
- const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
6882
-
6883
- const localMachineId = loadConfig().machineId || '';
6884
- const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
6885
- // Shared probe gate for this mesh_status call: the bootstrap
6886
- // hydrate below and the per-node render loop further down both
6887
- // probe the same peers — route both through this cache so they
6888
- // dedup within the call and reuse recent results across calls.
6889
- const meshGitProbeCache = this.meshGitProbeCache;
6890
- const directTruth = requireDirectPeerTruth
6891
- ? await hydrateInlineMeshDirectTruth({
6892
- mesh,
6893
- meshSource: meshRecord.source,
6894
- dispatchMeshCommand: this.deps.dispatchMeshCommand,
6895
- getMeshPeerConnectionStatus: this.deps.getMeshPeerConnectionStatus,
6896
- statusInstanceId: this.deps.statusInstanceId,
6897
- localMachineId,
6898
- // Standing-state model: only an explicit refresh fans
6899
- // out a blocking peer git probe. Default loads return
6900
- // held truth so one slow peer can't block the graph.
6901
- probeRemotePeers: refreshRequested,
6902
- probeCache: meshGitProbeCache,
6903
- })
6904
- : {
6905
- directEvidenceCount: 0,
6906
- localConfirmedCount: 0,
6907
- peerAttemptedCount: 0,
6908
- peerConfirmedCount: 0,
6909
- standingEvidenceCount: 0,
6910
- unavailableNodeIds: [] as string[],
6911
- deadNodeIds: [] as string[],
6912
- };
6913
- // Default/cached loads may not attempt a remote peer probe yet; do not surface that as
6914
- // a direct mesh truth failure until an explicit probe attempt actually fails.
6915
- const passivePeerTruthNotAttempted = requireDirectPeerTruth
6916
- && !refreshRequested
6917
- && directTruth.directEvidenceCount > 0
6918
- && directTruth.peerAttemptedCount === 0;
6919
- const effectiveDirectTruth = passivePeerTruthNotAttempted
6920
- ? { ...directTruth, unavailableNodeIds: [] as string[] }
6921
- : directTruth;
6922
- const unavailableDirectTruthNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
6923
- const unavailableNodesAreOnlyRemovedWorktrees = unavailableDirectTruthNodeIds.size > 0
6924
- && Array.isArray(mesh.nodes)
6925
- && mesh.nodes
6926
- .filter((node: any) => unavailableDirectTruthNodeIds.has(normalizeMeshNodeId(node) ?? ''))
6927
- .every((node: any) => node?.isLocalWorktree === true);
6928
- // Default (non-refresh) loads never hard-fail: held
6929
- // standing-state truth is returned and the graph renders
6930
- // immediately. The hard mesh_direct_peer_truth_unavailable
6931
- // failure is reserved for an explicit refresh that actually
6932
- // attempted a peer probe and could not confirm any evidence.
6933
- const directTruthSatisfied = !requireDirectPeerTruth
6934
- || !refreshRequested
6935
- || (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
6936
- if (requireDirectPeerTruth && refreshRequested && !directTruthSatisfied) {
6937
- const failureResult = {
6938
- success: false,
6939
- code: 'mesh_direct_peer_truth_unavailable',
6940
- error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
6941
- sourceOfTruth: {
6942
- membership: meshRecord.source === 'inline_cache'
6943
- ? 'coordinator_inline_mesh_cache'
6944
- : meshRecord.source === 'local_config'
6945
- ? 'local_mesh_config'
6946
- : 'inline_bootstrap_snapshot',
6947
- coordinatorOwnsLiveTruth: false,
6948
- currentStatus: 'direct_peer_truth_unavailable',
6949
- directPeerTruth: {
6950
- required: true,
6951
- satisfied: false,
6952
- directEvidenceCount: directTruth.directEvidenceCount,
6953
- localConfirmedCount: directTruth.localConfirmedCount,
6954
- peerAttemptedCount: directTruth.peerAttemptedCount,
6955
- peerConfirmedCount: directTruth.peerConfirmedCount,
6956
- unavailableNodeIds: directTruth.unavailableNodeIds,
6957
- },
6958
- },
6959
- };
6960
- logRepoMeshStatusDebug('direct_truth_unavailable', {
6961
- meshId,
6962
- command: 'mesh_status',
6963
- refreshRequested,
6964
- meshSource: meshRecord.source,
6965
- directTruth,
6966
- });
6967
- return failureResult;
6968
- }
6969
- const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
6970
- const coordinatorHostname = osHostname();
6971
- const selectedCoordinatorNodeId = readStringValue(
6972
- mesh.coordinator?.preferredNodeId,
6973
- normalizeMeshNodeId(mesh.nodes?.[0] as any),
6974
- );
6975
- const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
6976
- ? selectedCoordinatorNodeId
6977
- : undefined;
6978
- const refreshedAt = new Date().toISOString();
6979
- const nodeStatuses = [];
6980
- for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
6981
- const nodeId = normalizeMeshNodeId(node) ?? '';
6982
- const daemonId = readStringValue(node.daemonId);
6983
- const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
6984
- const nodeHostname = readMeshNodeHostname(node as Record<string, unknown>);
6985
- const providerPriority = readProviderPriorityFromPolicy(node.policy);
6986
- const configuredCoordinatorNode = Boolean(
6987
- nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
6988
- );
6989
- const sparseConfiguredCoordinatorNode = configuredCoordinatorNode
6990
- && !daemonId
6991
- && !nodeMachineId
6992
- && !nodeHostname;
6993
- const isSelfNode = Boolean(
6994
- nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
6995
- ) || Boolean(
6996
- daemonId && (daemonIdsEquivalent(daemonId, localMachineId) || daemonIdsEquivalent(daemonId, this.deps.statusInstanceId)),
6997
- ) || Boolean(meshRecord?.inline && nodeIndex === 0)
6998
- || sparseConfiguredCoordinatorNode;
6999
- const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
7000
- localMachineId,
7001
- localDaemonId: this.deps.statusInstanceId,
7002
- coordinatorHostname,
7003
- isSelfNode,
7004
- });
7005
- const status: Record<string, unknown> = {
7006
- nodeId,
7007
- machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
7008
- labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
7009
- ? 'explicit_metadata'
7010
- : 'workspace_host_provider_context',
7011
- workspace: node.workspace,
7012
- repoRoot: node.repoRoot,
7013
- isLocalWorktree: node.isLocalWorktree,
7014
- worktreeBranch: node.worktreeBranch,
7015
- role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
7016
- daemonId,
7017
- machineId: nodeMachineId || node.machineId,
7018
- machine: machineIdentity,
7019
- machineStatus: node.machineStatus,
7020
- health: 'unknown',
7021
- providers: node.providers || [],
7022
- providerPriority,
7023
- activeSessions: [],
7024
- activeSessionDetails: [],
7025
- launchReady: false,
7026
- };
7027
- if (isSelfNode) {
7028
- status.connection = {
7029
- perspective: 'selected_coordinator',
7030
- source: 'mesh_peer_status',
7031
- state: 'self',
7032
- transport: 'local',
7033
- reported: true,
7034
- reason: 'Selected coordinator daemon',
7035
- lastStateChangeAt: refreshedAt,
7036
- };
7037
- } else if (daemonId) {
7038
- const connection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
7039
- status.connection = connection ?? {
7040
- perspective: 'selected_coordinator',
7041
- source: 'not_reported',
7042
- state: 'unknown',
7043
- transport: 'unknown',
7044
- reported: false,
7045
- reason: 'No live mesh peer telemetry reported by the selected coordinator yet.',
7046
- };
7047
- } else {
7048
- status.connection = {
7049
- perspective: 'selected_coordinator',
7050
- source: 'not_reported',
7051
- state: 'unknown',
7052
- transport: 'unknown',
7053
- reported: false,
7054
- reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
7055
- };
7056
- }
7057
- const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
7058
- meshId,
7059
- node,
7060
- nodeId,
7061
- liveSessionRecords: liveMeshSessions,
7062
- allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
7063
- });
7064
- const workspace = readLiveMeshNodeWorkspace({
7065
- meshId,
7066
- nodeId,
7067
- liveSessionRecords: matchedLiveSessionRecords,
7068
- allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
7069
- }) || (typeof node.workspace === 'string' ? node.workspace : '');
7070
- status.workspace = workspace || node.workspace;
7071
- if (matchedLiveSessionRecords.length > 0) {
7072
- const sessionIds = matchedLiveSessionRecords
7073
- .map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
7074
- .filter(Boolean);
7075
- const providerTypes = matchedLiveSessionRecords
7076
- .map((record: any) => readStringValue(record?.providerType))
7077
- .filter(Boolean) as string[];
7078
- status.activeSessions = sessionIds;
7079
- status.activeSessionDetails = matchedLiveSessionRecords.map(summarizeMeshSessionRecord);
7080
- if (providerTypes.length > 0) {
7081
- status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
7082
- }
7083
- }
7084
- if (workspace) {
7085
- if (!fs.existsSync(workspace)) {
7086
- // Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
7087
- const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
7088
- let remoteProbeApplied = false;
7089
- if (inlineTransitGit) {
7090
- status.git = inlineTransitGit;
7091
- status.health = inlineTransitGit.isGitRepo
7092
- ? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
7093
- : 'degraded';
7094
- const connection = readObjectRecord(status.connection);
7095
- const connectionState = readStringValue(connection.state);
7096
- const connectionReported = readBooleanValue(connection.reported) ?? false;
7097
- if (!connectionReported || connectionState === 'unknown') {
7098
- status.connection = buildLivePeerGitConnection(connection, refreshedAt);
7099
- }
7100
- remoteProbeApplied = true;
7101
- } else if (refreshRequested && !isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
7102
- // Only an explicit refresh fans out a blocking
7103
- // per-node git probe. On the default load a peer
7104
- // with no held truth falls through to
7105
- // gitProbePending below — the graph still renders.
7106
- // Bounded retry (shared with the bootstrap hydrate
7107
- // path), gated on the peer staying connected, so a
7108
- // slow TURN-relayed peer is recovered rather than
7109
- // dropped after a single timeout.
7110
- const runNodeProbe = () => probeRemoteMeshGitStatusWithRetry({
7111
- dispatchMeshCommand: this.deps.dispatchMeshCommand,
7112
- daemonId,
7113
- workspace,
7114
- timeoutMs: MESH_DIRECT_PROBE_TIMEOUT_MS,
7115
- retryTimeoutMs: MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS,
7116
- getConnection: this.deps.getMeshPeerConnectionStatus,
7117
- onConnection: connection => { status.connection = connection; },
7118
- });
7119
- // Same shared cache as the bootstrap hydrate path: within one
7120
- // mesh_status call this dedups the bootstrap probe against this
7121
- // per-node probe for the same peer, and across calls it reuses a
7122
- // recent result so the dashboard auto-retry loop can't restart a
7123
- // fresh refreshUpstream probe seconds apart.
7124
- const remoteGit = await meshGitProbeCache.probe(daemonId, workspace, runNodeProbe);
7125
- if (remoteGit) {
7126
- status.git = remoteGit;
7127
- status.health = remoteGit.isGitRepo
7128
- ? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
7129
- : 'degraded';
7130
- const connection = readObjectRecord(status.connection);
7131
- const connectionState = readStringValue(connection.state);
7132
- const connectionReported = readBooleanValue(connection.reported) ?? false;
7133
- if (!connectionReported || connectionState === 'unknown') {
7134
- status.connection = buildLivePeerGitConnection(connection, refreshedAt);
7135
- }
7136
- const reporter = recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
7137
- persistNodeReporterPlatform(meshRecord.source, mesh, nodeId, reporter);
7138
- remoteProbeApplied = true;
7139
- }
7140
- }
7141
- if (!remoteProbeApplied) {
7142
- const connectionState = readStringValue((status.connection as any)?.state);
7143
- const pendingPeerGitProbe = !inlineTransitGit
7144
- && !isSelfNode
7145
- && !!daemonId
7146
- && (
7147
- readStringValue(status.machineStatus) === 'online'
7148
- || readStringValue(status.health) === 'online'
7149
- || connectionState === 'connecting'
7150
- || connectionState === 'connected'
7151
- || connectionState === 'unknown'
7152
- );
7153
- if (pendingPeerGitProbe) {
7154
- status.gitProbePending = true;
7155
- status.health = 'unknown';
7156
- }
7157
- if (applyCachedInlineMeshNodeStatus(
7158
- status,
7159
- node,
7160
- pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
7161
- )) {
7162
- applyInlineMeshBranchConvergence(mesh, node, status);
7163
- finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
7164
- nodeStatuses.push(status);
7165
- continue;
7166
- }
7167
- if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
7168
- applyInlineMeshBranchConvergence(mesh, node, status);
7169
- finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
7170
- nodeStatuses.push(status);
7171
- continue;
7172
- }
7173
- }
7174
- } else {
7175
- try {
7176
- const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
7177
- status.git = gitStatus;
7178
- const reporter = recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
7179
- persistNodeReporterPlatform(meshRecord.source, mesh, nodeId, reporter);
7180
- if (gitStatus.isGitRepo) {
7181
- status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
7182
- } else {
7183
- status.health = 'degraded';
7184
- if (gitStatus.error && !status.error) status.error = gitStatus.error;
7185
- }
7186
- } catch {
7187
- if (!applyCachedInlineMeshNodeStatus(status, node)) {
7188
- status.health = 'degraded';
7189
- }
7190
- }
7191
- }
7192
- } else {
7193
- applyCachedInlineMeshNodeStatus(status, node);
7194
- }
7195
- applyInlineMeshBranchConvergence(mesh, node, status);
7196
- finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
7197
- nodeStatuses.push(status);
7198
- }
7199
-
7200
- // (B3) Resolve the coordinator daemon scope for the peek.
7201
- // mesh_status is a read-only status query — it must not consume
7202
- // (drain) pending events as a side effect. Coordinators that see
7203
- // pendingCoordinatorEvents in the response are expected to call
7204
- // get_pending_mesh_events to explicitly drain them after processing.
7205
- const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
7206
- ? args.coordinatorDaemonId.trim()
7207
- : (this.deps.statusInstanceId || undefined);
7208
- const pendingCoordinatorEvents = getPendingMeshCoordinatorEvents(meshId, callerCoordinatorDaemonId);
7209
- // R4: surface recent fail-loud routing drops so a coordinator/operator can see
7210
- // that a worker completion was lost (envelope present, mesh unresolved) instead
7211
- // of it vanishing silently. Diagnostic-only — never cached (see omit below).
7212
- const unroutableDeliveries = getRecentUnroutableDeliveries();
7213
- const previewFreshness = (() => {
7214
- const localRepoRoot = nodeStatuses
7215
- .map((node: any) => readStringValue(node?.git?.repoRoot, node?.repoRoot, node?.workspace))
7216
- .find((candidate: string | undefined) => !!candidate && fs.existsSync(candidate));
7217
- return localRepoRoot ? buildPreviewFreshness(localRepoRoot) : undefined;
7218
- })();
7219
- const asyncRefineJobs = buildMeshAsyncRefineJobs({
7220
- meshId,
7221
- ledgerEntries: asyncRefineLedgerEntries,
7222
- pendingEvents: [...pendingCoordinatorEvents],
7223
- });
7224
- const historicalSessions = buildHistoricalMeshSessions({
7225
- meshId,
7226
- nodes: mesh.nodes || [],
7227
- liveSessionRecords: liveMeshSessions,
7228
- });
7229
- const { getMeshStatusMissionSummaries } = await import('../mesh/mesh-missions.js');
7230
- // withStats opts in to per-mission operational rollups (durations /
7231
- // retries) for the dashboard mission detail. The rollup scans a
7232
- // bounded ledger tail per mission, but only over the bounded set
7233
- // returned here (live + capped history), so the cost stays linear
7234
- // in visible missions rather than the whole mesh history.
7235
- const missions = getMeshStatusMissionSummaries(meshId, { verbose: verboseMissions, withStats: true });
7236
- const statusResult = {
7237
- success: true,
7238
- meshId: mesh.id,
7239
- meshName: mesh.name,
7240
- repoIdentity: mesh.repoIdentity,
7241
- defaultBranch: mesh.defaultBranch,
7242
- refreshedAt,
7243
- meshHost,
7244
- sourceOfTruth: {
7245
- membership: meshRecord?.source === 'inline_cache'
7246
- ? 'coordinator_inline_mesh_cache'
7247
- : meshRecord?.source === 'local_config'
7248
- ? 'local_mesh_config'
7249
- : 'inline_bootstrap_snapshot',
7250
- coordinatorOwnsLiveTruth: directTruthSatisfied,
7251
- meshHost: {
7252
- owner: 'mesh_host_daemon',
7253
- localRole: meshHost.role,
7254
- hostDaemonId: meshHost.hostDaemonId,
7255
- hostNodeId: meshHost.hostNodeId,
7256
- hostAddress: meshHost.hostAddress,
7257
- },
7258
- ...(requireDirectPeerTruth ? {
7259
- currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
7260
- directPeerTruth: {
7261
- required: true,
7262
- satisfied: directTruthSatisfied,
7263
- directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
7264
- localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
7265
- peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
7266
- peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
7267
- unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
7268
- partialNodeFailures: effectiveDirectTruth.unavailableNodeIds,
7269
- },
7270
- } : {}),
7271
- historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary', 'historicalSessions'],
7272
- },
7273
- branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
7274
- ...(previewFreshness ? { previewFreshness, deployFreshness: previewFreshness } : {}),
7275
- nodes: nodeStatuses,
7276
- queue: { tasks: queue, summary: queueSummary },
7277
- ledger: { entries: ledgerEntries, summary: ledgerSummary },
7278
- ...(missions.length > 0 ? { missions } : {}),
7279
- ...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
7280
- ...(historicalSessions ? { historicalSessions } : {}),
7281
- ...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
7282
- ...(unroutableDeliveries.length > 0 ? { unroutableDeliveries } : {}),
7283
- activeRefineJobs: Array.from(this.runningRefineJobs.values())
7284
- .filter(job => job.meshId === meshId)
7285
- .map(job => ({
7286
- jobId: job.jobId,
7287
- nodeId: job.targetNodeId,
7288
- workspace: job.workspace,
7289
- startedAt: job.startedAt,
7290
- status: job.status,
7291
- targetCoordinatorDaemonId: job.targetCoordinatorDaemonId,
7292
- })),
7293
- };
7294
- const { pendingCoordinatorEvents: _pendingCoordinatorEvents, unroutableDeliveries: _unroutableDeliveries, ...cacheableStatusResult } = statusResult as any;
7295
- // Verbose carries full mission goals; never store it in the shared
7296
- // (compact) aggregate cache or a later compact poll would return the
7297
- // heavy goals from cache. Return it without caching.
7298
- const rememberedStatus = verboseMissions
7299
- ? cacheableStatusResult
7300
- : this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
7301
- const returnedStatus = {
7302
- ...rememberedStatus,
7303
- ...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
7304
- ...(unroutableDeliveries.length > 0 ? { unroutableDeliveries } : {}),
7305
- };
7306
- logRepoMeshStatusDebug('return_live', {
7307
- meshId,
7308
- command: 'mesh_status',
7309
- refreshRequested,
7310
- refreshReason,
7311
- meshSource: meshRecord.source,
7312
- directTruth,
7313
- summary: summarizeRepoMeshStatusDebug(returnedStatus),
7314
- });
7315
- return returnedStatus;
7316
- } catch (e: any) {
7317
- return { success: false, error: e.message };
7318
- }
7319
- }
7320
-
7321
- case 'get_mesh_review_inbox': {
7322
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7323
- if (!meshId) return { success: false, error: 'meshId required' };
7324
- try {
7325
- const { deriveMeshReviewInboxItems } = await import('../mesh/mesh-review-inbox.js');
7326
- const { readLedgerEntries } = await import('../mesh/mesh-ledger.js');
7327
- const { getGitDiffSummary } = await import('../git/git-diff.js');
7328
- const { existsSync } = await import('node:fs');
7329
-
7330
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7331
- const mesh = meshRecord?.mesh;
7332
- if (!mesh) return { success: false, error: 'Mesh not found' };
7333
-
7334
- // Ensure we have a fresh aggregate status so nodeStatuses carry
7335
- // computed fields (connection.state, branchConvergence, isLocalWorktree)
7336
- // that the raw mesh.nodes config objects don't have.
7337
- // When the caller provides an inlineMesh, prefer its nodes directly
7338
- // (they already carry the computed fields from the coordinator).
7339
- const inlineNodes = args?.inlineMesh && Array.isArray((args.inlineMesh as any)?.nodes)
7340
- ? (args.inlineMesh as any).nodes as Record<string, unknown>[]
7341
- : null;
7342
- let cachedStatus = !inlineNodes ? this.getCachedAggregateMeshStatus(meshId, mesh, {}) : null;
7343
- if (!cachedStatus && !inlineNodes) {
7344
- const freshStatus = await this.execute('mesh_status', {
7345
- meshId,
7346
- inlineMesh: args?.inlineMesh,
7347
- refresh: true,
7348
- }, 'get_mesh_review_inbox');
7349
- cachedStatus = (freshStatus?.success !== false) ? freshStatus : null;
7350
- }
7351
- const nodeStatuses: Record<string, unknown>[] = inlineNodes
7352
- ? inlineNodes
7353
- : Array.isArray(cachedStatus?.nodes)
7354
- ? cachedStatus.nodes as Record<string, unknown>[]
7355
- : Array.isArray(mesh.nodes)
7356
- ? mesh.nodes as Record<string, unknown>[]
7357
- : [];
7358
-
7359
- const ledgerEntries = readLedgerEntries(meshId, { tail: 300 });
7360
- const derivation = deriveMeshReviewInboxItems({ nodes: nodeStatuses, ledgerEntries });
7361
-
7362
- for (const item of derivation.items) {
7363
- const workspace = item.workspace;
7364
- if (!workspace || !existsSync(workspace)) continue;
7365
- const baseRef = item.defaultBranch
7366
- ? `origin/${item.defaultBranch}`
7367
- : 'origin/main';
7368
- try {
7369
- const diffResult = await getGitDiffSummary(workspace, { baseRef, maxFiles: 100 });
7370
- if (diffResult.isGitRepo) {
7371
- item.diffSummary = {
7372
- baseRef,
7373
- files: diffResult.files.map(f => ({
7374
- path: f.path,
7375
- status: f.status,
7376
- insertions: f.insertions,
7377
- deletions: f.deletions,
7378
- binary: f.binary,
7379
- oldPath: f.oldPath,
7380
- })),
7381
- totalFiles: diffResult.files.length,
7382
- totalInsertions: diffResult.totalInsertions,
7383
- totalDeletions: diffResult.totalDeletions,
7384
- truncated: diffResult.truncated,
7385
- ...(diffResult.error ? { error: diffResult.error } : {}),
7386
- };
7387
- }
7388
- } catch {
7389
- item.diffSummary = null;
7390
- }
7391
- }
7392
-
7393
- return {
7394
- success: true,
7395
- meshId,
7396
- inbox: derivation.items,
7397
- remoteNodesExcluded: derivation.remoteNodesExcluded,
7398
- excludedRemoteNodeIds: derivation.excludedRemoteNodeIds,
7399
- };
7400
- } catch (e: any) {
7401
- return { success: false, error: e.message };
7402
- }
7403
- }
7404
-
7405
- default:
7406
- break;
6547
+ // RF-ROUTER HIGH family: high-coupling commands (mesh coordinator-event
6548
+ // relay + interactive prompt, mesh coordinator launch, mesh aggregate
6549
+ // status + review inbox) are handled by the registry after the LOW and
6550
+ // MED families and before the (now empty) switch. HIGH handlers reach the
6551
+ // most router-owned state — the aggregate-status memory cache and the
6552
+ // running-refine-job table — so the context carries those plus bound
6553
+ // read/write helpers and the router's own `execute` (the
6554
+ // get_mesh_review_inbox mesh_status re-entry). A hit returns the same
6555
+ // CommandRouterResult the inlined case used to; a miss falls through to
6556
+ // CommandHandler delegation.
6557
+ const highFamilyHandler = highFamilyRegistry.get(cmd);
6558
+ if (highFamilyHandler) {
6559
+ return await highFamilyHandler(this.buildHighFamilyContext(), args);
7407
6560
  }
7408
6561
 
7409
6562
  return null; // Not handled at this level → delegate to CommandHandler