@adhdev/daemon-core 0.9.76-rc.8 → 0.9.76

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 (65) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +5 -2
  2. package/dist/cli-adapters/provider-cli-runtime.d.ts +1 -0
  3. package/dist/cli-adapters/provider-cli-shared.d.ts +24 -0
  4. package/dist/commands/chat-commands.d.ts +2 -0
  5. package/dist/commands/cli-manager.d.ts +17 -4
  6. package/dist/commands/mesh-coordinator.d.ts +2 -0
  7. package/dist/commands/router.d.ts +11 -0
  8. package/dist/config/mesh-config.d.ts +3 -0
  9. package/dist/git/git-types.d.ts +1 -1
  10. package/dist/git/git-worktree.d.ts +64 -0
  11. package/dist/git/index.d.ts +2 -0
  12. package/dist/index.d.ts +4 -4
  13. package/dist/index.js +2427 -561
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.mjs +2432 -584
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/mesh/coordinator-prompt.d.ts +1 -0
  18. package/dist/mesh/mesh-events.d.ts +18 -0
  19. package/dist/providers/chat-message-normalization.d.ts +40 -0
  20. package/dist/providers/cli-provider-instance.d.ts +7 -1
  21. package/dist/providers/contracts.d.ts +20 -1
  22. package/dist/providers/io-contracts.d.ts +17 -1
  23. package/dist/providers/provider-input-support.d.ts +18 -2
  24. package/dist/providers/provider-instance-manager.d.ts +1 -0
  25. package/dist/providers/provider-instance.d.ts +4 -0
  26. package/dist/repo-mesh-types.d.ts +34 -0
  27. package/dist/session-host/runtime-support.d.ts +2 -1
  28. package/dist/shared-types.d.ts +8 -0
  29. package/dist/types.d.ts +9 -0
  30. package/package.json +4 -5
  31. package/src/chat/subscription-updates.ts +3 -1
  32. package/src/cli-adapters/provider-cli-adapter.ts +44 -11
  33. package/src/cli-adapters/provider-cli-runtime.ts +3 -2
  34. package/src/cli-adapters/provider-cli-shared.ts +201 -15
  35. package/src/commands/chat-commands.ts +166 -16
  36. package/src/commands/cli-manager.ts +78 -5
  37. package/src/commands/handler.ts +13 -4
  38. package/src/commands/mesh-coordinator.ts +155 -5
  39. package/src/commands/router.d.ts +1 -0
  40. package/src/commands/router.ts +606 -32
  41. package/src/config/mesh-config.ts +27 -2
  42. package/src/git/git-commands.ts +5 -1
  43. package/src/git/git-types.ts +1 -0
  44. package/src/git/git-worktree.ts +214 -0
  45. package/src/git/index.ts +14 -0
  46. package/src/index.ts +20 -1
  47. package/src/mesh/coordinator-prompt.ts +36 -14
  48. package/src/mesh/mesh-events.ts +173 -42
  49. package/src/providers/acp-provider-instance.ts +118 -30
  50. package/src/providers/chat-message-normalization.ts +241 -0
  51. package/src/providers/cli-provider-instance.d.ts +2 -0
  52. package/src/providers/cli-provider-instance.ts +219 -13
  53. package/src/providers/contracts.ts +25 -1
  54. package/src/providers/io-contracts.ts +63 -5
  55. package/src/providers/provider-input-support.ts +125 -1
  56. package/src/providers/provider-instance-manager.ts +20 -1
  57. package/src/providers/provider-instance.ts +4 -0
  58. package/src/providers/provider-schema.ts +38 -8
  59. package/src/providers/read-chat-contract.ts +8 -0
  60. package/src/repo-mesh-types.ts +38 -0
  61. package/src/session-host/runtime-support.ts +55 -7
  62. package/src/shared-types.ts +8 -0
  63. package/src/status/builders.ts +5 -3
  64. package/src/status/reporter.ts +6 -0
  65. package/src/types.ts +9 -0
@@ -30,14 +30,19 @@ 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, drainPendingMeshCoordinatorEvents } 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';
44
+ import { homedir } from 'os';
45
+ import { join as pathJoin, resolve as pathResolve } from 'path';
41
46
 
42
47
  type ReleaseChannel = 'stable' | 'preview';
43
48
  const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
@@ -61,14 +66,115 @@ function resolveUpgradeChannel(args: any): ReleaseChannel {
61
66
  || normalizeReleaseChannel(loadConfig().updateChannel)
62
67
  || 'stable';
63
68
  }
69
+
70
+ function readProviderPriorityFromPolicy(policy: unknown): string[] {
71
+ const record = policy && typeof policy === 'object' && !Array.isArray(policy)
72
+ ? policy as Record<string, unknown>
73
+ : {};
74
+ const raw = record.providerPriority;
75
+ if (!Array.isArray(raw)) return [];
76
+ const seen = new Set<string>();
77
+ return raw
78
+ .map(type => typeof type === 'string' ? type.trim() : '')
79
+ .filter(Boolean)
80
+ .filter(type => {
81
+ if (seen.has(type)) return false;
82
+ seen.add(type);
83
+ return true;
84
+ });
85
+ }
86
+
87
+ async function resolveProviderTypeFromPriority(args: {
88
+ nodeId: string;
89
+ providerPriority: string[];
90
+ providerLoader: ProviderLoader;
91
+ onStatusChange?: () => void;
92
+ }): Promise<{ providerType?: string; error?: string }> {
93
+ if (!args.providerPriority.length) {
94
+ return { error: `Node '${args.nodeId}' has no providerPriority policy; pass cliType explicitly or configure node.policy.providerPriority` };
95
+ }
96
+
97
+ const failed: string[] = [];
98
+ for (const requestedType of args.providerPriority) {
99
+ const normalizedType = args.providerLoader.resolveAlias(requestedType);
100
+ if (!args.providerLoader.isMachineProviderEnabled(normalizedType)) {
101
+ failed.push(`${requestedType}: disabled`);
102
+ continue;
103
+ }
104
+ const detected = await detectCLI(normalizedType, args.providerLoader, { includeVersion: false });
105
+ args.providerLoader.setCliDetectionResults([{
106
+ id: normalizedType,
107
+ installed: !!detected,
108
+ path: detected?.path,
109
+ }], false);
110
+ args.onStatusChange?.();
111
+ if (detected) return { providerType: normalizedType };
112
+ failed.push(`${requestedType}: not detected`);
113
+ }
114
+
115
+ return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join('; ')}` };
116
+ }
64
117
  import * as fs from 'fs';
65
118
 
119
+ type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
120
+
121
+ function loadYamlModule(): { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string } {
122
+ return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
123
+ }
124
+
125
+ function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
126
+ return format === 'hermes_config_yaml' ? 'mcp_servers' : 'mcpServers';
127
+ }
128
+
129
+ function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
130
+ if (!text.trim()) return {};
131
+ if (format === 'claude_mcp_json') return JSON.parse(text);
132
+ const parsed = loadYamlModule().load(text);
133
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
134
+ }
135
+
136
+ function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
137
+ if (format === 'claude_mcp_json') return JSON.stringify(config, null, 2);
138
+ return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
139
+ }
140
+
141
+ function resolveHermesUserHome(): string {
142
+ const explicitHome = process.env.HERMES_HOME?.trim();
143
+ return explicitHome || pathJoin(homedir(), '.hermes');
144
+ }
145
+
146
+ function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } {
147
+ const sourceHome = resolveHermesUserHome();
148
+ const sourceConfigPath = pathJoin(sourceHome, 'config.yaml');
149
+ if (!fs.existsSync(sourceConfigPath)) return { config: {}, sourceHome, sourceConfigPath };
150
+ if (pathResolve(sourceConfigPath) === pathResolve(targetConfigPath)) return { config: {}, sourceHome, sourceConfigPath };
151
+
152
+ const parsed = parseMeshCoordinatorMcpConfig(fs.readFileSync(sourceConfigPath, 'utf-8'), 'hermes_config_yaml');
153
+ const { mcp_servers: _mcpServers, ...baseConfig } = parsed;
154
+ return { config: baseConfig, sourceHome, sourceConfigPath };
155
+ }
156
+
157
+ function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
158
+ if (pathResolve(sourceHome) === pathResolve(targetHome)) return;
159
+ for (const fileName of ['.env', 'auth.json']) {
160
+ const sourcePath = pathJoin(sourceHome, fileName);
161
+ const targetPath = pathJoin(targetHome, fileName);
162
+ if (!fs.existsSync(sourcePath)) continue;
163
+ try {
164
+ fs.copyFileSync(sourcePath, targetPath);
165
+ } catch (error: any) {
166
+ LOG.warn('MeshCoordinator', `Could not copy Hermes ${fileName} into isolated coordinator home: ${error?.message || error}`);
167
+ }
168
+ }
169
+ }
170
+
66
171
  // ─── Types ───
67
172
 
68
173
  export interface SessionHostControlPlane {
69
174
  getDiagnostics(payload?: { includeSessions?: boolean; limit?: number }): Promise<any>;
70
175
  listSessions(): Promise<any[]>;
71
176
  stopSession(sessionId: string): Promise<any>;
177
+ deleteSession(sessionId: string, opts?: { force?: boolean }): Promise<any>;
72
178
  resumeSession(sessionId: string): Promise<any>;
73
179
  restartSession(sessionId: string): Promise<any>;
74
180
  sendSignal(sessionId: string, signal: string): Promise<any>;
@@ -226,6 +332,184 @@ export class DaemonCommandRouter {
226
332
  this.deps = deps;
227
333
  }
228
334
 
335
+ private getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
336
+ if (inlineMesh && typeof inlineMesh === 'object') {
337
+ this.inlineMeshCache.set(meshId, inlineMesh as any);
338
+ return inlineMesh as any;
339
+ }
340
+ return this.inlineMeshCache.get(meshId);
341
+ }
342
+
343
+ private async getMeshForCommand(meshId: string, inlineMesh?: unknown): Promise<{ mesh: any; inline: boolean } | null> {
344
+ try {
345
+ const { getMesh } = await import('../config/mesh-config.js');
346
+ const mesh = getMesh(meshId);
347
+ if (mesh) return { mesh, inline: false };
348
+ } catch { /* fall through to inline cache */ }
349
+ const cached = this.getCachedInlineMesh(meshId, inlineMesh);
350
+ return cached ? { mesh: cached, inline: true } : null;
351
+ }
352
+
353
+ private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
354
+ if (!mesh || !Array.isArray(mesh.nodes) || !node?.id) return;
355
+ const idx = mesh.nodes.findIndex((entry: any) => entry?.id === node.id || entry?.nodeId === node.id);
356
+ if (idx >= 0) mesh.nodes[idx] = node;
357
+ else mesh.nodes.push(node);
358
+ mesh.updatedAt = new Date().toISOString();
359
+ this.inlineMeshCache.set(meshId, mesh);
360
+ }
361
+
362
+ private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
363
+ if (!mesh || !Array.isArray(mesh.nodes)) return false;
364
+ const idx = mesh.nodes.findIndex((entry: any) => entry?.id === nodeId || entry?.nodeId === nodeId);
365
+ if (idx === -1) return false;
366
+ mesh.nodes.splice(idx, 1);
367
+ mesh.updatedAt = new Date().toISOString();
368
+ this.inlineMeshCache.set(meshId, mesh);
369
+ return true;
370
+ }
371
+
372
+ private normalizeMeshSessionCleanupMode(value: unknown): RepoMeshSessionCleanupMode {
373
+ return value === 'stop'
374
+ || value === 'delete_stopped'
375
+ || value === 'stop_and_delete'
376
+ || value === 'preserve'
377
+ ? value
378
+ : 'preserve';
379
+ }
380
+
381
+ private sessionMatchesMeshNode(record: any, node: any, nodeId: string, sessionIds?: Set<string>): boolean {
382
+ const sessionId = typeof record?.sessionId === 'string' ? record.sessionId : '';
383
+ if (!sessionId) return false;
384
+ if (sessionIds?.size) return sessionIds.has(sessionId);
385
+ const workspace = typeof node?.workspace === 'string' ? node.workspace : '';
386
+ if (workspace && record?.workspace === workspace) return true;
387
+ if (record?.meta?.meshNodeId === nodeId) return true;
388
+ return false;
389
+ }
390
+
391
+ private isCompletedHostedSession(record: any): boolean {
392
+ return record?.lifecycle === 'stopped' || record?.lifecycle === 'failed' || record?.lifecycle === 'interrupted';
393
+ }
394
+
395
+ private async cleanupMeshSessions(args: {
396
+ meshId: string;
397
+ nodeId: string;
398
+ node: any;
399
+ mode: RepoMeshSessionCleanupMode;
400
+ sessionIds?: string[];
401
+ dryRun?: boolean;
402
+ }): Promise<{ success: boolean; [key: string]: unknown }> {
403
+ if (args.mode === 'preserve') {
404
+ return { success: true, mode: 'preserve', matchedCount: 0, stoppedSessionIds: [], deletedSessionIds: [], skippedSessionIds: [] };
405
+ }
406
+ if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
407
+
408
+ const requestedSessionIds = Array.isArray(args.sessionIds)
409
+ ? new Set(args.sessionIds.map(id => typeof id === 'string' ? id.trim() : '').filter(Boolean))
410
+ : undefined;
411
+ const sessions = await this.deps.sessionHostControl.listSessions();
412
+ const matched = sessions.filter(record => this.sessionMatchesMeshNode(record, args.node, args.nodeId, requestedSessionIds));
413
+ const hasExplicitSessionIds = !!requestedSessionIds?.size;
414
+ const stoppedSessionIds: string[] = [];
415
+ const deletedSessionIds: string[] = [];
416
+ const skippedSessionIds: string[] = [];
417
+ const skippedLiveSessionIds: string[] = [];
418
+ const deleteUnsupportedSessionIds: string[] = [];
419
+ const recordsRemainSessionIds: string[] = [];
420
+ const errors: Array<{ sessionId: string; error: string }> = [];
421
+ const matchedBySurfaceKind = {
422
+ live_runtime: 0,
423
+ recovery_snapshot: 0,
424
+ inactive_record: 0,
425
+ };
426
+
427
+ for (const record of matched) {
428
+ const surfaceKind = getSessionHostSurfaceKind(record);
429
+ matchedBySurfaceKind[surfaceKind] += 1;
430
+ }
431
+
432
+ for (const record of matched) {
433
+ const sessionId = String(record.sessionId);
434
+ const completed = this.isCompletedHostedSession(record);
435
+ const surfaceKind = getSessionHostSurfaceKind(record);
436
+ const liveRuntime = surfaceKind === 'live_runtime';
437
+ if (!hasExplicitSessionIds && liveRuntime) {
438
+ skippedSessionIds.push(sessionId);
439
+ skippedLiveSessionIds.push(sessionId);
440
+ continue;
441
+ }
442
+ try {
443
+ if (args.mode === 'stop') {
444
+ if (!completed) {
445
+ if (!args.dryRun) await this.deps.sessionHostControl.stopSession(sessionId);
446
+ stoppedSessionIds.push(sessionId);
447
+ } else {
448
+ skippedSessionIds.push(sessionId);
449
+ }
450
+ continue;
451
+ }
452
+
453
+ if (args.mode === 'delete_stopped') {
454
+ if (completed) {
455
+ if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: false });
456
+ deletedSessionIds.push(sessionId);
457
+ } else {
458
+ skippedSessionIds.push(sessionId);
459
+ }
460
+ continue;
461
+ }
462
+
463
+ if (args.mode === 'stop_and_delete') {
464
+ if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: true });
465
+ deletedSessionIds.push(sessionId);
466
+ continue;
467
+ }
468
+ } catch (e: any) {
469
+ const message = e?.message || String(e);
470
+ if (message.includes('Unsupported session host request: delete_session')
471
+ && (args.mode === 'delete_stopped' || args.mode === 'stop_and_delete')) {
472
+ deleteUnsupportedSessionIds.push(sessionId);
473
+ recordsRemainSessionIds.push(sessionId);
474
+ if (args.mode === 'stop_and_delete' && !completed) {
475
+ try {
476
+ await this.deps.sessionHostControl.stopSession(sessionId);
477
+ stoppedSessionIds.push(sessionId);
478
+ } catch (stopError: any) {
479
+ errors.push({ sessionId, error: stopError?.message || String(stopError) });
480
+ continue;
481
+ }
482
+ }
483
+ skippedSessionIds.push(sessionId);
484
+ continue;
485
+ }
486
+ errors.push({ sessionId, error: message });
487
+ }
488
+ }
489
+
490
+ const deleteUnsupported = deleteUnsupportedSessionIds.length > 0;
491
+ return {
492
+ success: errors.length === 0,
493
+ mode: args.mode,
494
+ dryRun: args.dryRun === true,
495
+ matchedCount: matched.length,
496
+ matchedBySurfaceKind,
497
+ stoppedSessionIds,
498
+ deletedSessionIds,
499
+ skippedSessionIds,
500
+ skippedLiveSessionIds,
501
+ ...(deleteUnsupported ? {
502
+ deleteUnsupported: true,
503
+ effectiveCleanup: args.mode === 'stop_and_delete'
504
+ ? 'stopped_only_records_remain'
505
+ : 'delete_unsupported_records_remain',
506
+ deleteUnsupportedSessionIds,
507
+ recordsRemainSessionIds,
508
+ } : {}),
509
+ ...(errors.length ? { errors } : {}),
510
+ };
511
+ }
512
+
229
513
  private async traceSessionHostAction<T>(
230
514
  action: string,
231
515
  args: any,
@@ -363,6 +647,15 @@ export class DaemonCommandRouter {
363
647
  private async executeDaemonCommand(cmd: string, args: any): Promise<CommandRouterResult | null> {
364
648
  switch (cmd) {
365
649
  // ─── CLI / ACP commands ───
650
+ case 'mesh_forward_event': {
651
+ return handleMeshForwardEvent({ instanceManager: this.deps.instanceManager } as any, args as Record<string, unknown>);
652
+ }
653
+
654
+ case 'get_pending_mesh_events': {
655
+ const events = drainPendingMeshCoordinatorEvents();
656
+ return { success: true, events };
657
+ }
658
+
366
659
  case 'launch_cli':
367
660
  case 'stop_cli':
368
661
  case 'set_cli_view_mode':
@@ -983,7 +1276,27 @@ export class DaemonCommandRouter {
983
1276
  if (!name) return { success: false, error: 'name required' };
984
1277
  try {
985
1278
  const { createMesh } = await import('../config/mesh-config.js');
986
- const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch });
1279
+ const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy });
1280
+ return { success: true, mesh };
1281
+ } catch (e: any) {
1282
+ return { success: false, error: e.message };
1283
+ }
1284
+ }
1285
+
1286
+ case 'update_mesh': {
1287
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1288
+ if (!meshId) return { success: false, error: 'meshId required' };
1289
+ try {
1290
+ const { updateMesh } = await import('../config/mesh-config.js');
1291
+ const patch: Record<string, unknown> = {};
1292
+ if (typeof args?.name === 'string') patch.name = args.name;
1293
+ if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
1294
+ if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
1295
+ if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
1296
+ if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
1297
+ const mesh = updateMesh(meshId, patch as any);
1298
+ if (!mesh) return { success: false, error: 'Mesh not found' };
1299
+ this.inlineMeshCache.set(meshId, mesh);
987
1300
  return { success: true, mesh };
988
1301
  } catch (e: any) {
989
1302
  return { success: false, error: e.message };
@@ -1009,7 +1322,15 @@ export class DaemonCommandRouter {
1009
1322
  if (!workspace) return { success: false, error: 'workspace required' };
1010
1323
  try {
1011
1324
  const { addNode } = await import('../config/mesh-config.js');
1012
- const node = addNode(meshId, { workspace });
1325
+ const providerPriority = Array.isArray(args?.providerPriority)
1326
+ ? args.providerPriority.map((type: any) => typeof type === 'string' ? type.trim() : '').filter(Boolean)
1327
+ : [];
1328
+ const readOnly = args?.readOnly === true;
1329
+ const policy = {
1330
+ ...(readOnly ? { readOnly: true } : {}),
1331
+ ...(providerPriority.length ? { providerPriority } : {}),
1332
+ };
1333
+ const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}) });
1013
1334
  if (!node) return { success: false, error: 'Mesh not found' };
1014
1335
  return { success: true, node };
1015
1336
  } catch (e: any) {
@@ -1017,14 +1338,172 @@ export class DaemonCommandRouter {
1017
1338
  }
1018
1339
  }
1019
1340
 
1341
+ case 'update_mesh_node': {
1342
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1343
+ const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
1344
+ if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
1345
+ try {
1346
+ const { updateNode } = await import('../config/mesh-config.js');
1347
+ const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
1348
+ ? { ...(args.policy as Record<string, unknown>) }
1349
+ : {};
1350
+ if (Array.isArray(args?.providerPriority)) {
1351
+ const providerPriority = args.providerPriority
1352
+ .map((type: any) => typeof type === 'string' ? type.trim() : '')
1353
+ .filter(Boolean);
1354
+ delete (policy as any).provider_priority;
1355
+ if (providerPriority.length) {
1356
+ (policy as any).providerPriority = providerPriority;
1357
+ } else {
1358
+ delete (policy as any).providerPriority;
1359
+ }
1360
+ }
1361
+ const node = updateNode(meshId, nodeId, { policy: policy as any });
1362
+ if (!node) return { success: false, error: 'Mesh node not found' };
1363
+ return { success: true, node };
1364
+ } catch (e: any) {
1365
+ return { success: false, error: e.message };
1366
+ }
1367
+ }
1368
+
1369
+ case 'cleanup_mesh_sessions': {
1370
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1371
+ const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
1372
+ if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
1373
+ try {
1374
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
1375
+ const mesh = meshRecord?.mesh;
1376
+ if (!mesh) return { success: false, error: 'Mesh not found' };
1377
+ const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
1378
+ if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
1379
+ const mode = this.normalizeMeshSessionCleanupMode(args?.mode ?? mesh?.policy?.sessionCleanupOnNodeRemove);
1380
+ const sessionIds = Array.isArray(args?.sessionIds)
1381
+ ? args.sessionIds.map((id: any) => typeof id === 'string' ? id.trim() : '').filter(Boolean)
1382
+ : undefined;
1383
+ const result = await this.cleanupMeshSessions({
1384
+ meshId,
1385
+ nodeId,
1386
+ node,
1387
+ mode,
1388
+ sessionIds,
1389
+ dryRun: args?.dryRun === true,
1390
+ });
1391
+ return result;
1392
+ } catch (e: any) {
1393
+ return { success: false, error: e.message };
1394
+ }
1395
+ }
1396
+
1020
1397
  case 'remove_mesh_node': {
1021
1398
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1022
1399
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
1023
1400
  if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
1024
1401
  try {
1025
- const { removeNode } = await import('../config/mesh-config.js');
1026
- const removed = removeNode(meshId, nodeId);
1027
- return { success: true, removed };
1402
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
1403
+ const mesh = meshRecord?.mesh;
1404
+ const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
1405
+
1406
+ const sessionCleanupMode = this.normalizeMeshSessionCleanupMode(
1407
+ args?.sessionCleanupMode ?? args?.session_cleanup_mode ?? mesh?.policy?.sessionCleanupOnNodeRemove,
1408
+ );
1409
+ let sessionCleanup: Record<string, unknown> | undefined;
1410
+ if (node && sessionCleanupMode !== 'preserve') {
1411
+ sessionCleanup = await this.cleanupMeshSessions({ meshId, nodeId, node, mode: sessionCleanupMode });
1412
+ if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
1413
+ }
1414
+
1415
+ // If this is a worktree node, clean up the git worktree first
1416
+ if (node?.isLocalWorktree && node.workspace) {
1417
+ try {
1418
+ const sourceNode = node.clonedFromNodeId
1419
+ ? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
1420
+ : mesh?.nodes.find((n: any) => !n.isLocalWorktree);
1421
+ const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
1422
+ if (repoRoot) {
1423
+ const { removeWorktree } = await import('../git/git-worktree.js');
1424
+ await removeWorktree(repoRoot, node.workspace);
1425
+ }
1426
+ } catch (e: any) {
1427
+ LOG.warn('MeshNode', `Worktree cleanup failed for ${nodeId}: ${e.message}`);
1428
+ // Continue with node removal even if worktree cleanup fails
1429
+ }
1430
+ }
1431
+
1432
+ let removed = false;
1433
+ if (meshRecord?.inline) {
1434
+ removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
1435
+ } else {
1436
+ const { removeNode } = await import('../config/mesh-config.js');
1437
+ removed = removeNode(meshId, nodeId);
1438
+ }
1439
+ return { success: true, removed, ...(sessionCleanup ? { sessionCleanup } : {}) };
1440
+ } catch (e: any) {
1441
+ return { success: false, error: e.message };
1442
+ }
1443
+ }
1444
+
1445
+ case 'clone_mesh_node': {
1446
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1447
+ const sourceNodeId = typeof args?.sourceNodeId === 'string' ? args.sourceNodeId.trim() : '';
1448
+ const branch = typeof args?.branch === 'string' ? args.branch.trim() : '';
1449
+ const baseBranch = typeof args?.baseBranch === 'string' ? args.baseBranch.trim() : undefined;
1450
+ if (!meshId) return { success: false, error: 'meshId required' };
1451
+ if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
1452
+ if (!branch) return { success: false, error: 'branch required' };
1453
+
1454
+ try {
1455
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
1456
+ const mesh = meshRecord?.mesh;
1457
+ if (!mesh) return { success: false, error: 'Mesh not found' };
1458
+
1459
+ const sourceNode = mesh.nodes?.find((n: any) => n.id === sourceNodeId || n.nodeId === sourceNodeId);
1460
+ if (!sourceNode) return { success: false, error: `Source node '${sourceNodeId}' not found in mesh` };
1461
+
1462
+ const repoRoot = sourceNode.repoRoot || sourceNode.workspace;
1463
+ const { createWorktree } = await import('../git/git-worktree.js');
1464
+ const result = await createWorktree({
1465
+ repoRoot,
1466
+ branch,
1467
+ baseBranch,
1468
+ meshName: mesh.name,
1469
+ });
1470
+
1471
+ let node: any;
1472
+ if (meshRecord.inline) {
1473
+ const { randomUUID } = await import('crypto');
1474
+ node = {
1475
+ id: `node_${randomUUID().replace(/-/g, '')}`,
1476
+ workspace: result.worktreePath,
1477
+ repoRoot: result.worktreePath,
1478
+ daemonId: sourceNode.daemonId,
1479
+ userOverrides: { ...(sourceNode.userOverrides || {}) },
1480
+ policy: { ...(sourceNode.policy || {}) },
1481
+ isLocalWorktree: true,
1482
+ worktreeBranch: result.branch,
1483
+ clonedFromNodeId: sourceNodeId,
1484
+ };
1485
+ this.updateInlineMeshNode(meshId, mesh, node);
1486
+ } else {
1487
+ const { addNode } = await import('../config/mesh-config.js');
1488
+ node = addNode(meshId, {
1489
+ workspace: result.worktreePath,
1490
+ repoRoot: result.worktreePath,
1491
+ daemonId: sourceNode.daemonId,
1492
+ userOverrides: { ...(sourceNode.userOverrides || {}) },
1493
+ isLocalWorktree: true,
1494
+ worktreeBranch: result.branch,
1495
+ clonedFromNodeId: sourceNodeId,
1496
+ policy: { ...(sourceNode.policy || {}) },
1497
+ });
1498
+ if (!node) return { success: false, error: 'Failed to register worktree node' };
1499
+ }
1500
+
1501
+ return {
1502
+ success: true,
1503
+ node,
1504
+ worktreePath: result.worktreePath,
1505
+ branch: result.branch,
1506
+ };
1028
1507
  } catch (e: any) {
1029
1508
  return { success: false, error: e.message };
1030
1509
  }
@@ -1033,7 +1512,7 @@ export class DaemonCommandRouter {
1033
1512
  // ─── Mesh Coordinator Launch ───
1034
1513
  case 'launch_mesh_coordinator': {
1035
1514
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1036
- const cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : 'claude-cli';
1515
+ let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
1037
1516
  if (!meshId) return { success: false, error: 'meshId required' };
1038
1517
 
1039
1518
  try {
@@ -1071,9 +1550,29 @@ export class DaemonCommandRouter {
1071
1550
  }
1072
1551
  const workspace = typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '';
1073
1552
  if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
1553
+ if (!cliType) {
1554
+ const resolved = await resolveProviderTypeFromPriority({
1555
+ nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || 'coordinator'),
1556
+ providerPriority: readProviderPriorityFromPolicy(coordinatorNode.policy),
1557
+ providerLoader: this.deps.providerLoader,
1558
+ onStatusChange: this.deps.onStatusChange,
1559
+ });
1560
+ if (!resolved.providerType) {
1561
+ return {
1562
+ success: false,
1563
+ code: 'mesh_coordinator_provider_priority_unusable',
1564
+ error: resolved.error || 'No usable provider found from node providerPriority',
1565
+ meshId,
1566
+ cliType,
1567
+ workspace,
1568
+ };
1569
+ }
1570
+ cliType = resolved.providerType;
1571
+ }
1074
1572
  const providerMeta = this.deps.providerLoader.resolve?.(cliType) || this.deps.providerLoader.getMeta(cliType);
1075
1573
  const coordinatorSetup = resolveMeshCoordinatorSetup({
1076
1574
  provider: providerMeta,
1575
+ cliType,
1077
1576
  meshId,
1078
1577
  workspace,
1079
1578
  });
@@ -1101,7 +1600,8 @@ export class DaemonCommandRouter {
1101
1600
  };
1102
1601
  }
1103
1602
 
1104
- if (coordinatorSetup.configFormat !== 'claude_mcp_json') {
1603
+ const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
1604
+ if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
1105
1605
  return {
1106
1606
  success: false,
1107
1607
  code: 'mesh_coordinator_unsupported',
@@ -1112,19 +1612,51 @@ export class DaemonCommandRouter {
1112
1612
  };
1113
1613
  }
1114
1614
 
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;
1615
+ // Build the coordinator prompt before mutating workspace config or launching.
1616
+ // Prompt generation failures are configuration/data-shape errors; fail closed so
1617
+ // broken mesh state is visible instead of silently launching with weaker rules.
1618
+ let systemPrompt = '';
1619
+ try {
1620
+ systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
1621
+ } catch (error: any) {
1622
+ const message = error?.message || String(error);
1623
+ LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
1624
+ return {
1625
+ success: false,
1626
+ code: 'mesh_coordinator_prompt_failed',
1627
+ error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
1628
+ meshId,
1629
+ cliType,
1630
+ workspace,
1631
+ };
1632
+ }
1118
1633
 
1119
- // Backup existing MCP config if present.
1120
- const hadExistingMcpConfig = existsSync(mcpConfigPath);
1121
- let existingMcpConfig: any = {};
1122
- if (hadExistingMcpConfig) {
1634
+ // 1. Write provider-declared MCP config for CLIs that auto-import it.
1635
+ const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
1636
+ const { dirname } = await import('path');
1637
+ const mcpConfigPath = coordinatorSetup.configPath;
1638
+ const hermesManualFallback = cliType === 'hermes-cli' && configFormat === 'hermes_config_yaml'
1639
+ ? createHermesManualMeshCoordinatorSetup(meshId, workspace)
1640
+ : null;
1641
+ let hermesBaseConfig: { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } | null = null;
1642
+ if (hermesManualFallback) {
1123
1643
  try {
1124
- existingMcpConfig = JSON.parse(readFileSync(mcpConfigPath, 'utf-8'));
1125
- copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
1126
- } catch { /* empty */ }
1644
+ hermesBaseConfig = loadHermesCoordinatorBaseConfig(mcpConfigPath);
1645
+ } catch (error: any) {
1646
+ const message = `Failed to parse Hermes base config for automatic coordinator setup: ${error?.message || error}`;
1647
+ LOG.error('MeshCoordinator', message);
1648
+ return { success: false, code: 'mesh_coordinator_config_parse_failed', error: message, meshId, cliType, workspace };
1649
+ }
1127
1650
  }
1651
+ const returnManualFallback = (message: string) => ({
1652
+ success: false,
1653
+ code: 'mesh_coordinator_manual_mcp_setup_required',
1654
+ error: message,
1655
+ meshId,
1656
+ cliType,
1657
+ workspace,
1658
+ meshCoordinatorSetup: hermesManualFallback,
1659
+ });
1128
1660
 
1129
1661
  // Merge ADHDev mesh server into existing config.
1130
1662
  // Pass full mesh data as env var so the MCP server can bootstrap
@@ -1139,39 +1671,81 @@ export class DaemonCommandRouter {
1139
1671
  ADHDEV_MCP_TRANSPORT: 'ipc',
1140
1672
  };
1141
1673
  }
1674
+
1675
+ try {
1676
+ mkdirSync(dirname(mcpConfigPath), { recursive: true });
1677
+ } catch (error: any) {
1678
+ const message = `Could not prepare MCP config path for automatic setup: ${error?.message || error}`;
1679
+ LOG.error('MeshCoordinator', message);
1680
+ if (hermesManualFallback) return returnManualFallback(message);
1681
+ return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
1682
+ }
1683
+
1684
+ // Backup existing MCP config if present.
1685
+ const hadExistingMcpConfig = existsSync(mcpConfigPath);
1686
+ let existingMcpConfig: Record<string, any> = hermesBaseConfig?.config || {};
1687
+ if (hermesBaseConfig) {
1688
+ copyHermesCoordinatorCredentialFiles(hermesBaseConfig.sourceHome, dirname(mcpConfigPath));
1689
+ }
1690
+ if (hadExistingMcpConfig) {
1691
+ try {
1692
+ const parsedExistingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
1693
+ existingMcpConfig = { ...existingMcpConfig, ...parsedExistingMcpConfig };
1694
+ copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
1695
+ } catch (error: any) {
1696
+ LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
1697
+ return {
1698
+ success: false,
1699
+ code: 'mesh_coordinator_config_parse_failed',
1700
+ error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
1701
+ };
1702
+ }
1703
+ }
1704
+
1705
+ const mcpServersKey = getMcpServersKey(configFormat);
1706
+ const existingServers = existingMcpConfig[mcpServersKey];
1142
1707
  const mcpConfig = {
1143
1708
  ...existingMcpConfig,
1144
- mcpServers: {
1145
- ...(existingMcpConfig.mcpServers || {}),
1709
+ [mcpServersKey]: {
1710
+ ...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
1146
1711
  [coordinatorSetup.serverName]: mcpServerEntry,
1147
1712
  },
1148
1713
  };
1149
- writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');
1150
- LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
1151
-
1152
- // 2. Build coordinator system prompt
1153
- let systemPrompt = '';
1154
1714
  try {
1155
- systemPrompt = buildCoordinatorSystemPrompt({ mesh });
1156
- } catch {
1157
- 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).`;
1715
+ writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
1716
+ } catch (error: any) {
1717
+ const message = `Could not write MCP config for automatic setup: ${error?.message || error}`;
1718
+ LOG.error('MeshCoordinator', message);
1719
+ if (hermesManualFallback) return returnManualFallback(message);
1720
+ return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
1158
1721
  }
1722
+ LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
1159
1723
 
1160
1724
  const cliArgs: string[] = [];
1725
+ const launchEnv: Record<string, string> = {};
1726
+ if (configFormat === 'hermes_config_yaml') {
1727
+ launchEnv.HERMES_HOME = dirname(mcpConfigPath);
1728
+ launchEnv.HERMES_IGNORE_USER_CONFIG = '';
1729
+ }
1161
1730
  if (systemPrompt) {
1162
- cliArgs.push('--append-system-prompt', systemPrompt);
1731
+ if (configFormat === 'hermes_config_yaml') {
1732
+ launchEnv.HERMES_EPHEMERAL_SYSTEM_PROMPT = systemPrompt;
1733
+ } else {
1734
+ cliArgs.push('--append-system-prompt', systemPrompt);
1735
+ }
1163
1736
  }
1164
1737
  if (cliType === 'claude-cli') {
1165
1738
  cliArgs.push('--mcp-config', coordinatorSetup.configPath);
1166
1739
  }
1167
1740
 
1168
- // 3. Launch CLI session via existing cliManager
1169
- // Pass coordinator system prompt via --append-system-prompt so the
1170
- // CLI inherits its default behavior AND knows it is a mesh coordinator.
1741
+ // 3. Launch CLI session via existing cliManager.
1742
+ // Provider-specific prompt injection remains fail-closed: Claude gets
1743
+ // explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
1171
1744
  const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
1172
1745
  cliType,
1173
1746
  dir: workspace,
1174
1747
  cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
1748
+ env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
1175
1749
  settings: {
1176
1750
  meshCoordinatorFor: meshId
1177
1751
  }