@adhdev/daemon-core 0.9.76-rc.3 → 0.9.76-rc.31

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 (42) 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 +4 -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 +1058 -384
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +1085 -416
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/mesh/coordinator-prompt.d.ts +1 -0
  15. package/dist/providers/cli-provider-instance.d.ts +3 -0
  16. package/dist/providers/provider-instance-manager.d.ts +1 -0
  17. package/dist/providers/provider-instance.d.ts +2 -0
  18. package/dist/repo-mesh-types.d.ts +6 -0
  19. package/dist/shared-types.d.ts +22 -1
  20. package/package.json +3 -4
  21. package/src/cli-adapters/provider-cli-adapter.ts +6 -3
  22. package/src/cli-adapters/provider-cli-runtime.ts +3 -2
  23. package/src/commands/chat-commands.ts +50 -5
  24. package/src/commands/cli-manager.ts +78 -5
  25. package/src/commands/handler.ts +13 -4
  26. package/src/commands/mesh-coordinator.ts +149 -6
  27. package/src/commands/router.ts +319 -32
  28. package/src/config/mesh-config.ts +6 -0
  29. package/src/git/git-commands.ts +5 -1
  30. package/src/git/git-types.ts +1 -0
  31. package/src/git/git-worktree.ts +214 -0
  32. package/src/git/index.ts +14 -0
  33. package/src/mesh/coordinator-prompt.ts +25 -10
  34. package/src/mesh/mesh-events.ts +40 -17
  35. package/src/providers/cli-provider-instance.d.ts +2 -0
  36. package/src/providers/cli-provider-instance.ts +55 -7
  37. package/src/providers/provider-instance-manager.ts +20 -1
  38. package/src/providers/provider-instance.ts +2 -0
  39. package/src/repo-mesh-types.ts +6 -0
  40. package/src/shared-types.ts +24 -1
  41. package/src/status/builders.ts +17 -12
  42. package/src/status/reporter.ts +6 -0
@@ -1,6 +1,8 @@
1
- import { existsSync, realpathSync } from 'node:fs'
1
+ import { execFileSync } from 'node:child_process'
2
+ import { existsSync, readdirSync, realpathSync } from 'node:fs'
2
3
  import { createRequire } from 'node:module'
3
- import { dirname, join, resolve } from 'node:path'
4
+ import * as os from 'node:os'
5
+ import { dirname, isAbsolute, join, resolve } from 'node:path'
4
6
  import type { ProviderModule, MeshCoordinatorMcpConfigFormat } from '../providers/contracts.js'
5
7
 
6
8
  export interface MeshCoordinatorMcpServerLaunch {
@@ -32,6 +34,7 @@ export type MeshCoordinatorSetup =
32
34
 
33
35
  export interface ResolveMeshCoordinatorSetupOptions {
34
36
  provider?: ProviderModule | null
37
+ cliType?: string
35
38
  meshId: string
36
39
  workspace: string
37
40
  adhdevMcpCommand?: string
@@ -41,6 +44,58 @@ export interface ResolveMeshCoordinatorSetupOptions {
41
44
 
42
45
  const DEFAULT_SERVER_NAME = 'adhdev-mesh'
43
46
  const DEFAULT_ADHDEV_MCP_COMMAND = 'adhdev-mcp'
47
+ const HERMES_CLI_TYPE = 'hermes-cli'
48
+ const HERMES_MCP_CONFIG_PATH = '~/.hermes/config.yaml'
49
+
50
+ function isHermesProvider(provider: ProviderModule | null | undefined, cliType?: string): boolean {
51
+ const type = cliType?.trim() || provider?.type?.trim() || ''
52
+ return type === HERMES_CLI_TYPE
53
+ }
54
+
55
+ function resolveHermesMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetupOptions): MeshCoordinatorSetup {
56
+ const mcpServer = resolveAdhdevMcpServerLaunch({
57
+ meshId: options.meshId,
58
+ nodeExecutable: options.nodeExecutable,
59
+ adhdevMcpEntryPath: options.adhdevMcpEntryPath,
60
+ })
61
+ if (!mcpServer) {
62
+ return {
63
+ kind: 'unsupported',
64
+ reason: 'Could not resolve the ADHDev MCP server entrypoint and a Node runtime with WebSocket support for daemon IPC mode',
65
+ }
66
+ }
67
+ const configPath = resolveMcpConfigPath(HERMES_MCP_CONFIG_PATH, options.workspace)
68
+ if (!configPath.trim()) {
69
+ return createHermesManualMeshCoordinatorSetup(options.meshId, options.workspace)
70
+ }
71
+ return {
72
+ kind: 'auto_import',
73
+ serverName: DEFAULT_SERVER_NAME,
74
+ configPath,
75
+ configFormat: 'hermes_config_yaml',
76
+ mcpServer,
77
+ }
78
+ }
79
+
80
+ export function createHermesManualMeshCoordinatorSetup(meshId: string, workspace: string): MeshCoordinatorSetup {
81
+ return {
82
+ kind: 'manual',
83
+ serverName: DEFAULT_SERVER_NAME,
84
+ configFormat: 'hermes_config_yaml',
85
+ configPathCommand: HERMES_MCP_CONFIG_PATH,
86
+ requiresRestart: true,
87
+ instructions: 'Hermes CLI does not auto-import repo-local .mcp.json. Add this MCP server to Hermes config under mcp_servers, then start a fresh Hermes session.',
88
+ template: renderMeshCoordinatorTemplate(
89
+ 'mcp_servers:\n {{serverName}}:\n command: {{adhdevMcpCommand}}\n args:\n - --repo-mesh\n - {{meshId}}\n enabled: true\n',
90
+ {
91
+ meshId,
92
+ workspace,
93
+ serverName: DEFAULT_SERVER_NAME,
94
+ adhdevMcpCommand: DEFAULT_ADHDEV_MCP_COMMAND,
95
+ },
96
+ ),
97
+ }
98
+ }
44
99
 
45
100
  export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetupOptions): MeshCoordinatorSetup {
46
101
  const { provider, meshId, workspace } = options
@@ -52,6 +107,10 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
52
107
  }
53
108
  }
54
109
 
110
+ if (isHermesProvider(provider, options.cliType)) {
111
+ return resolveHermesMeshCoordinatorSetup(options)
112
+ }
113
+
55
114
  const mcpConfig = config.mcpConfig
56
115
  if (!mcpConfig || mcpConfig.mode === 'none') {
57
116
  return {
@@ -74,13 +133,13 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
74
133
  if (!mcpServer) {
75
134
  return {
76
135
  kind: 'unsupported',
77
- reason: 'Could not resolve the ADHDev MCP server entrypoint without relying on a PATH bin shim',
136
+ reason: 'Could not resolve the ADHDev MCP server entrypoint and a Node runtime with WebSocket support for daemon IPC mode',
78
137
  }
79
138
  }
80
139
  return {
81
140
  kind: 'auto_import',
82
141
  serverName,
83
- configPath: join(workspace, path),
142
+ configPath: resolveMcpConfigPath(path, workspace),
84
143
  configFormat: mcpConfig.format,
85
144
  mcpServer,
86
145
  }
@@ -118,6 +177,14 @@ function renderMeshCoordinatorTemplate(template: string, values: Record<string,
118
177
  return template.replace(/\{\{\s*(meshId|workspace|serverName|adhdevMcpCommand)\s*\}\}/g, (_, key: string) => values[key] || '')
119
178
  }
120
179
 
180
+ function resolveMcpConfigPath(configPath: string, workspace: string): string {
181
+ const trimmed = configPath.trim()
182
+ if (trimmed === '~') return os.homedir()
183
+ if (trimmed.startsWith('~/')) return join(os.homedir(), trimmed.slice(2))
184
+ if (isAbsolute(trimmed)) return trimmed
185
+ return join(workspace, trimmed)
186
+ }
187
+
121
188
  function resolveAdhdevMcpServerLaunch(options: {
122
189
  meshId: string
123
190
  nodeExecutable?: string
@@ -125,9 +192,85 @@ function resolveAdhdevMcpServerLaunch(options: {
125
192
  }): MeshCoordinatorMcpServerLaunch | null {
126
193
  const entryPath = resolveAdhdevMcpEntryPath(options.adhdevMcpEntryPath)
127
194
  if (!entryPath) return null
195
+ const nodeExecutable = resolveMcpNodeExecutable(options.nodeExecutable)
196
+ if (!nodeExecutable) return null
128
197
  return {
129
- command: options.nodeExecutable?.trim() || process.execPath,
130
- args: [entryPath, '--repo-mesh', options.meshId],
198
+ command: nodeExecutable,
199
+ args: [entryPath, '--mode', 'ipc', '--repo-mesh', options.meshId],
200
+ }
201
+ }
202
+
203
+ function resolveMcpNodeExecutable(explicitExecutable?: string): string | null {
204
+ const explicit = explicitExecutable?.trim()
205
+ if (explicit) return explicit
206
+
207
+ const candidates: string[] = []
208
+ const addCandidate = (candidate?: string | null) => {
209
+ const trimmed = candidate?.trim()
210
+ if (!trimmed) return
211
+ const normalized = normalizeExistingPath(trimmed) || trimmed
212
+ if (!candidates.includes(normalized)) candidates.push(normalized)
213
+ }
214
+
215
+ addCandidate(process.env.ADHDEV_MCP_NODE_EXECUTABLE)
216
+ addCandidate(process.env.ADHDEV_NODE_EXECUTABLE)
217
+ addCandidate(process.env.npm_node_execpath)
218
+ addNodeCandidatesFromPath(process.env.PATH, addCandidate)
219
+ addNodeCandidatesFromNvm(os.homedir(), addCandidate)
220
+ addCandidate('/opt/homebrew/bin/node')
221
+ addCandidate('/usr/local/bin/node')
222
+ addCandidate('/usr/bin/node')
223
+ addCandidate(process.execPath)
224
+
225
+ for (const candidate of candidates) {
226
+ if (nodeRuntimeSupportsWebSocket(candidate)) return candidate
227
+ }
228
+ return null
229
+ }
230
+
231
+ function addNodeCandidatesFromPath(pathValue: string | undefined, addCandidate: (candidate?: string | null) => void) {
232
+ for (const entry of (pathValue || '').split(':')) {
233
+ const dir = entry.trim()
234
+ if (!dir) continue
235
+ addCandidate(join(dir, 'node'))
236
+ }
237
+ }
238
+
239
+ function addNodeCandidatesFromNvm(homeDir: string, addCandidate: (candidate?: string | null) => void) {
240
+ const versionsDir = join(homeDir, '.nvm', 'versions', 'node')
241
+ try {
242
+ const versionDirs = readdirSync(versionsDir, { withFileTypes: true })
243
+ .filter((entry) => entry.isDirectory())
244
+ .map((entry) => entry.name)
245
+ .sort(compareNodeVersionNamesDescending)
246
+ for (const versionDir of versionDirs) {
247
+ addCandidate(join(versionsDir, versionDir, 'bin', 'node'))
248
+ }
249
+ } catch {
250
+ // nvm is optional; PATH and process.execPath candidates still cover normal installs.
251
+ }
252
+ }
253
+
254
+ function compareNodeVersionNamesDescending(a: string, b: string): number {
255
+ const parse = (value: string) => value.replace(/^v/, '').split('.').map((part) => Number.parseInt(part, 10) || 0)
256
+ const left = parse(a)
257
+ const right = parse(b)
258
+ for (let i = 0; i < Math.max(left.length, right.length); i++) {
259
+ const diff = (right[i] || 0) - (left[i] || 0)
260
+ if (diff !== 0) return diff
261
+ }
262
+ return b.localeCompare(a)
263
+ }
264
+
265
+ function nodeRuntimeSupportsWebSocket(nodeExecutable: string): boolean {
266
+ try {
267
+ execFileSync(nodeExecutable, ['-e', "process.exit(typeof WebSocket === 'function' ? 0 : 42)"], {
268
+ stdio: 'ignore',
269
+ timeout: 3000,
270
+ })
271
+ return true
272
+ } catch {
273
+ return false
131
274
  }
132
275
  }
133
276
 
@@ -30,10 +30,11 @@ 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';
38
39
  import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
39
40
  import { getSessionCompletionMarker } from '../status/snapshot.js';
@@ -61,8 +62,78 @@ function resolveUpgradeChannel(args: any): ReleaseChannel {
61
62
  || normalizeReleaseChannel(loadConfig().updateChannel)
62
63
  || 'stable';
63
64
  }
65
+
66
+ function readProviderPriorityFromPolicy(policy: unknown): string[] {
67
+ const record = policy && typeof policy === 'object' && !Array.isArray(policy)
68
+ ? policy as Record<string, unknown>
69
+ : {};
70
+ const raw = record.providerPriority;
71
+ if (!Array.isArray(raw)) return [];
72
+ const seen = new Set<string>();
73
+ return raw
74
+ .map(type => typeof type === 'string' ? type.trim() : '')
75
+ .filter(Boolean)
76
+ .filter(type => {
77
+ if (seen.has(type)) return false;
78
+ seen.add(type);
79
+ return true;
80
+ });
81
+ }
82
+
83
+ async function resolveProviderTypeFromPriority(args: {
84
+ nodeId: string;
85
+ providerPriority: string[];
86
+ providerLoader: ProviderLoader;
87
+ onStatusChange?: () => void;
88
+ }): Promise<{ providerType?: string; error?: string }> {
89
+ if (!args.providerPriority.length) {
90
+ return { error: `Node '${args.nodeId}' has no providerPriority policy; pass cliType explicitly or configure node.policy.providerPriority` };
91
+ }
92
+
93
+ const failed: string[] = [];
94
+ for (const requestedType of args.providerPriority) {
95
+ const normalizedType = args.providerLoader.resolveAlias(requestedType);
96
+ if (!args.providerLoader.isMachineProviderEnabled(normalizedType)) {
97
+ failed.push(`${requestedType}: disabled`);
98
+ continue;
99
+ }
100
+ const detected = await detectCLI(normalizedType, args.providerLoader, { includeVersion: false });
101
+ args.providerLoader.setCliDetectionResults([{
102
+ id: normalizedType,
103
+ installed: !!detected,
104
+ path: detected?.path,
105
+ }], false);
106
+ args.onStatusChange?.();
107
+ if (detected) return { providerType: normalizedType };
108
+ failed.push(`${requestedType}: not detected`);
109
+ }
110
+
111
+ return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join('; ')}` };
112
+ }
64
113
  import * as fs from 'fs';
65
114
 
115
+ type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
116
+
117
+ function loadYamlModule(): { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string } {
118
+ return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
119
+ }
120
+
121
+ function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
122
+ return format === 'hermes_config_yaml' ? 'mcp_servers' : 'mcpServers';
123
+ }
124
+
125
+ function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
126
+ if (!text.trim()) return {};
127
+ if (format === 'claude_mcp_json') return JSON.parse(text);
128
+ const parsed = loadYamlModule().load(text);
129
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
130
+ }
131
+
132
+ function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
133
+ if (format === 'claude_mcp_json') return JSON.stringify(config, null, 2);
134
+ return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
135
+ }
136
+
66
137
  // ─── Types ───
67
138
 
68
139
  export interface SessionHostControlPlane {
@@ -226,6 +297,43 @@ export class DaemonCommandRouter {
226
297
  this.deps = deps;
227
298
  }
228
299
 
300
+ private getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
301
+ if (inlineMesh && typeof inlineMesh === 'object') {
302
+ this.inlineMeshCache.set(meshId, inlineMesh as any);
303
+ return inlineMesh as any;
304
+ }
305
+ return this.inlineMeshCache.get(meshId);
306
+ }
307
+
308
+ private async getMeshForCommand(meshId: string, inlineMesh?: unknown): Promise<{ mesh: any; inline: boolean } | null> {
309
+ try {
310
+ const { getMesh } = await import('../config/mesh-config.js');
311
+ const mesh = getMesh(meshId);
312
+ if (mesh) return { mesh, inline: false };
313
+ } catch { /* fall through to inline cache */ }
314
+ const cached = this.getCachedInlineMesh(meshId, inlineMesh);
315
+ return cached ? { mesh: cached, inline: true } : null;
316
+ }
317
+
318
+ private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
319
+ if (!mesh || !Array.isArray(mesh.nodes) || !node?.id) return;
320
+ const idx = mesh.nodes.findIndex((entry: any) => entry?.id === node.id || entry?.nodeId === node.id);
321
+ if (idx >= 0) mesh.nodes[idx] = node;
322
+ else mesh.nodes.push(node);
323
+ mesh.updatedAt = new Date().toISOString();
324
+ this.inlineMeshCache.set(meshId, mesh);
325
+ }
326
+
327
+ private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
328
+ if (!mesh || !Array.isArray(mesh.nodes)) return false;
329
+ const idx = mesh.nodes.findIndex((entry: any) => entry?.id === nodeId || entry?.nodeId === nodeId);
330
+ if (idx === -1) return false;
331
+ mesh.nodes.splice(idx, 1);
332
+ mesh.updatedAt = new Date().toISOString();
333
+ this.inlineMeshCache.set(meshId, mesh);
334
+ return true;
335
+ }
336
+
229
337
  private async traceSessionHostAction<T>(
230
338
  action: string,
231
339
  args: any,
@@ -1009,7 +1117,15 @@ export class DaemonCommandRouter {
1009
1117
  if (!workspace) return { success: false, error: 'workspace required' };
1010
1118
  try {
1011
1119
  const { addNode } = await import('../config/mesh-config.js');
1012
- const node = addNode(meshId, { workspace });
1120
+ const providerPriority = Array.isArray(args?.providerPriority)
1121
+ ? args.providerPriority.map((type: any) => typeof type === 'string' ? type.trim() : '').filter(Boolean)
1122
+ : [];
1123
+ const readOnly = args?.readOnly === true;
1124
+ const policy = {
1125
+ ...(readOnly ? { readOnly: true } : {}),
1126
+ ...(providerPriority.length ? { providerPriority } : {}),
1127
+ };
1128
+ const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}) });
1013
1129
  if (!node) return { success: false, error: 'Mesh not found' };
1014
1130
  return { success: true, node };
1015
1131
  } catch (e: any) {
@@ -1022,18 +1138,111 @@ export class DaemonCommandRouter {
1022
1138
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
1023
1139
  if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
1024
1140
  try {
1025
- const { removeNode } = await import('../config/mesh-config.js');
1026
- const removed = removeNode(meshId, nodeId);
1141
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
1142
+ const mesh = meshRecord?.mesh;
1143
+ const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
1144
+
1145
+ // If this is a worktree node, clean up the git worktree first
1146
+ if (node?.isLocalWorktree && node.workspace) {
1147
+ try {
1148
+ const sourceNode = node.clonedFromNodeId
1149
+ ? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
1150
+ : mesh?.nodes.find((n: any) => !n.isLocalWorktree);
1151
+ const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
1152
+ if (repoRoot) {
1153
+ const { removeWorktree } = await import('../git/git-worktree.js');
1154
+ await removeWorktree(repoRoot, node.workspace);
1155
+ }
1156
+ } catch (e: any) {
1157
+ LOG.warn('MeshNode', `Worktree cleanup failed for ${nodeId}: ${e.message}`);
1158
+ // Continue with node removal even if worktree cleanup fails
1159
+ }
1160
+ }
1161
+
1162
+ let removed = false;
1163
+ if (meshRecord?.inline) {
1164
+ removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
1165
+ } else {
1166
+ const { removeNode } = await import('../config/mesh-config.js');
1167
+ removed = removeNode(meshId, nodeId);
1168
+ }
1027
1169
  return { success: true, removed };
1028
1170
  } catch (e: any) {
1029
1171
  return { success: false, error: e.message };
1030
1172
  }
1031
1173
  }
1032
1174
 
1175
+ case 'clone_mesh_node': {
1176
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1177
+ const sourceNodeId = typeof args?.sourceNodeId === 'string' ? args.sourceNodeId.trim() : '';
1178
+ const branch = typeof args?.branch === 'string' ? args.branch.trim() : '';
1179
+ const baseBranch = typeof args?.baseBranch === 'string' ? args.baseBranch.trim() : undefined;
1180
+ if (!meshId) return { success: false, error: 'meshId required' };
1181
+ if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
1182
+ if (!branch) return { success: false, error: 'branch required' };
1183
+
1184
+ try {
1185
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
1186
+ const mesh = meshRecord?.mesh;
1187
+ if (!mesh) return { success: false, error: 'Mesh not found' };
1188
+
1189
+ const sourceNode = mesh.nodes?.find((n: any) => n.id === sourceNodeId || n.nodeId === sourceNodeId);
1190
+ if (!sourceNode) return { success: false, error: `Source node '${sourceNodeId}' not found in mesh` };
1191
+
1192
+ const repoRoot = sourceNode.repoRoot || sourceNode.workspace;
1193
+ const { createWorktree } = await import('../git/git-worktree.js');
1194
+ const result = await createWorktree({
1195
+ repoRoot,
1196
+ branch,
1197
+ baseBranch,
1198
+ meshName: mesh.name,
1199
+ });
1200
+
1201
+ let node: any;
1202
+ if (meshRecord.inline) {
1203
+ const { randomUUID } = await import('crypto');
1204
+ node = {
1205
+ id: `node_${randomUUID().replace(/-/g, '')}`,
1206
+ workspace: result.worktreePath,
1207
+ repoRoot: result.worktreePath,
1208
+ daemonId: sourceNode.daemonId,
1209
+ userOverrides: { ...(sourceNode.userOverrides || {}) },
1210
+ policy: { ...(sourceNode.policy || {}) },
1211
+ isLocalWorktree: true,
1212
+ worktreeBranch: result.branch,
1213
+ clonedFromNodeId: sourceNodeId,
1214
+ };
1215
+ this.updateInlineMeshNode(meshId, mesh, node);
1216
+ } else {
1217
+ const { addNode } = await import('../config/mesh-config.js');
1218
+ node = addNode(meshId, {
1219
+ workspace: result.worktreePath,
1220
+ repoRoot: result.worktreePath,
1221
+ daemonId: sourceNode.daemonId,
1222
+ userOverrides: { ...(sourceNode.userOverrides || {}) },
1223
+ isLocalWorktree: true,
1224
+ worktreeBranch: result.branch,
1225
+ clonedFromNodeId: sourceNodeId,
1226
+ policy: { ...(sourceNode.policy || {}) },
1227
+ });
1228
+ if (!node) return { success: false, error: 'Failed to register worktree node' };
1229
+ }
1230
+
1231
+ return {
1232
+ success: true,
1233
+ node,
1234
+ worktreePath: result.worktreePath,
1235
+ branch: result.branch,
1236
+ };
1237
+ } catch (e: any) {
1238
+ return { success: false, error: e.message };
1239
+ }
1240
+ }
1241
+
1033
1242
  // ─── Mesh Coordinator Launch ───
1034
1243
  case 'launch_mesh_coordinator': {
1035
1244
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1036
- const cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : 'claude-cli';
1245
+ let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
1037
1246
  if (!meshId) return { success: false, error: 'meshId required' };
1038
1247
 
1039
1248
  try {
@@ -1071,9 +1280,29 @@ export class DaemonCommandRouter {
1071
1280
  }
1072
1281
  const workspace = typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '';
1073
1282
  if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
1283
+ if (!cliType) {
1284
+ const resolved = await resolveProviderTypeFromPriority({
1285
+ nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || 'coordinator'),
1286
+ providerPriority: readProviderPriorityFromPolicy(coordinatorNode.policy),
1287
+ providerLoader: this.deps.providerLoader,
1288
+ onStatusChange: this.deps.onStatusChange,
1289
+ });
1290
+ if (!resolved.providerType) {
1291
+ return {
1292
+ success: false,
1293
+ code: 'mesh_coordinator_provider_priority_unusable',
1294
+ error: resolved.error || 'No usable provider found from node providerPriority',
1295
+ meshId,
1296
+ cliType,
1297
+ workspace,
1298
+ };
1299
+ }
1300
+ cliType = resolved.providerType;
1301
+ }
1074
1302
  const providerMeta = this.deps.providerLoader.resolve?.(cliType) || this.deps.providerLoader.getMeta(cliType);
1075
1303
  const coordinatorSetup = resolveMeshCoordinatorSetup({
1076
1304
  provider: providerMeta,
1305
+ cliType,
1077
1306
  meshId,
1078
1307
  workspace,
1079
1308
  });
@@ -1101,7 +1330,8 @@ export class DaemonCommandRouter {
1101
1330
  };
1102
1331
  }
1103
1332
 
1104
- if (coordinatorSetup.configFormat !== 'claude_mcp_json') {
1333
+ const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
1334
+ if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
1105
1335
  return {
1106
1336
  success: false,
1107
1337
  code: 'mesh_coordinator_unsupported',
@@ -1112,20 +1342,42 @@ export class DaemonCommandRouter {
1112
1342
  };
1113
1343
  }
1114
1344
 
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 */ }
1345
+ // Build the coordinator prompt before mutating workspace config or launching.
1346
+ // Prompt generation failures are configuration/data-shape errors; fail closed so
1347
+ // broken mesh state is visible instead of silently launching with weaker rules.
1348
+ let systemPrompt = '';
1349
+ try {
1350
+ systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
1351
+ } catch (error: any) {
1352
+ const message = error?.message || String(error);
1353
+ LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
1354
+ return {
1355
+ success: false,
1356
+ code: 'mesh_coordinator_prompt_failed',
1357
+ error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
1358
+ meshId,
1359
+ cliType,
1360
+ workspace,
1361
+ };
1127
1362
  }
1128
1363
 
1364
+ // 1. Write provider-declared MCP config for CLIs that auto-import it.
1365
+ const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
1366
+ const { dirname } = await import('path');
1367
+ const mcpConfigPath = coordinatorSetup.configPath;
1368
+ const hermesManualFallback = cliType === 'hermes-cli' && configFormat === 'hermes_config_yaml'
1369
+ ? createHermesManualMeshCoordinatorSetup(meshId, workspace)
1370
+ : null;
1371
+ const returnManualFallback = (message: string) => ({
1372
+ success: false,
1373
+ code: 'mesh_coordinator_manual_mcp_setup_required',
1374
+ error: message,
1375
+ meshId,
1376
+ cliType,
1377
+ workspace,
1378
+ meshCoordinatorSetup: hermesManualFallback,
1379
+ });
1380
+
1129
1381
  // Merge ADHDev mesh server into existing config.
1130
1382
  // Pass full mesh data as env var so the MCP server can bootstrap
1131
1383
  // without depending on meshes.json or a running daemon.
@@ -1136,41 +1388,76 @@ export class DaemonCommandRouter {
1136
1388
  if (args?.inlineMesh) {
1137
1389
  mcpServerEntry.env = {
1138
1390
  ADHDEV_INLINE_MESH: JSON.stringify(mesh),
1391
+ ADHDEV_MCP_TRANSPORT: 'ipc',
1139
1392
  };
1140
1393
  }
1394
+
1395
+ try {
1396
+ mkdirSync(dirname(mcpConfigPath), { recursive: true });
1397
+ } catch (error: any) {
1398
+ const message = `Could not prepare MCP config path for automatic setup: ${error?.message || error}`;
1399
+ LOG.error('MeshCoordinator', message);
1400
+ if (hermesManualFallback) return returnManualFallback(message);
1401
+ return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
1402
+ }
1403
+
1404
+ // Backup existing MCP config if present.
1405
+ const hadExistingMcpConfig = existsSync(mcpConfigPath);
1406
+ let existingMcpConfig: Record<string, any> = {};
1407
+ if (hadExistingMcpConfig) {
1408
+ try {
1409
+ existingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
1410
+ copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
1411
+ } catch (error: any) {
1412
+ LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
1413
+ return {
1414
+ success: false,
1415
+ code: 'mesh_coordinator_config_parse_failed',
1416
+ error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
1417
+ };
1418
+ }
1419
+ }
1420
+
1421
+ const mcpServersKey = getMcpServersKey(configFormat);
1422
+ const existingServers = existingMcpConfig[mcpServersKey];
1141
1423
  const mcpConfig = {
1142
1424
  ...existingMcpConfig,
1143
- mcpServers: {
1144
- ...(existingMcpConfig.mcpServers || {}),
1425
+ [mcpServersKey]: {
1426
+ ...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
1145
1427
  [coordinatorSetup.serverName]: mcpServerEntry,
1146
1428
  },
1147
1429
  };
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
1430
  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).`;
1431
+ writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
1432
+ } catch (error: any) {
1433
+ const message = `Could not write MCP config for automatic setup: ${error?.message || error}`;
1434
+ LOG.error('MeshCoordinator', message);
1435
+ if (hermesManualFallback) return returnManualFallback(message);
1436
+ return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
1157
1437
  }
1438
+ LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
1158
1439
 
1159
1440
  const cliArgs: string[] = [];
1441
+ const launchEnv: Record<string, string> = {};
1160
1442
  if (systemPrompt) {
1161
- cliArgs.push('--append-system-prompt', systemPrompt);
1443
+ if (configFormat === 'hermes_config_yaml') {
1444
+ launchEnv.HERMES_EPHEMERAL_SYSTEM_PROMPT = systemPrompt;
1445
+ } else {
1446
+ cliArgs.push('--append-system-prompt', systemPrompt);
1447
+ }
1162
1448
  }
1163
1449
  if (cliType === 'claude-cli') {
1164
1450
  cliArgs.push('--mcp-config', coordinatorSetup.configPath);
1165
1451
  }
1166
1452
 
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.
1453
+ // 3. Launch CLI session via existing cliManager.
1454
+ // Provider-specific prompt injection remains fail-closed: Claude gets
1455
+ // explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
1170
1456
  const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
1171
1457
  cliType,
1172
1458
  dir: workspace,
1173
1459
  cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
1460
+ env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
1174
1461
  settings: {
1175
1462
  meshCoordinatorFor: meshId
1176
1463
  }