@adhdev/daemon-core 0.9.82-rc.47 → 0.9.82-rc.49

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.
@@ -39,6 +39,16 @@ import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../sessi
39
39
  import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
40
40
  import { buildSessionEntries } from '../status/builders.js';
41
41
  import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents } from '../mesh/mesh-events.js';
42
+ import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
43
+ import {
44
+ MESH_REFINE_CONFIG_LOCATIONS,
45
+ MESH_REFINE_CONFIG_SCHEMA,
46
+ loadMeshRefineConfig,
47
+ resolveMeshRefineValidationPlan,
48
+ suggestMeshRefineConfig,
49
+ validateMeshRefineConfig,
50
+ type MeshRefineValidationCommandPlan,
51
+ } from '../mesh/refine-config.js';
42
52
  import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
43
53
  import { getSessionCompletionMarker } from '../status/snapshot.js';
44
54
  import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
@@ -859,13 +869,7 @@ async function resolveProviderTypeFromPriority(args: {
859
869
  }
860
870
  type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
861
871
  type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
862
- type MeshRefineValidationCommand = {
863
- command: string;
864
- args: string[];
865
- displayCommand: string;
866
- category: string;
867
- source: string;
868
- };
872
+ type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
869
873
 
870
874
  type MeshRefineValidationSummary = {
871
875
  status: MeshRefineValidationStatus;
@@ -875,6 +879,10 @@ type MeshRefineValidationSummary = {
875
879
  skippedReason?: string;
876
880
  timeoutMs: number;
877
881
  outputLimitBytes: number;
882
+ configSource?: string;
883
+ configSourceType?: string;
884
+ suggestions?: unknown[];
885
+ suggestedConfig?: unknown;
878
886
  };
879
887
 
880
888
  type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
@@ -996,171 +1004,25 @@ async function runMeshRefinePatchEquivalenceGate(
996
1004
  }
997
1005
  }
998
1006
 
999
- function readPackageScripts(workspace: string): Record<string, string> {
1000
- try {
1001
- const packageJsonPath = pathJoin(workspace, 'package.json');
1002
- const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
1003
- return parsed?.scripts && typeof parsed.scripts === 'object' && !Array.isArray(parsed.scripts)
1004
- ? parsed.scripts as Record<string, string>
1005
- : {};
1006
- } catch {
1007
- return {};
1008
- }
1009
- }
1010
-
1011
- function tokenizeValidationCommand(command: string): string[] | null {
1012
- const trimmed = command.trim();
1013
- if (!trimmed) return null;
1014
- // Fail closed: the gate never hands shell syntax to a shell. Package-manager
1015
- // scripts are invoked via execFile(binary, args), and metacharacters/quotes are
1016
- // rejected before tokenization so `npm run test && rm -rf` cannot be smuggled in.
1017
- if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
1018
- const tokens = trimmed.split(/\s+/).filter(Boolean);
1019
- if (!tokens.length) return null;
1020
- if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
1021
- return tokens;
1022
- }
1023
-
1024
- function scriptMatchesValidationCategory(scriptName: string, category: string): boolean {
1025
- return scriptName === category || scriptName.startsWith(`${category}:`);
1026
- }
1027
-
1028
- function parsePackageManagerValidationCommand(
1029
- rawCommand: string,
1030
- category: string,
1031
- scripts: Record<string, string>,
1032
- source: string,
1033
- ): { command?: MeshRefineValidationCommand; rejected?: Record<string, unknown> } {
1034
- const tokens = tokenizeValidationCommand(rawCommand);
1035
- if (!tokens) {
1036
- return { rejected: { command: rawCommand, category, source, reason: 'unsafe command string is not allowlisted' } };
1037
- }
1038
-
1039
- const [binary, second, third, ...rest] = tokens;
1040
- let scriptName = '';
1041
- let command = binary;
1042
- let args: string[] = [];
1043
-
1044
- if ((binary === 'npm' || binary === 'pnpm' || binary === 'bun') && second === 'run' && third) {
1045
- scriptName = third;
1046
- args = ['run', scriptName, ...rest];
1047
- } else if (binary === 'npm' && second === 'test' && !third) {
1048
- scriptName = 'test';
1049
- args = ['test'];
1050
- } else if (binary === 'yarn' && second === 'run' && third) {
1051
- scriptName = third;
1052
- args = ['run', scriptName, ...rest];
1053
- } else if (binary === 'yarn' && second && !third) {
1054
- scriptName = second;
1055
- args = [scriptName];
1056
- } else {
1057
- return { rejected: { command: rawCommand, category, source, reason: 'command is not a supported package-manager script invocation' } };
1058
- }
1059
-
1060
- if (!scriptName || !Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
1061
- return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script is not declared in package.json' } };
1062
- }
1063
- if (!scriptMatchesValidationCategory(scriptName, category)) {
1064
- return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script name is outside the validation category allowlist' } };
1065
- }
1066
-
1007
+ function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
1008
+ const plan = resolveMeshRefineValidationPlan(mesh, workspace);
1067
1009
  return {
1068
- command: {
1069
- command,
1070
- args,
1071
- displayCommand: [command, ...args].join(' '),
1072
- category,
1073
- source,
1074
- },
1075
- };
1076
- }
1077
-
1078
- function collectProjectContextValidationCandidates(mesh: any): Array<{ command: string; category: string; source: string; confidence?: string }> {
1079
- const commands = mesh?.projectContext?.commands;
1080
- if (!commands || typeof commands !== 'object' || Array.isArray(commands)) return [];
1081
- const candidates: Array<{ command: string; category: string; source: string; confidence?: string }> = [];
1082
- for (const category of REFINE_VALIDATION_CATEGORIES) {
1083
- const entries = Array.isArray(commands[category]) ? commands[category] : [];
1084
- for (const entry of entries) {
1085
- if (typeof entry?.command !== 'string') continue;
1086
- candidates.push({
1087
- command: entry.command,
1088
- category,
1089
- source: typeof entry.sourcePath === 'string' ? entry.sourcePath : 'projectContext.commands',
1090
- confidence: typeof entry.confidence === 'string' ? entry.confidence : undefined,
1091
- });
1092
- }
1093
- }
1094
- return candidates.sort((a, b) => {
1095
- const rank = (value?: string) => value === 'high' ? 0 : value === 'medium' ? 1 : 2;
1096
- return rank(a.confidence) - rank(b.confidence);
1097
- });
1098
- }
1099
-
1100
- function collectPolicyValidationCandidates(mesh: any): Array<{ command: string; category: string; source: string }> {
1101
- const policy = mesh?.policy && typeof mesh.policy === 'object' && !Array.isArray(mesh.policy) ? mesh.policy : {};
1102
- const configured = Array.isArray(policy.validationCommands)
1103
- ? policy.validationCommands
1104
- : Array.isArray(policy.validationGate?.commands)
1105
- ? policy.validationGate.commands
1106
- : [];
1107
- return configured
1108
- .map((entry: any) => typeof entry === 'string' ? { command: entry, category: '', source: 'mesh.policy.validationCommands' } : entry)
1109
- .filter((entry: any) => entry && typeof entry.command === 'string')
1110
- .map((entry: any) => {
1111
- const commandText = entry.command.trim();
1112
- const category = REFINE_VALIDATION_CATEGORIES.find(cat => commandText.includes(` ${cat}`)) ?? '';
1113
- return { command: commandText, category, source: 'mesh.policy.validationCommands' };
1114
- })
1115
- .filter((entry: any) => !!entry.category);
1116
- }
1117
-
1118
- function selectMeshRefineValidationCommands(mesh: any, workspace: string): { commands: MeshRefineValidationCommand[]; rejectedCommands: Array<Record<string, unknown>>; source: string } {
1119
- const scripts = readPackageScripts(workspace);
1120
- const rejectedCommands: Array<Record<string, unknown>> = [];
1121
- const selected: MeshRefineValidationCommand[] = [];
1122
- const seen = new Set<string>();
1123
- const candidates = [
1124
- ...collectPolicyValidationCandidates(mesh),
1125
- ...collectProjectContextValidationCandidates(mesh),
1126
- ];
1127
-
1128
- for (const candidate of candidates) {
1129
- const parsed = parsePackageManagerValidationCommand(candidate.command, candidate.category, scripts, candidate.source);
1130
- if (parsed.rejected) {
1131
- rejectedCommands.push(parsed.rejected);
1132
- continue;
1133
- }
1134
- if (!parsed.command || seen.has(parsed.command.displayCommand)) continue;
1135
- selected.push(parsed.command);
1136
- seen.add(parsed.command.displayCommand);
1137
- if (selected.length >= REFINE_VALIDATION_MAX_COMMANDS) break;
1138
- }
1139
-
1140
- if (!selected.length && candidates.length === 0) {
1141
- for (const category of REFINE_VALIDATION_CATEGORIES) {
1142
- if (!Object.prototype.hasOwnProperty.call(scripts, category)) continue;
1143
- const fallback = parsePackageManagerValidationCommand(`npm run ${category}`, category, scripts, 'package.json:scripts');
1144
- if (fallback.command && !seen.has(fallback.command.displayCommand)) {
1145
- selected.push(fallback.command);
1146
- seen.add(fallback.command.displayCommand);
1147
- } else if (fallback.rejected) {
1148
- rejectedCommands.push(fallback.rejected);
1149
- }
1150
- if (selected.length >= 2) break;
1151
- }
1152
- }
1153
-
1154
- return {
1155
- commands: selected,
1156
- rejectedCommands,
1157
- source: selected.some(command => command.source === 'mesh.policy.validationCommands')
1158
- ? 'mesh_policy'
1159
- : selected.some(command => command.source !== 'package.json:scripts')
1160
- ? 'project_context'
1161
- : selected.length
1162
- ? 'package_json_scripts'
1163
- : 'unavailable',
1010
+ source: plan.source,
1011
+ sourceType: plan.sourceType,
1012
+ commands: plan.commands.map(command => ({
1013
+ displayCommand: command.displayCommand,
1014
+ category: command.category,
1015
+ source: command.source,
1016
+ cwd: command.cwd,
1017
+ timeoutMs: command.timeoutMs,
1018
+ })),
1019
+ unavailableReason: plan.unavailableReason,
1020
+ rejectedCommands: plan.rejectedCommands,
1021
+ suggestions: plan.suggestions,
1022
+ suggestedConfig: plan.suggestedConfig,
1023
+ note: plan.sourceType === 'unavailable'
1024
+ ? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
1025
+ : 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
1164
1026
  };
1165
1027
  }
1166
1028
 
@@ -1168,7 +1030,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
1168
1030
  const { execFile } = await import('node:child_process');
1169
1031
  const { promisify } = await import('node:util');
1170
1032
  const execFileAsync = promisify(execFile);
1171
- const selection = selectMeshRefineValidationCommands(mesh, workspace);
1033
+ const selection = resolveMeshRefineValidationPlan(mesh, workspace);
1172
1034
  const summary: MeshRefineValidationSummary = {
1173
1035
  status: 'skipped',
1174
1036
  required: true,
@@ -1177,22 +1039,28 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
1177
1039
  skippedReason: undefined,
1178
1040
  timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
1179
1041
  outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
1042
+ configSource: selection.source,
1043
+ configSourceType: selection.sourceType,
1044
+ suggestions: selection.suggestions,
1045
+ suggestedConfig: selection.suggestedConfig,
1180
1046
  };
1181
1047
 
1182
1048
  if (!selection.commands.length) {
1183
- summary.skippedReason = 'validation_unavailable: no allowlisted projectContext, mesh policy, or package.json build/test/typecheck/lint command was available';
1049
+ summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
1184
1050
  return summary;
1185
1051
  }
1186
1052
 
1187
1053
  for (const candidate of selection.commands) {
1188
1054
  const startedAt = Date.now();
1055
+ const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
1056
+ const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
1189
1057
  try {
1190
1058
  const result = await execFileAsync(candidate.command, candidate.args, {
1191
- cwd: workspace,
1059
+ cwd,
1192
1060
  encoding: 'utf8',
1193
- timeout: REFINE_VALIDATION_TIMEOUT_MS,
1061
+ timeout,
1194
1062
  maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
1195
- env: { ...process.env, CI: process.env.CI || '1' },
1063
+ env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
1196
1064
  });
1197
1065
  summary.commandsRun.push({
1198
1066
  command: candidate.command,
@@ -1200,6 +1068,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
1200
1068
  displayCommand: candidate.displayCommand,
1201
1069
  category: candidate.category,
1202
1070
  source: candidate.source,
1071
+ cwd,
1203
1072
  passed: true,
1204
1073
  exitCode: 0,
1205
1074
  durationMs: Date.now() - startedAt,
@@ -1213,6 +1082,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
1213
1082
  displayCommand: candidate.displayCommand,
1214
1083
  category: candidate.category,
1215
1084
  source: candidate.source,
1085
+ cwd,
1216
1086
  passed: false,
1217
1087
  exitCode: typeof error?.code === 'number' ? error.code : null,
1218
1088
  signal: typeof error?.signal === 'string' ? error.signal : null,
@@ -1466,6 +1336,50 @@ function summarizeSessionHostPruneResult(result: unknown): Record<string, unknow
1466
1336
  };
1467
1337
  }
1468
1338
 
1339
+ function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
1340
+ const raw = hostAddress.trim();
1341
+ if (!raw) throw new Error('hostAddress required');
1342
+ const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
1343
+ url.pathname = '/api/v1/command';
1344
+ url.search = '';
1345
+ url.hash = '';
1346
+ return url.toString();
1347
+ }
1348
+
1349
+ function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
1350
+ const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
1351
+ const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
1352
+ ? args.memberNode as Record<string, any>
1353
+ : null;
1354
+ const configured = Array.isArray(mesh?.nodes)
1355
+ ? (requestedNodeId
1356
+ ? mesh.nodes.find((node: any) => node?.id === requestedNodeId || node?.nodeId === requestedNodeId)
1357
+ : mesh.nodes[0])
1358
+ : null;
1359
+ const source = explicit || configured;
1360
+ const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
1361
+ ? source.workspace.trim()
1362
+ : typeof args?.workspace === 'string' && args.workspace.trim()
1363
+ ? args.workspace.trim()
1364
+ : process.cwd();
1365
+ if (!workspace) return null;
1366
+ const nodeId = typeof source?.id === 'string' && source.id.trim()
1367
+ ? source.id.trim()
1368
+ : typeof source?.nodeId === 'string' && source.nodeId.trim()
1369
+ ? source.nodeId.trim()
1370
+ : undefined;
1371
+ return {
1372
+ ...(nodeId ? { id: nodeId } : {}),
1373
+ workspace,
1374
+ ...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
1375
+ ...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
1376
+ ...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
1377
+ userOverrides: source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides) ? source.userOverrides : {},
1378
+ policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
1379
+ role: 'member',
1380
+ };
1381
+ }
1382
+
1469
1383
  export class DaemonCommandRouter {
1470
1384
  private deps: CommandRouterDeps;
1471
1385
  /** In-memory cache for cloud-originating meshes passed via inlineMesh.
@@ -1661,6 +1575,18 @@ export class DaemonCommandRouter {
1661
1575
  this.aggregateMeshStatusCache.delete(meshId);
1662
1576
  }
1663
1577
 
1578
+
1579
+ private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
1580
+ const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
1581
+ const mesh = meshRecord?.mesh;
1582
+ if (!mesh) return { success: false, error: 'Mesh not found' };
1583
+ const meshHost = resolveMeshHostStatus(mesh);
1584
+ if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
1585
+ return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
1586
+ }
1587
+ return null;
1588
+ }
1589
+
1664
1590
  private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
1665
1591
  if (!mesh || !Array.isArray(mesh.nodes) || !node?.id) return;
1666
1592
  const idx = mesh.nodes.findIndex((entry: any) => entry?.id === node.id || entry?.nodeId === node.id);
@@ -2876,7 +2802,10 @@ export class DaemonCommandRouter {
2876
2802
  if (!name) return { success: false, error: 'name required' };
2877
2803
  try {
2878
2804
  const { createMesh } = await import('../config/mesh-config.js');
2879
- const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy });
2805
+ const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
2806
+ ? args.meshHost
2807
+ : undefined;
2808
+ const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
2880
2809
  return { success: true, mesh };
2881
2810
  } catch (e: any) {
2882
2811
  return { success: false, error: e.message };
@@ -2893,6 +2822,7 @@ export class DaemonCommandRouter {
2893
2822
  if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
2894
2823
  if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
2895
2824
  if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
2825
+ if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
2896
2826
  if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
2897
2827
  const mesh = updateMesh(meshId, patch as any);
2898
2828
  if (!mesh) return { success: false, error: 'Mesh not found' };
@@ -2904,6 +2834,225 @@ export class DaemonCommandRouter {
2904
2834
  }
2905
2835
  }
2906
2836
 
2837
+ case 'get_mesh_host_pairing': {
2838
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2839
+ if (!meshId) return { success: false, error: 'meshId required' };
2840
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
2841
+ const mesh = meshRecord?.mesh;
2842
+ if (!mesh) return { success: false, error: 'Mesh not found' };
2843
+ const meshHost = resolveMeshHostStatus(mesh);
2844
+ const pairingStatus = meshHost.pairing?.status || 'not_configured';
2845
+ return {
2846
+ success: true,
2847
+ code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
2848
+ meshId,
2849
+ hostAddress: meshHost.hostAddress,
2850
+ meshHost,
2851
+ manualPairing: {
2852
+ status: pairingStatus,
2853
+ joinImplemented: true,
2854
+ protocol: 'standalone_command_direct_v1',
2855
+ description: 'Standalone manual pairing can save address/token metadata, apply a host join over direct standalone command HTTP or injected mesh command dispatch, and check persisted status. P2P signaling remains outside this slice.',
2856
+ },
2857
+ };
2858
+ }
2859
+
2860
+ case 'configure_mesh_host_pairing': {
2861
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2862
+ const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
2863
+ const token = typeof args?.token === 'string' ? args.token.trim() : '';
2864
+ if (!meshId) return { success: false, error: 'meshId required' };
2865
+ if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
2866
+ try {
2867
+ const { configureMeshHostPairing } = await import('../config/mesh-config.js');
2868
+ const configured = configureMeshHostPairing(meshId, { hostAddress, token });
2869
+ if (!configured) return { success: false, error: 'Mesh not found' };
2870
+ this.inlineMeshCache.set(meshId, configured.mesh);
2871
+ const meshHost = resolveMeshHostStatus(configured.mesh);
2872
+ return {
2873
+ success: true,
2874
+ code: 'mesh_host_pairing_pending',
2875
+ meshId,
2876
+ hostAddress: configured.hostAddress,
2877
+ meshHost,
2878
+ manualPairing: {
2879
+ status: meshHost.pairing?.status || 'pairing',
2880
+ joinImplemented: true,
2881
+ protocol: 'standalone_command_direct_v1',
2882
+ description: 'Manual Mesh Host pairing config was saved locally. Use join_mesh_host_pairing to apply it to the host. Raw token was not persisted.',
2883
+ },
2884
+ };
2885
+ } catch (e: any) {
2886
+ return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
2887
+ }
2888
+ }
2889
+
2890
+ case 'create_mesh_host_pairing_token': {
2891
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2892
+ if (!meshId) return { success: false, error: 'meshId required' };
2893
+ try {
2894
+ const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
2895
+ const created = createMeshHostPairingToken(meshId, {
2896
+ token: typeof args?.token === 'string' ? args.token : undefined,
2897
+ expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
2898
+ });
2899
+ if (!created) return { success: false, error: 'Mesh not found' };
2900
+ this.inlineMeshCache.set(meshId, created.mesh);
2901
+ this.invalidateAggregateMeshStatus(meshId);
2902
+ return {
2903
+ success: true,
2904
+ code: 'mesh_host_pairing_token_created',
2905
+ meshId,
2906
+ token: created.token,
2907
+ tokenId: created.tokenId,
2908
+ expiresAt: created.expiresAt,
2909
+ meshHost: resolveMeshHostStatus(created.mesh),
2910
+ warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
2911
+ };
2912
+ } catch (e: any) {
2913
+ return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
2914
+ }
2915
+ }
2916
+
2917
+ case 'apply_mesh_host_join': {
2918
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2919
+ const token = typeof args?.token === 'string' ? args.token.trim() : '';
2920
+ const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
2921
+ ? args.memberNode
2922
+ : null;
2923
+ if (!meshId) return { success: false, error: 'meshId required' };
2924
+ if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
2925
+ try {
2926
+ const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
2927
+ const applied = applyMeshHostJoinRequest(meshId, {
2928
+ token,
2929
+ memberNode: memberNode as any,
2930
+ memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
2931
+ });
2932
+ if (!applied) return { success: false, error: 'Mesh not found' };
2933
+ if (!applied.accepted) {
2934
+ return {
2935
+ success: false,
2936
+ code: 'mesh_host_join_rejected',
2937
+ meshId,
2938
+ tokenId: applied.tokenId,
2939
+ meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
2940
+ error: applied.reason,
2941
+ };
2942
+ }
2943
+ this.inlineMeshCache.set(meshId, applied.mesh);
2944
+ this.invalidateAggregateMeshStatus(meshId);
2945
+ try {
2946
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
2947
+ appendLedgerEntry(meshId, {
2948
+ kind: 'node_joined',
2949
+ nodeId: applied.node.id,
2950
+ payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
2951
+ });
2952
+ } catch { /* ledger append is best-effort */ }
2953
+ return {
2954
+ success: true,
2955
+ code: 'mesh_host_join_accepted',
2956
+ meshId,
2957
+ node: applied.node,
2958
+ tokenId: applied.tokenId,
2959
+ meshHost: resolveMeshHostStatus(applied.mesh),
2960
+ };
2961
+ } catch (e: any) {
2962
+ return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
2963
+ }
2964
+ }
2965
+
2966
+ case 'join_mesh_host_pairing': {
2967
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2968
+ const token = typeof args?.token === 'string' ? args.token.trim() : '';
2969
+ if (!meshId) return { success: false, error: 'meshId required' };
2970
+ if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
2971
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
2972
+ const mesh = meshRecord?.mesh;
2973
+ if (!mesh) return { success: false, error: 'Mesh not found' };
2974
+ const meshHost = resolveMeshHostStatus(mesh);
2975
+ if (meshHost.role !== 'member') {
2976
+ return { success: false, code: 'mesh_host_join_not_member', meshId, meshHost, error: 'join_mesh_host_pairing must run from a member daemon configured with a Mesh Host address/token.' };
2977
+ }
2978
+ try {
2979
+ const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
2980
+ const tokenId = tokenIdForManualPairing(token);
2981
+ if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
2982
+ return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
2983
+ }
2984
+ const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
2985
+ if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
2986
+ const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
2987
+ const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
2988
+ ? args.hostDaemonId.trim()
2989
+ : meshHost.hostDaemonId;
2990
+ let hostResult: any;
2991
+ let transport: string;
2992
+ if (hostDaemonId && this.deps.dispatchMeshCommand) {
2993
+ transport = 'mesh_command_dispatch';
2994
+ hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
2995
+ meshId: hostMeshId,
2996
+ token,
2997
+ memberMeshId: meshId,
2998
+ memberNode,
2999
+ });
3000
+ } else if (meshHost.hostAddress) {
3001
+ transport = 'standalone_http_command';
3002
+ const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
3003
+ const response = await fetch(commandUrl, {
3004
+ method: 'POST',
3005
+ headers: { 'Content-Type': 'application/json' },
3006
+ body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
3007
+ });
3008
+ hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
3009
+ if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
3010
+ } else {
3011
+ return {
3012
+ success: false,
3013
+ code: 'mesh_host_join_transport_unavailable',
3014
+ meshId,
3015
+ meshHost,
3016
+ error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
3017
+ };
3018
+ }
3019
+ if (!hostResult?.success) {
3020
+ return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
3021
+ }
3022
+ const joined = meshRecord.inline
3023
+ ? null
3024
+ : markMeshHostPairingJoined(meshId, {
3025
+ tokenId: hostResult.tokenId || tokenId,
3026
+ hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
3027
+ hostNodeId: hostResult.meshHost?.hostNodeId,
3028
+ joinedAt: hostResult.meshHost?.pairing?.joinedAt,
3029
+ });
3030
+ if (joined) {
3031
+ this.inlineMeshCache.set(meshId, joined.mesh);
3032
+ this.invalidateAggregateMeshStatus(meshId);
3033
+ }
3034
+ return {
3035
+ success: true,
3036
+ code: 'mesh_host_join_applied',
3037
+ meshId,
3038
+ hostMeshId,
3039
+ transport,
3040
+ node: hostResult.node,
3041
+ tokenId: hostResult.tokenId || tokenId,
3042
+ meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
3043
+ hostResult,
3044
+ manualPairing: {
3045
+ status: 'paired',
3046
+ joinImplemented: true,
3047
+ protocol: 'standalone_command_direct_v1',
3048
+ description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
3049
+ },
3050
+ };
3051
+ } catch (e: any) {
3052
+ return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
3053
+ }
3054
+ }
3055
+
2907
3056
  case 'delete_mesh': {
2908
3057
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2909
3058
  if (!meshId) return { success: false, error: 'meshId required' };
@@ -2997,6 +3146,8 @@ export class DaemonCommandRouter {
2997
3146
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2998
3147
  const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
2999
3148
  if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
3149
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
3150
+ if (ownerFailure) return ownerFailure;
3000
3151
  try {
3001
3152
  const { cancelTask } = await import('../mesh/mesh-work-queue.js');
3002
3153
  const reason = typeof args?.reason === 'string' ? args.reason : undefined;
@@ -3012,6 +3163,8 @@ export class DaemonCommandRouter {
3012
3163
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
3013
3164
  const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
3014
3165
  if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
3166
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
3167
+ if (ownerFailure) return ownerFailure;
3015
3168
  try {
3016
3169
  const { requeueTask } = await import('../mesh/mesh-work-queue.js');
3017
3170
  const task = requeueTask(meshId, taskId, {
@@ -3033,6 +3186,8 @@ export class DaemonCommandRouter {
3033
3186
  const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
3034
3187
  if (!meshId) return { success: false, error: 'meshId required' };
3035
3188
  if (!workspace) return { success: false, error: 'workspace required' };
3189
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
3190
+ if (ownerFailure) return ownerFailure;
3036
3191
  try {
3037
3192
  const { addNode } = await import('../config/mesh-config.js');
3038
3193
  const providerPriority = Array.isArray(args?.providerPriority)
@@ -3043,7 +3198,8 @@ export class DaemonCommandRouter {
3043
3198
  ...(readOnly ? { readOnly: true } : {}),
3044
3199
  ...(providerPriority.length ? { providerPriority } : {}),
3045
3200
  };
3046
- const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}) });
3201
+ const role = normalizeMeshDaemonRole(args?.role);
3202
+ const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}), ...(role ? { role } : {}) });
3047
3203
  if (!node) return { success: false, error: 'Mesh not found' };
3048
3204
  return { success: true, node };
3049
3205
  } catch (e: any) {
@@ -3055,6 +3211,8 @@ export class DaemonCommandRouter {
3055
3211
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
3056
3212
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
3057
3213
  if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
3214
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
3215
+ if (ownerFailure) return ownerFailure;
3058
3216
  try {
3059
3217
  const { updateNode } = await import('../config/mesh-config.js');
3060
3218
  const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
@@ -3083,6 +3241,8 @@ export class DaemonCommandRouter {
3083
3241
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
3084
3242
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
3085
3243
  if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
3244
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
3245
+ if (ownerFailure) return ownerFailure;
3086
3246
  try {
3087
3247
  const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
3088
3248
  const mesh = meshRecord?.mesh;
@@ -3108,6 +3268,57 @@ export class DaemonCommandRouter {
3108
3268
  }
3109
3269
  }
3110
3270
 
3271
+ case 'get_mesh_refine_config_schema': {
3272
+ return {
3273
+ success: true,
3274
+ schema: MESH_REFINE_CONFIG_SCHEMA,
3275
+ locations: MESH_REFINE_CONFIG_LOCATIONS,
3276
+ sourceOfTruth: 'repo mesh/refine config',
3277
+ heuristicRole: 'suggestions_only_not_execution_path',
3278
+ };
3279
+ }
3280
+
3281
+ case 'validate_mesh_refine_config': {
3282
+ const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
3283
+ const mesh = args?.inlineMesh || {};
3284
+ const loaded = args?.config !== undefined
3285
+ ? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
3286
+ : loadMeshRefineConfig(mesh, workspace);
3287
+ const validation = loaded.config
3288
+ ? validateMeshRefineConfig(loaded.config, loaded.source)
3289
+ : { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
3290
+ return { success: validation.valid, ...loaded, ...validation };
3291
+ }
3292
+
3293
+ case 'suggest_mesh_refine_config': {
3294
+ const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
3295
+ const mesh = args?.inlineMesh || {};
3296
+ return {
3297
+ success: true,
3298
+ ...suggestMeshRefineConfig(mesh, workspace),
3299
+ note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
3300
+ };
3301
+ }
3302
+
3303
+ case 'plan_mesh_refine_node': {
3304
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
3305
+ const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
3306
+ if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
3307
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
3308
+ const mesh = meshRecord?.mesh;
3309
+ const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
3310
+ if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
3311
+ return {
3312
+ success: true,
3313
+ dryRun: true,
3314
+ nodeId,
3315
+ workspace: node.workspace,
3316
+ validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
3317
+ mergeWillRun: false,
3318
+ cleanupWillRun: false,
3319
+ };
3320
+ }
3321
+
3111
3322
  case 'refine_mesh_node': {
3112
3323
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
3113
3324
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
@@ -3415,6 +3626,8 @@ export class DaemonCommandRouter {
3415
3626
  if (!meshId) return { success: false, error: 'meshId required' };
3416
3627
  if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
3417
3628
  if (!branch) return { success: false, error: 'branch required' };
3629
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
3630
+ if (ownerFailure) return ownerFailure;
3418
3631
 
3419
3632
  try {
3420
3633
  const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
@@ -3505,6 +3718,8 @@ export class DaemonCommandRouter {
3505
3718
  case 'trigger_mesh_queue': {
3506
3719
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
3507
3720
  if (!meshId) return { success: false, error: 'meshId required' };
3721
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
3722
+ if (ownerFailure) return ownerFailure;
3508
3723
  try {
3509
3724
  const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
3510
3725
  if (meshId) {
@@ -3536,6 +3751,15 @@ export class DaemonCommandRouter {
3536
3751
  mesh = getMesh(meshId);
3537
3752
  }
3538
3753
  if (!mesh) return { success: false, error: 'Mesh not found' };
3754
+ const meshHost = resolveMeshHostStatus(mesh);
3755
+ if (!meshHost.canOwnCoordinator) {
3756
+ return {
3757
+ success: false,
3758
+ ...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
3759
+ meshId,
3760
+ cliType,
3761
+ };
3762
+ }
3539
3763
  if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
3540
3764
 
3541
3765
  const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
@@ -3909,6 +4133,7 @@ export class DaemonCommandRouter {
3909
4133
  const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
3910
4134
  const mesh = meshRecord?.mesh;
3911
4135
  if (!mesh) return { success: false, error: 'Mesh not found' };
4136
+ const meshHost = resolveMeshHostStatus(mesh);
3912
4137
 
3913
4138
  const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
3914
4139
  const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
@@ -4030,6 +4255,7 @@ export class DaemonCommandRouter {
4030
4255
  repoRoot: node.repoRoot,
4031
4256
  isLocalWorktree: node.isLocalWorktree,
4032
4257
  worktreeBranch: node.worktreeBranch,
4258
+ role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
4033
4259
  daemonId,
4034
4260
  machineId: node.machineId,
4035
4261
  machineStatus: node.machineStatus,
@@ -4230,6 +4456,7 @@ export class DaemonCommandRouter {
4230
4456
  repoIdentity: mesh.repoIdentity,
4231
4457
  defaultBranch: mesh.defaultBranch,
4232
4458
  refreshedAt,
4459
+ meshHost,
4233
4460
  sourceOfTruth: {
4234
4461
  membership: meshRecord?.source === 'inline_cache'
4235
4462
  ? 'coordinator_inline_mesh_cache'
@@ -4237,6 +4464,13 @@ export class DaemonCommandRouter {
4237
4464
  ? 'local_mesh_config'
4238
4465
  : 'inline_bootstrap_snapshot',
4239
4466
  coordinatorOwnsLiveTruth: directTruthSatisfied,
4467
+ meshHost: {
4468
+ owner: 'mesh_host_daemon',
4469
+ localRole: meshHost.role,
4470
+ hostDaemonId: meshHost.hostDaemonId,
4471
+ hostNodeId: meshHost.hostNodeId,
4472
+ hostAddress: meshHost.hostAddress,
4473
+ },
4240
4474
  ...(requireDirectPeerTruth ? {
4241
4475
  currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
4242
4476
  directPeerTruth: {