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

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 (50) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +3 -0
  2. package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
  3. package/dist/commands/mesh-coordinator.d.ts +10 -0
  4. package/dist/commands/router.d.ts +4 -1
  5. package/dist/config/mesh-config.d.ts +1 -0
  6. package/dist/git/git-worktree.d.ts +15 -2
  7. package/dist/index.d.ts +10 -6
  8. package/dist/index.js +2116 -299
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +2101 -299
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/mesh/mesh-events.d.ts +14 -7
  13. package/dist/mesh/mesh-ledger-reconciliation.d.ts +55 -0
  14. package/dist/mesh/mesh-ledger.d.ts +84 -4
  15. package/dist/mesh/mesh-sync.d.ts +4 -12
  16. package/dist/mesh/mesh-visualization.d.ts +70 -0
  17. package/dist/mesh/mesh-work-queue.d.ts +58 -1
  18. package/dist/mesh/p2p-relay-failure.d.ts +35 -0
  19. package/dist/providers/chat-message-normalization.d.ts +1 -0
  20. package/dist/providers/cli-provider-instance.d.ts +6 -0
  21. package/dist/repo-mesh-types.d.ts +2 -0
  22. package/dist/shared-types.d.ts +38 -0
  23. package/package.json +1 -1
  24. package/src/boot/daemon-lifecycle.ts +5 -0
  25. package/src/cli-adapters/provider-cli-adapter.ts +30 -5
  26. package/src/commands/cli-manager.ts +0 -4
  27. package/src/commands/mesh-coordinator.ts +55 -7
  28. package/src/commands/router.ts +964 -26
  29. package/src/commands/stream-commands.ts +8 -1
  30. package/src/config/config.ts +2 -1
  31. package/src/config/mesh-config.ts +2 -0
  32. package/src/config/workspaces.ts +1 -1
  33. package/src/git/git-worktree.ts +56 -4
  34. package/src/index.d.ts +3 -0
  35. package/src/index.ts +29 -6
  36. package/src/mesh/coordinator-prompt.ts +21 -10
  37. package/src/mesh/mesh-events.ts +532 -22
  38. package/src/mesh/mesh-ledger-reconciliation.ts +115 -0
  39. package/src/mesh/mesh-ledger.ts +209 -8
  40. package/src/mesh/mesh-sync.ts +4 -34
  41. package/src/mesh/mesh-visualization.ts +341 -0
  42. package/src/mesh/mesh-work-queue.ts +183 -17
  43. package/src/mesh/p2p-relay-failure.ts +152 -0
  44. package/src/providers/acp-provider-instance.ts +2 -1
  45. package/src/providers/chat-message-normalization.ts +32 -0
  46. package/src/providers/cli-provider-instance.ts +155 -31
  47. package/src/providers/extension-provider-instance.ts +2 -1
  48. package/src/providers/ide-provider-instance.ts +2 -2
  49. package/src/repo-mesh-types.ts +2 -0
  50. package/src/shared-types.ts +38 -0
@@ -43,6 +43,7 @@ import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDa
43
43
  import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
44
44
  import { homedir } from 'os';
45
45
  import { join as pathJoin, resolve as pathResolve } from 'path';
46
+ import * as fs from 'fs';
46
47
 
47
48
  type ReleaseChannel = 'stable' | 'preview';
48
49
  const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
@@ -114,9 +115,271 @@ async function resolveProviderTypeFromPriority(args: {
114
115
 
115
116
  return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join('; ')}` };
116
117
  }
117
- import * as fs from 'fs';
118
-
119
118
  type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
119
+ type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
120
+ type MeshRefineValidationCommand = {
121
+ command: string;
122
+ args: string[];
123
+ displayCommand: string;
124
+ category: string;
125
+ source: string;
126
+ };
127
+
128
+ type MeshRefineValidationSummary = {
129
+ status: MeshRefineValidationStatus;
130
+ required: true;
131
+ commandsRun: Array<Record<string, unknown>>;
132
+ rejectedCommands: Array<Record<string, unknown>>;
133
+ skippedReason?: string;
134
+ timeoutMs: number;
135
+ outputLimitBytes: number;
136
+ };
137
+
138
+ const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
139
+ const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
140
+ const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
141
+ const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
142
+ const REFINE_VALIDATION_MAX_COMMANDS = 4;
143
+
144
+ function truncateValidationOutput(value: unknown): string {
145
+ const text = typeof value === 'string' ? value : value == null ? '' : String(value);
146
+ if (text.length <= REFINE_VALIDATION_SUMMARY_CHARS) return text;
147
+ return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
148
+ }
149
+
150
+ function readPackageScripts(workspace: string): Record<string, string> {
151
+ try {
152
+ const packageJsonPath = pathJoin(workspace, 'package.json');
153
+ const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
154
+ return parsed?.scripts && typeof parsed.scripts === 'object' && !Array.isArray(parsed.scripts)
155
+ ? parsed.scripts as Record<string, string>
156
+ : {};
157
+ } catch {
158
+ return {};
159
+ }
160
+ }
161
+
162
+ function tokenizeValidationCommand(command: string): string[] | null {
163
+ const trimmed = command.trim();
164
+ if (!trimmed) return null;
165
+ // Fail closed: the gate never hands shell syntax to a shell. Package-manager
166
+ // scripts are invoked via execFile(binary, args), and metacharacters/quotes are
167
+ // rejected before tokenization so `npm run test && rm -rf` cannot be smuggled in.
168
+ if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
169
+ const tokens = trimmed.split(/\s+/).filter(Boolean);
170
+ if (!tokens.length) return null;
171
+ if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
172
+ return tokens;
173
+ }
174
+
175
+ function scriptMatchesValidationCategory(scriptName: string, category: string): boolean {
176
+ return scriptName === category || scriptName.startsWith(`${category}:`);
177
+ }
178
+
179
+ function parsePackageManagerValidationCommand(
180
+ rawCommand: string,
181
+ category: string,
182
+ scripts: Record<string, string>,
183
+ source: string,
184
+ ): { command?: MeshRefineValidationCommand; rejected?: Record<string, unknown> } {
185
+ const tokens = tokenizeValidationCommand(rawCommand);
186
+ if (!tokens) {
187
+ return { rejected: { command: rawCommand, category, source, reason: 'unsafe command string is not allowlisted' } };
188
+ }
189
+
190
+ const [binary, second, third, ...rest] = tokens;
191
+ let scriptName = '';
192
+ let command = binary;
193
+ let args: string[] = [];
194
+
195
+ if ((binary === 'npm' || binary === 'pnpm' || binary === 'bun') && second === 'run' && third) {
196
+ scriptName = third;
197
+ args = ['run', scriptName, ...rest];
198
+ } else if (binary === 'npm' && second === 'test' && !third) {
199
+ scriptName = 'test';
200
+ args = ['test'];
201
+ } else if (binary === 'yarn' && second === 'run' && third) {
202
+ scriptName = third;
203
+ args = ['run', scriptName, ...rest];
204
+ } else if (binary === 'yarn' && second && !third) {
205
+ scriptName = second;
206
+ args = [scriptName];
207
+ } else {
208
+ return { rejected: { command: rawCommand, category, source, reason: 'command is not a supported package-manager script invocation' } };
209
+ }
210
+
211
+ if (!scriptName || !Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
212
+ return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script is not declared in package.json' } };
213
+ }
214
+ if (!scriptMatchesValidationCategory(scriptName, category)) {
215
+ return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script name is outside the validation category allowlist' } };
216
+ }
217
+
218
+ return {
219
+ command: {
220
+ command,
221
+ args,
222
+ displayCommand: [command, ...args].join(' '),
223
+ category,
224
+ source,
225
+ },
226
+ };
227
+ }
228
+
229
+ function collectProjectContextValidationCandidates(mesh: any): Array<{ command: string; category: string; source: string; confidence?: string }> {
230
+ const commands = mesh?.projectContext?.commands;
231
+ if (!commands || typeof commands !== 'object' || Array.isArray(commands)) return [];
232
+ const candidates: Array<{ command: string; category: string; source: string; confidence?: string }> = [];
233
+ for (const category of REFINE_VALIDATION_CATEGORIES) {
234
+ const entries = Array.isArray(commands[category]) ? commands[category] : [];
235
+ for (const entry of entries) {
236
+ if (typeof entry?.command !== 'string') continue;
237
+ candidates.push({
238
+ command: entry.command,
239
+ category,
240
+ source: typeof entry.sourcePath === 'string' ? entry.sourcePath : 'projectContext.commands',
241
+ confidence: typeof entry.confidence === 'string' ? entry.confidence : undefined,
242
+ });
243
+ }
244
+ }
245
+ return candidates.sort((a, b) => {
246
+ const rank = (value?: string) => value === 'high' ? 0 : value === 'medium' ? 1 : 2;
247
+ return rank(a.confidence) - rank(b.confidence);
248
+ });
249
+ }
250
+
251
+ function collectPolicyValidationCandidates(mesh: any): Array<{ command: string; category: string; source: string }> {
252
+ const policy = mesh?.policy && typeof mesh.policy === 'object' && !Array.isArray(mesh.policy) ? mesh.policy : {};
253
+ const configured = Array.isArray(policy.validationCommands)
254
+ ? policy.validationCommands
255
+ : Array.isArray(policy.validationGate?.commands)
256
+ ? policy.validationGate.commands
257
+ : [];
258
+ return configured
259
+ .map((entry: any) => typeof entry === 'string' ? { command: entry, category: '', source: 'mesh.policy.validationCommands' } : entry)
260
+ .filter((entry: any) => entry && typeof entry.command === 'string')
261
+ .map((entry: any) => {
262
+ const commandText = entry.command.trim();
263
+ const category = REFINE_VALIDATION_CATEGORIES.find(cat => commandText.includes(` ${cat}`)) ?? '';
264
+ return { command: commandText, category, source: 'mesh.policy.validationCommands' };
265
+ })
266
+ .filter((entry: any) => !!entry.category);
267
+ }
268
+
269
+ function selectMeshRefineValidationCommands(mesh: any, workspace: string): { commands: MeshRefineValidationCommand[]; rejectedCommands: Array<Record<string, unknown>>; source: string } {
270
+ const scripts = readPackageScripts(workspace);
271
+ const rejectedCommands: Array<Record<string, unknown>> = [];
272
+ const selected: MeshRefineValidationCommand[] = [];
273
+ const seen = new Set<string>();
274
+ const candidates = [
275
+ ...collectPolicyValidationCandidates(mesh),
276
+ ...collectProjectContextValidationCandidates(mesh),
277
+ ];
278
+
279
+ for (const candidate of candidates) {
280
+ const parsed = parsePackageManagerValidationCommand(candidate.command, candidate.category, scripts, candidate.source);
281
+ if (parsed.rejected) {
282
+ rejectedCommands.push(parsed.rejected);
283
+ continue;
284
+ }
285
+ if (!parsed.command || seen.has(parsed.command.displayCommand)) continue;
286
+ selected.push(parsed.command);
287
+ seen.add(parsed.command.displayCommand);
288
+ if (selected.length >= REFINE_VALIDATION_MAX_COMMANDS) break;
289
+ }
290
+
291
+ if (!selected.length && candidates.length === 0) {
292
+ for (const category of REFINE_VALIDATION_CATEGORIES) {
293
+ if (!Object.prototype.hasOwnProperty.call(scripts, category)) continue;
294
+ const fallback = parsePackageManagerValidationCommand(`npm run ${category}`, category, scripts, 'package.json:scripts');
295
+ if (fallback.command && !seen.has(fallback.command.displayCommand)) {
296
+ selected.push(fallback.command);
297
+ seen.add(fallback.command.displayCommand);
298
+ } else if (fallback.rejected) {
299
+ rejectedCommands.push(fallback.rejected);
300
+ }
301
+ if (selected.length >= 2) break;
302
+ }
303
+ }
304
+
305
+ return {
306
+ commands: selected,
307
+ rejectedCommands,
308
+ source: selected.some(command => command.source === 'mesh.policy.validationCommands')
309
+ ? 'mesh_policy'
310
+ : selected.some(command => command.source !== 'package.json:scripts')
311
+ ? 'project_context'
312
+ : selected.length
313
+ ? 'package_json_scripts'
314
+ : 'unavailable',
315
+ };
316
+ }
317
+
318
+ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promise<MeshRefineValidationSummary> {
319
+ const { execFile } = await import('node:child_process');
320
+ const { promisify } = await import('node:util');
321
+ const execFileAsync = promisify(execFile);
322
+ const selection = selectMeshRefineValidationCommands(mesh, workspace);
323
+ const summary: MeshRefineValidationSummary = {
324
+ status: 'skipped',
325
+ required: true,
326
+ commandsRun: [],
327
+ rejectedCommands: selection.rejectedCommands,
328
+ skippedReason: undefined,
329
+ timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
330
+ outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
331
+ };
332
+
333
+ if (!selection.commands.length) {
334
+ summary.skippedReason = 'validation_unavailable: no allowlisted projectContext, mesh policy, or package.json build/test/typecheck/lint command was available';
335
+ return summary;
336
+ }
337
+
338
+ for (const candidate of selection.commands) {
339
+ const startedAt = Date.now();
340
+ try {
341
+ const result = await execFileAsync(candidate.command, candidate.args, {
342
+ cwd: workspace,
343
+ encoding: 'utf8',
344
+ timeout: REFINE_VALIDATION_TIMEOUT_MS,
345
+ maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
346
+ env: { ...process.env, CI: process.env.CI || '1' },
347
+ });
348
+ summary.commandsRun.push({
349
+ command: candidate.command,
350
+ args: candidate.args,
351
+ displayCommand: candidate.displayCommand,
352
+ category: candidate.category,
353
+ source: candidate.source,
354
+ passed: true,
355
+ exitCode: 0,
356
+ durationMs: Date.now() - startedAt,
357
+ stdout: truncateValidationOutput(result.stdout),
358
+ stderr: truncateValidationOutput(result.stderr),
359
+ });
360
+ } catch (error: any) {
361
+ summary.commandsRun.push({
362
+ command: candidate.command,
363
+ args: candidate.args,
364
+ displayCommand: candidate.displayCommand,
365
+ category: candidate.category,
366
+ source: candidate.source,
367
+ passed: false,
368
+ exitCode: typeof error?.code === 'number' ? error.code : null,
369
+ signal: typeof error?.signal === 'string' ? error.signal : null,
370
+ timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
371
+ durationMs: Date.now() - startedAt,
372
+ stdout: truncateValidationOutput(error?.stdout),
373
+ stderr: truncateValidationOutput(error?.stderr || error?.message),
374
+ });
375
+ summary.status = 'failed';
376
+ return summary;
377
+ }
378
+ }
379
+
380
+ summary.status = 'passed';
381
+ return summary;
382
+ }
120
383
 
121
384
  function loadYamlModule(): { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string } {
122
385
  return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
@@ -154,6 +417,35 @@ function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Re
154
417
  return { config: baseConfig, sourceHome, sourceConfigPath };
155
418
  }
156
419
 
420
+ function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
421
+ const {
422
+ model: _model,
423
+ provider: _provider,
424
+ default_model: _defaultModel,
425
+ defaultProvider: _defaultProvider,
426
+ default_provider: _defaultProviderSnake,
427
+ modelProvider: _modelProvider,
428
+ model_provider: _modelProviderSnake,
429
+ ...sanitized
430
+ } = config;
431
+ const delegation = sanitized.delegation;
432
+ if (delegation && typeof delegation === 'object' && !Array.isArray(delegation)) {
433
+ const {
434
+ model: _delegationModel,
435
+ provider: _delegationProvider,
436
+ modelProvider: _delegationModelProvider,
437
+ model_provider: _delegationModelProviderSnake,
438
+ ...delegationRest
439
+ } = delegation;
440
+ if (Object.keys(delegationRest).length > 0) {
441
+ sanitized.delegation = delegationRest;
442
+ } else {
443
+ delete sanitized.delegation;
444
+ }
445
+ }
446
+ return sanitized;
447
+ }
448
+
157
449
  function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
158
450
  if (pathResolve(sourceHome) === pathResolve(targetHome)) return;
159
451
  for (const fileName of ['.env', 'auth.json']) {
@@ -332,7 +624,7 @@ export class DaemonCommandRouter {
332
624
  this.deps = deps;
333
625
  }
334
626
 
335
- private getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
627
+ public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
336
628
  if (inlineMesh && typeof inlineMesh === 'object') {
337
629
  this.inlineMeshCache.set(meshId, inlineMesh as any);
338
630
  return inlineMesh as any;
@@ -388,10 +680,238 @@ export class DaemonCommandRouter {
388
680
  return false;
389
681
  }
390
682
 
683
+ private async cleanupLocalWorktreeNode(args: {
684
+ mesh: any;
685
+ node: any;
686
+ nodeId: string;
687
+ }): Promise<{ success: true; skipped?: boolean; removedPath?: string; repoRoot?: string; reason?: string; fallback?: string; forced?: boolean; convergence?: Record<string, unknown> } | { success: false; code: string; error: string; recoveryHint: string; convergence?: Record<string, unknown> }> {
688
+ const workspace = typeof args.node?.workspace === 'string' ? args.node.workspace.trim() : '';
689
+ if (!workspace) {
690
+ return {
691
+ success: false,
692
+ code: 'mesh_worktree_cleanup_missing_workspace',
693
+ error: `Worktree node '${args.nodeId}' is missing workspace metadata`,
694
+ recoveryHint: 'Inspect the mesh node record before removing it, or remove stale metadata manually only after confirming no managed worktree remains.',
695
+ };
696
+ }
697
+
698
+ const worktreeExists = fs.existsSync(workspace);
699
+ const sourceNode = args.node?.clonedFromNodeId
700
+ ? args.mesh?.nodes?.find((n: any) => n.id === args.node.clonedFromNodeId || n.nodeId === args.node.clonedFromNodeId)
701
+ : args.mesh?.nodes?.find((n: any) => !n.isLocalWorktree);
702
+ const repoRoot = typeof sourceNode?.repoRoot === 'string' && sourceNode.repoRoot.trim()
703
+ ? sourceNode.repoRoot.trim()
704
+ : typeof sourceNode?.workspace === 'string' && sourceNode.workspace.trim()
705
+ ? sourceNode.workspace.trim()
706
+ : '';
707
+
708
+ if (!worktreeExists) {
709
+ return { success: true, skipped: true, removedPath: workspace, repoRoot: repoRoot || undefined, reason: 'worktree_path_missing' };
710
+ }
711
+ if (!repoRoot || !fs.existsSync(repoRoot)) {
712
+ return {
713
+ success: false,
714
+ code: 'mesh_worktree_cleanup_missing_source_repo',
715
+ error: `Refusing to remove worktree '${workspace}' because the source repo root is unavailable`,
716
+ recoveryHint: 'Run mesh_remove_node from the machine that owns the source repo, or verify the source node metadata before retrying.',
717
+ };
718
+ }
719
+ if (typeof args.node?.worktreeBranch !== 'string' || !args.node.worktreeBranch.trim()) {
720
+ return {
721
+ success: false,
722
+ code: 'mesh_worktree_cleanup_missing_branch',
723
+ error: `Refusing to remove worktree '${workspace}' because worktreeBranch metadata is missing`,
724
+ recoveryHint: 'Confirm this is an ADHDev-managed worktree before removing it manually; managed worktree nodes include worktreeBranch metadata.',
725
+ };
726
+ }
727
+
728
+ const { resolveWorktreePath, listWorktrees, removeWorktree } = await import('../git/git-worktree.js');
729
+ const normalizePath = (value: string) => {
730
+ const resolved = pathResolve(value);
731
+ try { return fs.realpathSync(resolved); } catch { return resolved; }
732
+ };
733
+ const expectedPath = normalizePath(resolveWorktreePath(repoRoot, String(args.mesh?.name || args.mesh?.id || 'mesh'), args.node.worktreeBranch));
734
+ const actualPath = normalizePath(workspace);
735
+ if (actualPath !== expectedPath) {
736
+ return {
737
+ success: false,
738
+ code: 'mesh_worktree_cleanup_unexpected_path',
739
+ error: `Refusing to remove worktree '${workspace}' because it is not at the expected managed path '${expectedPath}'`,
740
+ recoveryHint: 'Use git worktree list/status to inspect the path. Retry only after confirming the mesh node metadata points to an ADHDev-managed worktree.',
741
+ };
742
+ }
743
+
744
+ const entries = await listWorktrees(repoRoot);
745
+ const managedEntry = entries.find(entry => normalizePath(entry.path) === actualPath);
746
+ if (!managedEntry) {
747
+ return {
748
+ success: false,
749
+ code: 'mesh_worktree_cleanup_not_registered',
750
+ error: `Refusing to remove '${workspace}' because it is not registered in git worktree list for '${repoRoot}'`,
751
+ recoveryHint: 'Inspect git worktree list --porcelain from the source repo. If the path was already removed, prune git worktrees before retrying.',
752
+ };
753
+ }
754
+ if (managedEntry.branch && managedEntry.branch !== args.node.worktreeBranch) {
755
+ return {
756
+ success: false,
757
+ code: 'mesh_worktree_cleanup_branch_mismatch',
758
+ error: `Refusing to remove '${workspace}' because git reports branch '${managedEntry.branch}', expected '${args.node.worktreeBranch}'`,
759
+ recoveryHint: 'Inspect the worktree branch and mesh metadata before retrying cleanup.',
760
+ };
761
+ }
762
+
763
+ const forceFallbackConvergence = await this.getWorktreeForceCleanupConvergence({
764
+ repoRoot,
765
+ workspace,
766
+ node: args.node,
767
+ });
768
+
769
+ try {
770
+ const result = await removeWorktree(repoRoot, workspace, {
771
+ requireClean: true,
772
+ allowSubmoduleForceFallback: forceFallbackConvergence.allow,
773
+ });
774
+ return {
775
+ success: true,
776
+ removedPath: result.removedPath,
777
+ repoRoot,
778
+ ...(result.fallback ? {
779
+ fallback: result.fallback,
780
+ forced: result.forced,
781
+ reason: result.reason,
782
+ convergence: forceFallbackConvergence,
783
+ } : {}),
784
+ };
785
+ } catch (e: any) {
786
+ const message = String(e?.message || e || 'worktree cleanup failed');
787
+ const dirty = message.includes('dirty worktree') || message.includes('local changes');
788
+ const submoduleForceBlocked = /working trees containing submodules cannot be moved or removed/i.test(message) && !forceFallbackConvergence.allow;
789
+ return {
790
+ success: false,
791
+ code: dirty
792
+ ? 'mesh_worktree_cleanup_dirty'
793
+ : submoduleForceBlocked
794
+ ? 'mesh_worktree_cleanup_force_fallback_blocked'
795
+ : 'mesh_worktree_cleanup_failed',
796
+ error: submoduleForceBlocked
797
+ ? `${message}; refusing --force fallback because convergence could not be verified: ${forceFallbackConvergence.error || 'unknown convergence state'}`
798
+ : message,
799
+ recoveryHint: dirty
800
+ ? 'Commit, stash, or intentionally discard the worktree changes before retrying mesh_remove_node. The mesh registry entry is preserved until cleanup is safe.'
801
+ : submoduleForceBlocked
802
+ ? 'Verify the worktree branch is merged/contained in the source default branch (for example origin/main) or mark the node with a safe branchConvergence final state before retrying. The mesh registry entry is preserved.'
803
+ : 'Inspect git worktree status/list from the source repo and retry after resolving the reported cleanup failure.',
804
+ ...(submoduleForceBlocked ? { convergence: forceFallbackConvergence } : {}),
805
+ };
806
+ }
807
+ }
808
+
809
+ private async getWorktreeForceCleanupConvergence(args: {
810
+ repoRoot: string;
811
+ workspace: string;
812
+ node: any;
813
+ }): Promise<{ allow: boolean; status?: string; source?: string; ref?: string; error?: string }> {
814
+ const metadataStatus = typeof args.node?.branchConvergence?.status === 'string'
815
+ ? args.node.branchConvergence.status
816
+ : '';
817
+ if (metadataStatus === 'merged_to_main' || metadataStatus === 'cleanup_candidate') {
818
+ return { allow: true, status: metadataStatus, source: 'node_branch_convergence' };
819
+ }
820
+
821
+ const { execFile } = await import('node:child_process');
822
+ const { promisify } = await import('node:util');
823
+ const execFileAsync = promisify(execFile);
824
+ const runGit = async (gitArgs: string[], cwd: string): Promise<string> => {
825
+ const { stdout } = await execFileAsync('git', gitArgs, {
826
+ cwd,
827
+ encoding: 'utf8',
828
+ timeout: 30_000,
829
+ maxBuffer: 4 * 1024 * 1024,
830
+ windowsHide: true,
831
+ });
832
+ return String(stdout || '').trim();
833
+ };
834
+
835
+ let head = '';
836
+ try {
837
+ head = await runGit(['rev-parse', 'HEAD'], args.workspace);
838
+ } catch (e: any) {
839
+ return { allow: false, error: `could not resolve worktree HEAD: ${e?.message || e}` };
840
+ }
841
+ if (!head) return { allow: false, error: 'worktree HEAD is empty' };
842
+
843
+ const candidateRefs: string[] = [];
844
+ try {
845
+ const defaultBranch = await runGit(['branch', '--show-current'], args.repoRoot);
846
+ if (defaultBranch) {
847
+ candidateRefs.push(defaultBranch, `origin/${defaultBranch}`);
848
+ }
849
+ } catch { /* fall through to common refs */ }
850
+ candidateRefs.push('origin/main', 'origin/master', 'main', 'master');
851
+
852
+ const seen = new Set<string>();
853
+ const checkedRefs: string[] = [];
854
+ for (const ref of candidateRefs) {
855
+ if (!ref || seen.has(ref)) continue;
856
+ seen.add(ref);
857
+ let commit = '';
858
+ try {
859
+ commit = await runGit(['rev-parse', '--verify', `${ref}^{commit}`], args.repoRoot);
860
+ } catch {
861
+ continue;
862
+ }
863
+ checkedRefs.push(ref);
864
+ try {
865
+ await runGit(['merge-base', '--is-ancestor', head, commit], args.repoRoot);
866
+ return { allow: true, status: 'merged_to_default_ref', source: 'git_merge_base', ref };
867
+ } catch {
868
+ // Not contained in this candidate ref; keep checking other safe refs.
869
+ }
870
+ }
871
+
872
+ return {
873
+ allow: false,
874
+ status: metadataStatus || undefined,
875
+ error: checkedRefs.length
876
+ ? `worktree HEAD is not contained in checked refs: ${checkedRefs.join(', ')}`
877
+ : 'no default/main refs were available for convergence verification',
878
+ };
879
+ }
880
+
391
881
  private isCompletedHostedSession(record: any): boolean {
392
882
  return record?.lifecycle === 'stopped' || record?.lifecycle === 'failed' || record?.lifecycle === 'interrupted';
393
883
  }
394
884
 
885
+ private async recordIntentionalMeshSessionStop(args: {
886
+ meshId: string;
887
+ nodeId: string;
888
+ node: any;
889
+ sessionId: string;
890
+ mode: RepoMeshSessionCleanupMode;
891
+ source: 'mesh_cleanup_sessions' | 'mesh_remove_node';
892
+ action: 'stop_session' | 'delete_session_force';
893
+ }): Promise<void> {
894
+ try {
895
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
896
+ appendLedgerEntry(args.meshId, {
897
+ kind: 'session_stopped',
898
+ nodeId: args.nodeId,
899
+ sessionId: args.sessionId,
900
+ payload: {
901
+ intentional: true,
902
+ reason: 'operator_cleanup',
903
+ intentionalStopReason: 'operator_cleanup',
904
+ source: args.source,
905
+ cleanupMode: args.mode,
906
+ action: args.action,
907
+ workspace: typeof args.node?.workspace === 'string' ? args.node.workspace : undefined,
908
+ },
909
+ });
910
+ } catch (e: any) {
911
+ LOG.warn('MeshCleanup', `Failed to record intentional cleanup stop for ${args.sessionId}: ${e?.message || e}`);
912
+ }
913
+ }
914
+
395
915
  private async cleanupMeshSessions(args: {
396
916
  meshId: string;
397
917
  nodeId: string;
@@ -399,6 +919,7 @@ export class DaemonCommandRouter {
399
919
  mode: RepoMeshSessionCleanupMode;
400
920
  sessionIds?: string[];
401
921
  dryRun?: boolean;
922
+ source?: 'mesh_cleanup_sessions' | 'mesh_remove_node';
402
923
  }): Promise<{ success: boolean; [key: string]: unknown }> {
403
924
  if (args.mode === 'preserve') {
404
925
  return { success: true, mode: 'preserve', matchedCount: 0, stoppedSessionIds: [], deletedSessionIds: [], skippedSessionIds: [] };
@@ -418,6 +939,21 @@ export class DaemonCommandRouter {
418
939
  const deleteUnsupportedSessionIds: string[] = [];
419
940
  const recordsRemainSessionIds: string[] = [];
420
941
  const errors: Array<{ sessionId: string; error: string }> = [];
942
+ const cleanupSource = args.source || 'mesh_cleanup_sessions';
943
+ const markedIntentionalStopSessionIds = new Set<string>();
944
+ const markIntentionalStop = async (sessionId: string, action: 'stop_session' | 'delete_session_force') => {
945
+ if (args.dryRun || markedIntentionalStopSessionIds.has(sessionId)) return;
946
+ markedIntentionalStopSessionIds.add(sessionId);
947
+ await this.recordIntentionalMeshSessionStop({
948
+ meshId: args.meshId,
949
+ nodeId: args.nodeId,
950
+ node: args.node,
951
+ sessionId,
952
+ mode: args.mode,
953
+ source: cleanupSource,
954
+ action,
955
+ });
956
+ };
421
957
  const matchedBySurfaceKind = {
422
958
  live_runtime: 0,
423
959
  recovery_snapshot: 0,
@@ -442,7 +978,10 @@ export class DaemonCommandRouter {
442
978
  try {
443
979
  if (args.mode === 'stop') {
444
980
  if (!completed) {
445
- if (!args.dryRun) await this.deps.sessionHostControl.stopSession(sessionId);
981
+ if (!args.dryRun) {
982
+ await markIntentionalStop(sessionId, 'stop_session');
983
+ await this.deps.sessionHostControl.stopSession(sessionId);
984
+ }
446
985
  stoppedSessionIds.push(sessionId);
447
986
  } else {
448
987
  skippedSessionIds.push(sessionId);
@@ -461,6 +1000,7 @@ export class DaemonCommandRouter {
461
1000
  }
462
1001
 
463
1002
  if (args.mode === 'stop_and_delete') {
1003
+ if (!completed) await markIntentionalStop(sessionId, 'delete_session_force');
464
1004
  if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: true });
465
1005
  deletedSessionIds.push(sessionId);
466
1006
  continue;
@@ -473,6 +1013,7 @@ export class DaemonCommandRouter {
473
1013
  recordsRemainSessionIds.push(sessionId);
474
1014
  if (args.mode === 'stop_and_delete' && !completed) {
475
1015
  try {
1016
+ await markIntentionalStop(sessionId, 'stop_session');
476
1017
  await this.deps.sessionHostControl.stopSession(sessionId);
477
1018
  stoppedSessionIds.push(sessionId);
478
1019
  } catch (stopError: any) {
@@ -1331,6 +1872,102 @@ export class DaemonCommandRouter {
1331
1872
  }
1332
1873
  }
1333
1874
 
1875
+ case 'get_mesh_ledger_slice': {
1876
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1877
+ if (!meshId) return { success: false, error: 'meshId required' };
1878
+ try {
1879
+ const { readLedgerSlice } = await import('../mesh/mesh-ledger.js');
1880
+ const kind = Array.isArray(args?.kind) ? args.kind.filter((k: any) => typeof k === 'string') : undefined;
1881
+ const slice = readLedgerSlice(meshId, {
1882
+ afterId: typeof args?.afterId === 'string' ? args.afterId : undefined,
1883
+ since: typeof args?.since === 'string' ? args.since : undefined,
1884
+ kind,
1885
+ limit: typeof args?.limit === 'number' ? args.limit : undefined,
1886
+ });
1887
+ return { success: true, slice };
1888
+ } catch (e: any) {
1889
+ return { success: false, error: e.message };
1890
+ }
1891
+ }
1892
+
1893
+ case 'import_mesh_ledger_slice': {
1894
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1895
+ if (!meshId) return { success: false, error: 'meshId required' };
1896
+ try {
1897
+ const { appendRemoteLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
1898
+ const entries = Array.isArray(args?.entries)
1899
+ ? args.entries as any[]
1900
+ : Array.isArray(args?.slice?.entries)
1901
+ ? args.slice.entries as any[]
1902
+ : [];
1903
+ const result = appendRemoteLedgerEntries(meshId, entries as any);
1904
+ return { success: true, result, summary: getLedgerSummary(meshId) };
1905
+ } catch (e: any) {
1906
+ return { success: false, error: e.message };
1907
+ }
1908
+ }
1909
+
1910
+ case 'get_mesh_queue': {
1911
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1912
+ if (!meshId) return { success: false, error: 'meshId required' };
1913
+ try {
1914
+ const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
1915
+ const status = Array.isArray(args?.status)
1916
+ ? args.status.map((s: any) => typeof s === 'string' ? s.trim() : '').filter(Boolean)
1917
+ : undefined;
1918
+ const queue = getQueue(meshId, { status: status as any });
1919
+ const summary = getMeshQueueStats(meshId);
1920
+ return {
1921
+ success: true,
1922
+ queue,
1923
+ summary,
1924
+ sourceOfTruth: {
1925
+ kind: 'mesh_work_queue_file',
1926
+ activeStatuses: ['pending', 'assigned'],
1927
+ historicalStatuses: ['completed', 'failed', 'cancelled'],
1928
+ notes: 'pending/assigned are active work; completed/failed/cancelled are historical records.',
1929
+ },
1930
+ };
1931
+ } catch (e: any) {
1932
+ return { success: false, error: e.message };
1933
+ }
1934
+ }
1935
+
1936
+ case 'cancel_mesh_queue_task': {
1937
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1938
+ const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
1939
+ if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
1940
+ try {
1941
+ const { cancelTask } = await import('../mesh/mesh-work-queue.js');
1942
+ const reason = typeof args?.reason === 'string' ? args.reason : undefined;
1943
+ const task = cancelTask(meshId, taskId, { reason });
1944
+ if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
1945
+ return { success: true, task };
1946
+ } catch (e: any) {
1947
+ return { success: false, error: e.message };
1948
+ }
1949
+ }
1950
+
1951
+ case 'requeue_mesh_queue_task': {
1952
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1953
+ const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
1954
+ if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
1955
+ try {
1956
+ const { requeueTask } = await import('../mesh/mesh-work-queue.js');
1957
+ const task = requeueTask(meshId, taskId, {
1958
+ reason: typeof args?.reason === 'string' ? args.reason : undefined,
1959
+ targetNodeId: typeof args?.targetNodeId === 'string' ? args.targetNodeId.trim() : undefined,
1960
+ targetSessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : undefined,
1961
+ clearTargetNode: args?.clearTargetNode === true,
1962
+ clearTargetSession: args?.clearTargetSession !== false,
1963
+ });
1964
+ if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
1965
+ return { success: true, task };
1966
+ } catch (e: any) {
1967
+ return { success: false, error: e.message };
1968
+ }
1969
+ }
1970
+
1334
1971
  case 'add_mesh_node': {
1335
1972
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1336
1973
  const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
@@ -1403,6 +2040,7 @@ export class DaemonCommandRouter {
1403
2040
  mode,
1404
2041
  sessionIds,
1405
2042
  dryRun: args?.dryRun === true,
2043
+ source: 'mesh_cleanup_sessions',
1406
2044
  });
1407
2045
  return result;
1408
2046
  } catch (e: any) {
@@ -1441,10 +2079,62 @@ export class DaemonCommandRouter {
1441
2079
  const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
1442
2080
  const baseBranch = baseBranchStdout.trim();
1443
2081
 
2082
+ const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
2083
+ if (validationSummary.status === 'failed') {
2084
+ return {
2085
+ success: false,
2086
+ code: 'validation_failed',
2087
+ convergenceStatus: 'blocked_review',
2088
+ error: 'Refinery validation gate failed; merge/refine was not attempted.',
2089
+ branch,
2090
+ into: baseBranch,
2091
+ validationSummary,
2092
+ finalBranchConvergenceState: {
2093
+ branch,
2094
+ baseBranch,
2095
+ merged: false,
2096
+ removed: false,
2097
+ validation: 'failed',
2098
+ status: 'blocked_review',
2099
+ },
2100
+ };
2101
+ }
2102
+ if (validationSummary.status === 'skipped') {
2103
+ return {
2104
+ success: false,
2105
+ code: 'validation_unavailable',
2106
+ convergenceStatus: 'blocked_review',
2107
+ error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
2108
+ branch,
2109
+ into: baseBranch,
2110
+ validationSummary,
2111
+ finalBranchConvergenceState: {
2112
+ branch,
2113
+ baseBranch,
2114
+ merged: false,
2115
+ removed: false,
2116
+ validation: 'unavailable',
2117
+ status: 'blocked_review',
2118
+ },
2119
+ };
2120
+ }
2121
+
1444
2122
  try {
1445
2123
  await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
1446
2124
  } catch (e: any) {
1447
- return { success: false, error: `Merge failed (conflicts?): ${e.message}` };
2125
+ return {
2126
+ success: false,
2127
+ error: `Merge failed (conflicts?): ${e.message}`,
2128
+ validationSummary,
2129
+ finalBranchConvergenceState: {
2130
+ branch,
2131
+ baseBranch,
2132
+ merged: false,
2133
+ removed: false,
2134
+ validation: 'passed',
2135
+ status: 'not_mergeable',
2136
+ },
2137
+ };
1448
2138
  }
1449
2139
 
1450
2140
  const removeResult = await this.execute('remove_mesh_node', {
@@ -1459,11 +2149,27 @@ export class DaemonCommandRouter {
1459
2149
  appendLedgerEntry(meshId, {
1460
2150
  kind: 'node_removed',
1461
2151
  nodeId,
1462
- payload: { refined: true, mergedBranch: branch, into: baseBranch },
2152
+ payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
1463
2153
  });
1464
2154
  } catch {}
1465
2155
 
1466
- return { success: true, merged: true, branch, into: baseBranch, removeResult };
2156
+ return {
2157
+ success: true,
2158
+ merged: true,
2159
+ branch,
2160
+ into: baseBranch,
2161
+ removeResult,
2162
+ validationSummary,
2163
+ finalBranchConvergenceState: {
2164
+ branch: baseBranch,
2165
+ mergedBranch: branch,
2166
+ baseBranch,
2167
+ merged: true,
2168
+ removed: removeResult?.success !== false,
2169
+ validation: 'passed',
2170
+ status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
2171
+ },
2172
+ };
1467
2173
  } catch (e: any) {
1468
2174
  return { success: false, error: e.message };
1469
2175
  }
@@ -1483,25 +2189,25 @@ export class DaemonCommandRouter {
1483
2189
  );
1484
2190
  let sessionCleanup: Record<string, unknown> | undefined;
1485
2191
  if (node && sessionCleanupMode !== 'preserve') {
1486
- sessionCleanup = await this.cleanupMeshSessions({ meshId, nodeId, node, mode: sessionCleanupMode });
2192
+ sessionCleanup = await this.cleanupMeshSessions({ meshId, nodeId, node, mode: sessionCleanupMode, source: 'mesh_remove_node' });
1487
2193
  if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
1488
2194
  }
1489
2195
 
1490
- // If this is a worktree node, clean up the git worktree first
1491
- if (node?.isLocalWorktree && node.workspace) {
1492
- try {
1493
- const sourceNode = node.clonedFromNodeId
1494
- ? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
1495
- : mesh?.nodes.find((n: any) => !n.isLocalWorktree);
1496
- const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
1497
- if (repoRoot) {
1498
- const { removeWorktree } = await import('../git/git-worktree.js');
1499
- await removeWorktree(repoRoot, node.workspace);
1500
- }
1501
- } catch (e: any) {
1502
- LOG.warn('MeshNode', `Worktree cleanup failed for ${nodeId}: ${e.message}`);
1503
- // Continue with node removal even if worktree cleanup fails
2196
+ let worktreeCleanup: Record<string, unknown> | undefined;
2197
+ if (node?.isLocalWorktree) {
2198
+ const cleanupResult = await this.cleanupLocalWorktreeNode({ mesh, node, nodeId });
2199
+ if (cleanupResult.success === false) {
2200
+ return {
2201
+ success: false,
2202
+ removed: false,
2203
+ code: cleanupResult.code,
2204
+ error: cleanupResult.error,
2205
+ recoveryHint: cleanupResult.recoveryHint,
2206
+ ...(sessionCleanup ? { sessionCleanup } : {}),
2207
+ worktreeCleanup: cleanupResult,
2208
+ };
1504
2209
  }
2210
+ worktreeCleanup = cleanupResult;
1505
2211
  }
1506
2212
 
1507
2213
  let removed = false;
@@ -1519,12 +2225,21 @@ export class DaemonCommandRouter {
1519
2225
  appendLedgerEntry(meshId, {
1520
2226
  kind: 'node_removed',
1521
2227
  nodeId,
1522
- payload: { worktree: !!node?.isLocalWorktree, sessionCleanupMode },
2228
+ payload: {
2229
+ worktree: !!node?.isLocalWorktree,
2230
+ sessionCleanupMode,
2231
+ workspace: typeof node?.workspace === 'string' ? node.workspace : undefined,
2232
+ daemonId: typeof node?.daemonId === 'string' ? node.daemonId : undefined,
2233
+ worktreeBranch: typeof node?.worktreeBranch === 'string' ? node.worktreeBranch : undefined,
2234
+ worktreeCleanupFallback: typeof worktreeCleanup?.fallback === 'string' ? worktreeCleanup.fallback : undefined,
2235
+ forced: worktreeCleanup?.forced === true ? true : undefined,
2236
+ forceFallbackReason: typeof worktreeCleanup?.reason === 'string' ? worktreeCleanup.reason : undefined,
2237
+ },
1523
2238
  });
1524
2239
  } catch { /* ledger append is best-effort */ }
1525
2240
  }
1526
2241
 
1527
- return { success: true, removed, ...(sessionCleanup ? { sessionCleanup } : {}) };
2242
+ return { success: true, removed, ...(sessionCleanup ? { sessionCleanup } : {}), ...(worktreeCleanup ? { worktreeCleanup } : {}) };
1528
2243
  } catch (e: any) {
1529
2244
  return { success: false, error: e.message };
1530
2245
  }
@@ -1564,6 +2279,7 @@ export class DaemonCommandRouter {
1564
2279
  workspace: result.worktreePath,
1565
2280
  repoRoot: result.worktreePath,
1566
2281
  daemonId: sourceNode.daemonId,
2282
+ machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
1567
2283
  userOverrides: { ...(sourceNode.userOverrides || {}) },
1568
2284
  policy: { ...(sourceNode.policy || {}) },
1569
2285
  isLocalWorktree: true,
@@ -1577,6 +2293,7 @@ export class DaemonCommandRouter {
1577
2293
  workspace: result.worktreePath,
1578
2294
  repoRoot: result.worktreePath,
1579
2295
  daemonId: sourceNode.daemonId,
2296
+ machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
1580
2297
  userOverrides: { ...(sourceNode.userOverrides || {}) },
1581
2298
  isLocalWorktree: true,
1582
2299
  worktreeBranch: result.branch,
@@ -1711,6 +2428,105 @@ export class DaemonCommandRouter {
1711
2428
  };
1712
2429
  }
1713
2430
 
2431
+ // ─── CLI-command MCP registration (Codex, Gemini CLI) ───────────
2432
+ if (coordinatorSetup.kind === 'cli_command') {
2433
+ // Build coordinator prompt first — fail closed on errors.
2434
+ let cliCmdSystemPrompt = '';
2435
+ try {
2436
+ cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
2437
+ } catch (error: any) {
2438
+ const message = error?.message || String(error);
2439
+ LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
2440
+ return {
2441
+ success: false,
2442
+ code: 'mesh_coordinator_prompt_failed',
2443
+ error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
2444
+ meshId, cliType, workspace,
2445
+ };
2446
+ }
2447
+
2448
+ // Run the provider's MCP registration command.
2449
+ try {
2450
+ const { execFileSync: execCmdSync } = await import('node:child_process');
2451
+ const cmdParts = coordinatorSetup.command.trim().split(/\s+/);
2452
+ const [regCmd, ...regArgs] = cmdParts;
2453
+ LOG.info('MeshCoordinator', `Running MCP registration: ${coordinatorSetup.command}`);
2454
+ execCmdSync(regCmd, regArgs, { stdio: 'pipe', timeout: 15_000 });
2455
+ } catch (error: any) {
2456
+ // Non-fatal — server may already be registered (providers return exit 1 on duplicate).
2457
+ LOG.warn('MeshCoordinator', `MCP registration command failed (may be pre-registered): ${error?.message || error}`);
2458
+ }
2459
+
2460
+ // Inject system prompt using provider-native methods.
2461
+ // Codex: -c 'instructions="..."' CLI config override
2462
+ // Gemini: write GEMINI.md to workspace (auto-loaded as context)
2463
+ const cliCmdArgs: string[] = [];
2464
+ const cliCmdEnv: Record<string, string> = {};
2465
+ if (cliCmdSystemPrompt) {
2466
+ if (cliType === 'codex-cli') {
2467
+ // Codex reads `developer_instructions` from config.toml as system instructions.
2468
+ // The -c flag overrides a config key for this session only.
2469
+ cliCmdArgs.push('-c', `developer_instructions=${JSON.stringify(cliCmdSystemPrompt)}`);
2470
+ } else if (cliType === 'gemini-cli') {
2471
+ // Gemini CLI auto-loads GEMINI.md from CWD as project context.
2472
+ // Write a temporary GEMINI.md to the workspace before launch.
2473
+ try {
2474
+ const { writeFileSync: wfs, existsSync: efs, readFileSync: rfs } = await import('node:fs');
2475
+ const geminiMdPath = `${workspace}/GEMINI.md`;
2476
+ const marker = '<!-- adhdev-mesh-coordinator-prompt -->';
2477
+ const markerEnd = '<!-- /adhdev-mesh-coordinator-prompt -->';
2478
+ const block = `${marker}\n${cliCmdSystemPrompt}\n${markerEnd}`;
2479
+ if (efs(geminiMdPath)) {
2480
+ const existing = rfs(geminiMdPath, 'utf-8');
2481
+ // Replace existing block or append
2482
+ const replaced = existing.replace(
2483
+ new RegExp(`${marker}[\\s\\S]*?${markerEnd}`, 'g'),
2484
+ block,
2485
+ );
2486
+ wfs(geminiMdPath, replaced.includes(marker) ? replaced : `${existing}\n\n${block}`);
2487
+ } else {
2488
+ wfs(geminiMdPath, block);
2489
+ }
2490
+ LOG.info('MeshCoordinator', `Wrote coordinator prompt to ${workspace}/GEMINI.md`);
2491
+ } catch (e: any) {
2492
+ LOG.warn('MeshCoordinator', `Could not write GEMINI.md: ${e?.message || e}`);
2493
+ }
2494
+ }
2495
+ }
2496
+
2497
+ const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
2498
+ cliType,
2499
+ dir: workspace,
2500
+ cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
2501
+ env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
2502
+ settings: { meshCoordinatorFor: meshId },
2503
+ });
2504
+
2505
+ if (!cliCmdLaunch?.success) {
2506
+ return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
2507
+ }
2508
+
2509
+ LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
2510
+ try {
2511
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
2512
+ appendLedgerEntry(meshId, {
2513
+ kind: 'coordinator_started',
2514
+ sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
2515
+ providerType: cliType,
2516
+ payload: { workspace },
2517
+ });
2518
+ } catch { /* best-effort */ }
2519
+
2520
+ return {
2521
+ success: true,
2522
+ meshId,
2523
+ cliType,
2524
+ workspace,
2525
+ sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
2526
+ mcpRegistered: true,
2527
+ };
2528
+ }
2529
+
1714
2530
  const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
1715
2531
  if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
1716
2532
  return {
@@ -1777,9 +2593,11 @@ export class DaemonCommandRouter {
1777
2593
  args: coordinatorSetup.mcpServer.args,
1778
2594
  };
1779
2595
  if (args?.inlineMesh) {
2596
+ const modeArgIndex = coordinatorSetup.mcpServer.args.findIndex((value: string) => value === '--mode');
2597
+ const mcpTransport = modeArgIndex >= 0 ? coordinatorSetup.mcpServer.args[modeArgIndex + 1] : 'ipc';
1780
2598
  mcpServerEntry.env = {
1781
2599
  ADHDEV_INLINE_MESH: JSON.stringify(mesh),
1782
- ADHDEV_MCP_TRANSPORT: 'ipc',
2600
+ ADHDEV_MCP_TRANSPORT: mcpTransport === 'local' ? 'local' : 'ipc',
1783
2601
  };
1784
2602
  }
1785
2603
 
@@ -1801,7 +2619,10 @@ export class DaemonCommandRouter {
1801
2619
  if (hadExistingMcpConfig) {
1802
2620
  try {
1803
2621
  const parsedExistingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
1804
- existingMcpConfig = { ...existingMcpConfig, ...parsedExistingMcpConfig };
2622
+ const existingCoordinatorConfig = hermesManualFallback
2623
+ ? stripHermesCoordinatorTempModelProviderOverrides(parsedExistingMcpConfig)
2624
+ : parsedExistingMcpConfig;
2625
+ existingMcpConfig = { ...existingMcpConfig, ...existingCoordinatorConfig };
1805
2626
  copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
1806
2627
  } catch (error: any) {
1807
2628
  LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
@@ -1893,6 +2714,123 @@ export class DaemonCommandRouter {
1893
2714
  }
1894
2715
  }
1895
2716
 
2717
+ case 'mesh_status': {
2718
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2719
+ if (!meshId) return { success: false, error: 'meshId required' };
2720
+ try {
2721
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
2722
+ const mesh = meshRecord?.mesh;
2723
+ if (!mesh) return { success: false, error: 'Mesh not found' };
2724
+
2725
+ const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
2726
+ const queue = getQueue(meshId);
2727
+ const queueSummary = getMeshQueueStats(meshId);
2728
+
2729
+ const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
2730
+ const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
2731
+ const ledgerSummary = getLedgerSummary(meshId);
2732
+
2733
+ const nodeStatuses = [];
2734
+ for (const node of mesh.nodes || []) {
2735
+ const status: Record<string, unknown> = {
2736
+ nodeId: node.id || node.nodeId,
2737
+ machineLabel: node.machineLabel || node.id || node.nodeId,
2738
+ workspace: node.workspace,
2739
+ repoRoot: node.repoRoot,
2740
+ isLocalWorktree: node.isLocalWorktree,
2741
+ worktreeBranch: node.worktreeBranch,
2742
+ daemonId: node.daemonId,
2743
+ machineId: node.machineId,
2744
+ health: 'unknown',
2745
+ providers: node.providers || [],
2746
+ activeSessions: [],
2747
+ };
2748
+ if (node.workspace && typeof node.workspace === 'string') {
2749
+ try {
2750
+ const { execFile } = await import('node:child_process');
2751
+ const { promisify } = await import('node:util');
2752
+ const execFileAsync = promisify(execFile);
2753
+
2754
+ const runGit = async (args: string[]): Promise<string> => {
2755
+ const result = await execFileAsync('git', ['-C', node.workspace as string, ...args], {
2756
+ encoding: 'utf8',
2757
+ timeout: 10_000,
2758
+ });
2759
+ return result.stdout.trim();
2760
+ };
2761
+
2762
+ const branch = await runGit(['branch', '--show-current']).catch(() => '');
2763
+ const porc = await runGit(['status', '--porcelain']).catch(() => '');
2764
+ const headCommit = await runGit(['rev-parse', '--short', 'HEAD']).catch(() => null);
2765
+ const headMessage = await runGit(['log', '-1', '--format=%s']).catch(() => null);
2766
+ const upstream = await runGit(['rev-parse', '--abbrev-ref', '@{upstream}']).catch(() => null);
2767
+ const aheadBehind = await runGit(['rev-list', '--left-right', '--count', '@{upstream}...HEAD']).catch(() => '');
2768
+ const stashCount = await runGit(['stash', 'list']).catch(() => '');
2769
+
2770
+ let ahead = 0, behind = 0;
2771
+ if (aheadBehind) {
2772
+ const parts = aheadBehind.split(/\s+/);
2773
+ if (parts.length >= 2) {
2774
+ behind = parseInt(parts[0], 10) || 0;
2775
+ ahead = parseInt(parts[1], 10) || 0;
2776
+ }
2777
+ }
2778
+
2779
+ const dirty = porc.length > 0;
2780
+ const lines = porc ? porc.split('\n').filter(Boolean) : [];
2781
+ let staged = 0, modified = 0, untracked = 0, deleted = 0, renamed = 0;
2782
+ for (const line of lines) {
2783
+ const xy = line.slice(0, 2);
2784
+ if (xy[0] !== ' ' && xy[0] !== '?') staged++;
2785
+ if (xy[1] === 'M') modified++;
2786
+ if (xy[1] === 'D') deleted++;
2787
+ if (xy[0] === 'R' || xy[1] === 'R') renamed++;
2788
+ if (xy === '??') untracked++;
2789
+ }
2790
+
2791
+ status.git = {
2792
+ workspace: node.workspace,
2793
+ repoRoot: node.workspace,
2794
+ isGitRepo: true,
2795
+ branch: branch || null,
2796
+ headCommit,
2797
+ headMessage,
2798
+ upstream,
2799
+ ahead,
2800
+ behind,
2801
+ staged,
2802
+ modified,
2803
+ untracked,
2804
+ deleted,
2805
+ renamed,
2806
+ hasConflicts: false,
2807
+ conflictFiles: [],
2808
+ stashCount: stashCount ? stashCount.split('\n').filter(Boolean).length : 0,
2809
+ lastCheckedAt: Date.now(),
2810
+ };
2811
+ status.health = branch ? (dirty ? 'dirty' : 'online') : 'degraded';
2812
+ } catch {
2813
+ status.health = 'degraded';
2814
+ }
2815
+ }
2816
+ nodeStatuses.push(status);
2817
+ }
2818
+
2819
+ return {
2820
+ success: true,
2821
+ meshId: mesh.id,
2822
+ meshName: mesh.name,
2823
+ repoIdentity: mesh.repoIdentity,
2824
+ defaultBranch: mesh.defaultBranch,
2825
+ nodes: nodeStatuses,
2826
+ queue: { tasks: queue, summary: queueSummary },
2827
+ ledger: { entries: ledgerEntries, summary: ledgerSummary },
2828
+ };
2829
+ } catch (e: any) {
2830
+ return { success: false, error: e.message };
2831
+ }
2832
+ }
2833
+
1896
2834
  default:
1897
2835
  break;
1898
2836
  }