@adhdev/daemon-core 0.9.76-rc.10 → 0.9.76-rc.12

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.
@@ -14,5 +14,6 @@ export interface CoordinatorPromptContext {
14
14
  mesh: LocalMeshEntry;
15
15
  status?: RepoMeshStatus;
16
16
  userInstruction?: string;
17
+ coordinatorCliType?: string;
17
18
  }
18
19
  export declare function buildCoordinatorSystemPrompt(ctx: CoordinatorPromptContext): string;
@@ -67,6 +67,7 @@ export declare class CliProviderInstance implements ProviderInstance {
67
67
  constructor(provider: ProviderModule, workingDir: string, cliArgs?: string[], instanceId?: string, transportFactory?: PtyTransportFactory, options?: {
68
68
  providerSessionId?: string;
69
69
  launchMode?: 'new' | 'resume' | 'manual';
70
+ extraEnv?: Record<string, string>;
70
71
  onProviderSessionResolved?: (info: {
71
72
  instanceId: string;
72
73
  providerType: string;
@@ -149,6 +149,10 @@ export interface LocalMeshNodeEntry {
149
149
  policy: RepoMeshNodePolicy;
150
150
  /** For single-machine mesh: same daemon, different worktree */
151
151
  isLocalWorktree?: boolean;
152
+ /** Branch this worktree tracks (set when created via clone_mesh_node) */
153
+ worktreeBranch?: string;
154
+ /** Node ID this worktree was cloned from */
155
+ clonedFromNodeId?: string;
152
156
  }
153
157
  export interface RepoMeshStatus {
154
158
  meshId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.76-rc.10",
3
+ "version": "0.9.76-rc.12",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -52,6 +52,7 @@
52
52
  "chalk": "^5.3.0",
53
53
  "chokidar": "^5.0.0",
54
54
  "conf": "^13.0.0",
55
+ "js-yaml": "^4.1.1",
55
56
  "node-pty": "^1.2.0-beta.12",
56
57
  "ws": "^8.19.0"
57
58
  },
@@ -62,6 +63,7 @@
62
63
  "@adhdev/ghostty-vt-node": "*"
63
64
  },
64
65
  "devDependencies": {
66
+ "@types/js-yaml": "^4.0.9",
65
67
  "@types/node": "^22.0.0",
66
68
  "@types/ws": "^8.18.1",
67
69
  "tsup": "^8.2.0",
@@ -422,6 +422,7 @@ export class ProviderCliAdapter implements CliAdapter {
422
422
  provider: CliProviderModule,
423
423
  workingDir: string,
424
424
  private extraArgs: string[] = [],
425
+ private extraEnv: Record<string, string> = {},
425
426
  transportFactory: PtyTransportFactory = new NodePtyTransportFactory(),
426
427
  ) {
427
428
  this.provider = provider;
@@ -523,6 +524,7 @@ export class ProviderCliAdapter implements CliAdapter {
523
524
  runtimeSettings: this.runtimeSettings,
524
525
  workingDir: this.workingDir,
525
526
  extraArgs: this.extraArgs,
527
+ extraEnv: this.extraEnv,
526
528
  });
527
529
 
528
530
  LOG.info('CLI', `[${this.cliType}] Spawning in ${this.workingDir}`);
@@ -27,8 +27,9 @@ export function resolveCliSpawnPlan(options: {
27
27
  runtimeSettings: Record<string, any>;
28
28
  workingDir: string;
29
29
  extraArgs: string[];
30
+ extraEnv?: Record<string, string>;
30
31
  }): CliSpawnPlan {
31
- const { provider, runtimeSettings, workingDir, extraArgs } = options;
32
+ const { provider, runtimeSettings, workingDir, extraArgs, extraEnv } = options;
32
33
  const { spawn: spawnConfig } = provider;
33
34
  const configuredCommand = typeof runtimeSettings.executablePath === 'string' && runtimeSettings.executablePath.trim()
34
35
  ? runtimeSettings.executablePath.trim()
@@ -65,7 +66,7 @@ export function resolveCliSpawnPlan(options: {
65
66
  shellArgs = allArgs;
66
67
  }
67
68
 
68
- const env = buildCliSpawnEnv(process.env, spawnConfig.env);
69
+ const env = buildCliSpawnEnv(process.env, { ...(spawnConfig.env || {}), ...(extraEnv || {}) });
69
70
  // Some CLI agents, notably Hermes, route their tools through TERMINAL_CWD
70
71
  // rather than process.cwd(). Keep the generic ADHDev launch workspace as
71
72
  // the single source of truth so PTY cwd and tool cwd cannot diverge.
@@ -132,6 +132,12 @@ type CliAdapterWithExtraArgs = CliAdapter & {
132
132
  extraArgs?: string[];
133
133
  };
134
134
 
135
+ type CliStartOptions = {
136
+ resumeSessionId?: string;
137
+ settingsOverride?: Record<string, any>;
138
+ extraEnv?: Record<string, string>;
139
+ };
140
+
135
141
  function isUuid(value: string): boolean {
136
142
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
137
143
  }
@@ -365,6 +371,7 @@ export class DaemonCliManager {
365
371
  runtimeId: string,
366
372
  providerSessionId?: string,
367
373
  attachExisting = false,
374
+ extraEnv?: Record<string, string>,
368
375
  ): CliAdapter {
369
376
  // cliType normalize (Resolve alias)
370
377
  const normalizedType = this.providerLoader.resolveAlias(cliType);
@@ -382,7 +389,7 @@ export class DaemonCliManager {
382
389
  providerSessionId,
383
390
  attachExisting,
384
391
  );
385
- return new ProviderCliAdapter(resolvedProvider as CliProviderModule, workingDir, cliArgs, transportFactory);
392
+ return new ProviderCliAdapter(resolvedProvider as CliProviderModule, workingDir, cliArgs, extraEnv || {}, transportFactory);
386
393
  }
387
394
 
388
395
  throw new Error(`No CLI provider found for '${cliType}'. Create a provider.js in providers/cli/${cliType}/`);
@@ -425,6 +432,7 @@ export class DaemonCliManager {
425
432
  options?: {
426
433
  providerSessionId?: string;
427
434
  launchMode?: CliLaunchMode;
435
+ extraEnv?: Record<string, string>;
428
436
  onProviderSessionResolved?: (info: {
429
437
  instanceId: string;
430
438
  providerType: string;
@@ -480,7 +488,7 @@ export class DaemonCliManager {
480
488
  workingDir: string,
481
489
  cliArgs?: string[],
482
490
  initialModel?: string,
483
- options?: { resumeSessionId?: string, settingsOverride?: Record<string, any> },
491
+ options?: CliStartOptions,
484
492
  ): Promise<{ runtimeSessionId: string; providerSessionId?: string }> {
485
493
  const trimmed = (workingDir || '').trim();
486
494
  if (!trimmed) throw new Error('working directory required');
@@ -629,6 +637,7 @@ export class DaemonCliManager {
629
637
  {
630
638
  providerSessionId: sessionBinding.providerSessionId,
631
639
  launchMode: sessionBinding.launchMode,
640
+ extraEnv: options?.extraEnv,
632
641
  onProviderSessionResolved: ({ providerSessionId, providerName, providerType, workspace }) => {
633
642
  this.persistRecentActivity({
634
643
  kind: 'cli',
@@ -651,6 +660,7 @@ export class DaemonCliManager {
651
660
  key,
652
661
  sessionBinding.providerSessionId,
653
662
  false,
663
+ options?.extraEnv,
654
664
  );
655
665
  try {
656
666
  await adapter.spawn();
@@ -904,7 +914,7 @@ export class DaemonCliManager {
904
914
  dir,
905
915
  args?.cliArgs,
906
916
  args?.initialModel,
907
- { resumeSessionId: args?.resumeSessionId, settingsOverride: args?.settings },
917
+ { resumeSessionId: args?.resumeSessionId, settingsOverride: args?.settings, extraEnv: args?.env },
908
918
  );
909
919
 
910
920
  return {
@@ -1,6 +1,7 @@
1
1
  import { existsSync, realpathSync } from 'node:fs'
2
2
  import { createRequire } from 'node:module'
3
- import { dirname, join, resolve } from 'node:path'
3
+ import * as os from 'node:os'
4
+ import { dirname, isAbsolute, join, resolve } from 'node:path'
4
5
  import type { ProviderModule, MeshCoordinatorMcpConfigFormat } from '../providers/contracts.js'
5
6
 
6
7
  export interface MeshCoordinatorMcpServerLaunch {
@@ -80,7 +81,7 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
80
81
  return {
81
82
  kind: 'auto_import',
82
83
  serverName,
83
- configPath: join(workspace, path),
84
+ configPath: resolveMcpConfigPath(path, workspace),
84
85
  configFormat: mcpConfig.format,
85
86
  mcpServer,
86
87
  }
@@ -118,6 +119,14 @@ function renderMeshCoordinatorTemplate(template: string, values: Record<string,
118
119
  return template.replace(/\{\{\s*(meshId|workspace|serverName|adhdevMcpCommand)\s*\}\}/g, (_, key: string) => values[key] || '')
119
120
  }
120
121
 
122
+ function resolveMcpConfigPath(configPath: string, workspace: string): string {
123
+ const trimmed = configPath.trim()
124
+ if (trimmed === '~') return os.homedir()
125
+ if (trimmed.startsWith('~/')) return join(os.homedir(), trimmed.slice(2))
126
+ if (isAbsolute(trimmed)) return trimmed
127
+ return join(workspace, trimmed)
128
+ }
129
+
121
130
  function resolveAdhdevMcpServerLaunch(options: {
122
131
  meshId: string
123
132
  nodeExecutable?: string
@@ -30,6 +30,7 @@ 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';
@@ -63,6 +64,28 @@ function resolveUpgradeChannel(args: any): ReleaseChannel {
63
64
  }
64
65
  import * as fs from 'fs';
65
66
 
67
+ type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
68
+
69
+ function loadYamlModule(): { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string } {
70
+ return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
71
+ }
72
+
73
+ function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
74
+ return format === 'hermes_config_yaml' ? 'mcp_servers' : 'mcpServers';
75
+ }
76
+
77
+ function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
78
+ if (!text.trim()) return {};
79
+ if (format === 'claude_mcp_json') return JSON.parse(text);
80
+ const parsed = loadYamlModule().load(text);
81
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
82
+ }
83
+
84
+ function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
85
+ if (format === 'claude_mcp_json') return JSON.stringify(config, null, 2);
86
+ return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
87
+ }
88
+
66
89
  // ─── Types ───
67
90
 
68
91
  export interface SessionHostControlPlane {
@@ -226,6 +249,43 @@ export class DaemonCommandRouter {
226
249
  this.deps = deps;
227
250
  }
228
251
 
252
+ private getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
253
+ if (inlineMesh && typeof inlineMesh === 'object') {
254
+ this.inlineMeshCache.set(meshId, inlineMesh as any);
255
+ return inlineMesh as any;
256
+ }
257
+ return this.inlineMeshCache.get(meshId);
258
+ }
259
+
260
+ private async getMeshForCommand(meshId: string, inlineMesh?: unknown): Promise<{ mesh: any; inline: boolean } | null> {
261
+ try {
262
+ const { getMesh } = await import('../config/mesh-config.js');
263
+ const mesh = getMesh(meshId);
264
+ if (mesh) return { mesh, inline: false };
265
+ } catch { /* fall through to inline cache */ }
266
+ const cached = this.getCachedInlineMesh(meshId, inlineMesh);
267
+ return cached ? { mesh: cached, inline: true } : null;
268
+ }
269
+
270
+ private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
271
+ if (!mesh || !Array.isArray(mesh.nodes) || !node?.id) return;
272
+ const idx = mesh.nodes.findIndex((entry: any) => entry?.id === node.id || entry?.nodeId === node.id);
273
+ if (idx >= 0) mesh.nodes[idx] = node;
274
+ else mesh.nodes.push(node);
275
+ mesh.updatedAt = new Date().toISOString();
276
+ this.inlineMeshCache.set(meshId, mesh);
277
+ }
278
+
279
+ private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
280
+ if (!mesh || !Array.isArray(mesh.nodes)) return false;
281
+ const idx = mesh.nodes.findIndex((entry: any) => entry?.id === nodeId || entry?.nodeId === nodeId);
282
+ if (idx === -1) return false;
283
+ mesh.nodes.splice(idx, 1);
284
+ mesh.updatedAt = new Date().toISOString();
285
+ this.inlineMeshCache.set(meshId, mesh);
286
+ return true;
287
+ }
288
+
229
289
  private async traceSessionHostAction<T>(
230
290
  action: string,
231
291
  args: any,
@@ -1022,14 +1082,107 @@ export class DaemonCommandRouter {
1022
1082
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
1023
1083
  if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
1024
1084
  try {
1025
- const { removeNode } = await import('../config/mesh-config.js');
1026
- const removed = removeNode(meshId, nodeId);
1085
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
1086
+ const mesh = meshRecord?.mesh;
1087
+ const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
1088
+
1089
+ // If this is a worktree node, clean up the git worktree first
1090
+ if (node?.isLocalWorktree && node.workspace) {
1091
+ try {
1092
+ const sourceNode = node.clonedFromNodeId
1093
+ ? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
1094
+ : mesh?.nodes.find((n: any) => !n.isLocalWorktree);
1095
+ const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
1096
+ if (repoRoot) {
1097
+ const { removeWorktree } = await import('../git/git-worktree.js');
1098
+ await removeWorktree(repoRoot, node.workspace);
1099
+ }
1100
+ } catch (e: any) {
1101
+ LOG.warn('MeshNode', `Worktree cleanup failed for ${nodeId}: ${e.message}`);
1102
+ // Continue with node removal even if worktree cleanup fails
1103
+ }
1104
+ }
1105
+
1106
+ let removed = false;
1107
+ if (meshRecord?.inline) {
1108
+ removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
1109
+ } else {
1110
+ const { removeNode } = await import('../config/mesh-config.js');
1111
+ removed = removeNode(meshId, nodeId);
1112
+ }
1027
1113
  return { success: true, removed };
1028
1114
  } catch (e: any) {
1029
1115
  return { success: false, error: e.message };
1030
1116
  }
1031
1117
  }
1032
1118
 
1119
+ case 'clone_mesh_node': {
1120
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1121
+ const sourceNodeId = typeof args?.sourceNodeId === 'string' ? args.sourceNodeId.trim() : '';
1122
+ const branch = typeof args?.branch === 'string' ? args.branch.trim() : '';
1123
+ const baseBranch = typeof args?.baseBranch === 'string' ? args.baseBranch.trim() : undefined;
1124
+ if (!meshId) return { success: false, error: 'meshId required' };
1125
+ if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
1126
+ if (!branch) return { success: false, error: 'branch required' };
1127
+
1128
+ try {
1129
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
1130
+ const mesh = meshRecord?.mesh;
1131
+ if (!mesh) return { success: false, error: 'Mesh not found' };
1132
+
1133
+ const sourceNode = mesh.nodes?.find((n: any) => n.id === sourceNodeId || n.nodeId === sourceNodeId);
1134
+ if (!sourceNode) return { success: false, error: `Source node '${sourceNodeId}' not found in mesh` };
1135
+
1136
+ const repoRoot = sourceNode.repoRoot || sourceNode.workspace;
1137
+ const { createWorktree } = await import('../git/git-worktree.js');
1138
+ const result = await createWorktree({
1139
+ repoRoot,
1140
+ branch,
1141
+ baseBranch,
1142
+ meshName: mesh.name,
1143
+ });
1144
+
1145
+ let node: any;
1146
+ if (meshRecord.inline) {
1147
+ const { randomUUID } = await import('crypto');
1148
+ node = {
1149
+ id: `node_${randomUUID().replace(/-/g, '')}`,
1150
+ workspace: result.worktreePath,
1151
+ repoRoot: result.worktreePath,
1152
+ daemonId: sourceNode.daemonId,
1153
+ userOverrides: { ...(sourceNode.userOverrides || {}) },
1154
+ policy: { ...(sourceNode.policy || {}) },
1155
+ isLocalWorktree: true,
1156
+ worktreeBranch: result.branch,
1157
+ clonedFromNodeId: sourceNodeId,
1158
+ };
1159
+ this.updateInlineMeshNode(meshId, mesh, node);
1160
+ } else {
1161
+ const { addNode } = await import('../config/mesh-config.js');
1162
+ node = addNode(meshId, {
1163
+ workspace: result.worktreePath,
1164
+ repoRoot: result.worktreePath,
1165
+ daemonId: sourceNode.daemonId,
1166
+ userOverrides: { ...(sourceNode.userOverrides || {}) },
1167
+ isLocalWorktree: true,
1168
+ worktreeBranch: result.branch,
1169
+ clonedFromNodeId: sourceNodeId,
1170
+ policy: { ...(sourceNode.policy || {}) },
1171
+ });
1172
+ if (!node) return { success: false, error: 'Failed to register worktree node' };
1173
+ }
1174
+
1175
+ return {
1176
+ success: true,
1177
+ node,
1178
+ worktreePath: result.worktreePath,
1179
+ branch: result.branch,
1180
+ };
1181
+ } catch (e: any) {
1182
+ return { success: false, error: e.message };
1183
+ }
1184
+ }
1185
+
1033
1186
  // ─── Mesh Coordinator Launch ───
1034
1187
  case 'launch_mesh_coordinator': {
1035
1188
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
@@ -1101,7 +1254,8 @@ export class DaemonCommandRouter {
1101
1254
  };
1102
1255
  }
1103
1256
 
1104
- if (coordinatorSetup.configFormat !== 'claude_mcp_json') {
1257
+ const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
1258
+ if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
1105
1259
  return {
1106
1260
  success: false,
1107
1261
  code: 'mesh_coordinator_unsupported',
@@ -1112,18 +1266,46 @@ export class DaemonCommandRouter {
1112
1266
  };
1113
1267
  }
1114
1268
 
1115
- // 1. Write provider-declared MCP config to workspace for CLIs that auto-import it.
1116
- const { existsSync, readFileSync, writeFileSync, copyFileSync } = await import('fs');
1269
+ // Build the coordinator prompt before mutating workspace config or launching.
1270
+ // Prompt generation failures are configuration/data-shape errors; fail closed so
1271
+ // broken mesh state is visible instead of silently launching with weaker rules.
1272
+ let systemPrompt = '';
1273
+ try {
1274
+ systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
1275
+ } catch (error: any) {
1276
+ const message = error?.message || String(error);
1277
+ LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
1278
+ return {
1279
+ success: false,
1280
+ code: 'mesh_coordinator_prompt_failed',
1281
+ error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
1282
+ meshId,
1283
+ cliType,
1284
+ workspace,
1285
+ };
1286
+ }
1287
+
1288
+ // 1. Write provider-declared MCP config for CLIs that auto-import it.
1289
+ const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
1290
+ const { dirname } = await import('path');
1117
1291
  const mcpConfigPath = coordinatorSetup.configPath;
1292
+ mkdirSync(dirname(mcpConfigPath), { recursive: true });
1118
1293
 
1119
1294
  // Backup existing MCP config if present.
1120
1295
  const hadExistingMcpConfig = existsSync(mcpConfigPath);
1121
- let existingMcpConfig: any = {};
1296
+ let existingMcpConfig: Record<string, any> = {};
1122
1297
  if (hadExistingMcpConfig) {
1123
1298
  try {
1124
- existingMcpConfig = JSON.parse(readFileSync(mcpConfigPath, 'utf-8'));
1299
+ existingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
1125
1300
  copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
1126
- } catch { /* empty */ }
1301
+ } catch (error: any) {
1302
+ LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
1303
+ return {
1304
+ success: false,
1305
+ code: 'mesh_coordinator_config_parse_failed',
1306
+ error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
1307
+ };
1308
+ }
1127
1309
  }
1128
1310
 
1129
1311
  // Merge ADHDev mesh server into existing config.
@@ -1139,39 +1321,39 @@ export class DaemonCommandRouter {
1139
1321
  ADHDEV_MCP_TRANSPORT: 'ipc',
1140
1322
  };
1141
1323
  }
1324
+ const mcpServersKey = getMcpServersKey(configFormat);
1325
+ const existingServers = existingMcpConfig[mcpServersKey];
1142
1326
  const mcpConfig = {
1143
1327
  ...existingMcpConfig,
1144
- mcpServers: {
1145
- ...(existingMcpConfig.mcpServers || {}),
1328
+ [mcpServersKey]: {
1329
+ ...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
1146
1330
  [coordinatorSetup.serverName]: mcpServerEntry,
1147
1331
  },
1148
1332
  };
1149
- writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');
1333
+ writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
1150
1334
  LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
1151
1335
 
1152
- // 2. Build coordinator system prompt
1153
- let systemPrompt = '';
1154
- 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).`;
1158
- }
1159
-
1160
1336
  const cliArgs: string[] = [];
1337
+ const launchEnv: Record<string, string> = {};
1161
1338
  if (systemPrompt) {
1162
- cliArgs.push('--append-system-prompt', systemPrompt);
1339
+ if (configFormat === 'hermes_config_yaml') {
1340
+ launchEnv.HERMES_EPHEMERAL_SYSTEM_PROMPT = systemPrompt;
1341
+ } else {
1342
+ cliArgs.push('--append-system-prompt', systemPrompt);
1343
+ }
1163
1344
  }
1164
1345
  if (cliType === 'claude-cli') {
1165
1346
  cliArgs.push('--mcp-config', coordinatorSetup.configPath);
1166
1347
  }
1167
1348
 
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.
1349
+ // 3. Launch CLI session via existing cliManager.
1350
+ // Provider-specific prompt injection remains fail-closed: Claude gets
1351
+ // explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
1171
1352
  const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
1172
1353
  cliType,
1173
1354
  dir: workspace,
1174
1355
  cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
1356
+ env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
1175
1357
  settings: {
1176
1358
  meshCoordinatorFor: meshId
1177
1359
  }
@@ -160,9 +160,12 @@ export function deleteMesh(meshId: string): boolean {
160
160
  export interface AddNodeOptions {
161
161
  workspace: string;
162
162
  repoRoot?: string;
163
+ daemonId?: string;
163
164
  userOverrides?: Partial<RepoMeshNodeCapabilities>;
164
165
  policy?: RepoMeshNodePolicy;
165
166
  isLocalWorktree?: boolean;
167
+ worktreeBranch?: string;
168
+ clonedFromNodeId?: string;
166
169
  }
167
170
 
168
171
  export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntry | undefined {
@@ -183,9 +186,12 @@ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntr
183
186
  id: `node_${randomUUID().replace(/-/g, '')}`,
184
187
  workspace: opts.workspace.trim(),
185
188
  repoRoot: opts.repoRoot,
189
+ daemonId: opts.daemonId,
186
190
  userOverrides: opts.userOverrides || {},
187
191
  policy: opts.policy || {},
188
192
  isLocalWorktree: opts.isLocalWorktree,
193
+ worktreeBranch: opts.worktreeBranch,
194
+ clonedFromNodeId: opts.clonedFromNodeId,
189
195
  };
190
196
 
191
197
  mesh.nodes.push(node);