@adhdev/daemon-core 0.9.76-rc.2 → 0.9.76-rc.20
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/mesh-coordinator.d.ts +2 -0
- 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 +870 -361
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +897 -393
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +3 -0
- package/dist/repo-mesh-types.d.ts +4 -0
- package/dist/shared-types.d.ts +18 -1
- package/package.json +3 -4
- package/src/cli-adapters/provider-cli-adapter.ts +6 -3
- package/src/cli-adapters/provider-cli-runtime.ts +3 -2
- package/src/commands/chat-commands.ts +49 -4
- package/src/commands/cli-manager.ts +13 -3
- package/src/commands/mesh-coordinator.ts +149 -6
- package/src/commands/router.ts +247 -30
- 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.d.ts +2 -0
- package/src/providers/cli-provider-instance.ts +6 -1
- package/src/repo-mesh-types.ts +4 -0
- package/src/shared-types.ts +20 -1
- package/src/status/builders.ts +17 -12
package/src/commands/router.ts
CHANGED
|
@@ -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';
|
|
@@ -41,6 +42,10 @@ import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDa
|
|
|
41
42
|
|
|
42
43
|
type ReleaseChannel = 'stable' | 'preview';
|
|
43
44
|
const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
|
|
45
|
+
const CHANNEL_SERVER_URL: Record<ReleaseChannel, string> = {
|
|
46
|
+
stable: 'https://api.adhf.dev',
|
|
47
|
+
preview: 'https://api-preview.adhf.dev',
|
|
48
|
+
};
|
|
44
49
|
|
|
45
50
|
function normalizeReleaseChannel(value: unknown): ReleaseChannel | null {
|
|
46
51
|
if (typeof value !== 'string') return null;
|
|
@@ -59,6 +64,28 @@ function resolveUpgradeChannel(args: any): ReleaseChannel {
|
|
|
59
64
|
}
|
|
60
65
|
import * as fs from 'fs';
|
|
61
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
|
+
|
|
62
89
|
// ─── Types ───
|
|
63
90
|
|
|
64
91
|
export interface SessionHostControlPlane {
|
|
@@ -222,6 +249,43 @@ export class DaemonCommandRouter {
|
|
|
222
249
|
this.deps = deps;
|
|
223
250
|
}
|
|
224
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
|
+
|
|
225
289
|
private async traceSessionHostAction<T>(
|
|
226
290
|
action: string,
|
|
227
291
|
args: any,
|
|
@@ -892,6 +956,7 @@ export class DaemonCommandRouter {
|
|
|
892
956
|
// Check channel-pinned dist-tag and resolve it to a concrete install version.
|
|
893
957
|
const latest = String(execNpmCommandSync(['view', `${pkgName}@${npmTag}`, 'version'], { encoding: 'utf-8', timeout: 10000 }, npmSurface)).trim();
|
|
894
958
|
LOG.info('Upgrade', `Latest ${pkgName}@${npmTag}: v${latest}`);
|
|
959
|
+
updateConfig({ updateChannel: channel, serverUrl: CHANNEL_SERVER_URL[channel] } as any);
|
|
895
960
|
let currentInstalled: string | null = null;
|
|
896
961
|
try {
|
|
897
962
|
const currentJson = String(execNpmCommandSync(['ls', '-g', pkgName, '--depth=0', '--json'], {
|
|
@@ -1017,14 +1082,107 @@ export class DaemonCommandRouter {
|
|
|
1017
1082
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
1018
1083
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
1019
1084
|
try {
|
|
1020
|
-
const
|
|
1021
|
-
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
|
+
}
|
|
1022
1113
|
return { success: true, removed };
|
|
1023
1114
|
} catch (e: any) {
|
|
1024
1115
|
return { success: false, error: e.message };
|
|
1025
1116
|
}
|
|
1026
1117
|
}
|
|
1027
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
|
+
|
|
1028
1186
|
// ─── Mesh Coordinator Launch ───
|
|
1029
1187
|
case 'launch_mesh_coordinator': {
|
|
1030
1188
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
@@ -1069,6 +1227,7 @@ export class DaemonCommandRouter {
|
|
|
1069
1227
|
const providerMeta = this.deps.providerLoader.resolve?.(cliType) || this.deps.providerLoader.getMeta(cliType);
|
|
1070
1228
|
const coordinatorSetup = resolveMeshCoordinatorSetup({
|
|
1071
1229
|
provider: providerMeta,
|
|
1230
|
+
cliType,
|
|
1072
1231
|
meshId,
|
|
1073
1232
|
workspace,
|
|
1074
1233
|
});
|
|
@@ -1096,7 +1255,8 @@ export class DaemonCommandRouter {
|
|
|
1096
1255
|
};
|
|
1097
1256
|
}
|
|
1098
1257
|
|
|
1099
|
-
|
|
1258
|
+
const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
|
|
1259
|
+
if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
|
|
1100
1260
|
return {
|
|
1101
1261
|
success: false,
|
|
1102
1262
|
code: 'mesh_coordinator_unsupported',
|
|
@@ -1107,20 +1267,42 @@ export class DaemonCommandRouter {
|
|
|
1107
1267
|
};
|
|
1108
1268
|
}
|
|
1109
1269
|
|
|
1110
|
-
//
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1270
|
+
// Build the coordinator prompt before mutating workspace config or launching.
|
|
1271
|
+
// Prompt generation failures are configuration/data-shape errors; fail closed so
|
|
1272
|
+
// broken mesh state is visible instead of silently launching with weaker rules.
|
|
1273
|
+
let systemPrompt = '';
|
|
1274
|
+
try {
|
|
1275
|
+
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
|
|
1276
|
+
} catch (error: any) {
|
|
1277
|
+
const message = error?.message || String(error);
|
|
1278
|
+
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
1279
|
+
return {
|
|
1280
|
+
success: false,
|
|
1281
|
+
code: 'mesh_coordinator_prompt_failed',
|
|
1282
|
+
error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
|
|
1283
|
+
meshId,
|
|
1284
|
+
cliType,
|
|
1285
|
+
workspace,
|
|
1286
|
+
};
|
|
1122
1287
|
}
|
|
1123
1288
|
|
|
1289
|
+
// 1. Write provider-declared MCP config for CLIs that auto-import it.
|
|
1290
|
+
const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
|
|
1291
|
+
const { dirname } = await import('path');
|
|
1292
|
+
const mcpConfigPath = coordinatorSetup.configPath;
|
|
1293
|
+
const hermesManualFallback = cliType === 'hermes-cli' && configFormat === 'hermes_config_yaml'
|
|
1294
|
+
? createHermesManualMeshCoordinatorSetup(meshId, workspace)
|
|
1295
|
+
: null;
|
|
1296
|
+
const returnManualFallback = (message: string) => ({
|
|
1297
|
+
success: false,
|
|
1298
|
+
code: 'mesh_coordinator_manual_mcp_setup_required',
|
|
1299
|
+
error: message,
|
|
1300
|
+
meshId,
|
|
1301
|
+
cliType,
|
|
1302
|
+
workspace,
|
|
1303
|
+
meshCoordinatorSetup: hermesManualFallback,
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1124
1306
|
// Merge ADHDev mesh server into existing config.
|
|
1125
1307
|
// Pass full mesh data as env var so the MCP server can bootstrap
|
|
1126
1308
|
// without depending on meshes.json or a running daemon.
|
|
@@ -1131,41 +1313,76 @@ export class DaemonCommandRouter {
|
|
|
1131
1313
|
if (args?.inlineMesh) {
|
|
1132
1314
|
mcpServerEntry.env = {
|
|
1133
1315
|
ADHDEV_INLINE_MESH: JSON.stringify(mesh),
|
|
1316
|
+
ADHDEV_MCP_TRANSPORT: 'ipc',
|
|
1134
1317
|
};
|
|
1135
1318
|
}
|
|
1319
|
+
|
|
1320
|
+
try {
|
|
1321
|
+
mkdirSync(dirname(mcpConfigPath), { recursive: true });
|
|
1322
|
+
} catch (error: any) {
|
|
1323
|
+
const message = `Could not prepare MCP config path for automatic setup: ${error?.message || error}`;
|
|
1324
|
+
LOG.error('MeshCoordinator', message);
|
|
1325
|
+
if (hermesManualFallback) return returnManualFallback(message);
|
|
1326
|
+
return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// Backup existing MCP config if present.
|
|
1330
|
+
const hadExistingMcpConfig = existsSync(mcpConfigPath);
|
|
1331
|
+
let existingMcpConfig: Record<string, any> = {};
|
|
1332
|
+
if (hadExistingMcpConfig) {
|
|
1333
|
+
try {
|
|
1334
|
+
existingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
|
|
1335
|
+
copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
|
|
1336
|
+
} catch (error: any) {
|
|
1337
|
+
LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
|
|
1338
|
+
return {
|
|
1339
|
+
success: false,
|
|
1340
|
+
code: 'mesh_coordinator_config_parse_failed',
|
|
1341
|
+
error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const mcpServersKey = getMcpServersKey(configFormat);
|
|
1347
|
+
const existingServers = existingMcpConfig[mcpServersKey];
|
|
1136
1348
|
const mcpConfig = {
|
|
1137
1349
|
...existingMcpConfig,
|
|
1138
|
-
|
|
1139
|
-
...(
|
|
1350
|
+
[mcpServersKey]: {
|
|
1351
|
+
...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
|
|
1140
1352
|
[coordinatorSetup.serverName]: mcpServerEntry,
|
|
1141
1353
|
},
|
|
1142
1354
|
};
|
|
1143
|
-
writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');
|
|
1144
|
-
LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
|
|
1145
|
-
|
|
1146
|
-
// 2. Build coordinator system prompt
|
|
1147
|
-
let systemPrompt = '';
|
|
1148
1355
|
try {
|
|
1149
|
-
|
|
1150
|
-
} catch {
|
|
1151
|
-
|
|
1356
|
+
writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
|
|
1357
|
+
} catch (error: any) {
|
|
1358
|
+
const message = `Could not write MCP config for automatic setup: ${error?.message || error}`;
|
|
1359
|
+
LOG.error('MeshCoordinator', message);
|
|
1360
|
+
if (hermesManualFallback) return returnManualFallback(message);
|
|
1361
|
+
return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
|
|
1152
1362
|
}
|
|
1363
|
+
LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
|
|
1153
1364
|
|
|
1154
1365
|
const cliArgs: string[] = [];
|
|
1366
|
+
const launchEnv: Record<string, string> = {};
|
|
1155
1367
|
if (systemPrompt) {
|
|
1156
|
-
|
|
1368
|
+
if (configFormat === 'hermes_config_yaml') {
|
|
1369
|
+
launchEnv.HERMES_EPHEMERAL_SYSTEM_PROMPT = systemPrompt;
|
|
1370
|
+
} else {
|
|
1371
|
+
cliArgs.push('--append-system-prompt', systemPrompt);
|
|
1372
|
+
}
|
|
1157
1373
|
}
|
|
1158
1374
|
if (cliType === 'claude-cli') {
|
|
1159
1375
|
cliArgs.push('--mcp-config', coordinatorSetup.configPath);
|
|
1160
1376
|
}
|
|
1161
1377
|
|
|
1162
|
-
// 3. Launch CLI session via existing cliManager
|
|
1163
|
-
//
|
|
1164
|
-
// CLI
|
|
1378
|
+
// 3. Launch CLI session via existing cliManager.
|
|
1379
|
+
// Provider-specific prompt injection remains fail-closed: Claude gets
|
|
1380
|
+
// explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
|
|
1165
1381
|
const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
1166
1382
|
cliType,
|
|
1167
1383
|
dir: workspace,
|
|
1168
1384
|
cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
|
|
1385
|
+
env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
|
|
1169
1386
|
settings: {
|
|
1170
1387
|
meshCoordinatorFor: meshId
|
|
1171
1388
|
}
|
|
@@ -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);
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Worktree — Create/remove/list worktrees for Repo Mesh node cloning
|
|
3
|
+
*
|
|
4
|
+
* Used by the `clone_mesh_node` daemon command to create isolated
|
|
5
|
+
* worktree-based nodes for parallel branch work within a mesh.
|
|
6
|
+
*
|
|
7
|
+
* Worktrees are placed outside the source repo to avoid .gitignore
|
|
8
|
+
* pollution and submodule conflicts:
|
|
9
|
+
* <repoParent>/.adhdev-worktrees/<meshName>/<branch>/
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { mkdir } from 'node:fs/promises';
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { execFile } from 'node:child_process';
|
|
16
|
+
import { promisify } from 'node:util';
|
|
17
|
+
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
19
|
+
|
|
20
|
+
const WORKTREE_DIR_NAME = '.adhdev-worktrees';
|
|
21
|
+
const GIT_TIMEOUT_MS = 30_000;
|
|
22
|
+
const GIT_MAX_BUFFER = 4 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
// ─── Types ──────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export interface WorktreeCreateOptions {
|
|
27
|
+
/** Absolute path to the source repo's git root */
|
|
28
|
+
repoRoot: string;
|
|
29
|
+
/** Branch name for the new worktree */
|
|
30
|
+
branch: string;
|
|
31
|
+
/** Starting point for the branch (default: HEAD) */
|
|
32
|
+
baseBranch?: string;
|
|
33
|
+
/** Mesh name, used for organizing worktree directories */
|
|
34
|
+
meshName: string;
|
|
35
|
+
/** Override the auto-resolved target directory */
|
|
36
|
+
targetDir?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WorktreeCreateResult {
|
|
40
|
+
success: true;
|
|
41
|
+
worktreePath: string;
|
|
42
|
+
branch: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface WorktreeEntry {
|
|
46
|
+
path: string;
|
|
47
|
+
head: string;
|
|
48
|
+
branch: string | null;
|
|
49
|
+
bare: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WorktreeRemoveResult {
|
|
53
|
+
success: true;
|
|
54
|
+
removedPath: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Path Resolution ────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the target directory for a new worktree.
|
|
61
|
+
* Places worktrees at: <repoParent>/.adhdev-worktrees/<meshName>/<branch>/
|
|
62
|
+
*/
|
|
63
|
+
export function resolveWorktreePath(repoRoot: string, meshName: string, branch: string): string {
|
|
64
|
+
// Sanitize branch name for filesystem (e.g. feat/auth → feat-auth)
|
|
65
|
+
const safeBranch = branch.replace(/[/\\:*?"<>|]/g, '-').replace(/^\.+|\.+$/g, '');
|
|
66
|
+
const safeMeshName = meshName.replace(/[/\\:*?"<>|]/g, '-').replace(/^\.+|\.+$/g, '');
|
|
67
|
+
const parentDir = path.dirname(repoRoot);
|
|
68
|
+
return path.join(parentDir, WORKTREE_DIR_NAME, safeMeshName, safeBranch);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Create ─────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a new git worktree with a fresh branch.
|
|
75
|
+
*
|
|
76
|
+
* Runs: git worktree add <targetDir> -b <branch> [baseBranch]
|
|
77
|
+
*/
|
|
78
|
+
export async function createWorktree(opts: WorktreeCreateOptions): Promise<WorktreeCreateResult> {
|
|
79
|
+
const { repoRoot, branch, baseBranch, meshName } = opts;
|
|
80
|
+
const targetDir = opts.targetDir || resolveWorktreePath(repoRoot, meshName, branch);
|
|
81
|
+
|
|
82
|
+
if (existsSync(targetDir)) {
|
|
83
|
+
throw new Error(`Worktree target directory already exists: ${targetDir}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Ensure parent directory exists
|
|
87
|
+
await mkdir(path.dirname(targetDir), { recursive: true });
|
|
88
|
+
|
|
89
|
+
const args = ['worktree', 'add', targetDir, '-b', branch];
|
|
90
|
+
if (baseBranch) {
|
|
91
|
+
args.push(baseBranch);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
await execFileAsync('git', args, {
|
|
96
|
+
cwd: repoRoot,
|
|
97
|
+
encoding: 'utf8',
|
|
98
|
+
timeout: GIT_TIMEOUT_MS,
|
|
99
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
100
|
+
windowsHide: true,
|
|
101
|
+
});
|
|
102
|
+
} catch (error: any) {
|
|
103
|
+
const stderr = typeof error.stderr === 'string' ? error.stderr : '';
|
|
104
|
+
// Clean error messages for common failures
|
|
105
|
+
if (/already exists/i.test(stderr)) {
|
|
106
|
+
throw new Error(`Branch '${branch}' already exists or is checked out in another worktree`);
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`git worktree add failed: ${stderr.trim() || error.message}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
success: true,
|
|
113
|
+
worktreePath: targetDir,
|
|
114
|
+
branch,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Remove ─────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Remove a git worktree and clean up the directory.
|
|
122
|
+
*
|
|
123
|
+
* Runs: git worktree remove <worktreePath> --force
|
|
124
|
+
*/
|
|
125
|
+
export async function removeWorktree(repoRoot: string, worktreePath: string): Promise<WorktreeRemoveResult> {
|
|
126
|
+
if (!existsSync(worktreePath)) {
|
|
127
|
+
// Already gone — just prune
|
|
128
|
+
await pruneWorktrees(repoRoot);
|
|
129
|
+
return { success: true, removedPath: worktreePath };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await execFileAsync('git', ['worktree', 'remove', worktreePath, '--force'], {
|
|
134
|
+
cwd: repoRoot,
|
|
135
|
+
encoding: 'utf8',
|
|
136
|
+
timeout: GIT_TIMEOUT_MS,
|
|
137
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
138
|
+
windowsHide: true,
|
|
139
|
+
});
|
|
140
|
+
} catch (error: any) {
|
|
141
|
+
const stderr = typeof error.stderr === 'string' ? error.stderr : '';
|
|
142
|
+
throw new Error(`git worktree remove failed: ${stderr.trim() || error.message}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { success: true, removedPath: worktreePath };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── List ───────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* List all worktrees for a repository.
|
|
152
|
+
*
|
|
153
|
+
* Runs: git worktree list --porcelain
|
|
154
|
+
*/
|
|
155
|
+
export async function listWorktrees(repoRoot: string): Promise<WorktreeEntry[]> {
|
|
156
|
+
const { stdout } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], {
|
|
157
|
+
cwd: repoRoot,
|
|
158
|
+
encoding: 'utf8',
|
|
159
|
+
timeout: GIT_TIMEOUT_MS,
|
|
160
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
161
|
+
windowsHide: true,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return parseWorktreeListOutput(stdout);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Parse `git worktree list --porcelain` output into structured entries.
|
|
169
|
+
*/
|
|
170
|
+
export function parseWorktreeListOutput(output: string): WorktreeEntry[] {
|
|
171
|
+
const entries: WorktreeEntry[] = [];
|
|
172
|
+
const blocks = output.trim().split(/\n\n+/);
|
|
173
|
+
|
|
174
|
+
for (const block of blocks) {
|
|
175
|
+
if (!block.trim()) continue;
|
|
176
|
+
const lines = block.trim().split('\n');
|
|
177
|
+
const entry: WorktreeEntry = { path: '', head: '', branch: null, bare: false };
|
|
178
|
+
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
if (line.startsWith('worktree ')) {
|
|
181
|
+
entry.path = line.slice('worktree '.length).trim();
|
|
182
|
+
} else if (line.startsWith('HEAD ')) {
|
|
183
|
+
entry.head = line.slice('HEAD '.length).trim();
|
|
184
|
+
} else if (line.startsWith('branch ')) {
|
|
185
|
+
const ref = line.slice('branch '.length).trim();
|
|
186
|
+
// refs/heads/feat/auth → feat/auth
|
|
187
|
+
entry.branch = ref.replace(/^refs\/heads\//, '');
|
|
188
|
+
} else if (line === 'bare') {
|
|
189
|
+
entry.bare = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (entry.path) {
|
|
194
|
+
entries.push(entry);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return entries;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── Prune ──────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
async function pruneWorktrees(repoRoot: string): Promise<void> {
|
|
204
|
+
try {
|
|
205
|
+
await execFileAsync('git', ['worktree', 'prune'], {
|
|
206
|
+
cwd: repoRoot,
|
|
207
|
+
encoding: 'utf8',
|
|
208
|
+
timeout: GIT_TIMEOUT_MS,
|
|
209
|
+
windowsHide: true,
|
|
210
|
+
});
|
|
211
|
+
} catch {
|
|
212
|
+
// Prune is best-effort
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/git/index.ts
CHANGED
|
@@ -73,3 +73,17 @@ export type {
|
|
|
73
73
|
|
|
74
74
|
export { TurnSnapshotTracker } from './turn-snapshot-tracker.js';
|
|
75
75
|
export type { TurnCompletedCallback } from './turn-snapshot-tracker.js';
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
createWorktree,
|
|
79
|
+
listWorktrees,
|
|
80
|
+
parseWorktreeListOutput,
|
|
81
|
+
removeWorktree,
|
|
82
|
+
resolveWorktreePath,
|
|
83
|
+
} from './git-worktree.js';
|
|
84
|
+
export type {
|
|
85
|
+
WorktreeCreateOptions,
|
|
86
|
+
WorktreeCreateResult,
|
|
87
|
+
WorktreeEntry,
|
|
88
|
+
WorktreeRemoveResult,
|
|
89
|
+
} from './git-worktree.js';
|