@adhdev/daemon-core 0.9.82-rc.20 → 0.9.82-rc.201

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 (241) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +1 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/chat/source-machine.d.ts +166 -0
  4. package/dist/chat/source-resolver.d.ts +104 -0
  5. package/dist/chat/subscription-updates.d.ts +1 -0
  6. package/dist/cli-adapter-types.d.ts +23 -2
  7. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  8. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  9. package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
  10. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  11. package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
  12. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  13. package/dist/commands/cli-manager.d.ts +2 -1
  14. package/dist/commands/handler.d.ts +110 -0
  15. package/dist/commands/mesh-coordinator.d.ts +85 -1
  16. package/dist/commands/router.d.ts +26 -1
  17. package/dist/config/chat-history.d.ts +9 -0
  18. package/dist/config/config.d.ts +5 -0
  19. package/dist/config/mesh-config.d.ts +72 -1
  20. package/dist/git/git-commands.d.ts +7 -1
  21. package/dist/git/git-types.d.ts +2 -0
  22. package/dist/index.d.ts +39 -10
  23. package/dist/index.js +25488 -8333
  24. package/dist/index.js.map +1 -1
  25. package/dist/index.mjs +29124 -12045
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/installer.d.ts +1 -4
  28. package/dist/ipc/local-ipc-server.d.ts +91 -0
  29. package/dist/launch.d.ts +1 -1
  30. package/dist/logging/async-batch-writer.d.ts +10 -0
  31. package/dist/mesh/contracts.d.ts +164 -0
  32. package/dist/mesh/coordinator-prompt.d.ts +30 -0
  33. package/dist/mesh/coordinator-registry.d.ts +59 -0
  34. package/dist/mesh/mesh-active-work.d.ts +90 -0
  35. package/dist/mesh/mesh-delivery-policy.d.ts +126 -0
  36. package/dist/mesh/mesh-events.d.ts +164 -6
  37. package/dist/mesh/mesh-fast-forward.d.ts +41 -0
  38. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  39. package/dist/mesh/mesh-ledger.d.ts +58 -1
  40. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  41. package/dist/mesh/mesh-runtime-store.d.ts +149 -0
  42. package/dist/mesh/mesh-work-queue.d.ts +55 -6
  43. package/dist/mesh/preview-freshness.d.ts +18 -0
  44. package/dist/mesh/refine-config.d.ts +193 -0
  45. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  46. package/dist/providers/approval-utils.d.ts +13 -0
  47. package/dist/providers/cli-provider-instance.d.ts +39 -3
  48. package/dist/providers/contracts.d.ts +130 -6
  49. package/dist/providers/external-sources.d.ts +71 -0
  50. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  51. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  52. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  53. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  54. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  55. package/dist/providers/native-history/index.d.ts +13 -0
  56. package/dist/providers/provider-instance-manager.d.ts +12 -0
  57. package/dist/providers/provider-instance.d.ts +13 -1
  58. package/dist/providers/provider-loader.d.ts +26 -4
  59. package/dist/providers/provider-trust.d.ts +31 -0
  60. package/dist/providers/read-chat-contract.d.ts +29 -0
  61. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  62. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  63. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  64. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  65. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  66. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  67. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  68. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  69. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  70. package/dist/providers/sdk/v1/index.d.ts +30 -0
  71. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  72. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  73. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  74. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  75. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  76. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  77. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  78. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  79. package/dist/providers/spec/adapter.d.ts +60 -0
  80. package/dist/providers/spec/cli-adapter.d.ts +101 -0
  81. package/dist/providers/spec/driver.d.ts +195 -0
  82. package/dist/providers/spec/evaluator.d.ts +55 -0
  83. package/dist/providers/spec/loader.d.ts +14 -0
  84. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  85. package/dist/providers/spec/route.d.ts +4 -0
  86. package/dist/providers/spec/schema.gen.d.ts +599 -0
  87. package/dist/providers/spec/types.d.ts +282 -0
  88. package/dist/providers/transcript-v2.d.ts +176 -0
  89. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  90. package/dist/repo-mesh-types.d.ts +108 -1
  91. package/dist/sessions/registry.d.ts +3 -0
  92. package/dist/shared-types-extra.d.ts +1 -1
  93. package/dist/shared-types.d.ts +42 -1
  94. package/dist/status/normalize.d.ts +1 -1
  95. package/dist/status/normalize.js +1 -0
  96. package/dist/status/normalize.js.map +1 -1
  97. package/dist/status/normalize.mjs +1 -0
  98. package/dist/status/normalize.mjs.map +1 -1
  99. package/dist/status/reporter.d.ts +2 -0
  100. package/dist/status/snapshot.d.ts +1 -0
  101. package/dist/types.d.ts +5 -0
  102. package/package.json +7 -2
  103. package/src/agent-stream/poller.ts +2 -3
  104. package/src/boot/daemon-lifecycle.ts +19 -10
  105. package/src/boot/process-hardening.ts +89 -0
  106. package/src/chat/source-machine.ts +534 -0
  107. package/src/chat/source-resolver.ts +0 -0
  108. package/src/chat/subscription-updates.ts +20 -1
  109. package/src/cli-adapter-types.d.ts +1 -0
  110. package/src/cli-adapter-types.ts +22 -2
  111. package/src/cli-adapters/cli-script-runner.ts +421 -0
  112. package/src/cli-adapters/cli-state-engine.ts +1138 -0
  113. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  114. package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
  115. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  116. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  117. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  118. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  119. package/src/cli-adapters/provider-cli-shared.ts +59 -11
  120. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  121. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  122. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  123. package/src/commands/chat-commands.ts +1787 -60
  124. package/src/commands/cli-manager.ts +283 -14
  125. package/src/commands/handler.ts +809 -2
  126. package/src/commands/mesh-coordinator.ts +331 -122
  127. package/src/commands/router.ts +3628 -515
  128. package/src/config/chat-history.ts +95 -24
  129. package/src/config/config.ts +12 -0
  130. package/src/config/mesh-config.ts +280 -2
  131. package/src/config/recent-activity.ts +8 -2
  132. package/src/daemon/dev-cli-debug.ts +10 -1
  133. package/src/detection/ide-detector.ts +26 -16
  134. package/src/git/git-commands.ts +37 -7
  135. package/src/git/git-status.ts +35 -6
  136. package/src/git/git-types.ts +2 -0
  137. package/src/git/git-worktree.ts +8 -1
  138. package/src/index.ts +119 -9
  139. package/src/installer.d.ts +1 -1
  140. package/src/installer.ts +8 -6
  141. package/src/ipc/local-ipc-server.ts +278 -0
  142. package/src/launch.d.ts +1 -1
  143. package/src/launch.ts +37 -28
  144. package/src/logging/async-batch-writer.ts +55 -0
  145. package/src/logging/logger.ts +2 -1
  146. package/src/mesh/contracts.ts +329 -0
  147. package/src/mesh/coordinator-prompt.ts +204 -30
  148. package/src/mesh/coordinator-registry.ts +121 -0
  149. package/src/mesh/mesh-active-work.ts +437 -0
  150. package/src/mesh/mesh-delivery-policy.ts +298 -0
  151. package/src/mesh/mesh-events.ts +1341 -109
  152. package/src/mesh/mesh-fast-forward.ts +438 -0
  153. package/src/mesh/mesh-host-ownership.ts +73 -0
  154. package/src/mesh/mesh-ledger.ts +457 -104
  155. package/src/mesh/mesh-refine-status.ts +144 -0
  156. package/src/mesh/mesh-runtime-store.ts +769 -0
  157. package/src/mesh/mesh-work-queue.ts +267 -159
  158. package/src/mesh/preview-freshness.ts +118 -0
  159. package/src/mesh/refine-config.ts +366 -0
  160. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  161. package/src/providers/approval-utils.d.ts +4 -0
  162. package/src/providers/approval-utils.ts +47 -5
  163. package/src/providers/chat-message-normalization.ts +4 -11
  164. package/src/providers/cli-provider-instance.ts +806 -64
  165. package/src/providers/contracts.d.ts +55 -0
  166. package/src/providers/contracts.ts +141 -6
  167. package/src/providers/external-sources.ts +218 -0
  168. package/src/providers/ide-provider-instance.ts +19 -5
  169. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  170. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  171. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  172. package/src/providers/native-history/dispatcher.ts +340 -0
  173. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  174. package/src/providers/native-history/index.ts +30 -0
  175. package/src/providers/provider-instance-manager.ts +30 -0
  176. package/src/providers/provider-instance.ts +10 -1
  177. package/src/providers/provider-loader.ts +582 -50
  178. package/src/providers/provider-schema.ts +87 -14
  179. package/src/providers/provider-trust.ts +114 -0
  180. package/src/providers/read-chat-contract.ts +76 -16
  181. package/src/providers/sdk/README.md +49 -0
  182. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  183. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  184. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  185. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  186. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  187. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  188. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  189. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  190. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  191. package/src/providers/sdk/v1/index.ts +152 -0
  192. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  193. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  194. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  195. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  196. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  197. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  198. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  199. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  200. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  201. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  202. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  203. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  204. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  205. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  206. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  207. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  208. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  209. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  210. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  211. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  212. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  213. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  214. package/src/providers/sdk/v1/validators/index.ts +19 -0
  215. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  216. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  217. package/src/providers/spec/adapter.ts +176 -0
  218. package/src/providers/spec/cli-adapter.ts +573 -0
  219. package/src/providers/spec/driver.ts +733 -0
  220. package/src/providers/spec/evaluator.ts +373 -0
  221. package/src/providers/spec/loader.ts +130 -0
  222. package/src/providers/spec/native-history-executor.ts +939 -0
  223. package/src/providers/spec/route.ts +51 -0
  224. package/src/providers/spec/schema.gen.ts +559 -0
  225. package/src/providers/spec/schema.json +237 -0
  226. package/src/providers/spec/types.ts +301 -0
  227. package/src/providers/transcript-v2.ts +567 -0
  228. package/src/providers/types/interactive-prompt.ts +425 -0
  229. package/src/providers/version-archive.ts +38 -20
  230. package/src/repo-mesh-types.ts +118 -1
  231. package/src/sessions/registry.ts +3 -0
  232. package/src/shared-types-extra.ts +1 -1
  233. package/src/shared-types.ts +45 -1
  234. package/src/status/builders.ts +24 -6
  235. package/src/status/normalize.ts +2 -0
  236. package/src/status/reporter.ts +15 -0
  237. package/src/status/snapshot.ts +84 -25
  238. package/src/system/host-memory.ts +29 -12
  239. package/src/types.ts +5 -0
  240. package/dist/mesh/mesh-sync.d.ts +0 -53
  241. 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);
@@ -259,7 +849,7 @@ function readCachedInlineMeshActiveSessions(node: any): string[] {
259
849
  return sessionId ? [sessionId] : [];
260
850
  }
261
851
 
262
- function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
852
+ export function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
263
853
  const cachedStatus = readObjectRecord(node?.cachedStatus);
264
854
  const activeSession = readObjectRecord(cachedStatus.activeSession);
265
855
  const fallbackSession = Object.keys(activeSession).length
@@ -287,9 +877,14 @@ function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<strin
287
877
  node?.provider_type,
288
878
  ),
289
879
  state: readStringValue(fallbackSession.status, fallbackSession.state, fallbackSession.lifecycle),
880
+ chatStatus: readStringValue(fallbackSession.chatStatus, fallbackSession.chat_status),
290
881
  lifecycle: readStringValue(fallbackSession.lifecycle),
291
882
  title: readStringValue(fallbackSession.title, fallbackSession.displayName, fallbackSession.display_name) ?? null,
292
883
  workspace: readStringValue(fallbackSession.workspace, node?.workspace) ?? null,
884
+ role: readStringValue(fallbackSession.role) ?? null,
885
+ isSelfCoordinator: fallbackSession.isSelfCoordinator === true || fallbackSession.is_self_coordinator === true,
886
+ createdAt: readStringValue(fallbackSession.createdAt, fallbackSession.created_at) ?? null,
887
+ startedAt: readStringValue(fallbackSession.startedAt, fallbackSession.started_at) ?? null,
293
888
  lastActivityAt: readStringValue(fallbackSession.lastActivityAt, fallbackSession.last_activity_at) ?? null,
294
889
  recoveryState: readStringValue(fallbackSession.recoveryState, fallbackSession.recovery_state) ?? null,
295
890
  isCached: true,
@@ -313,21 +908,217 @@ function toIsoTimestamp(value: unknown): string | null {
313
908
  return stringValue || null;
314
909
  }
315
910
 
911
+ function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknown>): void {
912
+ const connection = readObjectRecord(status.connection);
913
+ const connectionFreshAt = toIsoTimestamp(connection.lastCommandAt ?? connection.lastConnectedAt ?? connection.lastStateChangeAt);
914
+ const git = readObjectRecord(status.git);
915
+ const gitCheckedAt = toIsoTimestamp(git.lastCheckedAt);
916
+ if (!status.lastSeenAt && connectionFreshAt) status.lastSeenAt = connectionFreshAt;
917
+ if (!status.updatedAt && (gitCheckedAt || connectionFreshAt)) {
918
+ status.updatedAt = gitCheckedAt ?? connectionFreshAt;
919
+ }
920
+ }
921
+
922
+ function finalizeMeshNodeStatus(args: {
923
+ status: Record<string, unknown>;
924
+ node: any;
925
+ daemonId?: string;
926
+ isSelfNode: boolean;
927
+ }): void {
928
+ const { status, node, daemonId, isSelfNode } = args;
929
+ if (!readStringValue(status.machineStatus)) {
930
+ const cachedStatus = readObjectRecord(node?.cachedStatus);
931
+ const machineStatus = readStringValue(cachedStatus.machineStatus, cachedStatus.machine_status, node?.machineStatus);
932
+ if (machineStatus) status.machineStatus = machineStatus;
933
+ }
934
+ synthesizeMeshNodeFreshnessFromConnection(status);
935
+ const bootstrap = readObjectRecord(node?.worktreeBootstrap);
936
+ if (node?.isLocalWorktree && readStringValue(bootstrap.status)) {
937
+ status.worktreeBootstrap = bootstrap;
938
+ if (bootstrap.status === 'failed' && bootstrap.required !== false) {
939
+ status.launchReady = false;
940
+ status.launchBlockedReason = 'worktree_bootstrap_failed';
941
+ status.launchBlockedMessage = readStringValue(bootstrap.error)
942
+ || 'Required worktree bootstrap failed; resolve it before launching an agent into this node.';
943
+ return;
944
+ }
945
+ if (bootstrap.status === 'running' && bootstrap.required !== false) {
946
+ status.launchReady = false;
947
+ status.launchBlockedReason = 'worktree_bootstrap_running';
948
+ status.launchBlockedMessage = 'Required worktree bootstrap is still running; wait for it to finish before launching an agent into this node.';
949
+ return;
950
+ }
951
+ }
952
+ const connectionState = readStringValue(readObjectRecord(status.connection).state);
953
+ status.launchReady = !!daemonId && (
954
+ readStringValue(status.machineStatus) === 'online'
955
+ || connectionState === 'connected'
956
+ || isSelfNode
957
+ );
958
+ }
959
+
960
+ async function probeRemoteMeshGitStatus(args: {
961
+ dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
962
+ daemonId: string;
963
+ workspace: string;
964
+ timeoutMs: number;
965
+ }): Promise<Record<string, unknown> | null> {
966
+ if (!args.dispatchMeshCommand) return null;
967
+ const remoteResult = await Promise.race([
968
+ args.dispatchMeshCommand(args.daemonId, 'git_status', { workspace: args.workspace, refreshUpstream: true }),
969
+ new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), args.timeoutMs)),
970
+ ]) as any;
971
+ const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
972
+ return remoteGit && typeof remoteGit === 'object' && typeof remoteGit.isGitRepo === 'boolean'
973
+ ? remoteGit as Record<string, unknown>
974
+ : null;
975
+ }
976
+
977
+ async function hydrateInlineMeshDirectTruth(args: {
978
+ mesh: any;
979
+ meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
980
+ dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
981
+ statusInstanceId?: string;
982
+ localMachineId?: string;
983
+ }): Promise<{
984
+ directEvidenceCount: number;
985
+ localConfirmedCount: number;
986
+ peerAttemptedCount: number;
987
+ peerConfirmedCount: number;
988
+ unavailableNodeIds: string[];
989
+ }> {
990
+ const nodes = Array.isArray(args.mesh?.nodes) ? args.mesh.nodes : [];
991
+ if (!nodes.length) {
992
+ return {
993
+ directEvidenceCount: 0,
994
+ localConfirmedCount: 0,
995
+ peerAttemptedCount: 0,
996
+ peerConfirmedCount: 0,
997
+ unavailableNodeIds: [],
998
+ };
999
+ }
1000
+
1001
+ const selectedCoordinatorNodeId = readStringValue(
1002
+ args.mesh?.coordinator?.preferredNodeId,
1003
+ nodes[0]?.id,
1004
+ nodes[0]?.nodeId,
1005
+ );
1006
+
1007
+ let localConfirmedCount = 0;
1008
+ let peerAttemptedCount = 0;
1009
+ let peerConfirmedCount = 0;
1010
+ const unavailableNodeIds: string[] = [];
1011
+
1012
+ for (const [nodeIndex, node] of nodes.entries()) {
1013
+ const nodeId = readStringValue(node?.id, node?.nodeId) || `node_${nodeIndex}`;
1014
+ const workspace = readStringValue(node?.workspace);
1015
+ const daemonId = readStringValue(node?.daemonId);
1016
+ const isSelfNode = Boolean(
1017
+ nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
1018
+ ) || Boolean(
1019
+ daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
1020
+ ) || Boolean(args.meshSource !== 'local_config' && nodeIndex === 0);
1021
+
1022
+ if (!workspace) {
1023
+ if (!isSelfNode && daemonId) unavailableNodeIds.push(nodeId);
1024
+ continue;
1025
+ }
1026
+
1027
+ if (fs.existsSync(workspace)) {
1028
+ try {
1029
+ const localGit = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
1030
+ if (localGit?.isGitRepo) {
1031
+ recordInlineMeshDirectGitTruth(node, localGit as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
1032
+ localConfirmedCount += 1;
1033
+ continue;
1034
+ }
1035
+ } catch {
1036
+ // Fall through to remote classification.
1037
+ }
1038
+ }
1039
+
1040
+ if (!daemonId || !args.dispatchMeshCommand) {
1041
+ if (!isSelfNode) unavailableNodeIds.push(nodeId);
1042
+ continue;
1043
+ }
1044
+
1045
+ peerAttemptedCount += 1;
1046
+ try {
1047
+ const remoteGit = await probeRemoteMeshGitStatus({
1048
+ dispatchMeshCommand: args.dispatchMeshCommand,
1049
+ daemonId,
1050
+ workspace,
1051
+ timeoutMs: 8_000,
1052
+ });
1053
+ if (remoteGit) {
1054
+ recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
1055
+ peerConfirmedCount += 1;
1056
+ continue;
1057
+ }
1058
+ } catch {
1059
+ // Strict direct-only path: do not fall back to persisted cloud truth here.
1060
+ }
1061
+
1062
+ unavailableNodeIds.push(nodeId);
1063
+ }
1064
+
1065
+ return {
1066
+ directEvidenceCount: localConfirmedCount + peerConfirmedCount,
1067
+ localConfirmedCount,
1068
+ peerAttemptedCount,
1069
+ peerConfirmedCount,
1070
+ unavailableNodeIds,
1071
+ };
1072
+ }
1073
+
316
1074
  function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
1075
+ const meta = readObjectRecord(record?.meta);
1076
+ const isSelfCoordinator = Boolean(readStringValue(meta.meshCoordinatorFor));
1077
+ const chatStatus = readStringValue(record?.chatStatus, record?.activeChat?.status, meta.chatStatus, meta.sessionStatus);
1078
+ const state = readLiveMeshSessionState(record);
1079
+ const statusNote = isSelfCoordinator && (!chatStatus || chatStatus === 'idle' || state === 'idle')
1080
+ ? 'Coordinator self status is sampled from the session host and may read idle while the coordinator is generating this response.'
1081
+ : null;
317
1082
  return {
318
1083
  sessionId: readStringValue(record?.sessionId) || 'unknown',
319
1084
  providerType: readStringValue(record?.providerType),
320
- state: readLiveMeshSessionState(record),
1085
+ state,
1086
+ chatStatus,
321
1087
  lifecycle: readStringValue(record?.lifecycle),
322
1088
  surfaceKind: getSessionHostSurfaceKind(record as any),
323
- recoveryState: readStringValue(record?.meta?.runtimeRecoveryState) ?? null,
1089
+ recoveryState: readStringValue(meta.runtimeRecoveryState) ?? null,
324
1090
  workspace: readStringValue(record?.workspace) ?? null,
325
1091
  title: readStringValue(record?.displayName, record?.workspaceLabel) ?? null,
1092
+ role: isSelfCoordinator ? 'coordinator' : readStringValue(meta.meshRole, meta.role) ?? null,
1093
+ isSelfCoordinator,
1094
+ statusNote,
1095
+ createdAt: toIsoTimestamp(record?.createdAt ?? record?.created_at),
1096
+ startedAt: toIsoTimestamp(record?.startedAt ?? record?.started_at ?? record?.spawnedAtMs ?? record?.spawned_at_ms),
326
1097
  lastActivityAt: toIsoTimestamp(record?.updatedAt ?? record?.lastActivityAt ?? record?.last_activity_at),
327
1098
  isCached: false,
328
1099
  };
329
1100
  }
330
1101
 
1102
+ function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string, nodeWorkspace = '', nodeIsMissingLocalWorktree = false): boolean {
1103
+ const recordNodeId = readStringValue(record?.meta?.meshNodeId);
1104
+ if (!recordNodeId || recordNodeId !== nodeId) return false;
1105
+ if (nodeIsMissingLocalWorktree) return false;
1106
+ const recordWorkspace = readStringValue(record?.workspace);
1107
+ if (nodeWorkspace && recordWorkspace && recordWorkspace !== nodeWorkspace) return false;
1108
+ const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
1109
+ return !recordMeshId || recordMeshId === meshId;
1110
+ }
1111
+
1112
+ function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
1113
+ const recordWorkspace = readStringValue(record?.workspace);
1114
+ if (!recordWorkspace || !workspace || recordWorkspace !== workspace) return false;
1115
+
1116
+ const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
1117
+ if (recordMeshId) return recordMeshId === meshId;
1118
+
1119
+ return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
1120
+ }
1121
+
331
1122
  function readLiveMeshNodeWorkspace(args: {
332
1123
  meshId: string;
333
1124
  nodeId: string;
@@ -335,7 +1126,7 @@ function readLiveMeshNodeWorkspace(args: {
335
1126
  allowCoordinatorSession?: boolean;
336
1127
  }): string {
337
1128
  const directNodeWorkspace = args.liveSessionRecords.find((record) => (
338
- readStringValue(record?.meta?.meshNodeId) === args.nodeId
1129
+ liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)
339
1130
  && readStringValue(record?.workspace)
340
1131
  ));
341
1132
  if (directNodeWorkspace) {
@@ -362,11 +1153,16 @@ function collectLiveMeshSessionRecords(args: {
362
1153
  liveSessionRecords: any[];
363
1154
  allowCoordinatorSession?: boolean;
364
1155
  }): any[] {
1156
+ const nodeWorkspace = readStringValue(args.node?.workspace);
1157
+ const nodeIsMissingLocalWorktree = args.node?.isLocalWorktree === true
1158
+ && !!nodeWorkspace
1159
+ && !fs.existsSync(nodeWorkspace);
365
1160
  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;
1161
+ const recordNodeId = readStringValue(record?.meta?.meshNodeId);
1162
+ if (recordNodeId && recordNodeId !== args.nodeId) return false;
1163
+ if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId, nodeWorkspace || '', nodeIsMissingLocalWorktree)) return true;
1164
+ if (nodeIsMissingLocalWorktree) return false;
1165
+ return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
370
1166
  });
371
1167
 
372
1168
  if (args.allowCoordinatorSession) {
@@ -381,11 +1177,62 @@ function collectLiveMeshSessionRecords(args: {
381
1177
  return matches;
382
1178
  }
383
1179
 
384
- function applyCachedInlineMeshNodeStatus(status: Record<string, unknown>, node: any): boolean {
1180
+ function buildHistoricalMeshSessions(args: {
1181
+ meshId: string;
1182
+ nodes: any[];
1183
+ liveSessionRecords: any[];
1184
+ }): { count: number; sessions: Record<string, unknown>[]; instruction: string } | undefined {
1185
+ const liveNodeIds = new Set<string>();
1186
+ const liveWorkspaces = new Set<string>();
1187
+ const missingLocalWorktreeNodeIds = new Set<string>();
1188
+ for (const node of args.nodes || []) {
1189
+ const nodeId = readStringValue(node?.id, node?.nodeId);
1190
+ const workspace = readStringValue(node?.workspace);
1191
+ if (nodeId) liveNodeIds.add(nodeId);
1192
+ if (workspace) liveWorkspaces.add(workspace);
1193
+ if (nodeId && node?.isLocalWorktree === true && workspace && !fs.existsSync(workspace)) {
1194
+ missingLocalWorktreeNodeIds.add(nodeId);
1195
+ }
1196
+ }
1197
+
1198
+ const sessions: Record<string, unknown>[] = [];
1199
+ for (const record of args.liveSessionRecords || []) {
1200
+ const meta = readObjectRecord(record?.meta);
1201
+ const recordMeshId = readStringValue(meta.meshNodeFor, meta.meshCoordinatorFor);
1202
+ if (recordMeshId !== args.meshId) continue;
1203
+ const recordNodeId = readStringValue(meta.meshNodeId);
1204
+ const workspace = readStringValue(record?.workspace);
1205
+ const removedNode = !!recordNodeId && (!liveNodeIds.has(recordNodeId) || missingLocalWorktreeNodeIds.has(recordNodeId));
1206
+ const orphanedWorkspace = !!workspace && !liveWorkspaces.has(workspace) && meta.meshCoordinatorFor !== args.meshId;
1207
+ if (!removedNode && !orphanedWorkspace) continue;
1208
+ sessions.push({
1209
+ ...summarizeMeshSessionRecord(record),
1210
+ classification: removedNode ? 'removedNode' : 'orphanedSession',
1211
+ historical: true,
1212
+ meshNodeId: recordNodeId || null,
1213
+ reason: removedNode
1214
+ ? 'Session is tagged to a mesh node that is no longer in live membership.'
1215
+ : 'Session workspace is no longer attached to a live mesh node.',
1216
+ });
1217
+ }
1218
+ if (sessions.length === 0) return undefined;
1219
+ return {
1220
+ count: sessions.length,
1221
+ sessions: sessions.slice(0, 5),
1222
+ 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.',
1223
+ };
1224
+ }
1225
+
1226
+ function applyCachedInlineMeshNodeStatus(
1227
+ status: Record<string, unknown>,
1228
+ node: any,
1229
+ options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
1230
+ ): boolean {
385
1231
  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);
1232
+ const liveGit = buildInlineMeshTransitGitStatus(node);
1233
+ const git = options?.skipGit ? undefined : (liveGit ?? buildCachedInlineMeshGitStatus(node));
1234
+ const error = options?.skipError ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.error, node?.error));
1235
+ const health = options?.skipHealth ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.health, node?.health));
389
1236
  const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
390
1237
  const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
391
1238
  const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
@@ -442,29 +1289,140 @@ async function resolveProviderTypeFromPriority(args: {
442
1289
  }
443
1290
  type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
444
1291
  type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
445
- type MeshRefineValidationCommand = {
446
- command: string;
447
- args: string[];
448
- displayCommand: string;
449
- category: string;
450
- source: string;
451
- };
1292
+ type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
452
1293
 
453
1294
  type MeshRefineValidationSummary = {
454
1295
  status: MeshRefineValidationStatus;
455
1296
  required: true;
456
1297
  commandsRun: Array<Record<string, unknown>>;
1298
+ bootstrapCommandsRun: Array<Record<string, unknown>>;
457
1299
  rejectedCommands: Array<Record<string, unknown>>;
458
1300
  skippedReason?: string;
1301
+ failureKind?: string;
1302
+ failureCode?: string;
459
1303
  timeoutMs: number;
460
1304
  outputLimitBytes: number;
1305
+ configSource?: string;
1306
+ configSourceType?: string;
1307
+ suggestions?: unknown[];
1308
+ suggestedConfig?: unknown;
1309
+ };
1310
+
1311
+ type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
1312
+
1313
+ type MeshRefinePatchEquivalenceSummary = {
1314
+ status: MeshRefineStageStatus;
1315
+ equivalent: boolean;
1316
+ baseHead: string;
1317
+ branchHead: string;
1318
+ mergeBase?: string;
1319
+ mergedTree?: string;
1320
+ expectedPatchId?: string;
1321
+ actualPatchId?: string;
1322
+ durationMs: number;
1323
+ error?: string;
1324
+ stdout?: string;
1325
+ stderr?: string;
1326
+ actionableHint?: MeshRefineSubmoduleConflictHint;
1327
+ };
1328
+
1329
+ type MeshRefineSubmoduleConflictHint = {
1330
+ kind: 'submodule_conflict';
1331
+ message: string;
1332
+ conflicts: Array<{
1333
+ path: string;
1334
+ baseCommit?: string;
1335
+ branchCommit?: string;
1336
+ }>;
1337
+ nextSteps: string[];
1338
+ };
1339
+
1340
+ type MeshRefineSubmoduleAlignmentSummary = {
1341
+ status: 'passed' | 'failed' | 'skipped';
1342
+ changedGitlinkPaths: string[];
1343
+ outOfSyncPaths: string[];
1344
+ updatedPaths: string[];
1345
+ verifiedPaths: string[];
1346
+ durationMs: number;
1347
+ reason?: string;
1348
+ command?: string;
1349
+ error?: string;
1350
+ stdout?: string;
1351
+ stderr?: string;
1352
+ };
1353
+
1354
+ type MeshRefineSubmoduleReachabilityEntry = {
1355
+ path: string;
1356
+ commit: string;
1357
+ reachable: boolean;
1358
+ publishRequired?: boolean;
1359
+ autoPublishAllowed?: boolean;
1360
+ autoPublishAttempted?: boolean;
1361
+ autoPublishSucceeded?: boolean;
1362
+ autoPublishVerified?: boolean;
1363
+ autoPublishRefspec?: string;
1364
+ autoPublishSkippedReason?: string;
1365
+ importedFromWorktree?: boolean;
1366
+ checkedLocal?: boolean;
1367
+ localReachable?: boolean;
1368
+ remote?: string;
1369
+ remoteUrl?: string;
1370
+ remoteReachable?: boolean;
1371
+ remoteMainBranch?: string;
1372
+ remoteMainReachable?: boolean;
1373
+ fetchedFromOrigin?: boolean;
1374
+ error?: string;
1375
+ publishStdout?: string;
1376
+ publishStderr?: string;
461
1377
  };
462
1378
 
1379
+ type MeshRefineSubmoduleReachabilitySummary = {
1380
+ status: MeshRefineStageStatus;
1381
+ checked: number;
1382
+ unreachable: MeshRefineSubmoduleReachabilityEntry[];
1383
+ entries: MeshRefineSubmoduleReachabilityEntry[];
1384
+ durationMs: number;
1385
+ autoPublishAllowed?: boolean;
1386
+ autoPublishPolicySource?: string;
1387
+ error?: string;
1388
+ };
1389
+
1390
+ type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
1391
+
1392
+ type MeshRefineJobHandle = {
1393
+ success: true;
1394
+ async: true;
1395
+ status: MeshRefineAsyncJobStatus;
1396
+ jobId: string;
1397
+ interactionId: string;
1398
+ meshId: string;
1399
+ nodeId: string;
1400
+ targetNodeId: string;
1401
+ targetDaemonId?: string;
1402
+ workspace?: string;
1403
+ startedAt: string;
1404
+ completedAt?: string;
1405
+ duplicate?: boolean;
1406
+ retryOfJobId?: string;
1407
+ eventDelivery: {
1408
+ pendingEvents: true;
1409
+ ledger: true;
1410
+ };
1411
+ evidence: {
1412
+ pendingEventsCommand: 'get_pending_mesh_events';
1413
+ ledgerCommand: 'get_mesh_ledger_slice';
1414
+ taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
1415
+ };
1416
+ };
1417
+
1418
+ type MeshRefineTerminalJob = MeshRefineJobHandle & { result?: Record<string, unknown> };
1419
+
463
1420
  const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
464
1421
  const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
465
1422
  const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
466
1423
  const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
467
1424
  const REFINE_VALIDATION_MAX_COMMANDS = 4;
1425
+ const REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
468
1426
 
469
1427
  function truncateValidationOutput(value: unknown): string {
470
1428
  const text = typeof value === 'string' ? value : value == null ? '' : String(value);
@@ -472,171 +1430,492 @@ function truncateValidationOutput(value: unknown): string {
472
1430
  return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
473
1431
  }
474
1432
 
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;
1433
+ function recordMeshRefineStage(
1434
+ stages: Array<Record<string, unknown>>,
1435
+ stage: string,
1436
+ status: MeshRefineStageStatus,
1437
+ startedAt: number,
1438
+ details?: Record<string, unknown>,
1439
+ ): void {
1440
+ stages.push({
1441
+ stage,
1442
+ status,
1443
+ durationMs: Date.now() - startedAt,
1444
+ ...(details || {}),
1445
+ });
498
1446
  }
499
1447
 
500
- function scriptMatchesValidationCategory(scriptName: string, category: string): boolean {
501
- return scriptName === category || scriptName.startsWith(`${category}:`);
1448
+ function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReachabilityEntry[]): string {
1449
+ const refs = entries
1450
+ .map(entry => `${entry.path}@${entry.commit}`)
1451
+ .join(', ');
1452
+ 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
1453
  }
503
1454
 
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' } };
1455
+ function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
1456
+ if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
1457
+ process.stderr.write(
1458
+ `[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via mesh.policy. `
1459
+ + `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
1460
+ );
1461
+ return { enabled: true, source: 'mesh.policy.allowAutoPublishSubmoduleMainCommits' };
513
1462
  }
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' } };
1463
+ const loaded = loadMeshRefineConfig(mesh, workspace);
1464
+ if (loaded.config?.allowAutoPublishSubmoduleMainCommits === true) {
1465
+ process.stderr.write(
1466
+ `[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via ${loaded.path || loaded.source}. `
1467
+ + `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
1468
+ );
1469
+ return { enabled: true, source: loaded.path || loaded.source };
534
1470
  }
1471
+ return { enabled: false };
1472
+ }
535
1473
 
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' } };
1474
+ async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
1475
+ const { execFileSync } = await import('node:child_process');
1476
+ const diff = execFileSync('git', ['diff', '--patch', '--full-index', fromRef, toRef], {
1477
+ cwd,
1478
+ encoding: 'utf8',
1479
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1480
+ });
1481
+ if (!diff.trim()) return '';
1482
+ const patchId = execFileSync('git', ['patch-id', '--stable'], {
1483
+ cwd,
1484
+ input: diff,
1485
+ encoding: 'utf8',
1486
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1487
+ }).trim();
1488
+ return patchId.split(/\s+/)[0] || '';
1489
+ }
1490
+
1491
+ async function runMeshRefinePatchEquivalenceGate(
1492
+ repoRoot: string,
1493
+ baseHead: string,
1494
+ branchHead: string,
1495
+ ): Promise<MeshRefinePatchEquivalenceSummary> {
1496
+ const startedAt = Date.now();
1497
+ try {
1498
+ const { execFileSync } = await import('node:child_process');
1499
+ const git = (args: string[]) => execFileSync('git', args, {
1500
+ cwd: repoRoot,
1501
+ encoding: 'utf8',
1502
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1503
+ });
1504
+ const mergeBase = git(['merge-base', baseHead, branchHead]).trim();
1505
+ const mergeTreeStdout = git(['merge-tree', '--write-tree', baseHead, branchHead]);
1506
+ const mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || '';
1507
+ if (!mergeBase || !mergedTree) {
1508
+ return {
1509
+ status: 'failed',
1510
+ equivalent: false,
1511
+ baseHead,
1512
+ branchHead,
1513
+ mergeBase: mergeBase || undefined,
1514
+ mergedTree: mergedTree || undefined,
1515
+ durationMs: Date.now() - startedAt,
1516
+ error: 'patch equivalence preflight could not resolve merge-base or synthetic merge tree',
1517
+ stdout: truncateValidationOutput(mergeTreeStdout),
1518
+ };
1519
+ }
1520
+ const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
1521
+ const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree);
1522
+ const equivalent = expectedPatchId === actualPatchId;
1523
+ return {
1524
+ status: equivalent ? 'passed' : 'failed',
1525
+ equivalent,
1526
+ baseHead,
1527
+ branchHead,
1528
+ mergeBase,
1529
+ mergedTree,
1530
+ expectedPatchId,
1531
+ actualPatchId,
1532
+ durationMs: Date.now() - startedAt,
1533
+ };
1534
+ } catch (e: any) {
1535
+ return {
1536
+ status: 'failed',
1537
+ equivalent: false,
1538
+ baseHead,
1539
+ branchHead,
1540
+ durationMs: Date.now() - startedAt,
1541
+ error: e?.message || String(e),
1542
+ stdout: truncateValidationOutput(e?.stdout),
1543
+ stderr: truncateValidationOutput(e?.stderr),
1544
+ actionableHint: buildPatchEquivalenceSubmoduleConflictHint(
1545
+ repoRoot,
1546
+ baseHead,
1547
+ branchHead,
1548
+ `${e?.message || ''}\n${e?.stdout || ''}\n${e?.stderr || ''}`,
1549
+ ),
1550
+ };
541
1551
  }
1552
+ }
542
1553
 
1554
+ function buildPatchEquivalenceSubmoduleConflictHint(
1555
+ repoRoot: string,
1556
+ baseHead: string,
1557
+ branchHead: string,
1558
+ output: string,
1559
+ ): MeshRefineSubmoduleConflictHint | undefined {
1560
+ if (!/(submodule|160000)/i.test(output) || !/(conflict|failed to merge)/i.test(output)) return undefined;
1561
+ const conflicts = readChangedGitlinkPaths(repoRoot, baseHead, branchHead)
1562
+ .map(path => ({
1563
+ path,
1564
+ baseCommit: readTreeObject(repoRoot, baseHead, path),
1565
+ branchCommit: readTreeObject(repoRoot, branchHead, path),
1566
+ }));
1567
+ if (conflicts.length === 0) return undefined;
543
1568
  return {
544
- command: {
545
- command,
546
- args,
547
- displayCommand: [command, ...args].join(' '),
548
- category,
549
- source,
550
- },
1569
+ kind: 'submodule_conflict',
1570
+ message: 'Refinery could not synthesize a safe merge tree because the branch and base point the same submodule path at different commits.',
1571
+ conflicts,
1572
+ nextSteps: [
1573
+ '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.',
1574
+ 'Resolve the submodule first by checking out or creating the intended submodule commit, then commit the chosen gitlink in the root branch.',
1575
+ 'Ensure the chosen submodule commit is reachable from the configured submodule remote main branch, then rerun mesh_refine_node.',
1576
+ ],
551
1577
  };
552
1578
  }
553
1579
 
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
- });
1580
+ function readChangedGitlinkPaths(repoRoot: string, fromRef: string, toRef: string): string[] {
1581
+ try {
1582
+ const output = execFileSync('git', ['diff', '--raw', '--no-abbrev', fromRef, toRef], {
1583
+ cwd: repoRoot,
1584
+ encoding: 'utf8',
1585
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1586
+ });
1587
+ const paths = new Set<string>();
1588
+ for (const line of output.split('\n')) {
1589
+ if (!line.trim()) continue;
1590
+ const metaAndPath = line.split('\t');
1591
+ const meta = metaAndPath[0] || '';
1592
+ const path = metaAndPath[metaAndPath.length - 1]?.trim();
1593
+ if (!path) continue;
1594
+ const parts = meta.split(/\s+/);
1595
+ if (parts[0]?.includes('160000') || parts[1]?.includes('160000')) {
1596
+ paths.add(path);
1597
+ }
568
1598
  }
1599
+ return [...paths].sort();
1600
+ } catch {
1601
+ return [];
569
1602
  }
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
1603
  }
575
1604
 
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);
1605
+ function readTreeObject(repoRoot: string, ref: string, path: string): string | undefined {
1606
+ try {
1607
+ const output = execFileSync('git', ['ls-tree', ref, '--', path], {
1608
+ cwd: repoRoot,
1609
+ encoding: 'utf8',
1610
+ maxBuffer: 1024 * 1024,
1611
+ }).trim();
1612
+ const match = output.match(/\bcommit\s+([0-9a-f]{40})\b/i);
1613
+ return match?.[1];
1614
+ } catch {
1615
+ return undefined;
1616
+ }
592
1617
  }
593
1618
 
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;
1619
+ async function alignRefinerySubmodulesAfterMerge(
1620
+ repoRoot: string,
1621
+ previousBaseHead: string,
1622
+ currentHead: string,
1623
+ options: { submoduleIgnorePaths?: string[] } = {},
1624
+ ): Promise<MeshRefineSubmoduleAlignmentSummary> {
1625
+ const startedAt = Date.now();
1626
+ const changedGitlinkPaths = readChangedGitlinkPaths(repoRoot, previousBaseHead, currentHead)
1627
+ .filter(path => !(options.submoduleIgnorePaths || []).includes(path));
1628
+ const preStatus = await getGitRepoStatus(repoRoot, {
1629
+ includeSubmodules: true,
1630
+ submoduleIgnorePaths: options.submoduleIgnorePaths,
1631
+ timeoutMs: 15_000,
1632
+ });
1633
+ const outOfSyncPaths = (preStatus.submodules || [])
1634
+ .filter(submodule => submodule.dirty || submodule.outOfSync || !!submodule.error)
1635
+ .map(submodule => submodule.path);
1636
+ const updatePaths = [...new Set([...changedGitlinkPaths, ...outOfSyncPaths])].sort();
1637
+
1638
+ if (updatePaths.length === 0) {
1639
+ return {
1640
+ status: 'skipped',
1641
+ changedGitlinkPaths,
1642
+ outOfSyncPaths,
1643
+ updatedPaths: [],
1644
+ verifiedPaths: [],
1645
+ durationMs: Date.now() - startedAt,
1646
+ reason: 'no_changed_or_out_of_sync_submodules',
1647
+ };
1648
+ }
1649
+
1650
+ const commandArgs = ['submodule', 'update', '--init', '--recursive', '--', ...updatePaths];
1651
+ try {
1652
+ const { execFile } = await import('node:child_process');
1653
+ const { promisify } = await import('node:util');
1654
+ const execFileAsync = promisify(execFile);
1655
+ const result = await execFileAsync('git', commandArgs, {
1656
+ cwd: repoRoot,
1657
+ encoding: 'utf8',
1658
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1659
+ timeout: 60_000,
1660
+ });
1661
+ const postStatus = await getGitRepoStatus(repoRoot, {
1662
+ includeSubmodules: true,
1663
+ submoduleIgnorePaths: options.submoduleIgnorePaths,
1664
+ timeoutMs: 15_000,
1665
+ });
1666
+ const remaining = (postStatus.submodules || [])
1667
+ .filter(submodule => updatePaths.includes(submodule.path) && (submodule.dirty || submodule.outOfSync || !!submodule.error));
1668
+ return {
1669
+ status: remaining.length === 0 ? 'passed' : 'failed',
1670
+ changedGitlinkPaths,
1671
+ outOfSyncPaths,
1672
+ updatedPaths: updatePaths,
1673
+ verifiedPaths: updatePaths.filter(path => !remaining.some(submodule => submodule.path === path)),
1674
+ durationMs: Date.now() - startedAt,
1675
+ command: `git ${commandArgs.join(' ')}`,
1676
+ stdout: truncateValidationOutput(result.stdout),
1677
+ stderr: truncateValidationOutput(result.stderr),
1678
+ ...(remaining.length > 0 ? { error: `Submodule checkout remained out of sync after update: ${remaining.map(entry => entry.path).join(', ')}` } : {}),
1679
+ };
1680
+ } catch (e: any) {
1681
+ return {
1682
+ status: 'failed',
1683
+ changedGitlinkPaths,
1684
+ outOfSyncPaths,
1685
+ updatedPaths: updatePaths,
1686
+ verifiedPaths: [],
1687
+ durationMs: Date.now() - startedAt,
1688
+ command: `git ${commandArgs.join(' ')}`,
1689
+ error: e?.message || String(e),
1690
+ stdout: truncateValidationOutput(e?.stdout),
1691
+ stderr: truncateValidationOutput(e?.stderr),
1692
+ };
1693
+ }
1694
+ }
1695
+
1696
+ async function runMeshRefineSubmoduleReachabilityGate(
1697
+ repoRoot: string,
1698
+ mergedTree: string,
1699
+ options: { allowAutoPublishSubmoduleMainCommits?: boolean; autoPublishPolicySource?: string; worktreeRoot?: string } = {},
1700
+ ): Promise<MeshRefineSubmoduleReachabilitySummary> {
1701
+ const startedAt = Date.now();
1702
+ const entries: MeshRefineSubmoduleReachabilityEntry[] = [];
1703
+ try {
1704
+ const { execFile } = await import('node:child_process');
1705
+ const { promisify } = await import('node:util');
1706
+ const execFileAsync = promisify(execFile);
1707
+ const runGit = async (cwd: string, args: string[]): Promise<string> => {
1708
+ const { stdout } = await execFileAsync('git', args, {
1709
+ cwd,
1710
+ encoding: 'utf8',
1711
+ timeout: 30_000,
1712
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1713
+ windowsHide: true,
1714
+ });
1715
+ return String(stdout || '');
1716
+ };
1717
+ const verifyRemoteMainContainsCommit = async (submodulePath: string, commit: string, branch = 'main'): Promise<void> => {
1718
+ await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
1719
+ await runGit(submodulePath, ['merge-base', '--is-ancestor', commit, `refs/remotes/origin/${branch}`]);
1720
+ };
1721
+ const publishCommitToRemoteMain = async (submodulePath: string, commit: string, branch = 'main'): Promise<{ stdout: string; stderr: string; refspec: string }> => {
1722
+ const refspec = `${commit}:refs/heads/${branch}`;
1723
+ const { stdout, stderr } = await execFileAsync('git', ['push', 'origin', refspec], {
1724
+ cwd: submodulePath,
1725
+ encoding: 'utf8',
1726
+ timeout: 30_000,
1727
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1728
+ windowsHide: true,
1729
+ });
1730
+ return { stdout: String(stdout || ''), stderr: String(stderr || ''), refspec };
1731
+ };
1732
+ const importCommitFromWorktreeSubmodule = async (submodulePath: string, worktreeSubmodulePath: string, commit: string): Promise<boolean> => {
1733
+ if (!fs.existsSync(worktreeSubmodulePath)) return false;
1734
+ try {
1735
+ await runGit(worktreeSubmodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
1736
+ } catch {
1737
+ return false;
1738
+ }
1739
+ await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', worktreeSubmodulePath, commit]);
1740
+ await runGit(submodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
1741
+ return true;
1742
+ };
1743
+
1744
+ const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
1745
+ const gitlinks = treeOutput
1746
+ .split('\0')
1747
+ .filter(Boolean)
1748
+ .map(record => {
1749
+ const match = /^160000\s+commit\s+([0-9a-f]{40})\t(.+)$/.exec(record);
1750
+ return match ? { commit: match[1], path: match[2] } : null;
1751
+ })
1752
+ .filter((entry): entry is { commit: string; path: string } => !!entry);
1753
+
1754
+ for (const gitlink of gitlinks) {
1755
+ const submodulePath = pathResolve(repoRoot, gitlink.path);
1756
+ const entry: MeshRefineSubmoduleReachabilityEntry = {
1757
+ path: gitlink.path,
1758
+ commit: gitlink.commit,
1759
+ reachable: false,
1760
+ };
1761
+ try {
1762
+ if (!fs.existsSync(submodulePath)) {
1763
+ entry.error = `Submodule checkout missing at ${gitlink.path}`;
1764
+ entry.publishRequired = true;
1765
+ if (options.allowAutoPublishSubmoduleMainCommits === true) {
1766
+ entry.autoPublishAllowed = true;
1767
+ entry.autoPublishAttempted = false;
1768
+ entry.autoPublishSkippedReason = `submodule checkout missing at ${gitlink.path}; cannot perform non-force push to origin/main`;
1769
+ }
1770
+ entries.push(entry);
1771
+ continue;
1772
+ }
1773
+
1774
+ entry.checkedLocal = true;
1775
+ try {
1776
+ await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
1777
+ entry.localReachable = true;
1778
+ } catch {
1779
+ entry.localReachable = false;
1780
+ if (options.allowAutoPublishSubmoduleMainCommits === true && options.worktreeRoot) {
1781
+ try {
1782
+ const imported = await importCommitFromWorktreeSubmodule(
1783
+ submodulePath,
1784
+ pathResolve(options.worktreeRoot, gitlink.path),
1785
+ gitlink.commit,
1786
+ );
1787
+ if (imported) {
1788
+ entry.localReachable = true;
1789
+ entry.importedFromWorktree = true;
1790
+ }
1791
+ } catch (importError: any) {
1792
+ 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))}`;
1793
+ }
1794
+ }
1795
+ // Probe the submodule remote before allowing cleanup/completion.
1796
+ }
1797
+
1798
+ try {
1799
+ entry.remote = 'origin';
1800
+ let remoteUrl = '';
1801
+ try {
1802
+ remoteUrl = (await runGit(submodulePath, ['remote', 'get-url', 'origin'])).trim();
1803
+ if (!remoteUrl) throw new Error('origin remote has no URL');
1804
+ entry.remoteUrl = remoteUrl;
1805
+ } catch {
1806
+ entry.error = 'Submodule remote reachability check failed: no configured origin remote';
1807
+ entry.publishRequired = true;
1808
+ if (options.allowAutoPublishSubmoduleMainCommits === true) {
1809
+ entry.autoPublishAllowed = true;
1810
+ entry.autoPublishAttempted = false;
1811
+ entry.autoPublishSkippedReason = 'submodule origin remote is not configured; cannot perform non-force push to origin/main';
1812
+ }
1813
+ entries.push(entry);
1814
+ continue;
1815
+ }
1816
+ entry.remoteMainBranch = 'main';
1817
+ try {
1818
+ await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
1819
+ entry.fetchedFromOrigin = true;
1820
+ entry.remoteReachable = true;
1821
+ entry.remoteMainReachable = true;
1822
+ entry.reachable = true;
1823
+ } catch (e: any) {
1824
+ entry.remoteReachable = false;
1825
+ entry.remoteMainReachable = false;
1826
+ entry.publishRequired = true;
1827
+ const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
1828
+ entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
1829
+ if (options.allowAutoPublishSubmoduleMainCommits === true && entry.localReachable === true) {
1830
+ entry.autoPublishAllowed = true;
1831
+ entry.autoPublishAttempted = true;
1832
+ try {
1833
+ const publish = await publishCommitToRemoteMain(submodulePath, gitlink.commit, 'main');
1834
+ entry.autoPublishRefspec = publish.refspec;
1835
+ entry.publishStdout = truncateValidationOutput(publish.stdout);
1836
+ entry.publishStderr = truncateValidationOutput(publish.stderr);
1837
+ entry.autoPublishSucceeded = true;
1838
+ await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
1839
+ entry.fetchedFromOrigin = true;
1840
+ entry.remoteReachable = true;
1841
+ entry.remoteMainReachable = true;
1842
+ entry.autoPublishVerified = true;
1843
+ entry.publishRequired = false;
1844
+ entry.reachable = true;
1845
+ entry.error = undefined;
1846
+ } catch (publishError: any) {
1847
+ entry.autoPublishSucceeded = false;
1848
+ entry.autoPublishVerified = false;
1849
+ const publishDetails = truncateValidationOutput(publishError?.stderr || publishError?.message || String(publishError));
1850
+ entry.error = `Submodule auto-publish to origin/main failed or could not be verified: ${publishDetails}`;
1851
+ }
1852
+ } else if (options.allowAutoPublishSubmoduleMainCommits === true) {
1853
+ entry.autoPublishAllowed = true;
1854
+ entry.autoPublishAttempted = false;
1855
+ entry.autoPublishSkippedReason = entry.autoPublishSkippedReason
1856
+ || 'candidate commit is not reachable in the source checkout or worktree submodule, so Refinery cannot push it to origin/main';
1857
+ }
1858
+ }
1859
+ } catch (e: any) {
1860
+ entry.remoteReachable = false;
1861
+ entry.remoteMainReachable = false;
1862
+ entry.publishRequired = true;
1863
+ const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
1864
+ entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
1865
+ }
1866
+ } catch (e: any) {
1867
+ entry.error = truncateValidationOutput(e?.message || String(e));
1868
+ entry.publishRequired = true;
1869
+ }
1870
+ entries.push(entry);
627
1871
  }
1872
+
1873
+ const unreachable = entries.filter(entry => !entry.reachable);
1874
+ return {
1875
+ status: unreachable.length ? 'failed' : 'passed',
1876
+ checked: entries.length,
1877
+ unreachable: unreachable.map(entry => ({ ...entry, publishRequired: entry.publishRequired !== false })),
1878
+ entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: entry.publishRequired !== false }),
1879
+ durationMs: Date.now() - startedAt,
1880
+ autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
1881
+ autoPublishPolicySource: options.autoPublishPolicySource,
1882
+ };
1883
+ } catch (e: any) {
1884
+ const unreachable = entries.filter(entry => !entry.reachable).map(entry => ({ ...entry, publishRequired: true }));
1885
+ return {
1886
+ status: 'failed',
1887
+ checked: entries.length,
1888
+ unreachable,
1889
+ entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: true }),
1890
+ durationMs: Date.now() - startedAt,
1891
+ autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
1892
+ autoPublishPolicySource: options.autoPublishPolicySource,
1893
+ error: truncateValidationOutput(e?.message || String(e)),
1894
+ };
628
1895
  }
1896
+ }
629
1897
 
1898
+ function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
1899
+ const plan = resolveMeshRefineValidationPlan(mesh, workspace);
1900
+ const mapCommand = (command: MeshRefineValidationCommandPlan) => ({
1901
+ displayCommand: command.displayCommand,
1902
+ category: command.category,
1903
+ source: command.source,
1904
+ cwd: command.cwd,
1905
+ timeoutMs: command.timeoutMs,
1906
+ });
630
1907
  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',
1908
+ source: plan.source,
1909
+ sourceType: plan.sourceType,
1910
+ bootstrapCommands: plan.bootstrapCommands.map(mapCommand),
1911
+ commands: plan.commands.map(mapCommand),
1912
+ unavailableReason: plan.unavailableReason,
1913
+ rejectedCommands: plan.rejectedCommands,
1914
+ suggestions: plan.suggestions,
1915
+ suggestedConfig: plan.suggestedConfig,
1916
+ note: plan.sourceType === 'unavailable'
1917
+ ? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
1918
+ : 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
640
1919
  };
641
1920
  }
642
1921
 
@@ -644,60 +1923,119 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
644
1923
  const { execFile } = await import('node:child_process');
645
1924
  const { promisify } = await import('node:util');
646
1925
  const execFileAsync = promisify(execFile);
647
- const selection = selectMeshRefineValidationCommands(mesh, workspace);
1926
+ const selection = resolveMeshRefineValidationPlan(mesh, workspace);
648
1927
  const summary: MeshRefineValidationSummary = {
649
1928
  status: 'skipped',
650
1929
  required: true,
651
1930
  commandsRun: [],
1931
+ bootstrapCommandsRun: [],
652
1932
  rejectedCommands: selection.rejectedCommands,
653
1933
  skippedReason: undefined,
654
1934
  timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
655
1935
  outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
1936
+ configSource: selection.source,
1937
+ configSourceType: selection.sourceType,
1938
+ suggestions: selection.suggestions,
1939
+ suggestedConfig: selection.suggestedConfig,
656
1940
  };
657
1941
 
658
1942
  if (!selection.commands.length) {
659
- summary.skippedReason = 'validation_unavailable: no allowlisted projectContext, mesh policy, or package.json build/test/typecheck/lint command was available';
1943
+ summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
660
1944
  return summary;
661
1945
  }
662
1946
 
663
- for (const candidate of selection.commands) {
1947
+ const commandRecord = (candidate: MeshRefineValidationCommand, cwd: string, startedAt: number, result: any, passed: boolean, extras: Record<string, unknown> = {}) => ({
1948
+ command: candidate.command,
1949
+ args: candidate.args,
1950
+ displayCommand: candidate.displayCommand,
1951
+ category: candidate.category,
1952
+ source: candidate.source,
1953
+ cwd,
1954
+ passed,
1955
+ durationMs: Date.now() - startedAt,
1956
+ stdout: truncateValidationOutput(result?.stdout),
1957
+ stderr: truncateValidationOutput(result?.stderr || result?.message),
1958
+ ...extras,
1959
+ });
1960
+ const isPackageManagerValidation = (candidate: MeshRefineValidationCommand): boolean => {
1961
+ const command = pathBasename(candidate.command).replace(/\.(?:cmd|exe)$/i, '');
1962
+ return ['npm', 'pnpm', 'yarn', 'bun'].includes(command)
1963
+ && candidate.args.some(arg => arg === 'run' || arg === 'test' || arg === 'exec');
1964
+ };
1965
+ const dependenciesLikelyMissing = (cwd: string): boolean => {
1966
+ if (!fs.existsSync(pathJoin(cwd, 'package.json'))) return false;
1967
+ if (fs.existsSync(pathJoin(cwd, 'node_modules'))) return false;
1968
+ return ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb', 'bun.lock']
1969
+ .some(lock => fs.existsSync(pathJoin(cwd, lock)));
1970
+ };
1971
+
1972
+ for (const candidate of selection.bootstrapCommands) {
664
1973
  const startedAt = Date.now();
1974
+ const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
1975
+ const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
665
1976
  try {
666
1977
  const result = await execFileAsync(candidate.command, candidate.args, {
667
- cwd: workspace,
1978
+ cwd,
668
1979
  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),
1980
+ timeout,
1981
+ maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
1982
+ env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
684
1983
  });
1984
+ summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
685
1985
  } 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,
1986
+ summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
693
1987
  exitCode: typeof error?.code === 'number' ? error.code : null,
694
1988
  signal: typeof error?.signal === 'string' ? error.signal : null,
695
1989
  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),
1990
+ failureKind: 'dependency_bootstrap_failed',
1991
+ }));
1992
+ summary.status = 'failed';
1993
+ summary.failureKind = 'dependency_bootstrap_failed';
1994
+ summary.failureCode = 'dependency_bootstrap_failed';
1995
+ return summary;
1996
+ }
1997
+ }
1998
+
1999
+ for (const candidate of selection.commands) {
2000
+ const startedAt = Date.now();
2001
+ const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
2002
+ const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
2003
+ if (selection.bootstrapCommands.length === 0 && isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd)) {
2004
+ summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, {
2005
+ 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.',
2006
+ }, false, {
2007
+ exitCode: null,
2008
+ skipped: true,
2009
+ failureKind: 'missing_dependencies',
2010
+ }));
2011
+ summary.status = 'failed';
2012
+ summary.failureKind = 'missing_dependencies';
2013
+ summary.failureCode = 'missing_dependencies';
2014
+ return summary;
2015
+ }
2016
+ try {
2017
+ const result = await execFileAsync(candidate.command, candidate.args, {
2018
+ cwd,
2019
+ encoding: 'utf8',
2020
+ timeout,
2021
+ maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
2022
+ env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
699
2023
  });
2024
+ summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
2025
+ } catch (error: any) {
2026
+ const stderr = truncateValidationOutput(error?.stderr || error?.message);
2027
+ const missingDependencyFailure = /Cannot find module|MODULE_NOT_FOUND|node_modules|command not found|not found/i.test(stderr);
2028
+ summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
2029
+ exitCode: typeof error?.code === 'number' ? error.code : null,
2030
+ signal: typeof error?.signal === 'string' ? error.signal : null,
2031
+ timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
2032
+ ...(missingDependencyFailure ? { failureKind: 'missing_dependencies' } : {}),
2033
+ }));
700
2034
  summary.status = 'failed';
2035
+ if (missingDependencyFailure) {
2036
+ summary.failureKind = 'missing_dependencies';
2037
+ summary.failureCode = 'missing_dependencies';
2038
+ }
701
2039
  return summary;
702
2040
  }
703
2041
  }
@@ -810,12 +2148,14 @@ export interface CommandRouterDeps {
810
2148
  /** Reference to detected IDEs array (mutable — router updates it) */
811
2149
  detectedIdes: { value: any[] };
812
2150
  sessionRegistry: SessionRegistry;
813
- /** Callback for CDP manager creation after launch_ide */
2151
+ /** Callback after CDP manager created (transport-specific extras) */
814
2152
  onCdpManagerCreated?: (ideType: string, manager: DaemonCdpManager) => void;
815
2153
  /** Callback after IDE connected (e.g., startAgentStreamPolling) */
816
2154
  onIdeConnected?: () => void;
817
2155
  /** Callback after status change (stop_ide, restart) */
818
2156
  onStatusChange?: () => void;
2157
+ /** Callback when a mesh state is invalidated */
2158
+ onMeshStateChange?: (meshId: string) => void;
819
2159
  /** Callback after chat-related commands */
820
2160
  onPostChatCommand?: () => void;
821
2161
  /** Get a connected CDP manager (for agent stream reset check) */
@@ -829,6 +2169,8 @@ export interface CommandRouterDeps {
829
2169
  sessionHostControl?: SessionHostControlPlane | null;
830
2170
  /** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
831
2171
  getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
2172
+ /** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
2173
+ dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
832
2174
  }
833
2175
 
834
2176
  export interface CommandRouterResult {
@@ -931,12 +2273,56 @@ function summarizeSessionHostDiagnostics(result: unknown): Record<string, unknow
931
2273
  };
932
2274
  }
933
2275
 
934
- function summarizeSessionHostPruneResult(result: unknown): Record<string, unknown> {
935
- const value = result && typeof result === 'object' ? result as Record<string, any> : {};
2276
+ function summarizeSessionHostPruneResult(result: unknown): Record<string, unknown> {
2277
+ const value = result && typeof result === 'object' ? result as Record<string, any> : {};
2278
+ return {
2279
+ duplicateGroupCount: typeof value.duplicateGroupCount === 'number' ? value.duplicateGroupCount : undefined,
2280
+ prunedCount: Array.isArray(value.prunedSessionIds) ? value.prunedSessionIds.length : undefined,
2281
+ keptCount: Array.isArray(value.keptSessionIds) ? value.keptSessionIds.length : undefined,
2282
+ };
2283
+ }
2284
+
2285
+ function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
2286
+ const raw = hostAddress.trim();
2287
+ if (!raw) throw new Error('hostAddress required');
2288
+ const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
2289
+ url.pathname = '/api/v1/command';
2290
+ url.search = '';
2291
+ url.hash = '';
2292
+ return url.toString();
2293
+ }
2294
+
2295
+ function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
2296
+ const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
2297
+ const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
2298
+ ? args.memberNode as Record<string, any>
2299
+ : null;
2300
+ const configured = Array.isArray(mesh?.nodes)
2301
+ ? (requestedNodeId
2302
+ ? mesh.nodes.find((node: any) => node?.id === requestedNodeId || node?.nodeId === requestedNodeId)
2303
+ : mesh.nodes[0])
2304
+ : null;
2305
+ const source = explicit || configured;
2306
+ const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
2307
+ ? source.workspace.trim()
2308
+ : typeof args?.workspace === 'string' && args.workspace.trim()
2309
+ ? args.workspace.trim()
2310
+ : process.cwd();
2311
+ if (!workspace) return null;
2312
+ const nodeId = typeof source?.id === 'string' && source.id.trim()
2313
+ ? source.id.trim()
2314
+ : typeof source?.nodeId === 'string' && source.nodeId.trim()
2315
+ ? source.nodeId.trim()
2316
+ : undefined;
936
2317
  return {
937
- duplicateGroupCount: typeof value.duplicateGroupCount === 'number' ? value.duplicateGroupCount : undefined,
938
- prunedCount: Array.isArray(value.prunedSessionIds) ? value.prunedSessionIds.length : undefined,
939
- keptCount: Array.isArray(value.keptSessionIds) ? value.keptSessionIds.length : undefined,
2318
+ ...(nodeId ? { id: nodeId } : {}),
2319
+ workspace,
2320
+ ...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
2321
+ ...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
2322
+ ...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
2323
+ userOverrides: source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides) ? source.userOverrides : {},
2324
+ policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
2325
+ role: 'member',
940
2326
  };
941
2327
  }
942
2328
 
@@ -946,11 +2332,160 @@ export class DaemonCommandRouter {
946
2332
  * Allows the MCP server to query mesh data via get_mesh even when
947
2333
  * the mesh doesn't exist in the local meshes.json file. */
948
2334
  private inlineMeshCache = new Map<string, any>();
2335
+ /** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
2336
+ private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any; queueRevision: string }>();
2337
+ /** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */
2338
+ private runningRefineJobs = new Map<string, MeshRefineJobHandle>();
2339
+ /** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
2340
+ private terminalRefineJobs = new Map<string, MeshRefineTerminalJob>();
949
2341
 
950
2342
  constructor(deps: CommandRouterDeps) {
951
2343
  this.deps = deps;
952
2344
  }
953
2345
 
2346
+ private cloneJsonValue<T>(value: T): T {
2347
+ if (typeof structuredClone === 'function') return structuredClone(value);
2348
+ return JSON.parse(JSON.stringify(value)) as T;
2349
+ }
2350
+
2351
+ private hydrateCachedAggregateMeshStatusFromInline(snapshot: any, mesh: any, options?: { requireDirectPeerTruth?: boolean }): any {
2352
+ if (!mesh || typeof mesh !== 'object' || !Array.isArray(mesh.nodes) || !Array.isArray(snapshot?.nodes)) return snapshot;
2353
+ const inlineNodesById = new Map<string, any>();
2354
+ for (const node of mesh.nodes) {
2355
+ const nodeId = readInlineMeshNodeId(node);
2356
+ if (nodeId) inlineNodesById.set(nodeId, node);
2357
+ }
2358
+ if (!inlineNodesById.size) return snapshot;
2359
+
2360
+ let changed = false;
2361
+ const unavailableNodeIds = new Set<string>();
2362
+ const sourceOfTruth = readObjectRecord(snapshot.sourceOfTruth);
2363
+ const directPeerTruth = readObjectRecord(sourceOfTruth.directPeerTruth);
2364
+ for (const entry of Array.isArray(directPeerTruth.unavailableNodeIds) ? directPeerTruth.unavailableNodeIds : []) {
2365
+ const nodeId = readStringValue(entry);
2366
+ if (nodeId) unavailableNodeIds.add(nodeId);
2367
+ }
2368
+
2369
+ const nodes = snapshot.nodes.map((statusNode: any) => {
2370
+ const nodeId = readStringValue(statusNode?.nodeId, statusNode?.id);
2371
+ const inlineNode = nodeId ? inlineNodesById.get(nodeId) : undefined;
2372
+ if (!inlineNode) return statusNode;
2373
+ const liveGit = buildInlineMeshTransitGitStatus(inlineNode);
2374
+ if (!liveGit) return statusNode;
2375
+ const nextStatus = { ...statusNode };
2376
+ nextStatus.git = liveGit;
2377
+ nextStatus.health = deriveMeshNodeHealthFromGit(liveGit);
2378
+ applyInlineMeshBranchConvergence(mesh, inlineNode, nextStatus);
2379
+ nextStatus.launchReady = readBooleanValue(nextStatus.launchReady) ?? true;
2380
+ const connection = readObjectRecord(nextStatus.connection);
2381
+ const connectionState = readStringValue(connection.state);
2382
+ const connectionReported = readBooleanValue(connection.reported) ?? false;
2383
+ if (!connectionReported || connectionState === 'unknown') {
2384
+ nextStatus.connection = buildLivePeerGitConnection(connection);
2385
+ }
2386
+ delete nextStatus.gitProbePending;
2387
+ const error = readStringValue(nextStatus.error);
2388
+ if (error && /pending_git|git probe|live peer git snapshot|no peer git snapshot/i.test(error)) delete nextStatus.error;
2389
+ if (!readStringValue(nextStatus.machineStatus)) nextStatus.machineStatus = 'online';
2390
+ if (nodeId) unavailableNodeIds.delete(nodeId);
2391
+ changed = true;
2392
+ return nextStatus;
2393
+ });
2394
+
2395
+ const aggregateDirectTruthSatisfied = sourceOfTruth.coordinatorOwnsLiveTruth === true
2396
+ || directPeerTruth.satisfied === true;
2397
+ if (!changed && !(options?.requireDirectPeerTruth && unavailableNodeIds.size > 0 && !aggregateDirectTruthSatisfied)) return snapshot;
2398
+ const nextSourceOfTruth = {
2399
+ ...sourceOfTruth,
2400
+ ...(Object.keys(directPeerTruth).length ? {
2401
+ directPeerTruth: {
2402
+ ...directPeerTruth,
2403
+ satisfied: options?.requireDirectPeerTruth === true
2404
+ ? aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0
2405
+ : directPeerTruth.satisfied,
2406
+ unavailableNodeIds: [...unavailableNodeIds],
2407
+ },
2408
+ ...(options?.requireDirectPeerTruth === true ? {
2409
+ coordinatorOwnsLiveTruth: aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0,
2410
+ currentStatus: aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0 ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
2411
+ } : {}),
2412
+ } : {}),
2413
+ };
2414
+ return {
2415
+ ...snapshot,
2416
+ ...(options?.requireDirectPeerTruth === true && unavailableNodeIds.size > 0 && !aggregateDirectTruthSatisfied ? {
2417
+ success: false,
2418
+ code: 'mesh_direct_peer_truth_unavailable',
2419
+ error: 'Selected coordinator could not confirm direct mesh truth for every remote node yet.',
2420
+ } : {}),
2421
+ sourceOfTruth: nextSourceOfTruth,
2422
+ branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodes),
2423
+ nodes,
2424
+ };
2425
+ }
2426
+
2427
+ private getCachedAggregateMeshStatus(meshId: string, mesh?: any, options?: { requireDirectPeerTruth?: boolean }): any | null {
2428
+ const cached = this.aggregateMeshStatusCache.get(meshId);
2429
+ if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null;
2430
+ if (cached.queueRevision !== getMeshQueueRevision(meshId)) return null;
2431
+ let snapshot = this.cloneJsonValue(cached.snapshot);
2432
+ snapshot = this.hydrateCachedAggregateMeshStatusFromInline(snapshot, mesh, options);
2433
+ if (shouldRefreshStalePendingAggregate(snapshot, options)) return null;
2434
+ const ageMs = Math.max(0, Date.now() - cached.builtAt);
2435
+ const sourceOfTruth = snapshot.sourceOfTruth && typeof snapshot.sourceOfTruth === 'object'
2436
+ ? snapshot.sourceOfTruth
2437
+ : {};
2438
+ snapshot.sourceOfTruth = {
2439
+ ...sourceOfTruth,
2440
+ aggregateSnapshot: {
2441
+ ...(sourceOfTruth.aggregateSnapshot && typeof sourceOfTruth.aggregateSnapshot === 'object'
2442
+ ? sourceOfTruth.aggregateSnapshot
2443
+ : {}),
2444
+ owner: 'coordinator_daemon_memory',
2445
+ cached: true,
2446
+ source: 'memory',
2447
+ refreshReason: 'memory_cache_hit',
2448
+ ageMs,
2449
+ cachedAt: new Date(cached.builtAt).toISOString(),
2450
+ returnedAt: new Date().toISOString(),
2451
+ },
2452
+ };
2453
+ return snapshot;
2454
+ }
2455
+
2456
+ private rememberAggregateMeshStatus(meshId: string, snapshot: any, refreshReason: string): any {
2457
+ if (!snapshot || typeof snapshot !== 'object' || snapshot.success !== true || !Array.isArray(snapshot.nodes)) return snapshot;
2458
+ const builtAt = Date.now();
2459
+ const next = this.cloneJsonValue(snapshot);
2460
+ const sourceOfTruth = next.sourceOfTruth && typeof next.sourceOfTruth === 'object'
2461
+ ? next.sourceOfTruth
2462
+ : {};
2463
+ next.sourceOfTruth = {
2464
+ ...sourceOfTruth,
2465
+ aggregateSnapshot: {
2466
+ owner: 'coordinator_daemon_memory',
2467
+ cached: false,
2468
+ source: 'live_refresh',
2469
+ refreshReason,
2470
+ ageMs: 0,
2471
+ cachedAt: new Date(builtAt).toISOString(),
2472
+ returnedAt: new Date(builtAt).toISOString(),
2473
+ },
2474
+ };
2475
+ this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next), queueRevision: getMeshQueueRevision(meshId) });
2476
+ return next;
2477
+ }
2478
+
2479
+ public getCachedInlineMeshNodes(): any[] {
2480
+ const nodes: any[] = [];
2481
+ for (const mesh of this.inlineMeshCache.values()) {
2482
+ if (Array.isArray(mesh?.nodes)) {
2483
+ nodes.push(...mesh.nodes);
2484
+ }
2485
+ }
2486
+ return nodes;
2487
+ }
2488
+
954
2489
  public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
955
2490
  if (inlineMesh && typeof inlineMesh === 'object') {
956
2491
  return this.warmInlineMeshCache(meshId, inlineMesh);
@@ -960,10 +2495,15 @@ export class DaemonCommandRouter {
960
2495
 
961
2496
  private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
962
2497
  if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
2498
+ const sanitizedInlineMesh = sanitizeInlineMesh(inlineMesh as any);
963
2499
  const cached = this.inlineMeshCache.get(meshId);
964
- if (cached) return cached;
965
- this.inlineMeshCache.set(meshId, inlineMesh as any);
966
- return inlineMesh as any;
2500
+ if (cached) {
2501
+ const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
2502
+ this.inlineMeshCache.set(meshId, merged);
2503
+ return merged;
2504
+ }
2505
+ this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
2506
+ return sanitizedInlineMesh as any;
967
2507
  }
968
2508
 
969
2509
  private async getMeshForCommand(
@@ -974,9 +2514,18 @@ export class DaemonCommandRouter {
974
2514
  const preferInline = options?.preferInline === true;
975
2515
  if (preferInline) {
976
2516
  const cached = this.getCachedInlineMesh(meshId);
977
- if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
978
- const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
979
- if (warmedInline) return { mesh: warmedInline, inline: true, source: 'inline_bootstrap' };
2517
+ if (cached) {
2518
+ if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
2519
+ const merged = reconcileInlineMeshCache(cached, inlineMesh as any);
2520
+ this.inlineMeshCache.set(meshId, sanitizeInlineMesh(merged));
2521
+ return { mesh: merged, inline: true, source: 'inline_cache' };
2522
+ }
2523
+ return { mesh: cached, inline: true, source: 'inline_cache' };
2524
+ }
2525
+ if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
2526
+ this.warmInlineMeshCache(meshId, inlineMesh);
2527
+ return { mesh: inlineMesh, inline: true, source: 'inline_bootstrap' };
2528
+ }
980
2529
  }
981
2530
  try {
982
2531
  const { getMesh } = await import('../config/mesh-config.js');
@@ -989,6 +2538,23 @@ export class DaemonCommandRouter {
989
2538
  return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
990
2539
  }
991
2540
 
2541
+ private invalidateAggregateMeshStatus(meshId: string): void {
2542
+ this.aggregateMeshStatusCache.delete(meshId);
2543
+ this.deps.onMeshStateChange?.(meshId);
2544
+ }
2545
+
2546
+
2547
+ private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
2548
+ const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
2549
+ const mesh = meshRecord?.mesh;
2550
+ if (!mesh) return { success: false, error: 'Mesh not found' };
2551
+ const meshHost = resolveMeshHostStatus(mesh);
2552
+ if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
2553
+ return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
2554
+ }
2555
+ return null;
2556
+ }
2557
+
992
2558
  private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
993
2559
  if (!mesh || !Array.isArray(mesh.nodes) || !node?.id) return;
994
2560
  const idx = mesh.nodes.findIndex((entry: any) => entry?.id === node.id || entry?.nodeId === node.id);
@@ -996,6 +2562,7 @@ export class DaemonCommandRouter {
996
2562
  else mesh.nodes.push(node);
997
2563
  mesh.updatedAt = new Date().toISOString();
998
2564
  this.inlineMeshCache.set(meshId, mesh);
2565
+ this.invalidateAggregateMeshStatus(meshId);
999
2566
  }
1000
2567
 
1001
2568
  private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
@@ -1005,6 +2572,7 @@ export class DaemonCommandRouter {
1005
2572
  mesh.nodes.splice(idx, 1);
1006
2573
  mesh.updatedAt = new Date().toISOString();
1007
2574
  this.inlineMeshCache.set(meshId, mesh);
2575
+ this.invalidateAggregateMeshStatus(meshId);
1008
2576
  return true;
1009
2577
  }
1010
2578
 
@@ -1534,6 +3102,567 @@ export class DaemonCommandRouter {
1534
3102
  }
1535
3103
  }
1536
3104
 
3105
+
3106
+ private buildRefineJobKey(meshId: string, nodeId: string): string {
3107
+ return `${meshId}:${nodeId}`;
3108
+ }
3109
+
3110
+ private buildRefineJobHandle(args: {
3111
+ meshId: string;
3112
+ nodeId: string;
3113
+ node?: any;
3114
+ status?: MeshRefineAsyncJobStatus;
3115
+ startedAt?: string;
3116
+ completedAt?: string;
3117
+ jobId?: string;
3118
+ interactionId?: string;
3119
+ retryOfJobId?: string;
3120
+ }): MeshRefineJobHandle {
3121
+ return {
3122
+ success: true,
3123
+ async: true,
3124
+ status: args.status || 'accepted',
3125
+ jobId: args.jobId || `refine_${createInteractionId()}`,
3126
+ interactionId: args.interactionId || createInteractionId(),
3127
+ meshId: args.meshId,
3128
+ nodeId: args.nodeId,
3129
+ targetNodeId: args.nodeId,
3130
+ targetDaemonId: readStringValue(args.node?.daemonId),
3131
+ workspace: readStringValue(args.node?.workspace),
3132
+ startedAt: args.startedAt || new Date().toISOString(),
3133
+ ...(args.completedAt ? { completedAt: args.completedAt } : {}),
3134
+ ...(args.retryOfJobId ? { retryOfJobId: args.retryOfJobId } : {}),
3135
+ eventDelivery: { pendingEvents: true, ledger: true },
3136
+ evidence: {
3137
+ pendingEventsCommand: 'get_pending_mesh_events',
3138
+ ledgerCommand: 'get_mesh_ledger_slice',
3139
+ taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
3140
+ },
3141
+ };
3142
+ }
3143
+
3144
+ private queueRefineJobEvent(event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
3145
+ const metadataEvent = {
3146
+ source: 'refine_mesh_node_async_job',
3147
+ jobId: handle.jobId,
3148
+ interactionId: handle.interactionId,
3149
+ meshId: handle.meshId,
3150
+ nodeId: handle.targetNodeId,
3151
+ targetDaemonId: handle.targetDaemonId,
3152
+ workspace: handle.workspace,
3153
+ status: handle.status,
3154
+ startedAt: handle.startedAt,
3155
+ completedAt: handle.completedAt,
3156
+ retryOfJobId: handle.retryOfJobId,
3157
+ ...(result ? { result } : {}),
3158
+ };
3159
+ const eventPayload = {
3160
+ event,
3161
+ meshId: handle.meshId,
3162
+ nodeLabel: handle.targetNodeId,
3163
+ nodeId: handle.targetNodeId,
3164
+ workspace: handle.workspace,
3165
+ metadataEvent,
3166
+ queuedAt: Date.now(),
3167
+ };
3168
+ if (typeof this.deps.instanceManager?.getByCategory === 'function') {
3169
+ const forwarded = handleMeshForwardEvent(
3170
+ { instanceManager: this.deps.instanceManager } as any,
3171
+ {
3172
+ event,
3173
+ meshId: handle.meshId,
3174
+ nodeId: handle.targetNodeId,
3175
+ workspace: handle.workspace,
3176
+ jobId: handle.jobId,
3177
+ interactionId: handle.interactionId,
3178
+ status: handle.status,
3179
+ targetDaemonId: handle.targetDaemonId,
3180
+ startedAt: handle.startedAt,
3181
+ completedAt: handle.completedAt,
3182
+ retryOfJobId: handle.retryOfJobId,
3183
+ ...(result ? { result } : {}),
3184
+ },
3185
+ );
3186
+ if (forwarded?.success === true) return;
3187
+ LOG.warn('Mesh', `[Refinery] Failed to forward async refine event ${event}: ${forwarded?.error || 'unknown error'}`);
3188
+ }
3189
+ queuePendingMeshCoordinatorEvent(eventPayload);
3190
+ }
3191
+
3192
+ private async appendRefineJobLedger(kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
3193
+ try {
3194
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
3195
+ appendLedgerEntry(handle.meshId, {
3196
+ kind,
3197
+ nodeId: handle.targetNodeId,
3198
+ payload: {
3199
+ source: 'refine_mesh_node_async_job',
3200
+ refineJob: {
3201
+ jobId: handle.jobId,
3202
+ interactionId: handle.interactionId,
3203
+ status: handle.status,
3204
+ meshId: handle.meshId,
3205
+ nodeId: handle.targetNodeId,
3206
+ targetDaemonId: handle.targetDaemonId,
3207
+ workspace: handle.workspace,
3208
+ startedAt: handle.startedAt,
3209
+ completedAt: handle.completedAt,
3210
+ retryOfJobId: handle.retryOfJobId,
3211
+ },
3212
+ async: true,
3213
+ retryOfJobId: handle.retryOfJobId,
3214
+ ...(result ? {
3215
+ success: result.success === true,
3216
+ result,
3217
+ finalBranchConvergenceState: result.finalBranchConvergenceState,
3218
+ } : {}),
3219
+ },
3220
+ });
3221
+ } catch (e: any) {
3222
+ LOG.warn('Mesh', `[Refinery] Failed to append async refine ledger entry: ${e?.message || e}`);
3223
+ }
3224
+ }
3225
+
3226
+ private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
3227
+ const refineStages: Array<Record<string, unknown>> = [];
3228
+ try {
3229
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
3230
+ const mesh = meshRecord?.mesh;
3231
+ const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
3232
+ if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
3233
+
3234
+ if (!node.isLocalWorktree || !node.workspace) {
3235
+ return { success: false, error: `Refinery requires a local worktree node`, refineStages };
3236
+ }
3237
+
3238
+ const sourceNode = node.clonedFromNodeId
3239
+ ? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
3240
+ : mesh?.nodes.find((n: any) => !n.isLocalWorktree);
3241
+ const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
3242
+ if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
3243
+
3244
+ const { execFile } = await import('node:child_process');
3245
+ const { promisify } = await import('node:util');
3246
+ const execFileAsync = promisify(execFile);
3247
+
3248
+ const resolveStarted = Date.now();
3249
+ const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
3250
+ const branch = branchStdout.trim();
3251
+ if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
3252
+
3253
+ const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
3254
+ const baseBranch = baseBranchStdout.trim();
3255
+ const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
3256
+ const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
3257
+ const baseHead = baseHeadStdout.trim();
3258
+ const branchHead = branchHeadStdout.trim();
3259
+ recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
3260
+
3261
+ const validationStarted = Date.now();
3262
+ const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
3263
+ recordMeshRefineStage(
3264
+ refineStages,
3265
+ 'validation',
3266
+ validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
3267
+ validationStarted,
3268
+ { validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
3269
+ );
3270
+ if (validationSummary.status === 'failed') {
3271
+ return {
3272
+ success: false,
3273
+ code: validationSummary.failureCode || 'validation_failed',
3274
+ convergenceStatus: 'blocked_review',
3275
+ error: validationSummary.failureCode === 'missing_dependencies'
3276
+ ? 'Refinery validation dependencies are missing; merge/refine was not attempted. Configure validation.bootstrapCommands if Refinery should bootstrap dependencies before validation.'
3277
+ : validationSummary.failureCode === 'dependency_bootstrap_failed'
3278
+ ? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
3279
+ : 'Refinery validation gate failed; merge/refine was not attempted.',
3280
+ branch,
3281
+ into: baseBranch,
3282
+ validationSummary,
3283
+ refineStages,
3284
+ finalBranchConvergenceState: {
3285
+ branch,
3286
+ baseBranch,
3287
+ merged: false,
3288
+ removed: false,
3289
+ validation: 'failed',
3290
+ status: 'blocked_review',
3291
+ },
3292
+ };
3293
+ }
3294
+ if (validationSummary.status === 'skipped') {
3295
+ return {
3296
+ success: false,
3297
+ code: 'validation_unavailable',
3298
+ convergenceStatus: 'blocked_review',
3299
+ error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
3300
+ branch,
3301
+ into: baseBranch,
3302
+ validationSummary,
3303
+ refineStages,
3304
+ finalBranchConvergenceState: {
3305
+ branch,
3306
+ baseBranch,
3307
+ merged: false,
3308
+ removed: false,
3309
+ validation: 'unavailable',
3310
+ status: 'blocked_review',
3311
+ },
3312
+ };
3313
+ }
3314
+
3315
+ const patchEquivalenceStarted = Date.now();
3316
+ const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
3317
+ recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
3318
+ equivalent: patchEquivalence.equivalent,
3319
+ expectedPatchId: patchEquivalence.expectedPatchId,
3320
+ actualPatchId: patchEquivalence.actualPatchId,
3321
+ error: patchEquivalence.error,
3322
+ actionableHint: patchEquivalence.actionableHint,
3323
+ });
3324
+ if (!patchEquivalence.equivalent) {
3325
+ return {
3326
+ success: false,
3327
+ code: 'patch_equivalence_failed',
3328
+ convergenceStatus: 'blocked_review',
3329
+ error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
3330
+ branch,
3331
+ into: baseBranch,
3332
+ validationSummary,
3333
+ patchEquivalence,
3334
+ refineStages,
3335
+ finalBranchConvergenceState: {
3336
+ branch,
3337
+ baseBranch,
3338
+ merged: false,
3339
+ removed: false,
3340
+ validation: 'passed',
3341
+ patchEquivalence: 'failed',
3342
+ status: 'blocked_review',
3343
+ },
3344
+ };
3345
+ }
3346
+
3347
+ const submoduleReachabilityStarted = Date.now();
3348
+ const autoPublishSubmoduleMainCommits = resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace);
3349
+ const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
3350
+ allowAutoPublishSubmoduleMainCommits: autoPublishSubmoduleMainCommits.enabled,
3351
+ autoPublishPolicySource: autoPublishSubmoduleMainCommits.source,
3352
+ worktreeRoot: node.workspace,
3353
+ });
3354
+ recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
3355
+ checked: submoduleReachability.checked,
3356
+ autoPublishAllowed: submoduleReachability.autoPublishAllowed,
3357
+ autoPublishPolicySource: submoduleReachability.autoPublishPolicySource,
3358
+ autoPublished: submoduleReachability.entries
3359
+ .filter(entry => entry.autoPublishAttempted)
3360
+ .map(entry => ({
3361
+ path: entry.path,
3362
+ commit: entry.commit,
3363
+ remote: entry.remote,
3364
+ remoteUrl: entry.remoteUrl,
3365
+ remoteMainBranch: entry.remoteMainBranch,
3366
+ refspec: entry.autoPublishRefspec,
3367
+ succeeded: entry.autoPublishSucceeded,
3368
+ verified: entry.autoPublishVerified,
3369
+ remoteMainReachable: entry.remoteMainReachable,
3370
+ error: entry.error,
3371
+ })),
3372
+ autoPublishSkipped: submoduleReachability.entries
3373
+ .filter(entry => entry.autoPublishAllowed === true && entry.autoPublishAttempted !== true)
3374
+ .map(entry => ({
3375
+ path: entry.path,
3376
+ commit: entry.commit,
3377
+ remote: entry.remote,
3378
+ remoteUrl: entry.remoteUrl,
3379
+ remoteMainBranch: entry.remoteMainBranch,
3380
+ reason: entry.autoPublishSkippedReason || entry.error || 'auto-publish was allowed but no publish attempt was possible',
3381
+ })),
3382
+ unreachable: submoduleReachability.unreachable.map(entry => ({
3383
+ path: entry.path,
3384
+ commit: entry.commit,
3385
+ publishRequired: entry.publishRequired === true,
3386
+ autoPublishAllowed: entry.autoPublishAllowed,
3387
+ autoPublishAttempted: entry.autoPublishAttempted,
3388
+ autoPublishSucceeded: entry.autoPublishSucceeded,
3389
+ autoPublishVerified: entry.autoPublishVerified,
3390
+ autoPublishRefspec: entry.autoPublishRefspec,
3391
+ autoPublishSkippedReason: entry.autoPublishSkippedReason,
3392
+ remote: entry.remote,
3393
+ remoteUrl: entry.remoteUrl,
3394
+ remoteReachable: entry.remoteReachable,
3395
+ remoteMainBranch: entry.remoteMainBranch,
3396
+ remoteMainReachable: entry.remoteMainReachable,
3397
+ error: entry.error,
3398
+ })),
3399
+ error: submoduleReachability.error,
3400
+ });
3401
+ if (submoduleReachability.status === 'failed') {
3402
+ const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
3403
+ return {
3404
+ success: false,
3405
+ code: 'submodule_reachability_failed',
3406
+ convergenceStatus: 'blocked_review',
3407
+ publishRequired: true,
3408
+ blockedReason: 'submodule_publish_required',
3409
+ 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.',
3410
+ nextStep,
3411
+ nextSteps: [
3412
+ 'Ask the user for explicit approval before pushing or publishing any submodule commit.',
3413
+ 'Push/publish each unreachable submodule commit to the configured submodule remote main branch shown in the evidence.',
3414
+ 'Rerun mesh_refine_node after remote reachability is confirmed.',
3415
+ 'Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.',
3416
+ ],
3417
+ unreachableSubmoduleCommits: submoduleReachability.unreachable.map(entry => ({
3418
+ path: entry.path,
3419
+ commit: entry.commit,
3420
+ remote: entry.remote,
3421
+ remoteUrl: entry.remoteUrl,
3422
+ remoteReachable: entry.remoteReachable,
3423
+ remoteMainBranch: entry.remoteMainBranch,
3424
+ remoteMainReachable: entry.remoteMainReachable,
3425
+ autoPublishAllowed: entry.autoPublishAllowed,
3426
+ autoPublishAttempted: entry.autoPublishAttempted,
3427
+ autoPublishSucceeded: entry.autoPublishSucceeded,
3428
+ autoPublishVerified: entry.autoPublishVerified,
3429
+ autoPublishRefspec: entry.autoPublishRefspec,
3430
+ autoPublishSkippedReason: entry.autoPublishSkippedReason,
3431
+ error: entry.error,
3432
+ })),
3433
+ branch,
3434
+ into: baseBranch,
3435
+ validationSummary,
3436
+ patchEquivalence,
3437
+ submoduleReachability,
3438
+ refineStages,
3439
+ finalBranchConvergenceState: {
3440
+ branch,
3441
+ baseBranch,
3442
+ merged: false,
3443
+ removed: false,
3444
+ validation: 'passed',
3445
+ patchEquivalence: 'passed',
3446
+ submoduleReachability: 'failed',
3447
+ status: 'blocked_review',
3448
+ reason: 'submodule_publish_required',
3449
+ nextStep,
3450
+ },
3451
+ };
3452
+ }
3453
+
3454
+ let mergeResult: Record<string, unknown> | undefined;
3455
+ const mergeStarted = Date.now();
3456
+ try {
3457
+ const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
3458
+ mergeResult = {
3459
+ stdout: truncateValidationOutput(result.stdout),
3460
+ stderr: truncateValidationOutput(result.stderr),
3461
+ durationMs: Date.now() - mergeStarted,
3462
+ };
3463
+ recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
3464
+ } catch (e: any) {
3465
+ recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
3466
+ error: e?.message || String(e),
3467
+ stdout: truncateValidationOutput(e?.stdout),
3468
+ stderr: truncateValidationOutput(e?.stderr),
3469
+ });
3470
+ return {
3471
+ success: false,
3472
+ error: `Merge failed (conflicts?): ${e.message}`,
3473
+ validationSummary,
3474
+ patchEquivalence,
3475
+ refineStages,
3476
+ finalBranchConvergenceState: {
3477
+ branch,
3478
+ baseBranch,
3479
+ merged: false,
3480
+ removed: false,
3481
+ validation: 'passed',
3482
+ patchEquivalence: 'passed',
3483
+ status: 'not_mergeable',
3484
+ },
3485
+ };
3486
+ }
3487
+
3488
+ const submoduleAlignmentStarted = Date.now();
3489
+ const submoduleAlignment = await alignRefinerySubmodulesAfterMerge(repoRoot, baseHead, 'HEAD', {
3490
+ submoduleIgnorePaths: Array.isArray(sourceNode?.policy?.submoduleIgnorePaths)
3491
+ ? sourceNode.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
3492
+ : undefined,
3493
+ });
3494
+ if (submoduleAlignment.status !== 'skipped') {
3495
+ recordMeshRefineStage(refineStages, 'submodule_alignment', submoduleAlignment.status, submoduleAlignmentStarted, {
3496
+ changedGitlinkPaths: submoduleAlignment.changedGitlinkPaths,
3497
+ outOfSyncPaths: submoduleAlignment.outOfSyncPaths,
3498
+ updatedPaths: submoduleAlignment.updatedPaths,
3499
+ verifiedPaths: submoduleAlignment.verifiedPaths,
3500
+ command: submoduleAlignment.command,
3501
+ error: submoduleAlignment.error,
3502
+ });
3503
+ }
3504
+ if (submoduleAlignment.status === 'failed') {
3505
+ return {
3506
+ success: false,
3507
+ code: 'post_merge_submodule_alignment_failed',
3508
+ error: 'Refinery merge completed but post-merge submodule checkout alignment failed; run the reported git submodule update command and re-check base workspace status.',
3509
+ merged: true,
3510
+ branch,
3511
+ into: baseBranch,
3512
+ validationSummary,
3513
+ patchEquivalence,
3514
+ submoduleReachability,
3515
+ submoduleAlignment,
3516
+ mergeResult,
3517
+ refineStages,
3518
+ finalBranchConvergenceState: {
3519
+ branch: baseBranch,
3520
+ mergedBranch: branch,
3521
+ baseBranch,
3522
+ merged: true,
3523
+ removed: false,
3524
+ validation: 'passed',
3525
+ patchEquivalence: 'passed',
3526
+ submoduleReachability: 'passed',
3527
+ submoduleAlignment: 'failed',
3528
+ status: 'post_merge_alignment_failed',
3529
+ nextStep: submoduleAlignment.command || 'Run git submodule update --init --recursive for the reported path(s), then re-check base workspace status.',
3530
+ },
3531
+ };
3532
+ }
3533
+
3534
+ const cleanupStarted = Date.now();
3535
+ const removeResult = await this.execute('remove_mesh_node', {
3536
+ meshId,
3537
+ nodeId,
3538
+ sessionCleanupMode: 'preserve',
3539
+ inlineMesh: args?.inlineMesh,
3540
+ });
3541
+ recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
3542
+ removed: removeResult?.removed,
3543
+ code: removeResult?.code,
3544
+ error: removeResult?.error,
3545
+ });
3546
+
3547
+ let ledgerError: string | undefined;
3548
+ const ledgerStarted = Date.now();
3549
+ try {
3550
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
3551
+ appendLedgerEntry(meshId, {
3552
+ kind: 'node_removed',
3553
+ nodeId,
3554
+ payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence, submoduleReachability, submoduleAlignment },
3555
+ });
3556
+ recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
3557
+ } catch (e: any) {
3558
+ ledgerError = e?.message || String(e);
3559
+ recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
3560
+ }
3561
+
3562
+ const finalBranchConvergenceState = {
3563
+ branch: baseBranch,
3564
+ mergedBranch: branch,
3565
+ baseBranch,
3566
+ merged: true,
3567
+ removed: removeResult?.success !== false,
3568
+ validation: 'passed',
3569
+ patchEquivalence: 'passed',
3570
+ submoduleAlignment: submoduleAlignment.status,
3571
+ status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
3572
+ };
3573
+
3574
+ if (removeResult?.success === false) {
3575
+ return {
3576
+ success: false,
3577
+ code: 'cleanup_failed',
3578
+ error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
3579
+ merged: true,
3580
+ branch,
3581
+ into: baseBranch,
3582
+ removeResult,
3583
+ validationSummary,
3584
+ patchEquivalence,
3585
+ submoduleReachability,
3586
+ submoduleAlignment,
3587
+ mergeResult,
3588
+ refineStages,
3589
+ ...(ledgerError ? { ledgerError } : {}),
3590
+ finalBranchConvergenceState,
3591
+ };
3592
+ }
3593
+
3594
+ return {
3595
+ success: true,
3596
+ merged: true,
3597
+ branch,
3598
+ into: baseBranch,
3599
+ removeResult,
3600
+ validationSummary,
3601
+ patchEquivalence,
3602
+ submoduleReachability,
3603
+ submoduleAlignment,
3604
+ mergeResult,
3605
+ refineStages,
3606
+ ...(ledgerError ? { ledgerError } : {}),
3607
+ finalBranchConvergenceState,
3608
+ };
3609
+ } catch (e: any) {
3610
+ return { success: false, error: e.message, refineStages };
3611
+ }
3612
+ }
3613
+
3614
+ private async finishMeshRefineJob(handle: MeshRefineJobHandle, args: any): Promise<void> {
3615
+ const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
3616
+ let result: Record<string, unknown>;
3617
+ try {
3618
+ result = await this.executeMeshRefineNodeSynchronously(handle.meshId, handle.targetNodeId, args) as Record<string, unknown>;
3619
+ } catch (e: any) {
3620
+ result = { success: false, error: e?.message || String(e) };
3621
+ }
3622
+ const completedAt = new Date().toISOString();
3623
+ const terminalHandle = this.buildRefineJobHandle({
3624
+ meshId: handle.meshId,
3625
+ nodeId: handle.targetNodeId,
3626
+ status: result.success === true ? 'completed' : 'failed',
3627
+ startedAt: handle.startedAt,
3628
+ completedAt,
3629
+ jobId: handle.jobId,
3630
+ interactionId: handle.interactionId,
3631
+ retryOfJobId: handle.retryOfJobId,
3632
+ node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
3633
+ });
3634
+ const terminal: MeshRefineTerminalJob = { ...terminalHandle, result };
3635
+ this.terminalRefineJobs.set(key, terminal);
3636
+ this.runningRefineJobs.delete(key);
3637
+ this.invalidateAggregateMeshStatus(handle.meshId);
3638
+ await this.appendRefineJobLedger(result.success === true ? 'task_completed' : 'task_failed', terminalHandle, result);
3639
+ this.queueRefineJobEvent(result.success === true ? 'refine:completed' : 'refine:failed', terminalHandle, result);
3640
+ }
3641
+
3642
+ private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
3643
+ const key = this.buildRefineJobKey(meshId, nodeId);
3644
+ const running = this.runningRefineJobs.get(key);
3645
+ if (running) return { ...running, duplicate: true };
3646
+ const terminal = this.terminalRefineJobs.get(key);
3647
+
3648
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
3649
+ const mesh = meshRecord?.mesh;
3650
+ const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
3651
+ if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
3652
+ if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
3653
+
3654
+ const handle = this.buildRefineJobHandle({ meshId, nodeId, node, retryOfJobId: terminal?.jobId });
3655
+ this.runningRefineJobs.set(key, handle);
3656
+ await this.appendRefineJobLedger('task_dispatched', handle);
3657
+ this.queueRefineJobEvent('refine:accepted', handle);
3658
+
3659
+ setImmediate(() => {
3660
+ void this.finishMeshRefineJob(handle, args);
3661
+ });
3662
+
3663
+ return handle;
3664
+ }
3665
+
1537
3666
  // ─── Daemon-level command core ───────────────────
1538
3667
 
1539
3668
  /**
@@ -1548,13 +3677,35 @@ export class DaemonCommandRouter {
1548
3677
  }
1549
3678
 
1550
3679
  case 'get_pending_mesh_events': {
1551
- const events = drainPendingMeshCoordinatorEvents();
3680
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
3681
+ // (B3) Respect coordinatorDaemonId when the caller declares it
3682
+ // so unicast events route to the right coordinator instead of
3683
+ // being silently consumed by the first drainer.
3684
+ const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
3685
+ ? args.coordinatorDaemonId.trim()
3686
+ : undefined;
3687
+ const events = drainPendingMeshCoordinatorEvents(meshId || undefined, coordinatorDaemonId);
1552
3688
  return { success: true, events };
1553
3689
  }
1554
3690
 
3691
+ case 'interactive_prompt_response': {
3692
+ const sessionId = typeof args?.targetSessionId === 'string' && args.targetSessionId.trim()
3693
+ ? args.targetSessionId.trim()
3694
+ : typeof args?.sessionId === 'string' && args.sessionId.trim()
3695
+ ? args.sessionId.trim()
3696
+ : '';
3697
+ if (!sessionId) return { success: false, error: 'targetSessionId required' };
3698
+ const response = normalizeInteractivePromptResponse(args?.response ?? args);
3699
+ const instance = this.deps.instanceManager.getInstance(sessionId);
3700
+ if (!instance) return { success: false, error: `No running instance for session ${sessionId}` };
3701
+ this.deps.instanceManager.sendEvent(sessionId, 'interactive_prompt_response', response);
3702
+ return { success: true };
3703
+ }
3704
+
1555
3705
  case 'launch_cli':
1556
3706
  case 'stop_cli':
1557
3707
  case 'set_cli_view_mode':
3708
+ case 'record_provider_pty':
1558
3709
  case 'agent_command': {
1559
3710
  return this.deps.cliManager.handleCliCommand(cmd, args);
1560
3711
  }
@@ -1750,7 +3901,7 @@ export class DaemonCommandRouter {
1750
3901
  : '';
1751
3902
  const providerMeta = this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType);
1752
3903
  const { sessions: historySessions, hasMore, source } = listProviderHistorySessions(providerType, {
1753
- canonicalHistory: providerMeta?.canonicalHistory,
3904
+ canonicalHistory: providerMeta?.nativeHistory,
1754
3905
  offset,
1755
3906
  limit,
1756
3907
  historyBehavior: providerMeta?.historyBehavior,
@@ -1972,6 +4123,151 @@ export class DaemonCommandRouter {
1972
4123
  };
1973
4124
  }
1974
4125
 
4126
+ // Session-info popup data. Aggregates whatever the daemon knows
4127
+ // about a single live session into one envelope so the dashboard
4128
+ // doesn't need to stitch together status + coordinator registry +
4129
+ // session registry on the client. Includes the actual system
4130
+ // prompt that was injected at launch when the session is a mesh
4131
+ // coordinator — that's the "what prompt did the agent see?"
4132
+ // question the info-icon dialog is meant to answer.
4133
+ case 'get_session_info': {
4134
+ const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
4135
+ : typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
4136
+ if (!sessionId) return { success: false, error: 'targetSessionId required' };
4137
+ // Fetch both lookups up front. We used to bail with "Session not
4138
+ // found" when sessionRegistry forgot the SID (auto-cleanup,
4139
+ // daemon restart with the session not yet restored, etc), which
4140
+ // hid the coordinator-side metadata even though the
4141
+ // coordinator-registry still has it. Now we return whichever
4142
+ // side we have. The dashboard renders "no coordinator-specific
4143
+ // prompt" only when *neither* side knows the session.
4144
+ const target = this.deps.sessionRegistry.get(sessionId);
4145
+ const coord = getCoordinatorForSession(sessionId);
4146
+ if (!target && !coord) return { success: false, error: 'Session not found', sessionId };
4147
+ const adapter = target
4148
+ ? this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter
4149
+ : undefined;
4150
+ const runtimeMeta = (adapter && typeof (adapter as any).getRuntimeMetadata === 'function')
4151
+ ? (adapter as any).getRuntimeMetadata()
4152
+ : undefined;
4153
+ const providerType = target?.providerType || coord?.cliType || '';
4154
+ const providerMetaForSession = providerType
4155
+ ? this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType)
4156
+ : undefined;
4157
+ return {
4158
+ success: true,
4159
+ session: {
4160
+ sessionId,
4161
+ providerType,
4162
+ providerName: providerMetaForSession?.name,
4163
+ transport: target?.transport,
4164
+ workspace: (target as any)?.workspace || coord?.workspace,
4165
+ spawnedAtMs: (target as any)?.spawnedAtMs || coord?.startedAt,
4166
+ providerSessionId: (target as any)?.providerSessionId,
4167
+ runtimeMetadata: runtimeMeta,
4168
+ },
4169
+ coordinator: coord ? {
4170
+ meshId: coord.meshId,
4171
+ startedAt: coord.startedAt,
4172
+ cliType: coord.cliType,
4173
+ systemPrompt: coord.systemPrompt,
4174
+ extraSystemPrompt: coord.extraSystemPrompt,
4175
+ injection: coord.injection,
4176
+ mcpConfigPath: coord.mcpConfigPath,
4177
+ } : null,
4178
+ };
4179
+ }
4180
+
4181
+ case 'get_spec_debug': {
4182
+ const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
4183
+ : typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
4184
+ if (!sessionId) return { success: false, error: 'targetSessionId required' };
4185
+ const target = this.deps.sessionRegistry.get(sessionId);
4186
+ if (!target) return { success: false, error: 'Session not found', sessionId };
4187
+ const adapterObj = this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter;
4188
+ const snapshot = adapterObj
4189
+ ? (typeof (adapterObj as any).getDebugSnapshot === 'function'
4190
+ ? (adapterObj as any).getDebugSnapshot()
4191
+ : typeof (adapterObj as any).getDebugState === 'function'
4192
+ ? (adapterObj as any).getDebugState()
4193
+ : null)
4194
+ : null;
4195
+ return {
4196
+ success: true,
4197
+ sessionId,
4198
+ providerType: target.providerType,
4199
+ isSpecProvider: snapshot !== null,
4200
+ snapshot,
4201
+ };
4202
+ }
4203
+
4204
+ // ── User-level coordinator-prompt files (~/.adhdev/coordinator-prompts/).
4205
+ // These live on this daemon's filesystem and never sync to the
4206
+ // cloud / other daemons — they're per-machine config. The
4207
+ // Settings page in the dashboard reads/writes via these two
4208
+ // commands instead of going through fs from the browser.
4209
+ case 'list_coordinator_prompts': {
4210
+ const fs = await import('node:fs');
4211
+ const path = await import('node:path');
4212
+ const os = await import('node:os');
4213
+ const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
4214
+ const entries: Record<string, { override: string; append: string }> = {};
4215
+ try {
4216
+ if (fs.existsSync(dir)) {
4217
+ for (const name of fs.readdirSync(dir)) {
4218
+ // Bucket files into <key>.{md|append.md}; ignore others
4219
+ // so a stray README or .DS_Store doesn't show up.
4220
+ const matchOverride = name.match(/^([a-zA-Z0-9_.-]+)\.md$/);
4221
+ const matchAppend = name.match(/^([a-zA-Z0-9_.-]+)\.append\.md$/);
4222
+ // append-pattern wins when both match (file is `.append.md`).
4223
+ const m = matchAppend || matchOverride;
4224
+ if (!m) continue;
4225
+ const isAppend = !!matchAppend;
4226
+ const key = m[1];
4227
+ const full = path.join(dir, name);
4228
+ let content = '';
4229
+ try { content = fs.readFileSync(full, 'utf8'); } catch { /* skip */ }
4230
+ if (!entries[key]) entries[key] = { override: '', append: '' };
4231
+ if (isAppend) entries[key].append = content;
4232
+ else entries[key].override = content;
4233
+ }
4234
+ }
4235
+ } catch (error: any) {
4236
+ return { success: false, error: error?.message || String(error) };
4237
+ }
4238
+ return { success: true, dir, entries };
4239
+ }
4240
+
4241
+ case 'write_coordinator_prompt': {
4242
+ const fs = await import('node:fs');
4243
+ const path = await import('node:path');
4244
+ const os = await import('node:os');
4245
+ const key = typeof args?.key === 'string' ? args.key.trim() : '';
4246
+ const kind = args?.kind === 'append' ? 'append' : 'override';
4247
+ const content = typeof args?.content === 'string' ? args.content : '';
4248
+ // Whitelist key chars so a malicious caller can't write
4249
+ // ../../etc/passwd. Same charset readUserPromptFile accepts.
4250
+ if (!key || !/^[a-zA-Z0-9_.-]+$/.test(key)) {
4251
+ return { success: false, error: 'key must match [a-zA-Z0-9_.-]+' };
4252
+ }
4253
+ const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
4254
+ const filename = kind === 'append' ? `${key}.append.md` : `${key}.md`;
4255
+ const full = path.join(dir, filename);
4256
+ try {
4257
+ fs.mkdirSync(dir, { recursive: true });
4258
+ if (content.trim()) {
4259
+ fs.writeFileSync(full, content, { encoding: 'utf8', mode: 0o600 });
4260
+ } else if (fs.existsSync(full)) {
4261
+ // Empty content = "reset to default" — delete the file
4262
+ // so the daemon's readUserPromptFile path falls through.
4263
+ fs.unlinkSync(full);
4264
+ }
4265
+ return { success: true, path: full, kind, key };
4266
+ } catch (error: any) {
4267
+ return { success: false, error: error?.message || String(error) };
4268
+ }
4269
+ }
4270
+
1975
4271
  case 'mark_session_seen': {
1976
4272
  const sessionId = args?.sessionId;
1977
4273
  if (!sessionId || typeof sessionId !== 'string') {
@@ -2150,46 +4446,305 @@ export class DaemonCommandRouter {
2150
4446
  }
2151
4447
  }
2152
4448
 
2153
- case 'get_mesh': {
4449
+ case 'get_mesh': {
4450
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4451
+ if (!meshId) return { success: false, error: 'meshId required' };
4452
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
4453
+ if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
4454
+
4455
+ const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
4456
+ const directTruth = await hydrateInlineMeshDirectTruth({
4457
+ mesh: meshRecord.mesh,
4458
+ meshSource: meshRecord.source,
4459
+ dispatchMeshCommand: this.deps.dispatchMeshCommand,
4460
+ statusInstanceId: this.deps.statusInstanceId,
4461
+ localMachineId: loadConfig().machineId || '',
4462
+ });
4463
+ const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
4464
+ const sourceOfTruth = {
4465
+ membership: meshRecord.source === 'inline_cache'
4466
+ ? 'coordinator_inline_mesh_cache'
4467
+ : meshRecord.source === 'local_config'
4468
+ ? 'local_mesh_config'
4469
+ : 'inline_bootstrap_snapshot',
4470
+ coordinatorOwnsLiveTruth: directTruthSatisfied,
4471
+ directPeerTruth: {
4472
+ required: requireDirectPeerTruth,
4473
+ satisfied: directTruthSatisfied,
4474
+ directEvidenceCount: directTruth.directEvidenceCount,
4475
+ localConfirmedCount: directTruth.localConfirmedCount,
4476
+ peerAttemptedCount: directTruth.peerAttemptedCount,
4477
+ peerConfirmedCount: directTruth.peerConfirmedCount,
4478
+ unavailableNodeIds: directTruth.unavailableNodeIds,
4479
+ },
4480
+ };
4481
+ if (requireDirectPeerTruth && !directTruthSatisfied) {
4482
+ return {
4483
+ success: false,
4484
+ code: 'mesh_direct_peer_truth_unavailable',
4485
+ error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
4486
+ sourceOfTruth,
4487
+ };
4488
+ }
4489
+ return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
4490
+ }
4491
+
4492
+ case 'create_mesh': {
4493
+ const name = typeof args?.name === 'string' ? args.name.trim() : '';
4494
+ const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
4495
+ const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
4496
+ const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
4497
+ if (!name) return { success: false, error: 'name required' };
4498
+ try {
4499
+ const { createMesh } = await import('../config/mesh-config.js');
4500
+ const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
4501
+ ? args.meshHost
4502
+ : undefined;
4503
+ const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
4504
+ return { success: true, mesh };
4505
+ } catch (e: any) {
4506
+ return { success: false, error: e.message };
4507
+ }
4508
+ }
4509
+
4510
+ case 'update_mesh': {
4511
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4512
+ if (!meshId) return { success: false, error: 'meshId required' };
4513
+ try {
4514
+ const { updateMesh } = await import('../config/mesh-config.js');
4515
+ const patch: Record<string, unknown> = {};
4516
+ if (typeof args?.name === 'string') patch.name = args.name;
4517
+ if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
4518
+ if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
4519
+ if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
4520
+ if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
4521
+ if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
4522
+ const mesh = updateMesh(meshId, patch as any);
4523
+ if (!mesh) return { success: false, error: 'Mesh not found' };
4524
+ this.inlineMeshCache.set(meshId, mesh);
4525
+ this.invalidateAggregateMeshStatus(meshId);
4526
+ return { success: true, mesh };
4527
+ } catch (e: any) {
4528
+ return { success: false, error: e.message };
4529
+ }
4530
+ }
4531
+
4532
+ case 'get_mesh_host_pairing': {
4533
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4534
+ if (!meshId) return { success: false, error: 'meshId required' };
4535
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
4536
+ const mesh = meshRecord?.mesh;
4537
+ if (!mesh) return { success: false, error: 'Mesh not found' };
4538
+ const meshHost = resolveMeshHostStatus(mesh);
4539
+ const pairingStatus = meshHost.pairing?.status || 'not_configured';
4540
+ return {
4541
+ success: true,
4542
+ code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
4543
+ meshId,
4544
+ hostAddress: meshHost.hostAddress,
4545
+ meshHost,
4546
+ manualPairing: {
4547
+ status: pairingStatus,
4548
+ joinImplemented: true,
4549
+ protocol: 'standalone_command_direct_v1',
4550
+ 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.',
4551
+ },
4552
+ };
4553
+ }
4554
+
4555
+ case 'configure_mesh_host_pairing': {
4556
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4557
+ const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
4558
+ const token = typeof args?.token === 'string' ? args.token.trim() : '';
4559
+ if (!meshId) return { success: false, error: 'meshId required' };
4560
+ if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
4561
+ try {
4562
+ const { configureMeshHostPairing } = await import('../config/mesh-config.js');
4563
+ const configured = configureMeshHostPairing(meshId, { hostAddress, token });
4564
+ if (!configured) return { success: false, error: 'Mesh not found' };
4565
+ this.inlineMeshCache.set(meshId, configured.mesh);
4566
+ const meshHost = resolveMeshHostStatus(configured.mesh);
4567
+ return {
4568
+ success: true,
4569
+ code: 'mesh_host_pairing_pending',
4570
+ meshId,
4571
+ hostAddress: configured.hostAddress,
4572
+ meshHost,
4573
+ manualPairing: {
4574
+ status: meshHost.pairing?.status || 'pairing',
4575
+ joinImplemented: true,
4576
+ protocol: 'standalone_command_direct_v1',
4577
+ 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.',
4578
+ },
4579
+ };
4580
+ } catch (e: any) {
4581
+ return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
4582
+ }
4583
+ }
4584
+
4585
+ case 'create_mesh_host_pairing_token': {
4586
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4587
+ if (!meshId) return { success: false, error: 'meshId required' };
4588
+ try {
4589
+ const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
4590
+ const created = createMeshHostPairingToken(meshId, {
4591
+ token: typeof args?.token === 'string' ? args.token : undefined,
4592
+ expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
4593
+ });
4594
+ if (!created) return { success: false, error: 'Mesh not found' };
4595
+ this.inlineMeshCache.set(meshId, created.mesh);
4596
+ this.invalidateAggregateMeshStatus(meshId);
4597
+ return {
4598
+ success: true,
4599
+ code: 'mesh_host_pairing_token_created',
4600
+ meshId,
4601
+ token: created.token,
4602
+ tokenId: created.tokenId,
4603
+ expiresAt: created.expiresAt,
4604
+ meshHost: resolveMeshHostStatus(created.mesh),
4605
+ warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
4606
+ };
4607
+ } catch (e: any) {
4608
+ return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
4609
+ }
4610
+ }
4611
+
4612
+ case 'apply_mesh_host_join': {
2154
4613
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4614
+ const token = typeof args?.token === 'string' ? args.token.trim() : '';
4615
+ const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
4616
+ ? args.memberNode
4617
+ : null;
2155
4618
  if (!meshId) return { success: false, error: 'meshId required' };
2156
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
2157
- if (meshRecord?.mesh) return { success: true, mesh: meshRecord.mesh };
2158
- return { success: false, error: 'Mesh not found' };
2159
- }
2160
-
2161
- case 'create_mesh': {
2162
- const name = typeof args?.name === 'string' ? args.name.trim() : '';
2163
- const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
2164
- const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
2165
- const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
2166
- if (!name) return { success: false, error: 'name required' };
4619
+ if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
2167
4620
  try {
2168
- const { createMesh } = await import('../config/mesh-config.js');
2169
- const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy });
2170
- return { success: true, mesh };
4621
+ const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
4622
+ const applied = applyMeshHostJoinRequest(meshId, {
4623
+ token,
4624
+ memberNode: memberNode as any,
4625
+ memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
4626
+ });
4627
+ if (!applied) return { success: false, error: 'Mesh not found' };
4628
+ if (!applied.accepted) {
4629
+ return {
4630
+ success: false,
4631
+ code: 'mesh_host_join_rejected',
4632
+ meshId,
4633
+ tokenId: applied.tokenId,
4634
+ meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
4635
+ error: applied.reason,
4636
+ };
4637
+ }
4638
+ this.inlineMeshCache.set(meshId, applied.mesh);
4639
+ this.invalidateAggregateMeshStatus(meshId);
4640
+ try {
4641
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
4642
+ appendLedgerEntry(meshId, {
4643
+ kind: 'node_joined',
4644
+ nodeId: applied.node.id,
4645
+ payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
4646
+ });
4647
+ } catch { /* ledger append is best-effort */ }
4648
+ return {
4649
+ success: true,
4650
+ code: 'mesh_host_join_accepted',
4651
+ meshId,
4652
+ node: applied.node,
4653
+ tokenId: applied.tokenId,
4654
+ meshHost: resolveMeshHostStatus(applied.mesh),
4655
+ };
2171
4656
  } catch (e: any) {
2172
- return { success: false, error: e.message };
4657
+ return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
2173
4658
  }
2174
4659
  }
2175
4660
 
2176
- case 'update_mesh': {
4661
+ case 'join_mesh_host_pairing': {
2177
4662
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4663
+ const token = typeof args?.token === 'string' ? args.token.trim() : '';
2178
4664
  if (!meshId) return { success: false, error: 'meshId required' };
4665
+ if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
4666
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
4667
+ const mesh = meshRecord?.mesh;
4668
+ if (!mesh) return { success: false, error: 'Mesh not found' };
4669
+ const meshHost = resolveMeshHostStatus(mesh);
4670
+ if (meshHost.role !== 'member') {
4671
+ 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.' };
4672
+ }
2179
4673
  try {
2180
- const { updateMesh } = await import('../config/mesh-config.js');
2181
- const patch: Record<string, unknown> = {};
2182
- if (typeof args?.name === 'string') patch.name = args.name;
2183
- if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
2184
- if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
2185
- if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
2186
- if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
2187
- const mesh = updateMesh(meshId, patch as any);
2188
- if (!mesh) return { success: false, error: 'Mesh not found' };
2189
- this.inlineMeshCache.set(meshId, mesh);
2190
- return { success: true, mesh };
4674
+ const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
4675
+ const tokenId = tokenIdForManualPairing(token);
4676
+ if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
4677
+ return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
4678
+ }
4679
+ const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
4680
+ if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
4681
+ const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
4682
+ const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
4683
+ ? args.hostDaemonId.trim()
4684
+ : meshHost.hostDaemonId;
4685
+ let hostResult: any;
4686
+ let transport: string;
4687
+ if (hostDaemonId && this.deps.dispatchMeshCommand) {
4688
+ transport = 'mesh_command_dispatch';
4689
+ hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
4690
+ meshId: hostMeshId,
4691
+ token,
4692
+ memberMeshId: meshId,
4693
+ memberNode,
4694
+ });
4695
+ } else if (meshHost.hostAddress) {
4696
+ transport = 'standalone_http_command';
4697
+ const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
4698
+ const response = await fetch(commandUrl, {
4699
+ method: 'POST',
4700
+ headers: { 'Content-Type': 'application/json' },
4701
+ body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
4702
+ });
4703
+ hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
4704
+ if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
4705
+ } else {
4706
+ return {
4707
+ success: false,
4708
+ code: 'mesh_host_join_transport_unavailable',
4709
+ meshId,
4710
+ meshHost,
4711
+ error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
4712
+ };
4713
+ }
4714
+ if (!hostResult?.success) {
4715
+ return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
4716
+ }
4717
+ const joined = meshRecord.inline
4718
+ ? null
4719
+ : markMeshHostPairingJoined(meshId, {
4720
+ tokenId: hostResult.tokenId || tokenId,
4721
+ hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
4722
+ hostNodeId: hostResult.meshHost?.hostNodeId,
4723
+ joinedAt: hostResult.meshHost?.pairing?.joinedAt,
4724
+ });
4725
+ if (joined) {
4726
+ this.inlineMeshCache.set(meshId, joined.mesh);
4727
+ this.invalidateAggregateMeshStatus(meshId);
4728
+ }
4729
+ return {
4730
+ success: true,
4731
+ code: 'mesh_host_join_applied',
4732
+ meshId,
4733
+ hostMeshId,
4734
+ transport,
4735
+ node: hostResult.node,
4736
+ tokenId: hostResult.tokenId || tokenId,
4737
+ meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
4738
+ hostResult,
4739
+ manualPairing: {
4740
+ status: 'paired',
4741
+ joinImplemented: true,
4742
+ protocol: 'standalone_command_direct_v1',
4743
+ description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
4744
+ },
4745
+ };
2191
4746
  } catch (e: any) {
2192
- return { success: false, error: e.message };
4747
+ return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
2193
4748
  }
2194
4749
  }
2195
4750
 
@@ -2286,6 +4841,8 @@ export class DaemonCommandRouter {
2286
4841
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2287
4842
  const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
2288
4843
  if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
4844
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
4845
+ if (ownerFailure) return ownerFailure;
2289
4846
  try {
2290
4847
  const { cancelTask } = await import('../mesh/mesh-work-queue.js');
2291
4848
  const reason = typeof args?.reason === 'string' ? args.reason : undefined;
@@ -2301,6 +4858,8 @@ export class DaemonCommandRouter {
2301
4858
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2302
4859
  const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
2303
4860
  if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
4861
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
4862
+ if (ownerFailure) return ownerFailure;
2304
4863
  try {
2305
4864
  const { requeueTask } = await import('../mesh/mesh-work-queue.js');
2306
4865
  const task = requeueTask(meshId, taskId, {
@@ -2322,6 +4881,8 @@ export class DaemonCommandRouter {
2322
4881
  const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
2323
4882
  if (!meshId) return { success: false, error: 'meshId required' };
2324
4883
  if (!workspace) return { success: false, error: 'workspace required' };
4884
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
4885
+ if (ownerFailure) return ownerFailure;
2325
4886
  try {
2326
4887
  const { addNode } = await import('../config/mesh-config.js');
2327
4888
  const providerPriority = Array.isArray(args?.providerPriority)
@@ -2332,8 +4893,26 @@ export class DaemonCommandRouter {
2332
4893
  ...(readOnly ? { readOnly: true } : {}),
2333
4894
  ...(providerPriority.length ? { providerPriority } : {}),
2334
4895
  };
2335
- const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}) });
4896
+ const role = normalizeMeshDaemonRole(args?.role);
4897
+ const daemonId = typeof args?.daemonId === 'string' && args.daemonId.trim() ? args.daemonId.trim() : undefined;
4898
+ const machineId = typeof args?.machineId === 'string' && args.machineId.trim() ? args.machineId.trim() : undefined;
4899
+ const repoRoot = typeof args?.repoRoot === 'string' && args.repoRoot.trim() ? args.repoRoot.trim() : undefined;
4900
+ const node = addNode(meshId, {
4901
+ workspace,
4902
+ ...(repoRoot ? { repoRoot } : {}),
4903
+ ...(daemonId ? { daemonId } : {}),
4904
+ ...(machineId ? { machineId } : {}),
4905
+ ...(policy ? { policy } : {}),
4906
+ ...(role ? { role } : {}),
4907
+ });
2336
4908
  if (!node) return { success: false, error: 'Mesh not found' };
4909
+ // mesh_status hands back a coordinator-memory aggregate
4910
+ // snapshot keyed on (meshId, queueRevision). Adding a
4911
+ // node touches neither, so without an explicit cache
4912
+ // bust the dashboard graph keeps rendering the pre-add
4913
+ // node list (empty for a fresh mesh) even after the
4914
+ // user clicks Refresh.
4915
+ this.invalidateAggregateMeshStatus(meshId);
2337
4916
  return { success: true, node };
2338
4917
  } catch (e: any) {
2339
4918
  return { success: false, error: e.message };
@@ -2344,6 +4923,8 @@ export class DaemonCommandRouter {
2344
4923
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2345
4924
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
2346
4925
  if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
4926
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
4927
+ if (ownerFailure) return ownerFailure;
2347
4928
  try {
2348
4929
  const { updateNode } = await import('../config/mesh-config.js');
2349
4930
  const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
@@ -2360,8 +4941,20 @@ export class DaemonCommandRouter {
2360
4941
  delete (policy as any).providerPriority;
2361
4942
  }
2362
4943
  }
2363
- const node = updateNode(meshId, nodeId, { policy: policy as any });
4944
+ const patch: Record<string, unknown> = { policy: policy as any };
4945
+ if (typeof args?.systemPrompt === 'string') {
4946
+ const trimmed = (args.systemPrompt as string).trim();
4947
+ patch.systemPrompt = trimmed || undefined;
4948
+ } else if (args?.systemPrompt === null) {
4949
+ patch.systemPrompt = undefined;
4950
+ }
4951
+ const node = updateNode(meshId, nodeId, patch as any);
2364
4952
  if (!node) return { success: false, error: 'Mesh node not found' };
4953
+ // Provider priority / systemPrompt changes don't touch
4954
+ // the queue revision, so without a manual bust the
4955
+ // cached aggregate keeps surfacing pre-update values
4956
+ // (priority chip, coordinator prompt preview, etc.).
4957
+ this.invalidateAggregateMeshStatus(meshId);
2365
4958
  return { success: true, node };
2366
4959
  } catch (e: any) {
2367
4960
  return { success: false, error: e.message };
@@ -2372,6 +4965,8 @@ export class DaemonCommandRouter {
2372
4965
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2373
4966
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
2374
4967
  if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
4968
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
4969
+ if (ownerFailure) return ownerFailure;
2375
4970
  try {
2376
4971
  const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
2377
4972
  const mesh = meshRecord?.mesh;
@@ -2397,131 +4992,97 @@ export class DaemonCommandRouter {
2397
4992
  }
2398
4993
  }
2399
4994
 
2400
- case 'refine_mesh_node': {
4995
+ case 'get_mesh_refine_config_schema': {
4996
+ return {
4997
+ success: true,
4998
+ schema: MESH_REFINE_CONFIG_SCHEMA,
4999
+ locations: MESH_REFINE_CONFIG_LOCATIONS,
5000
+ worktreeBootstrap: {
5001
+ schema: MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
5002
+ locations: MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
5003
+ sourceOfTruth: 'repo worktree bootstrap config',
5004
+ runBehavior: 'When present and enabled, clone_mesh_node runs commands after submodule initialization and records status on the worktree node.',
5005
+ },
5006
+ sourceOfTruth: 'repo mesh/refine config',
5007
+ heuristicRole: 'suggestions_only_not_execution_path',
5008
+ };
5009
+ }
5010
+
5011
+ case 'validate_mesh_refine_config': {
5012
+ const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
5013
+ const mesh = args?.inlineMesh || {};
5014
+ const loaded = args?.config !== undefined
5015
+ ? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
5016
+ : loadMeshRefineConfig(mesh, workspace);
5017
+ const validation = loaded.config
5018
+ ? validateMeshRefineConfig(loaded.config, loaded.source)
5019
+ : { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
5020
+ return { success: validation.valid, ...loaded, ...validation };
5021
+ }
5022
+
5023
+ case 'suggest_mesh_refine_config': {
5024
+ const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
5025
+ const mesh = args?.inlineMesh || {};
5026
+ return {
5027
+ success: true,
5028
+ ...suggestMeshRefineConfig(mesh, workspace),
5029
+ note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
5030
+ };
5031
+ }
5032
+
5033
+ case 'plan_mesh_refine_node': {
2401
5034
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2402
5035
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
2403
5036
  if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
2404
- try {
5037
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
5038
+ const mesh = meshRecord?.mesh;
5039
+ const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
5040
+ if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
5041
+ return {
5042
+ success: true,
5043
+ dryRun: true,
5044
+ nodeId,
5045
+ workspace: node.workspace,
5046
+ validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
5047
+ mergeWillRun: false,
5048
+ cleanupWillRun: false,
5049
+ };
5050
+ }
5051
+
5052
+ case 'fast_forward_mesh_node': {
5053
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
5054
+ const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
5055
+ let workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
5056
+ let submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
5057
+ ? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
5058
+ : undefined;
5059
+ if (!workspace && meshId && nodeId) {
2405
5060
  const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
2406
5061
  const mesh = meshRecord?.mesh;
2407
5062
  const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
2408
- if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
2409
-
2410
- if (!node.isLocalWorktree || !node.workspace) {
2411
- return { success: false, error: `Refinery requires a local worktree node` };
2412
- }
2413
-
2414
- const sourceNode = node.clonedFromNodeId
2415
- ? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
2416
- : mesh?.nodes.find((n: any) => !n.isLocalWorktree);
2417
- const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
2418
- if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
2419
-
2420
- const { execFile } = await import('node:child_process');
2421
- const { promisify } = await import('node:util');
2422
- const execFileAsync = promisify(execFile);
2423
-
2424
- const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
2425
- const branch = branchStdout.trim();
2426
- if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
2427
-
2428
- const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
2429
- const baseBranch = baseBranchStdout.trim();
2430
-
2431
- const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
2432
- if (validationSummary.status === 'failed') {
2433
- return {
2434
- success: false,
2435
- code: 'validation_failed',
2436
- convergenceStatus: 'blocked_review',
2437
- error: 'Refinery validation gate failed; 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: 'failed',
2447
- status: 'blocked_review',
2448
- },
2449
- };
2450
- }
2451
- if (validationSummary.status === 'skipped') {
2452
- return {
2453
- success: false,
2454
- code: 'validation_unavailable',
2455
- convergenceStatus: 'blocked_review',
2456
- error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
2457
- branch,
2458
- into: baseBranch,
2459
- validationSummary,
2460
- finalBranchConvergenceState: {
2461
- branch,
2462
- baseBranch,
2463
- merged: false,
2464
- removed: false,
2465
- validation: 'unavailable',
2466
- status: 'blocked_review',
2467
- },
2468
- };
2469
- }
2470
-
2471
- try {
2472
- await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
2473
- } catch (e: any) {
2474
- return {
2475
- success: false,
2476
- error: `Merge failed (conflicts?): ${e.message}`,
2477
- validationSummary,
2478
- finalBranchConvergenceState: {
2479
- branch,
2480
- baseBranch,
2481
- merged: false,
2482
- removed: false,
2483
- validation: 'passed',
2484
- status: 'not_mergeable',
2485
- },
2486
- };
5063
+ workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
5064
+ if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
5065
+ submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
2487
5066
  }
2488
-
2489
- const removeResult = await this.execute('remove_mesh_node', {
2490
- meshId,
2491
- nodeId,
2492
- sessionCleanupMode: 'kill',
2493
- inlineMesh: args?.inlineMesh,
2494
- });
2495
-
2496
- try {
2497
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
2498
- appendLedgerEntry(meshId, {
2499
- kind: 'node_removed',
2500
- nodeId,
2501
- payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
2502
- });
2503
- } catch {}
2504
-
2505
- return {
2506
- success: true,
2507
- merged: true,
2508
- branch,
2509
- into: baseBranch,
2510
- removeResult,
2511
- validationSummary,
2512
- finalBranchConvergenceState: {
2513
- branch: baseBranch,
2514
- mergedBranch: branch,
2515
- baseBranch,
2516
- merged: true,
2517
- removed: removeResult?.success !== false,
2518
- validation: 'passed',
2519
- status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
2520
- },
2521
- };
2522
- } catch (e: any) {
2523
- return { success: false, error: e.message };
2524
5067
  }
5068
+ const result = await (fastForwardMeshNode({
5069
+ meshId: meshId || undefined,
5070
+ nodeId: nodeId || undefined,
5071
+ workspace,
5072
+ branch: typeof args?.branch === 'string' ? args.branch : undefined,
5073
+ execute: args?.execute === true,
5074
+ dryRun: args?.dryRun === true,
5075
+ updateSubmodules: args?.updateSubmodules === true,
5076
+ submoduleIgnorePaths,
5077
+ }) as Promise<unknown>);
5078
+ return result as CommandRouterResult;
5079
+ }
5080
+
5081
+ case 'refine_mesh_node': {
5082
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
5083
+ const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
5084
+ if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
5085
+ return this.startMeshRefineJob(meshId, nodeId, args);
2525
5086
  }
2526
5087
 
2527
5088
  case 'remove_mesh_node': {
@@ -2562,9 +5123,15 @@ export class DaemonCommandRouter {
2562
5123
  let removed = false;
2563
5124
  if (meshRecord?.inline) {
2564
5125
  removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
5126
+ // Inline meshes share the same aggregate snapshot cache as
5127
+ // local-config meshes; without this bust the removed node
5128
+ // keeps showing up in the dashboard graph until the cache
5129
+ // ages out on its own.
5130
+ if (removed) this.invalidateAggregateMeshStatus(meshId);
2565
5131
  } else {
2566
5132
  const { removeNode } = await import('../config/mesh-config.js');
2567
5133
  removed = removeNode(meshId, nodeId);
5134
+ if (removed) this.invalidateAggregateMeshStatus(meshId);
2568
5135
  }
2569
5136
 
2570
5137
  // Record in task ledger
@@ -2602,6 +5169,8 @@ export class DaemonCommandRouter {
2602
5169
  if (!meshId) return { success: false, error: 'meshId required' };
2603
5170
  if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
2604
5171
  if (!branch) return { success: false, error: 'branch required' };
5172
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
5173
+ if (ownerFailure) return ownerFailure;
2605
5174
 
2606
5175
  try {
2607
5176
  const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
@@ -2650,39 +5219,124 @@ export class DaemonCommandRouter {
2650
5219
  policy: { ...(sourceNode.policy || {}) },
2651
5220
  });
2652
5221
  if (!node) return { success: false, error: 'Failed to register worktree node' };
5222
+ this.invalidateAggregateMeshStatus(meshId);
2653
5223
  }
2654
5224
 
2655
- // Initialize submodules if policy allows (default: true)
2656
- const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
2657
- if (initSubmodules) {
5225
+ const persistWorktreeSetupState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
5226
+ node.worktreeBootstrap = bootstrapState;
5227
+ if (meshRecord.inline) {
5228
+ this.updateInlineMeshNode(meshId, mesh, node);
5229
+ return;
5230
+ }
2658
5231
  try {
2659
- const { runGit } = await import('../git/git-executor.js');
2660
- await runGit(
2661
- { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
2662
- ['submodule', 'update', '--init', '--recursive'],
2663
- { timeoutMs: 120000 },
2664
- );
2665
- } catch (subErr: any) {
2666
- // Submodule init is best-effort; don't fail the clone
2667
- console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
5232
+ const { updateNode } = await import('../config/mesh-config.js');
5233
+ updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
5234
+ this.invalidateAggregateMeshStatus(meshId);
5235
+ } catch { /* bootstrap status persistence is best-effort */ }
5236
+ };
5237
+
5238
+ const appendCloneLedger = async (initSubmodules: boolean, bootstrapState: WorktreeBootstrapState): Promise<void> => {
5239
+ try {
5240
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
5241
+ appendLedgerEntry(meshId, {
5242
+ kind: 'node_cloned',
5243
+ nodeId: node.id,
5244
+ payload: {
5245
+ sourceNodeId,
5246
+ branch: result.branch,
5247
+ worktreePath: result.worktreePath,
5248
+ submodulesInitialized: initSubmodules,
5249
+ worktreeBootstrap: {
5250
+ status: bootstrapState.status,
5251
+ required: bootstrapState.required,
5252
+ configSource: bootstrapState.configSource,
5253
+ configSourceType: bootstrapState.configSourceType,
5254
+ lastCommand: bootstrapState.lastCommand,
5255
+ exitCode: bootstrapState.exitCode,
5256
+ },
5257
+ },
5258
+ });
5259
+ } catch { /* ledger append is best-effort */ }
5260
+ };
5261
+
5262
+ const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
5263
+ const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, result.worktreePath);
5264
+ const runningBootstrapState: WorktreeBootstrapState = {
5265
+ status: 'running',
5266
+ required: loadedBootstrap.config?.required !== false,
5267
+ configSource: loadedBootstrap.path || loadedBootstrap.source,
5268
+ configSourceType: loadedBootstrap.sourceType,
5269
+ startedAt: new Date().toISOString(),
5270
+ };
5271
+ await persistWorktreeSetupState(runningBootstrapState);
5272
+
5273
+ const finishWorktreeSetup = async (): Promise<{ submodulesInitialized: boolean; bootstrapState: WorktreeBootstrapState }> => {
5274
+ let submodulesInitialized = false;
5275
+ if (initSubmodules) {
5276
+ try {
5277
+ const { runGit } = await import('../git/git-executor.js');
5278
+ await runGit(
5279
+ { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
5280
+ ['submodule', 'update', '--init', '--recursive'],
5281
+ { timeoutMs: 120000 },
5282
+ );
5283
+ submodulesInitialized = true;
5284
+ } catch (subErr: any) {
5285
+ // Submodule init is best-effort; don't fail the clone
5286
+ console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
5287
+ }
2668
5288
  }
2669
- }
5289
+ const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
5290
+ await persistWorktreeSetupState(bootstrapState);
5291
+ await appendCloneLedger(submodulesInitialized, bootstrapState);
5292
+ return { submodulesInitialized, bootstrapState };
5293
+ };
2670
5294
 
2671
- // Record in task ledger
2672
- try {
2673
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
2674
- appendLedgerEntry(meshId, {
2675
- kind: 'node_cloned',
2676
- nodeId: node.id,
2677
- payload: { sourceNodeId, branch: result.branch, worktreePath: result.worktreePath, submodulesInitialized: initSubmodules },
5295
+ const requestedSetupWaitMs = Number(args?.setupWaitMs ?? args?.bootstrapWaitMs ?? 8000);
5296
+ const setupWaitMs = Number.isFinite(requestedSetupWaitMs)
5297
+ ? Math.min(Math.max(requestedSetupWaitMs, 0), 14000)
5298
+ : 8000;
5299
+ const setupPromise = finishWorktreeSetup();
5300
+ const setupResult = await Promise.race([
5301
+ setupPromise.then((value) => ({ completed: true as const, value })),
5302
+ new Promise<{ completed: false }>((resolve) => setTimeout(() => resolve({ completed: false }), setupWaitMs)),
5303
+ ]);
5304
+
5305
+ if (!setupResult.completed) {
5306
+ setupPromise.catch((error: any) => {
5307
+ const failedState: WorktreeBootstrapState = {
5308
+ ...runningBootstrapState,
5309
+ status: 'failed',
5310
+ completedAt: new Date().toISOString(),
5311
+ error: error?.message || String(error),
5312
+ };
5313
+ void persistWorktreeSetupState(failedState);
5314
+ void appendCloneLedger(false, failedState);
2678
5315
  });
2679
- } catch { /* ledger append is best-effort */ }
5316
+ return {
5317
+ success: true,
5318
+ async: true,
5319
+ status: 'accepted',
5320
+ node,
5321
+ worktreePath: result.worktreePath,
5322
+ branch: result.branch,
5323
+ worktreeBootstrap: runningBootstrapState,
5324
+ worktreeSetup: {
5325
+ status: 'running',
5326
+ setupWaitMs,
5327
+ message: 'Worktree node is registered; submodule/bootstrap setup is continuing in the background.',
5328
+ },
5329
+ };
5330
+ }
2680
5331
 
5332
+ const { submodulesInitialized, bootstrapState } = setupResult.value;
2681
5333
  return {
2682
5334
  success: true,
2683
5335
  node,
2684
5336
  worktreePath: result.worktreePath,
2685
5337
  branch: result.branch,
5338
+ submodulesInitialized,
5339
+ worktreeBootstrap: bootstrapState,
2686
5340
  };
2687
5341
  } catch (e: any) {
2688
5342
  return { success: false, error: e.message };
@@ -2691,12 +5345,12 @@ export class DaemonCommandRouter {
2691
5345
  case 'trigger_mesh_queue': {
2692
5346
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2693
5347
  if (!meshId) return { success: false, error: 'meshId required' };
5348
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
5349
+ if (ownerFailure) return ownerFailure;
2694
5350
  try {
2695
5351
  const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
2696
- if (meshId) {
2697
- triggerMeshQueue(this.deps as any, meshId);
2698
- }
2699
- return { success: true };
5352
+ const trigger = await triggerMeshQueue(this.deps as any, meshId);
5353
+ return { success: true, trigger };
2700
5354
  } catch (e: any) {
2701
5355
  return { success: false, error: e.message };
2702
5356
  }
@@ -2706,6 +5360,19 @@ export class DaemonCommandRouter {
2706
5360
  case 'launch_mesh_coordinator': {
2707
5361
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2708
5362
  let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
5363
+ // Optional per-launch system-prompt addition. Dashboard or API
5364
+ // callers (e.g. when spawning a mesh-node-specific coordinator)
5365
+ // can pass extra context that gets appended to the rendered
5366
+ // default prompt under the "## Additional Context" section.
5367
+ // Going through buildCoordinatorSystemPrompt's userInstruction
5368
+ // means user-level override files (~/.adhdev/coordinator-prompts)
5369
+ // and this per-launch addition compose cleanly: an override
5370
+ // wins outright, but if there's no override, the default
5371
+ // prompt + the optional append.md file + this extra context
5372
+ // all stack in declared order.
5373
+ const extraSystemPrompt = typeof args?.extraSystemPrompt === 'string'
5374
+ ? args.extraSystemPrompt.trim()
5375
+ : '';
2709
5376
  if (!meshId) return { success: false, error: 'meshId required' };
2710
5377
 
2711
5378
  try {
@@ -2722,6 +5389,15 @@ export class DaemonCommandRouter {
2722
5389
  mesh = getMesh(meshId);
2723
5390
  }
2724
5391
  if (!mesh) return { success: false, error: 'Mesh not found' };
5392
+ const meshHost = resolveMeshHostStatus(mesh);
5393
+ if (!meshHost.canOwnCoordinator) {
5394
+ return {
5395
+ success: false,
5396
+ ...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
5397
+ meshId,
5398
+ cliType,
5399
+ };
5400
+ }
2725
5401
  if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
2726
5402
 
2727
5403
  const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
@@ -2807,7 +5483,7 @@ export class DaemonCommandRouter {
2807
5483
  // Build coordinator prompt first — fail closed on errors.
2808
5484
  let cliCmdSystemPrompt = '';
2809
5485
  try {
2810
- cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
5486
+ cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined });
2811
5487
  } catch (error: any) {
2812
5488
  const message = error?.message || String(error);
2813
5489
  LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
@@ -2819,55 +5495,126 @@ export class DaemonCommandRouter {
2819
5495
  };
2820
5496
  }
2821
5497
 
2822
- // Run the provider's MCP registration command.
5498
+ // Run the provider's MCP registration command under a
5499
+ // PTY. Some providers (agy, future bubbletea CLIs)
5500
+ // refuse to run without /dev/tty, so pipe-only
5501
+ // execFileSync silently no-ops the registration and
5502
+ // the coordinator ends up without any mcp tools. With
5503
+ // a real PTY the registration goes through and the
5504
+ // exit code tells us whether it actually persisted.
5505
+ let mcpRegistrationOk = false;
5506
+ let mcpRegistrationFailure: {
5507
+ command: string;
5508
+ output: string;
5509
+ exitCode: number | null;
5510
+ signal: number | null;
5511
+ timedOut: boolean;
5512
+ } | null = null;
2823
5513
  try {
2824
- const { execFileSync: execCmdSync } = await import('node:child_process');
2825
- const cmdParts = coordinatorSetup.command.trim().split(/\s+/);
2826
- const [regCmd, ...regArgs] = cmdParts;
2827
- LOG.info('MeshCoordinator', `Running MCP registration: ${coordinatorSetup.command}`);
2828
- execCmdSync(regCmd, regArgs, { stdio: 'pipe', timeout: 15_000 });
5514
+ const { buildMeshCoordinatorRegistrationPlan, execUnderPty } = await import('./mesh-coordinator.js');
5515
+ const registrationPlan = buildMeshCoordinatorRegistrationPlan(
5516
+ cliType,
5517
+ coordinatorSetup.serverName,
5518
+ coordinatorSetup.command,
5519
+ );
5520
+ for (const step of registrationPlan) {
5521
+ const renderedCommand = [step.command, ...step.args].join(' ');
5522
+ LOG.info('MeshCoordinator', `Running MCP ${step.label} (pty): ${renderedCommand}`);
5523
+ const ptyResult = await execUnderPty(step.command, step.args, { cwd: workspace, timeoutMs: 20_000 });
5524
+ if (ptyResult.exitCode === 0 && !ptyResult.timedOut) {
5525
+ if (step.required) mcpRegistrationOk = true;
5526
+ continue;
5527
+ }
5528
+ LOG.warn('MeshCoordinator', `MCP ${step.label} failed exit=${ptyResult.exitCode} signal=${ptyResult.signal} timedOut=${ptyResult.timedOut} — output:\n${ptyResult.output.slice(-2000)}`);
5529
+ if (step.required) {
5530
+ mcpRegistrationFailure = {
5531
+ command: renderedCommand,
5532
+ output: ptyResult.output.slice(-2000),
5533
+ exitCode: ptyResult.exitCode,
5534
+ signal: ptyResult.signal,
5535
+ timedOut: ptyResult.timedOut,
5536
+ };
5537
+ break;
5538
+ }
5539
+ }
2829
5540
  } catch (error: any) {
2830
- // Non-fatal server may already be registered (providers return exit 1 on duplicate).
2831
- LOG.warn('MeshCoordinator', `MCP registration command failed (may be pre-registered): ${error?.message || error}`);
5541
+ LOG.warn('MeshCoordinator', `MCP registration command failed: ${error?.message || error}`);
5542
+ mcpRegistrationFailure = {
5543
+ command: coordinatorSetup.command,
5544
+ output: error?.message || String(error),
5545
+ exitCode: null,
5546
+ signal: null,
5547
+ timedOut: false,
5548
+ };
2832
5549
  }
2833
5550
 
2834
- // Inject system prompt using provider-native methods.
2835
- // Codex: -c 'instructions="..."' CLI config override
2836
- // Gemini: write GEMINI.md to workspace (auto-loaded as context)
2837
- const cliCmdArgs: string[] = [];
2838
- const cliCmdEnv: Record<string, string> = {};
2839
- if (cliCmdSystemPrompt) {
2840
- if (cliType === 'codex-cli') {
2841
- // Codex reads `developer_instructions` from config.toml as system instructions.
2842
- // The -c flag overrides a config key for this session only.
2843
- cliCmdArgs.push('-c', `developer_instructions=${JSON.stringify(cliCmdSystemPrompt)}`);
2844
- } else if (cliType === 'gemini-cli') {
2845
- // Gemini CLI auto-loads GEMINI.md from CWD as project context.
2846
- // Write a temporary GEMINI.md to the workspace before launch.
5551
+ if (!mcpRegistrationOk) {
5552
+ return {
5553
+ success: false,
5554
+ code: 'mesh_coordinator_mcp_registration_failed',
5555
+ error: `Could not register ${coordinatorSetup.serverName}; coordinator session was not launched`,
5556
+ meshId,
5557
+ cliType,
5558
+ workspace,
5559
+ registration: mcpRegistrationFailure,
5560
+ };
5561
+ }
5562
+
5563
+ // Codex gives repo-local .mcp.json precedence over its
5564
+ // global `codex mcp add` registration. Refresh an
5565
+ // existing ADHDev entry so a stale workspace command
5566
+ // cannot shadow the registration we just verified.
5567
+ if (cliType === 'codex-cli') {
5568
+ const repoMcpConfigPath = pathJoin(workspace, '.mcp.json');
5569
+ if (fs.existsSync(repoMcpConfigPath)) {
2847
5570
  try {
2848
- const { writeFileSync: wfs, existsSync: efs, readFileSync: rfs } = await import('node:fs');
2849
- const geminiMdPath = `${workspace}/GEMINI.md`;
2850
- const marker = '<!-- adhdev-mesh-coordinator-prompt -->';
2851
- const markerEnd = '<!-- /adhdev-mesh-coordinator-prompt -->';
2852
- const block = `${marker}\n${cliCmdSystemPrompt}\n${markerEnd}`;
2853
- if (efs(geminiMdPath)) {
2854
- const existing = rfs(geminiMdPath, 'utf-8');
2855
- // Replace existing block or append
2856
- const replaced = existing.replace(
2857
- new RegExp(`${marker}[\\s\\S]*?${markerEnd}`, 'g'),
2858
- block,
2859
- );
2860
- wfs(geminiMdPath, replaced.includes(marker) ? replaced : `${existing}\n\n${block}`);
2861
- } else {
2862
- wfs(geminiMdPath, block);
5571
+ const repoMcpConfig = parseMeshCoordinatorMcpConfig(
5572
+ fs.readFileSync(repoMcpConfigPath, 'utf-8'),
5573
+ 'claude_mcp_json',
5574
+ );
5575
+ const existingServers = repoMcpConfig.mcpServers;
5576
+ if (
5577
+ existingServers
5578
+ && typeof existingServers === 'object'
5579
+ && !Array.isArray(existingServers)
5580
+ && existingServers[coordinatorSetup.serverName]
5581
+ ) {
5582
+ fs.writeFileSync(repoMcpConfigPath, serializeMeshCoordinatorMcpConfig({
5583
+ ...repoMcpConfig,
5584
+ mcpServers: {
5585
+ ...existingServers,
5586
+ [coordinatorSetup.serverName]: coordinatorSetup.mcpServer,
5587
+ },
5588
+ }, 'claude_mcp_json'), 'utf-8');
5589
+ LOG.info('MeshCoordinator', `Refreshed repo-local ${repoMcpConfigPath} entry for ${coordinatorSetup.serverName}`);
2863
5590
  }
2864
- LOG.info('MeshCoordinator', `Wrote coordinator prompt to ${workspace}/GEMINI.md`);
2865
- } catch (e: any) {
2866
- LOG.warn('MeshCoordinator', `Could not write GEMINI.md: ${e?.message || e}`);
5591
+ } catch (error: any) {
5592
+ return {
5593
+ success: false,
5594
+ code: 'mesh_coordinator_config_write_failed',
5595
+ error: `Could not refresh repo-local MCP config: ${error?.message || error}`,
5596
+ meshId,
5597
+ cliType,
5598
+ workspace,
5599
+ };
2867
5600
  }
2868
5601
  }
2869
5602
  }
2870
5603
 
5604
+ // Inject system prompt declaratively from provider.v1.json.
5605
+ const cliCmdArgs: string[] = [];
5606
+ const cliCmdEnv: Record<string, string> = {};
5607
+ let cliCmdContextFilePath: string | undefined;
5608
+ if (cliCmdSystemPrompt) {
5609
+ const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
5610
+ const effect = applyMeshCoordinatorSystemPromptInjection(
5611
+ cliCmdSystemPrompt,
5612
+ providerMeta?.meshCoordinator?.systemPromptInjection,
5613
+ { cliArgs: cliCmdArgs, launchEnv: cliCmdEnv, workspace, cliType },
5614
+ );
5615
+ cliCmdContextFilePath = effect.contextFilePath;
5616
+ }
5617
+
2871
5618
  const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
2872
5619
  cliType,
2873
5620
  dir: workspace,
@@ -2876,16 +5623,52 @@ export class DaemonCommandRouter {
2876
5623
  settings: { meshCoordinatorFor: meshId },
2877
5624
  });
2878
5625
 
5626
+ // R48 inject-then-remove. Spawn was just kicked off above; agy and
5627
+ // gemini-cli read AGENTS.md / GEMINI.md exactly once at startup and
5628
+ // cache it for the rest of the session, so we can safely strip
5629
+ // the wrapper from disk shortly after launch. That keeps any
5630
+ // worker session launched into the same workspace later from
5631
+ // picking up our wrapper block.
5632
+ if (cliCmdLaunch?.success && cliCmdContextFilePath) {
5633
+ const stripPath = cliCmdContextFilePath;
5634
+ setTimeout(() => {
5635
+ void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
5636
+ stripCoordinatorWrapperFile(stripPath);
5637
+ LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (cli_command)`);
5638
+ }).catch(() => { /* best-effort */ });
5639
+ }, 5000);
5640
+ }
5641
+
2879
5642
  if (!cliCmdLaunch?.success) {
2880
5643
  return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
2881
5644
  }
2882
5645
 
2883
5646
  LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
5647
+ const cliCmdSessionId = cliCmdLaunch.sessionId || cliCmdLaunch.id;
5648
+ if (cliCmdSessionId) {
5649
+ const cliCmdInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
5650
+ registerMeshCoordinator({
5651
+ meshId,
5652
+ sessionId: cliCmdSessionId,
5653
+ workspace,
5654
+ startedAt: Date.now(),
5655
+ cliType,
5656
+ systemPrompt: cliCmdSystemPrompt || undefined,
5657
+ extraSystemPrompt: extraSystemPrompt || undefined,
5658
+ injection: cliCmdInjectionDecl ? {
5659
+ mode: cliCmdInjectionDecl.mode,
5660
+ target: 'flag' in cliCmdInjectionDecl ? cliCmdInjectionDecl.flag
5661
+ : 'name' in cliCmdInjectionDecl ? cliCmdInjectionDecl.name
5662
+ : 'path' in cliCmdInjectionDecl ? cliCmdInjectionDecl.path
5663
+ : undefined,
5664
+ } : undefined,
5665
+ });
5666
+ }
2884
5667
  try {
2885
5668
  const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
2886
5669
  appendLedgerEntry(meshId, {
2887
5670
  kind: 'coordinator_started',
2888
- sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
5671
+ sessionId: cliCmdSessionId,
2889
5672
  providerType: cliType,
2890
5673
  payload: { workspace },
2891
5674
  });
@@ -2896,8 +5679,8 @@ export class DaemonCommandRouter {
2896
5679
  meshId,
2897
5680
  cliType,
2898
5681
  workspace,
2899
- sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
2900
- mcpRegistered: true,
5682
+ sessionId: cliCmdSessionId,
5683
+ mcpRegistered: mcpRegistrationOk,
2901
5684
  };
2902
5685
  }
2903
5686
 
@@ -2918,7 +5701,7 @@ export class DaemonCommandRouter {
2918
5701
  // broken mesh state is visible instead of silently launching with weaker rules.
2919
5702
  let systemPrompt = '';
2920
5703
  try {
2921
- systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
5704
+ systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined });
2922
5705
  } catch (error: any) {
2923
5706
  const message = error?.message || String(error);
2924
5707
  LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
@@ -3033,12 +5816,15 @@ export class DaemonCommandRouter {
3033
5816
  launchEnv.HERMES_HOME = dirname(mcpConfigPath);
3034
5817
  launchEnv.HERMES_IGNORE_USER_CONFIG = '';
3035
5818
  }
5819
+ let autoImportContextFilePath: string | undefined;
3036
5820
  if (systemPrompt) {
3037
- if (configFormat === 'hermes_config_yaml') {
3038
- launchEnv.HERMES_EPHEMERAL_SYSTEM_PROMPT = systemPrompt;
3039
- } else {
3040
- cliArgs.push('--append-system-prompt', systemPrompt);
3041
- }
5821
+ const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
5822
+ const effect = applyMeshCoordinatorSystemPromptInjection(
5823
+ systemPrompt,
5824
+ providerMeta?.meshCoordinator?.systemPromptInjection,
5825
+ { cliArgs, launchEnv, workspace, cliType },
5826
+ );
5827
+ autoImportContextFilePath = effect.contextFilePath;
3042
5828
  }
3043
5829
  if (cliType === 'claude-cli') {
3044
5830
  cliArgs.push('--mcp-config', coordinatorSetup.configPath);
@@ -3057,18 +5843,53 @@ export class DaemonCommandRouter {
3057
5843
  }
3058
5844
  });
3059
5845
 
5846
+ // R48 inject-then-remove. See the cli_command branch for context;
5847
+ // same idea: strip the wrapper from disk ~5s after launch so the
5848
+ // user's AGENTS.md / GEMINI.md is untouched the moment any
5849
+ // worker session opens up in the same workspace.
5850
+ if (launchResult?.success && autoImportContextFilePath) {
5851
+ const stripPath = autoImportContextFilePath;
5852
+ setTimeout(() => {
5853
+ void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
5854
+ stripCoordinatorWrapperFile(stripPath);
5855
+ LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (auto_import)`);
5856
+ }).catch(() => { /* best-effort */ });
5857
+ }, 5000);
5858
+ }
5859
+
3060
5860
  if (!launchResult?.success) {
3061
5861
  return { success: false, error: launchResult?.error || 'Failed to launch CLI session' };
3062
5862
  }
3063
5863
 
3064
5864
  LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
5865
+ const launchSessionId = launchResult.sessionId || launchResult.id;
5866
+ if (launchSessionId) {
5867
+ const autoImportInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
5868
+ registerMeshCoordinator({
5869
+ meshId,
5870
+ sessionId: launchSessionId,
5871
+ workspace,
5872
+ startedAt: Date.now(),
5873
+ cliType,
5874
+ systemPrompt: systemPrompt || undefined,
5875
+ extraSystemPrompt: extraSystemPrompt || undefined,
5876
+ mcpConfigPath,
5877
+ injection: autoImportInjectionDecl ? {
5878
+ mode: autoImportInjectionDecl.mode,
5879
+ target: 'flag' in autoImportInjectionDecl ? autoImportInjectionDecl.flag
5880
+ : 'name' in autoImportInjectionDecl ? autoImportInjectionDecl.name
5881
+ : 'path' in autoImportInjectionDecl ? autoImportInjectionDecl.path
5882
+ : undefined,
5883
+ } : undefined,
5884
+ });
5885
+ }
3065
5886
 
3066
5887
  // Record coordinator launch in task ledger
3067
5888
  try {
3068
5889
  const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
3069
5890
  appendLedgerEntry(meshId, {
3070
5891
  kind: 'coordinator_started',
3071
- sessionId: launchResult.sessionId || launchResult.id,
5892
+ sessionId: launchSessionId,
3072
5893
  providerType: cliType,
3073
5894
  payload: { workspace },
3074
5895
  });
@@ -3079,7 +5900,7 @@ export class DaemonCommandRouter {
3079
5900
  meshId,
3080
5901
  cliType,
3081
5902
  workspace,
3082
- sessionId: launchResult.sessionId || launchResult.id,
5903
+ sessionId: launchSessionId,
3083
5904
  mcpConfigWritten: true,
3084
5905
  };
3085
5906
  } catch (e: any) {
@@ -3095,6 +5916,36 @@ export class DaemonCommandRouter {
3095
5916
  const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
3096
5917
  const mesh = meshRecord?.mesh;
3097
5918
  if (!mesh) return { success: false, error: 'Mesh not found' };
5919
+ const meshHost = resolveMeshHostStatus(mesh);
5920
+
5921
+ const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
5922
+ // See (B3) below: scope the peek to this daemon when the
5923
+ // caller doesn't tell us, otherwise scoped events look
5924
+ // missing and we falsely return a stale cache.
5925
+ const peekScope = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
5926
+ ? args.coordinatorDaemonId.trim()
5927
+ : (this.deps.statusInstanceId || undefined);
5928
+ const pendingCoordinatorEventCount = getPendingMeshCoordinatorEvents(meshId, peekScope).length;
5929
+ const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
5930
+ if (!refreshRequested && pendingCoordinatorEventCount === 0) {
5931
+ const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
5932
+ if (cachedStatus) {
5933
+ logRepoMeshStatusDebug('return_cached', {
5934
+ meshId,
5935
+ command: 'mesh_status',
5936
+ refreshRequested,
5937
+ summary: summarizeRepoMeshStatusDebug(cachedStatus),
5938
+ });
5939
+ return cachedStatus;
5940
+ }
5941
+ }
5942
+ const refreshReason = refreshRequested
5943
+ ? 'explicit_refresh'
5944
+ : pendingCoordinatorEventCount > 0
5945
+ ? 'pending_coordinator_events'
5946
+ : hadAggregateCache
5947
+ ? 'stale_pending_cache_refresh'
5948
+ : 'cold_cache_miss';
3098
5949
 
3099
5950
  const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
3100
5951
  const queue = getQueue(meshId);
@@ -3102,6 +5953,7 @@ export class DaemonCommandRouter {
3102
5953
 
3103
5954
  const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
3104
5955
  const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
5956
+ const asyncRefineLedgerEntries = readLedgerEntries(meshId, { tail: 100 });
3105
5957
  const ledgerSummary = getLedgerSummary(meshId);
3106
5958
  const sessionHostRecords = this.deps.sessionHostControl?.listSessions
3107
5959
  ? await this.deps.sessionHostControl.listSessions().catch(() => [])
@@ -3109,6 +5961,74 @@ export class DaemonCommandRouter {
3109
5961
  const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
3110
5962
 
3111
5963
  const localMachineId = loadConfig().machineId || '';
5964
+ const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
5965
+ const directTruth = requireDirectPeerTruth
5966
+ ? await hydrateInlineMeshDirectTruth({
5967
+ mesh,
5968
+ meshSource: meshRecord.source,
5969
+ dispatchMeshCommand: this.deps.dispatchMeshCommand,
5970
+ statusInstanceId: this.deps.statusInstanceId,
5971
+ localMachineId,
5972
+ })
5973
+ : {
5974
+ directEvidenceCount: 0,
5975
+ localConfirmedCount: 0,
5976
+ peerAttemptedCount: 0,
5977
+ peerConfirmedCount: 0,
5978
+ unavailableNodeIds: [] as string[],
5979
+ };
5980
+ // Default/cached loads may not attempt a remote peer probe yet; do not surface that as
5981
+ // a direct mesh truth failure until an explicit probe attempt actually fails.
5982
+ const passivePeerTruthNotAttempted = requireDirectPeerTruth
5983
+ && !refreshRequested
5984
+ && directTruth.directEvidenceCount > 0
5985
+ && directTruth.peerAttemptedCount === 0;
5986
+ const effectiveDirectTruth = passivePeerTruthNotAttempted
5987
+ ? { ...directTruth, unavailableNodeIds: [] as string[] }
5988
+ : directTruth;
5989
+ const unavailableDirectTruthNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
5990
+ const unavailableNodesAreOnlyRemovedWorktrees = unavailableDirectTruthNodeIds.size > 0
5991
+ && Array.isArray(mesh.nodes)
5992
+ && mesh.nodes
5993
+ .filter((node: any) => unavailableDirectTruthNodeIds.has(String(node.id || node.nodeId || '')))
5994
+ .every((node: any) => node?.isLocalWorktree === true);
5995
+ const directTruthSatisfied = !requireDirectPeerTruth
5996
+ || (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
5997
+ if (requireDirectPeerTruth && !directTruthSatisfied) {
5998
+ const failureResult = {
5999
+ success: false,
6000
+ code: 'mesh_direct_peer_truth_unavailable',
6001
+ error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
6002
+ sourceOfTruth: {
6003
+ membership: meshRecord.source === 'inline_cache'
6004
+ ? 'coordinator_inline_mesh_cache'
6005
+ : meshRecord.source === 'local_config'
6006
+ ? 'local_mesh_config'
6007
+ : 'inline_bootstrap_snapshot',
6008
+ coordinatorOwnsLiveTruth: false,
6009
+ currentStatus: 'direct_peer_truth_unavailable',
6010
+ directPeerTruth: {
6011
+ required: true,
6012
+ satisfied: false,
6013
+ directEvidenceCount: directTruth.directEvidenceCount,
6014
+ localConfirmedCount: directTruth.localConfirmedCount,
6015
+ peerAttemptedCount: directTruth.peerAttemptedCount,
6016
+ peerConfirmedCount: directTruth.peerConfirmedCount,
6017
+ unavailableNodeIds: directTruth.unavailableNodeIds,
6018
+ },
6019
+ },
6020
+ };
6021
+ logRepoMeshStatusDebug('direct_truth_unavailable', {
6022
+ meshId,
6023
+ command: 'mesh_status',
6024
+ refreshRequested,
6025
+ meshSource: meshRecord.source,
6026
+ directTruth,
6027
+ });
6028
+ return failureResult;
6029
+ }
6030
+ const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
6031
+ const coordinatorHostname = osHostname();
3112
6032
  const selectedCoordinatorNodeId = readStringValue(
3113
6033
  mesh.coordinator?.preferredNodeId,
3114
6034
  (mesh.nodes?.[0] as any)?.id,
@@ -3122,21 +6042,42 @@ export class DaemonCommandRouter {
3122
6042
  for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
3123
6043
  const nodeId = String(node.id || node.nodeId || '');
3124
6044
  const daemonId = readStringValue(node.daemonId);
6045
+ const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
6046
+ const nodeHostname = readMeshNodeHostname(node as Record<string, unknown>);
3125
6047
  const providerPriority = readProviderPriorityFromPolicy(node.policy);
6048
+ const configuredCoordinatorNode = Boolean(
6049
+ nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
6050
+ );
6051
+ const sparseConfiguredCoordinatorNode = configuredCoordinatorNode
6052
+ && !daemonId
6053
+ && !nodeMachineId
6054
+ && !nodeHostname;
3126
6055
  const isSelfNode = Boolean(
3127
6056
  nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
3128
6057
  ) || Boolean(
3129
6058
  daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
3130
- ) || Boolean(meshRecord?.inline && nodeIndex === 0);
6059
+ ) || Boolean(meshRecord?.inline && nodeIndex === 0)
6060
+ || sparseConfiguredCoordinatorNode;
6061
+ const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
6062
+ localMachineId,
6063
+ localDaemonId: this.deps.statusInstanceId,
6064
+ coordinatorHostname,
6065
+ isSelfNode,
6066
+ });
3131
6067
  const status: Record<string, unknown> = {
3132
6068
  nodeId,
3133
- machineLabel: node.machineLabel || node.id || node.nodeId,
6069
+ machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
6070
+ labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
6071
+ ? 'explicit_metadata'
6072
+ : 'workspace_host_provider_context',
3134
6073
  workspace: node.workspace,
3135
6074
  repoRoot: node.repoRoot,
3136
6075
  isLocalWorktree: node.isLocalWorktree,
3137
6076
  worktreeBranch: node.worktreeBranch,
6077
+ role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
3138
6078
  daemonId,
3139
- machineId: node.machineId,
6079
+ machineId: nodeMachineId || node.machineId,
6080
+ machine: machineIdentity,
3140
6081
  machineStatus: node.machineStatus,
3141
6082
  health: 'unknown',
3142
6083
  providers: node.providers || [],
@@ -3203,52 +6144,224 @@ export class DaemonCommandRouter {
3203
6144
  }
3204
6145
  }
3205
6146
  if (workspace) {
3206
- if (!fs.existsSync(workspace) && applyCachedInlineMeshNodeStatus(status, node)) {
3207
- status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
3208
- nodeStatuses.push(status);
3209
- continue;
3210
- }
3211
- try {
3212
- const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
3213
- status.git = gitStatus;
3214
- if (gitStatus.isGitRepo) {
3215
- status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
3216
- } else {
3217
- status.health = 'degraded';
3218
- if (gitStatus.error && !status.error) status.error = gitStatus.error;
6147
+ if (!fs.existsSync(workspace)) {
6148
+ // Workspace not local prefer direct live inline truth, then attempt a P2P git probe.
6149
+ const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
6150
+ let remoteProbeApplied = false;
6151
+ if (inlineTransitGit) {
6152
+ status.git = inlineTransitGit;
6153
+ status.health = inlineTransitGit.isGitRepo
6154
+ ? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
6155
+ : 'degraded';
6156
+ const connection = readObjectRecord(status.connection);
6157
+ const connectionState = readStringValue(connection.state);
6158
+ const connectionReported = readBooleanValue(connection.reported) ?? false;
6159
+ if (!connectionReported || connectionState === 'unknown') {
6160
+ status.connection = buildLivePeerGitConnection(connection, refreshedAt);
6161
+ }
6162
+ remoteProbeApplied = true;
6163
+ } else if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
6164
+ try {
6165
+ const remoteGit = await probeRemoteMeshGitStatus({
6166
+ dispatchMeshCommand: this.deps.dispatchMeshCommand,
6167
+ daemonId,
6168
+ workspace,
6169
+ timeoutMs: 8000,
6170
+ });
6171
+ if (remoteGit) {
6172
+ status.git = remoteGit;
6173
+ status.health = remoteGit.isGitRepo
6174
+ ? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
6175
+ : 'degraded';
6176
+ const connection = readObjectRecord(status.connection);
6177
+ const connectionState = readStringValue(connection.state);
6178
+ const connectionReported = readBooleanValue(connection.reported) ?? false;
6179
+ if (!connectionReported || connectionState === 'unknown') {
6180
+ status.connection = buildLivePeerGitConnection(connection, refreshedAt);
6181
+ }
6182
+ recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
6183
+ remoteProbeApplied = true;
6184
+ }
6185
+ } catch {
6186
+ const refreshedConnection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
6187
+ const refreshedConnectionState = readStringValue(refreshedConnection?.state);
6188
+ if (refreshedConnection && refreshedConnectionState === 'connected') {
6189
+ status.connection = refreshedConnection;
6190
+ try {
6191
+ const remoteGit = await probeRemoteMeshGitStatus({
6192
+ dispatchMeshCommand: this.deps.dispatchMeshCommand,
6193
+ daemonId,
6194
+ workspace,
6195
+ timeoutMs: 12000,
6196
+ });
6197
+ if (remoteGit) {
6198
+ status.git = remoteGit;
6199
+ status.health = remoteGit.isGitRepo
6200
+ ? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
6201
+ : 'degraded';
6202
+ const connection = readObjectRecord(status.connection);
6203
+ const connectionState = readStringValue(connection.state);
6204
+ const connectionReported = readBooleanValue(connection.reported) ?? false;
6205
+ if (!connectionReported || connectionState === 'unknown') {
6206
+ status.connection = buildLivePeerGitConnection(connection, refreshedAt);
6207
+ }
6208
+ recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
6209
+ remoteProbeApplied = true;
6210
+ }
6211
+ } catch {
6212
+ // Probe timed out again or P2P unavailable — fall back to cached status
6213
+ }
6214
+ }
6215
+ }
6216
+ }
6217
+ if (!remoteProbeApplied) {
6218
+ const connectionState = readStringValue((status.connection as any)?.state);
6219
+ const pendingPeerGitProbe = !inlineTransitGit
6220
+ && !isSelfNode
6221
+ && !!daemonId
6222
+ && (
6223
+ readStringValue(status.machineStatus) === 'online'
6224
+ || readStringValue(status.health) === 'online'
6225
+ || connectionState === 'connecting'
6226
+ || connectionState === 'connected'
6227
+ || connectionState === 'unknown'
6228
+ );
6229
+ if (pendingPeerGitProbe) {
6230
+ status.gitProbePending = true;
6231
+ status.health = 'unknown';
6232
+ }
6233
+ if (applyCachedInlineMeshNodeStatus(
6234
+ status,
6235
+ node,
6236
+ pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
6237
+ )) {
6238
+ applyInlineMeshBranchConvergence(mesh, node, status);
6239
+ finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
6240
+ nodeStatuses.push(status);
6241
+ continue;
6242
+ }
6243
+ if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
6244
+ applyInlineMeshBranchConvergence(mesh, node, status);
6245
+ finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
6246
+ nodeStatuses.push(status);
6247
+ continue;
6248
+ }
3219
6249
  }
3220
- } catch {
3221
- if (!applyCachedInlineMeshNodeStatus(status, node)) {
3222
- status.health = 'degraded';
6250
+ } else {
6251
+ try {
6252
+ const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
6253
+ status.git = gitStatus;
6254
+ recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
6255
+ if (gitStatus.isGitRepo) {
6256
+ status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
6257
+ } else {
6258
+ status.health = 'degraded';
6259
+ if (gitStatus.error && !status.error) status.error = gitStatus.error;
6260
+ }
6261
+ } catch {
6262
+ if (!applyCachedInlineMeshNodeStatus(status, node)) {
6263
+ status.health = 'degraded';
6264
+ }
3223
6265
  }
3224
6266
  }
3225
6267
  } else {
3226
6268
  applyCachedInlineMeshNodeStatus(status, node);
3227
6269
  }
3228
- status.launchReady = !!daemonId && (readStringValue(status.machineStatus) === 'online' || isSelfNode);
6270
+ applyInlineMeshBranchConvergence(mesh, node, status);
6271
+ finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
3229
6272
  nodeStatuses.push(status);
3230
6273
  }
3231
6274
 
3232
- return {
6275
+ // (B3) Resolve the coordinator daemon scope for the drain.
6276
+ // Emit-time always tags events with the worker's
6277
+ // targetCoordinatorDaemonId and writes them to the
6278
+ // scoped pending-events file. If the caller (MCP tool,
6279
+ // dashboard, etc.) doesn't tell us which coordinator
6280
+ // they're running under, fall back to *this daemon's*
6281
+ // ID — the caller reached us over our IPC/WS, so they
6282
+ // are by definition under this daemon. Falling back to
6283
+ // undefined would silently miss every scoped event.
6284
+ const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
6285
+ ? args.coordinatorDaemonId.trim()
6286
+ : (this.deps.statusInstanceId || undefined);
6287
+ const pendingCoordinatorEvents = drainPendingMeshCoordinatorEvents(meshId, callerCoordinatorDaemonId);
6288
+ const previewFreshness = (() => {
6289
+ const localRepoRoot = nodeStatuses
6290
+ .map((node: any) => readStringValue(node?.git?.repoRoot, node?.repoRoot, node?.workspace))
6291
+ .find((candidate: string | undefined) => !!candidate && fs.existsSync(candidate));
6292
+ return localRepoRoot ? buildPreviewFreshness(localRepoRoot) : undefined;
6293
+ })();
6294
+ const asyncRefineJobs = buildMeshAsyncRefineJobs({
6295
+ meshId,
6296
+ ledgerEntries: asyncRefineLedgerEntries,
6297
+ pendingEvents: pendingCoordinatorEvents,
6298
+ });
6299
+ const historicalSessions = buildHistoricalMeshSessions({
6300
+ meshId,
6301
+ nodes: mesh.nodes || [],
6302
+ liveSessionRecords: liveMeshSessions,
6303
+ });
6304
+ const statusResult = {
3233
6305
  success: true,
3234
6306
  meshId: mesh.id,
3235
6307
  meshName: mesh.name,
3236
6308
  repoIdentity: mesh.repoIdentity,
3237
6309
  defaultBranch: mesh.defaultBranch,
3238
- refreshedAt: new Date().toISOString(),
6310
+ refreshedAt,
6311
+ meshHost,
3239
6312
  sourceOfTruth: {
3240
6313
  membership: meshRecord?.source === 'inline_cache'
3241
6314
  ? 'coordinator_inline_mesh_cache'
3242
6315
  : meshRecord?.source === 'local_config'
3243
6316
  ? 'local_mesh_config'
3244
6317
  : 'inline_bootstrap_snapshot',
3245
- coordinatorOwnsLiveTruth: meshRecord?.source !== 'inline_bootstrap',
3246
- historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary'],
6318
+ coordinatorOwnsLiveTruth: directTruthSatisfied,
6319
+ meshHost: {
6320
+ owner: 'mesh_host_daemon',
6321
+ localRole: meshHost.role,
6322
+ hostDaemonId: meshHost.hostDaemonId,
6323
+ hostNodeId: meshHost.hostNodeId,
6324
+ hostAddress: meshHost.hostAddress,
6325
+ },
6326
+ ...(requireDirectPeerTruth ? {
6327
+ currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
6328
+ directPeerTruth: {
6329
+ required: true,
6330
+ satisfied: directTruthSatisfied,
6331
+ directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
6332
+ localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
6333
+ peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
6334
+ peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
6335
+ unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
6336
+ partialNodeFailures: effectiveDirectTruth.unavailableNodeIds,
6337
+ },
6338
+ } : {}),
6339
+ historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary', 'historicalSessions'],
3247
6340
  },
6341
+ branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
6342
+ ...(previewFreshness ? { previewFreshness, deployFreshness: previewFreshness } : {}),
3248
6343
  nodes: nodeStatuses,
3249
6344
  queue: { tasks: queue, summary: queueSummary },
3250
6345
  ledger: { entries: ledgerEntries, summary: ledgerSummary },
6346
+ ...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
6347
+ ...(historicalSessions ? { historicalSessions } : {}),
6348
+ ...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
3251
6349
  };
6350
+ const { pendingCoordinatorEvents: _pendingCoordinatorEvents, ...cacheableStatusResult } = statusResult as any;
6351
+ const rememberedStatus = this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
6352
+ const returnedStatus = pendingCoordinatorEvents.length > 0
6353
+ ? { ...rememberedStatus, pendingCoordinatorEvents }
6354
+ : rememberedStatus;
6355
+ logRepoMeshStatusDebug('return_live', {
6356
+ meshId,
6357
+ command: 'mesh_status',
6358
+ refreshRequested,
6359
+ refreshReason,
6360
+ meshSource: meshRecord.source,
6361
+ directTruth,
6362
+ summary: summarizeRepoMeshStatusDebug(returnedStatus),
6363
+ });
6364
+ return returnedStatus;
3252
6365
  } catch (e: any) {
3253
6366
  return { success: false, error: e.message };
3254
6367
  }
@@ -3306,7 +6419,7 @@ export class DaemonCommandRouter {
3306
6419
 
3307
6420
  // 3. Kill OS process if requested
3308
6421
  if (killProcess) {
3309
- const running = isIdeRunning(ideType);
6422
+ const running = await isIdeRunning(ideType);
3310
6423
  if (running) {
3311
6424
  LOG.info('StopIDE', `Killing IDE process: ${ideType}`);
3312
6425
  const killed = await killIdeProcess(ideType);