@adhdev/daemon-core 0.9.82-rc.18 → 0.9.82-rc.180

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 (223) hide show
  1. package/dist/boot/process-hardening.d.ts +50 -0
  2. package/dist/chat/source-machine.d.ts +166 -0
  3. package/dist/chat/source-resolver.d.ts +104 -0
  4. package/dist/chat/subscription-updates.d.ts +1 -0
  5. package/dist/cli-adapter-types.d.ts +22 -2
  6. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  7. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  8. package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
  9. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  10. package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
  11. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  12. package/dist/commands/handler.d.ts +110 -0
  13. package/dist/commands/mesh-coordinator.d.ts +72 -1
  14. package/dist/commands/router.d.ts +22 -0
  15. package/dist/config/chat-history.d.ts +8 -0
  16. package/dist/config/config.d.ts +5 -0
  17. package/dist/config/mesh-config.d.ts +72 -1
  18. package/dist/git/git-commands.d.ts +5 -1
  19. package/dist/index.d.ts +35 -8
  20. package/dist/index.js +19937 -4800
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +20357 -5290
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/installer.d.ts +1 -4
  25. package/dist/ipc/local-ipc-server.d.ts +91 -0
  26. package/dist/launch.d.ts +1 -1
  27. package/dist/logging/async-batch-writer.d.ts +10 -0
  28. package/dist/mesh/beads-db.d.ts +72 -0
  29. package/dist/mesh/contracts.d.ts +164 -0
  30. package/dist/mesh/coordinator-prompt.d.ts +30 -0
  31. package/dist/mesh/coordinator-registry.d.ts +59 -0
  32. package/dist/mesh/mesh-active-work.d.ts +90 -0
  33. package/dist/mesh/mesh-events.d.ts +97 -5
  34. package/dist/mesh/mesh-fast-forward.d.ts +41 -0
  35. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  36. package/dist/mesh/mesh-ledger.d.ts +58 -1
  37. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  38. package/dist/mesh/mesh-work-queue.d.ts +54 -6
  39. package/dist/mesh/preview-freshness.d.ts +18 -0
  40. package/dist/mesh/refine-config.d.ts +193 -0
  41. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  42. package/dist/providers/approval-utils.d.ts +13 -0
  43. package/dist/providers/cli-provider-instance.d.ts +15 -3
  44. package/dist/providers/contracts.d.ts +98 -5
  45. package/dist/providers/external-sources.d.ts +71 -0
  46. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  47. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  48. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  49. package/dist/providers/native-history/dispatcher.d.ts +25 -0
  50. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  51. package/dist/providers/native-history/index.d.ts +13 -0
  52. package/dist/providers/provider-instance.d.ts +3 -0
  53. package/dist/providers/provider-loader.d.ts +26 -4
  54. package/dist/providers/provider-trust.d.ts +31 -0
  55. package/dist/providers/read-chat-contract.d.ts +29 -0
  56. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  57. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  58. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  59. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  60. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  61. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  62. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  63. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  64. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  65. package/dist/providers/sdk/v1/index.d.ts +30 -0
  66. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  67. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  68. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  69. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  70. package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
  71. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  72. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  73. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  74. package/dist/providers/spec/adapter.d.ts +56 -0
  75. package/dist/providers/spec/cli-adapter.d.ts +95 -0
  76. package/dist/providers/spec/driver.d.ts +159 -0
  77. package/dist/providers/spec/evaluator.d.ts +47 -0
  78. package/dist/providers/spec/loader.d.ts +14 -0
  79. package/dist/providers/spec/native-history-executor.d.ts +39 -0
  80. package/dist/providers/spec/route.d.ts +4 -0
  81. package/dist/providers/spec/schema.gen.d.ts +511 -0
  82. package/dist/providers/spec/types.d.ts +212 -0
  83. package/dist/providers/transcript-v2.d.ts +176 -0
  84. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  85. package/dist/repo-mesh-types.d.ts +102 -1
  86. package/dist/sessions/registry.d.ts +3 -0
  87. package/dist/shared-types.d.ts +42 -1
  88. package/dist/status/reporter.d.ts +2 -0
  89. package/dist/status/snapshot.d.ts +1 -0
  90. package/dist/types.d.ts +5 -0
  91. package/package.json +7 -2
  92. package/src/agent-stream/poller.ts +2 -3
  93. package/src/boot/daemon-lifecycle.ts +17 -10
  94. package/src/boot/process-hardening.ts +89 -0
  95. package/src/chat/source-machine.ts +534 -0
  96. package/src/chat/source-resolver.ts +0 -0
  97. package/src/chat/subscription-updates.ts +14 -1
  98. package/src/cli-adapter-types.d.ts +1 -0
  99. package/src/cli-adapter-types.ts +21 -2
  100. package/src/cli-adapters/cli-script-runner.ts +421 -0
  101. package/src/cli-adapters/cli-state-engine.ts +1086 -0
  102. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  103. package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
  104. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  105. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  106. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  107. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  108. package/src/cli-adapters/provider-cli-shared.ts +59 -11
  109. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  110. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  111. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  112. package/src/commands/chat-commands.ts +1597 -60
  113. package/src/commands/cli-manager.ts +214 -3
  114. package/src/commands/handler.ts +809 -2
  115. package/src/commands/mesh-coordinator.ts +231 -127
  116. package/src/commands/router.ts +3495 -502
  117. package/src/config/chat-history.ts +91 -24
  118. package/src/config/config.ts +12 -0
  119. package/src/config/mesh-config.ts +280 -2
  120. package/src/config/recent-activity.ts +8 -2
  121. package/src/daemon/dev-cli-debug.ts +10 -1
  122. package/src/detection/ide-detector.ts +26 -16
  123. package/src/git/git-commands.ts +17 -5
  124. package/src/git/git-worktree.ts +8 -1
  125. package/src/index.ts +113 -7
  126. package/src/installer.d.ts +1 -1
  127. package/src/installer.ts +8 -6
  128. package/src/ipc/local-ipc-server.ts +278 -0
  129. package/src/launch.d.ts +1 -1
  130. package/src/launch.ts +37 -28
  131. package/src/logging/async-batch-writer.ts +55 -0
  132. package/src/logging/logger.ts +2 -1
  133. package/src/mesh/beads-db.ts +486 -0
  134. package/src/mesh/contracts.ts +329 -0
  135. package/src/mesh/coordinator-prompt.ts +204 -30
  136. package/src/mesh/coordinator-registry.ts +121 -0
  137. package/src/mesh/mesh-active-work.ts +437 -0
  138. package/src/mesh/mesh-events.ts +982 -67
  139. package/src/mesh/mesh-fast-forward.ts +438 -0
  140. package/src/mesh/mesh-host-ownership.ts +73 -0
  141. package/src/mesh/mesh-ledger.ts +457 -104
  142. package/src/mesh/mesh-refine-status.ts +144 -0
  143. package/src/mesh/mesh-work-queue.ts +263 -159
  144. package/src/mesh/preview-freshness.ts +118 -0
  145. package/src/mesh/refine-config.ts +366 -0
  146. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  147. package/src/providers/approval-utils.d.ts +4 -0
  148. package/src/providers/approval-utils.ts +47 -5
  149. package/src/providers/chat-message-normalization.ts +4 -11
  150. package/src/providers/cli-provider-instance.ts +598 -55
  151. package/src/providers/contracts.ts +105 -5
  152. package/src/providers/external-sources.ts +218 -0
  153. package/src/providers/ide-provider-instance.ts +19 -5
  154. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  155. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  156. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  157. package/src/providers/native-history/dispatcher.ts +231 -0
  158. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  159. package/src/providers/native-history/index.ts +30 -0
  160. package/src/providers/provider-instance.ts +3 -0
  161. package/src/providers/provider-loader.ts +585 -50
  162. package/src/providers/provider-schema.ts +31 -13
  163. package/src/providers/provider-trust.ts +114 -0
  164. package/src/providers/read-chat-contract.ts +76 -16
  165. package/src/providers/sdk/README.md +49 -0
  166. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  167. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  168. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  169. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  170. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  171. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  172. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  173. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  174. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  175. package/src/providers/sdk/v1/index.ts +152 -0
  176. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  177. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  178. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  179. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +458 -0
  180. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  181. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  182. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  183. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  184. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  185. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  186. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  187. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  188. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  189. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  190. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  191. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  192. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  193. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  194. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  195. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  196. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  197. package/src/providers/sdk/v1/types/common/index.ts +210 -0
  198. package/src/providers/sdk/v1/validators/index.ts +19 -0
  199. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  200. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  201. package/src/providers/spec/adapter.ts +168 -0
  202. package/src/providers/spec/cli-adapter.ts +453 -0
  203. package/src/providers/spec/driver.ts +540 -0
  204. package/src/providers/spec/evaluator.ts +298 -0
  205. package/src/providers/spec/loader.ts +130 -0
  206. package/src/providers/spec/native-history-executor.ts +706 -0
  207. package/src/providers/spec/route.ts +51 -0
  208. package/src/providers/spec/schema.gen.ts +511 -0
  209. package/src/providers/spec/schema.json +211 -0
  210. package/src/providers/spec/types.ts +231 -0
  211. package/src/providers/transcript-v2.ts +567 -0
  212. package/src/providers/types/interactive-prompt.ts +297 -0
  213. package/src/providers/version-archive.ts +38 -20
  214. package/src/repo-mesh-types.ts +112 -1
  215. package/src/sessions/registry.ts +3 -0
  216. package/src/shared-types.ts +45 -1
  217. package/src/status/builders.ts +24 -6
  218. package/src/status/reporter.ts +15 -0
  219. package/src/status/snapshot.ts +84 -25
  220. package/src/system/host-memory.ts +29 -12
  221. package/src/types.ts +5 -0
  222. package/dist/mesh/mesh-sync.d.ts +0 -53
  223. package/src/mesh/mesh-sync.ts +0 -111
@@ -38,14 +38,38 @@ import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../l
38
38
  import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
39
39
  import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
40
40
  import { buildSessionEntries } from '../status/builders.js';
41
- import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents } from '../mesh/mesh-events.js';
41
+ import { registerMeshCoordinator, getCoordinatorForSession } from '../mesh/coordinator-registry.js';
42
+ import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
43
+ import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
44
+ import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
45
+ import { buildPreviewFreshness } from '../mesh/preview-freshness.js';
46
+ import { buildMeshAsyncRefineJobs } from '../mesh/mesh-refine-status.js';
47
+ import {
48
+ MESH_REFINE_CONFIG_LOCATIONS,
49
+ MESH_REFINE_CONFIG_SCHEMA,
50
+ loadMeshRefineConfig,
51
+ resolveMeshRefineValidationPlan,
52
+ suggestMeshRefineConfig,
53
+ validateMeshRefineConfig,
54
+ type MeshRefineValidationCommandPlan,
55
+ } from '../mesh/refine-config.js';
56
+ import {
57
+ MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
58
+ MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
59
+ loadMeshWorktreeBootstrapConfig,
60
+ runMeshWorktreeBootstrap,
61
+ type WorktreeBootstrapState,
62
+ } from '../mesh/worktree-bootstrap-config.js';
42
63
  import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
43
64
  import { getSessionCompletionMarker } from '../status/snapshot.js';
44
65
  import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
66
+ import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
45
67
  import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
46
- import { homedir } from 'os';
47
- import { join as pathJoin, resolve as pathResolve } from 'path';
68
+ import { homedir, hostname as osHostname } from 'os';
69
+ import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
48
70
  import * as fs from 'fs';
71
+ import { execFileSync } from 'node:child_process';
72
+ import { normalizeInteractivePromptResponse } from '../providers/types/interactive-prompt.js';
49
73
 
50
74
  type ReleaseChannel = 'stable' | 'preview';
51
75
  const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
@@ -114,14 +138,95 @@ function readBooleanValue(...values: unknown[]): boolean | undefined {
114
138
  return undefined;
115
139
  }
116
140
 
117
- function readGitSubmodules(value: unknown): GitSubmoduleStatus[] | undefined {
141
+ function summarizeRepoMeshDebugGit(git: unknown): Record<string, unknown> | null {
142
+ const record = readObjectRecord(git);
143
+ if (!Object.keys(record).length) return null;
144
+ const submodules = Array.isArray(record.submodules)
145
+ ? record.submodules.map((entry: any) => ({
146
+ path: readStringValue(entry?.path) ?? null,
147
+ commit: readStringValue(entry?.commit)?.slice(0, 12) ?? null,
148
+ dirty: readBooleanValue(entry?.dirty) ?? false,
149
+ outOfSync: readBooleanValue(entry?.outOfSync, entry?.out_of_sync) ?? false,
150
+ }))
151
+ : [];
152
+ return {
153
+ isGitRepo: readBooleanValue(record.isGitRepo),
154
+ workspace: readStringValue(record.workspace) ?? null,
155
+ repoRoot: readStringValue(record.repoRoot, record.repo_root) ?? null,
156
+ branch: readStringValue(record.branch) ?? null,
157
+ upstream: readStringValue(record.upstream) ?? null,
158
+ upstreamStatus: readStringValue(record.upstreamStatus, record.upstream_status) ?? null,
159
+ headCommit: readStringValue(record.headCommit, record.head_commit)?.slice(0, 12) ?? null,
160
+ ahead: readNumberValue(record.ahead) ?? null,
161
+ behind: readNumberValue(record.behind) ?? null,
162
+ dirtyCounts: {
163
+ staged: readNumberValue(record.staged) ?? 0,
164
+ modified: readNumberValue(record.modified) ?? 0,
165
+ untracked: readNumberValue(record.untracked) ?? 0,
166
+ deleted: readNumberValue(record.deleted) ?? 0,
167
+ renamed: readNumberValue(record.renamed) ?? 0,
168
+ },
169
+ lastCheckedAt: readNumberValue(record.lastCheckedAt, record.last_checked_at) ?? null,
170
+ submoduleCount: submodules.length,
171
+ submodules,
172
+ };
173
+ }
174
+
175
+ function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
176
+ const nodes = Array.isArray(status?.nodes) ? status.nodes : [];
177
+ return {
178
+ success: status?.success,
179
+ meshId: readStringValue(status?.meshId, status?.mesh_id) ?? null,
180
+ refreshedAt: readStringValue(status?.refreshedAt, status?.refreshed_at) ?? null,
181
+ sourceOfTruth: status?.sourceOfTruth ?? null,
182
+ branchConvergenceSummary: status?.branchConvergenceSummary ?? status?.branch_convergence_summary ?? null,
183
+ nodeCount: nodes.length,
184
+ nodes: nodes.map((node: any) => ({
185
+ nodeId: readStringValue(node?.nodeId, node?.id) ?? null,
186
+ daemonId: readStringValue(node?.daemonId, node?.daemon_id) ?? null,
187
+ workspace: readStringValue(node?.workspace, node?.git?.workspace) ?? null,
188
+ health: readStringValue(node?.health) ?? null,
189
+ machineStatus: readStringValue(node?.machineStatus, node?.machine_status) ?? null,
190
+ connection: node?.connection && typeof node.connection === 'object' ? {
191
+ state: readStringValue(node.connection.state) ?? null,
192
+ transport: readStringValue(node.connection.transport) ?? null,
193
+ source: readStringValue(node.connection.source) ?? null,
194
+ reported: readBooleanValue(node.connection.reported) ?? null,
195
+ } : null,
196
+ gitProbePending: node?.gitProbePending === true,
197
+ launchReady: node?.launchReady === true,
198
+ git: summarizeRepoMeshDebugGit(node?.git),
199
+ branchConvergence: node?.branchConvergence ?? node?.branch_convergence ?? null,
200
+ })),
201
+ };
202
+ }
203
+
204
+ function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
205
+ try {
206
+ LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${JSON.stringify({ event, ...fields })}`);
207
+ } catch {
208
+ LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${event}`);
209
+ }
210
+ }
211
+
212
+ function joinRepoPath(root: string | undefined, relativePath: string | undefined): string | undefined {
213
+ const normalizedRoot = typeof root === 'string' ? root.trim().replace(/[\\/]+$/, '') : '';
214
+ const normalizedPath = typeof relativePath === 'string' ? relativePath.trim() : '';
215
+ if (!normalizedPath) return undefined;
216
+ if (/^(?:[A-Za-z]:[\\/]|\/)/.test(normalizedPath)) return normalizedPath;
217
+ if (!normalizedRoot) return undefined;
218
+ return `${normalizedRoot}/${normalizedPath.replace(/^[\\/]+/, '')}`;
219
+ }
220
+
221
+ function readGitSubmodules(value: unknown, parentRepoRoot?: string): GitSubmoduleStatus[] | undefined {
118
222
  if (!Array.isArray(value)) return undefined;
119
223
  const submodules = value
120
224
  .map(entry => {
121
225
  const submodule = readObjectRecord(entry);
122
226
  const path = readStringValue(submodule.path);
123
227
  const commit = readStringValue(submodule.commit);
124
- const repoPath = readStringValue(submodule.repoPath, submodule.repo_root);
228
+ const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
229
+ ?? joinRepoPath(parentRepoRoot, path);
125
230
  if (!path || !commit || !repoPath) return null;
126
231
  return {
127
232
  path,
@@ -137,60 +242,146 @@ function readGitSubmodules(value: unknown): GitSubmoduleStatus[] | undefined {
137
242
  return submodules.length > 0 ? submodules : undefined;
138
243
  }
139
244
 
140
- function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
141
- const cachedStatus = readObjectRecord(node?.cachedStatus);
142
- const cachedGit = readObjectRecord(cachedStatus.git);
143
- if (Object.keys(cachedGit).length) {
144
- const conflictFiles = Array.isArray(cachedGit.conflictFiles)
145
- ? cachedGit.conflictFiles.filter((value: unknown): value is string => typeof value === 'string')
146
- : [];
147
- const conflictCount = readNumberValue(cachedGit.conflicts) ?? conflictFiles.length;
148
- const hasConflicts = readBooleanValue(cachedGit.hasConflicts) ?? conflictCount > 0;
149
- const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
150
- if (isGitRepo !== undefined) {
151
- const submodules = readGitSubmodules(cachedGit.submodules);
152
- return {
153
- workspace: readStringValue(cachedGit.workspace, node?.workspace) || '',
154
- repoRoot: readStringValue(cachedGit.repoRoot, node?.repoRoot, node?.workspace) || null,
155
- isGitRepo,
156
- branch: readStringValue(cachedGit.branch) ?? null,
157
- headCommit: readStringValue(cachedGit.headCommit) ?? null,
158
- headMessage: readStringValue(cachedGit.headMessage) ?? null,
159
- upstream: readStringValue(cachedGit.upstream) ?? null,
160
- ahead: readNumberValue(cachedGit.ahead) ?? 0,
161
- behind: readNumberValue(cachedGit.behind) ?? 0,
162
- staged: readNumberValue(cachedGit.staged) ?? 0,
163
- modified: readNumberValue(cachedGit.modified) ?? 0,
164
- untracked: readNumberValue(cachedGit.untracked) ?? 0,
165
- deleted: readNumberValue(cachedGit.deleted) ?? 0,
166
- renamed: readNumberValue(cachedGit.renamed) ?? 0,
167
- hasConflicts,
168
- conflictFiles,
169
- stashCount: readNumberValue(cachedGit.stashCount) ?? 0,
170
- lastCheckedAt: readNumberValue(cachedGit.lastCheckedAt) ?? Date.now(),
171
- ...(submodules ? { submodules } : {}),
172
- };
173
- }
245
+ function buildMeshNodeDisplayLabel(node: Record<string, unknown>, nodeId: string, providerPriority: string[]): string {
246
+ const explicit = readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias);
247
+ if (explicit) return explicit;
248
+ const workspace = readStringValue(node.workspace, node.repoRoot, node.repo_root);
249
+ const workspaceName = workspace ? pathBasename(workspace) : undefined;
250
+ const host = readStringValue(node.machineName, node.machine_name, node.hostname, node.host, node.daemonId, node.daemon_id, node.machineId, node.machine_id);
251
+ const provider = providerPriority[0] || (Array.isArray(node.providers) ? readStringValue(...node.providers) : undefined);
252
+ const parts = [workspaceName, host, provider].filter(Boolean);
253
+ if (parts.length > 0) return parts.join(' · ');
254
+ return nodeId || 'unidentified mesh node';
255
+ }
256
+
257
+ function normalizeMeshHostname(value: unknown): string | undefined {
258
+ const hostname = readStringValue(value);
259
+ if (!hostname) return undefined;
260
+ return hostname.toLowerCase().replace(/\.$/, '');
261
+ }
262
+
263
+ function readMeshNodeMachineId(node: Record<string, unknown>): string | undefined {
264
+ return readStringValue(
265
+ node.machineId,
266
+ node.machine_id,
267
+ readObjectRecord(node.machine)?.id,
268
+ readObjectRecord(node.machine)?.machineId,
269
+ readObjectRecord(node.lastProbe)?.machineId,
270
+ readObjectRecord(node.last_probe)?.machine_id,
271
+ readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.id,
272
+ readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.machineId,
273
+ readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.id,
274
+ readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.machine_id,
275
+ );
276
+ }
277
+
278
+ function readMeshNodeDaemonId(node: Record<string, unknown>): string | undefined {
279
+ return readStringValue(
280
+ node.daemonId,
281
+ node.daemon_id,
282
+ readObjectRecord(node.machine)?.daemonId,
283
+ readObjectRecord(node.machine)?.daemon_id,
284
+ readObjectRecord(node.lastProbe)?.daemonId,
285
+ readObjectRecord(node.last_probe)?.daemon_id,
286
+ readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.daemonId,
287
+ readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.daemon_id,
288
+ readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.daemonId,
289
+ readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.daemon_id,
290
+ );
291
+ }
292
+
293
+ function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
294
+ return readStringValue(
295
+ node.hostname,
296
+ node.host,
297
+ node.machineHostname,
298
+ node.machine_hostname,
299
+ readObjectRecord(node.machine)?.hostname,
300
+ readObjectRecord(node.machine)?.host,
301
+ readObjectRecord(node.lastProbe)?.hostname,
302
+ readObjectRecord(node.last_probe)?.hostname,
303
+ readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.hostname,
304
+ readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.hostname,
305
+ );
306
+ }
307
+
308
+ function readMeshNodeDisplayMachineName(node: Record<string, unknown>): string | undefined {
309
+ return readStringValue(
310
+ node.machineName,
311
+ node.machine_name,
312
+ node.machineLabel,
313
+ node.machine_label,
314
+ node.machineNickname,
315
+ node.machine_nickname,
316
+ node.alias,
317
+ readObjectRecord(node.machine)?.name,
318
+ readObjectRecord(node.machine)?.displayName,
319
+ readObjectRecord(node.machine)?.display_name,
320
+ readObjectRecord(node.lastProbe)?.machineName,
321
+ readObjectRecord(node.last_probe)?.machine_name,
322
+ readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.name,
323
+ readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.name,
324
+ readMeshNodeHostname(node),
325
+ );
326
+ }
327
+
328
+ function compactMeshIdentityEvidence(value: string | undefined): string | undefined {
329
+ if (!value) return undefined;
330
+ return value.length > 24 ? `${value.slice(0, 12)}…${value.slice(-8)}` : value;
331
+ }
332
+
333
+ function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
334
+ localMachineId?: string;
335
+ localDaemonId?: string;
336
+ coordinatorHostname?: string;
337
+ isSelfNode?: boolean;
338
+ }): Record<string, unknown> {
339
+ const machineId = readMeshNodeMachineId(node);
340
+ const daemonId = readMeshNodeDaemonId(node);
341
+ const hostname = readMeshNodeHostname(node);
342
+ const machineName = readMeshNodeDisplayMachineName(node);
343
+ const coordinatorHostname = readStringValue(opts.coordinatorHostname);
344
+ const machineIdMatches = Boolean(opts.localMachineId && machineId && opts.localMachineId === machineId);
345
+ const daemonIdMatches = Boolean(opts.localDaemonId && daemonId && opts.localDaemonId === daemonId);
346
+ const hostnameMatches = Boolean(
347
+ normalizeMeshHostname(hostname)
348
+ && normalizeMeshHostname(coordinatorHostname)
349
+ && normalizeMeshHostname(hostname) === normalizeMeshHostname(coordinatorHostname),
350
+ );
351
+ const sameMachine = opts.isSelfNode === true || machineIdMatches || daemonIdMatches || hostnameMatches;
352
+ const evidence: string[] = [];
353
+ for (const [label, value] of [['machineName', machineName], ['hostname', hostname], ['machineId', machineId], ['daemonId', daemonId]] as const) {
354
+ const compact = compactMeshIdentityEvidence(value);
355
+ if (compact) evidence.push(`${label}:${compact}`);
174
356
  }
357
+ const locality = sameMachine ? 'same_machine' : (evidence.length > 0 ? 'remote_known' : 'remote_or_unknown');
358
+ const localityReason = sameMachine
359
+ ? (machineIdMatches ? 'matched coordinator machine id'
360
+ : daemonIdMatches ? 'matched coordinator daemon id'
361
+ : hostnameMatches ? 'matched coordinator hostname'
362
+ : 'selected coordinator node')
363
+ : evidence.length > 0
364
+ ? `known remote/other machine identity; no local coordinator match (${evidence.join(', ')})`
365
+ : 'no useful machine identity evidence available';
366
+ return {
367
+ daemonId,
368
+ machineId,
369
+ hostname,
370
+ machineName,
371
+ displayName: machineName || hostname || daemonId || machineId,
372
+ coordinatorHostname,
373
+ sameMachine,
374
+ locality,
375
+ localityReason,
376
+ identityEvidence: evidence,
377
+ };
378
+ }
175
379
 
176
- const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
177
- const gitResult = readObjectRecord(rawGit.result);
178
- const directStatus = readObjectRecord(rawGit.status);
179
- const nestedStatus = readObjectRecord(gitResult.status);
180
- const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
181
- const probeGit = readObjectRecord(rawProbe.git);
182
- const probeGitResult = readObjectRecord(probeGit.result);
183
- const probeDirectStatus = readObjectRecord(probeGit.status);
184
- const probeNestedStatus = readObjectRecord(probeGitResult.status);
185
- const status = Object.keys(directStatus).length
186
- ? directStatus
187
- : Object.keys(nestedStatus).length
188
- ? nestedStatus
189
- : Object.keys(probeDirectStatus).length
190
- ? probeDirectStatus
191
- : Object.keys(probeNestedStatus).length
192
- ? probeNestedStatus
193
- : {};
380
+ function normalizeInlineMeshGitStatus(
381
+ status: Record<string, unknown>,
382
+ node: any,
383
+ options?: { lastCheckedAt?: number },
384
+ ): Record<string, unknown> | undefined {
194
385
  const isGitRepo = readBooleanValue(status.isGitRepo);
195
386
  if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
196
387
  const conflictFiles = Array.isArray(status.conflictFiles)
@@ -198,15 +389,20 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
198
389
  : [];
199
390
  const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
200
391
  const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
201
- const submodules = readGitSubmodules(status.submodules);
392
+ const repoRoot = readStringValue(status.repoRoot, status.repo_root, node?.repoRoot, node?.repo_root, status.workspace, node?.workspace) || undefined;
393
+ const submodules = readGitSubmodules(status.submodules, repoRoot);
202
394
  return {
203
395
  workspace: readStringValue(status.workspace, node?.workspace) || '',
204
- repoRoot: readStringValue(status.repoRoot, node?.repoRoot, node?.workspace) || null,
396
+ repoRoot: repoRoot ?? null,
205
397
  isGitRepo,
206
398
  branch: readStringValue(status.branch) ?? null,
207
399
  headCommit: readStringValue(status.headCommit) ?? null,
208
400
  headMessage: readStringValue(status.headMessage) ?? null,
209
401
  upstream: readStringValue(status.upstream) ?? null,
402
+ upstreamStatus: readStringValue(status.upstreamStatus, status.upstream_status)
403
+ ?? (readStringValue(status.upstream) ? 'unchecked' : 'no_upstream'),
404
+ upstreamFetchedAt: readNumberValue(status.upstreamFetchedAt, status.upstream_fetched_at),
405
+ upstreamFetchError: readStringValue(status.upstreamFetchError, status.upstream_fetch_error),
210
406
  ahead: readNumberValue(status.ahead) ?? 0,
211
407
  behind: readNumberValue(status.behind) ?? 0,
212
408
  staged: readNumberValue(status.staged) ?? 0,
@@ -217,14 +413,247 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
217
413
  hasConflicts,
218
414
  conflictFiles,
219
415
  stashCount: readNumberValue(status.stashCount) ?? 0,
220
- lastCheckedAt: Date.now(),
416
+ lastCheckedAt: options?.lastCheckedAt ?? readNumberValue(status.lastCheckedAt) ?? Date.now(),
221
417
  ...(submodules ? { submodules } : {}),
222
418
  };
223
419
  }
224
420
 
421
+ function scoreInlineMeshGitStatus(git: Record<string, unknown> | undefined): number {
422
+ if (!git) return Number.NEGATIVE_INFINITY;
423
+ let score = 0;
424
+ if (readBooleanValue(git.isGitRepo) === true) score += 50;
425
+ if (readBooleanValue(git.isGitRepo) === false) score -= 10;
426
+ if (readStringValue(git.branch)) score += 20;
427
+ if (readStringValue(git.headCommit)) score += 20;
428
+ if (readStringValue(git.upstream)) score += 10;
429
+ if (readStringValue(git.upstreamStatus)) score += 5;
430
+ if (readNumberValue(git.ahead) !== undefined) score += 2;
431
+ if (readNumberValue(git.behind) !== undefined) score += 2;
432
+ if (Array.isArray(git.submodules) && git.submodules.length > 0) score += 4 + git.submodules.length;
433
+ if (readStringValue(git.error)) score -= 20;
434
+ return score;
435
+ }
436
+
437
+ function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
438
+ const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
439
+ const gitResult = readObjectRecord(rawGit.result);
440
+ const directStatus = readObjectRecord(rawGit.status);
441
+ const nestedStatus = readObjectRecord(gitResult.status);
442
+ const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
443
+ const probeGit = readObjectRecord(rawProbe.git);
444
+ const probeGitResult = readObjectRecord(probeGit.result);
445
+ const probeDirectStatus = readObjectRecord(probeGit.status);
446
+ const probeNestedStatus = readObjectRecord(probeGitResult.status);
447
+ const candidates = [directStatus, nestedStatus, probeDirectStatus, probeNestedStatus];
448
+ let best: { git: Record<string, unknown>; score: number } | null = null;
449
+ for (const status of candidates) {
450
+ const normalized = normalizeInlineMeshGitStatus(status, node, { lastCheckedAt: Date.now() });
451
+ if (!normalized) continue;
452
+ const score = scoreInlineMeshGitStatus(normalized);
453
+ if (!best || score > best.score) best = { git: normalized, score };
454
+ }
455
+ return best?.git;
456
+ }
457
+
458
+ function shouldRefreshStalePendingAggregate(snapshot: any, options?: { requireDirectPeerTruth?: boolean }): boolean {
459
+ if (options?.requireDirectPeerTruth !== true || !Array.isArray(snapshot?.nodes)) return false;
460
+ return snapshot.nodes.some((node: any) => {
461
+ if (node?.gitProbePending !== true) return false;
462
+ const git = readObjectRecord(node?.git);
463
+ return !readBooleanValue(git.isGitRepo) && !readStringValue(git.branch, git.headCommit, git.upstream);
464
+ });
465
+ }
466
+
467
+ function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
468
+ const source = readStringValue(connection.source);
469
+ const transport = readStringValue(connection.transport);
470
+ return {
471
+ ...connection,
472
+ perspective: readStringValue(connection.perspective) ?? 'selected_coordinator',
473
+ source: source && source !== 'not_reported' ? source : 'mesh_peer_status',
474
+ state: 'connected',
475
+ transport: transport && transport !== 'unknown' ? transport : 'direct',
476
+ reported: true,
477
+ reason: 'Live peer git snapshot reported by the selected coordinator.',
478
+ lastStateChangeAt: readStringValue(connection.lastStateChangeAt) ?? timestamp,
479
+ };
480
+ }
481
+
482
+ function recordInlineMeshDirectGitTruth(
483
+ node: any,
484
+ git: Record<string, unknown>,
485
+ source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
486
+ ): void {
487
+ if (!node || typeof node !== 'object' || Array.isArray(node)) return;
488
+ const checkedAt = readNumberValue(git.lastCheckedAt) ?? Date.now();
489
+ const updatedAt = new Date(checkedAt).toISOString();
490
+ const nextGit: Record<string, unknown> = {
491
+ ...git,
492
+ lastCheckedAt: checkedAt,
493
+ };
494
+ node.lastGit = {
495
+ source,
496
+ checkedAt,
497
+ status: nextGit,
498
+ };
499
+ node.last_git = node.lastGit;
500
+ node.machineStatus = 'online';
501
+ node.updatedAt = updatedAt;
502
+ node.lastSeenAt = updatedAt;
503
+ const repoRoot = readStringValue(nextGit.repoRoot);
504
+ if (repoRoot && !readStringValue(node.repoRoot)) node.repoRoot = repoRoot;
505
+ }
506
+
507
+ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
508
+ const liveGit = buildInlineMeshTransitGitStatus(node);
509
+ if (liveGit) return liveGit;
510
+
511
+ const cachedStatus = readObjectRecord(node?.cachedStatus);
512
+ const cachedGit = readObjectRecord(cachedStatus.git);
513
+ if (!Object.keys(cachedGit).length) return undefined;
514
+ return normalizeInlineMeshGitStatus(cachedGit, node);
515
+ }
516
+
517
+ function shouldDiscardCachedInlineMeshStatus(node: any): boolean {
518
+ const cachedStatus = readObjectRecord(node?.cachedStatus);
519
+ if (!Object.keys(cachedStatus).length) return false;
520
+ const cachedGit = readObjectRecord(cachedStatus.git);
521
+ const workspaceError = readStringValue(cachedStatus.error, node?.error);
522
+ if (workspaceError && /workspace must be an existing directory/i.test(workspaceError)) return true;
523
+ const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
524
+ const branch = readStringValue(cachedGit.branch);
525
+ const headCommit = readStringValue(cachedGit.headCommit);
526
+ return isGitRepo === false && !branch && !headCommit;
527
+ }
528
+
529
+ function stripInlineMeshTransientNodeState(node: any): any {
530
+ if (!node || typeof node !== 'object' || Array.isArray(node)) return node;
531
+ const {
532
+ cachedStatus,
533
+ lastGit: _lastGit,
534
+ last_git: _lastGitLegacy,
535
+ lastProbe: _lastProbe,
536
+ last_probe: _lastProbeLegacy,
537
+ error: _error,
538
+ health: _health,
539
+ machineStatus: _machineStatus,
540
+ lastSeenAt: _lastSeenAt,
541
+ last_seen_at: _lastSeenAtLegacy,
542
+ updatedAt: _updatedAt,
543
+ updated_at: _updatedAtLegacy,
544
+ activeSession: _activeSession,
545
+ active_session: _activeSessionLegacy,
546
+ activeSessionId: _activeSessionId,
547
+ active_session_id: _activeSessionIdLegacy,
548
+ sessionId: _sessionId,
549
+ session_id: _sessionIdLegacy,
550
+ providerType: _providerType,
551
+ provider_type: _providerTypeLegacy,
552
+ ...rest
553
+ } = node as Record<string, unknown>;
554
+ if (cachedStatus && !shouldDiscardCachedInlineMeshStatus(node)) {
555
+ return { ...rest, cachedStatus };
556
+ }
557
+ return rest;
558
+ }
559
+
560
+ function hasInlineMeshTransientNodeState(node: any): boolean {
561
+ if (!node || typeof node !== 'object' || Array.isArray(node)) return false;
562
+ return 'cachedStatus' in node
563
+ || 'lastGit' in node
564
+ || 'last_git' in node
565
+ || 'lastProbe' in node
566
+ || 'last_probe' in node
567
+ || 'error' in node
568
+ || 'health' in node
569
+ || 'machineStatus' in node
570
+ || 'lastSeenAt' in node
571
+ || 'last_seen_at' in node
572
+ || 'updatedAt' in node
573
+ || 'updated_at' in node
574
+ || 'activeSession' in node
575
+ || 'active_session' in node
576
+ || 'activeSessionId' in node
577
+ || 'active_session_id' in node
578
+ || 'sessionId' in node
579
+ || 'session_id' in node
580
+ || 'providerType' in node
581
+ || 'provider_type' in node;
582
+ }
583
+
584
+ function inlineMeshCarriesTransientNodeTruth(inlineMesh: any): boolean {
585
+ if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return false;
586
+ if (!Array.isArray(inlineMesh.nodes) || inlineMesh.nodes.length === 0) return false;
587
+ return inlineMesh.nodes.some((node: any) => hasInlineMeshTransientNodeState(node));
588
+ }
589
+
590
+ function readInlineMeshNodeId(node: any): string {
591
+ return readStringValue(node?.id, node?.nodeId) || '';
592
+ }
593
+
594
+ function sanitizeInlineMesh(inlineMesh: any): any {
595
+ if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return inlineMesh;
596
+ if (!Array.isArray(inlineMesh.nodes)) return inlineMesh;
597
+ let changed = false;
598
+ const nodes = inlineMesh.nodes.map((node: any) => {
599
+ if (!hasInlineMeshTransientNodeState(node)) return node;
600
+ changed = true;
601
+ return stripInlineMeshTransientNodeState(node);
602
+ });
603
+ if (!changed) return inlineMesh;
604
+ return {
605
+ ...inlineMesh,
606
+ nodes,
607
+ };
608
+ }
609
+
610
+ function reconcileInlineMeshCache(cached: any, incoming: any): any {
611
+ if (!cached || typeof cached !== 'object' || Array.isArray(cached)) return incoming;
612
+ if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) return cached;
613
+ const cachedNodes = Array.isArray(cached.nodes) ? cached.nodes : [];
614
+ const incomingNodes = Array.isArray(incoming.nodes) ? incoming.nodes : [];
615
+ if (!cachedNodes.length || !incomingNodes.length) return { ...cached, ...incoming };
616
+
617
+ const cachedUpdatedAt = Date.parse(readStringValue(cached.updatedAt, cached.updated_at) || '');
618
+ const incomingUpdatedAt = Date.parse(readStringValue(incoming.updatedAt, incoming.updated_at) || '');
619
+ const preserveCachedMembership = Number.isFinite(cachedUpdatedAt)
620
+ && (!Number.isFinite(incomingUpdatedAt) || cachedUpdatedAt > incomingUpdatedAt);
621
+
622
+ const cachedById = new Map<string, any>();
623
+ for (const node of cachedNodes) {
624
+ const nodeId = readInlineMeshNodeId(node);
625
+ if (nodeId) cachedById.set(nodeId, node);
626
+ }
627
+
628
+ const nodes = incomingNodes.map((incomingNode: any) => {
629
+ const nodeId = readInlineMeshNodeId(incomingNode);
630
+ const cachedNode = nodeId ? cachedById.get(nodeId) : undefined;
631
+ if (!cachedNode && preserveCachedMembership) return null;
632
+ if (!cachedNode) return incomingNode;
633
+ if (hasInlineMeshTransientNodeState(incomingNode)) {
634
+ return { ...cachedNode, ...incomingNode };
635
+ }
636
+ return { ...stripInlineMeshTransientNodeState(cachedNode), ...incomingNode };
637
+ }).filter(Boolean);
638
+
639
+ return {
640
+ ...cached,
641
+ ...incoming,
642
+ nodes,
643
+ };
644
+ }
645
+
225
646
  function hasGitWorktreeChanges(git: Record<string, unknown> | null | undefined): boolean {
226
- if (!git) return false;
227
- return Number(git.staged || 0) + Number(git.modified || 0) + Number(git.untracked || 0) + Number(git.deleted || 0) + Number(git.renamed || 0) > 0;
647
+ return countGitWorktreeChanges(git) > 0;
648
+ }
649
+
650
+ function countGitWorktreeChanges(git: Record<string, unknown> | null | undefined): number {
651
+ if (!git) return 0;
652
+ return Number(git.staged || 0)
653
+ + Number(git.modified || 0)
654
+ + Number(git.untracked || 0)
655
+ + Number(git.deleted || 0)
656
+ + Number(git.renamed || 0);
228
657
  }
229
658
 
230
659
  function getGitSubmoduleDriftState(git: Record<string, unknown> | null | undefined): { dirty: boolean; outOfSync: boolean } {
@@ -249,6 +678,167 @@ function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undef
249
678
  return 'online';
250
679
  }
251
680
 
681
+ function readMeshNodeLabel(status: Record<string, unknown>, node: any): string {
682
+ return readStringValue(status.nodeId, node?.id, node?.nodeId) ?? 'unknown';
683
+ }
684
+
685
+ function buildInlineMeshBranchConvergence(args: {
686
+ mesh: any;
687
+ node: any;
688
+ status: Record<string, unknown>;
689
+ }): Record<string, unknown> {
690
+ const git = readObjectRecord(args.status.git);
691
+ const nodeLabel = readMeshNodeLabel(args.status, args.node);
692
+ const defaultBranch = readStringValue(args.mesh?.defaultBranch) ?? 'main';
693
+ const branch = readStringValue(git.branch, args.node?.worktreeBranch) ?? null;
694
+ const upstream = readStringValue(git.upstream) ?? null;
695
+ const upstreamStatus = readStringValue(git.upstreamStatus, git.upstream_status)
696
+ ?? (upstream ? 'unchecked' : 'no_upstream');
697
+ const ahead = readNumberValue(git.ahead) ?? 0;
698
+ const behind = readNumberValue(git.behind) ?? 0;
699
+ const uncommittedChanges = countGitWorktreeChanges(git);
700
+ const hasConflicts = readBooleanValue(git.hasConflicts)
701
+ ?? (Array.isArray(git.conflictFiles) && git.conflictFiles.length > 0);
702
+ const base = {
703
+ defaultBranch,
704
+ branch,
705
+ upstream,
706
+ upstreamStatus,
707
+ ahead,
708
+ behind,
709
+ isWorktree: args.node?.isLocalWorktree === true || args.status.isLocalWorktree === true,
710
+ isDefaultBranch: branch === defaultBranch,
711
+ };
712
+
713
+ if (readBooleanValue(git.isGitRepo) !== true) {
714
+ return {
715
+ ...base,
716
+ status: 'blocked_review',
717
+ needsConvergence: true,
718
+ reason: 'git_status_unavailable',
719
+ nextStep: `Resolve git status for node '${nodeLabel}' before marking the task complete.`,
720
+ };
721
+ }
722
+
723
+ if (!branch) {
724
+ return {
725
+ ...base,
726
+ status: 'blocked_review',
727
+ needsConvergence: true,
728
+ reason: 'branch_unknown',
729
+ nextStep: `Inspect node '${nodeLabel}' git branch before deciding whether it is merged to ${defaultBranch}.`,
730
+ };
731
+ }
732
+
733
+ if (hasConflicts || uncommittedChanges > 0) {
734
+ return {
735
+ ...base,
736
+ status: 'not_mergeable',
737
+ needsConvergence: true,
738
+ reason: hasConflicts ? 'conflicts_present' : 'dirty_workspace',
739
+ nextStep: `Commit, checkpoint, or resolve node '${nodeLabel}' before any main convergence step.`,
740
+ };
741
+ }
742
+
743
+ if (branch === defaultBranch) {
744
+ if (upstream && upstreamStatus !== 'fresh') {
745
+ return {
746
+ ...base,
747
+ status: 'blocked_review',
748
+ needsConvergence: true,
749
+ reason: 'default_branch_upstream_unverified',
750
+ nextStep: `Refresh ${defaultBranch}'s upstream refs or resolve the fetch failure before declaring convergence complete for node '${nodeLabel}'.`,
751
+ };
752
+ }
753
+ if (ahead > 0 || behind > 0) {
754
+ return {
755
+ ...base,
756
+ status: 'blocked_review',
757
+ needsConvergence: true,
758
+ reason: 'default_branch_not_even_with_upstream',
759
+ nextStep: `Bring ${defaultBranch} even with its upstream before declaring convergence complete.`,
760
+ };
761
+ }
762
+ return {
763
+ ...base,
764
+ status: 'merged_to_main',
765
+ needsConvergence: false,
766
+ reason: 'clean_default_branch',
767
+ nextStep: null,
768
+ };
769
+ }
770
+
771
+ if (args.node?.isLocalWorktree === true || args.status.isLocalWorktree === true) {
772
+ return {
773
+ ...base,
774
+ status: 'cleanup_candidate',
775
+ needsConvergence: true,
776
+ reason: 'clean_non_default_worktree_branch',
777
+ nextStep: `Run mesh_refine_node(node_id: "${nodeLabel}") or explicitly classify this worktree as blocked_review/not_mergeable before ending the task.`,
778
+ };
779
+ }
780
+
781
+ if (upstream && upstreamStatus !== 'fresh') {
782
+ return {
783
+ ...base,
784
+ status: 'blocked_review',
785
+ needsConvergence: true,
786
+ reason: 'feature_branch_upstream_unverified',
787
+ nextStep: `Refresh branch '${branch}' upstream refs or resolve the fetch failure before deciding whether it is ready to merge into ${defaultBranch}.`,
788
+ };
789
+ }
790
+
791
+ if (!upstream || ahead > 0 || behind > 0) {
792
+ return {
793
+ ...base,
794
+ status: 'blocked_review',
795
+ needsConvergence: true,
796
+ reason: !upstream ? 'feature_branch_missing_upstream' : 'feature_branch_not_even_with_upstream',
797
+ nextStep: `Push or reconcile branch '${branch}', then merge it into ${defaultBranch} or mark it not_mergeable with a reason.`,
798
+ };
799
+ }
800
+
801
+ return {
802
+ ...base,
803
+ status: 'pushed_feature_branch_needs_merge',
804
+ needsConvergence: true,
805
+ reason: 'clean_non_default_branch',
806
+ nextStep: `Review and merge branch '${branch}' into ${defaultBranch}; do not report the task as fully complete while it remains off main.`,
807
+ };
808
+ }
809
+
810
+ function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
811
+ const git = readObjectRecord(status.git);
812
+ if (Object.keys(git).length === 0 && !status.gitProbePending) return;
813
+ const uncommittedChanges = countGitWorktreeChanges(git);
814
+ status.isDirty = uncommittedChanges > 0;
815
+ status.uncommittedChanges = uncommittedChanges;
816
+ status.branchConvergence = buildInlineMeshBranchConvergence({ mesh, node, status });
817
+ }
818
+
819
+ function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
820
+ const followUps = nodes
821
+ .filter(node => readObjectRecord(node.branchConvergence).needsConvergence === true)
822
+ .map(node => {
823
+ const convergence = readObjectRecord(node.branchConvergence);
824
+ return {
825
+ nodeId: node.nodeId,
826
+ workspace: node.workspace,
827
+ branch: convergence.branch,
828
+ status: convergence.status,
829
+ reason: convergence.reason,
830
+ nextStep: convergence.nextStep,
831
+ };
832
+ });
833
+
834
+ return {
835
+ needsFollowUp: followUps.length > 0,
836
+ unresolvedCount: followUps.length,
837
+ requiredFinalStates: ['merged_to_main', 'pushed_feature_branch_needs_merge', 'blocked_review', 'cleanup_candidate', 'not_mergeable'],
838
+ followUps,
839
+ };
840
+ }
841
+
252
842
  function readCachedInlineMeshActiveSessions(node: any): string[] {
253
843
  const cachedStatus = readObjectRecord(node?.cachedStatus);
254
844
  const activeSession = readObjectRecord(cachedStatus.activeSession);
@@ -313,6 +903,169 @@ function toIsoTimestamp(value: unknown): string | null {
313
903
  return stringValue || null;
314
904
  }
315
905
 
906
+ function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknown>): void {
907
+ const connection = readObjectRecord(status.connection);
908
+ const connectionFreshAt = toIsoTimestamp(connection.lastCommandAt ?? connection.lastConnectedAt ?? connection.lastStateChangeAt);
909
+ const git = readObjectRecord(status.git);
910
+ const gitCheckedAt = toIsoTimestamp(git.lastCheckedAt);
911
+ if (!status.lastSeenAt && connectionFreshAt) status.lastSeenAt = connectionFreshAt;
912
+ if (!status.updatedAt && (gitCheckedAt || connectionFreshAt)) {
913
+ status.updatedAt = gitCheckedAt ?? connectionFreshAt;
914
+ }
915
+ }
916
+
917
+ function finalizeMeshNodeStatus(args: {
918
+ status: Record<string, unknown>;
919
+ node: any;
920
+ daemonId?: string;
921
+ isSelfNode: boolean;
922
+ }): void {
923
+ const { status, node, daemonId, isSelfNode } = args;
924
+ if (!readStringValue(status.machineStatus)) {
925
+ const cachedStatus = readObjectRecord(node?.cachedStatus);
926
+ const machineStatus = readStringValue(cachedStatus.machineStatus, cachedStatus.machine_status, node?.machineStatus);
927
+ if (machineStatus) status.machineStatus = machineStatus;
928
+ }
929
+ synthesizeMeshNodeFreshnessFromConnection(status);
930
+ const bootstrap = readObjectRecord(node?.worktreeBootstrap);
931
+ if (node?.isLocalWorktree && readStringValue(bootstrap.status)) {
932
+ status.worktreeBootstrap = bootstrap;
933
+ if (bootstrap.status === 'failed' && bootstrap.required !== false) {
934
+ status.launchReady = false;
935
+ status.launchBlockedReason = 'worktree_bootstrap_failed';
936
+ status.launchBlockedMessage = readStringValue(bootstrap.error)
937
+ || 'Required worktree bootstrap failed; resolve it before launching an agent into this node.';
938
+ return;
939
+ }
940
+ if (bootstrap.status === 'running' && bootstrap.required !== false) {
941
+ status.launchReady = false;
942
+ status.launchBlockedReason = 'worktree_bootstrap_running';
943
+ status.launchBlockedMessage = 'Required worktree bootstrap is still running; wait for it to finish before launching an agent into this node.';
944
+ return;
945
+ }
946
+ }
947
+ const connectionState = readStringValue(readObjectRecord(status.connection).state);
948
+ status.launchReady = !!daemonId && (
949
+ readStringValue(status.machineStatus) === 'online'
950
+ || connectionState === 'connected'
951
+ || isSelfNode
952
+ );
953
+ }
954
+
955
+ async function probeRemoteMeshGitStatus(args: {
956
+ dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
957
+ daemonId: string;
958
+ workspace: string;
959
+ timeoutMs: number;
960
+ }): Promise<Record<string, unknown> | null> {
961
+ if (!args.dispatchMeshCommand) return null;
962
+ const remoteResult = await Promise.race([
963
+ args.dispatchMeshCommand(args.daemonId, 'git_status', { workspace: args.workspace, refreshUpstream: true }),
964
+ new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), args.timeoutMs)),
965
+ ]) as any;
966
+ const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
967
+ return remoteGit && typeof remoteGit === 'object' && typeof remoteGit.isGitRepo === 'boolean'
968
+ ? remoteGit as Record<string, unknown>
969
+ : null;
970
+ }
971
+
972
+ async function hydrateInlineMeshDirectTruth(args: {
973
+ mesh: any;
974
+ meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
975
+ dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
976
+ statusInstanceId?: string;
977
+ localMachineId?: string;
978
+ }): Promise<{
979
+ directEvidenceCount: number;
980
+ localConfirmedCount: number;
981
+ peerAttemptedCount: number;
982
+ peerConfirmedCount: number;
983
+ unavailableNodeIds: string[];
984
+ }> {
985
+ const nodes = Array.isArray(args.mesh?.nodes) ? args.mesh.nodes : [];
986
+ if (!nodes.length) {
987
+ return {
988
+ directEvidenceCount: 0,
989
+ localConfirmedCount: 0,
990
+ peerAttemptedCount: 0,
991
+ peerConfirmedCount: 0,
992
+ unavailableNodeIds: [],
993
+ };
994
+ }
995
+
996
+ const selectedCoordinatorNodeId = readStringValue(
997
+ args.mesh?.coordinator?.preferredNodeId,
998
+ nodes[0]?.id,
999
+ nodes[0]?.nodeId,
1000
+ );
1001
+
1002
+ let localConfirmedCount = 0;
1003
+ let peerAttemptedCount = 0;
1004
+ let peerConfirmedCount = 0;
1005
+ const unavailableNodeIds: string[] = [];
1006
+
1007
+ for (const [nodeIndex, node] of nodes.entries()) {
1008
+ const nodeId = readStringValue(node?.id, node?.nodeId) || `node_${nodeIndex}`;
1009
+ const workspace = readStringValue(node?.workspace);
1010
+ const daemonId = readStringValue(node?.daemonId);
1011
+ const isSelfNode = Boolean(
1012
+ nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
1013
+ ) || Boolean(
1014
+ daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
1015
+ ) || Boolean(args.meshSource !== 'local_config' && nodeIndex === 0);
1016
+
1017
+ if (!workspace) {
1018
+ if (!isSelfNode && daemonId) unavailableNodeIds.push(nodeId);
1019
+ continue;
1020
+ }
1021
+
1022
+ if (fs.existsSync(workspace)) {
1023
+ try {
1024
+ const localGit = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
1025
+ if (localGit?.isGitRepo) {
1026
+ recordInlineMeshDirectGitTruth(node, localGit as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
1027
+ localConfirmedCount += 1;
1028
+ continue;
1029
+ }
1030
+ } catch {
1031
+ // Fall through to remote classification.
1032
+ }
1033
+ }
1034
+
1035
+ if (!daemonId || !args.dispatchMeshCommand) {
1036
+ if (!isSelfNode) unavailableNodeIds.push(nodeId);
1037
+ continue;
1038
+ }
1039
+
1040
+ peerAttemptedCount += 1;
1041
+ try {
1042
+ const remoteGit = await probeRemoteMeshGitStatus({
1043
+ dispatchMeshCommand: args.dispatchMeshCommand,
1044
+ daemonId,
1045
+ workspace,
1046
+ timeoutMs: 8_000,
1047
+ });
1048
+ if (remoteGit) {
1049
+ recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
1050
+ peerConfirmedCount += 1;
1051
+ continue;
1052
+ }
1053
+ } catch {
1054
+ // Strict direct-only path: do not fall back to persisted cloud truth here.
1055
+ }
1056
+
1057
+ unavailableNodeIds.push(nodeId);
1058
+ }
1059
+
1060
+ return {
1061
+ directEvidenceCount: localConfirmedCount + peerConfirmedCount,
1062
+ localConfirmedCount,
1063
+ peerAttemptedCount,
1064
+ peerConfirmedCount,
1065
+ unavailableNodeIds,
1066
+ };
1067
+ }
1068
+
316
1069
  function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
317
1070
  return {
318
1071
  sessionId: readStringValue(record?.sessionId) || 'unknown',
@@ -328,6 +1081,26 @@ function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
328
1081
  };
329
1082
  }
330
1083
 
1084
+ function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string, nodeWorkspace = '', nodeIsMissingLocalWorktree = false): boolean {
1085
+ const recordNodeId = readStringValue(record?.meta?.meshNodeId);
1086
+ if (!recordNodeId || recordNodeId !== nodeId) return false;
1087
+ if (nodeIsMissingLocalWorktree) return false;
1088
+ const recordWorkspace = readStringValue(record?.workspace);
1089
+ if (nodeWorkspace && recordWorkspace && recordWorkspace !== nodeWorkspace) return false;
1090
+ const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
1091
+ return !recordMeshId || recordMeshId === meshId;
1092
+ }
1093
+
1094
+ function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
1095
+ const recordWorkspace = readStringValue(record?.workspace);
1096
+ if (!recordWorkspace || !workspace || recordWorkspace !== workspace) return false;
1097
+
1098
+ const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
1099
+ if (recordMeshId) return recordMeshId === meshId;
1100
+
1101
+ return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
1102
+ }
1103
+
331
1104
  function readLiveMeshNodeWorkspace(args: {
332
1105
  meshId: string;
333
1106
  nodeId: string;
@@ -335,7 +1108,7 @@ function readLiveMeshNodeWorkspace(args: {
335
1108
  allowCoordinatorSession?: boolean;
336
1109
  }): string {
337
1110
  const directNodeWorkspace = args.liveSessionRecords.find((record) => (
338
- readStringValue(record?.meta?.meshNodeId) === args.nodeId
1111
+ liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)
339
1112
  && readStringValue(record?.workspace)
340
1113
  ));
341
1114
  if (directNodeWorkspace) {
@@ -362,11 +1135,16 @@ function collectLiveMeshSessionRecords(args: {
362
1135
  liveSessionRecords: any[];
363
1136
  allowCoordinatorSession?: boolean;
364
1137
  }): any[] {
1138
+ const nodeWorkspace = readStringValue(args.node?.workspace);
1139
+ const nodeIsMissingLocalWorktree = args.node?.isLocalWorktree === true
1140
+ && !!nodeWorkspace
1141
+ && !fs.existsSync(nodeWorkspace);
365
1142
  const matches = args.liveSessionRecords.filter((record) => {
366
- if (readStringValue(record?.meta?.meshNodeId) === args.nodeId) return true;
367
- const recordWorkspace = readStringValue(record?.workspace);
368
- const nodeWorkspace = readStringValue(args.node?.workspace);
369
- return !!recordWorkspace && !!nodeWorkspace && recordWorkspace === nodeWorkspace;
1143
+ const recordNodeId = readStringValue(record?.meta?.meshNodeId);
1144
+ if (recordNodeId && recordNodeId !== args.nodeId) return false;
1145
+ if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId, nodeWorkspace || '', nodeIsMissingLocalWorktree)) return true;
1146
+ if (nodeIsMissingLocalWorktree) return false;
1147
+ return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
370
1148
  });
371
1149
 
372
1150
  if (args.allowCoordinatorSession) {
@@ -381,11 +1159,62 @@ function collectLiveMeshSessionRecords(args: {
381
1159
  return matches;
382
1160
  }
383
1161
 
384
- function applyCachedInlineMeshNodeStatus(status: Record<string, unknown>, node: any): boolean {
1162
+ function buildHistoricalMeshSessions(args: {
1163
+ meshId: string;
1164
+ nodes: any[];
1165
+ liveSessionRecords: any[];
1166
+ }): { count: number; sessions: Record<string, unknown>[]; instruction: string } | undefined {
1167
+ const liveNodeIds = new Set<string>();
1168
+ const liveWorkspaces = new Set<string>();
1169
+ const missingLocalWorktreeNodeIds = new Set<string>();
1170
+ for (const node of args.nodes || []) {
1171
+ const nodeId = readStringValue(node?.id, node?.nodeId);
1172
+ const workspace = readStringValue(node?.workspace);
1173
+ if (nodeId) liveNodeIds.add(nodeId);
1174
+ if (workspace) liveWorkspaces.add(workspace);
1175
+ if (nodeId && node?.isLocalWorktree === true && workspace && !fs.existsSync(workspace)) {
1176
+ missingLocalWorktreeNodeIds.add(nodeId);
1177
+ }
1178
+ }
1179
+
1180
+ const sessions: Record<string, unknown>[] = [];
1181
+ for (const record of args.liveSessionRecords || []) {
1182
+ const meta = readObjectRecord(record?.meta);
1183
+ const recordMeshId = readStringValue(meta.meshNodeFor, meta.meshCoordinatorFor);
1184
+ if (recordMeshId !== args.meshId) continue;
1185
+ const recordNodeId = readStringValue(meta.meshNodeId);
1186
+ const workspace = readStringValue(record?.workspace);
1187
+ const removedNode = !!recordNodeId && (!liveNodeIds.has(recordNodeId) || missingLocalWorktreeNodeIds.has(recordNodeId));
1188
+ const orphanedWorkspace = !!workspace && !liveWorkspaces.has(workspace) && meta.meshCoordinatorFor !== args.meshId;
1189
+ if (!removedNode && !orphanedWorkspace) continue;
1190
+ sessions.push({
1191
+ ...summarizeMeshSessionRecord(record),
1192
+ classification: removedNode ? 'removedNode' : 'orphanedSession',
1193
+ historical: true,
1194
+ meshNodeId: recordNodeId || null,
1195
+ reason: removedNode
1196
+ ? 'Session is tagged to a mesh node that is no longer in live membership.'
1197
+ : 'Session workspace is no longer attached to a live mesh node.',
1198
+ });
1199
+ }
1200
+ if (sessions.length === 0) return undefined;
1201
+ return {
1202
+ count: sessions.length,
1203
+ sessions: sessions.slice(0, 5),
1204
+ instruction: 'These sessions are separated from normal node activeSessions because their mesh node/workspace is no longer live. Use mesh_cleanup_sessions only if cleanup is intended.',
1205
+ };
1206
+ }
1207
+
1208
+ function applyCachedInlineMeshNodeStatus(
1209
+ status: Record<string, unknown>,
1210
+ node: any,
1211
+ options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
1212
+ ): boolean {
385
1213
  const cachedStatus = readObjectRecord(node?.cachedStatus);
386
- const git = buildCachedInlineMeshGitStatus(node);
387
- const error = readStringValue(cachedStatus.error, node?.error);
388
- const health = readStringValue(cachedStatus.health, node?.health);
1214
+ const liveGit = buildInlineMeshTransitGitStatus(node);
1215
+ const git = options?.skipGit ? undefined : (liveGit ?? buildCachedInlineMeshGitStatus(node));
1216
+ const error = options?.skipError ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.error, node?.error));
1217
+ const health = options?.skipHealth ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.health, node?.health));
389
1218
  const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
390
1219
  const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
391
1220
  const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
@@ -442,29 +1271,140 @@ async function resolveProviderTypeFromPriority(args: {
442
1271
  }
443
1272
  type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
444
1273
  type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
445
- type MeshRefineValidationCommand = {
446
- command: string;
447
- args: string[];
448
- displayCommand: string;
449
- category: string;
450
- source: string;
451
- };
1274
+ type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
452
1275
 
453
1276
  type MeshRefineValidationSummary = {
454
1277
  status: MeshRefineValidationStatus;
455
1278
  required: true;
456
1279
  commandsRun: Array<Record<string, unknown>>;
1280
+ bootstrapCommandsRun: Array<Record<string, unknown>>;
457
1281
  rejectedCommands: Array<Record<string, unknown>>;
458
1282
  skippedReason?: string;
1283
+ failureKind?: string;
1284
+ failureCode?: string;
459
1285
  timeoutMs: number;
460
1286
  outputLimitBytes: number;
1287
+ configSource?: string;
1288
+ configSourceType?: string;
1289
+ suggestions?: unknown[];
1290
+ suggestedConfig?: unknown;
1291
+ };
1292
+
1293
+ type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
1294
+
1295
+ type MeshRefinePatchEquivalenceSummary = {
1296
+ status: MeshRefineStageStatus;
1297
+ equivalent: boolean;
1298
+ baseHead: string;
1299
+ branchHead: string;
1300
+ mergeBase?: string;
1301
+ mergedTree?: string;
1302
+ expectedPatchId?: string;
1303
+ actualPatchId?: string;
1304
+ durationMs: number;
1305
+ error?: string;
1306
+ stdout?: string;
1307
+ stderr?: string;
1308
+ actionableHint?: MeshRefineSubmoduleConflictHint;
461
1309
  };
462
1310
 
1311
+ type MeshRefineSubmoduleConflictHint = {
1312
+ kind: 'submodule_conflict';
1313
+ message: string;
1314
+ conflicts: Array<{
1315
+ path: string;
1316
+ baseCommit?: string;
1317
+ branchCommit?: string;
1318
+ }>;
1319
+ nextSteps: string[];
1320
+ };
1321
+
1322
+ type MeshRefineSubmoduleAlignmentSummary = {
1323
+ status: 'passed' | 'failed' | 'skipped';
1324
+ changedGitlinkPaths: string[];
1325
+ outOfSyncPaths: string[];
1326
+ updatedPaths: string[];
1327
+ verifiedPaths: string[];
1328
+ durationMs: number;
1329
+ reason?: string;
1330
+ command?: string;
1331
+ error?: string;
1332
+ stdout?: string;
1333
+ stderr?: string;
1334
+ };
1335
+
1336
+ type MeshRefineSubmoduleReachabilityEntry = {
1337
+ path: string;
1338
+ commit: string;
1339
+ reachable: boolean;
1340
+ publishRequired?: boolean;
1341
+ autoPublishAllowed?: boolean;
1342
+ autoPublishAttempted?: boolean;
1343
+ autoPublishSucceeded?: boolean;
1344
+ autoPublishVerified?: boolean;
1345
+ autoPublishRefspec?: string;
1346
+ autoPublishSkippedReason?: string;
1347
+ importedFromWorktree?: boolean;
1348
+ checkedLocal?: boolean;
1349
+ localReachable?: boolean;
1350
+ remote?: string;
1351
+ remoteUrl?: string;
1352
+ remoteReachable?: boolean;
1353
+ remoteMainBranch?: string;
1354
+ remoteMainReachable?: boolean;
1355
+ fetchedFromOrigin?: boolean;
1356
+ error?: string;
1357
+ publishStdout?: string;
1358
+ publishStderr?: string;
1359
+ };
1360
+
1361
+ type MeshRefineSubmoduleReachabilitySummary = {
1362
+ status: MeshRefineStageStatus;
1363
+ checked: number;
1364
+ unreachable: MeshRefineSubmoduleReachabilityEntry[];
1365
+ entries: MeshRefineSubmoduleReachabilityEntry[];
1366
+ durationMs: number;
1367
+ autoPublishAllowed?: boolean;
1368
+ autoPublishPolicySource?: string;
1369
+ error?: string;
1370
+ };
1371
+
1372
+ type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
1373
+
1374
+ type MeshRefineJobHandle = {
1375
+ success: true;
1376
+ async: true;
1377
+ status: MeshRefineAsyncJobStatus;
1378
+ jobId: string;
1379
+ interactionId: string;
1380
+ meshId: string;
1381
+ nodeId: string;
1382
+ targetNodeId: string;
1383
+ targetDaemonId?: string;
1384
+ workspace?: string;
1385
+ startedAt: string;
1386
+ completedAt?: string;
1387
+ duplicate?: boolean;
1388
+ retryOfJobId?: string;
1389
+ eventDelivery: {
1390
+ pendingEvents: true;
1391
+ ledger: true;
1392
+ };
1393
+ evidence: {
1394
+ pendingEventsCommand: 'get_pending_mesh_events';
1395
+ ledgerCommand: 'get_mesh_ledger_slice';
1396
+ taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
1397
+ };
1398
+ };
1399
+
1400
+ type MeshRefineTerminalJob = MeshRefineJobHandle & { result?: Record<string, unknown> };
1401
+
463
1402
  const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
464
1403
  const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
465
1404
  const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
466
1405
  const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
467
1406
  const REFINE_VALIDATION_MAX_COMMANDS = 4;
1407
+ const REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
468
1408
 
469
1409
  function truncateValidationOutput(value: unknown): string {
470
1410
  const text = typeof value === 'string' ? value : value == null ? '' : String(value);
@@ -472,171 +1412,492 @@ function truncateValidationOutput(value: unknown): string {
472
1412
  return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
473
1413
  }
474
1414
 
475
- function readPackageScripts(workspace: string): Record<string, string> {
476
- try {
477
- const packageJsonPath = pathJoin(workspace, 'package.json');
478
- const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
479
- return parsed?.scripts && typeof parsed.scripts === 'object' && !Array.isArray(parsed.scripts)
480
- ? parsed.scripts as Record<string, string>
481
- : {};
482
- } catch {
483
- return {};
484
- }
485
- }
486
-
487
- function tokenizeValidationCommand(command: string): string[] | null {
488
- const trimmed = command.trim();
489
- if (!trimmed) return null;
490
- // Fail closed: the gate never hands shell syntax to a shell. Package-manager
491
- // scripts are invoked via execFile(binary, args), and metacharacters/quotes are
492
- // rejected before tokenization so `npm run test && rm -rf` cannot be smuggled in.
493
- if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
494
- const tokens = trimmed.split(/\s+/).filter(Boolean);
495
- if (!tokens.length) return null;
496
- if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
497
- return tokens;
1415
+ function recordMeshRefineStage(
1416
+ stages: Array<Record<string, unknown>>,
1417
+ stage: string,
1418
+ status: MeshRefineStageStatus,
1419
+ startedAt: number,
1420
+ details?: Record<string, unknown>,
1421
+ ): void {
1422
+ stages.push({
1423
+ stage,
1424
+ status,
1425
+ durationMs: Date.now() - startedAt,
1426
+ ...(details || {}),
1427
+ });
498
1428
  }
499
1429
 
500
- function scriptMatchesValidationCategory(scriptName: string, category: string): boolean {
501
- return scriptName === category || scriptName.startsWith(`${category}:`);
1430
+ function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReachabilityEntry[]): string {
1431
+ const refs = entries
1432
+ .map(entry => `${entry.path}@${entry.commit}`)
1433
+ .join(', ');
1434
+ return `Ask the user for explicit approval to push/publish the unreachable submodule commit(s) (${refs}) to the configured submodule remote main branch, then rerun mesh_refine_node. Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.`;
502
1435
  }
503
1436
 
504
- function parsePackageManagerValidationCommand(
505
- rawCommand: string,
506
- category: string,
507
- scripts: Record<string, string>,
508
- source: string,
509
- ): { command?: MeshRefineValidationCommand; rejected?: Record<string, unknown> } {
510
- const tokens = tokenizeValidationCommand(rawCommand);
511
- if (!tokens) {
512
- return { rejected: { command: rawCommand, category, source, reason: 'unsafe command string is not allowlisted' } };
1437
+ function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
1438
+ if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
1439
+ process.stderr.write(
1440
+ `[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via mesh.policy. `
1441
+ + `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
1442
+ );
1443
+ return { enabled: true, source: 'mesh.policy.allowAutoPublishSubmoduleMainCommits' };
513
1444
  }
514
-
515
- const [binary, second, third, ...rest] = tokens;
516
- let scriptName = '';
517
- let command = binary;
518
- let args: string[] = [];
519
-
520
- if ((binary === 'npm' || binary === 'pnpm' || binary === 'bun') && second === 'run' && third) {
521
- scriptName = third;
522
- args = ['run', scriptName, ...rest];
523
- } else if (binary === 'npm' && second === 'test' && !third) {
524
- scriptName = 'test';
525
- args = ['test'];
526
- } else if (binary === 'yarn' && second === 'run' && third) {
527
- scriptName = third;
528
- args = ['run', scriptName, ...rest];
529
- } else if (binary === 'yarn' && second && !third) {
530
- scriptName = second;
531
- args = [scriptName];
532
- } else {
533
- return { rejected: { command: rawCommand, category, source, reason: 'command is not a supported package-manager script invocation' } };
1445
+ const loaded = loadMeshRefineConfig(mesh, workspace);
1446
+ if (loaded.config?.allowAutoPublishSubmoduleMainCommits === true) {
1447
+ process.stderr.write(
1448
+ `[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via ${loaded.path || loaded.source}. `
1449
+ + `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
1450
+ );
1451
+ return { enabled: true, source: loaded.path || loaded.source };
534
1452
  }
1453
+ return { enabled: false };
1454
+ }
535
1455
 
536
- if (!scriptName || !Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
537
- return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script is not declared in package.json' } };
538
- }
539
- if (!scriptMatchesValidationCategory(scriptName, category)) {
540
- return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script name is outside the validation category allowlist' } };
1456
+ async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
1457
+ const { execFileSync } = await import('node:child_process');
1458
+ const diff = execFileSync('git', ['diff', '--patch', '--full-index', fromRef, toRef], {
1459
+ cwd,
1460
+ encoding: 'utf8',
1461
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1462
+ });
1463
+ if (!diff.trim()) return '';
1464
+ const patchId = execFileSync('git', ['patch-id', '--stable'], {
1465
+ cwd,
1466
+ input: diff,
1467
+ encoding: 'utf8',
1468
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1469
+ }).trim();
1470
+ return patchId.split(/\s+/)[0] || '';
1471
+ }
1472
+
1473
+ async function runMeshRefinePatchEquivalenceGate(
1474
+ repoRoot: string,
1475
+ baseHead: string,
1476
+ branchHead: string,
1477
+ ): Promise<MeshRefinePatchEquivalenceSummary> {
1478
+ const startedAt = Date.now();
1479
+ try {
1480
+ const { execFileSync } = await import('node:child_process');
1481
+ const git = (args: string[]) => execFileSync('git', args, {
1482
+ cwd: repoRoot,
1483
+ encoding: 'utf8',
1484
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1485
+ });
1486
+ const mergeBase = git(['merge-base', baseHead, branchHead]).trim();
1487
+ const mergeTreeStdout = git(['merge-tree', '--write-tree', baseHead, branchHead]);
1488
+ const mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || '';
1489
+ if (!mergeBase || !mergedTree) {
1490
+ return {
1491
+ status: 'failed',
1492
+ equivalent: false,
1493
+ baseHead,
1494
+ branchHead,
1495
+ mergeBase: mergeBase || undefined,
1496
+ mergedTree: mergedTree || undefined,
1497
+ durationMs: Date.now() - startedAt,
1498
+ error: 'patch equivalence preflight could not resolve merge-base or synthetic merge tree',
1499
+ stdout: truncateValidationOutput(mergeTreeStdout),
1500
+ };
1501
+ }
1502
+ const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
1503
+ const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree);
1504
+ const equivalent = expectedPatchId === actualPatchId;
1505
+ return {
1506
+ status: equivalent ? 'passed' : 'failed',
1507
+ equivalent,
1508
+ baseHead,
1509
+ branchHead,
1510
+ mergeBase,
1511
+ mergedTree,
1512
+ expectedPatchId,
1513
+ actualPatchId,
1514
+ durationMs: Date.now() - startedAt,
1515
+ };
1516
+ } catch (e: any) {
1517
+ return {
1518
+ status: 'failed',
1519
+ equivalent: false,
1520
+ baseHead,
1521
+ branchHead,
1522
+ durationMs: Date.now() - startedAt,
1523
+ error: e?.message || String(e),
1524
+ stdout: truncateValidationOutput(e?.stdout),
1525
+ stderr: truncateValidationOutput(e?.stderr),
1526
+ actionableHint: buildPatchEquivalenceSubmoduleConflictHint(
1527
+ repoRoot,
1528
+ baseHead,
1529
+ branchHead,
1530
+ `${e?.message || ''}\n${e?.stdout || ''}\n${e?.stderr || ''}`,
1531
+ ),
1532
+ };
541
1533
  }
1534
+ }
542
1535
 
1536
+ function buildPatchEquivalenceSubmoduleConflictHint(
1537
+ repoRoot: string,
1538
+ baseHead: string,
1539
+ branchHead: string,
1540
+ output: string,
1541
+ ): MeshRefineSubmoduleConflictHint | undefined {
1542
+ if (!/(submodule|160000)/i.test(output) || !/(conflict|failed to merge)/i.test(output)) return undefined;
1543
+ const conflicts = readChangedGitlinkPaths(repoRoot, baseHead, branchHead)
1544
+ .map(path => ({
1545
+ path,
1546
+ baseCommit: readTreeObject(repoRoot, baseHead, path),
1547
+ branchCommit: readTreeObject(repoRoot, branchHead, path),
1548
+ }));
1549
+ if (conflicts.length === 0) return undefined;
543
1550
  return {
544
- command: {
545
- command,
546
- args,
547
- displayCommand: [command, ...args].join(' '),
548
- category,
549
- source,
550
- },
1551
+ kind: 'submodule_conflict',
1552
+ message: 'Refinery could not synthesize a safe merge tree because the branch and base point the same submodule path at different commits.',
1553
+ conflicts,
1554
+ nextSteps: [
1555
+ 'Inspect the listed submodule path in both base and branch: baseCommit is the commit currently recorded by the base workspace, branchCommit is the commit recorded by the worktree branch.',
1556
+ 'Resolve the submodule first by checking out or creating the intended submodule commit, then commit the chosen gitlink in the root branch.',
1557
+ 'Ensure the chosen submodule commit is reachable from the configured submodule remote main branch, then rerun mesh_refine_node.',
1558
+ ],
551
1559
  };
552
1560
  }
553
1561
 
554
- function collectProjectContextValidationCandidates(mesh: any): Array<{ command: string; category: string; source: string; confidence?: string }> {
555
- const commands = mesh?.projectContext?.commands;
556
- if (!commands || typeof commands !== 'object' || Array.isArray(commands)) return [];
557
- const candidates: Array<{ command: string; category: string; source: string; confidence?: string }> = [];
558
- for (const category of REFINE_VALIDATION_CATEGORIES) {
559
- const entries = Array.isArray(commands[category]) ? commands[category] : [];
560
- for (const entry of entries) {
561
- if (typeof entry?.command !== 'string') continue;
562
- candidates.push({
563
- command: entry.command,
564
- category,
565
- source: typeof entry.sourcePath === 'string' ? entry.sourcePath : 'projectContext.commands',
566
- confidence: typeof entry.confidence === 'string' ? entry.confidence : undefined,
567
- });
1562
+ function readChangedGitlinkPaths(repoRoot: string, fromRef: string, toRef: string): string[] {
1563
+ try {
1564
+ const output = execFileSync('git', ['diff', '--raw', '--no-abbrev', fromRef, toRef], {
1565
+ cwd: repoRoot,
1566
+ encoding: 'utf8',
1567
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1568
+ });
1569
+ const paths = new Set<string>();
1570
+ for (const line of output.split('\n')) {
1571
+ if (!line.trim()) continue;
1572
+ const metaAndPath = line.split('\t');
1573
+ const meta = metaAndPath[0] || '';
1574
+ const path = metaAndPath[metaAndPath.length - 1]?.trim();
1575
+ if (!path) continue;
1576
+ const parts = meta.split(/\s+/);
1577
+ if (parts[0]?.includes('160000') || parts[1]?.includes('160000')) {
1578
+ paths.add(path);
1579
+ }
568
1580
  }
1581
+ return [...paths].sort();
1582
+ } catch {
1583
+ return [];
569
1584
  }
570
- return candidates.sort((a, b) => {
571
- const rank = (value?: string) => value === 'high' ? 0 : value === 'medium' ? 1 : 2;
572
- return rank(a.confidence) - rank(b.confidence);
573
- });
574
1585
  }
575
1586
 
576
- function collectPolicyValidationCandidates(mesh: any): Array<{ command: string; category: string; source: string }> {
577
- const policy = mesh?.policy && typeof mesh.policy === 'object' && !Array.isArray(mesh.policy) ? mesh.policy : {};
578
- const configured = Array.isArray(policy.validationCommands)
579
- ? policy.validationCommands
580
- : Array.isArray(policy.validationGate?.commands)
581
- ? policy.validationGate.commands
582
- : [];
583
- return configured
584
- .map((entry: any) => typeof entry === 'string' ? { command: entry, category: '', source: 'mesh.policy.validationCommands' } : entry)
585
- .filter((entry: any) => entry && typeof entry.command === 'string')
586
- .map((entry: any) => {
587
- const commandText = entry.command.trim();
588
- const category = REFINE_VALIDATION_CATEGORIES.find(cat => commandText.includes(` ${cat}`)) ?? '';
589
- return { command: commandText, category, source: 'mesh.policy.validationCommands' };
590
- })
591
- .filter((entry: any) => !!entry.category);
1587
+ function readTreeObject(repoRoot: string, ref: string, path: string): string | undefined {
1588
+ try {
1589
+ const output = execFileSync('git', ['ls-tree', ref, '--', path], {
1590
+ cwd: repoRoot,
1591
+ encoding: 'utf8',
1592
+ maxBuffer: 1024 * 1024,
1593
+ }).trim();
1594
+ const match = output.match(/\bcommit\s+([0-9a-f]{40})\b/i);
1595
+ return match?.[1];
1596
+ } catch {
1597
+ return undefined;
1598
+ }
592
1599
  }
593
1600
 
594
- function selectMeshRefineValidationCommands(mesh: any, workspace: string): { commands: MeshRefineValidationCommand[]; rejectedCommands: Array<Record<string, unknown>>; source: string } {
595
- const scripts = readPackageScripts(workspace);
596
- const rejectedCommands: Array<Record<string, unknown>> = [];
597
- const selected: MeshRefineValidationCommand[] = [];
598
- const seen = new Set<string>();
599
- const candidates = [
600
- ...collectPolicyValidationCandidates(mesh),
601
- ...collectProjectContextValidationCandidates(mesh),
602
- ];
603
-
604
- for (const candidate of candidates) {
605
- const parsed = parsePackageManagerValidationCommand(candidate.command, candidate.category, scripts, candidate.source);
606
- if (parsed.rejected) {
607
- rejectedCommands.push(parsed.rejected);
608
- continue;
609
- }
610
- if (!parsed.command || seen.has(parsed.command.displayCommand)) continue;
611
- selected.push(parsed.command);
612
- seen.add(parsed.command.displayCommand);
613
- if (selected.length >= REFINE_VALIDATION_MAX_COMMANDS) break;
614
- }
615
-
616
- if (!selected.length && candidates.length === 0) {
617
- for (const category of REFINE_VALIDATION_CATEGORIES) {
618
- if (!Object.prototype.hasOwnProperty.call(scripts, category)) continue;
619
- const fallback = parsePackageManagerValidationCommand(`npm run ${category}`, category, scripts, 'package.json:scripts');
620
- if (fallback.command && !seen.has(fallback.command.displayCommand)) {
621
- selected.push(fallback.command);
622
- seen.add(fallback.command.displayCommand);
623
- } else if (fallback.rejected) {
624
- rejectedCommands.push(fallback.rejected);
625
- }
626
- if (selected.length >= 2) break;
1601
+ async function alignRefinerySubmodulesAfterMerge(
1602
+ repoRoot: string,
1603
+ previousBaseHead: string,
1604
+ currentHead: string,
1605
+ options: { submoduleIgnorePaths?: string[] } = {},
1606
+ ): Promise<MeshRefineSubmoduleAlignmentSummary> {
1607
+ const startedAt = Date.now();
1608
+ const changedGitlinkPaths = readChangedGitlinkPaths(repoRoot, previousBaseHead, currentHead)
1609
+ .filter(path => !(options.submoduleIgnorePaths || []).includes(path));
1610
+ const preStatus = await getGitRepoStatus(repoRoot, {
1611
+ includeSubmodules: true,
1612
+ submoduleIgnorePaths: options.submoduleIgnorePaths,
1613
+ timeoutMs: 15_000,
1614
+ });
1615
+ const outOfSyncPaths = (preStatus.submodules || [])
1616
+ .filter(submodule => submodule.dirty || submodule.outOfSync || !!submodule.error)
1617
+ .map(submodule => submodule.path);
1618
+ const updatePaths = [...new Set([...changedGitlinkPaths, ...outOfSyncPaths])].sort();
1619
+
1620
+ if (updatePaths.length === 0) {
1621
+ return {
1622
+ status: 'skipped',
1623
+ changedGitlinkPaths,
1624
+ outOfSyncPaths,
1625
+ updatedPaths: [],
1626
+ verifiedPaths: [],
1627
+ durationMs: Date.now() - startedAt,
1628
+ reason: 'no_changed_or_out_of_sync_submodules',
1629
+ };
1630
+ }
1631
+
1632
+ const commandArgs = ['submodule', 'update', '--init', '--recursive', '--', ...updatePaths];
1633
+ try {
1634
+ const { execFile } = await import('node:child_process');
1635
+ const { promisify } = await import('node:util');
1636
+ const execFileAsync = promisify(execFile);
1637
+ const result = await execFileAsync('git', commandArgs, {
1638
+ cwd: repoRoot,
1639
+ encoding: 'utf8',
1640
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1641
+ timeout: 60_000,
1642
+ });
1643
+ const postStatus = await getGitRepoStatus(repoRoot, {
1644
+ includeSubmodules: true,
1645
+ submoduleIgnorePaths: options.submoduleIgnorePaths,
1646
+ timeoutMs: 15_000,
1647
+ });
1648
+ const remaining = (postStatus.submodules || [])
1649
+ .filter(submodule => updatePaths.includes(submodule.path) && (submodule.dirty || submodule.outOfSync || !!submodule.error));
1650
+ return {
1651
+ status: remaining.length === 0 ? 'passed' : 'failed',
1652
+ changedGitlinkPaths,
1653
+ outOfSyncPaths,
1654
+ updatedPaths: updatePaths,
1655
+ verifiedPaths: updatePaths.filter(path => !remaining.some(submodule => submodule.path === path)),
1656
+ durationMs: Date.now() - startedAt,
1657
+ command: `git ${commandArgs.join(' ')}`,
1658
+ stdout: truncateValidationOutput(result.stdout),
1659
+ stderr: truncateValidationOutput(result.stderr),
1660
+ ...(remaining.length > 0 ? { error: `Submodule checkout remained out of sync after update: ${remaining.map(entry => entry.path).join(', ')}` } : {}),
1661
+ };
1662
+ } catch (e: any) {
1663
+ return {
1664
+ status: 'failed',
1665
+ changedGitlinkPaths,
1666
+ outOfSyncPaths,
1667
+ updatedPaths: updatePaths,
1668
+ verifiedPaths: [],
1669
+ durationMs: Date.now() - startedAt,
1670
+ command: `git ${commandArgs.join(' ')}`,
1671
+ error: e?.message || String(e),
1672
+ stdout: truncateValidationOutput(e?.stdout),
1673
+ stderr: truncateValidationOutput(e?.stderr),
1674
+ };
1675
+ }
1676
+ }
1677
+
1678
+ async function runMeshRefineSubmoduleReachabilityGate(
1679
+ repoRoot: string,
1680
+ mergedTree: string,
1681
+ options: { allowAutoPublishSubmoduleMainCommits?: boolean; autoPublishPolicySource?: string; worktreeRoot?: string } = {},
1682
+ ): Promise<MeshRefineSubmoduleReachabilitySummary> {
1683
+ const startedAt = Date.now();
1684
+ const entries: MeshRefineSubmoduleReachabilityEntry[] = [];
1685
+ try {
1686
+ const { execFile } = await import('node:child_process');
1687
+ const { promisify } = await import('node:util');
1688
+ const execFileAsync = promisify(execFile);
1689
+ const runGit = async (cwd: string, args: string[]): Promise<string> => {
1690
+ const { stdout } = await execFileAsync('git', args, {
1691
+ cwd,
1692
+ encoding: 'utf8',
1693
+ timeout: 30_000,
1694
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1695
+ windowsHide: true,
1696
+ });
1697
+ return String(stdout || '');
1698
+ };
1699
+ const verifyRemoteMainContainsCommit = async (submodulePath: string, commit: string, branch = 'main'): Promise<void> => {
1700
+ await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
1701
+ await runGit(submodulePath, ['merge-base', '--is-ancestor', commit, `refs/remotes/origin/${branch}`]);
1702
+ };
1703
+ const publishCommitToRemoteMain = async (submodulePath: string, commit: string, branch = 'main'): Promise<{ stdout: string; stderr: string; refspec: string }> => {
1704
+ const refspec = `${commit}:refs/heads/${branch}`;
1705
+ const { stdout, stderr } = await execFileAsync('git', ['push', 'origin', refspec], {
1706
+ cwd: submodulePath,
1707
+ encoding: 'utf8',
1708
+ timeout: 30_000,
1709
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1710
+ windowsHide: true,
1711
+ });
1712
+ return { stdout: String(stdout || ''), stderr: String(stderr || ''), refspec };
1713
+ };
1714
+ const importCommitFromWorktreeSubmodule = async (submodulePath: string, worktreeSubmodulePath: string, commit: string): Promise<boolean> => {
1715
+ if (!fs.existsSync(worktreeSubmodulePath)) return false;
1716
+ try {
1717
+ await runGit(worktreeSubmodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
1718
+ } catch {
1719
+ return false;
1720
+ }
1721
+ await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', worktreeSubmodulePath, commit]);
1722
+ await runGit(submodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
1723
+ return true;
1724
+ };
1725
+
1726
+ const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
1727
+ const gitlinks = treeOutput
1728
+ .split('\0')
1729
+ .filter(Boolean)
1730
+ .map(record => {
1731
+ const match = /^160000\s+commit\s+([0-9a-f]{40})\t(.+)$/.exec(record);
1732
+ return match ? { commit: match[1], path: match[2] } : null;
1733
+ })
1734
+ .filter((entry): entry is { commit: string; path: string } => !!entry);
1735
+
1736
+ for (const gitlink of gitlinks) {
1737
+ const submodulePath = pathResolve(repoRoot, gitlink.path);
1738
+ const entry: MeshRefineSubmoduleReachabilityEntry = {
1739
+ path: gitlink.path,
1740
+ commit: gitlink.commit,
1741
+ reachable: false,
1742
+ };
1743
+ try {
1744
+ if (!fs.existsSync(submodulePath)) {
1745
+ entry.error = `Submodule checkout missing at ${gitlink.path}`;
1746
+ entry.publishRequired = true;
1747
+ if (options.allowAutoPublishSubmoduleMainCommits === true) {
1748
+ entry.autoPublishAllowed = true;
1749
+ entry.autoPublishAttempted = false;
1750
+ entry.autoPublishSkippedReason = `submodule checkout missing at ${gitlink.path}; cannot perform non-force push to origin/main`;
1751
+ }
1752
+ entries.push(entry);
1753
+ continue;
1754
+ }
1755
+
1756
+ entry.checkedLocal = true;
1757
+ try {
1758
+ await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
1759
+ entry.localReachable = true;
1760
+ } catch {
1761
+ entry.localReachable = false;
1762
+ if (options.allowAutoPublishSubmoduleMainCommits === true && options.worktreeRoot) {
1763
+ try {
1764
+ const imported = await importCommitFromWorktreeSubmodule(
1765
+ submodulePath,
1766
+ pathResolve(options.worktreeRoot, gitlink.path),
1767
+ gitlink.commit,
1768
+ );
1769
+ if (imported) {
1770
+ entry.localReachable = true;
1771
+ entry.importedFromWorktree = true;
1772
+ }
1773
+ } catch (importError: any) {
1774
+ entry.autoPublishSkippedReason = `candidate commit was not present in the source checkout and could not be imported from worktree submodule: ${truncateValidationOutput(importError?.stderr || importError?.message || String(importError))}`;
1775
+ }
1776
+ }
1777
+ // Probe the submodule remote before allowing cleanup/completion.
1778
+ }
1779
+
1780
+ try {
1781
+ entry.remote = 'origin';
1782
+ let remoteUrl = '';
1783
+ try {
1784
+ remoteUrl = (await runGit(submodulePath, ['remote', 'get-url', 'origin'])).trim();
1785
+ if (!remoteUrl) throw new Error('origin remote has no URL');
1786
+ entry.remoteUrl = remoteUrl;
1787
+ } catch {
1788
+ entry.error = 'Submodule remote reachability check failed: no configured origin remote';
1789
+ entry.publishRequired = true;
1790
+ if (options.allowAutoPublishSubmoduleMainCommits === true) {
1791
+ entry.autoPublishAllowed = true;
1792
+ entry.autoPublishAttempted = false;
1793
+ entry.autoPublishSkippedReason = 'submodule origin remote is not configured; cannot perform non-force push to origin/main';
1794
+ }
1795
+ entries.push(entry);
1796
+ continue;
1797
+ }
1798
+ entry.remoteMainBranch = 'main';
1799
+ try {
1800
+ await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
1801
+ entry.fetchedFromOrigin = true;
1802
+ entry.remoteReachable = true;
1803
+ entry.remoteMainReachable = true;
1804
+ entry.reachable = true;
1805
+ } catch (e: any) {
1806
+ entry.remoteReachable = false;
1807
+ entry.remoteMainReachable = false;
1808
+ entry.publishRequired = true;
1809
+ const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
1810
+ entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
1811
+ if (options.allowAutoPublishSubmoduleMainCommits === true && entry.localReachable === true) {
1812
+ entry.autoPublishAllowed = true;
1813
+ entry.autoPublishAttempted = true;
1814
+ try {
1815
+ const publish = await publishCommitToRemoteMain(submodulePath, gitlink.commit, 'main');
1816
+ entry.autoPublishRefspec = publish.refspec;
1817
+ entry.publishStdout = truncateValidationOutput(publish.stdout);
1818
+ entry.publishStderr = truncateValidationOutput(publish.stderr);
1819
+ entry.autoPublishSucceeded = true;
1820
+ await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
1821
+ entry.fetchedFromOrigin = true;
1822
+ entry.remoteReachable = true;
1823
+ entry.remoteMainReachable = true;
1824
+ entry.autoPublishVerified = true;
1825
+ entry.publishRequired = false;
1826
+ entry.reachable = true;
1827
+ entry.error = undefined;
1828
+ } catch (publishError: any) {
1829
+ entry.autoPublishSucceeded = false;
1830
+ entry.autoPublishVerified = false;
1831
+ const publishDetails = truncateValidationOutput(publishError?.stderr || publishError?.message || String(publishError));
1832
+ entry.error = `Submodule auto-publish to origin/main failed or could not be verified: ${publishDetails}`;
1833
+ }
1834
+ } else if (options.allowAutoPublishSubmoduleMainCommits === true) {
1835
+ entry.autoPublishAllowed = true;
1836
+ entry.autoPublishAttempted = false;
1837
+ entry.autoPublishSkippedReason = entry.autoPublishSkippedReason
1838
+ || 'candidate commit is not reachable in the source checkout or worktree submodule, so Refinery cannot push it to origin/main';
1839
+ }
1840
+ }
1841
+ } catch (e: any) {
1842
+ entry.remoteReachable = false;
1843
+ entry.remoteMainReachable = false;
1844
+ entry.publishRequired = true;
1845
+ const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
1846
+ entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
1847
+ }
1848
+ } catch (e: any) {
1849
+ entry.error = truncateValidationOutput(e?.message || String(e));
1850
+ entry.publishRequired = true;
1851
+ }
1852
+ entries.push(entry);
627
1853
  }
1854
+
1855
+ const unreachable = entries.filter(entry => !entry.reachable);
1856
+ return {
1857
+ status: unreachable.length ? 'failed' : 'passed',
1858
+ checked: entries.length,
1859
+ unreachable: unreachable.map(entry => ({ ...entry, publishRequired: entry.publishRequired !== false })),
1860
+ entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: entry.publishRequired !== false }),
1861
+ durationMs: Date.now() - startedAt,
1862
+ autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
1863
+ autoPublishPolicySource: options.autoPublishPolicySource,
1864
+ };
1865
+ } catch (e: any) {
1866
+ const unreachable = entries.filter(entry => !entry.reachable).map(entry => ({ ...entry, publishRequired: true }));
1867
+ return {
1868
+ status: 'failed',
1869
+ checked: entries.length,
1870
+ unreachable,
1871
+ entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: true }),
1872
+ durationMs: Date.now() - startedAt,
1873
+ autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
1874
+ autoPublishPolicySource: options.autoPublishPolicySource,
1875
+ error: truncateValidationOutput(e?.message || String(e)),
1876
+ };
628
1877
  }
1878
+ }
629
1879
 
1880
+ function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
1881
+ const plan = resolveMeshRefineValidationPlan(mesh, workspace);
1882
+ const mapCommand = (command: MeshRefineValidationCommandPlan) => ({
1883
+ displayCommand: command.displayCommand,
1884
+ category: command.category,
1885
+ source: command.source,
1886
+ cwd: command.cwd,
1887
+ timeoutMs: command.timeoutMs,
1888
+ });
630
1889
  return {
631
- commands: selected,
632
- rejectedCommands,
633
- source: selected.some(command => command.source === 'mesh.policy.validationCommands')
634
- ? 'mesh_policy'
635
- : selected.some(command => command.source !== 'package.json:scripts')
636
- ? 'project_context'
637
- : selected.length
638
- ? 'package_json_scripts'
639
- : 'unavailable',
1890
+ source: plan.source,
1891
+ sourceType: plan.sourceType,
1892
+ bootstrapCommands: plan.bootstrapCommands.map(mapCommand),
1893
+ commands: plan.commands.map(mapCommand),
1894
+ unavailableReason: plan.unavailableReason,
1895
+ rejectedCommands: plan.rejectedCommands,
1896
+ suggestions: plan.suggestions,
1897
+ suggestedConfig: plan.suggestedConfig,
1898
+ note: plan.sourceType === 'unavailable'
1899
+ ? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
1900
+ : 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
640
1901
  };
641
1902
  }
642
1903
 
@@ -644,60 +1905,119 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
644
1905
  const { execFile } = await import('node:child_process');
645
1906
  const { promisify } = await import('node:util');
646
1907
  const execFileAsync = promisify(execFile);
647
- const selection = selectMeshRefineValidationCommands(mesh, workspace);
1908
+ const selection = resolveMeshRefineValidationPlan(mesh, workspace);
648
1909
  const summary: MeshRefineValidationSummary = {
649
1910
  status: 'skipped',
650
1911
  required: true,
651
1912
  commandsRun: [],
1913
+ bootstrapCommandsRun: [],
652
1914
  rejectedCommands: selection.rejectedCommands,
653
1915
  skippedReason: undefined,
654
1916
  timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
655
1917
  outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
1918
+ configSource: selection.source,
1919
+ configSourceType: selection.sourceType,
1920
+ suggestions: selection.suggestions,
1921
+ suggestedConfig: selection.suggestedConfig,
656
1922
  };
657
1923
 
658
1924
  if (!selection.commands.length) {
659
- summary.skippedReason = 'validation_unavailable: no allowlisted projectContext, mesh policy, or package.json build/test/typecheck/lint command was available';
1925
+ summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
660
1926
  return summary;
661
1927
  }
662
1928
 
1929
+ const commandRecord = (candidate: MeshRefineValidationCommand, cwd: string, startedAt: number, result: any, passed: boolean, extras: Record<string, unknown> = {}) => ({
1930
+ command: candidate.command,
1931
+ args: candidate.args,
1932
+ displayCommand: candidate.displayCommand,
1933
+ category: candidate.category,
1934
+ source: candidate.source,
1935
+ cwd,
1936
+ passed,
1937
+ durationMs: Date.now() - startedAt,
1938
+ stdout: truncateValidationOutput(result?.stdout),
1939
+ stderr: truncateValidationOutput(result?.stderr || result?.message),
1940
+ ...extras,
1941
+ });
1942
+ const isPackageManagerValidation = (candidate: MeshRefineValidationCommand): boolean => {
1943
+ const command = pathBasename(candidate.command).replace(/\.(?:cmd|exe)$/i, '');
1944
+ return ['npm', 'pnpm', 'yarn', 'bun'].includes(command)
1945
+ && candidate.args.some(arg => arg === 'run' || arg === 'test' || arg === 'exec');
1946
+ };
1947
+ const dependenciesLikelyMissing = (cwd: string): boolean => {
1948
+ if (!fs.existsSync(pathJoin(cwd, 'package.json'))) return false;
1949
+ if (fs.existsSync(pathJoin(cwd, 'node_modules'))) return false;
1950
+ return ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb', 'bun.lock']
1951
+ .some(lock => fs.existsSync(pathJoin(cwd, lock)));
1952
+ };
1953
+
1954
+ for (const candidate of selection.bootstrapCommands) {
1955
+ const startedAt = Date.now();
1956
+ const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
1957
+ const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
1958
+ try {
1959
+ const result = await execFileAsync(candidate.command, candidate.args, {
1960
+ cwd,
1961
+ encoding: 'utf8',
1962
+ timeout,
1963
+ maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
1964
+ env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
1965
+ });
1966
+ summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
1967
+ } catch (error: any) {
1968
+ summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
1969
+ exitCode: typeof error?.code === 'number' ? error.code : null,
1970
+ signal: typeof error?.signal === 'string' ? error.signal : null,
1971
+ timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
1972
+ failureKind: 'dependency_bootstrap_failed',
1973
+ }));
1974
+ summary.status = 'failed';
1975
+ summary.failureKind = 'dependency_bootstrap_failed';
1976
+ summary.failureCode = 'dependency_bootstrap_failed';
1977
+ return summary;
1978
+ }
1979
+ }
1980
+
663
1981
  for (const candidate of selection.commands) {
664
1982
  const startedAt = Date.now();
1983
+ const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
1984
+ const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
1985
+ if (selection.bootstrapCommands.length === 0 && isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd)) {
1986
+ summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, {
1987
+ stderr: 'Dependencies appear to be missing: package.json and a lockfile are present, but node_modules is absent. Configure validation.bootstrapCommands in repo mesh/refine config if Refinery should install/bootstrap before validation.',
1988
+ }, false, {
1989
+ exitCode: null,
1990
+ skipped: true,
1991
+ failureKind: 'missing_dependencies',
1992
+ }));
1993
+ summary.status = 'failed';
1994
+ summary.failureKind = 'missing_dependencies';
1995
+ summary.failureCode = 'missing_dependencies';
1996
+ return summary;
1997
+ }
665
1998
  try {
666
1999
  const result = await execFileAsync(candidate.command, candidate.args, {
667
- cwd: workspace,
2000
+ cwd,
668
2001
  encoding: 'utf8',
669
- timeout: REFINE_VALIDATION_TIMEOUT_MS,
670
- maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
671
- env: { ...process.env, CI: process.env.CI || '1' },
672
- });
673
- summary.commandsRun.push({
674
- command: candidate.command,
675
- args: candidate.args,
676
- displayCommand: candidate.displayCommand,
677
- category: candidate.category,
678
- source: candidate.source,
679
- passed: true,
680
- exitCode: 0,
681
- durationMs: Date.now() - startedAt,
682
- stdout: truncateValidationOutput(result.stdout),
683
- stderr: truncateValidationOutput(result.stderr),
2002
+ timeout,
2003
+ maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
2004
+ env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
684
2005
  });
2006
+ summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
685
2007
  } catch (error: any) {
686
- summary.commandsRun.push({
687
- command: candidate.command,
688
- args: candidate.args,
689
- displayCommand: candidate.displayCommand,
690
- category: candidate.category,
691
- source: candidate.source,
692
- passed: false,
2008
+ const stderr = truncateValidationOutput(error?.stderr || error?.message);
2009
+ const missingDependencyFailure = /Cannot find module|MODULE_NOT_FOUND|node_modules|command not found|not found/i.test(stderr);
2010
+ summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
693
2011
  exitCode: typeof error?.code === 'number' ? error.code : null,
694
2012
  signal: typeof error?.signal === 'string' ? error.signal : null,
695
2013
  timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
696
- durationMs: Date.now() - startedAt,
697
- stdout: truncateValidationOutput(error?.stdout),
698
- stderr: truncateValidationOutput(error?.stderr || error?.message),
699
- });
2014
+ ...(missingDependencyFailure ? { failureKind: 'missing_dependencies' } : {}),
2015
+ }));
700
2016
  summary.status = 'failed';
2017
+ if (missingDependencyFailure) {
2018
+ summary.failureKind = 'missing_dependencies';
2019
+ summary.failureCode = 'missing_dependencies';
2020
+ }
701
2021
  return summary;
702
2022
  }
703
2023
  }
@@ -829,6 +2149,8 @@ export interface CommandRouterDeps {
829
2149
  sessionHostControl?: SessionHostControlPlane | null;
830
2150
  /** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
831
2151
  getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
2152
+ /** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
2153
+ dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
832
2154
  }
833
2155
 
834
2156
  export interface CommandRouterResult {
@@ -940,42 +2262,266 @@ function summarizeSessionHostPruneResult(result: unknown): Record<string, unknow
940
2262
  };
941
2263
  }
942
2264
 
2265
+ function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
2266
+ const raw = hostAddress.trim();
2267
+ if (!raw) throw new Error('hostAddress required');
2268
+ const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
2269
+ url.pathname = '/api/v1/command';
2270
+ url.search = '';
2271
+ url.hash = '';
2272
+ return url.toString();
2273
+ }
2274
+
2275
+ function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
2276
+ const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
2277
+ const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
2278
+ ? args.memberNode as Record<string, any>
2279
+ : null;
2280
+ const configured = Array.isArray(mesh?.nodes)
2281
+ ? (requestedNodeId
2282
+ ? mesh.nodes.find((node: any) => node?.id === requestedNodeId || node?.nodeId === requestedNodeId)
2283
+ : mesh.nodes[0])
2284
+ : null;
2285
+ const source = explicit || configured;
2286
+ const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
2287
+ ? source.workspace.trim()
2288
+ : typeof args?.workspace === 'string' && args.workspace.trim()
2289
+ ? args.workspace.trim()
2290
+ : process.cwd();
2291
+ if (!workspace) return null;
2292
+ const nodeId = typeof source?.id === 'string' && source.id.trim()
2293
+ ? source.id.trim()
2294
+ : typeof source?.nodeId === 'string' && source.nodeId.trim()
2295
+ ? source.nodeId.trim()
2296
+ : undefined;
2297
+ return {
2298
+ ...(nodeId ? { id: nodeId } : {}),
2299
+ workspace,
2300
+ ...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
2301
+ ...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
2302
+ ...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
2303
+ userOverrides: source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides) ? source.userOverrides : {},
2304
+ policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
2305
+ role: 'member',
2306
+ };
2307
+ }
2308
+
943
2309
  export class DaemonCommandRouter {
944
2310
  private deps: CommandRouterDeps;
945
2311
  /** In-memory cache for cloud-originating meshes passed via inlineMesh.
946
2312
  * Allows the MCP server to query mesh data via get_mesh even when
947
2313
  * the mesh doesn't exist in the local meshes.json file. */
948
2314
  private inlineMeshCache = new Map<string, any>();
2315
+ /** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
2316
+ private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any; queueRevision: string }>();
2317
+ /** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */
2318
+ private runningRefineJobs = new Map<string, MeshRefineJobHandle>();
2319
+ /** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
2320
+ private terminalRefineJobs = new Map<string, MeshRefineTerminalJob>();
949
2321
 
950
2322
  constructor(deps: CommandRouterDeps) {
951
2323
  this.deps = deps;
952
2324
  }
953
2325
 
2326
+ private cloneJsonValue<T>(value: T): T {
2327
+ if (typeof structuredClone === 'function') return structuredClone(value);
2328
+ return JSON.parse(JSON.stringify(value)) as T;
2329
+ }
2330
+
2331
+ private hydrateCachedAggregateMeshStatusFromInline(snapshot: any, mesh: any, options?: { requireDirectPeerTruth?: boolean }): any {
2332
+ if (!mesh || typeof mesh !== 'object' || !Array.isArray(mesh.nodes) || !Array.isArray(snapshot?.nodes)) return snapshot;
2333
+ const inlineNodesById = new Map<string, any>();
2334
+ for (const node of mesh.nodes) {
2335
+ const nodeId = readInlineMeshNodeId(node);
2336
+ if (nodeId) inlineNodesById.set(nodeId, node);
2337
+ }
2338
+ if (!inlineNodesById.size) return snapshot;
2339
+
2340
+ let changed = false;
2341
+ const unavailableNodeIds = new Set<string>();
2342
+ const sourceOfTruth = readObjectRecord(snapshot.sourceOfTruth);
2343
+ const directPeerTruth = readObjectRecord(sourceOfTruth.directPeerTruth);
2344
+ for (const entry of Array.isArray(directPeerTruth.unavailableNodeIds) ? directPeerTruth.unavailableNodeIds : []) {
2345
+ const nodeId = readStringValue(entry);
2346
+ if (nodeId) unavailableNodeIds.add(nodeId);
2347
+ }
2348
+
2349
+ const nodes = snapshot.nodes.map((statusNode: any) => {
2350
+ const nodeId = readStringValue(statusNode?.nodeId, statusNode?.id);
2351
+ const inlineNode = nodeId ? inlineNodesById.get(nodeId) : undefined;
2352
+ if (!inlineNode) return statusNode;
2353
+ const liveGit = buildInlineMeshTransitGitStatus(inlineNode);
2354
+ if (!liveGit) return statusNode;
2355
+ const nextStatus = { ...statusNode };
2356
+ nextStatus.git = liveGit;
2357
+ nextStatus.health = deriveMeshNodeHealthFromGit(liveGit);
2358
+ applyInlineMeshBranchConvergence(mesh, inlineNode, nextStatus);
2359
+ nextStatus.launchReady = readBooleanValue(nextStatus.launchReady) ?? true;
2360
+ const connection = readObjectRecord(nextStatus.connection);
2361
+ const connectionState = readStringValue(connection.state);
2362
+ const connectionReported = readBooleanValue(connection.reported) ?? false;
2363
+ if (!connectionReported || connectionState === 'unknown') {
2364
+ nextStatus.connection = buildLivePeerGitConnection(connection);
2365
+ }
2366
+ delete nextStatus.gitProbePending;
2367
+ const error = readStringValue(nextStatus.error);
2368
+ if (error && /pending_git|git probe|live peer git snapshot|no peer git snapshot/i.test(error)) delete nextStatus.error;
2369
+ if (!readStringValue(nextStatus.machineStatus)) nextStatus.machineStatus = 'online';
2370
+ if (nodeId) unavailableNodeIds.delete(nodeId);
2371
+ changed = true;
2372
+ return nextStatus;
2373
+ });
2374
+
2375
+ const aggregateDirectTruthSatisfied = sourceOfTruth.coordinatorOwnsLiveTruth === true
2376
+ || directPeerTruth.satisfied === true;
2377
+ if (!changed && !(options?.requireDirectPeerTruth && unavailableNodeIds.size > 0 && !aggregateDirectTruthSatisfied)) return snapshot;
2378
+ const nextSourceOfTruth = {
2379
+ ...sourceOfTruth,
2380
+ ...(Object.keys(directPeerTruth).length ? {
2381
+ directPeerTruth: {
2382
+ ...directPeerTruth,
2383
+ satisfied: options?.requireDirectPeerTruth === true
2384
+ ? aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0
2385
+ : directPeerTruth.satisfied,
2386
+ unavailableNodeIds: [...unavailableNodeIds],
2387
+ },
2388
+ ...(options?.requireDirectPeerTruth === true ? {
2389
+ coordinatorOwnsLiveTruth: aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0,
2390
+ currentStatus: aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0 ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
2391
+ } : {}),
2392
+ } : {}),
2393
+ };
2394
+ return {
2395
+ ...snapshot,
2396
+ ...(options?.requireDirectPeerTruth === true && unavailableNodeIds.size > 0 && !aggregateDirectTruthSatisfied ? {
2397
+ success: false,
2398
+ code: 'mesh_direct_peer_truth_unavailable',
2399
+ error: 'Selected coordinator could not confirm direct mesh truth for every remote node yet.',
2400
+ } : {}),
2401
+ sourceOfTruth: nextSourceOfTruth,
2402
+ branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodes),
2403
+ nodes,
2404
+ };
2405
+ }
2406
+
2407
+ private getCachedAggregateMeshStatus(meshId: string, mesh?: any, options?: { requireDirectPeerTruth?: boolean }): any | null {
2408
+ const cached = this.aggregateMeshStatusCache.get(meshId);
2409
+ if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null;
2410
+ if (cached.queueRevision !== getMeshQueueRevision(meshId)) return null;
2411
+ let snapshot = this.cloneJsonValue(cached.snapshot);
2412
+ snapshot = this.hydrateCachedAggregateMeshStatusFromInline(snapshot, mesh, options);
2413
+ if (shouldRefreshStalePendingAggregate(snapshot, options)) return null;
2414
+ const ageMs = Math.max(0, Date.now() - cached.builtAt);
2415
+ const sourceOfTruth = snapshot.sourceOfTruth && typeof snapshot.sourceOfTruth === 'object'
2416
+ ? snapshot.sourceOfTruth
2417
+ : {};
2418
+ snapshot.sourceOfTruth = {
2419
+ ...sourceOfTruth,
2420
+ aggregateSnapshot: {
2421
+ ...(sourceOfTruth.aggregateSnapshot && typeof sourceOfTruth.aggregateSnapshot === 'object'
2422
+ ? sourceOfTruth.aggregateSnapshot
2423
+ : {}),
2424
+ owner: 'coordinator_daemon_memory',
2425
+ cached: true,
2426
+ source: 'memory',
2427
+ refreshReason: 'memory_cache_hit',
2428
+ ageMs,
2429
+ cachedAt: new Date(cached.builtAt).toISOString(),
2430
+ returnedAt: new Date().toISOString(),
2431
+ },
2432
+ };
2433
+ return snapshot;
2434
+ }
2435
+
2436
+ private rememberAggregateMeshStatus(meshId: string, snapshot: any, refreshReason: string): any {
2437
+ if (!snapshot || typeof snapshot !== 'object' || snapshot.success !== true || !Array.isArray(snapshot.nodes)) return snapshot;
2438
+ const builtAt = Date.now();
2439
+ const next = this.cloneJsonValue(snapshot);
2440
+ const sourceOfTruth = next.sourceOfTruth && typeof next.sourceOfTruth === 'object'
2441
+ ? next.sourceOfTruth
2442
+ : {};
2443
+ next.sourceOfTruth = {
2444
+ ...sourceOfTruth,
2445
+ aggregateSnapshot: {
2446
+ owner: 'coordinator_daemon_memory',
2447
+ cached: false,
2448
+ source: 'live_refresh',
2449
+ refreshReason,
2450
+ ageMs: 0,
2451
+ cachedAt: new Date(builtAt).toISOString(),
2452
+ returnedAt: new Date(builtAt).toISOString(),
2453
+ },
2454
+ };
2455
+ this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next), queueRevision: getMeshQueueRevision(meshId) });
2456
+ return next;
2457
+ }
2458
+
954
2459
  public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
955
2460
  if (inlineMesh && typeof inlineMesh === 'object') {
956
- this.inlineMeshCache.set(meshId, inlineMesh as any);
957
- return inlineMesh as any;
2461
+ return this.warmInlineMeshCache(meshId, inlineMesh);
958
2462
  }
959
2463
  return this.inlineMeshCache.get(meshId);
960
2464
  }
961
2465
 
2466
+ private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
2467
+ if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
2468
+ const sanitizedInlineMesh = sanitizeInlineMesh(inlineMesh as any);
2469
+ const cached = this.inlineMeshCache.get(meshId);
2470
+ if (cached) {
2471
+ const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
2472
+ this.inlineMeshCache.set(meshId, merged);
2473
+ return merged;
2474
+ }
2475
+ this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
2476
+ return sanitizedInlineMesh as any;
2477
+ }
2478
+
962
2479
  private async getMeshForCommand(
963
2480
  meshId: string,
964
2481
  inlineMesh?: unknown,
965
2482
  options?: { preferInline?: boolean },
966
- ): Promise<{ mesh: any; inline: boolean } | null> {
2483
+ ): Promise<{ mesh: any; inline: boolean; source: 'inline_cache' | 'inline_bootstrap' | 'local_config' } | null> {
967
2484
  const preferInline = options?.preferInline === true;
968
2485
  if (preferInline) {
969
- const cached = this.getCachedInlineMesh(meshId, inlineMesh);
970
- if (cached) return { mesh: cached, inline: true };
2486
+ const cached = this.getCachedInlineMesh(meshId);
2487
+ if (cached) {
2488
+ if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
2489
+ const merged = reconcileInlineMeshCache(cached, inlineMesh as any);
2490
+ this.inlineMeshCache.set(meshId, sanitizeInlineMesh(merged));
2491
+ return { mesh: merged, inline: true, source: 'inline_cache' };
2492
+ }
2493
+ return { mesh: cached, inline: true, source: 'inline_cache' };
2494
+ }
2495
+ if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
2496
+ this.warmInlineMeshCache(meshId, inlineMesh);
2497
+ return { mesh: inlineMesh, inline: true, source: 'inline_bootstrap' };
2498
+ }
971
2499
  }
972
2500
  try {
973
2501
  const { getMesh } = await import('../config/mesh-config.js');
974
2502
  const mesh = getMesh(meshId);
975
- if (mesh) return { mesh, inline: false };
2503
+ if (mesh) return { mesh, inline: false, source: 'local_config' };
976
2504
  } catch { /* fall through to inline cache */ }
977
- const cached = this.getCachedInlineMesh(meshId, inlineMesh);
978
- return cached ? { mesh: cached, inline: true } : null;
2505
+ const cached = this.getCachedInlineMesh(meshId);
2506
+ if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
2507
+ const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
2508
+ return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
2509
+ }
2510
+
2511
+ private invalidateAggregateMeshStatus(meshId: string): void {
2512
+ this.aggregateMeshStatusCache.delete(meshId);
2513
+ }
2514
+
2515
+
2516
+ private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
2517
+ const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
2518
+ const mesh = meshRecord?.mesh;
2519
+ if (!mesh) return { success: false, error: 'Mesh not found' };
2520
+ const meshHost = resolveMeshHostStatus(mesh);
2521
+ if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
2522
+ return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
2523
+ }
2524
+ return null;
979
2525
  }
980
2526
 
981
2527
  private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
@@ -985,6 +2531,7 @@ export class DaemonCommandRouter {
985
2531
  else mesh.nodes.push(node);
986
2532
  mesh.updatedAt = new Date().toISOString();
987
2533
  this.inlineMeshCache.set(meshId, mesh);
2534
+ this.invalidateAggregateMeshStatus(meshId);
988
2535
  }
989
2536
 
990
2537
  private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
@@ -994,6 +2541,7 @@ export class DaemonCommandRouter {
994
2541
  mesh.nodes.splice(idx, 1);
995
2542
  mesh.updatedAt = new Date().toISOString();
996
2543
  this.inlineMeshCache.set(meshId, mesh);
2544
+ this.invalidateAggregateMeshStatus(meshId);
997
2545
  return true;
998
2546
  }
999
2547
 
@@ -1272,6 +2820,7 @@ export class DaemonCommandRouter {
1272
2820
  const deletedSessionIds: string[] = [];
1273
2821
  const skippedSessionIds: string[] = [];
1274
2822
  const skippedLiveSessionIds: string[] = [];
2823
+ const skippedCoordinatorSessionIds: string[] = [];
1275
2824
  const deleteUnsupportedSessionIds: string[] = [];
1276
2825
  const recordsRemainSessionIds: string[] = [];
1277
2826
  const errors: Array<{ sessionId: string; error: string }> = [];
@@ -1306,6 +2855,12 @@ export class DaemonCommandRouter {
1306
2855
  const completed = this.isCompletedHostedSession(record);
1307
2856
  const surfaceKind = getSessionHostSurfaceKind(record);
1308
2857
  const liveRuntime = surfaceKind === 'live_runtime';
2858
+ const coordinatorSession = readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId;
2859
+ if (!hasExplicitSessionIds && coordinatorSession) {
2860
+ skippedSessionIds.push(sessionId);
2861
+ skippedCoordinatorSessionIds.push(sessionId);
2862
+ continue;
2863
+ }
1309
2864
  if (!hasExplicitSessionIds && liveRuntime) {
1310
2865
  skippedSessionIds.push(sessionId);
1311
2866
  skippedLiveSessionIds.push(sessionId);
@@ -1375,6 +2930,7 @@ export class DaemonCommandRouter {
1375
2930
  deletedSessionIds,
1376
2931
  skippedSessionIds,
1377
2932
  skippedLiveSessionIds,
2933
+ skippedCoordinatorSessionIds,
1378
2934
  ...(deleteUnsupported ? {
1379
2935
  deleteUnsupported: true,
1380
2936
  effectiveCleanup: args.mode === 'stop_and_delete'
@@ -1515,6 +3071,567 @@ export class DaemonCommandRouter {
1515
3071
  }
1516
3072
  }
1517
3073
 
3074
+
3075
+ private buildRefineJobKey(meshId: string, nodeId: string): string {
3076
+ return `${meshId}:${nodeId}`;
3077
+ }
3078
+
3079
+ private buildRefineJobHandle(args: {
3080
+ meshId: string;
3081
+ nodeId: string;
3082
+ node?: any;
3083
+ status?: MeshRefineAsyncJobStatus;
3084
+ startedAt?: string;
3085
+ completedAt?: string;
3086
+ jobId?: string;
3087
+ interactionId?: string;
3088
+ retryOfJobId?: string;
3089
+ }): MeshRefineJobHandle {
3090
+ return {
3091
+ success: true,
3092
+ async: true,
3093
+ status: args.status || 'accepted',
3094
+ jobId: args.jobId || `refine_${createInteractionId()}`,
3095
+ interactionId: args.interactionId || createInteractionId(),
3096
+ meshId: args.meshId,
3097
+ nodeId: args.nodeId,
3098
+ targetNodeId: args.nodeId,
3099
+ targetDaemonId: readStringValue(args.node?.daemonId),
3100
+ workspace: readStringValue(args.node?.workspace),
3101
+ startedAt: args.startedAt || new Date().toISOString(),
3102
+ ...(args.completedAt ? { completedAt: args.completedAt } : {}),
3103
+ ...(args.retryOfJobId ? { retryOfJobId: args.retryOfJobId } : {}),
3104
+ eventDelivery: { pendingEvents: true, ledger: true },
3105
+ evidence: {
3106
+ pendingEventsCommand: 'get_pending_mesh_events',
3107
+ ledgerCommand: 'get_mesh_ledger_slice',
3108
+ taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
3109
+ },
3110
+ };
3111
+ }
3112
+
3113
+ private queueRefineJobEvent(event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
3114
+ const metadataEvent = {
3115
+ source: 'refine_mesh_node_async_job',
3116
+ jobId: handle.jobId,
3117
+ interactionId: handle.interactionId,
3118
+ meshId: handle.meshId,
3119
+ nodeId: handle.targetNodeId,
3120
+ targetDaemonId: handle.targetDaemonId,
3121
+ workspace: handle.workspace,
3122
+ status: handle.status,
3123
+ startedAt: handle.startedAt,
3124
+ completedAt: handle.completedAt,
3125
+ retryOfJobId: handle.retryOfJobId,
3126
+ ...(result ? { result } : {}),
3127
+ };
3128
+ const eventPayload = {
3129
+ event,
3130
+ meshId: handle.meshId,
3131
+ nodeLabel: handle.targetNodeId,
3132
+ nodeId: handle.targetNodeId,
3133
+ workspace: handle.workspace,
3134
+ metadataEvent,
3135
+ queuedAt: Date.now(),
3136
+ };
3137
+ if (typeof this.deps.instanceManager?.getByCategory === 'function') {
3138
+ const forwarded = handleMeshForwardEvent(
3139
+ { instanceManager: this.deps.instanceManager } as any,
3140
+ {
3141
+ event,
3142
+ meshId: handle.meshId,
3143
+ nodeId: handle.targetNodeId,
3144
+ workspace: handle.workspace,
3145
+ jobId: handle.jobId,
3146
+ interactionId: handle.interactionId,
3147
+ status: handle.status,
3148
+ targetDaemonId: handle.targetDaemonId,
3149
+ startedAt: handle.startedAt,
3150
+ completedAt: handle.completedAt,
3151
+ retryOfJobId: handle.retryOfJobId,
3152
+ ...(result ? { result } : {}),
3153
+ },
3154
+ );
3155
+ if (forwarded?.success === true) return;
3156
+ LOG.warn('Mesh', `[Refinery] Failed to forward async refine event ${event}: ${forwarded?.error || 'unknown error'}`);
3157
+ }
3158
+ queuePendingMeshCoordinatorEvent(eventPayload);
3159
+ }
3160
+
3161
+ private async appendRefineJobLedger(kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
3162
+ try {
3163
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
3164
+ appendLedgerEntry(handle.meshId, {
3165
+ kind,
3166
+ nodeId: handle.targetNodeId,
3167
+ payload: {
3168
+ source: 'refine_mesh_node_async_job',
3169
+ refineJob: {
3170
+ jobId: handle.jobId,
3171
+ interactionId: handle.interactionId,
3172
+ status: handle.status,
3173
+ meshId: handle.meshId,
3174
+ nodeId: handle.targetNodeId,
3175
+ targetDaemonId: handle.targetDaemonId,
3176
+ workspace: handle.workspace,
3177
+ startedAt: handle.startedAt,
3178
+ completedAt: handle.completedAt,
3179
+ retryOfJobId: handle.retryOfJobId,
3180
+ },
3181
+ async: true,
3182
+ retryOfJobId: handle.retryOfJobId,
3183
+ ...(result ? {
3184
+ success: result.success === true,
3185
+ result,
3186
+ finalBranchConvergenceState: result.finalBranchConvergenceState,
3187
+ } : {}),
3188
+ },
3189
+ });
3190
+ } catch (e: any) {
3191
+ LOG.warn('Mesh', `[Refinery] Failed to append async refine ledger entry: ${e?.message || e}`);
3192
+ }
3193
+ }
3194
+
3195
+ private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
3196
+ const refineStages: Array<Record<string, unknown>> = [];
3197
+ try {
3198
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
3199
+ const mesh = meshRecord?.mesh;
3200
+ const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
3201
+ if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
3202
+
3203
+ if (!node.isLocalWorktree || !node.workspace) {
3204
+ return { success: false, error: `Refinery requires a local worktree node`, refineStages };
3205
+ }
3206
+
3207
+ const sourceNode = node.clonedFromNodeId
3208
+ ? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
3209
+ : mesh?.nodes.find((n: any) => !n.isLocalWorktree);
3210
+ const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
3211
+ if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
3212
+
3213
+ const { execFile } = await import('node:child_process');
3214
+ const { promisify } = await import('node:util');
3215
+ const execFileAsync = promisify(execFile);
3216
+
3217
+ const resolveStarted = Date.now();
3218
+ const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
3219
+ const branch = branchStdout.trim();
3220
+ if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
3221
+
3222
+ const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
3223
+ const baseBranch = baseBranchStdout.trim();
3224
+ const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
3225
+ const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
3226
+ const baseHead = baseHeadStdout.trim();
3227
+ const branchHead = branchHeadStdout.trim();
3228
+ recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
3229
+
3230
+ const validationStarted = Date.now();
3231
+ const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
3232
+ recordMeshRefineStage(
3233
+ refineStages,
3234
+ 'validation',
3235
+ validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
3236
+ validationStarted,
3237
+ { validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
3238
+ );
3239
+ if (validationSummary.status === 'failed') {
3240
+ return {
3241
+ success: false,
3242
+ code: validationSummary.failureCode || 'validation_failed',
3243
+ convergenceStatus: 'blocked_review',
3244
+ error: validationSummary.failureCode === 'missing_dependencies'
3245
+ ? 'Refinery validation dependencies are missing; merge/refine was not attempted. Configure validation.bootstrapCommands if Refinery should bootstrap dependencies before validation.'
3246
+ : validationSummary.failureCode === 'dependency_bootstrap_failed'
3247
+ ? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
3248
+ : 'Refinery validation gate failed; merge/refine was not attempted.',
3249
+ branch,
3250
+ into: baseBranch,
3251
+ validationSummary,
3252
+ refineStages,
3253
+ finalBranchConvergenceState: {
3254
+ branch,
3255
+ baseBranch,
3256
+ merged: false,
3257
+ removed: false,
3258
+ validation: 'failed',
3259
+ status: 'blocked_review',
3260
+ },
3261
+ };
3262
+ }
3263
+ if (validationSummary.status === 'skipped') {
3264
+ return {
3265
+ success: false,
3266
+ code: 'validation_unavailable',
3267
+ convergenceStatus: 'blocked_review',
3268
+ error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
3269
+ branch,
3270
+ into: baseBranch,
3271
+ validationSummary,
3272
+ refineStages,
3273
+ finalBranchConvergenceState: {
3274
+ branch,
3275
+ baseBranch,
3276
+ merged: false,
3277
+ removed: false,
3278
+ validation: 'unavailable',
3279
+ status: 'blocked_review',
3280
+ },
3281
+ };
3282
+ }
3283
+
3284
+ const patchEquivalenceStarted = Date.now();
3285
+ const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
3286
+ recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
3287
+ equivalent: patchEquivalence.equivalent,
3288
+ expectedPatchId: patchEquivalence.expectedPatchId,
3289
+ actualPatchId: patchEquivalence.actualPatchId,
3290
+ error: patchEquivalence.error,
3291
+ actionableHint: patchEquivalence.actionableHint,
3292
+ });
3293
+ if (!patchEquivalence.equivalent) {
3294
+ return {
3295
+ success: false,
3296
+ code: 'patch_equivalence_failed',
3297
+ convergenceStatus: 'blocked_review',
3298
+ error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
3299
+ branch,
3300
+ into: baseBranch,
3301
+ validationSummary,
3302
+ patchEquivalence,
3303
+ refineStages,
3304
+ finalBranchConvergenceState: {
3305
+ branch,
3306
+ baseBranch,
3307
+ merged: false,
3308
+ removed: false,
3309
+ validation: 'passed',
3310
+ patchEquivalence: 'failed',
3311
+ status: 'blocked_review',
3312
+ },
3313
+ };
3314
+ }
3315
+
3316
+ const submoduleReachabilityStarted = Date.now();
3317
+ const autoPublishSubmoduleMainCommits = resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace);
3318
+ const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
3319
+ allowAutoPublishSubmoduleMainCommits: autoPublishSubmoduleMainCommits.enabled,
3320
+ autoPublishPolicySource: autoPublishSubmoduleMainCommits.source,
3321
+ worktreeRoot: node.workspace,
3322
+ });
3323
+ recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
3324
+ checked: submoduleReachability.checked,
3325
+ autoPublishAllowed: submoduleReachability.autoPublishAllowed,
3326
+ autoPublishPolicySource: submoduleReachability.autoPublishPolicySource,
3327
+ autoPublished: submoduleReachability.entries
3328
+ .filter(entry => entry.autoPublishAttempted)
3329
+ .map(entry => ({
3330
+ path: entry.path,
3331
+ commit: entry.commit,
3332
+ remote: entry.remote,
3333
+ remoteUrl: entry.remoteUrl,
3334
+ remoteMainBranch: entry.remoteMainBranch,
3335
+ refspec: entry.autoPublishRefspec,
3336
+ succeeded: entry.autoPublishSucceeded,
3337
+ verified: entry.autoPublishVerified,
3338
+ remoteMainReachable: entry.remoteMainReachable,
3339
+ error: entry.error,
3340
+ })),
3341
+ autoPublishSkipped: submoduleReachability.entries
3342
+ .filter(entry => entry.autoPublishAllowed === true && entry.autoPublishAttempted !== true)
3343
+ .map(entry => ({
3344
+ path: entry.path,
3345
+ commit: entry.commit,
3346
+ remote: entry.remote,
3347
+ remoteUrl: entry.remoteUrl,
3348
+ remoteMainBranch: entry.remoteMainBranch,
3349
+ reason: entry.autoPublishSkippedReason || entry.error || 'auto-publish was allowed but no publish attempt was possible',
3350
+ })),
3351
+ unreachable: submoduleReachability.unreachable.map(entry => ({
3352
+ path: entry.path,
3353
+ commit: entry.commit,
3354
+ publishRequired: entry.publishRequired === true,
3355
+ autoPublishAllowed: entry.autoPublishAllowed,
3356
+ autoPublishAttempted: entry.autoPublishAttempted,
3357
+ autoPublishSucceeded: entry.autoPublishSucceeded,
3358
+ autoPublishVerified: entry.autoPublishVerified,
3359
+ autoPublishRefspec: entry.autoPublishRefspec,
3360
+ autoPublishSkippedReason: entry.autoPublishSkippedReason,
3361
+ remote: entry.remote,
3362
+ remoteUrl: entry.remoteUrl,
3363
+ remoteReachable: entry.remoteReachable,
3364
+ remoteMainBranch: entry.remoteMainBranch,
3365
+ remoteMainReachable: entry.remoteMainReachable,
3366
+ error: entry.error,
3367
+ })),
3368
+ error: submoduleReachability.error,
3369
+ });
3370
+ if (submoduleReachability.status === 'failed') {
3371
+ const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
3372
+ return {
3373
+ success: false,
3374
+ code: 'submodule_reachability_failed',
3375
+ convergenceStatus: 'blocked_review',
3376
+ publishRequired: true,
3377
+ blockedReason: 'submodule_publish_required',
3378
+ error: 'Refinery submodule reachability preflight failed because one or more submodule gitlink commits are not reachable from their configured remote main branch; merge/refine cleanup was not attempted.',
3379
+ nextStep,
3380
+ nextSteps: [
3381
+ 'Ask the user for explicit approval before pushing or publishing any submodule commit.',
3382
+ 'Push/publish each unreachable submodule commit to the configured submodule remote main branch shown in the evidence.',
3383
+ 'Rerun mesh_refine_node after remote reachability is confirmed.',
3384
+ 'Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.',
3385
+ ],
3386
+ unreachableSubmoduleCommits: submoduleReachability.unreachable.map(entry => ({
3387
+ path: entry.path,
3388
+ commit: entry.commit,
3389
+ remote: entry.remote,
3390
+ remoteUrl: entry.remoteUrl,
3391
+ remoteReachable: entry.remoteReachable,
3392
+ remoteMainBranch: entry.remoteMainBranch,
3393
+ remoteMainReachable: entry.remoteMainReachable,
3394
+ autoPublishAllowed: entry.autoPublishAllowed,
3395
+ autoPublishAttempted: entry.autoPublishAttempted,
3396
+ autoPublishSucceeded: entry.autoPublishSucceeded,
3397
+ autoPublishVerified: entry.autoPublishVerified,
3398
+ autoPublishRefspec: entry.autoPublishRefspec,
3399
+ autoPublishSkippedReason: entry.autoPublishSkippedReason,
3400
+ error: entry.error,
3401
+ })),
3402
+ branch,
3403
+ into: baseBranch,
3404
+ validationSummary,
3405
+ patchEquivalence,
3406
+ submoduleReachability,
3407
+ refineStages,
3408
+ finalBranchConvergenceState: {
3409
+ branch,
3410
+ baseBranch,
3411
+ merged: false,
3412
+ removed: false,
3413
+ validation: 'passed',
3414
+ patchEquivalence: 'passed',
3415
+ submoduleReachability: 'failed',
3416
+ status: 'blocked_review',
3417
+ reason: 'submodule_publish_required',
3418
+ nextStep,
3419
+ },
3420
+ };
3421
+ }
3422
+
3423
+ let mergeResult: Record<string, unknown> | undefined;
3424
+ const mergeStarted = Date.now();
3425
+ try {
3426
+ const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
3427
+ mergeResult = {
3428
+ stdout: truncateValidationOutput(result.stdout),
3429
+ stderr: truncateValidationOutput(result.stderr),
3430
+ durationMs: Date.now() - mergeStarted,
3431
+ };
3432
+ recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
3433
+ } catch (e: any) {
3434
+ recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
3435
+ error: e?.message || String(e),
3436
+ stdout: truncateValidationOutput(e?.stdout),
3437
+ stderr: truncateValidationOutput(e?.stderr),
3438
+ });
3439
+ return {
3440
+ success: false,
3441
+ error: `Merge failed (conflicts?): ${e.message}`,
3442
+ validationSummary,
3443
+ patchEquivalence,
3444
+ refineStages,
3445
+ finalBranchConvergenceState: {
3446
+ branch,
3447
+ baseBranch,
3448
+ merged: false,
3449
+ removed: false,
3450
+ validation: 'passed',
3451
+ patchEquivalence: 'passed',
3452
+ status: 'not_mergeable',
3453
+ },
3454
+ };
3455
+ }
3456
+
3457
+ const submoduleAlignmentStarted = Date.now();
3458
+ const submoduleAlignment = await alignRefinerySubmodulesAfterMerge(repoRoot, baseHead, 'HEAD', {
3459
+ submoduleIgnorePaths: Array.isArray(sourceNode?.policy?.submoduleIgnorePaths)
3460
+ ? sourceNode.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
3461
+ : undefined,
3462
+ });
3463
+ if (submoduleAlignment.status !== 'skipped') {
3464
+ recordMeshRefineStage(refineStages, 'submodule_alignment', submoduleAlignment.status, submoduleAlignmentStarted, {
3465
+ changedGitlinkPaths: submoduleAlignment.changedGitlinkPaths,
3466
+ outOfSyncPaths: submoduleAlignment.outOfSyncPaths,
3467
+ updatedPaths: submoduleAlignment.updatedPaths,
3468
+ verifiedPaths: submoduleAlignment.verifiedPaths,
3469
+ command: submoduleAlignment.command,
3470
+ error: submoduleAlignment.error,
3471
+ });
3472
+ }
3473
+ if (submoduleAlignment.status === 'failed') {
3474
+ return {
3475
+ success: false,
3476
+ code: 'post_merge_submodule_alignment_failed',
3477
+ error: 'Refinery merge completed but post-merge submodule checkout alignment failed; run the reported git submodule update command and re-check base workspace status.',
3478
+ merged: true,
3479
+ branch,
3480
+ into: baseBranch,
3481
+ validationSummary,
3482
+ patchEquivalence,
3483
+ submoduleReachability,
3484
+ submoduleAlignment,
3485
+ mergeResult,
3486
+ refineStages,
3487
+ finalBranchConvergenceState: {
3488
+ branch: baseBranch,
3489
+ mergedBranch: branch,
3490
+ baseBranch,
3491
+ merged: true,
3492
+ removed: false,
3493
+ validation: 'passed',
3494
+ patchEquivalence: 'passed',
3495
+ submoduleReachability: 'passed',
3496
+ submoduleAlignment: 'failed',
3497
+ status: 'post_merge_alignment_failed',
3498
+ nextStep: submoduleAlignment.command || 'Run git submodule update --init --recursive for the reported path(s), then re-check base workspace status.',
3499
+ },
3500
+ };
3501
+ }
3502
+
3503
+ const cleanupStarted = Date.now();
3504
+ const removeResult = await this.execute('remove_mesh_node', {
3505
+ meshId,
3506
+ nodeId,
3507
+ sessionCleanupMode: 'preserve',
3508
+ inlineMesh: args?.inlineMesh,
3509
+ });
3510
+ recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
3511
+ removed: removeResult?.removed,
3512
+ code: removeResult?.code,
3513
+ error: removeResult?.error,
3514
+ });
3515
+
3516
+ let ledgerError: string | undefined;
3517
+ const ledgerStarted = Date.now();
3518
+ try {
3519
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
3520
+ appendLedgerEntry(meshId, {
3521
+ kind: 'node_removed',
3522
+ nodeId,
3523
+ payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence, submoduleReachability, submoduleAlignment },
3524
+ });
3525
+ recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
3526
+ } catch (e: any) {
3527
+ ledgerError = e?.message || String(e);
3528
+ recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
3529
+ }
3530
+
3531
+ const finalBranchConvergenceState = {
3532
+ branch: baseBranch,
3533
+ mergedBranch: branch,
3534
+ baseBranch,
3535
+ merged: true,
3536
+ removed: removeResult?.success !== false,
3537
+ validation: 'passed',
3538
+ patchEquivalence: 'passed',
3539
+ submoduleAlignment: submoduleAlignment.status,
3540
+ status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
3541
+ };
3542
+
3543
+ if (removeResult?.success === false) {
3544
+ return {
3545
+ success: false,
3546
+ code: 'cleanup_failed',
3547
+ error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
3548
+ merged: true,
3549
+ branch,
3550
+ into: baseBranch,
3551
+ removeResult,
3552
+ validationSummary,
3553
+ patchEquivalence,
3554
+ submoduleReachability,
3555
+ submoduleAlignment,
3556
+ mergeResult,
3557
+ refineStages,
3558
+ ...(ledgerError ? { ledgerError } : {}),
3559
+ finalBranchConvergenceState,
3560
+ };
3561
+ }
3562
+
3563
+ return {
3564
+ success: true,
3565
+ merged: true,
3566
+ branch,
3567
+ into: baseBranch,
3568
+ removeResult,
3569
+ validationSummary,
3570
+ patchEquivalence,
3571
+ submoduleReachability,
3572
+ submoduleAlignment,
3573
+ mergeResult,
3574
+ refineStages,
3575
+ ...(ledgerError ? { ledgerError } : {}),
3576
+ finalBranchConvergenceState,
3577
+ };
3578
+ } catch (e: any) {
3579
+ return { success: false, error: e.message, refineStages };
3580
+ }
3581
+ }
3582
+
3583
+ private async finishMeshRefineJob(handle: MeshRefineJobHandle, args: any): Promise<void> {
3584
+ const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
3585
+ let result: Record<string, unknown>;
3586
+ try {
3587
+ result = await this.executeMeshRefineNodeSynchronously(handle.meshId, handle.targetNodeId, args) as Record<string, unknown>;
3588
+ } catch (e: any) {
3589
+ result = { success: false, error: e?.message || String(e) };
3590
+ }
3591
+ const completedAt = new Date().toISOString();
3592
+ const terminalHandle = this.buildRefineJobHandle({
3593
+ meshId: handle.meshId,
3594
+ nodeId: handle.targetNodeId,
3595
+ status: result.success === true ? 'completed' : 'failed',
3596
+ startedAt: handle.startedAt,
3597
+ completedAt,
3598
+ jobId: handle.jobId,
3599
+ interactionId: handle.interactionId,
3600
+ retryOfJobId: handle.retryOfJobId,
3601
+ node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
3602
+ });
3603
+ const terminal: MeshRefineTerminalJob = { ...terminalHandle, result };
3604
+ this.terminalRefineJobs.set(key, terminal);
3605
+ this.runningRefineJobs.delete(key);
3606
+ this.invalidateAggregateMeshStatus(handle.meshId);
3607
+ await this.appendRefineJobLedger(result.success === true ? 'task_completed' : 'task_failed', terminalHandle, result);
3608
+ this.queueRefineJobEvent(result.success === true ? 'refine:completed' : 'refine:failed', terminalHandle, result);
3609
+ }
3610
+
3611
+ private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
3612
+ const key = this.buildRefineJobKey(meshId, nodeId);
3613
+ const running = this.runningRefineJobs.get(key);
3614
+ if (running) return { ...running, duplicate: true };
3615
+ const terminal = this.terminalRefineJobs.get(key);
3616
+
3617
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
3618
+ const mesh = meshRecord?.mesh;
3619
+ const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
3620
+ if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
3621
+ if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
3622
+
3623
+ const handle = this.buildRefineJobHandle({ meshId, nodeId, node, retryOfJobId: terminal?.jobId });
3624
+ this.runningRefineJobs.set(key, handle);
3625
+ await this.appendRefineJobLedger('task_dispatched', handle);
3626
+ this.queueRefineJobEvent('refine:accepted', handle);
3627
+
3628
+ setImmediate(() => {
3629
+ void this.finishMeshRefineJob(handle, args);
3630
+ });
3631
+
3632
+ return handle;
3633
+ }
3634
+
1518
3635
  // ─── Daemon-level command core ───────────────────
1519
3636
 
1520
3637
  /**
@@ -1529,13 +3646,35 @@ export class DaemonCommandRouter {
1529
3646
  }
1530
3647
 
1531
3648
  case 'get_pending_mesh_events': {
1532
- const events = drainPendingMeshCoordinatorEvents();
3649
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
3650
+ // (B3) Respect coordinatorDaemonId when the caller declares it
3651
+ // so unicast events route to the right coordinator instead of
3652
+ // being silently consumed by the first drainer.
3653
+ const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
3654
+ ? args.coordinatorDaemonId.trim()
3655
+ : undefined;
3656
+ const events = drainPendingMeshCoordinatorEvents(meshId || undefined, coordinatorDaemonId);
1533
3657
  return { success: true, events };
1534
3658
  }
1535
3659
 
3660
+ case 'interactive_prompt_response': {
3661
+ const sessionId = typeof args?.targetSessionId === 'string' && args.targetSessionId.trim()
3662
+ ? args.targetSessionId.trim()
3663
+ : typeof args?.sessionId === 'string' && args.sessionId.trim()
3664
+ ? args.sessionId.trim()
3665
+ : '';
3666
+ if (!sessionId) return { success: false, error: 'targetSessionId required' };
3667
+ const response = normalizeInteractivePromptResponse(args?.response ?? args);
3668
+ const instance = this.deps.instanceManager.getInstance(sessionId);
3669
+ if (!instance) return { success: false, error: `No running instance for session ${sessionId}` };
3670
+ this.deps.instanceManager.sendEvent(sessionId, 'interactive_prompt_response', response);
3671
+ return { success: true };
3672
+ }
3673
+
1536
3674
  case 'launch_cli':
1537
3675
  case 'stop_cli':
1538
3676
  case 'set_cli_view_mode':
3677
+ case 'record_provider_pty':
1539
3678
  case 'agent_command': {
1540
3679
  return this.deps.cliManager.handleCliCommand(cmd, args);
1541
3680
  }
@@ -1731,7 +3870,7 @@ export class DaemonCommandRouter {
1731
3870
  : '';
1732
3871
  const providerMeta = this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType);
1733
3872
  const { sessions: historySessions, hasMore, source } = listProviderHistorySessions(providerType, {
1734
- canonicalHistory: providerMeta?.canonicalHistory,
3873
+ canonicalHistory: providerMeta?.nativeHistory,
1735
3874
  offset,
1736
3875
  limit,
1737
3876
  historyBehavior: providerMeta?.historyBehavior,
@@ -1953,6 +4092,128 @@ export class DaemonCommandRouter {
1953
4092
  };
1954
4093
  }
1955
4094
 
4095
+ // Session-info popup data. Aggregates whatever the daemon knows
4096
+ // about a single live session into one envelope so the dashboard
4097
+ // doesn't need to stitch together status + coordinator registry +
4098
+ // session registry on the client. Includes the actual system
4099
+ // prompt that was injected at launch when the session is a mesh
4100
+ // coordinator — that's the "what prompt did the agent see?"
4101
+ // question the info-icon dialog is meant to answer.
4102
+ case 'get_session_info': {
4103
+ const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
4104
+ : typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
4105
+ if (!sessionId) return { success: false, error: 'targetSessionId required' };
4106
+ // Fetch both lookups up front. We used to bail with "Session not
4107
+ // found" when sessionRegistry forgot the SID (auto-cleanup,
4108
+ // daemon restart with the session not yet restored, etc), which
4109
+ // hid the coordinator-side metadata even though the
4110
+ // coordinator-registry still has it. Now we return whichever
4111
+ // side we have. The dashboard renders "no coordinator-specific
4112
+ // prompt" only when *neither* side knows the session.
4113
+ const target = this.deps.sessionRegistry.get(sessionId);
4114
+ const coord = getCoordinatorForSession(sessionId);
4115
+ if (!target && !coord) return { success: false, error: 'Session not found', sessionId };
4116
+ const adapter = target
4117
+ ? this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter
4118
+ : undefined;
4119
+ const runtimeMeta = (adapter && typeof (adapter as any).getRuntimeMetadata === 'function')
4120
+ ? (adapter as any).getRuntimeMetadata()
4121
+ : undefined;
4122
+ const providerType = target?.providerType || coord?.cliType || '';
4123
+ const providerMetaForSession = providerType
4124
+ ? this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType)
4125
+ : undefined;
4126
+ return {
4127
+ success: true,
4128
+ session: {
4129
+ sessionId,
4130
+ providerType,
4131
+ providerName: providerMetaForSession?.name,
4132
+ transport: target?.transport,
4133
+ workspace: (target as any)?.workspace || coord?.workspace,
4134
+ spawnedAtMs: (target as any)?.spawnedAtMs || coord?.startedAt,
4135
+ providerSessionId: (target as any)?.providerSessionId,
4136
+ runtimeMetadata: runtimeMeta,
4137
+ },
4138
+ coordinator: coord ? {
4139
+ meshId: coord.meshId,
4140
+ startedAt: coord.startedAt,
4141
+ cliType: coord.cliType,
4142
+ systemPrompt: coord.systemPrompt,
4143
+ extraSystemPrompt: coord.extraSystemPrompt,
4144
+ injection: coord.injection,
4145
+ mcpConfigPath: coord.mcpConfigPath,
4146
+ } : null,
4147
+ };
4148
+ }
4149
+
4150
+ // ── User-level coordinator-prompt files (~/.adhdev/coordinator-prompts/).
4151
+ // These live on this daemon's filesystem and never sync to the
4152
+ // cloud / other daemons — they're per-machine config. The
4153
+ // Settings page in the dashboard reads/writes via these two
4154
+ // commands instead of going through fs from the browser.
4155
+ case 'list_coordinator_prompts': {
4156
+ const fs = await import('node:fs');
4157
+ const path = await import('node:path');
4158
+ const os = await import('node:os');
4159
+ const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
4160
+ const entries: Record<string, { override: string; append: string }> = {};
4161
+ try {
4162
+ if (fs.existsSync(dir)) {
4163
+ for (const name of fs.readdirSync(dir)) {
4164
+ // Bucket files into <key>.{md|append.md}; ignore others
4165
+ // so a stray README or .DS_Store doesn't show up.
4166
+ const matchOverride = name.match(/^([a-zA-Z0-9_.-]+)\.md$/);
4167
+ const matchAppend = name.match(/^([a-zA-Z0-9_.-]+)\.append\.md$/);
4168
+ // append-pattern wins when both match (file is `.append.md`).
4169
+ const m = matchAppend || matchOverride;
4170
+ if (!m) continue;
4171
+ const isAppend = !!matchAppend;
4172
+ const key = m[1];
4173
+ const full = path.join(dir, name);
4174
+ let content = '';
4175
+ try { content = fs.readFileSync(full, 'utf8'); } catch { /* skip */ }
4176
+ if (!entries[key]) entries[key] = { override: '', append: '' };
4177
+ if (isAppend) entries[key].append = content;
4178
+ else entries[key].override = content;
4179
+ }
4180
+ }
4181
+ } catch (error: any) {
4182
+ return { success: false, error: error?.message || String(error) };
4183
+ }
4184
+ return { success: true, dir, entries };
4185
+ }
4186
+
4187
+ case 'write_coordinator_prompt': {
4188
+ const fs = await import('node:fs');
4189
+ const path = await import('node:path');
4190
+ const os = await import('node:os');
4191
+ const key = typeof args?.key === 'string' ? args.key.trim() : '';
4192
+ const kind = args?.kind === 'append' ? 'append' : 'override';
4193
+ const content = typeof args?.content === 'string' ? args.content : '';
4194
+ // Whitelist key chars so a malicious caller can't write
4195
+ // ../../etc/passwd. Same charset readUserPromptFile accepts.
4196
+ if (!key || !/^[a-zA-Z0-9_.-]+$/.test(key)) {
4197
+ return { success: false, error: 'key must match [a-zA-Z0-9_.-]+' };
4198
+ }
4199
+ const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
4200
+ const filename = kind === 'append' ? `${key}.append.md` : `${key}.md`;
4201
+ const full = path.join(dir, filename);
4202
+ try {
4203
+ fs.mkdirSync(dir, { recursive: true });
4204
+ if (content.trim()) {
4205
+ fs.writeFileSync(full, content, { encoding: 'utf8', mode: 0o600 });
4206
+ } else if (fs.existsSync(full)) {
4207
+ // Empty content = "reset to default" — delete the file
4208
+ // so the daemon's readUserPromptFile path falls through.
4209
+ fs.unlinkSync(full);
4210
+ }
4211
+ return { success: true, path: full, kind, key };
4212
+ } catch (error: any) {
4213
+ return { success: false, error: error?.message || String(error) };
4214
+ }
4215
+ }
4216
+
1956
4217
  case 'mark_session_seen': {
1957
4218
  const sessionId = args?.sessionId;
1958
4219
  if (!sessionId || typeof sessionId !== 'string') {
@@ -2124,53 +4385,312 @@ export class DaemonCommandRouter {
2124
4385
  // ─── Mesh CRUD (local meshes.json) ───
2125
4386
  case 'list_meshes': {
2126
4387
  try {
2127
- const { listMeshes } = await import('../config/mesh-config.js');
2128
- return { success: true, meshes: listMeshes() };
4388
+ const { listMeshes } = await import('../config/mesh-config.js');
4389
+ return { success: true, meshes: listMeshes() };
4390
+ } catch (e: any) {
4391
+ return { success: false, error: e.message };
4392
+ }
4393
+ }
4394
+
4395
+ case 'get_mesh': {
4396
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4397
+ if (!meshId) return { success: false, error: 'meshId required' };
4398
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
4399
+ if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
4400
+
4401
+ const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
4402
+ const directTruth = await hydrateInlineMeshDirectTruth({
4403
+ mesh: meshRecord.mesh,
4404
+ meshSource: meshRecord.source,
4405
+ dispatchMeshCommand: this.deps.dispatchMeshCommand,
4406
+ statusInstanceId: this.deps.statusInstanceId,
4407
+ localMachineId: loadConfig().machineId || '',
4408
+ });
4409
+ const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
4410
+ const sourceOfTruth = {
4411
+ membership: meshRecord.source === 'inline_cache'
4412
+ ? 'coordinator_inline_mesh_cache'
4413
+ : meshRecord.source === 'local_config'
4414
+ ? 'local_mesh_config'
4415
+ : 'inline_bootstrap_snapshot',
4416
+ coordinatorOwnsLiveTruth: directTruthSatisfied,
4417
+ directPeerTruth: {
4418
+ required: requireDirectPeerTruth,
4419
+ satisfied: directTruthSatisfied,
4420
+ directEvidenceCount: directTruth.directEvidenceCount,
4421
+ localConfirmedCount: directTruth.localConfirmedCount,
4422
+ peerAttemptedCount: directTruth.peerAttemptedCount,
4423
+ peerConfirmedCount: directTruth.peerConfirmedCount,
4424
+ unavailableNodeIds: directTruth.unavailableNodeIds,
4425
+ },
4426
+ };
4427
+ if (requireDirectPeerTruth && !directTruthSatisfied) {
4428
+ return {
4429
+ success: false,
4430
+ code: 'mesh_direct_peer_truth_unavailable',
4431
+ error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
4432
+ sourceOfTruth,
4433
+ };
4434
+ }
4435
+ return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
4436
+ }
4437
+
4438
+ case 'create_mesh': {
4439
+ const name = typeof args?.name === 'string' ? args.name.trim() : '';
4440
+ const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
4441
+ const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
4442
+ const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
4443
+ if (!name) return { success: false, error: 'name required' };
4444
+ try {
4445
+ const { createMesh } = await import('../config/mesh-config.js');
4446
+ const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
4447
+ ? args.meshHost
4448
+ : undefined;
4449
+ const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
4450
+ return { success: true, mesh };
4451
+ } catch (e: any) {
4452
+ return { success: false, error: e.message };
4453
+ }
4454
+ }
4455
+
4456
+ case 'update_mesh': {
4457
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4458
+ if (!meshId) return { success: false, error: 'meshId required' };
4459
+ try {
4460
+ const { updateMesh } = await import('../config/mesh-config.js');
4461
+ const patch: Record<string, unknown> = {};
4462
+ if (typeof args?.name === 'string') patch.name = args.name;
4463
+ if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
4464
+ if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
4465
+ if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
4466
+ if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
4467
+ if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
4468
+ const mesh = updateMesh(meshId, patch as any);
4469
+ if (!mesh) return { success: false, error: 'Mesh not found' };
4470
+ this.inlineMeshCache.set(meshId, mesh);
4471
+ this.invalidateAggregateMeshStatus(meshId);
4472
+ return { success: true, mesh };
4473
+ } catch (e: any) {
4474
+ return { success: false, error: e.message };
4475
+ }
4476
+ }
4477
+
4478
+ case 'get_mesh_host_pairing': {
4479
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4480
+ if (!meshId) return { success: false, error: 'meshId required' };
4481
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
4482
+ const mesh = meshRecord?.mesh;
4483
+ if (!mesh) return { success: false, error: 'Mesh not found' };
4484
+ const meshHost = resolveMeshHostStatus(mesh);
4485
+ const pairingStatus = meshHost.pairing?.status || 'not_configured';
4486
+ return {
4487
+ success: true,
4488
+ code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
4489
+ meshId,
4490
+ hostAddress: meshHost.hostAddress,
4491
+ meshHost,
4492
+ manualPairing: {
4493
+ status: pairingStatus,
4494
+ joinImplemented: true,
4495
+ protocol: 'standalone_command_direct_v1',
4496
+ 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.',
4497
+ },
4498
+ };
4499
+ }
4500
+
4501
+ case 'configure_mesh_host_pairing': {
4502
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4503
+ const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
4504
+ const token = typeof args?.token === 'string' ? args.token.trim() : '';
4505
+ if (!meshId) return { success: false, error: 'meshId required' };
4506
+ if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
4507
+ try {
4508
+ const { configureMeshHostPairing } = await import('../config/mesh-config.js');
4509
+ const configured = configureMeshHostPairing(meshId, { hostAddress, token });
4510
+ if (!configured) return { success: false, error: 'Mesh not found' };
4511
+ this.inlineMeshCache.set(meshId, configured.mesh);
4512
+ const meshHost = resolveMeshHostStatus(configured.mesh);
4513
+ return {
4514
+ success: true,
4515
+ code: 'mesh_host_pairing_pending',
4516
+ meshId,
4517
+ hostAddress: configured.hostAddress,
4518
+ meshHost,
4519
+ manualPairing: {
4520
+ status: meshHost.pairing?.status || 'pairing',
4521
+ joinImplemented: true,
4522
+ protocol: 'standalone_command_direct_v1',
4523
+ 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.',
4524
+ },
4525
+ };
2129
4526
  } catch (e: any) {
2130
- return { success: false, error: e.message };
4527
+ return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
2131
4528
  }
2132
4529
  }
2133
4530
 
2134
- case 'get_mesh': {
4531
+ case 'create_mesh_host_pairing_token': {
2135
4532
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2136
4533
  if (!meshId) return { success: false, error: 'meshId required' };
2137
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
2138
- if (meshRecord?.mesh) return { success: true, mesh: meshRecord.mesh };
2139
- return { success: false, error: 'Mesh not found' };
4534
+ try {
4535
+ const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
4536
+ const created = createMeshHostPairingToken(meshId, {
4537
+ token: typeof args?.token === 'string' ? args.token : undefined,
4538
+ expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
4539
+ });
4540
+ if (!created) return { success: false, error: 'Mesh not found' };
4541
+ this.inlineMeshCache.set(meshId, created.mesh);
4542
+ this.invalidateAggregateMeshStatus(meshId);
4543
+ return {
4544
+ success: true,
4545
+ code: 'mesh_host_pairing_token_created',
4546
+ meshId,
4547
+ token: created.token,
4548
+ tokenId: created.tokenId,
4549
+ expiresAt: created.expiresAt,
4550
+ meshHost: resolveMeshHostStatus(created.mesh),
4551
+ warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
4552
+ };
4553
+ } catch (e: any) {
4554
+ return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
4555
+ }
2140
4556
  }
2141
4557
 
2142
- case 'create_mesh': {
2143
- const name = typeof args?.name === 'string' ? args.name.trim() : '';
2144
- const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
2145
- const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
2146
- const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
2147
- if (!name) return { success: false, error: 'name required' };
4558
+ case 'apply_mesh_host_join': {
4559
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4560
+ const token = typeof args?.token === 'string' ? args.token.trim() : '';
4561
+ const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
4562
+ ? args.memberNode
4563
+ : null;
4564
+ if (!meshId) return { success: false, error: 'meshId required' };
4565
+ if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
2148
4566
  try {
2149
- const { createMesh } = await import('../config/mesh-config.js');
2150
- const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy });
2151
- return { success: true, mesh };
4567
+ const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
4568
+ const applied = applyMeshHostJoinRequest(meshId, {
4569
+ token,
4570
+ memberNode: memberNode as any,
4571
+ memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
4572
+ });
4573
+ if (!applied) return { success: false, error: 'Mesh not found' };
4574
+ if (!applied.accepted) {
4575
+ return {
4576
+ success: false,
4577
+ code: 'mesh_host_join_rejected',
4578
+ meshId,
4579
+ tokenId: applied.tokenId,
4580
+ meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
4581
+ error: applied.reason,
4582
+ };
4583
+ }
4584
+ this.inlineMeshCache.set(meshId, applied.mesh);
4585
+ this.invalidateAggregateMeshStatus(meshId);
4586
+ try {
4587
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
4588
+ appendLedgerEntry(meshId, {
4589
+ kind: 'node_joined',
4590
+ nodeId: applied.node.id,
4591
+ payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
4592
+ });
4593
+ } catch { /* ledger append is best-effort */ }
4594
+ return {
4595
+ success: true,
4596
+ code: 'mesh_host_join_accepted',
4597
+ meshId,
4598
+ node: applied.node,
4599
+ tokenId: applied.tokenId,
4600
+ meshHost: resolveMeshHostStatus(applied.mesh),
4601
+ };
2152
4602
  } catch (e: any) {
2153
- return { success: false, error: e.message };
4603
+ return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
2154
4604
  }
2155
4605
  }
2156
4606
 
2157
- case 'update_mesh': {
4607
+ case 'join_mesh_host_pairing': {
2158
4608
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4609
+ const token = typeof args?.token === 'string' ? args.token.trim() : '';
2159
4610
  if (!meshId) return { success: false, error: 'meshId required' };
4611
+ if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
4612
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
4613
+ const mesh = meshRecord?.mesh;
4614
+ if (!mesh) return { success: false, error: 'Mesh not found' };
4615
+ const meshHost = resolveMeshHostStatus(mesh);
4616
+ if (meshHost.role !== 'member') {
4617
+ 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.' };
4618
+ }
2160
4619
  try {
2161
- const { updateMesh } = await import('../config/mesh-config.js');
2162
- const patch: Record<string, unknown> = {};
2163
- if (typeof args?.name === 'string') patch.name = args.name;
2164
- if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
2165
- if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
2166
- if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
2167
- if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
2168
- const mesh = updateMesh(meshId, patch as any);
2169
- if (!mesh) return { success: false, error: 'Mesh not found' };
2170
- this.inlineMeshCache.set(meshId, mesh);
2171
- return { success: true, mesh };
4620
+ const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
4621
+ const tokenId = tokenIdForManualPairing(token);
4622
+ if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
4623
+ return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
4624
+ }
4625
+ const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
4626
+ if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
4627
+ const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
4628
+ const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
4629
+ ? args.hostDaemonId.trim()
4630
+ : meshHost.hostDaemonId;
4631
+ let hostResult: any;
4632
+ let transport: string;
4633
+ if (hostDaemonId && this.deps.dispatchMeshCommand) {
4634
+ transport = 'mesh_command_dispatch';
4635
+ hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
4636
+ meshId: hostMeshId,
4637
+ token,
4638
+ memberMeshId: meshId,
4639
+ memberNode,
4640
+ });
4641
+ } else if (meshHost.hostAddress) {
4642
+ transport = 'standalone_http_command';
4643
+ const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
4644
+ const response = await fetch(commandUrl, {
4645
+ method: 'POST',
4646
+ headers: { 'Content-Type': 'application/json' },
4647
+ body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
4648
+ });
4649
+ hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
4650
+ if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
4651
+ } else {
4652
+ return {
4653
+ success: false,
4654
+ code: 'mesh_host_join_transport_unavailable',
4655
+ meshId,
4656
+ meshHost,
4657
+ error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
4658
+ };
4659
+ }
4660
+ if (!hostResult?.success) {
4661
+ return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
4662
+ }
4663
+ const joined = meshRecord.inline
4664
+ ? null
4665
+ : markMeshHostPairingJoined(meshId, {
4666
+ tokenId: hostResult.tokenId || tokenId,
4667
+ hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
4668
+ hostNodeId: hostResult.meshHost?.hostNodeId,
4669
+ joinedAt: hostResult.meshHost?.pairing?.joinedAt,
4670
+ });
4671
+ if (joined) {
4672
+ this.inlineMeshCache.set(meshId, joined.mesh);
4673
+ this.invalidateAggregateMeshStatus(meshId);
4674
+ }
4675
+ return {
4676
+ success: true,
4677
+ code: 'mesh_host_join_applied',
4678
+ meshId,
4679
+ hostMeshId,
4680
+ transport,
4681
+ node: hostResult.node,
4682
+ tokenId: hostResult.tokenId || tokenId,
4683
+ meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
4684
+ hostResult,
4685
+ manualPairing: {
4686
+ status: 'paired',
4687
+ joinImplemented: true,
4688
+ protocol: 'standalone_command_direct_v1',
4689
+ description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
4690
+ },
4691
+ };
2172
4692
  } catch (e: any) {
2173
- return { success: false, error: e.message };
4693
+ return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
2174
4694
  }
2175
4695
  }
2176
4696
 
@@ -2267,6 +4787,8 @@ export class DaemonCommandRouter {
2267
4787
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2268
4788
  const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
2269
4789
  if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
4790
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
4791
+ if (ownerFailure) return ownerFailure;
2270
4792
  try {
2271
4793
  const { cancelTask } = await import('../mesh/mesh-work-queue.js');
2272
4794
  const reason = typeof args?.reason === 'string' ? args.reason : undefined;
@@ -2282,6 +4804,8 @@ export class DaemonCommandRouter {
2282
4804
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2283
4805
  const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
2284
4806
  if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
4807
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
4808
+ if (ownerFailure) return ownerFailure;
2285
4809
  try {
2286
4810
  const { requeueTask } = await import('../mesh/mesh-work-queue.js');
2287
4811
  const task = requeueTask(meshId, taskId, {
@@ -2303,6 +4827,8 @@ export class DaemonCommandRouter {
2303
4827
  const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
2304
4828
  if (!meshId) return { success: false, error: 'meshId required' };
2305
4829
  if (!workspace) return { success: false, error: 'workspace required' };
4830
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
4831
+ if (ownerFailure) return ownerFailure;
2306
4832
  try {
2307
4833
  const { addNode } = await import('../config/mesh-config.js');
2308
4834
  const providerPriority = Array.isArray(args?.providerPriority)
@@ -2313,7 +4839,18 @@ export class DaemonCommandRouter {
2313
4839
  ...(readOnly ? { readOnly: true } : {}),
2314
4840
  ...(providerPriority.length ? { providerPriority } : {}),
2315
4841
  };
2316
- const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}) });
4842
+ const role = normalizeMeshDaemonRole(args?.role);
4843
+ const daemonId = typeof args?.daemonId === 'string' && args.daemonId.trim() ? args.daemonId.trim() : undefined;
4844
+ const machineId = typeof args?.machineId === 'string' && args.machineId.trim() ? args.machineId.trim() : undefined;
4845
+ const repoRoot = typeof args?.repoRoot === 'string' && args.repoRoot.trim() ? args.repoRoot.trim() : undefined;
4846
+ const node = addNode(meshId, {
4847
+ workspace,
4848
+ ...(repoRoot ? { repoRoot } : {}),
4849
+ ...(daemonId ? { daemonId } : {}),
4850
+ ...(machineId ? { machineId } : {}),
4851
+ ...(policy ? { policy } : {}),
4852
+ ...(role ? { role } : {}),
4853
+ });
2317
4854
  if (!node) return { success: false, error: 'Mesh not found' };
2318
4855
  return { success: true, node };
2319
4856
  } catch (e: any) {
@@ -2325,6 +4862,8 @@ export class DaemonCommandRouter {
2325
4862
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2326
4863
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
2327
4864
  if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
4865
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
4866
+ if (ownerFailure) return ownerFailure;
2328
4867
  try {
2329
4868
  const { updateNode } = await import('../config/mesh-config.js');
2330
4869
  const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
@@ -2341,7 +4880,14 @@ export class DaemonCommandRouter {
2341
4880
  delete (policy as any).providerPriority;
2342
4881
  }
2343
4882
  }
2344
- const node = updateNode(meshId, nodeId, { policy: policy as any });
4883
+ const patch: Record<string, unknown> = { policy: policy as any };
4884
+ if (typeof args?.systemPrompt === 'string') {
4885
+ const trimmed = (args.systemPrompt as string).trim();
4886
+ patch.systemPrompt = trimmed || undefined;
4887
+ } else if (args?.systemPrompt === null) {
4888
+ patch.systemPrompt = undefined;
4889
+ }
4890
+ const node = updateNode(meshId, nodeId, patch as any);
2345
4891
  if (!node) return { success: false, error: 'Mesh node not found' };
2346
4892
  return { success: true, node };
2347
4893
  } catch (e: any) {
@@ -2353,6 +4899,8 @@ export class DaemonCommandRouter {
2353
4899
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2354
4900
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
2355
4901
  if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
4902
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
4903
+ if (ownerFailure) return ownerFailure;
2356
4904
  try {
2357
4905
  const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
2358
4906
  const mesh = meshRecord?.mesh;
@@ -2378,131 +4926,97 @@ export class DaemonCommandRouter {
2378
4926
  }
2379
4927
  }
2380
4928
 
2381
- case 'refine_mesh_node': {
4929
+ case 'get_mesh_refine_config_schema': {
4930
+ return {
4931
+ success: true,
4932
+ schema: MESH_REFINE_CONFIG_SCHEMA,
4933
+ locations: MESH_REFINE_CONFIG_LOCATIONS,
4934
+ worktreeBootstrap: {
4935
+ schema: MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
4936
+ locations: MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
4937
+ sourceOfTruth: 'repo worktree bootstrap config',
4938
+ runBehavior: 'When present and enabled, clone_mesh_node runs commands after submodule initialization and records status on the worktree node.',
4939
+ },
4940
+ sourceOfTruth: 'repo mesh/refine config',
4941
+ heuristicRole: 'suggestions_only_not_execution_path',
4942
+ };
4943
+ }
4944
+
4945
+ case 'validate_mesh_refine_config': {
4946
+ const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
4947
+ const mesh = args?.inlineMesh || {};
4948
+ const loaded = args?.config !== undefined
4949
+ ? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
4950
+ : loadMeshRefineConfig(mesh, workspace);
4951
+ const validation = loaded.config
4952
+ ? validateMeshRefineConfig(loaded.config, loaded.source)
4953
+ : { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
4954
+ return { success: validation.valid, ...loaded, ...validation };
4955
+ }
4956
+
4957
+ case 'suggest_mesh_refine_config': {
4958
+ const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
4959
+ const mesh = args?.inlineMesh || {};
4960
+ return {
4961
+ success: true,
4962
+ ...suggestMeshRefineConfig(mesh, workspace),
4963
+ note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
4964
+ };
4965
+ }
4966
+
4967
+ case 'plan_mesh_refine_node': {
2382
4968
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2383
4969
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
2384
4970
  if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
2385
- try {
4971
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
4972
+ const mesh = meshRecord?.mesh;
4973
+ const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
4974
+ if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
4975
+ return {
4976
+ success: true,
4977
+ dryRun: true,
4978
+ nodeId,
4979
+ workspace: node.workspace,
4980
+ validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
4981
+ mergeWillRun: false,
4982
+ cleanupWillRun: false,
4983
+ };
4984
+ }
4985
+
4986
+ case 'fast_forward_mesh_node': {
4987
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4988
+ const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
4989
+ let workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
4990
+ let submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
4991
+ ? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
4992
+ : undefined;
4993
+ if (!workspace && meshId && nodeId) {
2386
4994
  const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
2387
4995
  const mesh = meshRecord?.mesh;
2388
4996
  const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
2389
- if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
2390
-
2391
- if (!node.isLocalWorktree || !node.workspace) {
2392
- return { success: false, error: `Refinery requires a local worktree node` };
2393
- }
2394
-
2395
- const sourceNode = node.clonedFromNodeId
2396
- ? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
2397
- : mesh?.nodes.find((n: any) => !n.isLocalWorktree);
2398
- const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
2399
- if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
2400
-
2401
- const { execFile } = await import('node:child_process');
2402
- const { promisify } = await import('node:util');
2403
- const execFileAsync = promisify(execFile);
2404
-
2405
- const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
2406
- const branch = branchStdout.trim();
2407
- if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
2408
-
2409
- const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
2410
- const baseBranch = baseBranchStdout.trim();
2411
-
2412
- const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
2413
- if (validationSummary.status === 'failed') {
2414
- return {
2415
- success: false,
2416
- code: 'validation_failed',
2417
- convergenceStatus: 'blocked_review',
2418
- error: 'Refinery validation gate failed; merge/refine was not attempted.',
2419
- branch,
2420
- into: baseBranch,
2421
- validationSummary,
2422
- finalBranchConvergenceState: {
2423
- branch,
2424
- baseBranch,
2425
- merged: false,
2426
- removed: false,
2427
- validation: 'failed',
2428
- status: 'blocked_review',
2429
- },
2430
- };
2431
- }
2432
- if (validationSummary.status === 'skipped') {
2433
- return {
2434
- success: false,
2435
- code: 'validation_unavailable',
2436
- convergenceStatus: 'blocked_review',
2437
- error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
2438
- branch,
2439
- into: baseBranch,
2440
- validationSummary,
2441
- finalBranchConvergenceState: {
2442
- branch,
2443
- baseBranch,
2444
- merged: false,
2445
- removed: false,
2446
- validation: 'unavailable',
2447
- status: 'blocked_review',
2448
- },
2449
- };
2450
- }
2451
-
2452
- try {
2453
- await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
2454
- } catch (e: any) {
2455
- return {
2456
- success: false,
2457
- error: `Merge failed (conflicts?): ${e.message}`,
2458
- validationSummary,
2459
- finalBranchConvergenceState: {
2460
- branch,
2461
- baseBranch,
2462
- merged: false,
2463
- removed: false,
2464
- validation: 'passed',
2465
- status: 'not_mergeable',
2466
- },
2467
- };
4997
+ workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
4998
+ if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
4999
+ submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
2468
5000
  }
2469
-
2470
- const removeResult = await this.execute('remove_mesh_node', {
2471
- meshId,
2472
- nodeId,
2473
- sessionCleanupMode: 'kill',
2474
- inlineMesh: args?.inlineMesh,
2475
- });
2476
-
2477
- try {
2478
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
2479
- appendLedgerEntry(meshId, {
2480
- kind: 'node_removed',
2481
- nodeId,
2482
- payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
2483
- });
2484
- } catch {}
2485
-
2486
- return {
2487
- success: true,
2488
- merged: true,
2489
- branch,
2490
- into: baseBranch,
2491
- removeResult,
2492
- validationSummary,
2493
- finalBranchConvergenceState: {
2494
- branch: baseBranch,
2495
- mergedBranch: branch,
2496
- baseBranch,
2497
- merged: true,
2498
- removed: removeResult?.success !== false,
2499
- validation: 'passed',
2500
- status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
2501
- },
2502
- };
2503
- } catch (e: any) {
2504
- return { success: false, error: e.message };
2505
5001
  }
5002
+ const result = await (fastForwardMeshNode({
5003
+ meshId: meshId || undefined,
5004
+ nodeId: nodeId || undefined,
5005
+ workspace,
5006
+ branch: typeof args?.branch === 'string' ? args.branch : undefined,
5007
+ execute: args?.execute === true,
5008
+ dryRun: args?.dryRun === true,
5009
+ updateSubmodules: args?.updateSubmodules === true,
5010
+ submoduleIgnorePaths,
5011
+ }) as Promise<unknown>);
5012
+ return result as CommandRouterResult;
5013
+ }
5014
+
5015
+ case 'refine_mesh_node': {
5016
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
5017
+ const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
5018
+ if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
5019
+ return this.startMeshRefineJob(meshId, nodeId, args);
2506
5020
  }
2507
5021
 
2508
5022
  case 'remove_mesh_node': {
@@ -2546,6 +5060,7 @@ export class DaemonCommandRouter {
2546
5060
  } else {
2547
5061
  const { removeNode } = await import('../config/mesh-config.js');
2548
5062
  removed = removeNode(meshId, nodeId);
5063
+ if (removed) this.invalidateAggregateMeshStatus(meshId);
2549
5064
  }
2550
5065
 
2551
5066
  // Record in task ledger
@@ -2583,6 +5098,8 @@ export class DaemonCommandRouter {
2583
5098
  if (!meshId) return { success: false, error: 'meshId required' };
2584
5099
  if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
2585
5100
  if (!branch) return { success: false, error: 'branch required' };
5101
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
5102
+ if (ownerFailure) return ownerFailure;
2586
5103
 
2587
5104
  try {
2588
5105
  const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
@@ -2631,39 +5148,124 @@ export class DaemonCommandRouter {
2631
5148
  policy: { ...(sourceNode.policy || {}) },
2632
5149
  });
2633
5150
  if (!node) return { success: false, error: 'Failed to register worktree node' };
5151
+ this.invalidateAggregateMeshStatus(meshId);
2634
5152
  }
2635
5153
 
2636
- // Initialize submodules if policy allows (default: true)
2637
- const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
2638
- if (initSubmodules) {
5154
+ const persistWorktreeSetupState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
5155
+ node.worktreeBootstrap = bootstrapState;
5156
+ if (meshRecord.inline) {
5157
+ this.updateInlineMeshNode(meshId, mesh, node);
5158
+ return;
5159
+ }
2639
5160
  try {
2640
- const { runGit } = await import('../git/git-executor.js');
2641
- await runGit(
2642
- { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
2643
- ['submodule', 'update', '--init', '--recursive'],
2644
- { timeoutMs: 120000 },
2645
- );
2646
- } catch (subErr: any) {
2647
- // Submodule init is best-effort; don't fail the clone
2648
- console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
5161
+ const { updateNode } = await import('../config/mesh-config.js');
5162
+ updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
5163
+ this.invalidateAggregateMeshStatus(meshId);
5164
+ } catch { /* bootstrap status persistence is best-effort */ }
5165
+ };
5166
+
5167
+ const appendCloneLedger = async (initSubmodules: boolean, bootstrapState: WorktreeBootstrapState): Promise<void> => {
5168
+ try {
5169
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
5170
+ appendLedgerEntry(meshId, {
5171
+ kind: 'node_cloned',
5172
+ nodeId: node.id,
5173
+ payload: {
5174
+ sourceNodeId,
5175
+ branch: result.branch,
5176
+ worktreePath: result.worktreePath,
5177
+ submodulesInitialized: initSubmodules,
5178
+ worktreeBootstrap: {
5179
+ status: bootstrapState.status,
5180
+ required: bootstrapState.required,
5181
+ configSource: bootstrapState.configSource,
5182
+ configSourceType: bootstrapState.configSourceType,
5183
+ lastCommand: bootstrapState.lastCommand,
5184
+ exitCode: bootstrapState.exitCode,
5185
+ },
5186
+ },
5187
+ });
5188
+ } catch { /* ledger append is best-effort */ }
5189
+ };
5190
+
5191
+ const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
5192
+ const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, result.worktreePath);
5193
+ const runningBootstrapState: WorktreeBootstrapState = {
5194
+ status: 'running',
5195
+ required: loadedBootstrap.config?.required !== false,
5196
+ configSource: loadedBootstrap.path || loadedBootstrap.source,
5197
+ configSourceType: loadedBootstrap.sourceType,
5198
+ startedAt: new Date().toISOString(),
5199
+ };
5200
+ await persistWorktreeSetupState(runningBootstrapState);
5201
+
5202
+ const finishWorktreeSetup = async (): Promise<{ submodulesInitialized: boolean; bootstrapState: WorktreeBootstrapState }> => {
5203
+ let submodulesInitialized = false;
5204
+ if (initSubmodules) {
5205
+ try {
5206
+ const { runGit } = await import('../git/git-executor.js');
5207
+ await runGit(
5208
+ { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
5209
+ ['submodule', 'update', '--init', '--recursive'],
5210
+ { timeoutMs: 120000 },
5211
+ );
5212
+ submodulesInitialized = true;
5213
+ } catch (subErr: any) {
5214
+ // Submodule init is best-effort; don't fail the clone
5215
+ console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
5216
+ }
2649
5217
  }
2650
- }
5218
+ const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
5219
+ await persistWorktreeSetupState(bootstrapState);
5220
+ await appendCloneLedger(submodulesInitialized, bootstrapState);
5221
+ return { submodulesInitialized, bootstrapState };
5222
+ };
2651
5223
 
2652
- // Record in task ledger
2653
- try {
2654
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
2655
- appendLedgerEntry(meshId, {
2656
- kind: 'node_cloned',
2657
- nodeId: node.id,
2658
- payload: { sourceNodeId, branch: result.branch, worktreePath: result.worktreePath, submodulesInitialized: initSubmodules },
5224
+ const requestedSetupWaitMs = Number(args?.setupWaitMs ?? args?.bootstrapWaitMs ?? 8000);
5225
+ const setupWaitMs = Number.isFinite(requestedSetupWaitMs)
5226
+ ? Math.min(Math.max(requestedSetupWaitMs, 0), 14000)
5227
+ : 8000;
5228
+ const setupPromise = finishWorktreeSetup();
5229
+ const setupResult = await Promise.race([
5230
+ setupPromise.then((value) => ({ completed: true as const, value })),
5231
+ new Promise<{ completed: false }>((resolve) => setTimeout(() => resolve({ completed: false }), setupWaitMs)),
5232
+ ]);
5233
+
5234
+ if (!setupResult.completed) {
5235
+ setupPromise.catch((error: any) => {
5236
+ const failedState: WorktreeBootstrapState = {
5237
+ ...runningBootstrapState,
5238
+ status: 'failed',
5239
+ completedAt: new Date().toISOString(),
5240
+ error: error?.message || String(error),
5241
+ };
5242
+ void persistWorktreeSetupState(failedState);
5243
+ void appendCloneLedger(false, failedState);
2659
5244
  });
2660
- } catch { /* ledger append is best-effort */ }
5245
+ return {
5246
+ success: true,
5247
+ async: true,
5248
+ status: 'accepted',
5249
+ node,
5250
+ worktreePath: result.worktreePath,
5251
+ branch: result.branch,
5252
+ worktreeBootstrap: runningBootstrapState,
5253
+ worktreeSetup: {
5254
+ status: 'running',
5255
+ setupWaitMs,
5256
+ message: 'Worktree node is registered; submodule/bootstrap setup is continuing in the background.',
5257
+ },
5258
+ };
5259
+ }
2661
5260
 
5261
+ const { submodulesInitialized, bootstrapState } = setupResult.value;
2662
5262
  return {
2663
5263
  success: true,
2664
5264
  node,
2665
5265
  worktreePath: result.worktreePath,
2666
5266
  branch: result.branch,
5267
+ submodulesInitialized,
5268
+ worktreeBootstrap: bootstrapState,
2667
5269
  };
2668
5270
  } catch (e: any) {
2669
5271
  return { success: false, error: e.message };
@@ -2672,6 +5274,8 @@ export class DaemonCommandRouter {
2672
5274
  case 'trigger_mesh_queue': {
2673
5275
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2674
5276
  if (!meshId) return { success: false, error: 'meshId required' };
5277
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
5278
+ if (ownerFailure) return ownerFailure;
2675
5279
  try {
2676
5280
  const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
2677
5281
  if (meshId) {
@@ -2687,6 +5291,19 @@ export class DaemonCommandRouter {
2687
5291
  case 'launch_mesh_coordinator': {
2688
5292
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2689
5293
  let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
5294
+ // Optional per-launch system-prompt addition. Dashboard or API
5295
+ // callers (e.g. when spawning a mesh-node-specific coordinator)
5296
+ // can pass extra context that gets appended to the rendered
5297
+ // default prompt under the "## Additional Context" section.
5298
+ // Going through buildCoordinatorSystemPrompt's userInstruction
5299
+ // means user-level override files (~/.adhdev/coordinator-prompts)
5300
+ // and this per-launch addition compose cleanly: an override
5301
+ // wins outright, but if there's no override, the default
5302
+ // prompt + the optional append.md file + this extra context
5303
+ // all stack in declared order.
5304
+ const extraSystemPrompt = typeof args?.extraSystemPrompt === 'string'
5305
+ ? args.extraSystemPrompt.trim()
5306
+ : '';
2690
5307
  if (!meshId) return { success: false, error: 'meshId required' };
2691
5308
 
2692
5309
  try {
@@ -2703,6 +5320,15 @@ export class DaemonCommandRouter {
2703
5320
  mesh = getMesh(meshId);
2704
5321
  }
2705
5322
  if (!mesh) return { success: false, error: 'Mesh not found' };
5323
+ const meshHost = resolveMeshHostStatus(mesh);
5324
+ if (!meshHost.canOwnCoordinator) {
5325
+ return {
5326
+ success: false,
5327
+ ...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
5328
+ meshId,
5329
+ cliType,
5330
+ };
5331
+ }
2706
5332
  if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
2707
5333
 
2708
5334
  const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
@@ -2788,7 +5414,7 @@ export class DaemonCommandRouter {
2788
5414
  // Build coordinator prompt first — fail closed on errors.
2789
5415
  let cliCmdSystemPrompt = '';
2790
5416
  try {
2791
- cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
5417
+ cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined });
2792
5418
  } catch (error: any) {
2793
5419
  const message = error?.message || String(error);
2794
5420
  LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
@@ -2800,53 +5426,45 @@ export class DaemonCommandRouter {
2800
5426
  };
2801
5427
  }
2802
5428
 
2803
- // Run the provider's MCP registration command.
5429
+ // Run the provider's MCP registration command under a
5430
+ // PTY. Some providers (agy, future bubbletea CLIs)
5431
+ // refuse to run without /dev/tty, so pipe-only
5432
+ // execFileSync silently no-ops the registration and
5433
+ // the coordinator ends up without any mcp tools. With
5434
+ // a real PTY the registration goes through and the
5435
+ // exit code tells us whether it actually persisted.
5436
+ let mcpRegistrationOk = false;
2804
5437
  try {
2805
- const { execFileSync: execCmdSync } = await import('node:child_process');
5438
+ const { execUnderPty } = await import('./mesh-coordinator.js');
2806
5439
  const cmdParts = coordinatorSetup.command.trim().split(/\s+/);
2807
5440
  const [regCmd, ...regArgs] = cmdParts;
2808
- LOG.info('MeshCoordinator', `Running MCP registration: ${coordinatorSetup.command}`);
2809
- execCmdSync(regCmd, regArgs, { stdio: 'pipe', timeout: 15_000 });
5441
+ LOG.info('MeshCoordinator', `Running MCP registration (pty): ${coordinatorSetup.command}`);
5442
+ const ptyResult = await execUnderPty(regCmd, regArgs, { cwd: workspace, timeoutMs: 20_000 });
5443
+ if (ptyResult.timedOut) {
5444
+ LOG.warn('MeshCoordinator', `MCP registration timed out — last output:\n${ptyResult.output.slice(-2000)}`);
5445
+ } else if (ptyResult.exitCode === 0) {
5446
+ mcpRegistrationOk = true;
5447
+ LOG.info('MeshCoordinator', `MCP registration succeeded (exit=0)`);
5448
+ } else {
5449
+ // Non-fatal — many providers return non-zero on duplicate registration.
5450
+ LOG.warn('MeshCoordinator', `MCP registration exit=${ptyResult.exitCode} signal=${ptyResult.signal} — output:\n${ptyResult.output.slice(-2000)}`);
5451
+ }
2810
5452
  } catch (error: any) {
2811
- // Non-fatal server may already be registered (providers return exit 1 on duplicate).
2812
- LOG.warn('MeshCoordinator', `MCP registration command failed (may be pre-registered): ${error?.message || error}`);
5453
+ LOG.warn('MeshCoordinator', `MCP registration command failed: ${error?.message || error}`);
2813
5454
  }
2814
5455
 
2815
- // Inject system prompt using provider-native methods.
2816
- // Codex: -c 'instructions="..."' CLI config override
2817
- // Gemini: write GEMINI.md to workspace (auto-loaded as context)
5456
+ // Inject system prompt declaratively from provider.v1.json.
2818
5457
  const cliCmdArgs: string[] = [];
2819
5458
  const cliCmdEnv: Record<string, string> = {};
5459
+ let cliCmdContextFilePath: string | undefined;
2820
5460
  if (cliCmdSystemPrompt) {
2821
- if (cliType === 'codex-cli') {
2822
- // Codex reads `developer_instructions` from config.toml as system instructions.
2823
- // The -c flag overrides a config key for this session only.
2824
- cliCmdArgs.push('-c', `developer_instructions=${JSON.stringify(cliCmdSystemPrompt)}`);
2825
- } else if (cliType === 'gemini-cli') {
2826
- // Gemini CLI auto-loads GEMINI.md from CWD as project context.
2827
- // Write a temporary GEMINI.md to the workspace before launch.
2828
- try {
2829
- const { writeFileSync: wfs, existsSync: efs, readFileSync: rfs } = await import('node:fs');
2830
- const geminiMdPath = `${workspace}/GEMINI.md`;
2831
- const marker = '<!-- adhdev-mesh-coordinator-prompt -->';
2832
- const markerEnd = '<!-- /adhdev-mesh-coordinator-prompt -->';
2833
- const block = `${marker}\n${cliCmdSystemPrompt}\n${markerEnd}`;
2834
- if (efs(geminiMdPath)) {
2835
- const existing = rfs(geminiMdPath, 'utf-8');
2836
- // Replace existing block or append
2837
- const replaced = existing.replace(
2838
- new RegExp(`${marker}[\\s\\S]*?${markerEnd}`, 'g'),
2839
- block,
2840
- );
2841
- wfs(geminiMdPath, replaced.includes(marker) ? replaced : `${existing}\n\n${block}`);
2842
- } else {
2843
- wfs(geminiMdPath, block);
2844
- }
2845
- LOG.info('MeshCoordinator', `Wrote coordinator prompt to ${workspace}/GEMINI.md`);
2846
- } catch (e: any) {
2847
- LOG.warn('MeshCoordinator', `Could not write GEMINI.md: ${e?.message || e}`);
2848
- }
2849
- }
5461
+ const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
5462
+ const effect = applyMeshCoordinatorSystemPromptInjection(
5463
+ cliCmdSystemPrompt,
5464
+ providerMeta?.meshCoordinator?.systemPromptInjection,
5465
+ { cliArgs: cliCmdArgs, launchEnv: cliCmdEnv, workspace, cliType },
5466
+ );
5467
+ cliCmdContextFilePath = effect.contextFilePath;
2850
5468
  }
2851
5469
 
2852
5470
  const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
@@ -2857,16 +5475,52 @@ export class DaemonCommandRouter {
2857
5475
  settings: { meshCoordinatorFor: meshId },
2858
5476
  });
2859
5477
 
5478
+ // R48 inject-then-remove. Spawn was just kicked off above; agy and
5479
+ // gemini-cli read AGENTS.md / GEMINI.md exactly once at startup and
5480
+ // cache it for the rest of the session, so we can safely strip
5481
+ // the wrapper from disk shortly after launch. That keeps any
5482
+ // worker session launched into the same workspace later from
5483
+ // picking up our wrapper block.
5484
+ if (cliCmdLaunch?.success && cliCmdContextFilePath) {
5485
+ const stripPath = cliCmdContextFilePath;
5486
+ setTimeout(() => {
5487
+ void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
5488
+ stripCoordinatorWrapperFile(stripPath);
5489
+ LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (cli_command)`);
5490
+ }).catch(() => { /* best-effort */ });
5491
+ }, 5000);
5492
+ }
5493
+
2860
5494
  if (!cliCmdLaunch?.success) {
2861
5495
  return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
2862
5496
  }
2863
5497
 
2864
5498
  LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
5499
+ const cliCmdSessionId = cliCmdLaunch.sessionId || cliCmdLaunch.id;
5500
+ if (cliCmdSessionId) {
5501
+ const cliCmdInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
5502
+ registerMeshCoordinator({
5503
+ meshId,
5504
+ sessionId: cliCmdSessionId,
5505
+ workspace,
5506
+ startedAt: Date.now(),
5507
+ cliType,
5508
+ systemPrompt: cliCmdSystemPrompt || undefined,
5509
+ extraSystemPrompt: extraSystemPrompt || undefined,
5510
+ injection: cliCmdInjectionDecl ? {
5511
+ mode: cliCmdInjectionDecl.mode,
5512
+ target: 'flag' in cliCmdInjectionDecl ? cliCmdInjectionDecl.flag
5513
+ : 'name' in cliCmdInjectionDecl ? cliCmdInjectionDecl.name
5514
+ : 'path' in cliCmdInjectionDecl ? cliCmdInjectionDecl.path
5515
+ : undefined,
5516
+ } : undefined,
5517
+ });
5518
+ }
2865
5519
  try {
2866
5520
  const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
2867
5521
  appendLedgerEntry(meshId, {
2868
5522
  kind: 'coordinator_started',
2869
- sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
5523
+ sessionId: cliCmdSessionId,
2870
5524
  providerType: cliType,
2871
5525
  payload: { workspace },
2872
5526
  });
@@ -2877,8 +5531,8 @@ export class DaemonCommandRouter {
2877
5531
  meshId,
2878
5532
  cliType,
2879
5533
  workspace,
2880
- sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
2881
- mcpRegistered: true,
5534
+ sessionId: cliCmdSessionId,
5535
+ mcpRegistered: mcpRegistrationOk,
2882
5536
  };
2883
5537
  }
2884
5538
 
@@ -2899,7 +5553,7 @@ export class DaemonCommandRouter {
2899
5553
  // broken mesh state is visible instead of silently launching with weaker rules.
2900
5554
  let systemPrompt = '';
2901
5555
  try {
2902
- systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
5556
+ systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined });
2903
5557
  } catch (error: any) {
2904
5558
  const message = error?.message || String(error);
2905
5559
  LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
@@ -3014,12 +5668,15 @@ export class DaemonCommandRouter {
3014
5668
  launchEnv.HERMES_HOME = dirname(mcpConfigPath);
3015
5669
  launchEnv.HERMES_IGNORE_USER_CONFIG = '';
3016
5670
  }
5671
+ let autoImportContextFilePath: string | undefined;
3017
5672
  if (systemPrompt) {
3018
- if (configFormat === 'hermes_config_yaml') {
3019
- launchEnv.HERMES_EPHEMERAL_SYSTEM_PROMPT = systemPrompt;
3020
- } else {
3021
- cliArgs.push('--append-system-prompt', systemPrompt);
3022
- }
5673
+ const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
5674
+ const effect = applyMeshCoordinatorSystemPromptInjection(
5675
+ systemPrompt,
5676
+ providerMeta?.meshCoordinator?.systemPromptInjection,
5677
+ { cliArgs, launchEnv, workspace, cliType },
5678
+ );
5679
+ autoImportContextFilePath = effect.contextFilePath;
3023
5680
  }
3024
5681
  if (cliType === 'claude-cli') {
3025
5682
  cliArgs.push('--mcp-config', coordinatorSetup.configPath);
@@ -3038,18 +5695,53 @@ export class DaemonCommandRouter {
3038
5695
  }
3039
5696
  });
3040
5697
 
5698
+ // R48 inject-then-remove. See the cli_command branch for context;
5699
+ // same idea: strip the wrapper from disk ~5s after launch so the
5700
+ // user's AGENTS.md / GEMINI.md is untouched the moment any
5701
+ // worker session opens up in the same workspace.
5702
+ if (launchResult?.success && autoImportContextFilePath) {
5703
+ const stripPath = autoImportContextFilePath;
5704
+ setTimeout(() => {
5705
+ void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
5706
+ stripCoordinatorWrapperFile(stripPath);
5707
+ LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (auto_import)`);
5708
+ }).catch(() => { /* best-effort */ });
5709
+ }, 5000);
5710
+ }
5711
+
3041
5712
  if (!launchResult?.success) {
3042
5713
  return { success: false, error: launchResult?.error || 'Failed to launch CLI session' };
3043
5714
  }
3044
5715
 
3045
5716
  LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
5717
+ const launchSessionId = launchResult.sessionId || launchResult.id;
5718
+ if (launchSessionId) {
5719
+ const autoImportInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
5720
+ registerMeshCoordinator({
5721
+ meshId,
5722
+ sessionId: launchSessionId,
5723
+ workspace,
5724
+ startedAt: Date.now(),
5725
+ cliType,
5726
+ systemPrompt: systemPrompt || undefined,
5727
+ extraSystemPrompt: extraSystemPrompt || undefined,
5728
+ mcpConfigPath,
5729
+ injection: autoImportInjectionDecl ? {
5730
+ mode: autoImportInjectionDecl.mode,
5731
+ target: 'flag' in autoImportInjectionDecl ? autoImportInjectionDecl.flag
5732
+ : 'name' in autoImportInjectionDecl ? autoImportInjectionDecl.name
5733
+ : 'path' in autoImportInjectionDecl ? autoImportInjectionDecl.path
5734
+ : undefined,
5735
+ } : undefined,
5736
+ });
5737
+ }
3046
5738
 
3047
5739
  // Record coordinator launch in task ledger
3048
5740
  try {
3049
5741
  const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
3050
5742
  appendLedgerEntry(meshId, {
3051
5743
  kind: 'coordinator_started',
3052
- sessionId: launchResult.sessionId || launchResult.id,
5744
+ sessionId: launchSessionId,
3053
5745
  providerType: cliType,
3054
5746
  payload: { workspace },
3055
5747
  });
@@ -3060,7 +5752,7 @@ export class DaemonCommandRouter {
3060
5752
  meshId,
3061
5753
  cliType,
3062
5754
  workspace,
3063
- sessionId: launchResult.sessionId || launchResult.id,
5755
+ sessionId: launchSessionId,
3064
5756
  mcpConfigWritten: true,
3065
5757
  };
3066
5758
  } catch (e: any) {
@@ -3076,6 +5768,36 @@ export class DaemonCommandRouter {
3076
5768
  const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
3077
5769
  const mesh = meshRecord?.mesh;
3078
5770
  if (!mesh) return { success: false, error: 'Mesh not found' };
5771
+ const meshHost = resolveMeshHostStatus(mesh);
5772
+
5773
+ const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
5774
+ // See (B3) below: scope the peek to this daemon when the
5775
+ // caller doesn't tell us, otherwise scoped events look
5776
+ // missing and we falsely return a stale cache.
5777
+ const peekScope = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
5778
+ ? args.coordinatorDaemonId.trim()
5779
+ : (this.deps.statusInstanceId || undefined);
5780
+ const pendingCoordinatorEventCount = getPendingMeshCoordinatorEvents(meshId, peekScope).length;
5781
+ const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
5782
+ if (!refreshRequested && pendingCoordinatorEventCount === 0) {
5783
+ const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
5784
+ if (cachedStatus) {
5785
+ logRepoMeshStatusDebug('return_cached', {
5786
+ meshId,
5787
+ command: 'mesh_status',
5788
+ refreshRequested,
5789
+ summary: summarizeRepoMeshStatusDebug(cachedStatus),
5790
+ });
5791
+ return cachedStatus;
5792
+ }
5793
+ }
5794
+ const refreshReason = refreshRequested
5795
+ ? 'explicit_refresh'
5796
+ : pendingCoordinatorEventCount > 0
5797
+ ? 'pending_coordinator_events'
5798
+ : hadAggregateCache
5799
+ ? 'stale_pending_cache_refresh'
5800
+ : 'cold_cache_miss';
3079
5801
 
3080
5802
  const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
3081
5803
  const queue = getQueue(meshId);
@@ -3083,6 +5805,7 @@ export class DaemonCommandRouter {
3083
5805
 
3084
5806
  const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
3085
5807
  const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
5808
+ const asyncRefineLedgerEntries = readLedgerEntries(meshId, { tail: 100 });
3086
5809
  const ledgerSummary = getLedgerSummary(meshId);
3087
5810
  const sessionHostRecords = this.deps.sessionHostControl?.listSessions
3088
5811
  ? await this.deps.sessionHostControl.listSessions().catch(() => [])
@@ -3090,6 +5813,74 @@ export class DaemonCommandRouter {
3090
5813
  const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
3091
5814
 
3092
5815
  const localMachineId = loadConfig().machineId || '';
5816
+ const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
5817
+ const directTruth = requireDirectPeerTruth
5818
+ ? await hydrateInlineMeshDirectTruth({
5819
+ mesh,
5820
+ meshSource: meshRecord.source,
5821
+ dispatchMeshCommand: this.deps.dispatchMeshCommand,
5822
+ statusInstanceId: this.deps.statusInstanceId,
5823
+ localMachineId,
5824
+ })
5825
+ : {
5826
+ directEvidenceCount: 0,
5827
+ localConfirmedCount: 0,
5828
+ peerAttemptedCount: 0,
5829
+ peerConfirmedCount: 0,
5830
+ unavailableNodeIds: [] as string[],
5831
+ };
5832
+ // Default/cached loads may not attempt a remote peer probe yet; do not surface that as
5833
+ // a direct mesh truth failure until an explicit probe attempt actually fails.
5834
+ const passivePeerTruthNotAttempted = requireDirectPeerTruth
5835
+ && !refreshRequested
5836
+ && directTruth.directEvidenceCount > 0
5837
+ && directTruth.peerAttemptedCount === 0;
5838
+ const effectiveDirectTruth = passivePeerTruthNotAttempted
5839
+ ? { ...directTruth, unavailableNodeIds: [] as string[] }
5840
+ : directTruth;
5841
+ const unavailableDirectTruthNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
5842
+ const unavailableNodesAreOnlyRemovedWorktrees = unavailableDirectTruthNodeIds.size > 0
5843
+ && Array.isArray(mesh.nodes)
5844
+ && mesh.nodes
5845
+ .filter((node: any) => unavailableDirectTruthNodeIds.has(String(node.id || node.nodeId || '')))
5846
+ .every((node: any) => node?.isLocalWorktree === true);
5847
+ const directTruthSatisfied = !requireDirectPeerTruth
5848
+ || (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
5849
+ if (requireDirectPeerTruth && !directTruthSatisfied) {
5850
+ const failureResult = {
5851
+ success: false,
5852
+ code: 'mesh_direct_peer_truth_unavailable',
5853
+ error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
5854
+ sourceOfTruth: {
5855
+ membership: meshRecord.source === 'inline_cache'
5856
+ ? 'coordinator_inline_mesh_cache'
5857
+ : meshRecord.source === 'local_config'
5858
+ ? 'local_mesh_config'
5859
+ : 'inline_bootstrap_snapshot',
5860
+ coordinatorOwnsLiveTruth: false,
5861
+ currentStatus: 'direct_peer_truth_unavailable',
5862
+ directPeerTruth: {
5863
+ required: true,
5864
+ satisfied: false,
5865
+ directEvidenceCount: directTruth.directEvidenceCount,
5866
+ localConfirmedCount: directTruth.localConfirmedCount,
5867
+ peerAttemptedCount: directTruth.peerAttemptedCount,
5868
+ peerConfirmedCount: directTruth.peerConfirmedCount,
5869
+ unavailableNodeIds: directTruth.unavailableNodeIds,
5870
+ },
5871
+ },
5872
+ };
5873
+ logRepoMeshStatusDebug('direct_truth_unavailable', {
5874
+ meshId,
5875
+ command: 'mesh_status',
5876
+ refreshRequested,
5877
+ meshSource: meshRecord.source,
5878
+ directTruth,
5879
+ });
5880
+ return failureResult;
5881
+ }
5882
+ const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
5883
+ const coordinatorHostname = osHostname();
3093
5884
  const selectedCoordinatorNodeId = readStringValue(
3094
5885
  mesh.coordinator?.preferredNodeId,
3095
5886
  (mesh.nodes?.[0] as any)?.id,
@@ -3103,21 +5894,42 @@ export class DaemonCommandRouter {
3103
5894
  for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
3104
5895
  const nodeId = String(node.id || node.nodeId || '');
3105
5896
  const daemonId = readStringValue(node.daemonId);
5897
+ const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
5898
+ const nodeHostname = readMeshNodeHostname(node as Record<string, unknown>);
3106
5899
  const providerPriority = readProviderPriorityFromPolicy(node.policy);
5900
+ const configuredCoordinatorNode = Boolean(
5901
+ nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
5902
+ );
5903
+ const sparseConfiguredCoordinatorNode = configuredCoordinatorNode
5904
+ && !daemonId
5905
+ && !nodeMachineId
5906
+ && !nodeHostname;
3107
5907
  const isSelfNode = Boolean(
3108
5908
  nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
3109
5909
  ) || Boolean(
3110
5910
  daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
3111
- ) || Boolean(meshRecord?.inline && nodeIndex === 0);
5911
+ ) || Boolean(meshRecord?.inline && nodeIndex === 0)
5912
+ || sparseConfiguredCoordinatorNode;
5913
+ const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
5914
+ localMachineId,
5915
+ localDaemonId: this.deps.statusInstanceId,
5916
+ coordinatorHostname,
5917
+ isSelfNode,
5918
+ });
3112
5919
  const status: Record<string, unknown> = {
3113
5920
  nodeId,
3114
- machineLabel: node.machineLabel || node.id || node.nodeId,
5921
+ machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
5922
+ labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
5923
+ ? 'explicit_metadata'
5924
+ : 'workspace_host_provider_context',
3115
5925
  workspace: node.workspace,
3116
5926
  repoRoot: node.repoRoot,
3117
5927
  isLocalWorktree: node.isLocalWorktree,
3118
5928
  worktreeBranch: node.worktreeBranch,
5929
+ role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
3119
5930
  daemonId,
3120
- machineId: node.machineId,
5931
+ machineId: nodeMachineId || node.machineId,
5932
+ machine: machineIdentity,
3121
5933
  machineStatus: node.machineStatus,
3122
5934
  health: 'unknown',
3123
5935
  providers: node.providers || [],
@@ -3184,43 +5996,224 @@ export class DaemonCommandRouter {
3184
5996
  }
3185
5997
  }
3186
5998
  if (workspace) {
3187
- if (!fs.existsSync(workspace) && applyCachedInlineMeshNodeStatus(status, node)) {
3188
- status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
3189
- nodeStatuses.push(status);
3190
- continue;
3191
- }
3192
- try {
3193
- const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
3194
- status.git = gitStatus;
3195
- if (gitStatus.isGitRepo) {
3196
- status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
3197
- } else {
3198
- status.health = 'degraded';
3199
- if (gitStatus.error && !status.error) status.error = gitStatus.error;
5999
+ if (!fs.existsSync(workspace)) {
6000
+ // Workspace not local prefer direct live inline truth, then attempt a P2P git probe.
6001
+ const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
6002
+ let remoteProbeApplied = false;
6003
+ if (inlineTransitGit) {
6004
+ status.git = inlineTransitGit;
6005
+ status.health = inlineTransitGit.isGitRepo
6006
+ ? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
6007
+ : 'degraded';
6008
+ const connection = readObjectRecord(status.connection);
6009
+ const connectionState = readStringValue(connection.state);
6010
+ const connectionReported = readBooleanValue(connection.reported) ?? false;
6011
+ if (!connectionReported || connectionState === 'unknown') {
6012
+ status.connection = buildLivePeerGitConnection(connection, refreshedAt);
6013
+ }
6014
+ remoteProbeApplied = true;
6015
+ } else if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
6016
+ try {
6017
+ const remoteGit = await probeRemoteMeshGitStatus({
6018
+ dispatchMeshCommand: this.deps.dispatchMeshCommand,
6019
+ daemonId,
6020
+ workspace,
6021
+ timeoutMs: 8000,
6022
+ });
6023
+ if (remoteGit) {
6024
+ status.git = remoteGit;
6025
+ status.health = remoteGit.isGitRepo
6026
+ ? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
6027
+ : 'degraded';
6028
+ const connection = readObjectRecord(status.connection);
6029
+ const connectionState = readStringValue(connection.state);
6030
+ const connectionReported = readBooleanValue(connection.reported) ?? false;
6031
+ if (!connectionReported || connectionState === 'unknown') {
6032
+ status.connection = buildLivePeerGitConnection(connection, refreshedAt);
6033
+ }
6034
+ recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
6035
+ remoteProbeApplied = true;
6036
+ }
6037
+ } catch {
6038
+ const refreshedConnection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
6039
+ const refreshedConnectionState = readStringValue(refreshedConnection?.state);
6040
+ if (refreshedConnection && refreshedConnectionState === 'connected') {
6041
+ status.connection = refreshedConnection;
6042
+ try {
6043
+ const remoteGit = await probeRemoteMeshGitStatus({
6044
+ dispatchMeshCommand: this.deps.dispatchMeshCommand,
6045
+ daemonId,
6046
+ workspace,
6047
+ timeoutMs: 12000,
6048
+ });
6049
+ if (remoteGit) {
6050
+ status.git = remoteGit;
6051
+ status.health = remoteGit.isGitRepo
6052
+ ? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
6053
+ : 'degraded';
6054
+ const connection = readObjectRecord(status.connection);
6055
+ const connectionState = readStringValue(connection.state);
6056
+ const connectionReported = readBooleanValue(connection.reported) ?? false;
6057
+ if (!connectionReported || connectionState === 'unknown') {
6058
+ status.connection = buildLivePeerGitConnection(connection, refreshedAt);
6059
+ }
6060
+ recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
6061
+ remoteProbeApplied = true;
6062
+ }
6063
+ } catch {
6064
+ // Probe timed out again or P2P unavailable — fall back to cached status
6065
+ }
6066
+ }
6067
+ }
6068
+ }
6069
+ if (!remoteProbeApplied) {
6070
+ const connectionState = readStringValue((status.connection as any)?.state);
6071
+ const pendingPeerGitProbe = !inlineTransitGit
6072
+ && !isSelfNode
6073
+ && !!daemonId
6074
+ && (
6075
+ readStringValue(status.machineStatus) === 'online'
6076
+ || readStringValue(status.health) === 'online'
6077
+ || connectionState === 'connecting'
6078
+ || connectionState === 'connected'
6079
+ || connectionState === 'unknown'
6080
+ );
6081
+ if (pendingPeerGitProbe) {
6082
+ status.gitProbePending = true;
6083
+ status.health = 'unknown';
6084
+ }
6085
+ if (applyCachedInlineMeshNodeStatus(
6086
+ status,
6087
+ node,
6088
+ pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
6089
+ )) {
6090
+ applyInlineMeshBranchConvergence(mesh, node, status);
6091
+ finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
6092
+ nodeStatuses.push(status);
6093
+ continue;
6094
+ }
6095
+ if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
6096
+ applyInlineMeshBranchConvergence(mesh, node, status);
6097
+ finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
6098
+ nodeStatuses.push(status);
6099
+ continue;
6100
+ }
3200
6101
  }
3201
- } catch {
3202
- if (!applyCachedInlineMeshNodeStatus(status, node)) {
3203
- status.health = 'degraded';
6102
+ } else {
6103
+ try {
6104
+ const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
6105
+ status.git = gitStatus;
6106
+ recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
6107
+ if (gitStatus.isGitRepo) {
6108
+ status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
6109
+ } else {
6110
+ status.health = 'degraded';
6111
+ if (gitStatus.error && !status.error) status.error = gitStatus.error;
6112
+ }
6113
+ } catch {
6114
+ if (!applyCachedInlineMeshNodeStatus(status, node)) {
6115
+ status.health = 'degraded';
6116
+ }
3204
6117
  }
3205
6118
  }
3206
6119
  } else {
3207
6120
  applyCachedInlineMeshNodeStatus(status, node);
3208
6121
  }
3209
- status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
6122
+ applyInlineMeshBranchConvergence(mesh, node, status);
6123
+ finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
3210
6124
  nodeStatuses.push(status);
3211
6125
  }
3212
6126
 
3213
- return {
6127
+ // (B3) Resolve the coordinator daemon scope for the drain.
6128
+ // Emit-time always tags events with the worker's
6129
+ // targetCoordinatorDaemonId and writes them to the
6130
+ // scoped pending-events file. If the caller (MCP tool,
6131
+ // dashboard, etc.) doesn't tell us which coordinator
6132
+ // they're running under, fall back to *this daemon's*
6133
+ // ID — the caller reached us over our IPC/WS, so they
6134
+ // are by definition under this daemon. Falling back to
6135
+ // undefined would silently miss every scoped event.
6136
+ const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
6137
+ ? args.coordinatorDaemonId.trim()
6138
+ : (this.deps.statusInstanceId || undefined);
6139
+ const pendingCoordinatorEvents = drainPendingMeshCoordinatorEvents(meshId, callerCoordinatorDaemonId);
6140
+ const previewFreshness = (() => {
6141
+ const localRepoRoot = nodeStatuses
6142
+ .map((node: any) => readStringValue(node?.git?.repoRoot, node?.repoRoot, node?.workspace))
6143
+ .find((candidate: string | undefined) => !!candidate && fs.existsSync(candidate));
6144
+ return localRepoRoot ? buildPreviewFreshness(localRepoRoot) : undefined;
6145
+ })();
6146
+ const asyncRefineJobs = buildMeshAsyncRefineJobs({
6147
+ meshId,
6148
+ ledgerEntries: asyncRefineLedgerEntries,
6149
+ pendingEvents: pendingCoordinatorEvents,
6150
+ });
6151
+ const historicalSessions = buildHistoricalMeshSessions({
6152
+ meshId,
6153
+ nodes: mesh.nodes || [],
6154
+ liveSessionRecords: liveMeshSessions,
6155
+ });
6156
+ const statusResult = {
3214
6157
  success: true,
3215
6158
  meshId: mesh.id,
3216
6159
  meshName: mesh.name,
3217
6160
  repoIdentity: mesh.repoIdentity,
3218
6161
  defaultBranch: mesh.defaultBranch,
3219
- refreshedAt: new Date().toISOString(),
6162
+ refreshedAt,
6163
+ meshHost,
6164
+ sourceOfTruth: {
6165
+ membership: meshRecord?.source === 'inline_cache'
6166
+ ? 'coordinator_inline_mesh_cache'
6167
+ : meshRecord?.source === 'local_config'
6168
+ ? 'local_mesh_config'
6169
+ : 'inline_bootstrap_snapshot',
6170
+ coordinatorOwnsLiveTruth: directTruthSatisfied,
6171
+ meshHost: {
6172
+ owner: 'mesh_host_daemon',
6173
+ localRole: meshHost.role,
6174
+ hostDaemonId: meshHost.hostDaemonId,
6175
+ hostNodeId: meshHost.hostNodeId,
6176
+ hostAddress: meshHost.hostAddress,
6177
+ },
6178
+ ...(requireDirectPeerTruth ? {
6179
+ currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
6180
+ directPeerTruth: {
6181
+ required: true,
6182
+ satisfied: directTruthSatisfied,
6183
+ directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
6184
+ localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
6185
+ peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
6186
+ peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
6187
+ unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
6188
+ partialNodeFailures: effectiveDirectTruth.unavailableNodeIds,
6189
+ },
6190
+ } : {}),
6191
+ historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary', 'historicalSessions'],
6192
+ },
6193
+ branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
6194
+ ...(previewFreshness ? { previewFreshness, deployFreshness: previewFreshness } : {}),
3220
6195
  nodes: nodeStatuses,
3221
6196
  queue: { tasks: queue, summary: queueSummary },
3222
6197
  ledger: { entries: ledgerEntries, summary: ledgerSummary },
6198
+ ...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
6199
+ ...(historicalSessions ? { historicalSessions } : {}),
6200
+ ...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
3223
6201
  };
6202
+ const { pendingCoordinatorEvents: _pendingCoordinatorEvents, ...cacheableStatusResult } = statusResult as any;
6203
+ const rememberedStatus = this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
6204
+ const returnedStatus = pendingCoordinatorEvents.length > 0
6205
+ ? { ...rememberedStatus, pendingCoordinatorEvents }
6206
+ : rememberedStatus;
6207
+ logRepoMeshStatusDebug('return_live', {
6208
+ meshId,
6209
+ command: 'mesh_status',
6210
+ refreshRequested,
6211
+ refreshReason,
6212
+ meshSource: meshRecord.source,
6213
+ directTruth,
6214
+ summary: summarizeRepoMeshStatusDebug(returnedStatus),
6215
+ });
6216
+ return returnedStatus;
3224
6217
  } catch (e: any) {
3225
6218
  return { success: false, error: e.message };
3226
6219
  }
@@ -3278,7 +6271,7 @@ export class DaemonCommandRouter {
3278
6271
 
3279
6272
  // 3. Kill OS process if requested
3280
6273
  if (killProcess) {
3281
- const running = isIdeRunning(ideType);
6274
+ const running = await isIdeRunning(ideType);
3282
6275
  if (running) {
3283
6276
  LOG.info('StopIDE', `Killing IDE process: ${ideType}`);
3284
6277
  const killed = await killIdeProcess(ideType);