@adhdev/daemon-core 0.9.82-rc.17 → 0.9.82-rc.170

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