@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.
- package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -1
- package/dist/cli-adapters/provider-cli-runtime.d.ts +1 -0
- package/dist/commands/cli-manager.d.ts +6 -4
- package/dist/commands/router.d.ts +4 -0
- package/dist/config/mesh-config.d.ts +3 -0
- package/dist/git/git-worktree.d.ts +64 -0
- package/dist/git/index.d.ts +2 -0
- package/dist/index.js +660 -334
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +680 -359
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +1 -0
- package/dist/repo-mesh-types.d.ts +4 -0
- package/package.json +3 -1
- package/src/cli-adapters/provider-cli-adapter.ts +2 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -2
- package/src/commands/cli-manager.ts +13 -3
- package/src/commands/mesh-coordinator.ts +11 -2
- package/src/commands/router.ts +205 -23
- package/src/config/mesh-config.ts +6 -0
- package/src/git/git-worktree.ts +214 -0
- package/src/git/index.ts +14 -0
- package/src/mesh/coordinator-prompt.ts +25 -10
- package/src/providers/cli-provider-instance.ts +2 -1
- package/src/repo-mesh-types.ts +4 -0
|
@@ -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.
|
|
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?:
|
|
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
|
|
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:
|
|
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
|
package/src/commands/router.ts
CHANGED
|
@@ -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
|
|
1026
|
-
const
|
|
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
|
-
|
|
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
|
-
//
|
|
1116
|
-
|
|
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 =
|
|
1299
|
+
existingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
|
|
1125
1300
|
copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
|
|
1126
|
-
} catch
|
|
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
|
-
|
|
1145
|
-
...(
|
|
1328
|
+
[mcpServersKey]: {
|
|
1329
|
+
...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
|
|
1146
1330
|
[coordinatorSetup.serverName]: mcpServerEntry,
|
|
1147
1331
|
},
|
|
1148
1332
|
};
|
|
1149
|
-
writeFileSync(mcpConfigPath,
|
|
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
|
-
|
|
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
|
-
//
|
|
1170
|
-
// CLI
|
|
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);
|