@adhdev/daemon-core 0.9.76-rc.4 → 0.9.76-rc.40

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 (46) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -1
  2. package/dist/cli-adapters/provider-cli-runtime.d.ts +1 -0
  3. package/dist/commands/cli-manager.d.ts +17 -4
  4. package/dist/commands/mesh-coordinator.d.ts +2 -0
  5. package/dist/commands/router.d.ts +11 -0
  6. package/dist/config/mesh-config.d.ts +3 -0
  7. package/dist/git/git-types.d.ts +1 -1
  8. package/dist/git/git-worktree.d.ts +64 -0
  9. package/dist/git/index.d.ts +2 -0
  10. package/dist/index.js +1318 -426
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +1345 -457
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/mesh/coordinator-prompt.d.ts +1 -0
  15. package/dist/mesh/mesh-events.d.ts +9 -0
  16. package/dist/providers/chat-message-normalization.d.ts +11 -0
  17. package/dist/providers/cli-provider-instance.d.ts +3 -0
  18. package/dist/providers/provider-instance-manager.d.ts +1 -0
  19. package/dist/providers/provider-instance.d.ts +2 -0
  20. package/dist/repo-mesh-types.d.ts +13 -0
  21. package/dist/shared-types.d.ts +22 -1
  22. package/package.json +3 -4
  23. package/src/cli-adapters/provider-cli-adapter.ts +13 -6
  24. package/src/cli-adapters/provider-cli-runtime.ts +3 -2
  25. package/src/commands/chat-commands.ts +76 -5
  26. package/src/commands/cli-manager.ts +78 -5
  27. package/src/commands/handler.ts +13 -4
  28. package/src/commands/mesh-coordinator.ts +149 -6
  29. package/src/commands/router.d.ts +1 -0
  30. package/src/commands/router.ts +505 -34
  31. package/src/config/mesh-config.ts +23 -2
  32. package/src/git/git-commands.ts +5 -1
  33. package/src/git/git-types.ts +1 -0
  34. package/src/git/git-worktree.ts +214 -0
  35. package/src/git/index.ts +14 -0
  36. package/src/mesh/coordinator-prompt.ts +25 -10
  37. package/src/mesh/mesh-events.ts +109 -43
  38. package/src/providers/chat-message-normalization.ts +54 -0
  39. package/src/providers/cli-provider-instance.d.ts +2 -0
  40. package/src/providers/cli-provider-instance.ts +55 -7
  41. package/src/providers/provider-instance-manager.ts +20 -1
  42. package/src/providers/provider-instance.ts +2 -0
  43. package/src/repo-mesh-types.ts +15 -0
  44. package/src/shared-types.ts +24 -1
  45. package/src/status/builders.ts +17 -12
  46. package/src/status/reporter.ts +6 -0
@@ -30,14 +30,17 @@ import { SessionRegistry } from '../sessions/registry.js';
30
30
  import { LOG } from '../logging/logger.js';
31
31
  import { logCommand } from '../logging/command-log.js';
32
32
  import type { CommandLogEntry } from '../logging/command-log.js';
33
+ import * as yaml from 'js-yaml';
33
34
  import { getRecentLogs, LOG_PATH } from '../logging/logger.js';
34
35
  import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
35
36
  import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
36
- import { resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
37
+ import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
37
38
  import { buildSessionEntries } from '../status/builders.js';
39
+ import { handleMeshForwardEvent } from '../mesh/mesh-events.js';
38
40
  import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
39
41
  import { getSessionCompletionMarker } from '../status/snapshot.js';
40
42
  import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
43
+ import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
41
44
 
42
45
  type ReleaseChannel = 'stable' | 'preview';
43
46
  const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
@@ -61,14 +64,85 @@ function resolveUpgradeChannel(args: any): ReleaseChannel {
61
64
  || normalizeReleaseChannel(loadConfig().updateChannel)
62
65
  || 'stable';
63
66
  }
67
+
68
+ function readProviderPriorityFromPolicy(policy: unknown): string[] {
69
+ const record = policy && typeof policy === 'object' && !Array.isArray(policy)
70
+ ? policy as Record<string, unknown>
71
+ : {};
72
+ const raw = record.providerPriority;
73
+ if (!Array.isArray(raw)) return [];
74
+ const seen = new Set<string>();
75
+ return raw
76
+ .map(type => typeof type === 'string' ? type.trim() : '')
77
+ .filter(Boolean)
78
+ .filter(type => {
79
+ if (seen.has(type)) return false;
80
+ seen.add(type);
81
+ return true;
82
+ });
83
+ }
84
+
85
+ async function resolveProviderTypeFromPriority(args: {
86
+ nodeId: string;
87
+ providerPriority: string[];
88
+ providerLoader: ProviderLoader;
89
+ onStatusChange?: () => void;
90
+ }): Promise<{ providerType?: string; error?: string }> {
91
+ if (!args.providerPriority.length) {
92
+ return { error: `Node '${args.nodeId}' has no providerPriority policy; pass cliType explicitly or configure node.policy.providerPriority` };
93
+ }
94
+
95
+ const failed: string[] = [];
96
+ for (const requestedType of args.providerPriority) {
97
+ const normalizedType = args.providerLoader.resolveAlias(requestedType);
98
+ if (!args.providerLoader.isMachineProviderEnabled(normalizedType)) {
99
+ failed.push(`${requestedType}: disabled`);
100
+ continue;
101
+ }
102
+ const detected = await detectCLI(normalizedType, args.providerLoader, { includeVersion: false });
103
+ args.providerLoader.setCliDetectionResults([{
104
+ id: normalizedType,
105
+ installed: !!detected,
106
+ path: detected?.path,
107
+ }], false);
108
+ args.onStatusChange?.();
109
+ if (detected) return { providerType: normalizedType };
110
+ failed.push(`${requestedType}: not detected`);
111
+ }
112
+
113
+ return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join('; ')}` };
114
+ }
64
115
  import * as fs from 'fs';
65
116
 
117
+ type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
118
+
119
+ function loadYamlModule(): { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string } {
120
+ return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
121
+ }
122
+
123
+ function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
124
+ return format === 'hermes_config_yaml' ? 'mcp_servers' : 'mcpServers';
125
+ }
126
+
127
+ function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
128
+ if (!text.trim()) return {};
129
+ if (format === 'claude_mcp_json') return JSON.parse(text);
130
+ const parsed = loadYamlModule().load(text);
131
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
132
+ }
133
+
134
+ function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
135
+ if (format === 'claude_mcp_json') return JSON.stringify(config, null, 2);
136
+ return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
137
+ }
138
+
66
139
  // ─── Types ───
67
140
 
68
141
  export interface SessionHostControlPlane {
69
142
  getDiagnostics(payload?: { includeSessions?: boolean; limit?: number }): Promise<any>;
70
143
  listSessions(): Promise<any[]>;
71
144
  stopSession(sessionId: string): Promise<any>;
145
+ deleteSession(sessionId: string, opts?: { force?: boolean }): Promise<any>;
72
146
  resumeSession(sessionId: string): Promise<any>;
73
147
  restartSession(sessionId: string): Promise<any>;
74
148
  sendSignal(sessionId: string, signal: string): Promise<any>;
@@ -226,6 +300,135 @@ export class DaemonCommandRouter {
226
300
  this.deps = deps;
227
301
  }
228
302
 
303
+ private getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
304
+ if (inlineMesh && typeof inlineMesh === 'object') {
305
+ this.inlineMeshCache.set(meshId, inlineMesh as any);
306
+ return inlineMesh as any;
307
+ }
308
+ return this.inlineMeshCache.get(meshId);
309
+ }
310
+
311
+ private async getMeshForCommand(meshId: string, inlineMesh?: unknown): Promise<{ mesh: any; inline: boolean } | null> {
312
+ try {
313
+ const { getMesh } = await import('../config/mesh-config.js');
314
+ const mesh = getMesh(meshId);
315
+ if (mesh) return { mesh, inline: false };
316
+ } catch { /* fall through to inline cache */ }
317
+ const cached = this.getCachedInlineMesh(meshId, inlineMesh);
318
+ return cached ? { mesh: cached, inline: true } : null;
319
+ }
320
+
321
+ private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
322
+ if (!mesh || !Array.isArray(mesh.nodes) || !node?.id) return;
323
+ const idx = mesh.nodes.findIndex((entry: any) => entry?.id === node.id || entry?.nodeId === node.id);
324
+ if (idx >= 0) mesh.nodes[idx] = node;
325
+ else mesh.nodes.push(node);
326
+ mesh.updatedAt = new Date().toISOString();
327
+ this.inlineMeshCache.set(meshId, mesh);
328
+ }
329
+
330
+ private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
331
+ if (!mesh || !Array.isArray(mesh.nodes)) return false;
332
+ const idx = mesh.nodes.findIndex((entry: any) => entry?.id === nodeId || entry?.nodeId === nodeId);
333
+ if (idx === -1) return false;
334
+ mesh.nodes.splice(idx, 1);
335
+ mesh.updatedAt = new Date().toISOString();
336
+ this.inlineMeshCache.set(meshId, mesh);
337
+ return true;
338
+ }
339
+
340
+ private normalizeMeshSessionCleanupMode(value: unknown): RepoMeshSessionCleanupMode {
341
+ return value === 'stop'
342
+ || value === 'delete_stopped'
343
+ || value === 'stop_and_delete'
344
+ || value === 'preserve'
345
+ ? value
346
+ : 'preserve';
347
+ }
348
+
349
+ private sessionMatchesMeshNode(record: any, node: any, nodeId: string, sessionIds?: Set<string>): boolean {
350
+ const sessionId = typeof record?.sessionId === 'string' ? record.sessionId : '';
351
+ if (!sessionId) return false;
352
+ if (sessionIds?.size) return sessionIds.has(sessionId);
353
+ const workspace = typeof node?.workspace === 'string' ? node.workspace : '';
354
+ if (workspace && record?.workspace === workspace) return true;
355
+ if (record?.meta?.meshNodeId === nodeId) return true;
356
+ return false;
357
+ }
358
+
359
+ private isCompletedHostedSession(record: any): boolean {
360
+ return record?.lifecycle === 'stopped' || record?.lifecycle === 'failed' || record?.lifecycle === 'interrupted';
361
+ }
362
+
363
+ private async cleanupMeshSessions(args: {
364
+ meshId: string;
365
+ nodeId: string;
366
+ node: any;
367
+ mode: RepoMeshSessionCleanupMode;
368
+ sessionIds?: string[];
369
+ dryRun?: boolean;
370
+ }): Promise<{ success: boolean; [key: string]: unknown }> {
371
+ if (args.mode === 'preserve') {
372
+ return { success: true, mode: 'preserve', matchedCount: 0, stoppedSessionIds: [], deletedSessionIds: [], skippedSessionIds: [] };
373
+ }
374
+ if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
375
+
376
+ const requestedSessionIds = Array.isArray(args.sessionIds)
377
+ ? new Set(args.sessionIds.map(id => typeof id === 'string' ? id.trim() : '').filter(Boolean))
378
+ : undefined;
379
+ const sessions = await this.deps.sessionHostControl.listSessions();
380
+ const matched = sessions.filter(record => this.sessionMatchesMeshNode(record, args.node, args.nodeId, requestedSessionIds));
381
+ const stoppedSessionIds: string[] = [];
382
+ const deletedSessionIds: string[] = [];
383
+ const skippedSessionIds: string[] = [];
384
+ const errors: Array<{ sessionId: string; error: string }> = [];
385
+
386
+ for (const record of matched) {
387
+ const sessionId = String(record.sessionId);
388
+ const completed = this.isCompletedHostedSession(record);
389
+ try {
390
+ if (args.mode === 'stop') {
391
+ if (!completed) {
392
+ if (!args.dryRun) await this.deps.sessionHostControl.stopSession(sessionId);
393
+ stoppedSessionIds.push(sessionId);
394
+ } else {
395
+ skippedSessionIds.push(sessionId);
396
+ }
397
+ continue;
398
+ }
399
+
400
+ if (args.mode === 'delete_stopped') {
401
+ if (completed) {
402
+ if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: false });
403
+ deletedSessionIds.push(sessionId);
404
+ } else {
405
+ skippedSessionIds.push(sessionId);
406
+ }
407
+ continue;
408
+ }
409
+
410
+ if (args.mode === 'stop_and_delete') {
411
+ if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: true });
412
+ deletedSessionIds.push(sessionId);
413
+ continue;
414
+ }
415
+ } catch (e: any) {
416
+ errors.push({ sessionId, error: e?.message || String(e) });
417
+ }
418
+ }
419
+
420
+ return {
421
+ success: errors.length === 0,
422
+ mode: args.mode,
423
+ dryRun: args.dryRun === true,
424
+ matchedCount: matched.length,
425
+ stoppedSessionIds,
426
+ deletedSessionIds,
427
+ skippedSessionIds,
428
+ ...(errors.length ? { errors } : {}),
429
+ };
430
+ }
431
+
229
432
  private async traceSessionHostAction<T>(
230
433
  action: string,
231
434
  args: any,
@@ -363,6 +566,10 @@ export class DaemonCommandRouter {
363
566
  private async executeDaemonCommand(cmd: string, args: any): Promise<CommandRouterResult | null> {
364
567
  switch (cmd) {
365
568
  // ─── CLI / ACP commands ───
569
+ case 'mesh_forward_event': {
570
+ return handleMeshForwardEvent({ instanceManager: this.deps.instanceManager } as any, args as Record<string, unknown>);
571
+ }
572
+
366
573
  case 'launch_cli':
367
574
  case 'stop_cli':
368
575
  case 'set_cli_view_mode':
@@ -983,7 +1190,27 @@ export class DaemonCommandRouter {
983
1190
  if (!name) return { success: false, error: 'name required' };
984
1191
  try {
985
1192
  const { createMesh } = await import('../config/mesh-config.js');
986
- const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch });
1193
+ const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy });
1194
+ return { success: true, mesh };
1195
+ } catch (e: any) {
1196
+ return { success: false, error: e.message };
1197
+ }
1198
+ }
1199
+
1200
+ case 'update_mesh': {
1201
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1202
+ if (!meshId) return { success: false, error: 'meshId required' };
1203
+ try {
1204
+ const { updateMesh } = await import('../config/mesh-config.js');
1205
+ const patch: Record<string, unknown> = {};
1206
+ if (typeof args?.name === 'string') patch.name = args.name;
1207
+ if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
1208
+ if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
1209
+ if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
1210
+ if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
1211
+ const mesh = updateMesh(meshId, patch as any);
1212
+ if (!mesh) return { success: false, error: 'Mesh not found' };
1213
+ this.inlineMeshCache.set(meshId, mesh);
987
1214
  return { success: true, mesh };
988
1215
  } catch (e: any) {
989
1216
  return { success: false, error: e.message };
@@ -1009,7 +1236,15 @@ export class DaemonCommandRouter {
1009
1236
  if (!workspace) return { success: false, error: 'workspace required' };
1010
1237
  try {
1011
1238
  const { addNode } = await import('../config/mesh-config.js');
1012
- const node = addNode(meshId, { workspace });
1239
+ const providerPriority = Array.isArray(args?.providerPriority)
1240
+ ? args.providerPriority.map((type: any) => typeof type === 'string' ? type.trim() : '').filter(Boolean)
1241
+ : [];
1242
+ const readOnly = args?.readOnly === true;
1243
+ const policy = {
1244
+ ...(readOnly ? { readOnly: true } : {}),
1245
+ ...(providerPriority.length ? { providerPriority } : {}),
1246
+ };
1247
+ const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}) });
1013
1248
  if (!node) return { success: false, error: 'Mesh not found' };
1014
1249
  return { success: true, node };
1015
1250
  } catch (e: any) {
@@ -1017,14 +1252,172 @@ export class DaemonCommandRouter {
1017
1252
  }
1018
1253
  }
1019
1254
 
1255
+ case 'update_mesh_node': {
1256
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1257
+ const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
1258
+ if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
1259
+ try {
1260
+ const { updateNode } = await import('../config/mesh-config.js');
1261
+ const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
1262
+ ? { ...(args.policy as Record<string, unknown>) }
1263
+ : {};
1264
+ if (Array.isArray(args?.providerPriority)) {
1265
+ const providerPriority = args.providerPriority
1266
+ .map((type: any) => typeof type === 'string' ? type.trim() : '')
1267
+ .filter(Boolean);
1268
+ delete (policy as any).provider_priority;
1269
+ if (providerPriority.length) {
1270
+ (policy as any).providerPriority = providerPriority;
1271
+ } else {
1272
+ delete (policy as any).providerPriority;
1273
+ }
1274
+ }
1275
+ const node = updateNode(meshId, nodeId, { policy: policy as any });
1276
+ if (!node) return { success: false, error: 'Mesh node not found' };
1277
+ return { success: true, node };
1278
+ } catch (e: any) {
1279
+ return { success: false, error: e.message };
1280
+ }
1281
+ }
1282
+
1283
+ case 'cleanup_mesh_sessions': {
1284
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1285
+ const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
1286
+ if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
1287
+ try {
1288
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
1289
+ const mesh = meshRecord?.mesh;
1290
+ if (!mesh) return { success: false, error: 'Mesh not found' };
1291
+ const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
1292
+ if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
1293
+ const mode = this.normalizeMeshSessionCleanupMode(args?.mode ?? mesh?.policy?.sessionCleanupOnNodeRemove);
1294
+ const sessionIds = Array.isArray(args?.sessionIds)
1295
+ ? args.sessionIds.map((id: any) => typeof id === 'string' ? id.trim() : '').filter(Boolean)
1296
+ : undefined;
1297
+ const result = await this.cleanupMeshSessions({
1298
+ meshId,
1299
+ nodeId,
1300
+ node,
1301
+ mode,
1302
+ sessionIds,
1303
+ dryRun: args?.dryRun === true,
1304
+ });
1305
+ return result;
1306
+ } catch (e: any) {
1307
+ return { success: false, error: e.message };
1308
+ }
1309
+ }
1310
+
1020
1311
  case 'remove_mesh_node': {
1021
1312
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1022
1313
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
1023
1314
  if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
1024
1315
  try {
1025
- const { removeNode } = await import('../config/mesh-config.js');
1026
- const removed = removeNode(meshId, nodeId);
1027
- return { success: true, removed };
1316
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
1317
+ const mesh = meshRecord?.mesh;
1318
+ const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
1319
+
1320
+ const sessionCleanupMode = this.normalizeMeshSessionCleanupMode(
1321
+ args?.sessionCleanupMode ?? args?.session_cleanup_mode ?? mesh?.policy?.sessionCleanupOnNodeRemove,
1322
+ );
1323
+ let sessionCleanup: Record<string, unknown> | undefined;
1324
+ if (node && sessionCleanupMode !== 'preserve') {
1325
+ sessionCleanup = await this.cleanupMeshSessions({ meshId, nodeId, node, mode: sessionCleanupMode });
1326
+ if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
1327
+ }
1328
+
1329
+ // If this is a worktree node, clean up the git worktree first
1330
+ if (node?.isLocalWorktree && node.workspace) {
1331
+ try {
1332
+ const sourceNode = node.clonedFromNodeId
1333
+ ? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
1334
+ : mesh?.nodes.find((n: any) => !n.isLocalWorktree);
1335
+ const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
1336
+ if (repoRoot) {
1337
+ const { removeWorktree } = await import('../git/git-worktree.js');
1338
+ await removeWorktree(repoRoot, node.workspace);
1339
+ }
1340
+ } catch (e: any) {
1341
+ LOG.warn('MeshNode', `Worktree cleanup failed for ${nodeId}: ${e.message}`);
1342
+ // Continue with node removal even if worktree cleanup fails
1343
+ }
1344
+ }
1345
+
1346
+ let removed = false;
1347
+ if (meshRecord?.inline) {
1348
+ removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
1349
+ } else {
1350
+ const { removeNode } = await import('../config/mesh-config.js');
1351
+ removed = removeNode(meshId, nodeId);
1352
+ }
1353
+ return { success: true, removed, ...(sessionCleanup ? { sessionCleanup } : {}) };
1354
+ } catch (e: any) {
1355
+ return { success: false, error: e.message };
1356
+ }
1357
+ }
1358
+
1359
+ case 'clone_mesh_node': {
1360
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1361
+ const sourceNodeId = typeof args?.sourceNodeId === 'string' ? args.sourceNodeId.trim() : '';
1362
+ const branch = typeof args?.branch === 'string' ? args.branch.trim() : '';
1363
+ const baseBranch = typeof args?.baseBranch === 'string' ? args.baseBranch.trim() : undefined;
1364
+ if (!meshId) return { success: false, error: 'meshId required' };
1365
+ if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
1366
+ if (!branch) return { success: false, error: 'branch required' };
1367
+
1368
+ try {
1369
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
1370
+ const mesh = meshRecord?.mesh;
1371
+ if (!mesh) return { success: false, error: 'Mesh not found' };
1372
+
1373
+ const sourceNode = mesh.nodes?.find((n: any) => n.id === sourceNodeId || n.nodeId === sourceNodeId);
1374
+ if (!sourceNode) return { success: false, error: `Source node '${sourceNodeId}' not found in mesh` };
1375
+
1376
+ const repoRoot = sourceNode.repoRoot || sourceNode.workspace;
1377
+ const { createWorktree } = await import('../git/git-worktree.js');
1378
+ const result = await createWorktree({
1379
+ repoRoot,
1380
+ branch,
1381
+ baseBranch,
1382
+ meshName: mesh.name,
1383
+ });
1384
+
1385
+ let node: any;
1386
+ if (meshRecord.inline) {
1387
+ const { randomUUID } = await import('crypto');
1388
+ node = {
1389
+ id: `node_${randomUUID().replace(/-/g, '')}`,
1390
+ workspace: result.worktreePath,
1391
+ repoRoot: result.worktreePath,
1392
+ daemonId: sourceNode.daemonId,
1393
+ userOverrides: { ...(sourceNode.userOverrides || {}) },
1394
+ policy: { ...(sourceNode.policy || {}) },
1395
+ isLocalWorktree: true,
1396
+ worktreeBranch: result.branch,
1397
+ clonedFromNodeId: sourceNodeId,
1398
+ };
1399
+ this.updateInlineMeshNode(meshId, mesh, node);
1400
+ } else {
1401
+ const { addNode } = await import('../config/mesh-config.js');
1402
+ node = addNode(meshId, {
1403
+ workspace: result.worktreePath,
1404
+ repoRoot: result.worktreePath,
1405
+ daemonId: sourceNode.daemonId,
1406
+ userOverrides: { ...(sourceNode.userOverrides || {}) },
1407
+ isLocalWorktree: true,
1408
+ worktreeBranch: result.branch,
1409
+ clonedFromNodeId: sourceNodeId,
1410
+ policy: { ...(sourceNode.policy || {}) },
1411
+ });
1412
+ if (!node) return { success: false, error: 'Failed to register worktree node' };
1413
+ }
1414
+
1415
+ return {
1416
+ success: true,
1417
+ node,
1418
+ worktreePath: result.worktreePath,
1419
+ branch: result.branch,
1420
+ };
1028
1421
  } catch (e: any) {
1029
1422
  return { success: false, error: e.message };
1030
1423
  }
@@ -1033,7 +1426,7 @@ export class DaemonCommandRouter {
1033
1426
  // ─── Mesh Coordinator Launch ───
1034
1427
  case 'launch_mesh_coordinator': {
1035
1428
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1036
- const cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : 'claude-cli';
1429
+ let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
1037
1430
  if (!meshId) return { success: false, error: 'meshId required' };
1038
1431
 
1039
1432
  try {
@@ -1071,9 +1464,29 @@ export class DaemonCommandRouter {
1071
1464
  }
1072
1465
  const workspace = typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '';
1073
1466
  if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
1467
+ if (!cliType) {
1468
+ const resolved = await resolveProviderTypeFromPriority({
1469
+ nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || 'coordinator'),
1470
+ providerPriority: readProviderPriorityFromPolicy(coordinatorNode.policy),
1471
+ providerLoader: this.deps.providerLoader,
1472
+ onStatusChange: this.deps.onStatusChange,
1473
+ });
1474
+ if (!resolved.providerType) {
1475
+ return {
1476
+ success: false,
1477
+ code: 'mesh_coordinator_provider_priority_unusable',
1478
+ error: resolved.error || 'No usable provider found from node providerPriority',
1479
+ meshId,
1480
+ cliType,
1481
+ workspace,
1482
+ };
1483
+ }
1484
+ cliType = resolved.providerType;
1485
+ }
1074
1486
  const providerMeta = this.deps.providerLoader.resolve?.(cliType) || this.deps.providerLoader.getMeta(cliType);
1075
1487
  const coordinatorSetup = resolveMeshCoordinatorSetup({
1076
1488
  provider: providerMeta,
1489
+ cliType,
1077
1490
  meshId,
1078
1491
  workspace,
1079
1492
  });
@@ -1101,7 +1514,8 @@ export class DaemonCommandRouter {
1101
1514
  };
1102
1515
  }
1103
1516
 
1104
- if (coordinatorSetup.configFormat !== 'claude_mcp_json') {
1517
+ const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
1518
+ if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
1105
1519
  return {
1106
1520
  success: false,
1107
1521
  code: 'mesh_coordinator_unsupported',
@@ -1112,20 +1526,42 @@ export class DaemonCommandRouter {
1112
1526
  };
1113
1527
  }
1114
1528
 
1115
- // 1. Write provider-declared MCP config to workspace for CLIs that auto-import it.
1116
- const { existsSync, readFileSync, writeFileSync, copyFileSync } = await import('fs');
1117
- const mcpConfigPath = coordinatorSetup.configPath;
1118
-
1119
- // Backup existing MCP config if present.
1120
- const hadExistingMcpConfig = existsSync(mcpConfigPath);
1121
- let existingMcpConfig: any = {};
1122
- if (hadExistingMcpConfig) {
1123
- try {
1124
- existingMcpConfig = JSON.parse(readFileSync(mcpConfigPath, 'utf-8'));
1125
- copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
1126
- } catch { /* empty */ }
1529
+ // Build the coordinator prompt before mutating workspace config or launching.
1530
+ // Prompt generation failures are configuration/data-shape errors; fail closed so
1531
+ // broken mesh state is visible instead of silently launching with weaker rules.
1532
+ let systemPrompt = '';
1533
+ try {
1534
+ systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
1535
+ } catch (error: any) {
1536
+ const message = error?.message || String(error);
1537
+ LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
1538
+ return {
1539
+ success: false,
1540
+ code: 'mesh_coordinator_prompt_failed',
1541
+ error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
1542
+ meshId,
1543
+ cliType,
1544
+ workspace,
1545
+ };
1127
1546
  }
1128
1547
 
1548
+ // 1. Write provider-declared MCP config for CLIs that auto-import it.
1549
+ const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
1550
+ const { dirname } = await import('path');
1551
+ const mcpConfigPath = coordinatorSetup.configPath;
1552
+ const hermesManualFallback = cliType === 'hermes-cli' && configFormat === 'hermes_config_yaml'
1553
+ ? createHermesManualMeshCoordinatorSetup(meshId, workspace)
1554
+ : null;
1555
+ const returnManualFallback = (message: string) => ({
1556
+ success: false,
1557
+ code: 'mesh_coordinator_manual_mcp_setup_required',
1558
+ error: message,
1559
+ meshId,
1560
+ cliType,
1561
+ workspace,
1562
+ meshCoordinatorSetup: hermesManualFallback,
1563
+ });
1564
+
1129
1565
  // Merge ADHDev mesh server into existing config.
1130
1566
  // Pass full mesh data as env var so the MCP server can bootstrap
1131
1567
  // without depending on meshes.json or a running daemon.
@@ -1136,41 +1572,76 @@ export class DaemonCommandRouter {
1136
1572
  if (args?.inlineMesh) {
1137
1573
  mcpServerEntry.env = {
1138
1574
  ADHDEV_INLINE_MESH: JSON.stringify(mesh),
1575
+ ADHDEV_MCP_TRANSPORT: 'ipc',
1139
1576
  };
1140
1577
  }
1578
+
1579
+ try {
1580
+ mkdirSync(dirname(mcpConfigPath), { recursive: true });
1581
+ } catch (error: any) {
1582
+ const message = `Could not prepare MCP config path for automatic setup: ${error?.message || error}`;
1583
+ LOG.error('MeshCoordinator', message);
1584
+ if (hermesManualFallback) return returnManualFallback(message);
1585
+ return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
1586
+ }
1587
+
1588
+ // Backup existing MCP config if present.
1589
+ const hadExistingMcpConfig = existsSync(mcpConfigPath);
1590
+ let existingMcpConfig: Record<string, any> = {};
1591
+ if (hadExistingMcpConfig) {
1592
+ try {
1593
+ existingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
1594
+ copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
1595
+ } catch (error: any) {
1596
+ LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
1597
+ return {
1598
+ success: false,
1599
+ code: 'mesh_coordinator_config_parse_failed',
1600
+ error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
1601
+ };
1602
+ }
1603
+ }
1604
+
1605
+ const mcpServersKey = getMcpServersKey(configFormat);
1606
+ const existingServers = existingMcpConfig[mcpServersKey];
1141
1607
  const mcpConfig = {
1142
1608
  ...existingMcpConfig,
1143
- mcpServers: {
1144
- ...(existingMcpConfig.mcpServers || {}),
1609
+ [mcpServersKey]: {
1610
+ ...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
1145
1611
  [coordinatorSetup.serverName]: mcpServerEntry,
1146
1612
  },
1147
1613
  };
1148
- writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');
1149
- LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
1150
-
1151
- // 2. Build coordinator system prompt
1152
- let systemPrompt = '';
1153
1614
  try {
1154
- systemPrompt = buildCoordinatorSystemPrompt({ mesh });
1155
- } catch {
1156
- systemPrompt = `You are a Repo Mesh Coordinator for "${mesh.name}". Use the adhdev-mesh MCP tools (mesh_status, mesh_list_nodes, mesh_send_task, mesh_read_chat, mesh_launch_session, etc.) to orchestrate work across ${mesh.nodes.length} node(s).`;
1615
+ writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
1616
+ } catch (error: any) {
1617
+ const message = `Could not write MCP config for automatic setup: ${error?.message || error}`;
1618
+ LOG.error('MeshCoordinator', message);
1619
+ if (hermesManualFallback) return returnManualFallback(message);
1620
+ return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
1157
1621
  }
1622
+ LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
1158
1623
 
1159
1624
  const cliArgs: string[] = [];
1625
+ const launchEnv: Record<string, string> = {};
1160
1626
  if (systemPrompt) {
1161
- cliArgs.push('--append-system-prompt', systemPrompt);
1627
+ if (configFormat === 'hermes_config_yaml') {
1628
+ launchEnv.HERMES_EPHEMERAL_SYSTEM_PROMPT = systemPrompt;
1629
+ } else {
1630
+ cliArgs.push('--append-system-prompt', systemPrompt);
1631
+ }
1162
1632
  }
1163
1633
  if (cliType === 'claude-cli') {
1164
1634
  cliArgs.push('--mcp-config', coordinatorSetup.configPath);
1165
1635
  }
1166
1636
 
1167
- // 3. Launch CLI session via existing cliManager
1168
- // Pass coordinator system prompt via --append-system-prompt so the
1169
- // CLI inherits its default behavior AND knows it is a mesh coordinator.
1637
+ // 3. Launch CLI session via existing cliManager.
1638
+ // Provider-specific prompt injection remains fail-closed: Claude gets
1639
+ // explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
1170
1640
  const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
1171
1641
  cliType,
1172
1642
  dir: workspace,
1173
1643
  cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
1644
+ env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
1174
1645
  settings: {
1175
1646
  meshCoordinatorFor: meshId
1176
1647
  }