@adhdev/daemon-core 0.9.82-rc.364 → 0.9.82-rc.366

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.
Files changed (41) hide show
  1. package/dist/commands/high-family/index.d.ts +3 -0
  2. package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
  3. package/dist/commands/high-family/mesh-events.d.ts +2 -0
  4. package/dist/commands/high-family/mesh-status.d.ts +2 -0
  5. package/dist/commands/high-family/types.d.ts +60 -0
  6. package/dist/commands/med-family/cli-agent.d.ts +2 -0
  7. package/dist/commands/med-family/fast-forward.d.ts +2 -0
  8. package/dist/commands/med-family/ide.d.ts +10 -0
  9. package/dist/commands/med-family/index.d.ts +3 -0
  10. package/dist/commands/med-family/mesh-crud.d.ts +2 -0
  11. package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
  12. package/dist/commands/med-family/mesh-queue.d.ts +2 -0
  13. package/dist/commands/med-family/types.d.ts +116 -0
  14. package/dist/commands/router.d.ts +291 -0
  15. package/dist/index.js +3824 -3565
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +3811 -3553
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/mesh/mesh-events-coordinator.d.ts +8 -0
  20. package/dist/system/hash.d.ts +8 -0
  21. package/package.json +2 -2
  22. package/src/commands/cli-manager.ts +30 -3
  23. package/src/commands/high-family/index.ts +28 -0
  24. package/src/commands/high-family/mesh-coordinator-launch.ts +592 -0
  25. package/src/commands/high-family/mesh-events.ts +47 -0
  26. package/src/commands/high-family/mesh-status.ts +639 -0
  27. package/src/commands/high-family/types.ts +76 -0
  28. package/src/commands/med-family/cli-agent.ts +218 -0
  29. package/src/commands/med-family/fast-forward.ts +198 -0
  30. package/src/commands/med-family/ide.ts +163 -0
  31. package/src/commands/med-family/index.ts +35 -0
  32. package/src/commands/med-family/mesh-crud.ts +788 -0
  33. package/src/commands/med-family/mesh-host-pairing.ts +234 -0
  34. package/src/commands/med-family/mesh-queue.ts +131 -0
  35. package/src/commands/med-family/types.ts +120 -0
  36. package/src/commands/mesh-coordinator.ts +2 -2
  37. package/src/commands/router.ts +328 -2847
  38. package/src/config/mesh-config.ts +3 -2
  39. package/src/mesh/mesh-active-work.ts +59 -81
  40. package/src/mesh/mesh-events-coordinator.ts +35 -1
  41. package/src/system/hash.ts +23 -0
@@ -13,6 +13,11 @@ import { DaemonCdpManager } from '../cdp/manager.js';
13
13
  import { registerExtensionProviders } from '../cdp/setup.js';
14
14
  import { DaemonCommandHandler } from './handler.js';
15
15
  import { lowFamilyRegistry } from './low-family/index.js';
16
+ import { medFamilyRegistry } from './med-family/index.js';
17
+ import { launchIde } from './med-family/ide.js';
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';
16
21
  import { DaemonCliManager } from './cli-manager.js';
17
22
  import { supportsExplicitSessionResume } from './cli-manager.js';
18
23
  import type { HostedCliRuntimeDescriptor } from './cli-manager.js';
@@ -49,17 +54,12 @@ import { logCommand } from '../logging/command-log.js';
49
54
  import type { CommandLogEntry } from '../logging/command-log.js';
50
55
  import * as yaml from 'js-yaml';
51
56
  import { createInteractionId, recordDebugTrace } from '../logging/debug-trace.js';
52
- import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
53
- import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
54
- import { registerMeshCoordinator } from '../mesh/coordinator-registry.js';
55
- import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, type PendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
56
- import { getRecentUnroutableDeliveries } from '../mesh/mesh-routing.js';
57
+ import { getSessionHostSurfaceKind } from '../session-host/runtime-surface.js';
58
+ import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
57
59
  import { buildMeshWorkerRelayStamp } from '../mesh/mesh-events-utils.js';
58
- import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
60
+ import { buildMeshHostRequiredFailure, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
59
61
  import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
60
62
  import { analyzeMeshRefineNodeChangeArea, orderMeshRefineBatchNodes } from '../mesh/mesh-refine-batch.js';
61
- import { buildPreviewFreshness } from '../mesh/preview-freshness.js';
62
- import { buildMeshAsyncRefineJobs } from '../mesh/mesh-refine-status.js';
63
63
  import {
64
64
  MESH_REFINE_CONFIG_LOCATIONS,
65
65
  MESH_REFINE_CONFIG_SCHEMA,
@@ -85,11 +85,10 @@ import { homedir, hostname as osHostname } from 'os';
85
85
  import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
86
86
  import * as fs from 'fs';
87
87
  import { execFileSync } from 'node:child_process';
88
- import { normalizeInteractivePromptResponse } from '../providers/types/interactive-prompt.js';
89
88
  import { workingDirBasename } from '../providers/working-dir.js';
90
89
  import { resolveWin32Executable } from '../cli-adapters/resolve-executable.js';
91
90
 
92
- function readProviderPriorityFromPolicy(policy: unknown): string[] {
91
+ export function readProviderPriorityFromPolicy(policy: unknown): string[] {
93
92
  const record = policy && typeof policy === 'object' && !Array.isArray(policy)
94
93
  ? policy as Record<string, unknown>
95
94
  : {};
@@ -114,7 +113,7 @@ function readProviderPriorityFromPolicy(policy: unknown): string[] {
114
113
  * the field entirely (full backward compat). Routing is governed by required_tags;
115
114
  * any legacy `role` field on the input is ignored.
116
115
  */
117
- function normalizeProviderRoles(value: unknown): Array<{ providerType: string; maxParallel?: number }> {
116
+ export function normalizeProviderRoles(value: unknown): Array<{ providerType: string; maxParallel?: number }> {
118
117
  if (!Array.isArray(value)) return [];
119
118
  const byType = new Map<string, { providerType: string; maxParallel?: number }>();
120
119
  for (const raw of value) {
@@ -130,13 +129,13 @@ function normalizeProviderRoles(value: unknown): Array<{ providerType: string; m
130
129
  return [...byType.values()];
131
130
  }
132
131
 
133
- function readObjectRecord(value: unknown): Record<string, any> {
132
+ export function readObjectRecord(value: unknown): Record<string, any> {
134
133
  return value && typeof value === 'object' && !Array.isArray(value)
135
134
  ? value as Record<string, any>
136
135
  : {};
137
136
  }
138
137
 
139
- function readStringValue(...values: unknown[]): string | undefined {
138
+ export function readStringValue(...values: unknown[]): string | undefined {
140
139
  for (const value of values) {
141
140
  if (typeof value === 'string' && value.trim()) return value.trim();
142
141
  }
@@ -150,7 +149,7 @@ function readNumberValue(...values: unknown[]): number | undefined {
150
149
  return undefined;
151
150
  }
152
151
 
153
- function readBooleanValue(...values: unknown[]): boolean | undefined {
152
+ export function readBooleanValue(...values: unknown[]): boolean | undefined {
154
153
  for (const value of values) {
155
154
  if (typeof value === 'boolean') return value;
156
155
  }
@@ -160,7 +159,7 @@ function readBooleanValue(...values: unknown[]): boolean | undefined {
160
159
  // summarizeRepoMeshDebugGit was a hand-synced copy of the cloud git-shape
161
160
  // summarizer; both now call shared summarizeGitShape (@adhdev/mesh-shared).
162
161
 
163
- function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
162
+ export function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
164
163
  const nodes = Array.isArray(status?.nodes) ? status.nodes : [];
165
164
  return {
166
165
  success: status?.success,
@@ -192,7 +191,7 @@ function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
192
191
  };
193
192
  }
194
193
 
195
- function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
194
+ export function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
196
195
  try {
197
196
  LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${JSON.stringify({ event, ...fields })}`);
198
197
  } catch {
@@ -224,7 +223,7 @@ function normalizeMeshHostname(value: unknown): string | undefined {
224
223
  return hostname.toLowerCase().replace(/\.$/, '');
225
224
  }
226
225
 
227
- function readMeshNodeMachineId(node: Record<string, unknown>): string | undefined {
226
+ export function readMeshNodeMachineId(node: Record<string, unknown>): string | undefined {
228
227
  return readStringValue(
229
228
  node.machineId,
230
229
  node.machine_id,
@@ -254,7 +253,7 @@ function readMeshNodeDaemonId(node: Record<string, unknown>): string | undefined
254
253
  );
255
254
  }
256
255
 
257
- function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
256
+ export function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
258
257
  return readStringValue(
259
258
  node.hostname,
260
259
  node.host,
@@ -294,7 +293,7 @@ function compactMeshIdentityEvidence(value: string | undefined): string | undefi
294
293
  return value.length > 24 ? `${value.slice(0, 12)}…${value.slice(-8)}` : value;
295
294
  }
296
295
 
297
- function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
296
+ export function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
298
297
  localMachineId?: string;
299
298
  localDaemonId?: string;
300
299
  coordinatorHostname?: string;
@@ -354,7 +353,7 @@ function normalizeInlineMeshGitStatus(
354
353
  return sharedNormalizeGitStatus(status, readObjectRecord(node), options) as Record<string, unknown> | undefined;
355
354
  }
356
355
 
357
- function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
356
+ export function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
358
357
  return sharedPickBestTransitGitStatus(readObjectRecord(node), { lastCheckedAt: Date.now() }) as Record<string, unknown> | undefined;
359
358
  }
360
359
 
@@ -367,7 +366,7 @@ function shouldRefreshStalePendingAggregate(snapshot: any, options?: { requireDi
367
366
  });
368
367
  }
369
368
 
370
- function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
369
+ export function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
371
370
  const source = readStringValue(connection.source);
372
371
  const transport = readStringValue(connection.transport);
373
372
  return {
@@ -382,7 +381,7 @@ function buildLivePeerGitConnection(connection: Record<string, unknown>, timesta
382
381
  };
383
382
  }
384
383
 
385
- function recordInlineMeshDirectGitTruth(
384
+ export function recordInlineMeshDirectGitTruth(
386
385
  node: any,
387
386
  git: Record<string, unknown>,
388
387
  source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
@@ -459,7 +458,7 @@ function stampNodeReporterPlatform(node: any, platform: string | null, arch: str
459
458
  * Fire-and-forget (same pattern as the worktreeBootstrap writer) — a persistence
460
459
  * failure must never block the status response.
461
460
  */
462
- function persistNodeReporterPlatform(
461
+ export function persistNodeReporterPlatform(
463
462
  meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config',
464
463
  mesh: any,
465
464
  nodeId: string | undefined,
@@ -733,7 +732,7 @@ function isInlineMeshAutoFastForwardEligible(git: Record<string, unknown> | null
733
732
  return dirty !== true && countGitWorktreeChanges(git) === 0;
734
733
  }
735
734
 
736
- function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
735
+ export function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
737
736
  if (!git || readBooleanValue(git.isGitRepo) === false) return 'degraded';
738
737
  const branch = readStringValue(git.branch);
739
738
  if (!branch) return 'degraded';
@@ -872,7 +871,7 @@ function buildInlineMeshBranchConvergence(args: {
872
871
  };
873
872
  }
874
873
 
875
- function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
874
+ export function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
876
875
  const git = readObjectRecord(status.git);
877
876
  if (Object.keys(git).length === 0 && !status.gitProbePending) return;
878
877
  const uncommittedChanges = countGitWorktreeChanges(git);
@@ -887,7 +886,7 @@ function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<s
887
886
  }
888
887
  }
889
888
 
890
- function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
889
+ export function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
891
890
  const followUps = nodes
892
891
  .filter(node => {
893
892
  if (readObjectRecord(node.branchConvergence).needsConvergence !== true) return false;
@@ -1073,7 +1072,7 @@ function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknow
1073
1072
  }
1074
1073
  }
1075
1074
 
1076
- function finalizeMeshNodeStatus(args: {
1075
+ export function finalizeMeshNodeStatus(args: {
1077
1076
  status: Record<string, unknown>;
1078
1077
  node: any;
1079
1078
  daemonId?: string;
@@ -1129,8 +1128,8 @@ function readMeshTimeoutEnvMs(name: string, defaultMs: number): number {
1129
1128
  // round-trip to slow (often TURN-relayed) peers, so such a node was permanently
1130
1129
  // marked unavailable and blocked the whole mesh graph. Default raised to 25s
1131
1130
  // (still under the P2P REQUEST_TIMEOUT of 30s) and made env-overridable.
1132
- const MESH_DIRECT_PROBE_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_TIMEOUT_MS', 25_000);
1133
- const MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS', 25_000);
1131
+ export const MESH_DIRECT_PROBE_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_TIMEOUT_MS', 25_000);
1132
+ export const MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS', 25_000);
1134
1133
  // How long a successful per-peer git_status probe stays fresh enough to be
1135
1134
  // reused instead of issuing another blocking `refreshUpstream:true` fan-out.
1136
1135
  // A slow (TURN-relayed) peer's probe can take 9-23s, and the dashboard's
@@ -1156,7 +1155,7 @@ const MESH_DIRECT_PROBE_REUSE_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_REUSE
1156
1155
  * Lives on the router instance so the gate spans separate mesh_status calls,
1157
1156
  * which is exactly where the refresh storm happens.
1158
1157
  */
1159
- class MeshGitProbeCache {
1158
+ export class MeshGitProbeCache {
1160
1159
  private inflight = new Map<string, Promise<Record<string, unknown> | null>>();
1161
1160
  private recent = new Map<string, { at: number; value: Record<string, unknown> }>();
1162
1161
 
@@ -1263,7 +1262,7 @@ function isMeshConnectionDefinitivelyDown(
1263
1262
  * attempt; a non-`connected` state short-circuits the retry loop (the very first
1264
1263
  * attempt always runs so a missing connection getter still gets one try).
1265
1264
  */
1266
- async function probeRemoteMeshGitStatusWithRetry(args: {
1265
+ export async function probeRemoteMeshGitStatusWithRetry(args: {
1267
1266
  dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
1268
1267
  daemonId: string;
1269
1268
  workspace: string;
@@ -1311,7 +1310,7 @@ async function probeRemoteMeshGitStatusWithRetry(args: {
1311
1310
  return null;
1312
1311
  }
1313
1312
 
1314
- async function hydrateInlineMeshDirectTruth(args: {
1313
+ export async function hydrateInlineMeshDirectTruth(args: {
1315
1314
  mesh: any;
1316
1315
  meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
1317
1316
  dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
@@ -1480,7 +1479,7 @@ async function hydrateInlineMeshDirectTruth(args: {
1480
1479
  };
1481
1480
  }
1482
1481
 
1483
- function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
1482
+ export function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
1484
1483
  const meta = readObjectRecord(record?.meta);
1485
1484
  const isSelfCoordinator = Boolean(readStringValue(meta.meshCoordinatorFor));
1486
1485
  const chatStatus = readStringValue(record?.chatStatus, record?.activeChat?.status, meta.chatStatus, meta.sessionStatus);
@@ -1528,7 +1527,7 @@ function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, work
1528
1527
  return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
1529
1528
  }
1530
1529
 
1531
- function readLiveMeshNodeWorkspace(args: {
1530
+ export function readLiveMeshNodeWorkspace(args: {
1532
1531
  meshId: string;
1533
1532
  nodeId: string;
1534
1533
  liveSessionRecords: any[];
@@ -1555,7 +1554,7 @@ function readLiveMeshNodeWorkspace(args: {
1555
1554
  return '';
1556
1555
  }
1557
1556
 
1558
- function collectLiveMeshSessionRecords(args: {
1557
+ export function collectLiveMeshSessionRecords(args: {
1559
1558
  meshId: string;
1560
1559
  node: any;
1561
1560
  nodeId: string;
@@ -1586,7 +1585,7 @@ function collectLiveMeshSessionRecords(args: {
1586
1585
  return matches;
1587
1586
  }
1588
1587
 
1589
- function buildHistoricalMeshSessions(args: {
1588
+ export function buildHistoricalMeshSessions(args: {
1590
1589
  meshId: string;
1591
1590
  nodes: any[];
1592
1591
  liveSessionRecords: any[];
@@ -1632,7 +1631,7 @@ function buildHistoricalMeshSessions(args: {
1632
1631
  };
1633
1632
  }
1634
1633
 
1635
- function applyCachedInlineMeshNodeStatus(
1634
+ export function applyCachedInlineMeshNodeStatus(
1636
1635
  status: Record<string, unknown>,
1637
1636
  node: any,
1638
1637
  options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
@@ -1666,7 +1665,7 @@ function applyCachedInlineMeshNodeStatus(
1666
1665
  return activeSessions.length > 0 || !!machineStatus || !!lastSeenAt || !!updatedAt;
1667
1666
  }
1668
1667
 
1669
- async function resolveProviderTypeFromPriority(args: {
1668
+ export async function resolveProviderTypeFromPriority(args: {
1670
1669
  nodeId: string;
1671
1670
  providerPriority: string[];
1672
1671
  providerLoader: ProviderLoader;
@@ -1696,7 +1695,7 @@ async function resolveProviderTypeFromPriority(args: {
1696
1695
 
1697
1696
  return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join('; ')}` };
1698
1697
  }
1699
- type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
1698
+ export type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
1700
1699
  type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
1701
1700
  type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
1702
1701
 
@@ -1846,7 +1845,7 @@ type MeshRefineSubmoduleReachabilitySummary = {
1846
1845
 
1847
1846
  type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
1848
1847
 
1849
- type MeshRefineJobHandle = {
1848
+ export type MeshRefineJobHandle = {
1850
1849
  success: true;
1851
1850
  async: true;
1852
1851
  status: MeshRefineAsyncJobStatus;
@@ -1979,6 +1978,51 @@ function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReach
1979
1978
  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.`;
1980
1979
  }
1981
1980
 
1981
+ /**
1982
+ * Async git exec helper used across the synchronous-refine stage pipeline. Bound
1983
+ * once in the orchestrator and threaded through RefineContext so every stage runs
1984
+ * git the same way (execFile + promisify, utf8). Returns the child's stdout/stderr.
1985
+ */
1986
+ type RefineExecFileAsync = (file: string, args: string[], options: { cwd: string; encoding: 'utf8' }) => Promise<{ stdout: string; stderr: string }>;
1987
+
1988
+ /**
1989
+ * Accumulated state shared by the synchronous-refine stages. The orchestrator
1990
+ * (executeMeshRefineNodeSynchronously) seeds this in the resolve_refs stage and
1991
+ * each later stage reads / extends it. `branchHead` and `patchEquivalence` are the
1992
+ * only fields a stage mutates after creation (auto-rebase updates both), so they
1993
+ * are carried on the mutable context rather than re-threaded through return types.
1994
+ */
1995
+ interface RefineContext {
1996
+ meshId: string;
1997
+ nodeId: string;
1998
+ args: any;
1999
+ refineStages: Array<Record<string, unknown>>;
2000
+ execFileAsync: RefineExecFileAsync;
2001
+ mesh: any;
2002
+ node: any;
2003
+ sourceNode: any;
2004
+ repoRoot: string;
2005
+ branch: string;
2006
+ baseBranch: string;
2007
+ baseHead: string;
2008
+ branchHead: string;
2009
+ validationSummary: Awaited<ReturnType<typeof runMeshRefineValidationGate>>;
2010
+ patchEquivalence: Awaited<ReturnType<typeof runMeshRefinePatchEquivalenceGate>>;
2011
+ submoduleReachability: Awaited<ReturnType<typeof runMeshRefineSubmoduleReachabilityGate>>;
2012
+ }
2013
+
2014
+ /**
2015
+ * Stage outcome for the synchronous-refine pipeline. A stage either produces a
2016
+ * terminal CommandRouterResult (an early-exit gate failure, or a successful
2017
+ * already-merged short-circuit), in which case the orchestrator returns it
2018
+ * immediately, or it returns `continue` with the (possibly extended) context for
2019
+ * the next stage. This makes the orchestrator a flat sequence of stage calls
2020
+ * while preserving the original body's exact early-return control flow.
2021
+ */
2022
+ type RefineStageOutcome =
2023
+ | { kind: 'terminal'; result: CommandRouterResult }
2024
+ | { kind: 'continue'; ctx: RefineContext };
2025
+
1982
2026
  function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
1983
2027
  if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
1984
2028
  process.stderr.write(
@@ -3045,7 +3089,7 @@ async function runMeshRefineSubmoduleReachabilityGate(
3045
3089
  }
3046
3090
  }
3047
3091
 
3048
- function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
3092
+ export function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
3049
3093
  const plan = resolveMeshRefineValidationPlan(mesh, workspace);
3050
3094
  const mapCommand = (command: MeshRefineValidationCommandPlan) => ({
3051
3095
  displayCommand: command.displayCommand,
@@ -3282,18 +3326,18 @@ function loadYamlModule(): { load: (input: string) => any; dump: (input: any, op
3282
3326
  return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
3283
3327
  }
3284
3328
 
3285
- function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
3329
+ export function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
3286
3330
  return format === 'hermes_config_yaml' ? 'mcp_servers' : 'mcpServers';
3287
3331
  }
3288
3332
 
3289
- function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
3333
+ export function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
3290
3334
  if (!text.trim()) return {};
3291
3335
  if (format === 'claude_mcp_json') return JSON.parse(text);
3292
3336
  const parsed = loadYamlModule().load(text);
3293
3337
  return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
3294
3338
  }
3295
3339
 
3296
- function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
3340
+ export function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
3297
3341
  if (format === 'claude_mcp_json') return JSON.stringify(config, null, 2);
3298
3342
  return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
3299
3343
  }
@@ -3303,7 +3347,7 @@ function resolveHermesUserHome(): string {
3303
3347
  return explicitHome || pathJoin(homedir(), '.hermes');
3304
3348
  }
3305
3349
 
3306
- function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } {
3350
+ export function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } {
3307
3351
  const sourceHome = resolveHermesUserHome();
3308
3352
  const sourceConfigPath = pathJoin(sourceHome, 'config.yaml');
3309
3353
  if (!fs.existsSync(sourceConfigPath)) return { config: {}, sourceHome, sourceConfigPath };
@@ -3314,7 +3358,7 @@ function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Re
3314
3358
  return { config: baseConfig, sourceHome, sourceConfigPath };
3315
3359
  }
3316
3360
 
3317
- function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
3361
+ export function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
3318
3362
  const {
3319
3363
  model: _model,
3320
3364
  provider: _provider,
@@ -3343,7 +3387,7 @@ function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string,
3343
3387
  return sanitized;
3344
3388
  }
3345
3389
 
3346
- function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
3390
+ export function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
3347
3391
  if (pathResolve(sourceHome) === pathResolve(targetHome)) return;
3348
3392
  for (const fileName of ['.env', 'auth.json']) {
3349
3393
  const sourcePath = pathJoin(sourceHome, fileName);
@@ -3470,7 +3514,7 @@ function normalizeCommandArgsWithInteractionId(args: any): Record<string, unknow
3470
3514
  * basename to be a literal `*.json`, and re-joins under the verified parent so
3471
3515
  * the returned path can't point outside the tree. Used by get/write_spec_source.
3472
3516
  */
3473
- function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
3517
+ export function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
3474
3518
  const raw = hostAddress.trim();
3475
3519
  if (!raw) throw new Error('hostAddress required');
3476
3520
  const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
@@ -3480,7 +3524,7 @@ function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
3480
3524
  return url.toString();
3481
3525
  }
3482
3526
 
3483
- function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
3527
+ export function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
3484
3528
  const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
3485
3529
  const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
3486
3530
  ? args.memberNode as Record<string, any>
@@ -3862,6 +3906,62 @@ export class DaemonCommandRouter {
3862
3906
  this.deps.onMeshStateChange?.(meshId);
3863
3907
  }
3864
3908
 
3909
+ /**
3910
+ * Build the MedFamilyContext handed to RF-ROUTER MED family handlers. Binds the
3911
+ * router-private collaborators those handlers need (mesh resolution, owner
3912
+ * gating, inline-cache mutation, worktree / session cleanup, refine job
3913
+ * starters, IDE stop/launch) plus the inline-mesh and git-probe caches. The
3914
+ * `launchIde` field closes over the freshly-built context so restart_session /
3915
+ * restart_ide invoke the IDE launch directly instead of recursing through
3916
+ * executeDaemonCommand('launch_ide').
3917
+ */
3918
+ private buildMedFamilyContext(): MedFamilyContext {
3919
+ const ctx: MedFamilyContext = {
3920
+ deps: this.deps,
3921
+ getMeshForCommand: this.getMeshForCommand.bind(this),
3922
+ getCachedInlineMesh: this.getCachedInlineMesh.bind(this),
3923
+ requireMeshHostMutationOwner: this.requireMeshHostMutationOwner.bind(this),
3924
+ invalidateAggregateMeshStatus: this.invalidateAggregateMeshStatus.bind(this),
3925
+ updateInlineMeshNode: this.updateInlineMeshNode.bind(this),
3926
+ removeInlineMeshNode: this.removeInlineMeshNode.bind(this),
3927
+ normalizeMeshSessionCleanupMode: this.normalizeMeshSessionCleanupMode.bind(this),
3928
+ cleanupMeshSessions: this.cleanupMeshSessions.bind(this),
3929
+ cleanupLocalWorktreeNode: this.cleanupLocalWorktreeNode.bind(this),
3930
+ startMeshRefineJob: this.startMeshRefineJob.bind(this),
3931
+ batchRefineMeshNodes: this.batchRefineMeshNodes.bind(this),
3932
+ startMeshRefineBatchJob: this.startMeshRefineBatchJob.bind(this),
3933
+ stopIde: this.stopIde.bind(this),
3934
+ launchIde: (args: any) => launchIde(ctx, args),
3935
+ inlineMeshCache: this.inlineMeshCache,
3936
+ meshGitProbeCache: this.meshGitProbeCache,
3937
+ };
3938
+ return ctx;
3939
+ }
3940
+
3941
+ /**
3942
+ * Build the HighFamilyContext handed to RF-ROUTER HIGH family handlers. Binds
3943
+ * the router-private collaborators those handlers need (mesh resolution, the
3944
+ * aggregate-status memory cache + its bound read/write helpers, the
3945
+ * running-refine-job table, inline-mesh + git-probe caches, and the router's
3946
+ * own `execute` for the get_mesh_review_inbox mesh_status re-entry). HIGH
3947
+ * handlers reach more router-owned state than MED, but the binding shape is
3948
+ * the same: bound methods + direct field references, none reachable from
3949
+ * `deps`.
3950
+ */
3951
+ private buildHighFamilyContext(): HighFamilyContext {
3952
+ return {
3953
+ deps: this.deps,
3954
+ getMeshForCommand: this.getMeshForCommand.bind(this),
3955
+ getCachedAggregateMeshStatus: this.getCachedAggregateMeshStatus.bind(this),
3956
+ rememberAggregateMeshStatus: this.rememberAggregateMeshStatus.bind(this),
3957
+ execute: this.execute.bind(this),
3958
+ aggregateMeshStatusCache: this.aggregateMeshStatusCache,
3959
+ runningRefineJobs: this.runningRefineJobs,
3960
+ inlineMeshCache: this.inlineMeshCache,
3961
+ meshGitProbeCache: this.meshGitProbeCache,
3962
+ };
3963
+ }
3964
+
3865
3965
 
3866
3966
  private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
3867
3967
  const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
@@ -4766,33 +4866,77 @@ export class DaemonCommandRouter {
4766
4866
  }
4767
4867
  }
4768
4868
 
4869
+ /**
4870
+ * Synchronous refinery for a single worktree node — the gate pipeline that
4871
+ * validates, preflights (patch-equivalence / submodule-reachability /
4872
+ * no-op), merges, aligns submodules, cleans up the worktree node and
4873
+ * (optionally) pushes. The body is a flat sequence of stage methods; each
4874
+ * stage either returns a terminal CommandRouterResult (gate failure or a
4875
+ * successful already-merged short-circuit) or `continue` with the extended
4876
+ * context. Behavior — stage order, every early-exit, and every result shape —
4877
+ * is identical to the previous single inlined body.
4878
+ */
4769
4879
  private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
4770
4880
  const refineStages: Array<Record<string, unknown>> = [];
4771
4881
  try {
4882
+ const resolved = await this.refineResolveRefsStage(meshId, nodeId, args, refineStages);
4883
+ if (resolved.kind === 'terminal') return resolved.result;
4884
+ const ctx = resolved.ctx;
4885
+
4886
+ const validation = await this.refineValidationStage(ctx);
4887
+ if (validation.kind === 'terminal') return validation.result;
4888
+
4889
+ const patchEquivalence = await this.refinePatchEquivalenceStage(ctx);
4890
+ if (patchEquivalence.kind === 'terminal') return patchEquivalence.result;
4891
+
4892
+ const submoduleReachability = await this.refineSubmoduleReachabilityStage(ctx);
4893
+ if (submoduleReachability.kind === 'terminal') return submoduleReachability.result;
4894
+
4895
+ const effectiveDiff = await this.refineEffectiveDiffStage(ctx);
4896
+ if (effectiveDiff.kind === 'terminal') return effectiveDiff.result;
4897
+
4898
+ const merge = await this.refineMergeAndFinalizeStage(ctx);
4899
+ return (merge as { kind: 'terminal'; result: CommandRouterResult }).result;
4900
+ } catch (e: any) {
4901
+ return { success: false, error: e.message, refineStages };
4902
+ }
4903
+ }
4904
+
4905
+ /**
4906
+ * resolve_refs stage: resolve the mesh / worktree node / source node /
4907
+ * repoRoot, then the worktree branch, base branch, fetched base head and
4908
+ * branch head. Seeds the RefineContext consumed by every later stage.
4909
+ */
4910
+ private async refineResolveRefsStage(
4911
+ meshId: string,
4912
+ nodeId: string,
4913
+ args: any,
4914
+ refineStages: Array<Record<string, unknown>>,
4915
+ ): Promise<RefineStageOutcome> {
4772
4916
  // preferInline: same as startMeshRefineJob — inline-cache-only clone nodes must resolve.
4773
4917
  const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
4774
4918
  const mesh = meshRecord?.mesh;
4775
4919
  const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
4776
- if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
4920
+ if (!node) return { kind: 'terminal', result: { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages } };
4777
4921
 
4778
4922
  if (!node.isLocalWorktree || !node.workspace) {
4779
- return { success: false, error: `Refinery requires a local worktree node`, refineStages };
4923
+ return { kind: 'terminal', result: { success: false, error: `Refinery requires a local worktree node`, refineStages } };
4780
4924
  }
4781
4925
 
4782
4926
  const sourceNode = node.clonedFromNodeId
4783
4927
  ? mesh?.nodes.find((n: any) => meshNodeIdMatches(n, node.clonedFromNodeId))
4784
4928
  : mesh?.nodes.find((n: any) => !n.isLocalWorktree);
4785
4929
  const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
4786
- if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
4930
+ if (!repoRoot) return { kind: 'terminal', result: { success: false, error: 'Source node repoRoot not found', refineStages } };
4787
4931
 
4788
4932
  const { execFile } = await import('node:child_process');
4789
4933
  const { promisify } = await import('node:util');
4790
- const execFileAsync = promisify(execFile);
4934
+ const execFileAsync = promisify(execFile) as unknown as RefineExecFileAsync;
4791
4935
 
4792
4936
  const resolveStarted = Date.now();
4793
4937
  const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
4794
4938
  const branch = branchStdout.trim();
4795
- if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
4939
+ if (!branch) return { kind: 'terminal', result: { success: false, error: 'Could not determine branch of the worktree node', refineStages } };
4796
4940
 
4797
4941
  const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
4798
4942
  const baseBranch = baseBranchStdout.trim();
@@ -4819,9 +4963,39 @@ export class DaemonCommandRouter {
4819
4963
 
4820
4964
  const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
4821
4965
  const baseHead = baseHeadRaw;
4822
- let branchHead = branchHeadStdout.trim();
4966
+ const branchHead = branchHeadStdout.trim();
4823
4967
  recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead, ...(fetchWarning ? { fetchWarning } : {}) });
4824
4968
 
4969
+ return {
4970
+ kind: 'continue',
4971
+ ctx: {
4972
+ meshId,
4973
+ nodeId,
4974
+ args,
4975
+ refineStages,
4976
+ execFileAsync,
4977
+ mesh,
4978
+ node,
4979
+ sourceNode,
4980
+ repoRoot,
4981
+ branch,
4982
+ baseBranch,
4983
+ baseHead,
4984
+ branchHead,
4985
+ validationSummary: undefined as any,
4986
+ patchEquivalence: undefined as any,
4987
+ submoduleReachability: undefined as any,
4988
+ },
4989
+ };
4990
+ }
4991
+
4992
+ /**
4993
+ * validation stage: run the refinery validation gate (typecheck / test /
4994
+ * lint / build per node config) and block on failure or when no allowlisted
4995
+ * command was available. On pass, stores the summary on the context.
4996
+ */
4997
+ private async refineValidationStage(ctx: RefineContext): Promise<RefineStageOutcome> {
4998
+ const { mesh, node, branch, baseBranch, refineStages } = ctx;
4825
4999
  const validationStarted = Date.now();
4826
5000
  const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace, {
4827
5001
  // M2-2: consume the node's persisted bootstrap state; persist re-runs.
@@ -4833,6 +5007,7 @@ export class DaemonCommandRouter {
4833
5007
  .catch(() => { /* persistence is best-effort */ });
4834
5008
  },
4835
5009
  });
5010
+ ctx.validationSummary = validationSummary;
4836
5011
  recordMeshRefineStage(
4837
5012
  refineStages,
4838
5013
  'validation',
@@ -4868,7 +5043,7 @@ export class DaemonCommandRouter {
4868
5043
  tail ? `Output (tail):\n${tail}` : '',
4869
5044
  ].filter(Boolean).join('\n');
4870
5045
  };
4871
- return {
5046
+ return { kind: 'terminal', result: {
4872
5047
  success: false,
4873
5048
  code: validationSummary.failureCode || 'validation_failed',
4874
5049
  convergenceStatus: 'blocked_review',
@@ -4885,10 +5060,10 @@ export class DaemonCommandRouter {
4885
5060
  validation: 'failed',
4886
5061
  status: 'blocked_review',
4887
5062
  },
4888
- };
5063
+ } };
4889
5064
  }
4890
5065
  if (validationSummary.status === 'skipped') {
4891
- return {
5066
+ return { kind: 'terminal', result: {
4892
5067
  success: false,
4893
5068
  code: 'validation_unavailable',
4894
5069
  convergenceStatus: 'blocked_review',
@@ -4905,9 +5080,22 @@ export class DaemonCommandRouter {
4905
5080
  validation: 'unavailable',
4906
5081
  status: 'blocked_review',
4907
5082
  },
4908
- };
5083
+ } };
4909
5084
  }
4910
5085
 
5086
+ return { kind: 'continue', ctx };
5087
+ }
5088
+
5089
+ /**
5090
+ * patch_equivalence stage: preflight that the worktree branch's cumulative
5091
+ * patch is equivalent to base+branch. On a "behind base" branch, auto-rebase
5092
+ * once and re-check; on an empty merge-tree with real branch changes, treat as
5093
+ * already-merged-via-another-path and short-circuit to cleanup. Mutates the
5094
+ * context's branchHead (after rebase) and patchEquivalence (rebased gate).
5095
+ */
5096
+ private async refinePatchEquivalenceStage(ctx: RefineContext): Promise<RefineStageOutcome> {
5097
+ const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, validationSummary, refineStages, execFileAsync } = ctx;
5098
+ let branchHead = ctx.branchHead;
4911
5099
  const patchEquivalenceStarted = Date.now();
4912
5100
  let patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
4913
5101
  recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
@@ -4950,7 +5138,7 @@ export class DaemonCommandRouter {
4950
5138
  patchEquivalence = rebasedPatchEquivalence;
4951
5139
  didAutoRebase = true;
4952
5140
  } else {
4953
- return {
5141
+ return { kind: 'terminal', result: {
4954
5142
  success: false,
4955
5143
  code: 'needs_rebase',
4956
5144
  convergenceStatus: 'blocked_review',
@@ -4969,14 +5157,14 @@ export class DaemonCommandRouter {
4969
5157
  patchEquivalence: 'failed',
4970
5158
  status: 'blocked_review',
4971
5159
  },
4972
- };
5160
+ } };
4973
5161
  }
4974
5162
  } catch (rebaseErr: any) {
4975
5163
  try { execFileSync('git', ['rebase', '--abort'], { cwd: node.workspace, stdio: 'ignore' }); } catch { /* ignore */ }
4976
5164
  recordMeshRefineStage(refineStages, 'patch_equivalence_after_auto_rebase', 'failed', autoRebaseStarted, {
4977
5165
  error: rebaseErr?.message || String(rebaseErr),
4978
5166
  });
4979
- return {
5167
+ return { kind: 'terminal', result: {
4980
5168
  success: false,
4981
5169
  code: 'needs_rebase_with_conflicts',
4982
5170
  convergenceStatus: 'blocked_review',
@@ -4995,7 +5183,7 @@ export class DaemonCommandRouter {
4995
5183
  patchEquivalence: 'failed',
4996
5184
  status: 'blocked_review',
4997
5185
  },
4998
- };
5186
+ } };
4999
5187
  }
5000
5188
  }
5001
5189
 
@@ -5011,7 +5199,7 @@ export class DaemonCommandRouter {
5011
5199
  // is a degenerate worktree case, not an "already merged" scenario.
5012
5200
  const alreadyMergedViaOtherPath = !patchEquivalence.actualPatchId && !!patchEquivalence.expectedPatchId;
5013
5201
  if (!didAutoRebase && !alreadyMergedViaOtherPath) {
5014
- return {
5202
+ return { kind: 'terminal', result: {
5015
5203
  success: false,
5016
5204
  code: 'patch_equivalence_failed',
5017
5205
  convergenceStatus: 'blocked_review',
@@ -5030,7 +5218,7 @@ export class DaemonCommandRouter {
5030
5218
  patchEquivalence: 'failed',
5031
5219
  status: 'blocked_review',
5032
5220
  },
5033
- };
5221
+ } };
5034
5222
  }
5035
5223
 
5036
5224
  if (!didAutoRebase && alreadyMergedViaOtherPath) {
@@ -5059,7 +5247,7 @@ export class DaemonCommandRouter {
5059
5247
  payload: { alreadyMergedViaOtherPath: true, branch, into: baseBranch, validationSummary, patchEquivalence },
5060
5248
  });
5061
5249
  } catch { /* ledger append is best-effort */ }
5062
- return {
5250
+ return { kind: 'terminal', result: {
5063
5251
  success: removeResult?.success !== false,
5064
5252
  code: 'already_merged',
5065
5253
  merged: false,
@@ -5081,10 +5269,23 @@ export class DaemonCommandRouter {
5081
5269
  patchEquivalence: 'already_merged',
5082
5270
  status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged_to_main',
5083
5271
  },
5084
- };
5272
+ } };
5085
5273
  }
5086
5274
  }
5087
5275
 
5276
+ ctx.branchHead = branchHead;
5277
+ ctx.patchEquivalence = patchEquivalence;
5278
+ return { kind: 'continue', ctx };
5279
+ }
5280
+
5281
+ /**
5282
+ * submodule_reachability stage: verify every submodule gitlink commit that
5283
+ * would land via the merge is reachable from its configured remote main
5284
+ * branch (optionally auto-publishing when policy allows). Blocks the merge
5285
+ * when any commit is unreachable. Stores the result on the context.
5286
+ */
5287
+ private async refineSubmoduleReachabilityStage(ctx: RefineContext): Promise<RefineStageOutcome> {
5288
+ const { mesh, node, repoRoot, branch, baseBranch, branchHead, validationSummary, patchEquivalence, refineStages } = ctx;
5088
5289
  const submoduleReachabilityStarted = Date.now();
5089
5290
  const autoPublishSubmoduleMainCommits = resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace);
5090
5291
  const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
@@ -5141,7 +5342,7 @@ export class DaemonCommandRouter {
5141
5342
  });
5142
5343
  if (submoduleReachability.status === 'failed') {
5143
5344
  const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
5144
- return {
5345
+ return { kind: 'terminal', result: {
5145
5346
  success: false,
5146
5347
  code: 'submodule_reachability_failed',
5147
5348
  convergenceStatus: 'blocked_review',
@@ -5189,9 +5390,21 @@ export class DaemonCommandRouter {
5189
5390
  reason: 'submodule_publish_required',
5190
5391
  nextStep,
5191
5392
  },
5192
- };
5393
+ } };
5193
5394
  }
5194
5395
 
5396
+ ctx.submoduleReachability = submoduleReachability;
5397
+ return { kind: 'continue', ctx };
5398
+ }
5399
+
5400
+ /**
5401
+ * effective_diff stage (no-op guard): block a silent no-op merge where the
5402
+ * branch produces no effective root-tree diff against base — typically a
5403
+ * submodule that has commits but whose root-level gitlink (pointer) bump was
5404
+ * never committed, so the merge would land nothing real on main.
5405
+ */
5406
+ private async refineEffectiveDiffStage(ctx: RefineContext): Promise<RefineStageOutcome> {
5407
+ const { repoRoot, baseHead, branchHead, branch, baseBranch, validationSummary, patchEquivalence, refineStages } = ctx;
5195
5408
  // No-op guard: block a silent no-op merge where the root tree is identical to base.
5196
5409
  // This catches the trap where a submodule has commits but the root branch never
5197
5410
  // committed the gitlink (oss-pointer) bump — merging would report success while the
@@ -5213,7 +5426,7 @@ export class DaemonCommandRouter {
5213
5426
  hintLines.length ? `Submodules with uncommitted pointer bumps:\n${hintLines.join('\n')}` : '',
5214
5427
  `Fix: commit the submodule pointer bump on '${branch}' (git add <submodule-path> && git commit), then re-run refine.`,
5215
5428
  ].filter(Boolean).join('\n');
5216
- return {
5429
+ return { kind: 'terminal', result: {
5217
5430
  success: false,
5218
5431
  code: 'no_effective_diff',
5219
5432
  convergenceStatus: 'blocked_review',
@@ -5236,9 +5449,20 @@ export class DaemonCommandRouter {
5236
5449
  reason: 'no_effective_diff',
5237
5450
  ...(effectiveDiff.submoduleHints?.length ? { submoduleHints: effectiveDiff.submoduleHints } : {}),
5238
5451
  },
5239
- };
5452
+ } };
5240
5453
  }
5241
5454
 
5455
+ return { kind: 'continue', ctx };
5456
+ }
5457
+
5458
+ /**
5459
+ * merge + finalize stage: perform the --no-ff merge, align submodule
5460
+ * checkouts after merge, clean up (remove) the worktree node per policy,
5461
+ * append the refinery ledger entry, and (unless approval is required) push the
5462
+ * base branch. Always terminal — produces the final CommandRouterResult.
5463
+ */
5464
+ private async refineMergeAndFinalizeStage(ctx: RefineContext): Promise<RefineStageOutcome> {
5465
+ const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, sourceNode, validationSummary, patchEquivalence, submoduleReachability, mesh, refineStages, execFileAsync } = ctx;
5242
5466
  let mergeResult: Record<string, unknown> | undefined;
5243
5467
  const mergeStarted = Date.now();
5244
5468
  try {
@@ -5255,7 +5479,7 @@ export class DaemonCommandRouter {
5255
5479
  stdout: truncateValidationOutput(e?.stdout),
5256
5480
  stderr: truncateValidationOutput(e?.stderr),
5257
5481
  });
5258
- return {
5482
+ return { kind: 'terminal', result: {
5259
5483
  success: false,
5260
5484
  error: `Merge failed (conflicts?): ${e.message}`,
5261
5485
  validationSummary,
@@ -5270,7 +5494,7 @@ export class DaemonCommandRouter {
5270
5494
  patchEquivalence: 'passed',
5271
5495
  status: 'not_mergeable',
5272
5496
  },
5273
- };
5497
+ } };
5274
5498
  }
5275
5499
 
5276
5500
  const submoduleAlignmentStarted = Date.now();
@@ -5290,7 +5514,7 @@ export class DaemonCommandRouter {
5290
5514
  });
5291
5515
  }
5292
5516
  if (submoduleAlignment.status === 'failed') {
5293
- return {
5517
+ return { kind: 'terminal', result: {
5294
5518
  success: false,
5295
5519
  code: 'post_merge_submodule_alignment_failed',
5296
5520
  error: 'Refinery merge completed but post-merge submodule checkout alignment failed; run the reported git submodule update command and re-check base workspace status.',
@@ -5316,7 +5540,7 @@ export class DaemonCommandRouter {
5316
5540
  status: 'post_merge_alignment_failed',
5317
5541
  nextStep: submoduleAlignment.command || 'Run git submodule update --init --recursive for the reported path(s), then re-check base workspace status.',
5318
5542
  },
5319
- };
5543
+ } };
5320
5544
  }
5321
5545
 
5322
5546
  const cleanupStarted = Date.now();
@@ -5396,7 +5620,7 @@ export class DaemonCommandRouter {
5396
5620
  };
5397
5621
 
5398
5622
  if (removeResult?.success === false) {
5399
- return {
5623
+ return { kind: 'terminal', result: {
5400
5624
  success: false,
5401
5625
  code: 'cleanup_failed',
5402
5626
  error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
@@ -5412,7 +5636,7 @@ export class DaemonCommandRouter {
5412
5636
  refineStages,
5413
5637
  ...(ledgerError ? { ledgerError } : {}),
5414
5638
  finalBranchConvergenceState,
5415
- };
5639
+ } };
5416
5640
  }
5417
5641
 
5418
5642
  // Push logic: after a successful merge, either auto-push or surface push info
@@ -5439,7 +5663,7 @@ export class DaemonCommandRouter {
5439
5663
  }
5440
5664
  }
5441
5665
 
5442
- return {
5666
+ return { kind: 'terminal', result: {
5443
5667
  success: true,
5444
5668
  merged: true,
5445
5669
  branch,
@@ -5461,10 +5685,7 @@ export class DaemonCommandRouter {
5461
5685
  pushCommand: `git push origin ${baseBranch}`,
5462
5686
  pushNote: 'requireApprovalForPush is enabled — run the push command or obtain user approval before pushing.',
5463
5687
  }),
5464
- };
5465
- } catch (e: any) {
5466
- return { success: false, error: e.message, refineStages };
5467
- }
5688
+ } };
5468
5689
  }
5469
5690
 
5470
5691
  /**
@@ -6183,2772 +6404,32 @@ export class DaemonCommandRouter {
6183
6404
  }, args);
6184
6405
  }
6185
6406
 
6186
- switch (cmd) {
6187
- // ─── CLI / ACP commands ───
6188
- case 'mesh_forward_event': {
6189
- return handleMeshForwardEvent({ instanceManager: this.deps.instanceManager } as any, args as Record<string, unknown>);
6190
- }
6191
-
6192
- case 'get_pending_mesh_events': {
6193
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6194
- // (B3) Respect coordinatorDaemonId when the caller declares it
6195
- // so unicast events route to the right coordinator instead of
6196
- // being silently consumed by the first drainer.
6197
- const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
6198
- ? args.coordinatorDaemonId.trim()
6199
- : undefined;
6200
- const events = drainPendingMeshCoordinatorEvents(meshId || undefined, coordinatorDaemonId);
6201
- return { success: true, events };
6202
- }
6203
-
6204
- case 'interactive_prompt_response': {
6205
- const sessionId = typeof args?.targetSessionId === 'string' && args.targetSessionId.trim()
6206
- ? args.targetSessionId.trim()
6207
- : typeof args?.sessionId === 'string' && args.sessionId.trim()
6208
- ? args.sessionId.trim()
6209
- : '';
6210
- if (!sessionId) return { success: false, error: 'targetSessionId required' };
6211
- const response = normalizeInteractivePromptResponse(args?.response ?? args);
6212
- const instance = this.deps.instanceManager.getInstance(sessionId);
6213
- if (!instance) return { success: false, error: `No running instance for session ${sessionId}` };
6214
- this.deps.instanceManager.sendEvent(sessionId, 'interactive_prompt_response', response);
6215
- return { success: true };
6216
- }
6217
-
6218
- case 'launch_cli': {
6219
- // The coordinator routing anchor (meshCoordinatorDaemonId) is stamped
6220
- // upstream by mesh_launch_session, which resolves
6221
- // coordinatorNode.daemonId || ctx.localDaemonId || ctx.localMachineId and
6222
- // fail-closes for a remote node when none resolve. We deliberately do NOT
6223
- // self-stamp this daemon's own id when the field is missing: for a
6224
- // P2P-relayed remote worker launch, stamping the worker's own id would make
6225
- // the self-forward gate (mesh-events-coordinator: daemonIdsEquivalent) treat the
6226
- // worker as its own coordinator, suppressing the spontaneous completion-event
6227
- // forward and leaving the event in the pending inbox until a read_chat
6228
- // reconcile drains it. If the anchor is genuinely absent here, leave it
6229
- // absent rather than poison the routing.
6230
- const launchResult = await this.deps.cliManager.handleCliCommand(cmd, args);
6231
- // Bug C fix (part 1): when launching a mesh node worker session, surface
6232
- // bootstrapPending:true if the node's worktree bootstrap is still running.
6233
- // This is informational — the launch is NOT blocked here (blocking is done
6234
- // upstream by getWorktreeBootstrapLaunchBlock in the MCP layer).
6235
- const meshNodeId = readStringValue((args?.settings as any)?.meshNodeId);
6236
- const meshId = readStringValue((args?.settings as any)?.meshNodeFor);
6237
- if (meshNodeId && meshId && launchResult?.success !== false) {
6238
- try {
6239
- const { getMesh } = await import('../config/mesh-config.js');
6240
- const meshObj = getMesh(meshId) ?? this.getCachedInlineMesh(meshId);
6241
- const nodeObj = Array.isArray(meshObj?.nodes)
6242
- ? meshObj.nodes.find((n: any) => meshNodeIdMatches(n, meshNodeId))
6243
- : undefined;
6244
- const bootstrapStatus = readStringValue(nodeObj?.worktreeBootstrap?.status);
6245
- if (bootstrapStatus === 'running') {
6246
- return { success: true, ...launchResult, bootstrapPending: true };
6247
- }
6248
- } catch { /* best-effort — do not fail launch for bootstrap probe errors */ }
6249
- }
6250
- return launchResult;
6251
- }
6252
- case 'stop_cli':
6253
- case 'set_cli_view_mode':
6254
- case 'record_provider_pty': {
6255
- return this.deps.cliManager.handleCliCommand(cmd, args);
6256
- }
6257
- case 'agent_command': {
6258
- // Relay-safety stamp: a dispatch carrying meshContext.coordinatorDaemonId
6259
- // (mesh_send_task / queue assignment over P2P) is the worker daemon's chance
6260
- // to persist the coordinator routing anchor onto the target session BEFORE the
6261
- // turn runs. Without meshCoordinatorDaemonId on the session, the core forwarder
6262
- // (injectMeshSystemMessage) cannot resolve a remote coordinator target, so the
6263
- // completion event sits in the pending queue until a read_chat reconcile drains
6264
- // it. Stamping here makes a reused/relaunched remote session relay-safe at
6265
- // dispatch time even when it was not launched via mesh_launch_session.
6266
- {
6267
- const dispatchSessionId = readStringValue(args?.targetSessionId, (args as any)?.sessionId, (args as any)?.instanceId);
6268
- const dispatchMeshContext = args?.meshContext as Record<string, unknown> | undefined;
6269
- if (dispatchSessionId && dispatchMeshContext) {
6270
- try {
6271
- const inst = this.deps.instanceManager.getInstance(dispatchSessionId);
6272
- if (inst && typeof inst.updateSettings === 'function') {
6273
- const stamp = buildMeshWorkerRelayStamp(
6274
- inst.getState?.()?.settings as Record<string, unknown> | undefined,
6275
- {
6276
- meshId: dispatchMeshContext.meshId,
6277
- nodeId: dispatchMeshContext.nodeId,
6278
- coordinatorDaemonId: dispatchMeshContext.coordinatorDaemonId,
6279
- // Session-level anchor: preserved across the P2P dispatch to a
6280
- // remote worker so its completion echoes back to the right session.
6281
- coordinatorSessionId: dispatchMeshContext.coordinatorSessionId,
6282
- },
6283
- );
6284
- if (stamp) inst.updateSettings(stamp);
6285
- }
6286
- } catch { /* best-effort — dispatch still proceeds without the stamp */ }
6287
- }
6288
- }
6289
- const agentResult = await this.deps.cliManager.handleCliCommand(cmd, args);
6290
- // Bug C fix (part 2): when dispatching a task to a mesh node session, override
6291
- // the dispatch acknowledgement risk reason to 'bootstrap_still_running' when
6292
- // the target node's worktree bootstrap is still running. Informational only —
6293
- // dispatch is NOT blocked.
6294
- const meshCtx = args?.meshContext as Record<string, unknown> | undefined;
6295
- const dispatchNodeId = readStringValue(meshCtx?.nodeId);
6296
- const dispatchMeshId = readStringValue(meshCtx?.meshId);
6297
- if (dispatchNodeId && dispatchMeshId && agentResult?.success !== false) {
6298
- try {
6299
- const { getMesh } = await import('../config/mesh-config.js');
6300
- const meshObj = getMesh(dispatchMeshId) ?? this.getCachedInlineMesh(dispatchMeshId);
6301
- const nodeObj = Array.isArray(meshObj?.nodes)
6302
- ? meshObj.nodes.find((n: any) => meshNodeIdMatches(n, dispatchNodeId))
6303
- : undefined;
6304
- const bootstrapStatus = readStringValue(nodeObj?.worktreeBootstrap?.status);
6305
- if (bootstrapStatus === 'running') {
6306
- return {
6307
- success: true,
6308
- ...agentResult,
6309
- dispatchAcknowledgementRisk: true,
6310
- dispatchAcknowledgementRiskReason: 'bootstrap_still_running',
6311
- nextAction: 'Wait for worktree_bootstrap_complete event before dispatching work to this node.',
6312
- };
6313
- }
6314
- } catch { /* best-effort */ }
6315
- }
6316
- return agentResult;
6317
- }
6318
-
6319
- // ─── Logs ───
6320
- case 'list_saved_sessions': {
6321
- const providerType = typeof args?.providerType === 'string'
6322
- ? args.providerType.trim()
6323
- : typeof args?.agentType === 'string'
6324
- ? args.agentType.trim()
6325
- : '';
6326
- const kind = args?.kind === 'acp' ? 'acp' : 'cli';
6327
- if (!providerType) {
6328
- return { success: false, error: 'providerType required' };
6329
- }
6330
-
6331
- const wantsAll = args?.all === true;
6332
- const offset = wantsAll ? 0 : Math.max(0, Number(args?.offset) || 0);
6333
- const limit = wantsAll ? Number.MAX_SAFE_INTEGER : Math.max(1, Math.min(100, Number(args?.limit) || 30));
6334
- const requestedWorkspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
6335
- const requestedProviderSessionId = typeof args?.providerSessionId === 'string'
6336
- ? args.providerSessionId.trim()
6337
- : typeof args?.activeProviderSessionId === 'string'
6338
- ? args.activeProviderSessionId.trim()
6339
- : '';
6340
- const providerMeta = this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType);
6341
- const { sessions: historySessions, hasMore, source } = listProviderHistorySessions(providerType, {
6342
- canonicalHistory: providerMeta?.nativeHistory,
6343
- offset,
6344
- limit,
6345
- historyBehavior: providerMeta?.historyBehavior,
6346
- scripts: providerMeta?.scripts as any,
6347
- });
6348
- const state = loadState();
6349
- const savedSessions = getSavedProviderSessions(state, { providerType, kind });
6350
- const recentSessions = getRecentActivity(state, 200)
6351
- .filter(entry => entry.providerType === providerType && entry.kind === kind && entry.providerSessionId);
6352
- const savedSessionById = new Map(savedSessions.map(entry => [entry.providerSessionId, entry]));
6353
- const recentSessionById = new Map(recentSessions.map(entry => [entry.providerSessionId!, entry]));
6354
- const canResumeById = supportsExplicitSessionResume(providerMeta?.resume);
6355
-
6356
- return {
6357
- success: true,
6358
- sessions: historySessions.map(session => {
6359
- const saved = savedSessionById.get(session.historySessionId);
6360
- const recent = recentSessionById.get(session.historySessionId);
6361
- const workspace = saved?.workspace
6362
- || recent?.workspace
6363
- || session.workspace
6364
- || (requestedWorkspace && requestedProviderSessionId === session.historySessionId ? requestedWorkspace : undefined);
6365
- return {
6366
- id: session.historySessionId,
6367
- providerSessionId: session.historySessionId,
6368
- providerType,
6369
- providerName: saved?.providerName || recent?.providerName || providerType,
6370
- kind: saved?.kind || recent?.kind || kind,
6371
- title: saved?.title || recent?.title || session.sessionTitle || session.preview || providerType,
6372
- workspace,
6373
- summaryMetadata: saved?.summaryMetadata || recent?.summaryMetadata,
6374
- preview: session.preview,
6375
- messageCount: session.messageCount,
6376
- firstMessageAt: session.firstMessageAt,
6377
- lastMessageAt: session.lastMessageAt,
6378
- canResume: !!workspace && canResumeById,
6379
- historySource: session.source,
6380
- sourcePath: session.sourcePath,
6381
- sourceMtimeMs: session.sourceMtimeMs,
6382
- };
6383
- }),
6384
- hasMore,
6385
- source,
6386
- };
6387
- }
6388
-
6389
- // ─── restart_session: IDE / CLI / ACP unified ───
6390
- case 'restart_session': {
6391
- const targetType = args?.cliType || args?.agentType || args?.ideType;
6392
- if (!targetType) throw new Error('cliType or ideType required');
6393
-
6394
- // Check if IDE (in cdpManagers or provider category is ide)
6395
- const isIde = this.deps.cdpManagers.has(targetType) ||
6396
- this.deps.providerLoader.getMeta(targetType)?.category === 'ide';
6397
-
6398
- if (isIde) {
6399
- // IDE restart: stop (with process kill) → launch
6400
- await this.stopIde(targetType, true);
6401
- const launchResult = await this.executeDaemonCommand('launch_ide', { ideType: targetType, enableCdp: true, workspace: args?.workspace });
6402
- return { success: true, restarted: true, ideType: targetType, launch: launchResult };
6403
- }
6404
-
6405
- // CLI/ACP restart: delegate to CliManager
6406
- return this.deps.cliManager.handleCliCommand(cmd, args);
6407
- }
6408
-
6409
- // ─── IDE stop ───
6410
- case 'stop_ide': {
6411
- const ideType = args?.ideType;
6412
- if (!ideType) throw new Error('ideType required');
6413
- const killProcess = args?.killProcess !== false; // default true
6414
- await this.stopIde(ideType, killProcess);
6415
- try {
6416
- const results = await detectIDEs(this.deps.providerLoader);
6417
- this.deps.detectedIdes.value = results;
6418
- this.deps.providerLoader.setIdeDetectionResults(results, true);
6419
- } catch { /* ignore detection refresh errors */ }
6420
- return { success: true, ideType, stopped: true, processKilled: killProcess };
6421
- }
6422
-
6423
- // ─── IDE restart ───
6424
- case 'restart_ide': {
6425
- const ideType = args?.ideType;
6426
- if (!ideType) throw new Error('ideType required');
6427
- await this.stopIde(ideType, true); // always kill process on restart
6428
- const launchResult = await this.executeDaemonCommand('launch_ide', { ideType, enableCdp: true, workspace: args?.workspace });
6429
- return { success: true, ideType, restarted: true, launch: launchResult };
6430
- }
6431
-
6432
- // ─── IDE launch + CDP connect ───
6433
- case 'launch_ide': {
6434
- const ideKey = args?.ideId || args?.ideType;
6435
- const resolvedWorkspace = resolveIdeLaunchWorkspace(
6436
- {
6437
- workspace: args?.workspace,
6438
- workspaceId: args?.workspaceId,
6439
- useDefaultWorkspace: args?.useDefaultWorkspace,
6440
- },
6441
- loadConfig(),
6442
- );
6443
- const launchArgs = {
6444
- ideId: ideKey,
6445
- workspace: resolvedWorkspace,
6446
- newWindow: args?.newWindow,
6447
- };
6448
- LOG.info('LaunchIDE', `target=${ideKey || 'auto'}`);
6449
- const result = await launchWithCdp(launchArgs);
6450
-
6451
- if (result.success && result.port && result.ideId && !this.deps.cdpManagers.has(result.ideId)) {
6452
- const logFn = this.deps.getCdpLogFn
6453
- ? this.deps.getCdpLogFn(result.ideId)
6454
- : LOG.forComponent(`CDP:${result.ideId}`).asLogFn();
6455
- const provider = this.deps.providerLoader.getMeta(result.ideId);
6456
- const manager = new DaemonCdpManager(result.port, logFn, undefined, provider?.targetFilter);
6457
- const connected = await manager.connect();
6458
- if (connected) {
6459
- // Register active extension providers for this IDE in CDP manager
6460
- registerExtensionProviders(this.deps.providerLoader, manager, result.ideId);
6461
- this.deps.cdpManagers.set(result.ideId, manager);
6462
- LOG.info('CDP', `Connected: ${result.ideId} (port ${result.port})`);
6463
- LOG.info('CDP', `${this.deps.cdpManagers.size} IDE(s) connected`);
6464
-
6465
- // Notify consumer (e.g. setupIdeInstance)
6466
- this.deps.onCdpManagerCreated?.(result.ideId, manager);
6467
- }
6468
- }
6469
- this.deps.onIdeConnected?.();
6470
- try {
6471
- const results = await detectIDEs(this.deps.providerLoader);
6472
- this.deps.detectedIdes.value = results;
6473
- this.deps.providerLoader.setIdeDetectionResults(results, true);
6474
- } catch { /* ignore detection refresh errors */ }
6475
- if (result.success && resolvedWorkspace) {
6476
- try {
6477
- const next = appendRecentActivity(loadState(), {
6478
- kind: 'ide',
6479
- providerType: result.ideId || ideKey,
6480
- providerName: result.ideId || ideKey,
6481
- workspace: resolvedWorkspace,
6482
- title: result.ideId || ideKey,
6483
- });
6484
- saveState(next);
6485
- } catch { /* ignore activity persist errors */ }
6486
- } else if (result.success && (result.ideId || ideKey)) {
6487
- try {
6488
- saveState(appendRecentActivity(loadState(), {
6489
- kind: 'ide',
6490
- providerType: result.ideId || ideKey,
6491
- providerName: result.ideId || ideKey,
6492
- title: result.ideId || ideKey,
6493
- }));
6494
- } catch { /* ignore activity persist errors */ }
6495
- }
6496
- return { ...result };
6497
- }
6498
-
6499
- // ─── Detect providers ───
6500
- case 'detect_provider': {
6501
- const providerType = typeof args?.providerType === 'string' ? args.providerType.trim() : '';
6502
- if (!providerType) return { success: false, error: 'providerType is required' };
6503
- const normalizedType = this.deps.providerLoader.resolveAlias(providerType);
6504
- const provider = this.deps.providerLoader.getByAlias(providerType);
6505
- if (!provider) return { success: false, error: `Provider not found: ${providerType}` };
6506
- if (provider.category !== 'cli' && provider.category !== 'acp') {
6507
- return { success: false, error: `Provider detection is only supported for CLI/ACP providers: ${providerType}` };
6508
- }
6509
- if (!this.deps.providerLoader.isMachineProviderEnabled(normalizedType)) {
6510
- return { success: false, error: `Provider is disabled on this machine: ${providerType}` };
6511
- }
6512
- const detected = await detectCLI(normalizedType, this.deps.providerLoader, { includeVersion: false });
6513
- this.deps.providerLoader.setCliDetectionResults([{
6514
- id: normalizedType,
6515
- installed: !!detected,
6516
- path: detected?.path,
6517
- }], false);
6518
- this.deps.onStatusChange?.();
6519
- return {
6520
- success: true,
6521
- providerType: normalizedType,
6522
- detected: !!detected,
6523
- path: detected?.path || null,
6524
- };
6525
- }
6526
-
6527
- // ─── Detect IDEs ───
6528
- case 'detect_ides': {
6529
- const results = await detectIDEs(this.deps.providerLoader);
6530
- this.deps.detectedIdes.value = results;
6531
- this.deps.providerLoader.setIdeDetectionResults(results, true);
6532
- return { success: true, detectedInfo: results };
6533
- }
6534
-
6535
- // ─── Mesh CRUD (local meshes.json) ───
6536
- case 'list_meshes': {
6537
- try {
6538
- const { listMeshes } = await import('../config/mesh-config.js');
6539
- return { success: true, meshes: listMeshes() };
6540
- } catch (e: any) {
6541
- return { success: false, error: e.message };
6542
- }
6543
- }
6544
-
6545
- case 'get_mesh': {
6546
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6547
- if (!meshId) return { success: false, error: 'meshId required' };
6548
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
6549
- if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
6550
-
6551
- const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
6552
- // Only an explicit refresh fans out a blocking peer probe.
6553
- // Default loads are satisfied from held standing-state git truth.
6554
- const probeRemotePeers = args?.refresh === true || args?.forceRefresh === true;
6555
- const directTruth = await hydrateInlineMeshDirectTruth({
6556
- mesh: meshRecord.mesh,
6557
- meshSource: meshRecord.source,
6558
- dispatchMeshCommand: this.deps.dispatchMeshCommand,
6559
- getMeshPeerConnectionStatus: this.deps.getMeshPeerConnectionStatus,
6560
- statusInstanceId: this.deps.statusInstanceId,
6561
- localMachineId: loadConfig().machineId || '',
6562
- probeRemotePeers,
6563
- probeCache: this.meshGitProbeCache,
6564
- });
6565
- const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
6566
- const sourceOfTruth = {
6567
- membership: meshRecord.source === 'inline_cache'
6568
- ? 'coordinator_inline_mesh_cache'
6569
- : meshRecord.source === 'local_config'
6570
- ? 'local_mesh_config'
6571
- : 'inline_bootstrap_snapshot',
6572
- coordinatorOwnsLiveTruth: directTruthSatisfied,
6573
- directPeerTruth: {
6574
- required: requireDirectPeerTruth,
6575
- satisfied: directTruthSatisfied,
6576
- directEvidenceCount: directTruth.directEvidenceCount,
6577
- localConfirmedCount: directTruth.localConfirmedCount,
6578
- peerAttemptedCount: directTruth.peerAttemptedCount,
6579
- peerConfirmedCount: directTruth.peerConfirmedCount,
6580
- unavailableNodeIds: directTruth.unavailableNodeIds,
6581
- },
6582
- };
6583
- if (requireDirectPeerTruth && !directTruthSatisfied) {
6584
- return {
6585
- success: false,
6586
- code: 'mesh_direct_peer_truth_unavailable',
6587
- error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
6588
- sourceOfTruth,
6589
- };
6590
- }
6591
- return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
6592
- }
6593
-
6594
- case 'create_mesh': {
6595
- const name = typeof args?.name === 'string' ? args.name.trim() : '';
6596
- const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
6597
- const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
6598
- const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
6599
- if (!name) return { success: false, error: 'name required' };
6600
- try {
6601
- const { createMesh } = await import('../config/mesh-config.js');
6602
- const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
6603
- ? args.meshHost
6604
- : undefined;
6605
- const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
6606
- return { success: true, mesh };
6607
- } catch (e: any) {
6608
- return { success: false, error: e.message };
6609
- }
6610
- }
6611
-
6612
- case 'update_mesh': {
6613
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6614
- if (!meshId) return { success: false, error: 'meshId required' };
6615
- try {
6616
- const { updateMesh } = await import('../config/mesh-config.js');
6617
- const patch: Record<string, unknown> = {};
6618
- if (typeof args?.name === 'string') patch.name = args.name;
6619
- if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
6620
- if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
6621
- if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
6622
- if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
6623
- if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
6624
- const mesh = updateMesh(meshId, patch as any);
6625
- if (!mesh) return { success: false, error: 'Mesh not found' };
6626
- this.inlineMeshCache.set(meshId, mesh);
6627
- this.invalidateAggregateMeshStatus(meshId);
6628
- return { success: true, mesh };
6629
- } catch (e: any) {
6630
- return { success: false, error: e.message };
6631
- }
6632
- }
6633
-
6634
- case 'get_mesh_host_pairing': {
6635
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6636
- if (!meshId) return { success: false, error: 'meshId required' };
6637
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
6638
- const mesh = meshRecord?.mesh;
6639
- if (!mesh) return { success: false, error: 'Mesh not found' };
6640
- const meshHost = resolveMeshHostStatus(mesh);
6641
- const pairingStatus = meshHost.pairing?.status || 'not_configured';
6642
- return {
6643
- success: true,
6644
- code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
6645
- meshId,
6646
- hostAddress: meshHost.hostAddress,
6647
- meshHost,
6648
- manualPairing: {
6649
- status: pairingStatus,
6650
- joinImplemented: true,
6651
- protocol: 'standalone_command_direct_v1',
6652
- description: 'Standalone manual pairing can save address/token metadata, apply a host join over direct standalone command HTTP or injected mesh command dispatch, and check persisted status. P2P signaling remains outside this slice.',
6653
- },
6654
- };
6655
- }
6656
-
6657
- case 'configure_mesh_host_pairing': {
6658
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6659
- const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
6660
- const token = typeof args?.token === 'string' ? args.token.trim() : '';
6661
- if (!meshId) return { success: false, error: 'meshId required' };
6662
- if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
6663
- try {
6664
- const { configureMeshHostPairing } = await import('../config/mesh-config.js');
6665
- const configured = configureMeshHostPairing(meshId, { hostAddress, token });
6666
- if (!configured) return { success: false, error: 'Mesh not found' };
6667
- this.inlineMeshCache.set(meshId, configured.mesh);
6668
- const meshHost = resolveMeshHostStatus(configured.mesh);
6669
- return {
6670
- success: true,
6671
- code: 'mesh_host_pairing_pending',
6672
- meshId,
6673
- hostAddress: configured.hostAddress,
6674
- meshHost,
6675
- manualPairing: {
6676
- status: meshHost.pairing?.status || 'pairing',
6677
- joinImplemented: true,
6678
- protocol: 'standalone_command_direct_v1',
6679
- description: 'Manual Mesh Host pairing config was saved locally. Use join_mesh_host_pairing to apply it to the host. Raw token was not persisted.',
6680
- },
6681
- };
6682
- } catch (e: any) {
6683
- return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
6684
- }
6685
- }
6686
-
6687
- case 'create_mesh_host_pairing_token': {
6688
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6689
- if (!meshId) return { success: false, error: 'meshId required' };
6690
- try {
6691
- const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
6692
- const created = createMeshHostPairingToken(meshId, {
6693
- token: typeof args?.token === 'string' ? args.token : undefined,
6694
- expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
6695
- });
6696
- if (!created) return { success: false, error: 'Mesh not found' };
6697
- this.inlineMeshCache.set(meshId, created.mesh);
6698
- this.invalidateAggregateMeshStatus(meshId);
6699
- return {
6700
- success: true,
6701
- code: 'mesh_host_pairing_token_created',
6702
- meshId,
6703
- token: created.token,
6704
- tokenId: created.tokenId,
6705
- expiresAt: created.expiresAt,
6706
- meshHost: resolveMeshHostStatus(created.mesh),
6707
- warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
6708
- };
6709
- } catch (e: any) {
6710
- return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
6711
- }
6712
- }
6713
-
6714
- case 'apply_mesh_host_join': {
6715
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6716
- const token = typeof args?.token === 'string' ? args.token.trim() : '';
6717
- const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
6718
- ? args.memberNode
6719
- : null;
6720
- if (!meshId) return { success: false, error: 'meshId required' };
6721
- if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
6722
- try {
6723
- const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
6724
- const applied = applyMeshHostJoinRequest(meshId, {
6725
- token,
6726
- memberNode: memberNode as any,
6727
- memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
6728
- });
6729
- if (!applied) return { success: false, error: 'Mesh not found' };
6730
- if (!applied.accepted) {
6731
- return {
6732
- success: false,
6733
- code: 'mesh_host_join_rejected',
6734
- meshId,
6735
- tokenId: applied.tokenId,
6736
- meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
6737
- error: applied.reason,
6738
- };
6739
- }
6740
- this.inlineMeshCache.set(meshId, applied.mesh);
6741
- this.invalidateAggregateMeshStatus(meshId);
6742
- try {
6743
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
6744
- appendLedgerEntry(meshId, {
6745
- kind: 'node_joined',
6746
- nodeId: applied.node.id,
6747
- payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
6748
- });
6749
- } catch { /* ledger append is best-effort */ }
6750
- return {
6751
- success: true,
6752
- code: 'mesh_host_join_accepted',
6753
- meshId,
6754
- node: applied.node,
6755
- tokenId: applied.tokenId,
6756
- meshHost: resolveMeshHostStatus(applied.mesh),
6757
- };
6758
- } catch (e: any) {
6759
- return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
6760
- }
6761
- }
6762
-
6763
- case 'join_mesh_host_pairing': {
6764
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6765
- const token = typeof args?.token === 'string' ? args.token.trim() : '';
6766
- if (!meshId) return { success: false, error: 'meshId required' };
6767
- if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
6768
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
6769
- const mesh = meshRecord?.mesh;
6770
- if (!mesh) return { success: false, error: 'Mesh not found' };
6771
- const meshHost = resolveMeshHostStatus(mesh);
6772
- if (meshHost.role !== 'member') {
6773
- return { success: false, code: 'mesh_host_join_not_member', meshId, meshHost, error: 'join_mesh_host_pairing must run from a member daemon configured with a Mesh Host address/token.' };
6774
- }
6775
- try {
6776
- const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
6777
- const tokenId = tokenIdForManualPairing(token);
6778
- if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
6779
- return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
6780
- }
6781
- const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
6782
- if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
6783
- const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
6784
- const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
6785
- ? args.hostDaemonId.trim()
6786
- : meshHost.hostDaemonId;
6787
- let hostResult: any;
6788
- let transport: string;
6789
- if (hostDaemonId && this.deps.dispatchMeshCommand) {
6790
- transport = 'mesh_command_dispatch';
6791
- hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
6792
- meshId: hostMeshId,
6793
- token,
6794
- memberMeshId: meshId,
6795
- memberNode,
6796
- });
6797
- } else if (meshHost.hostAddress) {
6798
- transport = 'standalone_http_command';
6799
- const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
6800
- const response = await fetch(commandUrl, {
6801
- method: 'POST',
6802
- headers: { 'Content-Type': 'application/json' },
6803
- body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
6804
- });
6805
- hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
6806
- if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
6807
- } else {
6808
- return {
6809
- success: false,
6810
- code: 'mesh_host_join_transport_unavailable',
6811
- meshId,
6812
- meshHost,
6813
- error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
6814
- };
6815
- }
6816
- if (!hostResult?.success) {
6817
- return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
6818
- }
6819
- const joined = meshRecord.inline
6820
- ? null
6821
- : markMeshHostPairingJoined(meshId, {
6822
- tokenId: hostResult.tokenId || tokenId,
6823
- hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
6824
- hostNodeId: hostResult.meshHost?.hostNodeId,
6825
- joinedAt: hostResult.meshHost?.pairing?.joinedAt,
6826
- });
6827
- if (joined) {
6828
- this.inlineMeshCache.set(meshId, joined.mesh);
6829
- this.invalidateAggregateMeshStatus(meshId);
6830
- }
6831
- return {
6832
- success: true,
6833
- code: 'mesh_host_join_applied',
6834
- meshId,
6835
- hostMeshId,
6836
- transport,
6837
- node: hostResult.node,
6838
- tokenId: hostResult.tokenId || tokenId,
6839
- meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
6840
- hostResult,
6841
- manualPairing: {
6842
- status: 'paired',
6843
- joinImplemented: true,
6844
- protocol: 'standalone_command_direct_v1',
6845
- description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
6846
- },
6847
- };
6848
- } catch (e: any) {
6849
- return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
6850
- }
6851
- }
6852
-
6853
- case 'delete_mesh': {
6854
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6855
- if (!meshId) return { success: false, error: 'meshId required' };
6856
- try {
6857
- const { deleteMesh } = await import('../config/mesh-config.js');
6858
- const deleted = deleteMesh(meshId);
6859
- return { success: true, deleted };
6860
- } catch (e: any) {
6861
- return { success: false, error: e.message };
6862
- }
6863
- }
6864
-
6865
- case 'get_mesh_queue': {
6866
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6867
- if (!meshId) return { success: false, error: 'meshId required' };
6868
- try {
6869
- const { getMeshQueueStats, getQueue, describeTaskDependencyState } = await import('../mesh/mesh-work-queue.js');
6870
- const status = Array.isArray(args?.status)
6871
- ? args.status.map((s: any) => typeof s === 'string' ? s.trim() : '').filter(Boolean)
6872
- : undefined;
6873
- const rawQueue = getQueue(meshId, { status: status as any });
6874
- // M1: annotate dependency state at view time (waitingOn / dependenciesSatisfied).
6875
- const statusById = new Map(getQueue(meshId).map(task => [task.id, task.status]));
6876
- const queue = rawQueue.map(task =>
6877
- Array.isArray(task.dependsOn) && task.dependsOn.length > 0
6878
- ? { ...task, ...describeTaskDependencyState(task, statusById) }
6879
- : task);
6880
- const summary = getMeshQueueStats(meshId);
6881
- return {
6882
- success: true,
6883
- queue,
6884
- summary,
6885
- sourceOfTruth: {
6886
- kind: 'mesh_work_queue_file',
6887
- activeStatuses: ['pending', 'assigned'],
6888
- historicalStatuses: ['completed', 'failed', 'cancelled'],
6889
- notes: 'pending/assigned are active work; completed/failed/cancelled are historical records.',
6890
- },
6891
- };
6892
- } catch (e: any) {
6893
- return { success: false, error: e.message };
6894
- }
6895
- }
6896
-
6897
- case 'cancel_mesh_queue_task': {
6898
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6899
- const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
6900
- if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
6901
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
6902
- if (ownerFailure) return ownerFailure;
6903
- try {
6904
- const { cancelTask } = await import('../mesh/mesh-work-queue.js');
6905
- const reason = typeof args?.reason === 'string' ? args.reason : undefined;
6906
- const task = cancelTask(meshId, taskId, { reason });
6907
- if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
6908
- return { success: true, task };
6909
- } catch (e: any) {
6910
- return { success: false, error: e.message };
6911
- }
6912
- }
6913
-
6914
- case 'requeue_mesh_queue_task': {
6915
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6916
- const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
6917
- if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
6918
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
6919
- if (ownerFailure) return ownerFailure;
6920
- try {
6921
- const { requeueTask } = await import('../mesh/mesh-work-queue.js');
6922
- const task = requeueTask(meshId, taskId, {
6923
- reason: typeof args?.reason === 'string' ? args.reason : undefined,
6924
- targetNodeId: typeof args?.targetNodeId === 'string' ? args.targetNodeId.trim() : undefined,
6925
- targetSessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : undefined,
6926
- clearTargetNode: args?.clearTargetNode === true,
6927
- clearTargetSession: args?.clearTargetSession !== false,
6928
- });
6929
- if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
6930
- return { success: true, task };
6931
- } catch (e: any) {
6932
- return { success: false, error: e.message };
6933
- }
6934
- }
6935
-
6936
- case 'add_mesh_node': {
6937
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6938
- const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
6939
- if (!meshId) return { success: false, error: 'meshId required' };
6940
- if (!workspace) return { success: false, error: 'workspace required' };
6941
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
6942
- if (ownerFailure) return ownerFailure;
6943
- try {
6944
- const { addNode } = await import('../config/mesh-config.js');
6945
- const providerPriority = Array.isArray(args?.providerPriority)
6946
- ? args.providerPriority.map((type: any) => typeof type === 'string' ? type.trim() : '').filter(Boolean)
6947
- : [];
6948
- const readOnly = args?.readOnly === true;
6949
- const providerRoles = normalizeProviderRoles(args?.providerRoles);
6950
- const policy = {
6951
- ...(readOnly ? { readOnly: true } : {}),
6952
- ...(providerPriority.length ? { providerPriority } : {}),
6953
- ...(providerRoles.length ? { providerRoles } : {}),
6954
- };
6955
- const role = normalizeMeshDaemonRole(args?.role);
6956
- const daemonId = typeof args?.daemonId === 'string' && args.daemonId.trim() ? args.daemonId.trim() : undefined;
6957
- const machineId = typeof args?.machineId === 'string' && args.machineId.trim() ? args.machineId.trim() : undefined;
6958
- const repoRoot = typeof args?.repoRoot === 'string' && args.repoRoot.trim() ? args.repoRoot.trim() : undefined;
6959
- const node = addNode(meshId, {
6960
- workspace,
6961
- ...(repoRoot ? { repoRoot } : {}),
6962
- ...(daemonId ? { daemonId } : {}),
6963
- ...(machineId ? { machineId } : {}),
6964
- ...(policy ? { policy } : {}),
6965
- ...(role ? { role } : {}),
6966
- });
6967
- if (!node) return { success: false, error: 'Mesh not found' };
6968
- // mesh_status hands back a coordinator-memory aggregate
6969
- // snapshot keyed on (meshId, queueRevision). Adding a
6970
- // node touches neither, so without an explicit cache
6971
- // bust the dashboard graph keeps rendering the pre-add
6972
- // node list (empty for a fresh mesh) even after the
6973
- // user clicks Refresh.
6974
- this.invalidateAggregateMeshStatus(meshId);
6975
- return { success: true, node };
6976
- } catch (e: any) {
6977
- return { success: false, error: e.message };
6978
- }
6979
- }
6980
-
6981
- case 'update_mesh_node': {
6982
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6983
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
6984
- if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
6985
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
6986
- if (ownerFailure) return ownerFailure;
6987
- try {
6988
- const { updateNode } = await import('../config/mesh-config.js');
6989
- const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
6990
- ? { ...(args.policy as Record<string, unknown>) }
6991
- : {};
6992
- if (Array.isArray(args?.providerPriority)) {
6993
- const providerPriority = args.providerPriority
6994
- .map((type: any) => typeof type === 'string' ? type.trim() : '')
6995
- .filter(Boolean);
6996
- delete (policy as any).provider_priority;
6997
- if (providerPriority.length) {
6998
- (policy as any).providerPriority = providerPriority;
6999
- } else {
7000
- delete (policy as any).providerPriority;
7001
- }
7002
- }
7003
- // providerRoles: per-(node, provider) role label + maxParallel cap.
7004
- // Passing an explicit (possibly empty) array clears/replaces the
7005
- // declarations; omitting the arg leaves any value already on policy
7006
- // untouched (a full policy object passed by the caller still carries it).
7007
- if (Array.isArray(args?.providerRoles)) {
7008
- const providerRoles = normalizeProviderRoles(args.providerRoles);
7009
- if (providerRoles.length) {
7010
- (policy as any).providerRoles = providerRoles;
7011
- } else {
7012
- delete (policy as any).providerRoles;
7013
- }
7014
- }
7015
- const patch: Record<string, unknown> = { policy: policy as any };
7016
- if (typeof args?.systemPrompt === 'string') {
7017
- const trimmed = (args.systemPrompt as string).trim();
7018
- patch.systemPrompt = trimmed || undefined;
7019
- } else if (args?.systemPrompt === null) {
7020
- patch.systemPrompt = undefined;
7021
- }
7022
- const node = updateNode(meshId, nodeId, patch as any);
7023
- if (!node) return { success: false, error: 'Mesh node not found' };
7024
- // Provider priority / systemPrompt changes don't touch
7025
- // the queue revision, so without a manual bust the
7026
- // cached aggregate keeps surfacing pre-update values
7027
- // (priority chip, coordinator prompt preview, etc.).
7028
- this.invalidateAggregateMeshStatus(meshId);
7029
- return { success: true, node };
7030
- } catch (e: any) {
7031
- return { success: false, error: e.message };
7032
- }
7033
- }
7034
-
7035
- case 'cleanup_mesh_sessions': {
7036
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7037
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
7038
- if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
7039
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
7040
- if (ownerFailure) return ownerFailure;
7041
- try {
7042
- // preferInline so inline-cache-only clone nodes resolve (matches owner check above).
7043
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7044
- const mesh = meshRecord?.mesh;
7045
- if (!mesh) return { success: false, error: 'Mesh not found' };
7046
- const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7047
- if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
7048
- const mode = this.normalizeMeshSessionCleanupMode(args?.mode ?? mesh?.policy?.sessionCleanupOnNodeRemove);
7049
- const sessionIds = Array.isArray(args?.sessionIds)
7050
- ? args.sessionIds.map((id: any) => typeof id === 'string' ? id.trim() : '').filter(Boolean)
7051
- : undefined;
7052
- const result = await this.cleanupMeshSessions({
7053
- meshId,
7054
- nodeId,
7055
- node,
7056
- mode,
7057
- sessionIds,
7058
- dryRun: args?.dryRun === true,
7059
- source: 'mesh_cleanup_sessions',
7060
- });
7061
- return result;
7062
- } catch (e: any) {
7063
- return { success: false, error: e.message };
7064
- }
7065
- }
7066
-
7067
- case 'mesh_init': {
7068
- const workspace = typeof args?.workspace === 'string' && args.workspace.trim() ? args.workspace.trim() : process.cwd();
7069
- const mesh = args?.inlineMesh || {};
7070
- try {
7071
- const detected = await detectCLIs(this.deps.providerLoader, { includeVersion: true });
7072
- return { ...runMeshInit(mesh, workspace, detected, {
7073
- write: args?.write === true,
7074
- overwrite: args?.overwrite === true,
7075
- }) };
7076
- } catch (e: any) {
7077
- return { success: false, error: e?.message || String(e) };
7078
- }
7079
- }
7080
-
7081
- case 'plan_mesh_refine_node': {
7082
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7083
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
7084
- if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
7085
- // preferInline: plan is the dry-run sibling of refine — clone nodes must resolve.
7086
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7087
- const mesh = meshRecord?.mesh;
7088
- const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7089
- if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
7090
- return {
7091
- success: true,
7092
- dryRun: true,
7093
- nodeId,
7094
- workspace: node.workspace,
7095
- validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
7096
- mergeWillRun: false,
7097
- cleanupWillRun: false,
7098
- };
7099
- }
7100
-
7101
- case 'fast_forward_mesh_node': {
7102
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7103
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
7104
- let workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
7105
- let submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
7106
- ? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
7107
- : undefined;
7108
- let nodeDaemonId: string | undefined;
7109
- let allowAutoPublishSubmoduleMainCommits = false;
7110
- if (meshId && nodeId) {
7111
- // preferInline so fast-forward can resolve inline-cache-only clone worktree nodes.
7112
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7113
- const mesh = meshRecord?.mesh;
7114
- const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7115
- if (!workspace) {
7116
- workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
7117
- }
7118
- if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
7119
- submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
7120
- }
7121
- allowAutoPublishSubmoduleMainCommits = mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true;
7122
- nodeDaemonId = typeof node?.daemonId === 'string' ? node.daemonId.trim() : undefined;
7123
- }
7124
- // If the target node belongs to a remote daemon, forward the command there.
7125
- // _meshDirectDispatch prevents re-forwarding (and P2P self-dial) when the stored
7126
- // daemonId uses a legacy format that doesn't match the receiving daemon's identity.
7127
- const selfDaemonId = this.deps.statusInstanceId;
7128
- // daemonIdsEquivalent: a legacy-form stored daemonId that resolves to THIS
7129
- // machine's core must be treated as local (not remote) so it is not forwarded /
7130
- // P2P self-dialed. Equivalent → local.
7131
- const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
7132
- if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
7133
- const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'fast_forward_mesh_node', {
7134
- ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
7135
- workspace,
7136
- _meshDirectDispatch: true,
7137
- });
7138
- return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
7139
- }
7140
- const result = await (fastForwardMeshNode({
7141
- meshId: meshId || undefined,
7142
- nodeId: nodeId || undefined,
7143
- workspace,
7144
- branch: typeof args?.branch === 'string' ? args.branch : undefined,
7145
- execute: args?.execute === true,
7146
- dryRun: args?.dryRun === true,
7147
- updateSubmodules: args?.updateSubmodules === true,
7148
- submoduleIgnorePaths,
7149
- mode: args?.mode === 'push' ? 'push' : 'merge',
7150
- pushSubmodules: args?.pushSubmodules === true,
7151
- allowAutoPublishSubmoduleMainCommits,
7152
- }) as Promise<unknown>);
7153
- return result as CommandRouterResult;
7154
- }
7155
-
7156
- case 'refine_mesh_node': {
7157
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7158
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
7159
- if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
7160
-
7161
- // Remote forward: a worktree node lives on its OWN daemon's machine, so the
7162
- // refine (cd into node.workspace, merge → push → cleanup) must run on THAT
7163
- // daemon — not the coordinator, whose filesystem has no such path. The sibling
7164
- // fast_forward_mesh_node / clone_mesh_node handlers already forward to the
7165
- // node's daemon; refine_mesh_node was the gap (the coordinator would cd into a
7166
- // non-existent local path and fail), so remote-machine worktrees could not be
7167
- // converged at all. Forward both dry-run (plan reads the worktree git state)
7168
- // and execute (async merge job) so the same machine that owns the worktree
7169
- // resolves it.
7170
- //
7171
- // coordinatorDaemonId: refine is ASYNC — the completed/failed event is queued
7172
- // on the executing daemon's pending-events queue scoped to a coordinator id and
7173
- // recovered by the coordinator's reconcile loop (pullRemoteNodeQueues →
7174
- // get_pending_mesh_events). Without stamping our own status id, the remote
7175
- // daemon would fall back to ITS OWN statusInstanceId as the coordinator
7176
- // (startMeshRefineJob), scoping the terminal event to the wrong inbox where the
7177
- // real coordinator never pulls it. Stamp the canonical status id (which is in
7178
- // the coordinator's self-identity set used to scope the remote drain) so the
7179
- // event routes back here. Preserve any caller-supplied coordinatorDaemonId.
7180
- //
7181
- // _meshDirectDispatch prevents re-forwarding (and P2P self-dial) once the call
7182
- // has landed on the owning daemon — that daemon then executes locally even if
7183
- // the stored daemonId uses a legacy form that doesn't match its own identity.
7184
- {
7185
- const meshRecordForForward = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7186
- const forwardNode = meshRecordForForward?.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7187
- const nodeDaemonId = typeof forwardNode?.daemonId === 'string' ? forwardNode.daemonId.trim() : undefined;
7188
- const selfDaemonId = this.deps.statusInstanceId;
7189
- // daemonIdsEquivalent: a legacy-form daemonId resolving to this machine's core
7190
- // is local — execute locally instead of forwarding. Equivalent → local.
7191
- const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
7192
- if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
7193
- const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
7194
- ? args.coordinatorDaemonId.trim()
7195
- : undefined;
7196
- const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'refine_mesh_node', {
7197
- ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
7198
- coordinatorDaemonId: callerCoordinatorDaemonId || selfDaemonId,
7199
- _meshDirectDispatch: true,
7200
- });
7201
- return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
7202
- }
7203
- }
7204
-
7205
- // Dry-run (plan-only) is the default and stays synchronous: it does no
7206
- // validation/merge/push and returns the plan instantly. Only execute=true
7207
- // (and not dry_run) goes through the async refine job that actually
7208
- // validates → merges → pushes → cleans up. Mirrors the
7209
- // batch_refine_mesh_nodes / fast_forward_mesh_node dry_run/execute contract.
7210
- const isDryRun = args?.dryRun !== false && args?.execute !== true;
7211
- if (isDryRun) {
7212
- // preferInline: plan is the dry-run sibling of refine — clone nodes must resolve.
7213
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7214
- const mesh = meshRecord?.mesh;
7215
- const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7216
- if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
7217
- return {
7218
- success: true,
7219
- dryRun: true,
7220
- nodeId,
7221
- workspace: node.workspace,
7222
- validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
7223
- mergeWillRun: false,
7224
- cleanupWillRun: false,
7225
- hint: 'Dry-run only — no merge/push/cleanup performed. Re-invoke with execute:true to converge this node.',
7226
- };
7227
- }
7228
- return this.startMeshRefineJob(meshId, nodeId, args);
7229
- }
7230
-
7231
- case 'batch_refine_mesh_nodes': {
7232
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7233
- if (!meshId) return { success: false, error: 'meshId required' };
7234
- const requestedNodeIds = Array.isArray(args?.nodeIds)
7235
- ? (args.nodeIds as unknown[]).filter((v): v is string => typeof v === 'string' && v.trim().length > 0).map(v => v.trim())
7236
- : undefined;
7237
- // Dry-run (plan-only) stays synchronous: it does no validation/merge and
7238
- // returns instantly. Execute goes through the async batch job — immediate
7239
- // {async:true, status:'accepted'} + background convergence + terminal event,
7240
- // matching the single-node refine_mesh_node contract so long validation
7241
- // suites can't time out the IPC and strand the coordinator.
7242
- const isDryRun = args?.dryRun !== false && args?.execute !== true;
7243
- if (isDryRun) return this.batchRefineMeshNodes(meshId, requestedNodeIds, args);
7244
- return this.startMeshRefineBatchJob(meshId, requestedNodeIds, args);
7245
- }
7246
-
7247
- case 'remove_mesh_node': {
7248
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7249
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
7250
- if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
7251
- try {
7252
- // preferInline so removal can resolve inline-cache-only clone worktree nodes.
7253
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7254
- const mesh = meshRecord?.mesh;
7255
- const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7256
-
7257
- // Guard: refuse to remove the coordinator's OWN local base node
7258
- // (same machine, NOT a worktree). Removing it breaks live mesh
7259
- // membership — the coordinator can no longer be reached and has
7260
- // to be restarted. Worktree clones are always safe to remove;
7261
- // only the non-worktree node bound to this daemon is protected.
7262
- // An explicit force:true overrides for intentional mesh teardown.
7263
- if (node && !args?._meshDirectDispatch && node.isLocalWorktree !== true && args?.force !== true) {
7264
- const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : '';
7265
- const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>) || '';
7266
- const selfDaemonId = this.deps.statusInstanceId || '';
7267
- const selfMachineId = (() => { try { return loadConfig().machineId || ''; } catch { return ''; } })();
7268
- const isCoordinatorBaseNode =
7269
- (!!selfDaemonId && (nodeDaemonId === selfDaemonId || nodeMachineId === selfDaemonId))
7270
- || (!!selfMachineId && (nodeDaemonId === selfMachineId || nodeMachineId === selfMachineId));
7271
- if (isCoordinatorBaseNode) {
7272
- return {
7273
- success: false,
7274
- removed: false,
7275
- code: 'mesh_remove_coordinator_base_node_protected',
7276
- error: `Refusing to remove the coordinator's own base node '${typeof node.workspace === 'string' ? node.workspace : nodeId}'. `
7277
- + `It is the local non-worktree node bound to this coordinator daemon; removing it breaks live mesh membership and forces a restart.`,
7278
- recoveryHint: 'Remove worktree clone nodes instead, or pass force:true only if you are intentionally tearing down this mesh and accept that the coordinator must be re-registered/restarted.',
7279
- };
7280
- }
7281
- }
7282
-
7283
- const sessionCleanupMode = this.normalizeMeshSessionCleanupMode(
7284
- args?.sessionCleanupMode ?? args?.session_cleanup_mode ?? mesh?.policy?.sessionCleanupOnNodeRemove,
7285
- );
7286
- // Explicit sessionIds (e.g. supplied by refine auto-cleanup) bypass the
7287
- // workspace-only-match guard so a delegate session that lacks a
7288
- // meta.meshNodeId binding can still be stopped/deleted.
7289
- const explicitSessionIds = Array.isArray(args?.sessionIds)
7290
- ? (args.sessionIds as unknown[]).filter((v): v is string => typeof v === 'string' && v.trim().length > 0).map(v => v.trim())
7291
- : undefined;
7292
- let sessionCleanup: Record<string, unknown> | undefined;
7293
- if (node && sessionCleanupMode !== 'preserve') {
7294
- sessionCleanup = await this.cleanupMeshSessions({
7295
- meshId,
7296
- nodeId,
7297
- node,
7298
- mode: sessionCleanupMode,
7299
- ...(explicitSessionIds && explicitSessionIds.length > 0 ? { sessionIds: explicitSessionIds } : {}),
7300
- source: 'mesh_remove_node',
7301
- });
7302
- if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
7303
- }
7304
-
7305
- let worktreeCleanup: Record<string, unknown> | undefined;
7306
- if (node?.isLocalWorktree) {
7307
- const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
7308
- // daemonIdsEquivalent: an equivalent-form daemonId is this machine —
7309
- // clean up locally, do not forward. Equivalent → local.
7310
- const isRemoteWorktree = nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
7311
- && !args?._meshDirectDispatch;
7312
- if (isRemoteWorktree) {
7313
- // Worktree lives on a different machine — ask that daemon to clean it up.
7314
- // _meshDirectDispatch prevents re-forwarding when stored daemonId uses legacy format.
7315
- const forwarded = await this.deps.dispatchMeshCommand!(nodeDaemonId!, 'remove_mesh_node', {
7316
- ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
7317
- _meshDirectDispatch: true,
7318
- });
7319
- return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
7320
- }
7321
- const cleanupResult = await this.cleanupLocalWorktreeNode({ mesh, node, nodeId, force: args?.force === true });
7322
- // De-gating: membership removal is NOT gated on the worktree
7323
- // directory actually being deleted. cleanupLocalWorktreeNode now
7324
- // returns success:true (with a residue flag) whenever the path is
7325
- // proven managed and the only remaining problem is leftover
7326
- // directory bytes (e.g. Windows EINVAL). A success:false here means
7327
- // a genuinely-unsafe condition — missing metadata, a non-managed /
7328
- // unexpected path, a branch mismatch, a dirty worktree, or an
7329
- // unverified force fallback — and those still block removal.
7330
- if (cleanupResult.success === false) {
7331
- return {
7332
- success: false,
7333
- removed: false,
7334
- code: cleanupResult.code,
7335
- error: cleanupResult.error,
7336
- recoveryHint: cleanupResult.recoveryHint,
7337
- ...(sessionCleanup ? { sessionCleanup } : {}),
7338
- worktreeCleanup: cleanupResult,
7339
- };
7340
- }
7341
- worktreeCleanup = cleanupResult;
7342
- }
7343
-
7344
- let removed = false;
7345
- if (meshRecord?.inline) {
7346
- removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
7347
- // Inline meshes share the same aggregate snapshot cache as
7348
- // local-config meshes; without this bust the removed node
7349
- // keeps showing up in the dashboard graph until the cache
7350
- // ages out on its own.
7351
- if (removed) this.invalidateAggregateMeshStatus(meshId);
7352
- // Node was already absent from the inline mesh (e.g. removed by a
7353
- // prior refine cleanup). Treat as removed so caller gets removed:true.
7354
- if (!removed && !node) removed = true;
7355
- } else {
7356
- const { removeNode } = await import('../config/mesh-config.js');
7357
- removed = removeNode(meshId, nodeId);
7358
- // Node already absent from config (e.g. removed by a prior refine
7359
- // cleanup after a successful Refinery merge). Treat as removed so
7360
- // the response is accurate.
7361
- if (!removed && !node) removed = true;
7362
- if (removed) this.invalidateAggregateMeshStatus(meshId);
7363
- }
7364
-
7365
- // Record in task ledger
7366
- if (removed) {
7367
- try {
7368
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
7369
- appendLedgerEntry(meshId, {
7370
- kind: 'node_removed',
7371
- nodeId,
7372
- payload: {
7373
- worktree: !!node?.isLocalWorktree,
7374
- sessionCleanupMode,
7375
- workspace: typeof node?.workspace === 'string' ? node.workspace : undefined,
7376
- daemonId: typeof node?.daemonId === 'string' ? node.daemonId : undefined,
7377
- worktreeBranch: typeof node?.worktreeBranch === 'string' ? node.worktreeBranch : undefined,
7378
- worktreeCleanupFallback: typeof worktreeCleanup?.fallback === 'string' ? worktreeCleanup.fallback : undefined,
7379
- forced: worktreeCleanup?.forced === true ? true : undefined,
7380
- forceFallbackReason: typeof worktreeCleanup?.reason === 'string' ? worktreeCleanup.reason : undefined,
7381
- },
7382
- });
7383
- } catch { /* ledger append is best-effort */ }
7384
- }
7385
-
7386
- // Surface leftover-directory residue at the top level so callers
7387
- // see the node was dropped from the mesh even though the worktree
7388
- // directory could not be fully removed (best-effort, non-gating).
7389
- const residueWarning = worktreeCleanup?.residue === true && typeof worktreeCleanup?.residueWarning === 'string'
7390
- ? worktreeCleanup.residueWarning
7391
- : undefined;
7392
- return {
7393
- success: true,
7394
- removed,
7395
- ...(residueWarning ? { residueWarning } : {}),
7396
- ...(sessionCleanup ? { sessionCleanup } : {}),
7397
- ...(worktreeCleanup ? { worktreeCleanup } : {}),
7398
- };
7399
- } catch (e: any) {
7400
- return { success: false, error: e.message };
7401
- }
7402
- }
7403
-
7404
- case 'clone_mesh_node': {
7405
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7406
- const sourceNodeId = typeof args?.sourceNodeId === 'string' ? args.sourceNodeId.trim() : '';
7407
- const branch = typeof args?.branch === 'string' ? args.branch.trim() : '';
7408
- const baseBranch = typeof args?.baseBranch === 'string' ? args.baseBranch.trim() : undefined;
7409
- if (!meshId) return { success: false, error: 'meshId required' };
7410
- if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
7411
- if (!branch) return { success: false, error: 'branch required' };
7412
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
7413
- if (ownerFailure) return ownerFailure;
7414
-
7415
- try {
7416
- // Resolve with preferInline so the clone writes the new node into the
7417
- // same representation that get_mesh reads back. The MCP coordinator
7418
- // passes inlineMesh on every mesh command, so when it owns an inline
7419
- // mesh the membership read path (get_mesh, preferInline: true) returns
7420
- // the inline cache. Without preferInline here, clone could resolve to a
7421
- // local-config mesh and write the node only to config — leaving the
7422
- // inline cache (and therefore get_mesh / refreshMeshFromDaemon) without
7423
- // the node, so the new worktree node is never visible in live mesh
7424
- // membership even though worktree_bootstrap_complete fires.
7425
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7426
- const mesh = meshRecord?.mesh;
7427
- if (!mesh) return { success: false, error: 'Mesh not found' };
7428
-
7429
- const sourceNode = mesh.nodes?.find((n: any) => meshNodeIdMatches(n, sourceNodeId));
7430
- if (!sourceNode) return { success: false, error: `Source node '${sourceNodeId}' not found in mesh` };
7431
-
7432
- // Forward to the source node's daemon if it's on a different machine.
7433
- // _meshDirectDispatch prevents infinite re-forwarding when the stored daemonId
7434
- // uses a legacy format that doesn't match the receiving daemon's statusInstanceId.
7435
- const sourceDaemonId = typeof sourceNode.daemonId === 'string' ? sourceNode.daemonId.trim() : undefined;
7436
- // daemonIdsEquivalent: an equivalent-form source daemonId is this machine —
7437
- // clone locally, do not forward. Equivalent → local.
7438
- if (sourceDaemonId && !daemonIdsEquivalent(sourceDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
7439
- && !args?._meshDirectDispatch) {
7440
- const forwarded = await this.deps.dispatchMeshCommand(sourceDaemonId, 'clone_mesh_node', {
7441
- ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
7442
- _meshDirectDispatch: true,
7443
- });
7444
- return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
7445
- }
7446
-
7447
- const repoRoot = sourceNode.repoRoot || sourceNode.workspace;
7448
- const { createWorktree } = await import('../git/git-worktree.js');
7449
- const result = await createWorktree({
7450
- repoRoot,
7451
- branch,
7452
- baseBranch,
7453
- meshName: mesh.name,
7454
- });
7455
-
7456
- let node: any;
7457
- if (meshRecord.inline) {
7458
- const { randomUUID } = await import('crypto');
7459
- node = {
7460
- id: `node_${randomUUID().replace(/-/g, '')}`,
7461
- workspace: result.worktreePath,
7462
- repoRoot: result.worktreePath,
7463
- daemonId: sourceNode.daemonId,
7464
- machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
7465
- userOverrides: { ...(sourceNode.userOverrides || {}) },
7466
- policy: { ...(sourceNode.policy || {}) },
7467
- isLocalWorktree: true,
7468
- worktreeBranch: result.branch,
7469
- clonedFromNodeId: sourceNodeId,
7470
- };
7471
- this.updateInlineMeshNode(meshId, mesh, node);
7472
- } else {
7473
- const { addNode } = await import('../config/mesh-config.js');
7474
- node = addNode(meshId, {
7475
- workspace: result.worktreePath,
7476
- repoRoot: result.worktreePath,
7477
- daemonId: sourceNode.daemonId,
7478
- machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
7479
- userOverrides: { ...(sourceNode.userOverrides || {}) },
7480
- isLocalWorktree: true,
7481
- worktreeBranch: result.branch,
7482
- clonedFromNodeId: sourceNodeId,
7483
- policy: { ...(sourceNode.policy || {}) },
7484
- });
7485
- if (!node) return { success: false, error: 'Failed to register worktree node' };
7486
- // Also reconcile the freshly-registered node into any warmed inline
7487
- // cache for this mesh. get_mesh (preferInline: true) reads the inline
7488
- // cache first when one exists; if we only wrote to local config the
7489
- // node would be invisible to membership reads. updateInlineMeshNode is
7490
- // a no-op when no inline cache is present.
7491
- const inlineForReconcile = this.getCachedInlineMesh(meshId);
7492
- if (inlineForReconcile) this.updateInlineMeshNode(meshId, inlineForReconcile, node);
7493
- this.invalidateAggregateMeshStatus(meshId);
7494
- }
7495
-
7496
- const persistWorktreeSetupState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
7497
- node.worktreeBootstrap = bootstrapState;
7498
- if (meshRecord.inline) {
7499
- this.updateInlineMeshNode(meshId, mesh, node);
7500
- return;
7501
- }
7502
- try {
7503
- const { updateNode } = await import('../config/mesh-config.js');
7504
- updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
7505
- this.invalidateAggregateMeshStatus(meshId);
7506
- } catch { /* bootstrap status persistence is best-effort */ }
7507
- };
7508
-
7509
- const appendCloneLedger = async (initSubmodules: boolean, bootstrapState: WorktreeBootstrapState): Promise<void> => {
7510
- try {
7511
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
7512
- appendLedgerEntry(meshId, {
7513
- kind: 'node_cloned',
7514
- nodeId: node.id,
7515
- payload: {
7516
- sourceNodeId,
7517
- branch: result.branch,
7518
- worktreePath: result.worktreePath,
7519
- submodulesInitialized: initSubmodules,
7520
- worktreeBootstrap: {
7521
- status: bootstrapState.status,
7522
- required: bootstrapState.required,
7523
- configSource: bootstrapState.configSource,
7524
- configSourceType: bootstrapState.configSourceType,
7525
- lastCommand: bootstrapState.lastCommand,
7526
- exitCode: bootstrapState.exitCode,
7527
- },
7528
- },
7529
- });
7530
- } catch { /* ledger append is best-effort */ }
7531
- };
7532
-
7533
- const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
7534
- const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, result.worktreePath);
7535
- const runningBootstrapState: WorktreeBootstrapState = {
7536
- status: 'running',
7537
- required: loadedBootstrap.config?.required !== false,
7538
- configSource: loadedBootstrap.path || loadedBootstrap.source,
7539
- configSourceType: loadedBootstrap.sourceType,
7540
- startedAt: new Date().toISOString(),
7541
- };
7542
- await persistWorktreeSetupState(runningBootstrapState);
7543
-
7544
- const finishWorktreeSetup = async (): Promise<{ submodulesInitialized: boolean; bootstrapState: WorktreeBootstrapState }> => {
7545
- let submodulesInitialized = false;
7546
- if (initSubmodules) {
7547
- try {
7548
- const { runGit } = await import('../git/git-executor.js');
7549
- await runGit(
7550
- { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
7551
- ['submodule', 'update', '--init', '--recursive'],
7552
- { timeoutMs: 120000 },
7553
- );
7554
- submodulesInitialized = true;
7555
-
7556
- // Sync oss submodule to source node HEAD (best-effort)
7557
- const sourceWorkspace = sourceNode.repoRoot || sourceNode.workspace;
7558
- if (sourceWorkspace) {
7559
- try {
7560
- const { runGit: rg } = await import('../git/git-executor.js');
7561
- const sourceCtx = { workspace: sourceWorkspace, repoRoot: sourceWorkspace, isGitRepo: true };
7562
- const worktreeCtx = { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true };
7563
-
7564
- // Read source node's oss submodule SHA
7565
- const sourceStatusOut = await rg(sourceCtx, ['submodule', 'status', 'oss'], { timeoutMs: 10000 });
7566
- const sourceStatusLine = (typeof sourceStatusOut === 'string' ? sourceStatusOut : (sourceStatusOut as any)?.stdout ?? '').trim();
7567
- const sourceShaMatch = sourceStatusLine.match(/^[+\- ]?([0-9a-f]{40})/);
7568
- const sourceSha = sourceShaMatch?.[1];
7569
-
7570
- if (sourceSha) {
7571
- // Read worktree's current oss HEAD
7572
- const ossCtx = { workspace: `${result.worktreePath}/oss`, repoRoot: `${result.worktreePath}/oss`, isGitRepo: true };
7573
- const worktreeOssHeadOut = await rg(ossCtx, ['rev-parse', 'HEAD'], { timeoutMs: 10000 });
7574
- const worktreeOssSha = (typeof worktreeOssHeadOut === 'string' ? worktreeOssHeadOut : (worktreeOssHeadOut as any)?.stdout ?? '').trim();
7575
-
7576
- if (worktreeOssSha !== sourceSha) {
7577
- // Fetch target SHA from source node's oss directory
7578
- await rg(ossCtx, ['fetch', `${sourceWorkspace}/oss`, 'HEAD'], { timeoutMs: 60000 });
7579
- await rg(ossCtx, ['checkout', sourceSha], { timeoutMs: 10000 });
7580
- await rg(worktreeCtx, ['add', 'oss'], { timeoutMs: 10000 });
7581
- await rg(worktreeCtx, ['commit', '-m', 'chore: sync oss to source node HEAD on clone'], { timeoutMs: 10000 });
7582
- console.log(`[mesh] Synced oss submodule to source HEAD ${sourceSha.slice(0, 8)} in worktree`);
7583
- }
7584
- }
7585
- } catch (ossErr: any) {
7586
- console.warn('[mesh] oss submodule sync to source HEAD failed (best-effort):', ossErr.message);
7587
- }
7588
- }
7589
- } catch (subErr: any) {
7590
- // Submodule init is best-effort; don't fail the clone
7591
- console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
7592
- }
7593
- }
7594
- const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
7595
- await persistWorktreeSetupState(bootstrapState);
7596
- await appendCloneLedger(submodulesInitialized, bootstrapState);
7597
- return { submodulesInitialized, bootstrapState };
7598
- };
7599
-
7600
- const requestedSetupWaitMs = Number(args?.setupWaitMs ?? args?.bootstrapWaitMs ?? 8000);
7601
- const setupWaitMs = Number.isFinite(requestedSetupWaitMs)
7602
- ? Math.min(Math.max(requestedSetupWaitMs, 0), 14000)
7603
- : 8000;
7604
- const setupPromise = finishWorktreeSetup();
7605
- const setupResult = await Promise.race([
7606
- setupPromise.then((value) => ({ completed: true as const, value })),
7607
- new Promise<{ completed: false }>((resolve) => setTimeout(() => resolve({ completed: false }), setupWaitMs)),
7608
- ]);
7609
-
7610
- const emitBootstrapEvent = (eventStatus: 'bootstrap_complete' | 'bootstrap_failed', bootstrapState: WorktreeBootstrapState, startedAtMs: number, extraPayload?: Record<string, unknown>): void => {
7611
- try {
7612
- const durationMs = Date.now() - startedAtMs;
7613
- const event = `worktree_${eventStatus}` as const;
7614
- const metadataEvent = {
7615
- source: 'clone_mesh_node_bootstrap',
7616
- nodeId: node.id,
7617
- status: eventStatus,
7618
- worktreePath: result.worktreePath,
7619
- durationMs,
7620
- bootstrapStatus: bootstrapState.status,
7621
- ...(bootstrapState.error ? { error: bootstrapState.error } : {}),
7622
- ...(bootstrapState.exitCode !== undefined ? { exitCode: bootstrapState.exitCode } : {}),
7623
- ...(extraPayload || {}),
7624
- };
7625
- if (typeof this.deps.instanceManager?.getByCategory === 'function') {
7626
- const forwarded = handleMeshForwardEvent(
7627
- { instanceManager: this.deps.instanceManager } as any,
7628
- { event, meshId, nodeId: node.id, workspace: result.worktreePath, metadataEvent },
7629
- );
7630
- if (forwarded?.success === true) return;
7631
- }
7632
- queuePendingMeshCoordinatorEvent({
7633
- event,
7634
- meshId,
7635
- nodeLabel: node.id,
7636
- nodeId: node.id,
7637
- workspace: result.worktreePath,
7638
- metadataEvent,
7639
- queuedAt: Date.now(),
7640
- });
7641
- } catch { /* event emission is best-effort */ }
7642
- };
7643
-
7644
- const bootstrapStartedMs = Date.now();
7645
-
7646
- if (!setupResult.completed) {
7647
- setupPromise
7648
- .then(({ bootstrapState }) => {
7649
- emitBootstrapEvent('bootstrap_complete', bootstrapState, bootstrapStartedMs);
7650
- })
7651
- .catch((error: any) => {
7652
- const failedState: WorktreeBootstrapState = {
7653
- ...runningBootstrapState,
7654
- status: 'failed',
7655
- completedAt: new Date().toISOString(),
7656
- error: error?.message || String(error),
7657
- };
7658
- void persistWorktreeSetupState(failedState);
7659
- void appendCloneLedger(false, failedState);
7660
- emitBootstrapEvent('bootstrap_failed', failedState, bootstrapStartedMs, { error: error?.message || String(error) });
7661
- });
7662
- return {
7663
- success: true,
7664
- async: true,
7665
- status: 'accepted',
7666
- node,
7667
- worktreePath: result.worktreePath,
7668
- branch: result.branch,
7669
- worktreeBootstrap: runningBootstrapState,
7670
- worktreeSetup: {
7671
- status: 'running',
7672
- setupWaitMs,
7673
- message: 'Worktree node is registered; submodule/bootstrap setup is continuing in the background.',
7674
- },
7675
- };
7676
- }
7677
-
7678
- const { submodulesInitialized, bootstrapState } = setupResult.value;
7679
- emitBootstrapEvent('bootstrap_complete', bootstrapState, bootstrapStartedMs);
7680
- return {
7681
- success: true,
7682
- node,
7683
- worktreePath: result.worktreePath,
7684
- branch: result.branch,
7685
- submodulesInitialized,
7686
- worktreeBootstrap: bootstrapState,
7687
- };
7688
- } catch (e: any) {
7689
- return { success: false, error: e.message };
7690
- }
7691
- }
7692
- case 'retry_mesh_node_bootstrap': {
7693
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7694
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
7695
- if (!meshId) return { success: false, error: 'meshId required' };
7696
- if (!nodeId) return { success: false, error: 'nodeId required' };
7697
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'bootstrap retry');
7698
- if (ownerFailure) return ownerFailure;
7699
-
7700
- try {
7701
- // preferInline so bootstrap-retry can resolve inline-cache-only clone worktree nodes.
7702
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
7703
- const mesh = meshRecord?.mesh;
7704
- if (!mesh) return { success: false, error: 'Mesh not found' };
7705
-
7706
- const node = mesh.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
7707
- if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
7708
- if (!node.isLocalWorktree) return { success: false, error: 'Node is not a local worktree node' };
7709
-
7710
- // Bootstrap runs scripts in the worktree path — forward to the node's daemon if remote.
7711
- // _meshDirectDispatch prevents re-forwarding when stored daemonId uses legacy format.
7712
- const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
7713
- // daemonIdsEquivalent: an equivalent-form daemonId is this machine —
7714
- // bootstrap locally, do not forward. Equivalent → local.
7715
- if (nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
7716
- && !args?._meshDirectDispatch) {
7717
- const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId, 'retry_mesh_node_bootstrap', {
7718
- ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
7719
- _meshDirectDispatch: true,
7720
- });
7721
- return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
7722
- }
7723
-
7724
- const currentBootstrap = node.worktreeBootstrap as WorktreeBootstrapState | undefined;
7725
- if (currentBootstrap?.status === 'running') {
7726
- return { success: false, error: 'Bootstrap is already running for this node' };
7727
- }
7728
-
7729
- const worktreePath: string = node.workspace || node.repoRoot;
7730
- if (!worktreePath) return { success: false, error: 'Node has no workspace path' };
7731
-
7732
- const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, worktreePath);
7733
- const runningState: WorktreeBootstrapState = {
7734
- status: 'running',
7735
- required: loadedBootstrap.config?.required !== false,
7736
- configSource: loadedBootstrap.path || loadedBootstrap.source,
7737
- configSourceType: loadedBootstrap.sourceType,
7738
- startedAt: new Date().toISOString(),
7739
- };
7740
-
7741
- const persistState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
7742
- node.worktreeBootstrap = bootstrapState;
7743
- if (meshRecord.inline) {
7744
- this.updateInlineMeshNode(meshId, mesh, node);
7745
- return;
7746
- }
7747
- try {
7748
- const { updateNode } = await import('../config/mesh-config.js');
7749
- updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
7750
- this.invalidateAggregateMeshStatus(meshId);
7751
- } catch { /* best-effort */ }
7752
- };
7753
-
7754
- await persistState(runningState);
7755
- const bootstrapState = await runMeshWorktreeBootstrap(mesh, worktreePath);
7756
- await persistState(bootstrapState);
7757
-
7758
- return { success: true, bootstrapState };
7759
- } catch (e: any) {
7760
- return { success: false, error: e.message };
7761
- }
7762
- }
7763
-
7764
- case 'trigger_mesh_queue': {
7765
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7766
- if (!meshId) return { success: false, error: 'meshId required' };
7767
- const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
7768
- if (ownerFailure) return ownerFailure;
7769
- try {
7770
- const { triggerMeshQueue, tryAssignQueueTask } = await import('../mesh/mesh-events.js');
7771
-
7772
- // Bug A fix: when preferredNodeId is provided, attempt to claim a pending
7773
- // task for the preferred node's idle session first, before the general
7774
- // round-robin trigger picks a different node.
7775
- const preferredNodeId = typeof args?.preferredNodeId === 'string' ? args.preferredNodeId.trim() : '';
7776
- if (preferredNodeId) {
7777
- const cliInstances = this.deps.instanceManager.getByCategory('cli');
7778
- // Sort: preferred node's sessions first, others after
7779
- const sorted = [...cliInstances].sort((a, b) => {
7780
- const aSettings = a.getState().settings as Record<string, unknown> || {};
7781
- const bSettings = b.getState().settings as Record<string, unknown> || {};
7782
- const aNode = readStringValue(aSettings.meshNodeId, aSettings.nodeId);
7783
- const bNode = readStringValue(bSettings.meshNodeId, bSettings.nodeId);
7784
- return (aNode === preferredNodeId ? -1 : 0) - (bNode === preferredNodeId ? -1 : 0);
7785
- });
7786
- for (const inst of sorted) {
7787
- const state = inst.getState();
7788
- const settings = state.settings as Record<string, unknown> || {};
7789
- const nodeId = readStringValue(settings.meshNodeId, settings.nodeId);
7790
- if (!nodeId || nodeId !== preferredNodeId) continue;
7791
- const meshNodeFor = readStringValue(settings.meshNodeFor);
7792
- if (meshNodeFor !== meshId) continue;
7793
- const status = (readStringValue(state.status) || '').toLowerCase();
7794
- if (status !== 'idle') continue;
7795
- const sessionId = typeof state.instanceId === 'string' ? state.instanceId : '';
7796
- const providerType = readStringValue(state.type, settings.providerType) || '';
7797
- if (sessionId && providerType) {
7798
- tryAssignQueueTask(this.deps as any, meshId, nodeId, sessionId, providerType);
7799
- break;
7800
- }
7801
- }
7802
- }
7803
-
7804
- const trigger = await triggerMeshQueue(this.deps as any, meshId);
7805
- return { success: true, trigger };
7806
- } catch (e: any) {
7807
- return { success: false, error: e.message };
7808
- }
7809
- }
7810
-
7811
- // ─── Mesh Coordinator Launch ───
7812
- case 'launch_mesh_coordinator': {
7813
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
7814
- let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
7815
- // Optional per-launch system-prompt addition. Dashboard or API
7816
- // callers (e.g. when spawning a mesh-node-specific coordinator)
7817
- // can pass extra context that gets appended to the rendered
7818
- // default prompt under the "## Additional Context" section.
7819
- // Going through buildCoordinatorSystemPrompt's userInstruction
7820
- // means user-level override files (~/.adhdev/coordinator-prompts)
7821
- // and this per-launch addition compose cleanly: an override
7822
- // wins outright, but if there's no override, the default
7823
- // prompt + the optional append.md file + this extra context
7824
- // all stack in declared order.
7825
- const extraSystemPrompt = typeof args?.extraSystemPrompt === 'string'
7826
- ? args.extraSystemPrompt.trim()
7827
- : '';
7828
- if (!meshId) return { success: false, error: 'meshId required' };
7829
-
7830
- try {
7831
- const { buildCoordinatorSystemPrompt } = await import('../mesh/coordinator-prompt.js');
7832
- const { buildMissionPromptSection } = await import('../mesh/mesh-missions.js');
7833
- // M3-3: inject the active mission summary into the coordinator prompt.
7834
- // Best-effort — a store failure must not block coordinator launch.
7835
- const buildMissionSectionBestEffort = (id: string): string => {
7836
- try { return buildMissionPromptSection(id); } catch { return ''; }
7837
- };
7838
-
7839
- // Support inline mesh data from cloud (bypasses local meshes.json lookup)
7840
- let mesh: any;
7841
- if (args?.inlineMesh && typeof args.inlineMesh === 'object') {
7842
- mesh = args.inlineMesh;
7843
- // Cache cloud mesh so the MCP server can retrieve it via get_mesh
7844
- this.inlineMeshCache.set(meshId, mesh);
7845
- } else {
7846
- const { getMesh } = await import('../config/mesh-config.js');
7847
- mesh = getMesh(meshId);
7848
- }
7849
- if (!mesh) return { success: false, error: 'Mesh not found' };
7850
- const meshHost = resolveMeshHostStatus(mesh);
7851
- if (!meshHost.canOwnCoordinator) {
7852
- return {
7853
- success: false,
7854
- ...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
7855
- meshId,
7856
- cliType,
7857
- };
7858
- }
7859
- if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
7860
-
7861
- const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
7862
- ? args.coordinatorNodeId.trim()
7863
- : '';
7864
- const preferredCoordinatorNodeId = requestedCoordinatorNodeId
7865
- || (typeof mesh.coordinator?.preferredNodeId === 'string' ? mesh.coordinator.preferredNodeId.trim() : '');
7866
- const coordinatorNode = preferredCoordinatorNodeId
7867
- ? mesh.nodes.find((node: any) => node?.id === preferredCoordinatorNodeId || node?.nodeId === preferredCoordinatorNodeId)
7868
- : mesh.nodes[0];
7869
- if (!coordinatorNode) {
7870
- return {
7871
- success: false,
7872
- code: 'mesh_coordinator_node_not_found',
7873
- error: `Coordinator node ${preferredCoordinatorNodeId} was not found in mesh`,
7874
- meshId,
7875
- cliType,
7876
- };
7877
- }
7878
- const sessionHostRecords = this.deps.sessionHostControl?.listSessions
7879
- ? await this.deps.sessionHostControl.listSessions().catch(() => [])
7880
- : [];
7881
- const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
7882
- const workspace = readLiveMeshNodeWorkspace({
7883
- meshId,
7884
- nodeId: String(normalizeMeshNodeId(coordinatorNode) || preferredCoordinatorNodeId || ''),
7885
- liveSessionRecords: liveMeshSessions,
7886
- allowCoordinatorSession: true,
7887
- }) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
7888
- if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
7889
- if (!cliType) {
7890
- const resolved = await resolveProviderTypeFromPriority({
7891
- nodeId: String(normalizeMeshNodeId(coordinatorNode) || preferredCoordinatorNodeId || 'coordinator'),
7892
- providerPriority: readProviderPriorityFromPolicy(coordinatorNode.policy),
7893
- providerLoader: this.deps.providerLoader,
7894
- onStatusChange: this.deps.onStatusChange,
7895
- });
7896
- if (!resolved.providerType) {
7897
- return {
7898
- success: false,
7899
- code: 'mesh_coordinator_provider_priority_unusable',
7900
- error: resolved.error || 'No usable provider found from node providerPriority',
7901
- meshId,
7902
- cliType,
7903
- workspace,
7904
- };
7905
- }
7906
- cliType = resolved.providerType;
7907
- }
7908
- const providerMeta = this.deps.providerLoader.resolve?.(cliType) || this.deps.providerLoader.getMeta(cliType);
7909
- const coordinatorSetup = resolveMeshCoordinatorSetup({
7910
- provider: providerMeta,
7911
- cliType,
7912
- meshId,
7913
- workspace,
7914
- });
7915
-
7916
- if (coordinatorSetup.kind === 'unsupported') {
7917
- return {
7918
- success: false,
7919
- code: 'mesh_coordinator_unsupported',
7920
- error: coordinatorSetup.reason,
7921
- meshId,
7922
- cliType,
7923
- workspace,
7924
- };
7925
- }
7926
-
7927
- if (coordinatorSetup.kind === 'manual') {
7928
- return {
7929
- success: false,
7930
- code: 'mesh_coordinator_manual_mcp_setup_required',
7931
- error: coordinatorSetup.instructions,
7932
- meshId,
7933
- cliType,
7934
- workspace,
7935
- meshCoordinatorSetup: coordinatorSetup,
7936
- };
7937
- }
7938
-
7939
- // ─── CLI-command MCP registration (Codex, Gemini CLI) ───────────
7940
- if (coordinatorSetup.kind === 'cli_command') {
7941
- // Build coordinator prompt first — fail closed on errors.
7942
- let cliCmdSystemPrompt = '';
7943
- try {
7944
- cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id) });
7945
- } catch (error: any) {
7946
- const message = error?.message || String(error);
7947
- LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
7948
- return {
7949
- success: false,
7950
- code: 'mesh_coordinator_prompt_failed',
7951
- error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
7952
- meshId, cliType, workspace,
7953
- };
7954
- }
7955
-
7956
- // Run the provider's MCP registration command under a
7957
- // PTY. Some providers (agy, future bubbletea CLIs)
7958
- // refuse to run without /dev/tty, so pipe-only
7959
- // execFileSync silently no-ops the registration and
7960
- // the coordinator ends up without any mcp tools. With
7961
- // a real PTY the registration goes through and the
7962
- // exit code tells us whether it actually persisted.
7963
- let mcpRegistrationOk = false;
7964
- let mcpRegistrationFailure: {
7965
- command: string;
7966
- output: string;
7967
- exitCode: number | null;
7968
- signal: number | null;
7969
- timedOut: boolean;
7970
- } | null = null;
7971
- try {
7972
- const { buildMeshCoordinatorRegistrationPlan, execUnderPty } = await import('./mesh-coordinator.js');
7973
- const registrationPlan = buildMeshCoordinatorRegistrationPlan(
7974
- cliType,
7975
- coordinatorSetup.serverName,
7976
- coordinatorSetup.command,
7977
- );
7978
- for (const step of registrationPlan) {
7979
- const renderedCommand = [step.command, ...step.args].join(' ');
7980
- LOG.info('MeshCoordinator', `Running MCP ${step.label} (pty): ${renderedCommand}`);
7981
- const ptyResult = await execUnderPty(step.command, step.args, { cwd: workspace, timeoutMs: 20_000 });
7982
- if (ptyResult.exitCode === 0 && !ptyResult.timedOut) {
7983
- if (step.required) mcpRegistrationOk = true;
7984
- continue;
7985
- }
7986
- LOG.warn('MeshCoordinator', `MCP ${step.label} failed exit=${ptyResult.exitCode} signal=${ptyResult.signal} timedOut=${ptyResult.timedOut} — output:\n${ptyResult.output.slice(-2000)}`);
7987
- if (step.required) {
7988
- mcpRegistrationFailure = {
7989
- command: renderedCommand,
7990
- output: ptyResult.output.slice(-2000),
7991
- exitCode: ptyResult.exitCode,
7992
- signal: ptyResult.signal,
7993
- timedOut: ptyResult.timedOut,
7994
- };
7995
- break;
7996
- }
7997
- }
7998
- } catch (error: any) {
7999
- LOG.warn('MeshCoordinator', `MCP registration command failed: ${error?.message || error}`);
8000
- mcpRegistrationFailure = {
8001
- command: coordinatorSetup.command,
8002
- output: error?.message || String(error),
8003
- exitCode: null,
8004
- signal: null,
8005
- timedOut: false,
8006
- };
8007
- }
8008
-
8009
- if (!mcpRegistrationOk) {
8010
- return {
8011
- success: false,
8012
- code: 'mesh_coordinator_mcp_registration_failed',
8013
- error: `Could not register ${coordinatorSetup.serverName}; coordinator session was not launched`,
8014
- meshId,
8015
- cliType,
8016
- workspace,
8017
- registration: mcpRegistrationFailure,
8018
- };
8019
- }
8020
-
8021
- // Codex gives repo-local .mcp.json precedence over its
8022
- // global `codex mcp add` registration. Refresh an
8023
- // existing ADHDev entry so a stale workspace command
8024
- // cannot shadow the registration we just verified.
8025
- if (cliType === 'codex-cli') {
8026
- const repoMcpConfigPath = pathJoin(workspace, '.mcp.json');
8027
- if (fs.existsSync(repoMcpConfigPath)) {
8028
- try {
8029
- const repoMcpConfig = parseMeshCoordinatorMcpConfig(
8030
- fs.readFileSync(repoMcpConfigPath, 'utf-8'),
8031
- 'claude_mcp_json',
8032
- );
8033
- const existingServers = repoMcpConfig.mcpServers;
8034
- if (
8035
- existingServers
8036
- && typeof existingServers === 'object'
8037
- && !Array.isArray(existingServers)
8038
- && existingServers[coordinatorSetup.serverName]
8039
- ) {
8040
- fs.writeFileSync(repoMcpConfigPath, serializeMeshCoordinatorMcpConfig({
8041
- ...repoMcpConfig,
8042
- mcpServers: {
8043
- ...existingServers,
8044
- [coordinatorSetup.serverName]: coordinatorSetup.mcpServer,
8045
- },
8046
- }, 'claude_mcp_json'), 'utf-8');
8047
- LOG.info('MeshCoordinator', `Refreshed repo-local ${repoMcpConfigPath} entry for ${coordinatorSetup.serverName}`);
8048
- }
8049
- } catch (error: any) {
8050
- return {
8051
- success: false,
8052
- code: 'mesh_coordinator_config_write_failed',
8053
- error: `Could not refresh repo-local MCP config: ${error?.message || error}`,
8054
- meshId,
8055
- cliType,
8056
- workspace,
8057
- };
8058
- }
8059
- }
8060
- }
8061
-
8062
- // Inject system prompt declaratively from provider.v1.json.
8063
- const cliCmdArgs: string[] = [];
8064
- const cliCmdEnv: Record<string, string> = {};
8065
- let cliCmdContextFilePath: string | undefined;
8066
- if (cliCmdSystemPrompt) {
8067
- const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
8068
- const effect = applyMeshCoordinatorSystemPromptInjection(
8069
- cliCmdSystemPrompt,
8070
- providerMeta?.meshCoordinator?.systemPromptInjection,
8071
- { cliArgs: cliCmdArgs, launchEnv: cliCmdEnv, workspace, cliType },
8072
- );
8073
- cliCmdContextFilePath = effect.contextFilePath;
8074
- }
8075
-
8076
- const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
8077
- cliType,
8078
- dir: workspace,
8079
- cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
8080
- env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
8081
- settings: { meshCoordinatorFor: meshId },
8082
- });
8083
-
8084
- // R48 inject-then-remove. Spawn was just kicked off above; agy and
8085
- // gemini-cli read AGENTS.md / GEMINI.md exactly once at startup and
8086
- // cache it for the rest of the session, so we can safely strip
8087
- // the wrapper from disk shortly after launch. That keeps any
8088
- // worker session launched into the same workspace later from
8089
- // picking up our wrapper block.
8090
- if (cliCmdLaunch?.success && cliCmdContextFilePath) {
8091
- const stripPath = cliCmdContextFilePath;
8092
- setTimeout(() => {
8093
- void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
8094
- stripCoordinatorWrapperFile(stripPath);
8095
- LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (cli_command)`);
8096
- }).catch(() => { /* best-effort */ });
8097
- }, 5000);
8098
- }
8099
-
8100
- if (!cliCmdLaunch?.success) {
8101
- return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
8102
- }
8103
-
8104
- LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
8105
- const cliCmdSessionId = cliCmdLaunch.sessionId || cliCmdLaunch.id;
8106
- if (cliCmdSessionId) {
8107
- const cliCmdInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
8108
- registerMeshCoordinator({
8109
- meshId,
8110
- sessionId: cliCmdSessionId,
8111
- workspace,
8112
- startedAt: Date.now(),
8113
- cliType,
8114
- systemPrompt: cliCmdSystemPrompt || undefined,
8115
- extraSystemPrompt: extraSystemPrompt || undefined,
8116
- injection: cliCmdInjectionDecl ? {
8117
- mode: cliCmdInjectionDecl.mode,
8118
- target: 'flag' in cliCmdInjectionDecl ? cliCmdInjectionDecl.flag
8119
- : 'name' in cliCmdInjectionDecl ? cliCmdInjectionDecl.name
8120
- : 'path' in cliCmdInjectionDecl ? cliCmdInjectionDecl.path
8121
- : undefined,
8122
- } : undefined,
8123
- });
8124
- }
8125
- try {
8126
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
8127
- appendLedgerEntry(meshId, {
8128
- kind: 'coordinator_started',
8129
- sessionId: cliCmdSessionId,
8130
- providerType: cliType,
8131
- payload: { workspace },
8132
- });
8133
- } catch { /* best-effort */ }
8134
-
8135
- return {
8136
- success: true,
8137
- meshId,
8138
- cliType,
8139
- workspace,
8140
- sessionId: cliCmdSessionId,
8141
- mcpRegistered: mcpRegistrationOk,
8142
- };
8143
- }
8144
-
8145
- const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
8146
- if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
8147
- return {
8148
- success: false,
8149
- code: 'mesh_coordinator_unsupported',
8150
- error: `Unsupported auto-import MCP config format: ${String(coordinatorSetup.configFormat)}`,
8151
- meshId,
8152
- cliType,
8153
- workspace,
8154
- };
8155
- }
8156
-
8157
- // Build the coordinator prompt before mutating workspace config or launching.
8158
- // Prompt generation failures are configuration/data-shape errors; fail closed so
8159
- // broken mesh state is visible instead of silently launching with weaker rules.
8160
- let systemPrompt = '';
8161
- try {
8162
- systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id) });
8163
- } catch (error: any) {
8164
- const message = error?.message || String(error);
8165
- LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
8166
- return {
8167
- success: false,
8168
- code: 'mesh_coordinator_prompt_failed',
8169
- error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
8170
- meshId,
8171
- cliType,
8172
- workspace,
8173
- };
8174
- }
8175
-
8176
- // 1. Write provider-declared MCP config for CLIs that auto-import it.
8177
- const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
8178
- const { dirname } = await import('path');
8179
- const mcpConfigPath = coordinatorSetup.configPath;
8180
- const hermesManualFallback = cliType === 'hermes-cli' && configFormat === 'hermes_config_yaml'
8181
- ? createHermesManualMeshCoordinatorSetup(meshId, workspace)
8182
- : null;
8183
- let hermesBaseConfig: { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } | null = null;
8184
- if (hermesManualFallback) {
8185
- try {
8186
- hermesBaseConfig = loadHermesCoordinatorBaseConfig(mcpConfigPath);
8187
- } catch (error: any) {
8188
- const message = `Failed to parse Hermes base config for automatic coordinator setup: ${error?.message || error}`;
8189
- LOG.error('MeshCoordinator', message);
8190
- return { success: false, code: 'mesh_coordinator_config_parse_failed', error: message, meshId, cliType, workspace };
8191
- }
8192
- }
8193
- const returnManualFallback = (message: string) => ({
8194
- success: false,
8195
- code: 'mesh_coordinator_manual_mcp_setup_required',
8196
- error: message,
8197
- meshId,
8198
- cliType,
8199
- workspace,
8200
- meshCoordinatorSetup: hermesManualFallback,
8201
- });
8202
-
8203
- // Merge ADHDev mesh server into existing config.
8204
- // Pass full mesh data as env var so the MCP server can bootstrap
8205
- // without depending on meshes.json or a running daemon.
8206
- const mcpServerEntry: Record<string, any> = {
8207
- command: coordinatorSetup.mcpServer.command,
8208
- args: coordinatorSetup.mcpServer.args,
8209
- };
8210
- if (args?.inlineMesh) {
8211
- const modeArgIndex = coordinatorSetup.mcpServer.args.findIndex((value: string) => value === '--mode');
8212
- const mcpTransport = modeArgIndex >= 0 ? coordinatorSetup.mcpServer.args[modeArgIndex + 1] : 'ipc';
8213
- mcpServerEntry.env = {
8214
- ADHDEV_INLINE_MESH: JSON.stringify(mesh),
8215
- ADHDEV_MCP_TRANSPORT: mcpTransport === 'local' ? 'local' : 'ipc',
8216
- };
8217
- }
8218
-
8219
- try {
8220
- mkdirSync(dirname(mcpConfigPath), { recursive: true });
8221
- } catch (error: any) {
8222
- const message = `Could not prepare MCP config path for automatic setup: ${error?.message || error}`;
8223
- LOG.error('MeshCoordinator', message);
8224
- if (hermesManualFallback) return returnManualFallback(message);
8225
- return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
8226
- }
8227
-
8228
- // Backup existing MCP config if present.
8229
- const hadExistingMcpConfig = existsSync(mcpConfigPath);
8230
- let existingMcpConfig: Record<string, any> = hermesBaseConfig?.config || {};
8231
- if (hermesBaseConfig) {
8232
- copyHermesCoordinatorCredentialFiles(hermesBaseConfig.sourceHome, dirname(mcpConfigPath));
8233
- }
8234
- if (hadExistingMcpConfig) {
8235
- try {
8236
- const parsedExistingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
8237
- const existingCoordinatorConfig = hermesManualFallback
8238
- ? stripHermesCoordinatorTempModelProviderOverrides(parsedExistingMcpConfig)
8239
- : parsedExistingMcpConfig;
8240
- existingMcpConfig = { ...existingMcpConfig, ...existingCoordinatorConfig };
8241
- copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
8242
- } catch (error: any) {
8243
- LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
8244
- return {
8245
- success: false,
8246
- code: 'mesh_coordinator_config_parse_failed',
8247
- error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
8248
- };
8249
- }
8250
- }
8251
-
8252
- const mcpServersKey = getMcpServersKey(configFormat);
8253
- const existingServers = existingMcpConfig[mcpServersKey];
8254
- const mcpConfig = {
8255
- ...existingMcpConfig,
8256
- [mcpServersKey]: {
8257
- ...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
8258
- [coordinatorSetup.serverName]: mcpServerEntry,
8259
- },
8260
- };
8261
- try {
8262
- writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
8263
- } catch (error: any) {
8264
- const message = `Could not write MCP config for automatic setup: ${error?.message || error}`;
8265
- LOG.error('MeshCoordinator', message);
8266
- if (hermesManualFallback) return returnManualFallback(message);
8267
- return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
8268
- }
8269
- LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
8270
-
8271
- const cliArgs: string[] = [];
8272
- const launchEnv: Record<string, string> = {};
8273
- if (configFormat === 'hermes_config_yaml') {
8274
- launchEnv.HERMES_HOME = dirname(mcpConfigPath);
8275
- launchEnv.HERMES_IGNORE_USER_CONFIG = '';
8276
- }
8277
- let autoImportContextFilePath: string | undefined;
8278
- if (systemPrompt) {
8279
- const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
8280
- const effect = applyMeshCoordinatorSystemPromptInjection(
8281
- systemPrompt,
8282
- providerMeta?.meshCoordinator?.systemPromptInjection,
8283
- { cliArgs, launchEnv, workspace, cliType },
8284
- );
8285
- autoImportContextFilePath = effect.contextFilePath;
8286
- }
8287
- if (cliType === 'claude-cli') {
8288
- cliArgs.push('--mcp-config', coordinatorSetup.configPath);
8289
- }
8290
-
8291
- // 3. Launch CLI session via existing cliManager.
8292
- // Provider-specific prompt injection remains fail-closed: Claude gets
8293
- // explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
8294
- const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
8295
- cliType,
8296
- dir: workspace,
8297
- cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
8298
- env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
8299
- settings: {
8300
- meshCoordinatorFor: meshId
8301
- }
8302
- });
8303
-
8304
- // R48 inject-then-remove. See the cli_command branch for context;
8305
- // same idea: strip the wrapper from disk ~5s after launch so the
8306
- // user's AGENTS.md / GEMINI.md is untouched the moment any
8307
- // worker session opens up in the same workspace.
8308
- if (launchResult?.success && autoImportContextFilePath) {
8309
- const stripPath = autoImportContextFilePath;
8310
- setTimeout(() => {
8311
- void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
8312
- stripCoordinatorWrapperFile(stripPath);
8313
- LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (auto_import)`);
8314
- }).catch(() => { /* best-effort */ });
8315
- }, 5000);
8316
- }
8317
-
8318
- if (!launchResult?.success) {
8319
- return { success: false, error: launchResult?.error || 'Failed to launch CLI session' };
8320
- }
8321
-
8322
- LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
8323
- const launchSessionId = launchResult.sessionId || launchResult.id;
8324
- if (launchSessionId) {
8325
- const autoImportInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
8326
- registerMeshCoordinator({
8327
- meshId,
8328
- sessionId: launchSessionId,
8329
- workspace,
8330
- startedAt: Date.now(),
8331
- cliType,
8332
- systemPrompt: systemPrompt || undefined,
8333
- extraSystemPrompt: extraSystemPrompt || undefined,
8334
- mcpConfigPath,
8335
- injection: autoImportInjectionDecl ? {
8336
- mode: autoImportInjectionDecl.mode,
8337
- target: 'flag' in autoImportInjectionDecl ? autoImportInjectionDecl.flag
8338
- : 'name' in autoImportInjectionDecl ? autoImportInjectionDecl.name
8339
- : 'path' in autoImportInjectionDecl ? autoImportInjectionDecl.path
8340
- : undefined,
8341
- } : undefined,
8342
- });
8343
- }
8344
-
8345
- // Record coordinator launch in task ledger
8346
- try {
8347
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
8348
- appendLedgerEntry(meshId, {
8349
- kind: 'coordinator_started',
8350
- sessionId: launchSessionId,
8351
- providerType: cliType,
8352
- payload: { workspace },
8353
- });
8354
- } catch { /* ledger append is best-effort */ }
8355
-
8356
- return {
8357
- success: true,
8358
- meshId,
8359
- cliType,
8360
- workspace,
8361
- sessionId: launchSessionId,
8362
- mcpConfigWritten: true,
8363
- };
8364
- } catch (e: any) {
8365
- LOG.error('MeshCoordinator', `Failed: ${e.message}`);
8366
- return { success: false, error: e.message };
8367
- }
8368
- }
8369
-
8370
- case 'mesh_status': {
8371
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
8372
- if (!meshId) return { success: false, error: 'meshId required' };
8373
- try {
8374
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
8375
- const mesh = meshRecord?.mesh;
8376
- if (!mesh) return { success: false, error: 'Mesh not found' };
8377
- const meshHost = resolveMeshHostStatus(mesh);
8378
-
8379
- const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
8380
- // Compact (default) elides each mission's full goal text from the
8381
- // payload — coordinators polling node health don't need every
8382
- // mission's multi-hundred-char goal repeated. verbose=true (or the
8383
- // explicit compact=false) restores full goals. Verbose bypasses the
8384
- // shared (compact) aggregate cache so a verbose call never poisons
8385
- // the compact cache and vice versa.
8386
- const verboseMissions = args?.verbose === true || args?.compact === false;
8387
- // See (B3) below: scope the peek to this daemon when the
8388
- // caller doesn't tell us, otherwise scoped events look
8389
- // missing and we falsely return a stale cache.
8390
- const peekScope = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
8391
- ? args.coordinatorDaemonId.trim()
8392
- : (this.deps.statusInstanceId || undefined);
8393
- const pendingCoordinatorEventCount = getPendingMeshCoordinatorEvents(meshId, peekScope).length;
8394
- const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
8395
- if (!refreshRequested && !verboseMissions && pendingCoordinatorEventCount === 0) {
8396
- const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
8397
- if (cachedStatus) {
8398
- logRepoMeshStatusDebug('return_cached', {
8399
- meshId,
8400
- command: 'mesh_status',
8401
- refreshRequested,
8402
- summary: summarizeRepoMeshStatusDebug(cachedStatus),
8403
- });
8404
- return cachedStatus;
8405
- }
8406
- }
8407
- const refreshReason = refreshRequested
8408
- ? 'explicit_refresh'
8409
- : pendingCoordinatorEventCount > 0
8410
- ? 'pending_coordinator_events'
8411
- : hadAggregateCache
8412
- ? 'stale_pending_cache_refresh'
8413
- : 'cold_cache_miss';
8414
-
8415
- const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
8416
- const queue = getQueue(meshId);
8417
- const queueSummary = getMeshQueueStats(meshId);
8418
-
8419
- const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
8420
- const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
8421
- const asyncRefineLedgerEntries = readLedgerEntries(meshId, { tail: 100 });
8422
- const ledgerSummary = getLedgerSummary(meshId);
8423
- const sessionHostRecords = this.deps.sessionHostControl?.listSessions
8424
- ? await this.deps.sessionHostControl.listSessions().catch(() => [])
8425
- : [];
8426
- const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
8427
-
8428
- const localMachineId = loadConfig().machineId || '';
8429
- const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
8430
- // Shared probe gate for this mesh_status call: the bootstrap
8431
- // hydrate below and the per-node render loop further down both
8432
- // probe the same peers — route both through this cache so they
8433
- // dedup within the call and reuse recent results across calls.
8434
- const meshGitProbeCache = this.meshGitProbeCache;
8435
- const directTruth = requireDirectPeerTruth
8436
- ? await hydrateInlineMeshDirectTruth({
8437
- mesh,
8438
- meshSource: meshRecord.source,
8439
- dispatchMeshCommand: this.deps.dispatchMeshCommand,
8440
- getMeshPeerConnectionStatus: this.deps.getMeshPeerConnectionStatus,
8441
- statusInstanceId: this.deps.statusInstanceId,
8442
- localMachineId,
8443
- // Standing-state model: only an explicit refresh fans
8444
- // out a blocking peer git probe. Default loads return
8445
- // held truth so one slow peer can't block the graph.
8446
- probeRemotePeers: refreshRequested,
8447
- probeCache: meshGitProbeCache,
8448
- })
8449
- : {
8450
- directEvidenceCount: 0,
8451
- localConfirmedCount: 0,
8452
- peerAttemptedCount: 0,
8453
- peerConfirmedCount: 0,
8454
- standingEvidenceCount: 0,
8455
- unavailableNodeIds: [] as string[],
8456
- deadNodeIds: [] as string[],
8457
- };
8458
- // Default/cached loads may not attempt a remote peer probe yet; do not surface that as
8459
- // a direct mesh truth failure until an explicit probe attempt actually fails.
8460
- const passivePeerTruthNotAttempted = requireDirectPeerTruth
8461
- && !refreshRequested
8462
- && directTruth.directEvidenceCount > 0
8463
- && directTruth.peerAttemptedCount === 0;
8464
- const effectiveDirectTruth = passivePeerTruthNotAttempted
8465
- ? { ...directTruth, unavailableNodeIds: [] as string[] }
8466
- : directTruth;
8467
- const unavailableDirectTruthNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
8468
- const unavailableNodesAreOnlyRemovedWorktrees = unavailableDirectTruthNodeIds.size > 0
8469
- && Array.isArray(mesh.nodes)
8470
- && mesh.nodes
8471
- .filter((node: any) => unavailableDirectTruthNodeIds.has(normalizeMeshNodeId(node) ?? ''))
8472
- .every((node: any) => node?.isLocalWorktree === true);
8473
- // Default (non-refresh) loads never hard-fail: held
8474
- // standing-state truth is returned and the graph renders
8475
- // immediately. The hard mesh_direct_peer_truth_unavailable
8476
- // failure is reserved for an explicit refresh that actually
8477
- // attempted a peer probe and could not confirm any evidence.
8478
- const directTruthSatisfied = !requireDirectPeerTruth
8479
- || !refreshRequested
8480
- || (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
8481
- if (requireDirectPeerTruth && refreshRequested && !directTruthSatisfied) {
8482
- const failureResult = {
8483
- success: false,
8484
- code: 'mesh_direct_peer_truth_unavailable',
8485
- error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
8486
- sourceOfTruth: {
8487
- membership: meshRecord.source === 'inline_cache'
8488
- ? 'coordinator_inline_mesh_cache'
8489
- : meshRecord.source === 'local_config'
8490
- ? 'local_mesh_config'
8491
- : 'inline_bootstrap_snapshot',
8492
- coordinatorOwnsLiveTruth: false,
8493
- currentStatus: 'direct_peer_truth_unavailable',
8494
- directPeerTruth: {
8495
- required: true,
8496
- satisfied: false,
8497
- directEvidenceCount: directTruth.directEvidenceCount,
8498
- localConfirmedCount: directTruth.localConfirmedCount,
8499
- peerAttemptedCount: directTruth.peerAttemptedCount,
8500
- peerConfirmedCount: directTruth.peerConfirmedCount,
8501
- unavailableNodeIds: directTruth.unavailableNodeIds,
8502
- },
8503
- },
8504
- };
8505
- logRepoMeshStatusDebug('direct_truth_unavailable', {
8506
- meshId,
8507
- command: 'mesh_status',
8508
- refreshRequested,
8509
- meshSource: meshRecord.source,
8510
- directTruth,
8511
- });
8512
- return failureResult;
8513
- }
8514
- const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
8515
- const coordinatorHostname = osHostname();
8516
- const selectedCoordinatorNodeId = readStringValue(
8517
- mesh.coordinator?.preferredNodeId,
8518
- normalizeMeshNodeId(mesh.nodes?.[0] as any),
8519
- );
8520
- const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
8521
- ? selectedCoordinatorNodeId
8522
- : undefined;
8523
- const refreshedAt = new Date().toISOString();
8524
- const nodeStatuses = [];
8525
- for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
8526
- const nodeId = normalizeMeshNodeId(node) ?? '';
8527
- const daemonId = readStringValue(node.daemonId);
8528
- const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
8529
- const nodeHostname = readMeshNodeHostname(node as Record<string, unknown>);
8530
- const providerPriority = readProviderPriorityFromPolicy(node.policy);
8531
- const configuredCoordinatorNode = Boolean(
8532
- nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
8533
- );
8534
- const sparseConfiguredCoordinatorNode = configuredCoordinatorNode
8535
- && !daemonId
8536
- && !nodeMachineId
8537
- && !nodeHostname;
8538
- const isSelfNode = Boolean(
8539
- nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
8540
- ) || Boolean(
8541
- daemonId && (daemonIdsEquivalent(daemonId, localMachineId) || daemonIdsEquivalent(daemonId, this.deps.statusInstanceId)),
8542
- ) || Boolean(meshRecord?.inline && nodeIndex === 0)
8543
- || sparseConfiguredCoordinatorNode;
8544
- const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
8545
- localMachineId,
8546
- localDaemonId: this.deps.statusInstanceId,
8547
- coordinatorHostname,
8548
- isSelfNode,
8549
- });
8550
- const status: Record<string, unknown> = {
8551
- nodeId,
8552
- machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
8553
- labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
8554
- ? 'explicit_metadata'
8555
- : 'workspace_host_provider_context',
8556
- workspace: node.workspace,
8557
- repoRoot: node.repoRoot,
8558
- isLocalWorktree: node.isLocalWorktree,
8559
- worktreeBranch: node.worktreeBranch,
8560
- role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
8561
- daemonId,
8562
- machineId: nodeMachineId || node.machineId,
8563
- machine: machineIdentity,
8564
- machineStatus: node.machineStatus,
8565
- health: 'unknown',
8566
- providers: node.providers || [],
8567
- providerPriority,
8568
- activeSessions: [],
8569
- activeSessionDetails: [],
8570
- launchReady: false,
8571
- };
8572
- if (isSelfNode) {
8573
- status.connection = {
8574
- perspective: 'selected_coordinator',
8575
- source: 'mesh_peer_status',
8576
- state: 'self',
8577
- transport: 'local',
8578
- reported: true,
8579
- reason: 'Selected coordinator daemon',
8580
- lastStateChangeAt: refreshedAt,
8581
- };
8582
- } else if (daemonId) {
8583
- const connection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
8584
- status.connection = connection ?? {
8585
- perspective: 'selected_coordinator',
8586
- source: 'not_reported',
8587
- state: 'unknown',
8588
- transport: 'unknown',
8589
- reported: false,
8590
- reason: 'No live mesh peer telemetry reported by the selected coordinator yet.',
8591
- };
8592
- } else {
8593
- status.connection = {
8594
- perspective: 'selected_coordinator',
8595
- source: 'not_reported',
8596
- state: 'unknown',
8597
- transport: 'unknown',
8598
- reported: false,
8599
- reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
8600
- };
8601
- }
8602
- const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
8603
- meshId,
8604
- node,
8605
- nodeId,
8606
- liveSessionRecords: liveMeshSessions,
8607
- allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
8608
- });
8609
- const workspace = readLiveMeshNodeWorkspace({
8610
- meshId,
8611
- nodeId,
8612
- liveSessionRecords: matchedLiveSessionRecords,
8613
- allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
8614
- }) || (typeof node.workspace === 'string' ? node.workspace : '');
8615
- status.workspace = workspace || node.workspace;
8616
- if (matchedLiveSessionRecords.length > 0) {
8617
- const sessionIds = matchedLiveSessionRecords
8618
- .map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
8619
- .filter(Boolean);
8620
- const providerTypes = matchedLiveSessionRecords
8621
- .map((record: any) => readStringValue(record?.providerType))
8622
- .filter(Boolean) as string[];
8623
- status.activeSessions = sessionIds;
8624
- status.activeSessionDetails = matchedLiveSessionRecords.map(summarizeMeshSessionRecord);
8625
- if (providerTypes.length > 0) {
8626
- status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
8627
- }
8628
- }
8629
- if (workspace) {
8630
- if (!fs.existsSync(workspace)) {
8631
- // Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
8632
- const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
8633
- let remoteProbeApplied = false;
8634
- if (inlineTransitGit) {
8635
- status.git = inlineTransitGit;
8636
- status.health = inlineTransitGit.isGitRepo
8637
- ? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
8638
- : 'degraded';
8639
- const connection = readObjectRecord(status.connection);
8640
- const connectionState = readStringValue(connection.state);
8641
- const connectionReported = readBooleanValue(connection.reported) ?? false;
8642
- if (!connectionReported || connectionState === 'unknown') {
8643
- status.connection = buildLivePeerGitConnection(connection, refreshedAt);
8644
- }
8645
- remoteProbeApplied = true;
8646
- } else if (refreshRequested && !isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
8647
- // Only an explicit refresh fans out a blocking
8648
- // per-node git probe. On the default load a peer
8649
- // with no held truth falls through to
8650
- // gitProbePending below — the graph still renders.
8651
- // Bounded retry (shared with the bootstrap hydrate
8652
- // path), gated on the peer staying connected, so a
8653
- // slow TURN-relayed peer is recovered rather than
8654
- // dropped after a single timeout.
8655
- const runNodeProbe = () => probeRemoteMeshGitStatusWithRetry({
8656
- dispatchMeshCommand: this.deps.dispatchMeshCommand,
8657
- daemonId,
8658
- workspace,
8659
- timeoutMs: MESH_DIRECT_PROBE_TIMEOUT_MS,
8660
- retryTimeoutMs: MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS,
8661
- getConnection: this.deps.getMeshPeerConnectionStatus,
8662
- onConnection: connection => { status.connection = connection; },
8663
- });
8664
- // Same shared cache as the bootstrap hydrate path: within one
8665
- // mesh_status call this dedups the bootstrap probe against this
8666
- // per-node probe for the same peer, and across calls it reuses a
8667
- // recent result so the dashboard auto-retry loop can't restart a
8668
- // fresh refreshUpstream probe seconds apart.
8669
- const remoteGit = await meshGitProbeCache.probe(daemonId, workspace, runNodeProbe);
8670
- if (remoteGit) {
8671
- status.git = remoteGit;
8672
- status.health = remoteGit.isGitRepo
8673
- ? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
8674
- : 'degraded';
8675
- const connection = readObjectRecord(status.connection);
8676
- const connectionState = readStringValue(connection.state);
8677
- const connectionReported = readBooleanValue(connection.reported) ?? false;
8678
- if (!connectionReported || connectionState === 'unknown') {
8679
- status.connection = buildLivePeerGitConnection(connection, refreshedAt);
8680
- }
8681
- const reporter = recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
8682
- persistNodeReporterPlatform(meshRecord.source, mesh, nodeId, reporter);
8683
- remoteProbeApplied = true;
8684
- }
8685
- }
8686
- if (!remoteProbeApplied) {
8687
- const connectionState = readStringValue((status.connection as any)?.state);
8688
- const pendingPeerGitProbe = !inlineTransitGit
8689
- && !isSelfNode
8690
- && !!daemonId
8691
- && (
8692
- readStringValue(status.machineStatus) === 'online'
8693
- || readStringValue(status.health) === 'online'
8694
- || connectionState === 'connecting'
8695
- || connectionState === 'connected'
8696
- || connectionState === 'unknown'
8697
- );
8698
- if (pendingPeerGitProbe) {
8699
- status.gitProbePending = true;
8700
- status.health = 'unknown';
8701
- }
8702
- if (applyCachedInlineMeshNodeStatus(
8703
- status,
8704
- node,
8705
- pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
8706
- )) {
8707
- applyInlineMeshBranchConvergence(mesh, node, status);
8708
- finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
8709
- nodeStatuses.push(status);
8710
- continue;
8711
- }
8712
- if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
8713
- applyInlineMeshBranchConvergence(mesh, node, status);
8714
- finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
8715
- nodeStatuses.push(status);
8716
- continue;
8717
- }
8718
- }
8719
- } else {
8720
- try {
8721
- const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
8722
- status.git = gitStatus;
8723
- const reporter = recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
8724
- persistNodeReporterPlatform(meshRecord.source, mesh, nodeId, reporter);
8725
- if (gitStatus.isGitRepo) {
8726
- status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
8727
- } else {
8728
- status.health = 'degraded';
8729
- if (gitStatus.error && !status.error) status.error = gitStatus.error;
8730
- }
8731
- } catch {
8732
- if (!applyCachedInlineMeshNodeStatus(status, node)) {
8733
- status.health = 'degraded';
8734
- }
8735
- }
8736
- }
8737
- } else {
8738
- applyCachedInlineMeshNodeStatus(status, node);
8739
- }
8740
- applyInlineMeshBranchConvergence(mesh, node, status);
8741
- finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
8742
- nodeStatuses.push(status);
8743
- }
8744
-
8745
- // (B3) Resolve the coordinator daemon scope for the peek.
8746
- // mesh_status is a read-only status query — it must not consume
8747
- // (drain) pending events as a side effect. Coordinators that see
8748
- // pendingCoordinatorEvents in the response are expected to call
8749
- // get_pending_mesh_events to explicitly drain them after processing.
8750
- const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
8751
- ? args.coordinatorDaemonId.trim()
8752
- : (this.deps.statusInstanceId || undefined);
8753
- const pendingCoordinatorEvents = getPendingMeshCoordinatorEvents(meshId, callerCoordinatorDaemonId);
8754
- // R4: surface recent fail-loud routing drops so a coordinator/operator can see
8755
- // that a worker completion was lost (envelope present, mesh unresolved) instead
8756
- // of it vanishing silently. Diagnostic-only — never cached (see omit below).
8757
- const unroutableDeliveries = getRecentUnroutableDeliveries();
8758
- const previewFreshness = (() => {
8759
- const localRepoRoot = nodeStatuses
8760
- .map((node: any) => readStringValue(node?.git?.repoRoot, node?.repoRoot, node?.workspace))
8761
- .find((candidate: string | undefined) => !!candidate && fs.existsSync(candidate));
8762
- return localRepoRoot ? buildPreviewFreshness(localRepoRoot) : undefined;
8763
- })();
8764
- const asyncRefineJobs = buildMeshAsyncRefineJobs({
8765
- meshId,
8766
- ledgerEntries: asyncRefineLedgerEntries,
8767
- pendingEvents: [...pendingCoordinatorEvents],
8768
- });
8769
- const historicalSessions = buildHistoricalMeshSessions({
8770
- meshId,
8771
- nodes: mesh.nodes || [],
8772
- liveSessionRecords: liveMeshSessions,
8773
- });
8774
- const { getMeshStatusMissionSummaries } = await import('../mesh/mesh-missions.js');
8775
- // withStats opts in to per-mission operational rollups (durations /
8776
- // retries) for the dashboard mission detail. The rollup scans a
8777
- // bounded ledger tail per mission, but only over the bounded set
8778
- // returned here (live + capped history), so the cost stays linear
8779
- // in visible missions rather than the whole mesh history.
8780
- const missions = getMeshStatusMissionSummaries(meshId, { verbose: verboseMissions, withStats: true });
8781
- const statusResult = {
8782
- success: true,
8783
- meshId: mesh.id,
8784
- meshName: mesh.name,
8785
- repoIdentity: mesh.repoIdentity,
8786
- defaultBranch: mesh.defaultBranch,
8787
- refreshedAt,
8788
- meshHost,
8789
- sourceOfTruth: {
8790
- membership: meshRecord?.source === 'inline_cache'
8791
- ? 'coordinator_inline_mesh_cache'
8792
- : meshRecord?.source === 'local_config'
8793
- ? 'local_mesh_config'
8794
- : 'inline_bootstrap_snapshot',
8795
- coordinatorOwnsLiveTruth: directTruthSatisfied,
8796
- meshHost: {
8797
- owner: 'mesh_host_daemon',
8798
- localRole: meshHost.role,
8799
- hostDaemonId: meshHost.hostDaemonId,
8800
- hostNodeId: meshHost.hostNodeId,
8801
- hostAddress: meshHost.hostAddress,
8802
- },
8803
- ...(requireDirectPeerTruth ? {
8804
- currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
8805
- directPeerTruth: {
8806
- required: true,
8807
- satisfied: directTruthSatisfied,
8808
- directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
8809
- localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
8810
- peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
8811
- peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
8812
- unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
8813
- partialNodeFailures: effectiveDirectTruth.unavailableNodeIds,
8814
- },
8815
- } : {}),
8816
- historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary', 'historicalSessions'],
8817
- },
8818
- branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
8819
- ...(previewFreshness ? { previewFreshness, deployFreshness: previewFreshness } : {}),
8820
- nodes: nodeStatuses,
8821
- queue: { tasks: queue, summary: queueSummary },
8822
- ledger: { entries: ledgerEntries, summary: ledgerSummary },
8823
- ...(missions.length > 0 ? { missions } : {}),
8824
- ...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
8825
- ...(historicalSessions ? { historicalSessions } : {}),
8826
- ...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
8827
- ...(unroutableDeliveries.length > 0 ? { unroutableDeliveries } : {}),
8828
- activeRefineJobs: Array.from(this.runningRefineJobs.values())
8829
- .filter(job => job.meshId === meshId)
8830
- .map(job => ({
8831
- jobId: job.jobId,
8832
- nodeId: job.targetNodeId,
8833
- workspace: job.workspace,
8834
- startedAt: job.startedAt,
8835
- status: job.status,
8836
- targetCoordinatorDaemonId: job.targetCoordinatorDaemonId,
8837
- })),
8838
- };
8839
- const { pendingCoordinatorEvents: _pendingCoordinatorEvents, unroutableDeliveries: _unroutableDeliveries, ...cacheableStatusResult } = statusResult as any;
8840
- // Verbose carries full mission goals; never store it in the shared
8841
- // (compact) aggregate cache or a later compact poll would return the
8842
- // heavy goals from cache. Return it without caching.
8843
- const rememberedStatus = verboseMissions
8844
- ? cacheableStatusResult
8845
- : this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
8846
- const returnedStatus = {
8847
- ...rememberedStatus,
8848
- ...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
8849
- ...(unroutableDeliveries.length > 0 ? { unroutableDeliveries } : {}),
8850
- };
8851
- logRepoMeshStatusDebug('return_live', {
8852
- meshId,
8853
- command: 'mesh_status',
8854
- refreshRequested,
8855
- refreshReason,
8856
- meshSource: meshRecord.source,
8857
- directTruth,
8858
- summary: summarizeRepoMeshStatusDebug(returnedStatus),
8859
- });
8860
- return returnedStatus;
8861
- } catch (e: any) {
8862
- return { success: false, error: e.message };
8863
- }
8864
- }
8865
-
8866
- case 'get_mesh_review_inbox': {
8867
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
8868
- if (!meshId) return { success: false, error: 'meshId required' };
8869
- try {
8870
- const { deriveMeshReviewInboxItems } = await import('../mesh/mesh-review-inbox.js');
8871
- const { readLedgerEntries } = await import('../mesh/mesh-ledger.js');
8872
- const { getGitDiffSummary } = await import('../git/git-diff.js');
8873
- const { existsSync } = await import('node:fs');
8874
-
8875
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
8876
- const mesh = meshRecord?.mesh;
8877
- if (!mesh) return { success: false, error: 'Mesh not found' };
8878
-
8879
- // Ensure we have a fresh aggregate status so nodeStatuses carry
8880
- // computed fields (connection.state, branchConvergence, isLocalWorktree)
8881
- // that the raw mesh.nodes config objects don't have.
8882
- // When the caller provides an inlineMesh, prefer its nodes directly
8883
- // (they already carry the computed fields from the coordinator).
8884
- const inlineNodes = args?.inlineMesh && Array.isArray((args.inlineMesh as any)?.nodes)
8885
- ? (args.inlineMesh as any).nodes as Record<string, unknown>[]
8886
- : null;
8887
- let cachedStatus = !inlineNodes ? this.getCachedAggregateMeshStatus(meshId, mesh, {}) : null;
8888
- if (!cachedStatus && !inlineNodes) {
8889
- const freshStatus = await this.execute('mesh_status', {
8890
- meshId,
8891
- inlineMesh: args?.inlineMesh,
8892
- refresh: true,
8893
- }, 'get_mesh_review_inbox');
8894
- cachedStatus = (freshStatus?.success !== false) ? freshStatus : null;
8895
- }
8896
- const nodeStatuses: Record<string, unknown>[] = inlineNodes
8897
- ? inlineNodes
8898
- : Array.isArray(cachedStatus?.nodes)
8899
- ? cachedStatus.nodes as Record<string, unknown>[]
8900
- : Array.isArray(mesh.nodes)
8901
- ? mesh.nodes as Record<string, unknown>[]
8902
- : [];
8903
-
8904
- const ledgerEntries = readLedgerEntries(meshId, { tail: 300 });
8905
- const derivation = deriveMeshReviewInboxItems({ nodes: nodeStatuses, ledgerEntries });
8906
-
8907
- for (const item of derivation.items) {
8908
- const workspace = item.workspace;
8909
- if (!workspace || !existsSync(workspace)) continue;
8910
- const baseRef = item.defaultBranch
8911
- ? `origin/${item.defaultBranch}`
8912
- : 'origin/main';
8913
- try {
8914
- const diffResult = await getGitDiffSummary(workspace, { baseRef, maxFiles: 100 });
8915
- if (diffResult.isGitRepo) {
8916
- item.diffSummary = {
8917
- baseRef,
8918
- files: diffResult.files.map(f => ({
8919
- path: f.path,
8920
- status: f.status,
8921
- insertions: f.insertions,
8922
- deletions: f.deletions,
8923
- binary: f.binary,
8924
- oldPath: f.oldPath,
8925
- })),
8926
- totalFiles: diffResult.files.length,
8927
- totalInsertions: diffResult.totalInsertions,
8928
- totalDeletions: diffResult.totalDeletions,
8929
- truncated: diffResult.truncated,
8930
- ...(diffResult.error ? { error: diffResult.error } : {}),
8931
- };
8932
- }
8933
- } catch {
8934
- item.diffSummary = null;
8935
- }
8936
- }
8937
-
8938
- return {
8939
- success: true,
8940
- meshId,
8941
- inbox: derivation.items,
8942
- remoteNodesExcluded: derivation.remoteNodesExcluded,
8943
- excludedRemoteNodeIds: derivation.excludedRemoteNodeIds,
8944
- };
8945
- } catch (e: any) {
8946
- return { success: false, error: e.message };
8947
- }
8948
- }
6407
+ // RF-ROUTER MED family: medium-coupling commands (CLI/ACP agent, IDE
6408
+ // lifecycle, mesh CRUD, mesh queue, mesh host pairing, fast-forward /
6409
+ // refine convergence) are handled by the registry after the LOW family and
6410
+ // before the switch. Unlike LOW handlers, MED handlers need router-private
6411
+ // collaborators, so the context carries bound methods + the inline-mesh /
6412
+ // git-probe caches + the launchIde helper (which breaks the original
6413
+ // launch_ide ↔ restart_* self-recursion). A hit returns the same
6414
+ // CommandRouterResult the inlined case used to; a miss falls through.
6415
+ const medFamilyHandler = medFamilyRegistry.get(cmd);
6416
+ if (medFamilyHandler) {
6417
+ return await medFamilyHandler(this.buildMedFamilyContext(), args);
6418
+ }
8949
6419
 
8950
- default:
8951
- break;
6420
+ // RF-ROUTER HIGH family: high-coupling commands (mesh coordinator-event
6421
+ // relay + interactive prompt, mesh coordinator launch, mesh aggregate
6422
+ // status + review inbox) are handled by the registry after the LOW and
6423
+ // MED families and before the (now empty) switch. HIGH handlers reach the
6424
+ // most router-owned state — the aggregate-status memory cache and the
6425
+ // running-refine-job table — so the context carries those plus bound
6426
+ // read/write helpers and the router's own `execute` (the
6427
+ // get_mesh_review_inbox mesh_status re-entry). A hit returns the same
6428
+ // CommandRouterResult the inlined case used to; a miss falls through to
6429
+ // CommandHandler delegation.
6430
+ const highFamilyHandler = highFamilyRegistry.get(cmd);
6431
+ if (highFamilyHandler) {
6432
+ return await highFamilyHandler(this.buildHighFamilyContext(), args);
8952
6433
  }
8953
6434
 
8954
6435
  return null; // Not handled at this level → delegate to CommandHandler